diff --git a/pom.xml b/pom.xml index e42300aa..4414ea16 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ spring-ai-alibaba-bailian-example spring-ai-alibaba-evaluation-example spring-ai-alibaba-mem0-example - + diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/README.md b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/README.md new file mode 100644 index 00000000..c96a50eb --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/README.md @@ -0,0 +1,109 @@ +# Spring AI Alibaba SubAgent Personal Assistant Example + +本示例展示如何使用 Spring AI Alibaba 的 ReactAgent 框架构建一个多智能体监督者模式系统,通过主智能体协调多个子智能体,实现日历代理与邮件发送功能。 +## 功能特性 + +- **多智能体**: Subagent 使用 Tool Calling 模式被 Supervisor Agent 调用。 +- **人工介入**: 通过在 Supervisor Agent 配置 hooks 加入中断功能。 + +## 快速开始 + +### 前置条件 + +- Java 21+ +- Maven 3.6+ +- DashScope API Key + +### 运行步骤 + +1. **设置 API Key** + + ```bash + export DASHSCOPE_API_KEY=your-api-key + ``` + +2. **构建项目** + + ```bash + cd subagent-personal-assistant-example + mvn clean package -DskipTests + ``` + +3. **运行应用** + + ```bash + mvn spring-boot:run + ``` + +4. **访问API** + + ``` shell + curl --location 'http://127.0.0.1:8080/react/agent/supervisorAgent?query=Schedule%20a%20meeting%20with%20the%20design%20team%20next%20Tuesday%20at%202pm%20for%201%20hour%2C%20and%20send%20them%20an%20email%20reminder%20about%20reviewing%20the%20new%20mockups.&threadId=user-session-124&nodeId=_AGENT_HOOK_HITL' \ +--header 'Content-Type: application/json' \ +--header 'Accept: text/event-stream' \ +--data '' + ```` +## Agent说明 +* supervisor_agent: 监督者智能体,负责协调多个子智能体,实现日历代理与邮件发送功能。 +* calendar_agent: 日历日程助理,检查可用时间和安排日历事件。 +* email_agent: 创建邮件主题和邮件发送。 +## 工具说明 + +| 工具名称 | 功能 | 输入参数 | +|---------|-------------------|-----------------------| +| `get_user_email_tool` | 获取用户邮箱 | UserInfo | +| `get_current_date_time` | 获取当前日期和时间 | 无 | +| `get_available_time_slots` | 获取可用时间 slots |AvailableTimeInfo | +| `create_calendar_event` | 创建日历事件 | CalendarInfo | +| `send_email` | 发送邮件 | EmailInfo | + +## 示例对话 + +``` +用户: Schedule a meeting with the design team next Tuesday at 2pm for 1 hour, and send them an email reminder about reviewing the new mockups. + +Agent: +I'll help you schedule a meeting with the design team and send them an email reminder. Let me break this down into steps: + +1. First, I need to get the email addresses of the design team members +2. Then schedule the calendar event for next Tuesday at 2pm for 1 hour +3. Finally, send an email reminder about reviewing the new mockups + +Let me start by getting the design team members' information: + +================================= Tool Message ================================= + +id: call_e06f221fa93b4e8ea77e62d0 +name: get_user_email_tool +responseData: "Available user list for [{\"departmentName\":\"design(设计团队)\",\"email\":\"wangwu@agent.cn\",\"userName\":\"wangwu(王五)\"}]" +Now I have the design team member information. I'll schedule the meeting for next Tuesday at 2pm for 1 hour with Wang Wu (王五) from the design team: + +检测到中断,需要人工审批 +工具: calendar_agent +参数: {"input": "Schedule a meeting with wangwu@agent.cn next Tuesday at 2pm for 1 hour. Subject: Design Team Meeting - Mockup Review"} +描述: The AI is requesting to use the tool: calendar_agent. +Description: Calendar event pending approval +With the following arguments: {"input": "Schedule a meeting with wangwu@agent.cn next Tuesday at 2pm for 1 hour. Subject: Design Team Meeting - Mockup Review"} +Do you approve? +检测到中断,等待人工介入... node: _AGENT_HOOK_HITL +人工介入开始... +检测到中断,需要人工审批 +工具: email_agent +参数: {"input": "Send an email to wangwu@agent.cn with subject 'Reminder: Design Team Meeting - Mockup Review' and content 'Hi Wang Wu, This is a reminder about our upcoming meeting next Tuesday (February 17, 2026) at 2:00 PM to review the new mockups. Please come prepared with your feedback. Looking forward to our discussion!'"} +描述: The AI is requesting to use the tool: email_agent. +Description: Outbound email pending approval +With the following arguments: {"input": "Send an email to wangwu@agent.cn with subject 'Reminder: Design Team Meeting - Mockup Review' and content 'Hi Wang Wu, This is a reminder about our upcoming meeting next Tuesday (February 17, 2026) at 2:00 PM to review the new mockups. Please come prepared with your feedback. Looking forward to our discussion!'"} +Do you approve? +检测到中断,等待人工介入... node: _AGENT_HOOK_HITL +人工介入开始... +Email sent to wangwu@agent.cn - Subject: Reminder: Design Team Meeting - Mockup Review + body: Hi Wang Wu, This is a reminder about our upcoming meeting next Tuesday (February 17, 2026) at 2:00 PM to review the new mockups. Please come prepared with your feedback. Looking forward to our discussion! +``` +## 注意事项 +1. 当检测到人工接入时,需要带着nodeId重新发起请求。 +2. 当前模型使用的qwen3-max-2026-01-23,模型不同可能导致示例结果有所偏差。 + +## 相关链接 + +- [Spring AI Alibaba 文档](https://java2ai.com) +- [Spring AI Alibaba GitHub](https://github.com/alibaba/spring-ai-alibaba) \ No newline at end of file diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/pom.xml b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/pom.xml new file mode 100644 index 00000000..c78c3b99 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.alibaba.cloud.ai + spring-ai-alibaba-examples + 1.0.0 + ../../pom.xml + + + subagent-personal-assistant-example + Spring AI Alibaba SubAgent Personal Assistant Example + Multi Agent Personal Assistant + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-agent-framework + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/HITLHelper.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/HITLHelper.java new file mode 100644 index 00000000..ea7733d0 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/HITLHelper.java @@ -0,0 +1,94 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloud.alibaba.ai.example.agent; + +import com.alibaba.cloud.ai.graph.action.InterruptionMetadata; + +public class HITLHelper { + /** + * 批准所有工具调用 + */ + public static InterruptionMetadata approveAll(InterruptionMetadata interruptionMetadata) { + InterruptionMetadata.Builder builder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .description("Agree to tool execution.") + .build() + ); + }); + + return builder.build(); + } + + /** + * 拒绝所有工具调用 + */ + public static InterruptionMetadata rejectAll( + InterruptionMetadata interruptionMetadata, + String reason) { + InterruptionMetadata.Builder builder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED) + .description(reason) + .build() + ); + }); + + return builder.build(); + } + + /** + * 编辑特定工具的参数 + */ + public static InterruptionMetadata editTool( + InterruptionMetadata interruptionMetadata, + String toolName, + String newArguments) { + InterruptionMetadata.Builder builder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + if (toolFeedback.getName().equals(toolName)) { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .arguments(newArguments) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED) + .build() + ); + } else { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .build() + ); + } + }); + + return builder.build(); + } + +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/SubAgentPersonalAssistantApplication.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/SubAgentPersonalAssistantApplication.java new file mode 100644 index 00000000..dd797775 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/SubAgentPersonalAssistantApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloud.alibaba.ai.example.agent; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author wangjx + * @since 2026-02-13 + */ +@SpringBootApplication +public class SubAgentPersonalAssistantApplication { + public static void main(String[] args) { + SpringApplication.run(SubAgentPersonalAssistantApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/config/AgentConfig.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/config/AgentConfig.java new file mode 100644 index 00000000..1e68072b --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/config/AgentConfig.java @@ -0,0 +1,148 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.config; + +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.agent.AgentTool; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook; +import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.cloud.alibaba.ai.example.agent.tool.*; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * Configuration class for setting up AI agents used in the personal assistant application. + * This class defines and configures various agents including calendar scheduling, + * email composition, and a supervisor agent that coordinates between them. + * + * @author wangjx + * @since 2026-02-13 + */ +@Configuration +public class AgentConfig { + + + private final static String CALENDAR_AGENT_PROMPT = """ + You are a calendar scheduling assistant. + Parse natural language scheduling requests (e.g., 'next Tuesday at 2pm') + into proper ISO datetime formats. + Use get_available_time_slots to check availability when needed. + Use create_calendar_event to schedule events. + Always confirm what was scheduled in your final response. + """; + + private final static String EMAIL_AGENT_PROMPT = """ + You are an email assistant. + Compose professional emails based on natural language requests. + Extract recipient information and craft appropriate subject lines and body text. + Use send_email to send the message. + Always confirm what was sent in your final response. + """; + + private final static String SUPERVISOR_PROMPT = """ + You are a helpful personal assistant. + You can schedule calendar events and send emails. + Break down user requests into appropriate tool calls and coordinate the results. + When a request involves multiple actions, use multiple tools in sequence. + """; + + private final DashScopeChatModel dashScopeChatModel; + + public AgentConfig(DashScopeChatModel dashScopeChatModel) { + this.dashScopeChatModel = dashScopeChatModel; + } + + @Bean("supervisorAgent") + public ReactAgent reactAgent() { + // 配置检查点保存器(人工介入需要检查点来处理中断) + MemorySaver memorySaver = new MemorySaver(); + ToolCallback calendarAgent = AgentTool.getFunctionToolCallback(calendarAgent()); + ToolCallback emailAgent = AgentTool.getFunctionToolCallback(emailAgent()); + + + return ReactAgent.builder() + .name("supervisor_agent") + .model(dashScopeChatModel) + .systemPrompt(SUPERVISOR_PROMPT) + .hooks(createHumanInTheLoopHook()) + .tools(List.of(calendarAgent, emailAgent, new UserDataTool().toolCallback())) + .saver(memorySaver) + .build(); + } + + + public ReactAgent emailAgent() { + + String instruction = + """ + Send emails using natural language. + Use this when the user wants to send notifications, reminders, or any email + communication. Handles recipient extraction, subject generation, and email + composition. + Input: Natural language email request (e.g., 'send them a reminder about + the meeting') + """; + // 创建 Agent + return ReactAgent.builder() + .name("email_agent") + .model(dashScopeChatModel) + .tools(List.of(new SendEmailTool().toolCallback())) + .systemPrompt(EMAIL_AGENT_PROMPT) + .instruction(instruction) + .inputType(String.class) + .build(); + } + + @Bean("calendarAgent") + public ReactAgent calendarAgent() { + + String instruction = """ + Schedule calendar events using natural language. + Use this when the user wants to create, modify, or check calendar appointments. + Handles date/time parsing, availability checking, and event creation. + Input: Natural language scheduling request (e.g., 'meeting with design team + next Tuesday at 2pm') + """; + + // 创建 Agent + return ReactAgent.builder() + .name("calendar_agent") + .model(dashScopeChatModel) + .tools(List.of(new CreateCalendarEventTool().toolCallback(), new AvailableTimeSlotsTool().toolCallback(), new DateTimeTools().toolCallback())) + .systemPrompt(CALENDAR_AGENT_PROMPT) + .instruction(instruction) + .inputType(String.class) + .build(); + + } + + private HumanInTheLoopHook createHumanInTheLoopHook() { + // 创建人工介入Hook + return HumanInTheLoopHook.builder() + .approvalOn("calendar_agent", ToolConfig.builder() + .description("Calendar event pending approval") + .build()) + .approvalOn("email_agent", ToolConfig.builder() + .description("Outbound email pending approval") + .build()).build(); + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/controller/PersonalAssistantController.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/controller/PersonalAssistantController.java new file mode 100644 index 00000000..5dcd904a --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/controller/PersonalAssistantController.java @@ -0,0 +1,133 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.controller; + +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.action.InterruptionMetadata; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; +import com.alibaba.cloud.ai.graph.streaming.StreamingOutput; +import com.cloud.alibaba.ai.example.agent.HITLHelper; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.ToolResponseMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * REST controller for managing personal assistant functionalities. + * This controller handles requests related to the supervisor agent, + * including streaming responses and human-in-the-loop interventions. + * + * @author wangjx + * @since 2026-02-13 + */ +@RestController +public class PersonalAssistantController { + + private static final Map> TOOL_FEEDBACK_MAP = new ConcurrentHashMap<>(); + + @Autowired + @Qualifier("supervisorAgent") + private ReactAgent supervisorAgent; + + + /** + * Handles GET requests to the supervisor agent endpoint. + * Supports both regular streaming and human-in-the-loop interventions. + * + * @param query the user's query string + * @param threadId the session thread identifier + * @param nodeId the node identifier for human intervention + * @return a Flux stream of responses from the supervisor agent + * @throws GraphRunnerException if there's an error during graph execution + */ + @GetMapping(value = "/react/agent/supervisorAgent", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux supervisorAgentTest(String query, String threadId, String nodeId) throws GraphRunnerException { + RunnableConfig config; + if (nodeId != null && TOOL_FEEDBACK_MAP.containsKey(nodeId)) { + System.out.println("人工介入开始..."); + // Human intervention using checkpoint mechanism. + // You must provide a thread ID to associate execution with a session thread, + // so that conversations can be paused and resumed (required for human review). + InterruptionMetadata metadata = InterruptionMetadata.builder().toolFeedbacks(TOOL_FEEDBACK_MAP.get(nodeId)).build(); + InterruptionMetadata approvalMetadata = HITLHelper.approveAll(metadata); + // Resume execution using approval decision + config = RunnableConfig.builder() + .threadId(threadId) // Same thread ID + .addHumanFeedback(approvalMetadata) + .build(); + TOOL_FEEDBACK_MAP.remove(nodeId); + return supervisorAgent.stream(query, config) + .doOnNext(this::println); + } else { + + config = RunnableConfig.builder() + .threadId(threadId) + .build(); + } + return supervisorAgent.stream(query, config) + .doOnNext(this::println); + + } + + private void println(NodeOutput nodeOutput) { + if (nodeOutput instanceof StreamingOutput streamingOutput) { + String node = streamingOutput.node(); + Message message = streamingOutput.message(); + if (message == null) { + return; + } + if ("_AGENT_MODEL_".equals(node)) { + System.out.print(message.getText()); + } + if ("_AGENT_TOOL_".equals(node)) { + ToolResponseMessage responseMessage = (ToolResponseMessage) message; + List responses = responseMessage.getResponses(); + System.out.println("================================= Tool Message =================================\n"); + for (ToolResponseMessage.ToolResponse respons : responses) { + String string = respons.responseData(); + System.out.println("id: " + respons.id()); + System.out.println("name: " + respons.name()); + System.out.println("responseData: " + string); + + } + } + } else if (nodeOutput instanceof InterruptionMetadata interruptionMetadata) { + System.out.println("检测到中断,需要人工审批"); + List toolFeedbacks = + interruptionMetadata.toolFeedbacks(); + + for (InterruptionMetadata.ToolFeedback feedback : toolFeedbacks) { + System.out.println("工具: " + feedback.getName()); + System.out.println("参数: " + feedback.getArguments()); + System.out.println("描述: " + feedback.getDescription()); + } + String node = interruptionMetadata.node(); + System.out.println("检测到中断,等待人工介入... node: " + node); + TOOL_FEEDBACK_MAP.put(node, toolFeedbacks); + } + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/AvailableTimeInfo.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/AvailableTimeInfo.java new file mode 100644 index 00000000..6fb1a054 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/AvailableTimeInfo.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.model; + +import java.util.List; + +/** + * + * @author wangjx + * @since 2026-02-13 + * */ +public class AvailableTimeInfo { + /** + * 日期信息,表示可用时间的具体日期 + */ + private String date; + + /** + * 持续时间(分钟),表示该时间段的长度 + */ + private Integer durationMinutes; + + /** + * 参与者列表,包含所有参与此时间段的人员信息 + */ + private List attendees; + + public List getAttendees() { + return attendees; + } + + public void setAttendees(List attendees) { + this.attendees = attendees; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public Integer getDurationMinutes() { + return durationMinutes; + } + + public void setDurationMinutes(Integer durationMinutes) { + this.durationMinutes = durationMinutes; + } +} + diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/CalendarInfo.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/CalendarInfo.java new file mode 100644 index 00000000..dbc80fb6 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/CalendarInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.model; + +import java.util.List; + +/** + * + * @author wangjx + * @since 2026-02-13 + * */ +public class CalendarInfo { + /** + * 日历事件的标题 + */ + private String title; + + /** + * 日历事件的开始时间 + */ + private String startTime; + + /** + * 日历事件的结束时间 + */ + private String endTime; + + /** + * 日历事件的参与者列表 + */ + private List attendees; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public List getAttendees() { + return attendees; + } + + public void setAttendees(List attendees) { + this.attendees = attendees; + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/EmailInfo.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/EmailInfo.java new file mode 100644 index 00000000..c7f05806 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/EmailInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.model; + +import java.util.List; + +/** + * + * @author wangjx + * @since 2026-02-13 + * */ +public class EmailInfo { + /** + * 收件人邮箱地址列表 + */ + private List to; + + /** + * 邮件主题 + */ + private String subject; + + /** + * 邮件正文内容 + */ + private String body; + + public List getTo() { + return to; + } + + public void setTo(List to) { + this.to = to; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/UserInfo.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/UserInfo.java new file mode 100644 index 00000000..dc3196b8 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/model/UserInfo.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.model; + +/** + * + * @author wangjx + * @since 2026-02-13 + * */ +public class UserInfo { + /** + * 用户名称 + */ + private String userName; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 部门名称 + */ + private String departmentName; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getDepartmentName() { + return departmentName; + } + + public void setDepartmentName(String departmentName) { + this.departmentName = departmentName; + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/AvailableTimeSlotsTool.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/AvailableTimeSlotsTool.java new file mode 100644 index 00000000..d74223d4 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/AvailableTimeSlotsTool.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloud.alibaba.ai.example.agent.tool; + +import com.cloud.alibaba.ai.example.agent.model.AvailableTimeInfo; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * A tool that retrieves available time slots for scheduling meetings. + * This tool implements the BiFunction interface to process input parameters + * and return formatted time slot information. + * + * @author wangjx + * @since 2026-02-13 + */ +public class AvailableTimeSlotsTool implements BiFunction { + + + @Override + public String apply(AvailableTimeInfo args, ToolContext toolContext) { + // Parse input parameters + String date =args.getDate(); + + // Validate date format (simplified version) + if (!isValidIsoDate(date)) { + return "Error: Invalid ISO date format"; + } + + // Simulate querying available time slots + List timeSlots = List.of("09:00", "14:00", "16:00"); + return String.format("Available time slots for %s: %s", date, String.join(", ", timeSlots)); + } + + + private boolean isValidIsoDate(String date) { + // Simple validation of ISO 8601 date format + return date != null && date.matches("\\d{4}-\\d{2}-\\d{2}"); + } + + + public ToolCallback toolCallback() { + return FunctionToolCallback.builder("get_available_time_slots", this) + .description("get_available_time_slots") + .inputType(AvailableTimeInfo.class) + .build(); + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/CreateCalendarEventTool.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/CreateCalendarEventTool.java new file mode 100644 index 00000000..4c925ffe --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/CreateCalendarEventTool.java @@ -0,0 +1,67 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.tool; + +import com.cloud.alibaba.ai.example.agent.model.CalendarInfo; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * A tool for creating calendar events. + * This class implements BiFunction to process CalendarInfo and ToolContext, + * validating input data and simulating event creation. + * + * @author wangjx + * @since 2026-02-13 + */ +public class CreateCalendarEventTool implements BiFunction { + + + @Override + public String apply(CalendarInfo calendarInfo, ToolContext toolContext) { + // Parse parameters + String title = calendarInfo.getTitle(); + String startTime = calendarInfo.getStartTime(); + String endTime = calendarInfo.getEndTime(); + List attendees = calendarInfo.getAttendees(); + + // Validate time format (simplified version) + if (!isValidIsoDateTime(startTime) || !isValidIsoDateTime(endTime)) { + return "Error: Invalid ISO datetime format"; + } + + // Simulate event creation + return String.format("Event created: %s from %s to %s with %d attendees", + title, startTime, endTime, attendees==null ? 0:attendees.size()); + } + + + private boolean isValidIsoDateTime(String datetime) { + // Simple validation of ISO 8601 format (should use stricter validation in production) + return datetime != null && datetime.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"); + } + public ToolCallback toolCallback() { + return FunctionToolCallback.builder("create_calendar_event", this) + .description("create_calendar_event") + .inputType(CalendarInfo.class) + .build(); + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/DateTimeTools.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/DateTimeTools.java new file mode 100644 index 00000000..d7038fd0 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/DateTimeTools.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloud.alibaba.ai.example.agent.tool; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.function.BiFunction; + +/** + * Utility class for handling date and time operations. + * Provides functionality to retrieve the current date and time. + * + * @author wangjx + * @since 2026-02-13 + */ +public class DateTimeTools implements BiFunction, ToolContext, String> { + + + @Override + public String apply(Map map, ToolContext toolContext) { + return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + public ToolCallback toolCallback() { + return FunctionToolCallback.builder("get_current_date_time", this) + .description("get_current_date_time") + .inputType(Map.class) + .build(); + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/SendEmailTool.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/SendEmailTool.java new file mode 100644 index 00000000..da219f1c --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/SendEmailTool.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloud.alibaba.ai.example.agent.tool; + +import com.cloud.alibaba.ai.example.agent.model.EmailInfo; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * A tool for sending emails based on natural language input. + * This class implements the BiFunction interface to process email information + * and simulate email sending functionality. + * The tool validates email addresses, formats the email content, + * and provides a callback mechanism for integration with AI agents. + * + * @author wangjx + * @since 2026-02-13 + */ +public class SendEmailTool implements BiFunction { + @Override + public String apply(EmailInfo args, ToolContext toolContext) { + // 参数解析 + List to = args.getTo(); + String subject = args.getSubject(); + String body = args.getBody(); + // 验证邮箱格式(简化版) + if (to == null || to.isEmpty()){ + return "Error: No recipient email addresses provided."; + } + // 模拟发送邮件 + System.out.printf("Email sent to %s - Subject: %s%n body: %s", String.join(", ", to), subject, body); + return String.format("Email sent to %s - Subject: %s", String.join(", ", to), subject); + } + + + private boolean isValidEmail(String email) { + // 简单验证邮箱格式(实际应使用更严格的验证) + return email != null && email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); + } + + public ToolCallback toolCallback() { + return FunctionToolCallback.builder("send_email", this) + .description(""" + Send emails using natural language. + Use this when the user wants to send notifications, reminders, or any email + communication. Handles recipient extraction, subject generation, and email + composition. + Input: Natural language email request (e.g., "send them a reminder about + the meeting" + """) + .inputType(EmailInfo.class) + .build(); + + } + +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/UserDataTool.java b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/UserDataTool.java new file mode 100644 index 00000000..d72f518b --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/java/com/cloud/alibaba/ai/example/agent/tool/UserDataTool.java @@ -0,0 +1,92 @@ +/* + * Copyright 2026-2027 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cloud.alibaba.ai.example.agent.tool; + +import com.alibaba.fastjson.JSON; +import com.cloud.alibaba.ai.example.agent.model.UserInfo; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +/** + * A tool class that provides functionality to retrieve user information based on username or department. + * It implements BiFunction interface to process UserInfo and ToolContext inputs and return relevant user data as a JSON string. + * The class contains a predefined list of users and supports filtering by username or department name. + * + * @author wangjx + * @since 2026-02-13 + */ +public class UserDataTool implements BiFunction { + + private static final List USER_INFO_LIST = new ArrayList<>(); + + static { + UserInfo userInfo = new UserInfo(); + userInfo.setUserName("zhangsan(张三)"); + userInfo.setEmail("zhangsan@agent.cn"); + userInfo.setDepartmentName("development(研发部)"); + USER_INFO_LIST.add(userInfo); + UserInfo userInfo1 = new UserInfo(); + userInfo1.setUserName("lisi(李四)"); + userInfo1.setEmail("lisi@agent.cn"); + userInfo1.setDepartmentName("development(研发部)"); + USER_INFO_LIST.add(userInfo1); + + UserInfo userInfo2 = new UserInfo(); + userInfo2.setUserName("wangwu(王五)"); + userInfo2.setEmail("wangwu@agent.cn"); + userInfo2.setDepartmentName("design(设计团队)"); + + USER_INFO_LIST.add(userInfo2); + + } + + @Override + public String apply(UserInfo userInfo, ToolContext toolContext) { + String userName = userInfo.getUserName(); + + if (StringUtils.hasLength(userName)) { + List userInfos = USER_INFO_LIST.stream().filter(f -> f.getUserName().contains(userName)).toList(); + if (!userInfos.isEmpty()) { + String str = JSON.toJSONString(userInfos); + return String.format("Available user list for %s", str); + } + } + String department = userInfo.getDepartmentName(); + + if (StringUtils.hasLength(department)) { + List userInfos = USER_INFO_LIST.stream().filter(f -> f.getDepartmentName().contains(department)).toList(); + if (!userInfos.isEmpty()) { + String str = JSON.toJSONString(userInfos); + return String.format("Available user list for %s", str); + } + } + return ""; + } + + public ToolCallback toolCallback() { + return FunctionToolCallback.builder("get_user_email_tool", this) + .description("You can provide the functionality to retrieve a user's email address by their username, and to obtain all user names within a department by specifying the department name.") + .inputType(UserInfo.class) + .build(); + } +} diff --git a/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/resources/application.yml b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/resources/application.yml new file mode 100644 index 00000000..8404a286 --- /dev/null +++ b/spring-ai-alibaba-agent-example/subagent-personal-assistant-example/src/main/resources/application.yml @@ -0,0 +1,11 @@ + spring: + application: + name: subagent-personal-assistant-example + ai: + dashscope: + api-key: ${DASHSCOPE_API_KEY} + chat: + options: + model: 'qwen3-max-2026-01-23' + server: + port: 8080