diff --git a/.gitignore b/.gitignore index fc42d635cf7a1b8209eb69368381e90415874151..a510310c852467840f275019ab43b006dc7343e6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ target/ *.ipr *.log *.flattened-pom.xml +*.md ### NetBeans ### nbproject/private/ diff --git a/AGENT_TEAMS.md b/AGENT_TEAMS.md new file mode 100644 index 0000000000000000000000000000000000000000..2829bce9a355c4cfb1ee76f36b42541f170a9fdc --- /dev/null +++ b/AGENT_TEAMS.md @@ -0,0 +1,381 @@ +# Agent Teams 模式 + +## 概述 + +Agent Teams 是一种多代理协作模式,通过 **MainAgent(协调器)** 和 **SubAgents(执行者)** 的分工协作,实现复杂任务的自动化分解和执行。 + +--- + +## 架构设计 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 用户请求 │ +│ "实现用户登录功能" │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MainAgent (协调器) │ +│ - 分析任务需求 │ +│ - 创建子任务 │ +│ - 协调 SubAgents │ +│ - 汇总执行结果 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ ExploreSub │ │ PlanSub │ │ BashSub │ +│ 探索代码库 │ │ 设计方案 │ │ 执行命令 │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ │ │ + └───────────────┴───────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ SharedTaskList │ + │ (共享任务队列) │ + └─────────────────────┘ +``` + +--- + +## 核心组件 + +### 1. MainAgent(主代理) + +**职责**: 任务协调和决策 + +**功能**: +- 接收用户请求,分析任务复杂度 +- 将复杂任务分解为多个子任务 +- 协调 SubAgents 认领和执行任务 +- 汇总结果并生成最终回复 + +### 2. SubAgents(子代理) + +**内置类型**: + +| 子代理 | 代码 | 专长 | 最大步数 | +|--------|------|------|----------| +| 探索代理 | `explore` | 快速定位文件、理解代码结构 | 15 | +| 规划代理 | `plan` | 任务分解、实现方案设计 | 20 | +| 命令代理 | `bash` | Git 操作、构建、测试 | 10 | +| 通用代理 | `general-purpose` | 复杂多步骤任务 | 25 | + +### 3. SharedTaskList(共享任务队列) + +**功能**: +- 存储所有团队任务 +- 支持任务依赖关系 +- 自动分配可认领任务 +- 追踪任务状态和进度 + +--- + +## Teammate 管理(团队成员) + +类似 Claude Code 的 `/teammate` 功能,支持动态创建和管理团队成员。 + +### 创建团队成员 + +```java +// 通过 AgentTeamsSkill 创建新成员 +teammate( + name="security-expert", + role="安全专家", + description="专注于安全审计、漏洞检测和合规性检查", + expertise="security,auth,encryption", + model="gpt-4" +) +``` + +**输出格式**(表格): +``` +✅ 团队成员创建成功 + +## 成员信息 + +| 属性 | 值 | +|------|------| +| **名称** | `security-expert` | +| **角色** | 安全专家 | +| **描述** | 专注于安全审计、漏洞检测和合规性检查 | +| **专业领域** | security,auth,encryption | +| **模型** | gpt-4 | +| **文件** | `.soloncode/agents/security-expert.md` | +| **状态** | 🟢 已激活 | +``` + +### 查看所有成员 + +```java +// 列出所有团队成员(表格格式) +teammates() +``` + +**输出格式**: +``` +## 团队成员 + +| 名称 | 角色 | 描述 | 状态 | 模型 | +|------|------|------|------|------| +| `explore` | Explore | 代码探索专家 | 🟢 活跃 | 默认 | +| `plan` | Plan | 方案设计专家 | 🟢 活跃 | 默认 | +| `security-expert` | 安全专家 | 安全审计和漏洞检测 | 🟢 活跃 | gpt-4 | + +**总计**: 3 位活跃成员 +``` + +### 移除团队成员 + +```java +// 禁用指定成员 +remove_teammate("security-expert") +``` + +--- + +## 简单代码实现 + +### 基础初始化 + +```java +// 1. 创建 SubAgentAgentBuilder +SubAgentAgentBuilder builder = SubAgentAgentBuilder.of(chatModel) + .workDir("./work") + .sessionProvider(sessionProvider) + .poolManager(poolManager) + .addAllFrom(subagentManager); + +// 2. 构建 MainAgent +MainAgent mainAgent = builder.build(); + +// 3. 执行团队协作任务 +AgentResponse response = mainAgent.execute( + Prompt.of("实现用户登录功能,包括探索代码、设计方案、开发实现、编写测试") +); + +// 4. 获取任务统计 +SharedTaskList.TaskStatistics stats = mainAgent.getTaskList().getStatistics(); +System.out.println("总任务: " + stats.totalTasks); +System.out.println("已完成: " + stats.completedTasks); +``` + +### 创建和管理任务 + +```java +// 获取共享任务列表 +SharedTaskList taskList = mainAgent.getTaskList(); + +// 创建新任务 +TeamTask exploreTask = new TeamTask(); +exploreTask.setTitle("探索现有认证代码"); +exploreTask.setDescription("分析项目中的认证相关代码结构"); +exploreTask.setType(TeamTask.TaskType.EXPLORATION); +exploreTask.setPriority(8); + +// 添加到任务队列 +CompletableFuture future = taskList.addTask(exploreTask); +TeamTask addedTask = future.join(); + +// 创建带依赖的任务 +TeamTask implTask = new TeamTask(); +implTask.setTitle("实现登录功能"); +implTask.setDependencies(Arrays.asList(addedTask.getId())); // 依赖 exploreTask + +taskList.addTask(implTask); +``` + +### SubAgent 认领任务 + +```java +// SubAgent 主动认领任务 +Subagent exploreAgent = subagentManager.getAgent("explore"); + +// 查看可认领任务 +List claimableTasks = taskList.getClaimableTasks(); + +// 认领并执行 +for (TeamTask task : claimableTasks) { + if (task.getType() == TeamTask.TaskType.EXPLORATION) { + exploreAgent.claimAndExecute(task); + break; + } +} +``` + +### 监听任务事件 + +```java +// 订阅任务事件 +EventBus eventBus = mainAgent.getEventBus(); + +// 监听任务创建 +eventBus.subscribe(AgentEventType.TASK_CREATED, event -> { + TeamTask task = (TeamTask) event.getData(); + System.out.println("新任务创建: " + task.getTitle()); +}); + +// 监听任务完成 +eventBus.subscribe(AgentEventType.TASK_COMPLETED, event -> { + TeamTask task = (TeamTask) event.getData(); + System.out.println("任务完成: " + task.getTitle()); +}); + +// 监听任务失败 +eventBus.subscribe(AgentEventType.TASK_FAILED, event -> { + TeamTask task = (TeamTask) event.getData(); + System.out.println("任务失败: " + task.getErrorMessage()); +}); +``` + +--- + +## 工作流程 + +``` +1. 用户请求 + ↓ +2. MainAgent 分析任务 + ↓ +3. 创建主任务并添加到 SharedTaskList + ↓ +4. MainAgent 分解任务为子任务 + ↓ +5. 子任务添加到 SharedTaskList(带依赖关系) + ↓ +6. SubAgents 扫描可认领任务 + ↓ +7. SubAgent 认领并执行任务 + ↓ +8. 更新任务状态,触发事件 + ↓ +9. 重复 6-8 直到所有任务完成 + ↓ +10. MainAgent 汇总结果并返回 +``` + +--- + +## 典型使用场景 + +### 场景 1: 功能开发 + +``` +用户: "实现用户登录功能" + +MainAgent: + ├─ 创建任务: "实现用户登录" + ├─ 分解: + │ ├─ Task 1: 探索现有认证代码 (explore) + │ ├─ Task 2: 设计登录方案 (plan) + │ ├─ Task 3: 实现登录逻辑 (general-purpose) + │ └─ Task 4: 编写测试 (general-purpose) + └─ 协调 SubAgents 执行 +``` + +### 场景 2: 代码重构 + +``` +用户: "重构 UserService 类,使其符合 SOLID 原则" + +MainAgent: + ├─ 创建任务: "重构 UserService" + ├─ 分解: + │ ├─ Task 1: 分析 UserService 依赖 (explore) + │ ├─ Task 2: 设计重构方案 (plan) + │ ├─ Task 3: 执行重构 (general-purpose) + │ └─ Task 4: 运行测试验证 (bash) + └─ 协调 SubAgents 执行 +``` + +### 场景 3: 问题诊断 + +``` +用户: "登录时偶尔超时,帮我排查问题" + +MainAgent: + ├─ 创建任务: "诊断登录超时问题" + ├─ 分解: + │ ├─ Task 1: 搜索日志文件 (bash) + │ ├─ Task 2: 分析认证代码 (explore) + │ ├─ Task 3: 检查数据库连接 (bash) + │ └─ Task 4: 生成诊断报告 (general-purpose) + └─ 协调 SubAgents 执行 +``` + +--- + +## 配置示例 + +**config.yml**: +```yaml +solon: + code: + cli: + workDir: ./work + chatModel: + apiUrl: https://api.openai.com/v1 + apiKey: ${OPENAI_API_KEY} + model: gpt-4 + defaultOptions: + temperature: 0.7 + + # 启用子代理系统 + subAgentEnabled: true + + # 子代理模型配置(可选) + subAgentModels: + explore: gpt-4o-mini + plan: gpt-4 + bash: gpt-4o-mini + general-purpose: gpt-4 +``` + +--- + +## 关键特性 + +| 特性 | 说明 | +|------|------| +| **任务分解** | MainAgent 自动将复杂任务分解为可执行的子任务 | +| **依赖管理** | 支持任务间依赖关系,确保执行顺序正确 | +| **并行执行** | 多个 SubAgent 可并行执行独立任务 | +| **状态跟踪** | 实时追踪任务状态、进度和结果 | +| **事件驱动** | 基于 EventBus 的异步事件通知 | +| **容错机制** | 任务失败自动重试或标记错误 | + +--- + +## 总结 + +Agent Teams 模式通过 **MainAgent 协调 + SubAgents 执行** 的分工协作,实现了复杂任务的自动化处理: + +1. **MainAgent** 负责任务分解和协调决策 +2. **SubAgents** 专注于特定领域的任务执行 +3. **SharedTaskList** 提供统一的任务管理 +4. **EventBus** 实现松耦合的事件通信 +5. **Teammate 管理** 支持动态创建和管理团队成员(类似 Claude Code) + +### 核心工具 + +| 工具 | 功能 | 输出格式 | +|------|------|----------| +| `team_task()` | 启动团队协作任务 | 统计 + 结果 | +| `team_status()` | 查看任务状态 | 表格 + 统计 | +| `create_task()` | 创建新任务 | 任务信息 | +| `teammate()` | 创建团队成员 | 表格 | +| `teammates()` | 列出所有成员 | 表格 | +| `remove_teammate()` | 移除团队成员 | 状态信息 | +| `subagent()` | 调用子代理 | task_id + 结果 | + +这种模式特别适合需要多个步骤、涉及多个文件或需要专业领域知识的复杂任务。 + +--- + +**作者**: bai +**日期**: 2026-03-09 +**版本**: 1.0 diff --git a/SUBAGENT.md b/SUBAGENT.md index a504a7f840d1684bb25f1395f1ed52b3a1d12478..a2d678b0b3a0f00316448a1355accf034a00e41c 100644 --- a/SUBAGENT.md +++ b/SUBAGENT.md @@ -1,374 +1,981 @@ +# SubAgent 子代理系统完整指南 +## 目录 +- [架构设计](#架构设计) +- [核心组件](#核心组件) +- [实现细节](#实现细节) +- [使用示例](#使用示例) +- [自定义子代理](#自定义子代理) +- [最佳实践](#最佳实践) -# Subagent 子代理系统使用指南 +--- -Subagent 系统提供了类似 Claude Code 的子代理功能,允许主 Agent 调用专门的子代理来处理特定任务,提高任务处理效率。 +## 架构设计 -## 功能概述 +### 整体架构 -Subagent 系统包含以下类型的子代理: +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 主 Agent (AgentKernel) │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ TaskSkill / AgentTeamsSkill │ │ +│ │ ┌────────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ +│ │ │ task() │ │ team_task() │ │ team_status() │ │ │ +│ │ │ create_agent()│ │ create_task() │ │ subagent() │ │ │ +│ │ └────────────────┘ └──────────────┘ └─────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ SubagentManager (子代理管理器) │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ AgentPool 扫描和发现 │ │ +│ │ - @soloncode_agents/ - .soloncode/agents/ │ │ +│ │ - @opencode_agents/ - .opencode/agents/ │ │ +│ │ - @claude_agents/ - .claude/agents/ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ +│ │ │ Explore │ │ Plan │ │ Bash │ │ │ +│ │ │ Agent │ │ Agent │ │ Agent │ │ │ +│ │ └──────────────┘ └──────────────┘ └───────────────┘ │ │ +│ │ ┌──────────────────────────────────────────────────────┐ │ │ +│ │ │ GeneralPurpose Agent │ │ │ +│ │ └──────────────────────────────────────────────────────┘ │ │ +│ │ ┌──────────────────────────────────────────────────────┐ │ │ +│ │ │ SolonCodeGuide Agent │ │ │ +│ │ └──────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ 会话管理 (Sessions) │ + │ - 独立的会话空间 │ + │ - 会话历史缓存 │ + └────────────────────────┘ +``` -| 类型 | 代码 | 描述 | -|------|------|------| -| 探索代理 | `explore` | 快速探索代码库,查找文件、理解代码结构 | -| 计划代理 | `plan` | 软件架构师,设计实现方案和执行计划 | -| 命令代理 | `bash` | 命令执行专家,处理 git、构建等终端任务 | -| 通用代理 | `general-purpose` | 处理复杂的多步骤任务 | -| Solon指南代理 | `solon-guide` | Solon Code、Agent SDK 和 API 专家,可从官网读取文档 | +--- -## 启用 Subagent +## 核心组件 -在创建 AgentKernel 时启用子代理功能: +### 1. Subagent 接口 -```java -AgentProperties properties = new AgentProperties(); -properties.setSubagentEnabled(true); +**位置**: `org.noear.solon.bot.core.subagent.Subagent` -AgentKernel mainAgent = new AgentKernel(chatModel, ..., properties); +```java +public interface Subagent { + /** + * 获取子代理类型 + */ + String getType(); + + /** + * 获取描述 + */ + String getDescription(); + + /** + * 获取系统提示词 + */ + String getSystemPrompt(); + + /** + * 执行任务(同步) + * + * @param __cwd 工作目录 + * @param sessionId 会话ID + * @param prompt 任务提示 + * @return 执行结果 + */ + AgentResponse call(String __cwd, String sessionId, Prompt prompt) throws Throwable; + + /** + * 执行任务(流式) + * + * @param __cwd 工作目录 + * @param sessionId 会话ID + * @param prompt 任务提示 + * @return 流式结果 + */ + Flux stream(String __cwd, String sessionId, Prompt prompt); +} ``` -## 使用方式 +### 2. SubagentManager + +**位置**: `org.noear.solon.bot.core.subagent.SubagentManager` -### 方式一:主 Agent 通过工具调用 +**核心功能**: +- 管理子代理的创建和缓存 +- 从 AgentPool 扫描和发现子代理 +- 根据类型获取子代理实例 -主 Agent 可以使用 `subagent` 工具调用子代理: +**API**: +```java +public class SubagentManager { + // 注册 AgentPool + void agentPool(String alias, String path); + + // 获取子代理 + Subagent getAgent(String type); + + // 获取所有子代理 + List getAgents(); + // 添加子代理 + void addSubagent(Subagent subagent); +} ``` -用户:帮我探索一下项目的核心类 -主 Agent 会: -1. 识别任务类型为"探索代码库" -2. 调用 subagent 工具,type="explore" -3. 将任务委托给探索代理处理 -4. 返回探索结果给用户 +### 3. AbsSubagent 抽象类 + +**位置**: `org.noear.solon.bot.core.subagent.AbsSubagent` + +**功能**: +- 提供子代理的基础实现 +- 管理 ReActAgent 的创建和缓存 +- 提供默认的工具集和配置 + +**继承层次**: +``` +AbsSubagent (抽象基类) + ├── ExploreSubagent + ├── PlanSubagent + ├── BashSubagent + ├── GeneralPurposeSubagent + └── SolonGuideSubagent ``` -### 方式二:直接调用 Subagent +--- + +## 实现细节 -通过 SubagentManager 直接调用: +### 1. 子代理创建流程 ```java +// SubagentManager.java + +public Subagent getAgent(String type) { + // 1. 从缓存获取 + Subagent agent = agents.get(type); + if (agent != null) { + return agent; + } + + // 2. 从 AgentPool 扫描 + for (AgentPool pool : agentPools) { + agent = pool.loadAgent(type); + if (agent != null) { + // 缓存并返回 + agents.put(type, agent); + return agent; + } + } + + // 3. 未找到 + return null; +} +``` -// 获取 SubagentManager -SubagentManager manager = mainAgent.getSubagentManager(); +### 2. AgentPool 扫描机制 -// 使用探索代理 -String prompt = "请探索这个项目的代码结构,找出所有的 Java 源文件"; -AgentResponse response = manager.getAgent(SubagentType.EXPLORE) - .execute(sessionId, Prompt.of(prompt)); +```java +// AgentPool.java + +public Subagent loadAgent(String type) { + // 1. 查找提示词文件 + Optional promptFile = findPromptFile(type); + + if (!promptFile.isPresent()) { + return null; + } + + // 2. 解析 YAML frontmatter 和 Markdown 内容 + SubAgentMetadata metadata = parseMetadata(promptFile.get()); + String systemPrompt = parseSystemPrompt(promptFile.get()); + + // 3. 创建子代理实例 + AbsSubagent agent = createAgentInstance(metadata, systemPrompt); + + return agent; +} + +private Optional findPromptFile(String type) { + // 搜索 type.md 文件 + // 搜索顺序:soloncode_agents > opencode_agents > claude_agents + Path[] searchPaths = { + paths.get("soloncode_agents", ".soloncode/agents"), + paths.get("opencode_agents", ".opencode/agents"), + paths.get("claude_agents", ".claude/agents") + }; + + for (Path path : searchPaths) { + Path file = path.resolve(type + ".md"); + if (Files.exists(file)) { + return Optional.of(file); + } + } + + return Optional.empty(); +} ``` -## 各类型子代理详解 +### 3. 会话隔离 -### 1. ExploreSubagent - 探索代理 - -**适用场景:** -- 查找特定文件或模式 -- 理解代码库结构 -- 搜索类、函数、变量定义 -- 分析模块依赖关系 +每个子代理调用都有独立的会话ID: -**示例:** ```java -import org.noear.solon.ai.chat.prompt.Prompt; +// 生成唯一会话ID +String sessionId = "subagent_" + type + "_" + System.currentTimeMillis(); -// 查找所有配置文件 -manager.getAgent(SubagentType.EXPLORE) - .execute(sessionId, Prompt.of("找出项目中所有的 .yml 和 .properties 配置文件")); +// 执行任务 +AgentResponse response = agent.call( + __cwd, // 工作目录 + sessionId, // 会话ID + Prompt.of(prompt) +); -// 理解某个模块的结构 -manager.getAgent(SubagentType.EXPLORE) - .execute(sessionId, Prompt.of("分析 soloncodecli/core 包的代码结构")); +// 会话ID 格式:subagent_explore_1234567890 ``` -**特点:** -- 优先使用 Glob 工具进行文件查找 -- 使用 Grep 工具搜索代码内容 -- 只读操作,不会修改代码 -- 较少的步数限制(15步) +**好处**: +- ✅ 子代理之间不会相互干扰 +- ✅ 可以通过 `taskId` 继续之前的任务 +- ✅ 便于追踪和调试 -### 2. PlanSubagent - 计划代理 +### 4. Token 统计 -**适用场景:** -- 设计新功能的实现方案 -- 规划重构策略 -- 制定迁移计划 -- 架构设计 +子代理的 Token 使用会累计到主 Agent: -**示例:** ```java -import org.noear.solon.ai.chat.prompt.Prompt; +// TaskSkill.handle() / AgentTeamsSkill.subagent() -manager.getAgent(SubagentType.PLAN) - .execute(sessionId, Prompt.of("为添加用户认证功能设计实现方案")); +AgentSession __parentSession = kernel.getSession(__sessionId); +ReActTrace __parentTrace = ReActTrace.getCurrent(__parentSession.getSnapshot()); -manager.getAgent(SubagentType.PLAN) - .execute(sessionId, Prompt.of("规划将代码迁移到新架构的步骤")); -``` +// 执行子代理 +AgentResponse response = agent.call(...); -**输出格式:** -1. 概述:实现思路 -2. 关键文件:需要修改的文件 -3. 执行步骤:具体的实现步骤 -4. 注意事项:潜在风险 -5. 验证方案:如何验证实现 +// 累计 Token +__parentTrace.getMetrics().addMetrics(response.getMetrics()); +``` -**特点:** -- 专注于规划和设计 -- 不执行代码修改 -- 考虑架构权衡 -- 提供清晰的执行计划 +--- -### 3. BashSubagent - 命令代理 +## 使用示例 -**适用场景:** -- Git 操作(commit, push, pull, branch) -- 项目构建(mvn, gradle, npm) -- 测试执行 -- 依赖安装 +### 示例 1: 基础调用 -**示例:** ```java import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.subagent.SubagentManager; + +// 初始化 +AgentKernel kernel = new AgentKernel(chatModel, properties); +SubagentManager manager = kernel.getSubagentManager(); -manager.getAgent(SubagentType.BASH) - .execute(sessionId, Prompt.of("运行所有单元测试")); +// 调用探索代理 +String sessionId = "session_" + System.currentTimeMillis(); -manager.getAgent(SubagentType.BASH) - .execute(sessionId, Prompt.of("创建一个新的 git 分支 feature/auth")); +AgentResponse response = manager.getAgent("explore") + .call("./work", sessionId, Prompt.of("探索项目的核心类")); -manager.getAgent(SubagentType.BASH) - .execute(sessionId, Prompt.of("使用 Maven 构建项目")); +// 处理结果 +System.out.println(response.getContent()); ``` -**特点:** -- 只包含 Bash 工具 -- 专注于命令执行 -- 较少的步数限制(10步) -- 小的会话窗口(3) +### 示例 2: 通过工具调用(推荐) -### 4. GeneralPurposeSubagent - 通用代理 +```java +// 1. 将 TaskSkill 或 AgentTeamsSkill 添加到 AgentKernel +AgentKernel kernel = new AgentKernel(chatModel, properties); +TaskSkill taskSkill = new TaskSkill(kernel, subagentManager); + +kernel.getReActAgent() + .defaultSkillAdd(taskSkill); + +// 2. LLM 自动调用 +// 用户:"帮我探索项目的认证模块" + +// 3. 主 Agent 的内部推理 +/* +识别任务:代码探索 +选择工具:subagent +参数: + - type="explore" + - prompt="探索项目中的认证相关模块,找出所有涉及的类、接口和配置" + +执行:调用 explore 子代理 +返回:探索结果 +*/ +``` -**适用场景:** -- 复杂的多步骤任务 -- 需要多种工具协作的任务 -- 跨模块的任务协调 -- 涉及网络检索的研究任务 +### 示例 3: 继续之前的任务 -**示例:** ```java -import org.noear.solon.ai.chat.prompt.Prompt; +// 第一次调用 +AgentResponse response1 = manager.getAgent("explore") + .call("./work", "session_1", Prompt.of("探索认证模块")); + +String taskId = response.getMetadata().get("taskId"); +// taskId: "subagent_explore_1234567890" + +// 第二次调用(继续同一个任务) +AgentResponse response2 = manager.getAgent("explore") + .call("./work", "session_1", Prompt.of("继续深入分析认证流程")); + +// 或者使用 taskId +AgentResponse response2 = manager.getAgent("explore") + .call("./work", taskId, Prompt.of("继续深入分析认证流程")); +``` -manager.getAgent(SubagentType.GENERAL_PURPOSE) - .execute(sessionId, Prompt.of("重构登录模块,添加多因素认证功能")); +### 示例 4: 团队协作场景 -manager.getAgent(SubagentType.GENERAL_PURPOSE) - .execute(sessionId, Prompt.of("研究并实现一个新的 API 端点")); +```java +// 任务:实现用户登录功能 + +// 1. 探索现有代码 +AgentResponse exploreResult = manager.getAgent("explore") + .call(sessionId, Prompt.of("探索项目中的认证相关代码")); + +// 2. 设计实现方案 +AgentResponse planResult = manager.getAgent("plan") + .call(sessionId, Prompt.of( + "基于探索结果,设计用户认证功能的实现方案。\n" + + "现有代码:" + exploreResult.getContent() + )); + +// 3. 执行实现 +AgentResponse implResult = manager.getAgent("general-purpose") + .call(sessionId, Prompt.of( + "按照设计方案实现用户认证功能:\n" + + "1. 创建 UserService 接口\n" + + "2. 实现 AuthenticationController\n" + + "3. 添加登录表单" + )); + +// 4. 运行测试 +AgentResponse testResult = manager.getAgent("bash") + .call(sessionId, Prompt.of("运行项目的单元测试")); ``` -**特点:** -- 包含完整的工具集 -- 最多步数限制(25步) -- 最大的会话窗口(10) -- 功能最全面 +--- + +## 自定义子代理 + +### 方式1: 通过 YAML 配置文件 + +**文件位置**: `.soloncode/agents/my-custom-agent.md` + +```markdown +--- +code: my-custom-agent +name: 我的自定义代理 +description: 专门处理数据库相关任务 +model: gpt-4 +enabled: true +tools: + - read + - write + - grep +skills: + - expert + - lucene +max_turns: 20 +--- + +# 系统提示词 + +你是数据库专家,擅长: + +1. **SQL 查询优化** + - 分析查询性能 + - 优化索引 + - 重写复杂查询 + +2. **数据建模** + - 设计合理的表结构 + - 定义关系 + - 规范化数据 + +3. **数据库迁移** + - 编写迁移脚本 + - 处理版本升级 + +请在处理任务时: +- 优先考虑查询性能 +- 确保数据一致性 +- 遵循数据库设计范式 +``` -### 5. SolonCodeGuideSubagent - Solon 指南代理 +**自动发现**: 重启应用后自动加载 -**适用场景:** -- 查询 Solon Code 相关问题 -- 学习 Solon Agent SDK 使用方法 -- 了解 Solon API 接口和用法 -- 从 Solon 官网获取最新文档 +### 方式2: 通过代码创建 -**示例:** ```java -import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.bot.core.subagent.AbsSubagent; +import org.noear.solon.bot.core.AgentKernel; + +public class DatabaseExpertAgent extends AbsSubagent { + + public DatabaseExpertAgent(AgentKernel mainAgent) { + super(mainAgent); + } + + @Override + public String getType() { + return "database-expert"; + } + + @Override + protected String getDefaultDescription() { + return "数据库专家,擅长 SQL 优化和数据建模"; + } + + @Override + protected String getDefaultSystemPrompt() { + return """ + ## 数据库专家 + + 你是数据库专家,专注于: + + ### 核心能力 + - SQL 查询优化 + - 数据库设计 + - 性能调优 + + ### 工具使用策略 + 1. **read**: 读取 schema 文件 + 2. **grep**: 搜索 SQL 语句 + 3. **write**: 修改数据库脚本 + + ### 最佳实践 + - 优先分析索引使用情况 + - 识别慢查询 + - 建议合理的索引 + """; + } + + @Override + protected void customize(ReActAgent.Builder builder) { + // 添加工具 + builder.defaultSkillAdd(mainAgent.getCliSkills()); + builder.defaultSkillAdd(LuceneSkill.getInstance()); + + // 设置配置 + builder.maxSteps(20); + builder.sessionWindowSize(8); + } +} +``` -// 查询 Solon 快速入门文档 -manager.getAgent(SubagentType.SOLON_CODE_GUIDE) - .execute(sessionId, Prompt.of("Solon 的快速入门方法是什么?")); +**注册子代理**: +```java +DatabaseExpertAgent agent = new DatabaseExpertAgent(kernel); +manager.addSubagent(agent); +``` -// 学习 Agent SDK -manager.getAgent(SubagentType.SOLON_CODE_GUIDE) - .execute(sessionId, Prompt.of("如何在 Solon Agent SDK 中创建自定义工具?")); +### 方式3: 动态创建(通过工具) -// 了解 API 用法 -manager.getAgent(SubagentType.SOLON_CODE_GUIDE) - .execute(sessionId, Prompt.of("ChatModel 接口如何使用?")); +```java +// 通过 TaskSkill 或 AgentTeamsSkill 的 create_agent() 工具 + +AgentResponse response = kernel.prompt(Prompt.of( + "创建一个数据库专家代理" + + "code: database-expert" + + "name: 数据库专家" + + "description: 专门处理SQL优化和数据建模" + + "systemPrompt: 你是SQL专家..." + + "tools: read,write,grep" + + "skills: expert,lucene" +)).call(); + +// 代理定义保存到 .soloncode/agents/database-expert.md +// 下次启动时自动加载 ``` -**专属工具:** -- `read_solon_doc`: 读取 Solon 官网文档(支持本地缓存) - - 支持文档:learn-start, agent-quick-start, agent-tools, agent-skill 等 - - 自动缓存到 `.soloncode/cache/docs/` 目录 - - 提供内存缓存提升性能 -- `list_solon_docs`: 列出所有可用的官方文档 -- `clear_solon_doc_cache`: 清除文档缓存 +--- -**特点:** -- 专注于 Solon 技术栈 -- 可直接从官网获取最新文档 -- 支持文档缓存,减少网络请求 -- 较少的步数限制(15步) -- 小的会话窗口(5) +## 内置子代理详解 -## 最佳实践 +### 1. ExploreSubagent - 探索代理 -### 1. 选择合适的子代理类型 +**代码**: `org.noear.solon.bot.core.subagent.ExploreSubagent` +**系统提示词**: ``` -简单文件查找 -> explore -设计实现方案 -> plan -执行命令 -> bash -复杂的多步骤任务 -> general-purpose +你是代码探索专家,擅长快速定位文件和理解代码结构。 + +### 工具使用策略 +1. **优先使用 Glob**: 定位文件路径(如 **/*.java) +2. **使用 Grep**: 搜索代码内容 +3. **最后才用 Read**: 读取具体文件 + +### 工作流程 +1. 根据 task 描述,选择合适的搜索策略 +2. 使用 Glob 找到相关文件 +3. 使用 Grep 搜索关键代码 +4. 必要时读取文件了解细节 ``` -### 2. 组合使用子代理 - +**配置**: +```java +maxSteps: 15 // 最大步数 +sessionWindowSize: 5 // 会话窗口 +tools: glob, grep, read // 只读工具 ``` -任务:添加新功能 -1. 使用 explore 代理查找相关文件 -2. 使用 plan 代理设计实现方案 -3. 使用 general-purpose 代理执行实现 -4. 使用 bash 代理运行测试验证 +**适用任务**: +- 查找特定文件 +- 理解代码结构 +- 搜索类、函数定义 +- 分析依赖关系 + +--- + +### 2. PlanSubagent - 计划代理 + +**代码**: `org.noear.solon.bot.core.subagent.PlanSubagent` + +**系统提示词**: ``` +## 软件架构师 -### 3. 子代理工具调用 +你是软件架构师,负责设计和规划。 -主 Agent 可以智能地选择合适的子代理: +### 工作流程 +1. **理解需求**: 仔细阅读 task 描述 +2. **分析现状**: 查看现有代码和架构 +3. **设计方案**: 提供清晰的实现方案 +4. **拆解任务**: 将大任务分解为小步骤 +### 输出格式 +1. **概述**: 实现思路和技术选型 +2. **关键文件**: 需要创建/修改的文件列表 +3. **执行步骤**: 详细的实现步骤 +4. **注意事项**: 潜在风险和注意事项 ``` -用户:帮我理解这个项目的认证模块 -主 Agent 的思考过程: -1. 这是一个代码理解任务 -2. 应该使用 explore 子代理 -3. 调用 subagent(type="explore", prompt="...") -4. 返回探索结果 +**配置**: +```java +maxSteps: 20 // 最大步数 +sessionWindowSize: 8 // 会话窗口 +tools: glob, grep, read // 只读工具 ``` -## 配置选项 +**适用任务**: +- 设计新功能 +- 规划重构策略 +- 制定迁移计划 +- 架构设计 -### 禁用 Subagent +--- + +### 3. BashSubagent - 命令代理 + +**代码**: `org.noear.solon.bot.core.subagent.BashSubagent` + +**系统提示词**: +``` +## 命令执行专家 -如果不希望使用子代理功能: +你是命令执行专家,擅长处理终端任务。 +### 支持的命令类型 +- **Git 操作**: commit, push, pull, branch, merge +- **项目构建**: mvn, gradle, npm, make +- **测试执行**: pytest, npm test, go test +- **依赖管理**: npm install, pip install, go mod + +### 注意事项 +- 每次只执行一个命令 +- 先检查命令是否成功 +- 失败时提供错误信息和建议解决方案 +``` + +**配置**: ```java -CodeAgent codeAgent = new CodeAgent(chatModel) - .enableSubagent(false); // 禁用子代理 +maxSteps: 10 // 最大步数 +sessionWindowSize: 3 // 会话窗口 +tools: bash // 仅 Bash 工具 +``` + +**适用任务**: +- Git 操作 +- 项目构建 +- 测试执行 +- 依赖安装 + +--- + +### 4. GeneralPurposeSubagent - 通用代理 + +**代码**: `org.noear.solon.bot.core.subagent.GeneralPurposeSubagent` + +**系统提示词**: ``` +## 通用任务代理 + +你是全能型执行专家,负责处理复杂、多步骤且需要综合能力的开发任务。 + +### 工具使用策略 +1. **本地搜索** (内部): 定位项目内代码、符号或文件时 + - Lucene: 全文搜索 + - Glob: 文件查找 + - Grep: 代码搜索 -### 自定义子代理配置 +2. **全网调研** (外部): 遇到新技术、查阅第三方文档时 + - WebSearch: 搜索互联网信息 + - WebFetch: 获取文档内容 -可以通过 SubagentConfig 自定义每个子代理的行为: +3. **闭环执行**: 拥有写权限,可以直接修改和验证 +### 工作原则 +1. **理解优先**: 动笔修改前,必须充分理解现有逻辑 +2. **分步验证**: 每完成一个关键步骤,建议运行测试验证 +3. **系统性思考**: 修改代码时需考虑对周边模块的影响 +``` + +**配置**: ```java -import org.noear.solon.ai.codecli.core.subagent.SubagentConfig; -import org.noear.solon.ai.codecli.core.subagent.SubagentType; +maxSteps: 25 // 最大步数 +sessionWindowSize: 10 // 会话窗口 +tools: ALL // 所有工具 +``` + +**适用任务**: +- 复杂的多步骤任务 +- 跨模块任务协调 +- 涉及网络检索 +- 需要多种工具协作 -SubagentConfig config = new SubagentConfig(SubagentType.EXPLORE); -config.setMaxSteps(20); // 设置最大步数 -config.setDescription("自定义描述"); +--- + +### 5. SolonGuideSubagent - Solon 指南代理 + +**代码**: `org.noear.solon.bot.core.subagent.SolonGuideSubagent` + +**系统提示词**: ``` +## Solon 技术文档专家 -### 注册自定义 AgentPool +你是 Solon 技术专家,专注于 Solon 框架和 Agent SDK。 -可以注册自定义的 agentPool,让系统从多个目录发现子代理: +### 专属工具 +- `read_solon_doc`: 读取 Solon 官网文档(支持本地缓存) +- `list_solon_docs`: 列出所有可用的官方文档 +- `clear_solon_doc_cache`: 清除文档缓存 + +### 可用文档 +- learn-start: Solon 快速入门 +- agent-quick-start: Agent SDK 快速入门 +- agent-tools: Agent 工具开发 +- agent-skill: Agent 技能开发 +- ... + +### 使用流程 +1. 检查本地缓存 +2. 从官网获取最新文档 +3. 分析并解答用户问题 +``` +**配置**: ```java -// 获取 SubagentManager -SubagentManager manager = codeAgent.getSubagentManager(); +maxSteps: 15 // 最大步数 +sessionWindowSize: 5 // 会话窗口 +tools: read_solon_doc, list_solon_docs, lucene, grep +``` -// 注册自定义 agent 池 -manager.agentPool("@my_agents", "/path/to/my/agents"); -manager.agentPool("@shared_agents", "./shared/agents"); +**适用任务**: +- 查询 Solon 文档 +- 学习 Solon API +- 开发 Solon Agent 技能 -// 系统默认已注册以下池: -// - @soloncode_agents -> .soloncode/agents/ -// - @opencode_agents -> .opencode/agents/ -// - @claude_agents -> .claude/agents/ +--- + +## 最佳实践 + +### 1. 选择合适的子代理 + +```python +# 决策树 +if 任务类型 == "代码探索" or "文件搜索": + 子代理 = "explore" +elif 任务类型 == "方案设计" or "架构规划": + 子代理 = "plan" +elif 任务类型 == "命令执行" or "Git操作": + 子代理 = "bash" +elif 任务复杂度 == "高" or 需要多种工具: + 子代理 = "general-purpose" ``` -**AgentPool 搜索优先级:** -1. 自定义注册的 agentPool(按注册顺序) -2. `.opencode/agents/` -3. `.claude/agents/` -4. `.soloncode/agents/`(默认池) +### 2. 任务分解策略 -当创建子代理时,系统会按优先级搜索提示词文件。 +```python +# 复杂任务分解为多个子任务 -## 工具列表 +任务:实现用户认证功能 -Subagent 系统提供以下工具: +# 步骤1: 探索现有代码 +subagent(type="explore", prompt="探索认证相关代码") -| 工具名 | 描述 | -|--------|------| -| `subagent` | 启动指定类型的子代理执行任务 | -| `subagent_list` | 列出所有可用的子代理(包括预定义和自定义代理) | +# 步骤2: 设计方案 +subagent(type="plan", prompt="基于探索结果设计认证方案") -### subagent_list 工具详解 +# 步骤3: 实现 +subagent(type="general-purpose", prompt="按照设计实现认证功能") -`subagent_list` 工具会扫描所有已注册的 agentPools,动态发现并列出所有可用的子代理: +# 步骤4: 测试 +subagent(type="bash", prompt="运行测试") +``` -**输出格式:** +### 3. 继续之前的任务 + +```python +# 第一次调用 +response1 = subagent( + type="explore", + prompt="探索认证模块", + description="认证探索" +) +# 返回: task_id = "subagent_explore_1234567890" + +# 第二次调用(继续) +response2 = subagent( + type="explore", + prompt="继续深入分析", + taskId="subagent_explore_1234567890" # 使用 task_id 继续会话 +) ``` -可用的子代理: -【预定义子代理】 -- **explore** (EXPLORE): 快速探索代码库... -- **plan** (PLAN): 设计实现方案... -- **bash** (BASH): 执行命令... -- **general-purpose** (GENERAL_PURPOSE): 通用代理... +### 4. 错误处理 + +```python +# 子代理调用失败时的处理 -【自定义子代理】 -- **performance-tester** (来自 @soloncode_agents): 性能测试专家... -- **my-custom-agent** (来自 @opencode_agents): 我的自定义代理... +try: + response = subagent(type="explore", prompt="...") +except SubagentNotFoundException as e: + # 子代理不存在 + print(f"错误: 未知的子代理类型: {e.type}") + print(f"可用的子代理: {e.available_agents}") -提示:可以通过 .soloncode/agents/ 目录添加自定义子代理 +except AgentExecutionException as e: + # 子代理执行失败 + print(f"错误: 子代理执行失败: {e.message}") + print(f"堆栈跟踪: {e.stack_trace}") ``` -**特性:** -- 自动去重:如果自定义代理与预定义代理同名,只显示预定义版本 -- 显示来源:标注每个自定义代理来自哪个 agentPool -- 描述提取:从 MD 文件的第一行提取描述信息 -- 支持多池:扫描所有已注册的 agentPools 目录 +--- -## 配置文件位置 +## 配置选项 + +### 1. 全局配置 + +**config.yml**: +```yaml +solon: + code: + cli: + # 启用子代理 + subAgentEnabled: true + + # 子代理模型配置(可选) + subAgentModels: + explore: gpt-4 + plan: gpt-4 + bash: gpt-3.5 + general-purpose: gpt-4 +``` -Subagent 的提示词文件存储在项目根目录的 `.soloncode/agents/` 目录下: +### 2. 自定义子代理配置 +**Java 代码**: +```java +// 创建子代理时自定义配置 +SubagentMetadata metadata = new SubagentMetadata(); +metadata.setCode("my-agent"); +metadata.setModel("gpt-4"); +metadata.setMaxTurns(30); +metadata.setTools(Arrays.asList("read", "write", "grep")); ``` -项目根目录/ -├── .soloncode/ ← SolonCode 系统目录 -│ ├── agents/ ← Subagent 提示词文件 -│ │ ├── explore.md # 探索代理 -│ │ ├── plan.md # 计划代理 -│ │ ├── bash.md # Bash代理 -│ │ ├── general-purpose.md # 通用代理 -│ │ └── *.md # 用户自定义代理 -│ ├── sessions/ ← 会话历史 -│ └── skills/ ← 技能文件 -├── work/ ← 工作目录 -├── AGENTS.md ← 主 Agent 配置 -└── config.yml ← 全局配置 + +**YAML 文件**: +```yaml +--- +code: my-agent +model: gpt-4 +max_turns: 30 +tools: + - read + - write + - grep ``` -用户可以编辑 `.soloncode/agents/` 下的 MD 文件来自定义 Subagent 的行为,修改后重启即可生效。 +--- -## 与 Claude Code 的兼容性 +## 文件结构 -本 Subagent 系统设计上兼容 Claude Code 的子代理模式: +``` +.soloncode/ +├── agents/ # 子代理定义文件 +│ ├── explore.md +│ ├── plan.md +│ ├── bash.md +│ ├── general-purpose.md +│ ├── solon-guide.md +│ └── *.md # 用户自定义代理 +│ +├── sessions/ # 会话历史 +│ ├── subagent_explore_123/ +│ └── subagent_plan_456/ +│ +├── skills/ # 技能文件 +└── ... +``` -- 支持相同的子代理类型概念 -- 提供类似的工具调用接口 -- 可以通过 AGENTS.md 自定义子代理行为 +--- -## 测试示例 +## 测试和调试 -参考 `SubagentTest.java` 查看完整的使用示例: +### 运行测试 ```bash -# 运行子代理测试 +# 测试所有子代理 mvn test -Dtest=SubagentTest + +# 测试特定子代理 +mvn test -Dtest=SubagentTest#testExploreSubagent + +# 测试子代理调用 +mvn test -Dtest=AgentTeamsComprehensiveTest +``` + +### 调试技巧 + +**1. 查看子代理日志** +```java +// 设置日志级别 +LoggerContext.getLogger(SubagentManager.class.getName()).setLevel(Level.DEBUG); + +// 日志输出 +DEBUG: 分派任务 -> 类型: explore, 会话: subagent_explore_123 +INFO: 子代理任务完成: subagent_explore_123 ``` + +**2. 检查会话历史** +```bash +# 查看特定会话的历史 +ls .soloncode/sessions/subagent_explore_123/ +``` + +**3. 验证子代理注册** +```java +// 列出所有可用的子代理 +List agents = manager.getAgents(); +for (Subagent agent : agents) { + System.out.println(STR."- `\{agent.getType()}\`: \{agent.getDescription()}"); +} +``` + +--- + +## 高级特性 + +### 1. 任务链(Task Chaining) + +```java +// 前一个子代理的结果传递给下一个 +String exploreResult = manager.getAgent("explore") + .call(sessionId, Prompt.of("探索认证模块")) + .getContent(); + +String planResult = manager.getAgent("plan") + .call(sessionId, Prompt.of( + "基于探索结果设计认证方案\n\n" + + "现有代码:\n" + exploreResult + )) + .getContent(); +``` + +### 2. 流式执行 + +```java +// 流式获取子代理输出 +Flux chunks = manager.getAgent("explore") + .stream(__cwd, sessionId, Prompt.of("探索代码")); + +chunks.subscribe( + chunk -> System.out.println(chunk.getContent()), + error -> error.printStackTrace(), + () -> System.out.println("完成") +); +``` + +### 3. 性能优化 + +```java +// 使用对象池减少创建开销 +SubagentManager manager = new SubagentManager(10); // 缓存大小 + +// 预加载常用子代理 +manager.getAgent("explore"); // 预加载 +manager.getAgent("plan"); // 预加载 +``` + +--- + +## 常见问题 + +### Q1: 子代理调用失败 + +**问题**: `ERROR: 未知的子代理类型 'xxx'` + +**解决**: +1. 检查子代理类型名称是否正确 +2. 确认 `.soloncode/agents/` 下是否有对应的 `.md` 文件 +3. 使用 `subagent_list()` 查看所有可用子代理 + +### Q2: 子代理执行超时 + +**问题**: 调用子代理后长时间无响应 + +**解决**: +1. 检查子代理的 `maxSteps` 是否足够 +2. 简化任务提示,减少上下文 +3. 使用更快的模型 +4. 检查网络连接(如果涉及网络工具) + +### Q3: 子代理无法访问工具 + +**问题**: 子代理报告找不到工具 + +**解决**: +1. 确认工具已正确注册到 `AgentKernel` +2. 检查子代理的 `customize()` 方法 +3. 查看子代理的系统提示词中是否说明了工具使用策略 + +--- + +## 总结 + +SubAgent 系统通过专门的子代理提供强大的任务委派能力: + +| 特性 | 好处 | +|------|------| +| **类型专门化** | 每个子代理针对特定任务优化 | +| **独立会话** | 子代理有独立的会话空间,不会污染主对话 | +| **Token 节省** | 复杂任务委派给子代理,节省主对话上下文 | +| **可扩展** | 可以轻松添加新的子代理类型 | +| **兼容性** | 兼容 Claude Code 的子代理模式 | + +--- + +**作者**: bai +**版本**: 2.0 +**状态**: ✅ 完整 +**更新日期**: 2026-03-09 diff --git a/SUBAGENT_IMPLEMENTATION.md b/SUBAGENT_IMPLEMENTATION.md deleted file mode 100644 index 6f43a5653c06c6f3d80b06a2b36dd6e76b238877..0000000000000000000000000000000000000000 --- a/SUBAGENT_IMPLEMENTATION.md +++ /dev/null @@ -1,168 +0,0 @@ -# Subagent 子代理系统实现总结 - -## 概述 - -已成功实现类似 Claude Code 的 Subagent 子代理模式,允许主 Agent 调用专门的子代理来处理特定任务。 - -## 实现的文件 - -### 核心接口和类 - -1. **SubagentType.java** - 子代理类型枚举 - - `EXPLORE` - 快速探索代码库 - - `PLAN` - 软件架构师,设计实现计划 - - `BASH` - 命令执行专家 - - `GENERAL_PURPOSE` - 通用任务处理 - -2. **SubagentConfig.java** - 子代理配置类 - - 支持配置类型、描述、ChatModel、工作目录、最大步数等 - -3. **Subagent.java** - 子代理接口 - - 定义 `execute()` 和 `stream()` 方法 - -4. **AbstractSubagent.java** - 抽象子代理基类 - - 实现通用逻辑 - - 提供系统提示词构建框架 - -### 具体实现 - -5. **ExploreSubagent.java** - 探索代理 - - 专注于快速文件查找和代码结构理解 - - 使用 Glob、Grep、Read 工具 - - 只读操作,不修改代码 - -6. **PlanSubagent.java** - 计划代理 - - 软件架构师角色 - - 设计实现方案和执行计划 - - 提供结构化的输出格式 - -7. **BashSubagent.java** - 命令代理 - - 专注于终端命令执行 - - 只包含 Bash 工具 - - 适合 Git、构建、测试等场景 - -8. **GeneralPurposeSubagent.java** - 通用代理 - - 包含完整的工具集 - - 处理复杂的多步骤任务 - - 功能最全面的子代理 - -### 管理和工具 - -9. **SubagentManager.java** - 子代理管理器 - - 管理所有子代理的生命周期 - - 按需创建和缓存子代理实例 - - 提供统一的访问接口 - -10. **SubagentTool.java** - 子代理工具 - - 将子代理能力暴露为可调用工具 - - 提供 `subagent` 和 `subagent_list` 工具 - - 供主 Agent 调用 - -### 集成 - -11. **CodeAgent.java** - 修改以支持子代理 - - 添加 `enableSubagent()` 方法 - - 添加 `getSubagentManager()` 方法 - - 在 `prepare()` 中集成 SubagentTool - -### 测试 - -12. **SubagentTest.java** - 测试类 - - 包含各种子代理的使用示例 - - 演示如何调用不同类型的子代理 - -## 架构设计 - -``` -┌─────────────────────────────────────────────────────────┐ -│ 主 Agent (CodeAgent) │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ SubagentTool (工具) ││ -│ │ - subagent(type, prompt) ││ -│ │ - subagent_list() ││ -│ └─────────────────────────────────────────────────────┘│ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ SubagentManager ││ -│ │ ┌────────────┐ ┌────────────┐ ┌─────────────┐ ││ -│ │ │ Explore │ │ Plan │ │ Bash │ ││ -│ │ │ Agent │ │ Agent │ │ Agent │ ││ -│ │ └────────────┘ └────────────┘ └─────────────┘ ││ -│ │ ┌─────────────────────────────────────────────┐ ││ -│ │ │ GeneralPurpose Agent │ ││ -│ │ └─────────────────────────────────────────────┘ ││ -│ └─────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────┘ -``` - -## 使用方式 - -### 1. 启用子代理 - -```java -CodeAgent codeAgent = new CodeAgent(chatModel) - .enableSubagent(true) - .prepare(); -``` - -### 2. 主 Agent 调用 - -主 Agent 可以使用 `subagent` 工具: - -``` -用户:帮我探索项目的核心类 - -主 Agent 自动: -1. 识别任务类型 -2. 调用 subagent(type="explore", prompt="...") -3. 子代理执行并返回结果 -4. 主 Agent 将结果返回给用户 -``` - -### 3. 直接调用 - -```java -SubagentManager manager = codeAgent.getSubagentManager(); -var response = manager.getAgent(SubagentType.EXPLORE) - .execute(Prompt.of("探索项目结构")); -``` - -## 特点 - -1. **类型专门化** - 不同子代理针对不同任务优化 -2. **按需创建** - SubagentManager 按需创建并缓存子代理 -3. **独立会话** - 每个子代理有独立的会话空间 -4. **工具限制** - 根据任务类型提供合适的工具集 -5. **步数控制** - 不同子代理有不同的步数限制 -6. **兼容性** - 设计上兼容 Claude Code 的子代理模式 - -## 配置 - -子代理的默认配置: - -| 类型 | 最大步数 | 会话窗口 | 主要工具 | -|------|----------|----------|----------| -| explore | 15 | 5 | Glob, Grep, Read | -| plan | 20 | 8 | 只读工具 | -| bash | 10 | 3 | Bash | -| general-purpose | 25 | 10 | 所有工具 | - -## 文档 - -- `SUBAGENT.md` - 详细的使用指南 -- `CLAUDE.md` - 已更新,包含子代理说明 -- `SubagentTest.java` - 使用示例 - -## 后续扩展 - -可以继续添加的子代理类型: - -1. **RefactorAgent** - 专门处理代码重构 -2. **TestAgent** - 专门生成和运行测试 -3. **DocAgent** - 专门生成文档 -4. **DebugAgent** - 专门调试问题 - -## 总结 - -Subagent 系统为 Solon Code 提供了强大的任务委派能力,让主 Agent 可以将特定任务交给专门的子代理处理,提高了整体的任务处理效率和质量。 diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentKernel.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentKernel.java index 88a2b42bf2946be08d8469d49356ebc3593cbe12..01502798e1c6a38614f9c64aa199afabc40c65f3 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentKernel.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentKernel.java @@ -1,6 +1,7 @@ package org.noear.solon.bot.core; import com.microsoft.playwright.Playwright; +import lombok.Getter; import org.noear.solon.ai.agent.AgentChunk; import org.noear.solon.ai.agent.AgentResponse; import org.noear.solon.ai.agent.AgentSession; @@ -13,10 +14,19 @@ import org.noear.solon.ai.agent.react.intercept.SummarizationStrategy; import org.noear.solon.ai.agent.react.intercept.summarize.*; import org.noear.solon.ai.chat.ChatModel; import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.bot.core.event.EventBus; +import org.noear.solon.bot.core.goalker.GoalKeeperIntegration; +import org.noear.solon.bot.core.memory.SharedMemoryManager; +import org.noear.solon.bot.core.message.MessageChannel; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; import org.noear.solon.ai.skills.restapi.RestApiSkill; import org.noear.solon.bot.core.config.ApiServerParameters; import org.noear.solon.bot.core.subagent.SubagentManager; import org.noear.solon.bot.core.subagent.TaskSkill; +import org.noear.solon.bot.core.teams.AgentTeamsSkill; +import org.noear.solon.bot.core.teams.MainAgent; +import org.noear.solon.bot.core.teams.SharedTaskList; +import org.noear.solon.bot.core.teams.TeamNameSuggestionTool; import org.noear.solon.bot.core.tool.ApplyPatchTool; import org.noear.solon.bot.core.tool.CodeSearchTool; import org.noear.solon.bot.core.tool.WebfetchTool; @@ -49,6 +59,7 @@ import java.util.function.Consumer; * @since 3.9.1 */ @Preview("3.9.1") +@Getter public class AgentKernel { private final static Logger LOG = LoggerFactory.getLogger(AgentKernel.class); @@ -58,15 +69,16 @@ public class AgentKernel { public final static String SOLONCODE_SESSIONS = ".soloncode/sessions/"; public final static String SOLONCODE_SKILLS = ".soloncode/skills/"; public final static String SOLONCODE_AGENTS = ".soloncode/agents/"; + public final static String SOLONCODE_AGENTS_TEAMS = ".soloncode/agentsTeams/"; public final static String SOLONCODE_DOWNLOADS = ".soloncode/downloads/"; public final static String SOLONCODE_BROWSER = ".soloncode/browser/"; + public final static String SOLONCODE_MEMORY = ".soloncode/memory/"; public final static String OPENCODE_SKILLS = ".opencode/skills/"; public final static String OPENCODE_AGENTS = ".opencode/agents/"; public final static String CLAUDE_SKILLS = ".claude/skills/"; public final static String CLAUDE_AGENTS = ".claude/agents/"; - private final ChatModel chatModel; private final AgentSessionProvider sessionProvider; private final AgentProperties properties; @@ -86,6 +98,13 @@ public class AgentKernel { private SubagentManager subagentManager; + // Agent Teams 相关组件 + private MainAgent mainAgent; + private EventBus eventBus; + private SharedTaskList taskList; + private SharedMemoryManager memoryManager; + private MessageChannel messageChannel; + public String getVersion() { return "v0.0.22"; } @@ -185,11 +204,16 @@ public class AgentKernel { subagentManager.agentPool(Paths.get(properties.getWorkDir(), AgentKernel.OPENCODE_AGENTS)); // 注册 claude agents subagentManager.agentPool(Paths.get(properties.getWorkDir(), AgentKernel.CLAUDE_AGENTS)); + // 注册 soloncode agentsTeams(递归扫描团队成员目录) + subagentManager.agentPool(Paths.get(properties.getWorkDir(), AgentKernel.SOLONCODE_AGENTS_TEAMS), true); // SubagentSkill 会通过 @ToolMapping 自动注册为工具 agentBuilder.defaultSkillAdd(new TaskSkill(this, subagentManager)); LOG.info("子代理模式已启用"); } + if (properties.isAgentTeamEnabled()){ + initAgentTeams(properties, agentBuilder); + } //上下文摘要 SummarizationStrategy strategy = new CompositeSummarizationStrategy() @@ -234,20 +258,75 @@ public class AgentKernel { reActAgent = agentBuilder.build(); } - public ChatModel getChatModel() { - return chatModel; - } - - - public CliSkillProvider getCliSkills() { - return cliSkills; - } /** - * 获取子代理管理器 + * 初始化 Agent Teams 模式 */ - public SubagentManager getSubagentManager() { - return subagentManager; + private void initAgentTeams(AgentProperties properties, ReActAgent.Builder agentBuilder) { + try { + LOG.info("正在初始化 Agent Teams 模式..."); + + // 1. 创建 EventBus(事件总线) + this.eventBus = new EventBus(); + LOG.debug("EventBus 已创建"); + + // 2. 创建 SharedTaskList(共享任务列表) + this.taskList = new SharedTaskList(eventBus); + LOG.debug("SharedTaskList 已创建"); + + // 3. 创建 SharedMemoryManager(共享内存管理器) + Path memoryPath = Paths.get(properties.getWorkDir(), SOLONCODE_MEMORY); + this.memoryManager = new SharedMemoryManager(memoryPath); + LOG.debug("SharedMemoryManager 已创建,路径: {}", memoryPath); + + // 4. 创建 MessageChannel(消息通道) + Path messagePath = Paths.get(properties.getWorkDir(), SOLONCODE_MEMORY); + this.messageChannel = new MessageChannel(messagePath.toString()); + LOG.debug("MessageChannel 已创建,路径: {}", messagePath); + + // 5. 创建 MainAgent 配置 + SubAgentMetadata mainAgentConfig = new SubAgentMetadata(); + mainAgentConfig.setCode("main-agent"); + mainAgentConfig.setName("主代理"); + mainAgentConfig.setDescription("Agent Teams 协调器,负责任务分解和团队协作"); + mainAgentConfig.setEnabled(true); + + // 6. 创建 MainAgent(传入 kernel 和 subagentManager 以支持 subagent 功能) + this.mainAgent = new MainAgent( + mainAgentConfig, + sessionProvider, + memoryManager, + eventBus, + messageChannel, + taskList, + properties.getWorkDir(), + cliSkills.getPoolManager(), + this, // AgentKernel + subagentManager // SubagentManager + ); + LOG.debug("MainAgent 已创建"); + + // 5.1 初始化 MainAgent(需要传入 ChatModel) + this.mainAgent.initialize(chatModel); + LOG.debug("MainAgent 已初始化"); + + // 6. 创建 AgentTeamsSkill 并注册到主 Agent + AgentTeamsSkill agentTeamsSkill = new AgentTeamsSkill( + mainAgent, + this, // AgentKernel + subagentManager + ); + agentBuilder.defaultSkillAdd(agentTeamsSkill); + agentBuilder.defaultToolAdd(new TeamNameSuggestionTool(subagentManager)); + + LOG.info("AgentTeamsSkill 已注册"); + + LOG.info("Agent Teams 模式初始化完成 [OK]"); + + } catch (Throwable e) { + LOG.error("Agent Teams 模式初始化失败", e); + throw new RuntimeException("Failed to initialize Agent Teams mode", e); + } } @@ -327,4 +406,5 @@ public class AgentKernel { return buildRequest(sessionId, prompt) .call(); } + } \ No newline at end of file diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentProperties.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentProperties.java index d895160d82d31132c7bf6c695f48e29a16e24c0c..20c51cfcaf6f8b9d290a0fef53ecd07db7380054 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentProperties.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/AgentProperties.java @@ -48,6 +48,7 @@ public class AgentProperties implements Serializable { private boolean hitlEnabled = false; private boolean subagentEnabled = true; + private boolean agentTeamEnabled = true; private boolean browserEnabled = true; private boolean cliEnabled = true; @@ -60,11 +61,127 @@ public class AgentProperties implements Serializable { private String acpTransport = "stdio"; private String acpEndpoint = "/acp"; + /** + * 共享记忆配置 + */ + public boolean sharedMemoryEnabled = false; + public SharedMemoryConfig sharedMemory = new SharedMemoryConfig(); + + /** + * 事件总线配置 + */ + public boolean eventBusEnabled = false; + public EventBusConfig eventBus = new EventBusConfig(); + + /** + * 消息通道配置 + */ + public boolean messageChannelEnabled = false; + public MessageChannelConfig messageChannel = new MessageChannelConfig(); + + /** + * Agent Teams 模式配置 + */ + public boolean teamsEnabled = false; + + public Map mcpServers; + public ChatConfig chatModel; + + /** + * SubAgent 模型配置 + * 格式:subAgentCode -> modelName + * 例如:{"explore": "glm-4-flash", "plan": "glm-4.7"} + * 如果未配置,将使用默认的 chatModel.model + */ + public Map subAgentModels; private Map restApis; - private Map mcpServers; - private ChatConfig chatModel; @Deprecated private Map mountPool; private Map skillPools; + + /** + * 共享记忆配置类 + */ + public static class SharedMemoryConfig { + /** + * 短期记忆TTL(毫秒,默认1小时) + */ + public long shortTermTtl = 3600_000L; + + /** + * 长期记忆TTL(毫秒,默认7天) + */ + public long longTermTtl = 7 * 24 * 3600_000L; + + /** + * 清理间隔(毫秒,默认5分钟) + */ + public long cleanupInterval = 300_000L; + + /** + * 写入时立即持久化 + */ + public boolean persistOnWrite = true; + + /** + * 短期记忆最大数量 + */ + public int maxShortTermCount = 1000; + + /** + * 长期记忆最大数量 + */ + public int maxLongTermCount = 500; + } + + /** + * 事件总线配置类 + */ + public static class EventBusConfig { + /** + * 异步处理线程数(默认CPU核心数) + */ + public Integer asyncThreads; + + /** + * 事件历史最大数量 + */ + public int maxHistorySize = 1000; + + /** + * 默认优先级(0-10) + */ + public int defaultPriority = 5; + + /** + * 处理超时时间(秒) + */ + public int timeoutSeconds = 30; + } + + /** + * 消息通道配置类 + */ + public static class MessageChannelConfig { + /** + * 处理线程数 + */ + public Integer threads; + + /** + * 默认消息TTL(毫秒,默认60秒) + */ + public long defaultTtl = 60_000L; + + /** + * 每个代理的最大队列长度 + */ + public int maxQueueSize = 1000; + + /** + * 是否持久化消息 + */ + public boolean persistMessages = true; + } } diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/AgentEvent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/AgentEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..c7b8611696b886f1f529c925a28ca15331da2196 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/AgentEvent.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.event; + +import lombok.Getter; + +import java.util.UUID; + +/** + * 事件对象 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class AgentEvent { + private final String eventId; + private final AgentEventType eventType; + private final String customEventTypeCode; // 用于自定义事件类型 + private final Object payload; + private final EventMetadata metadata; + private final long timestamp; + + /** + * 使用枚举类型创建事件 + */ + public AgentEvent(AgentEventType eventType, Object payload, EventMetadata metadata) { + this.eventId = UUID.randomUUID().toString(); + this.eventType = eventType; + this.customEventTypeCode = eventType.getCode(); + this.payload = payload; + this.metadata = metadata; + this.timestamp = System.currentTimeMillis(); + } + + /** + * 使用自定义事件类型代码创建事件 + */ + public AgentEvent(String customEventTypeCode, Object payload, EventMetadata metadata) { + this.eventId = UUID.randomUUID().toString(); + this.eventType = AgentEventType.CUSTOM; + this.customEventTypeCode = customEventTypeCode; + this.payload = payload; + this.metadata = metadata; + this.timestamp = System.currentTimeMillis(); + } + + + /** + * 获取事件类型代码(向后兼容) + */ + public String getEventTypeCode() { + if (eventType == AgentEventType.CUSTOM && customEventTypeCode != null) { + return customEventTypeCode; + } + return eventType.getCode(); + } + + + @Override + public String toString() { + return "AgentEvent{" + + "eventId='" + eventId + '\'' + + ", eventType='" + getEventTypeCode() + '\'' + + ", payload=" + payload + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/AgentEventType.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/AgentEventType.java new file mode 100644 index 0000000000000000000000000000000000000000..bcfc2f4f2b202454b8e8ff50b1199aa97c1c4504 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/AgentEventType.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.event; + +import lombok.Getter; + +/** + * 预定义事件类型枚举 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public enum AgentEventType { + // ========== 主代理任务事件 ========== + /** 主代理任务开始 */ + MAIN_TASK_STARTED("main.task.started"), + /** 主代理任务完成 */ + MAIN_TASK_COMPLETED("main.task.completed"), + /** 主代理任务失败 */ + MAIN_TASK_FAILED("main.task.failed"), + + // ========== 子任务事件 ========== + /** 任务创建 */ + TASK_CREATED("task.created"), + /** 任务开始 */ + TASK_STARTED("task.started"), + /** 任务完成 */ + TASK_COMPLETED("task.completed"), + /** 任务失败 */ + TASK_FAILED("task.failed"), + /** 任务进度更新 */ + TASK_PROGRESS("task.progress"), + /** 任务认领 */ + TASK_CLAIMED("task.claimed"), + /** 任务释放 */ + TASK_RELEASED("task.released"), + + // ========== 记忆事件 ========== + /** 记忆已存储 */ + MEMORY_STORED("memory.stored"), + /** 记忆已检索 */ + MEMORY_RETRIEVED("memory.retrieved"), + + // ========== 代理事件 ========== + /** 代理已初始化 */ + AGENT_INITIALIZED("agent.initialized"), + /** 代理错误 */ + AGENT_ERROR("agent.error"), + + // ========== 自定义事件 ========== + /** 自定义事件类型(用于未预定义的事件) */ + CUSTOM(""); + + private final String code; + + AgentEventType(String code) { + this.code = code; + } + + /** + * 根据代码获取枚举 + */ + public static AgentEventType fromCode(String code) { + for (AgentEventType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("未知的事件类型: " + code); + } + + /** + * 从代码获取枚举,如果未找到则返回 null + * 对于自定义事件类型,请直接使用 String 订阅/发布 + */ + public static AgentEventType fromCodeOrNull(String code) { + for (AgentEventType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + return null; // 未找到,表示这是一个自定义事件类型 + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventBus.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventBus.java new file mode 100644 index 0000000000000000000000000000000000000000..47f8da92640708ef6fc4ad7aa48bfe44e8c1b62c --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventBus.java @@ -0,0 +1,361 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * 事件总线 + * + * 负责事件的发布、订阅、路由和分发 + * + * @author bai + * @since 3.9.5 + */ +public class EventBus { + private static final Logger LOG = LoggerFactory.getLogger(EventBus.class); + + // 订阅者注册表:eventType -> handlers + private final Map> subscribers = new ConcurrentHashMap<>(); + + // 自定义事件类型订阅表:customEventTypeCode -> handlers + private final Map> customSubscribers = new ConcurrentHashMap<>(); + + // 异步执行器 + private final ExecutorService executor; + + // 事件历史(用于调试,可配置) + private final Queue eventHistory; + private final int maxHistorySize; + + /** + * 构造函数(使用默认配置) + */ + public EventBus() { + this(Runtime.getRuntime().availableProcessors(), 1000); + } + + /** + * 完整构造函数 + * + * @param asyncThreads 异步处理线程数 + * @param maxHistorySize 事件历史最大数量 + */ + public EventBus(int asyncThreads, int maxHistorySize) { + this.executor = Executors.newFixedThreadPool(asyncThreads, r -> { + Thread t = new Thread(r, "EventBus-Thread"); + t.setDaemon(true); + return t; + }); + this.maxHistorySize = maxHistorySize; + this.eventHistory = new LinkedList<>(); + + LOG.info("事件总线初始化: asyncThreads={}, maxHistorySize={}", asyncThreads, maxHistorySize); + } + + /** + * 订阅事件(枚举类型) + * + * @param eventType 事件类型(支持通配符 *) + * @param handler 事件处理器 + * @return 订阅ID(用于取消订阅) + */ + public String subscribe(AgentEventType eventType, EventHandler handler) { + String subscriptionId = UUID.randomUUID().toString(); + + EventHandlerWrapper wrapper = new EventHandlerWrapper(subscriptionId, eventType, handler); + + subscribers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()) + .add(wrapper); + + LOG.debug("新订阅: eventType={}, subscriptionId={}", eventType, subscriptionId); + return subscriptionId; + } + + /** + * 订阅事件(自定义字符串类型) + * + * @param customEventTypeCode 自定义事件类型代码(支持通配符 *) + * @param handler 事件处理器 + * @return 订阅ID(用于取消订阅) + */ + public String subscribe(String customEventTypeCode, EventHandler handler) { + String subscriptionId = UUID.randomUUID().toString(); + + EventHandlerWrapper wrapper = new EventHandlerWrapper(subscriptionId, customEventTypeCode, handler); + + customSubscribers.computeIfAbsent(customEventTypeCode, k -> new CopyOnWriteArrayList<>()) + .add(wrapper); + + LOG.debug("新订阅: customEventType={}, subscriptionId={}", customEventTypeCode, subscriptionId); + return subscriptionId; + } + + /** + * 取消订阅 + * + * @param subscriptionId 订阅ID + */ + public void unsubscribe(String subscriptionId) { + // 从枚举类型订阅中移除 + subscribers.values().forEach(handlers -> + handlers.removeIf(wrapper -> wrapper.getSubscriptionId().equals(subscriptionId)) + ); + + // 从自定义类型订阅中移除 + customSubscribers.values().forEach(handlers -> + handlers.removeIf(wrapper -> wrapper.getSubscriptionId().equals(subscriptionId)) + ); + + LOG.debug("取消订阅: subscriptionId={}", subscriptionId); + } + + /** + * 发布事件(异步) + * + * @param event 事件对象 + * @return CompletableFuture + */ + public CompletableFuture publishAsync(AgentEvent event) { + return CompletableFuture.runAsync(() -> publish(event), executor); + } + + /** + * 发布事件(同步) + * + * @param event 事件对象 + */ + public void publish(AgentEvent event) { + try { + // 1. 记录历史 + recordHistory(event); + + // 2. 查找匹配的订阅者(枚举 + 自定义) + List matchedHandlers = findHandlers(event); + + if (matchedHandlers.isEmpty()) { + LOG.debug("无订阅者处理事件: eventType={}", event.getEventTypeCode()); + return; + } + + // 4. 分发事件(异步) + + // 5. 等待所有处理完成(带超时) + CompletableFuture.allOf(matchedHandlers.stream() + .map(wrapper -> wrapper.handle(event)).toArray(CompletableFuture[]::new)) + .whenComplete((v, ex) -> { + if (ex != null) { + LOG.warn("事件处理超时或失败: eventType={}, error={}", + event.getEventType(), ex.getMessage()); + } + }); + + LOG.debug("事件已发布: eventType={}, handlers={}", + event.getEventType(), matchedHandlers.size()); + + } catch (Exception e) { + LOG.error("事件发布失败: eventType={}, error={}", + event.getEventType(), e.getMessage(), e); + } + } + + /** + * 获取事件历史 + * + * @param limit 最大数量 + * @return 事件列表 + */ + public List getEventHistory(int limit) { + synchronized (eventHistory) { + return eventHistory.stream() + .limit(limit) + .collect(Collectors.toList()); + } + } + + /** + * 获取订阅者数量 + * + * @return 订阅者数量 + */ + public int getSubscriberCount() { + int enumCount = subscribers.values().stream() + .mapToInt(List::size) + .sum(); + + int customCount = customSubscribers.values().stream() + .mapToInt(List::size) + .sum(); + + return enumCount + customCount; + } + + /** + * 获取指定事件类型的订阅者数量 + * + * @param eventType 事件类型 + * @return 订阅者数量 + */ + public int getSubscriberCount(AgentEventType eventType) { + List handlers = subscribers.get(eventType); + return handlers != null ? handlers.size() : 0; + } + + /** + * 清空所有订阅 + */ + public void clearSubscribers() { + subscribers.clear(); + customSubscribers.clear(); + LOG.info("所有订阅已清空"); + } + + /** + * 关闭事件总线 + */ + public void shutdown() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + LOG.info("事件总线已关闭"); + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // ========== 私有方法 ========== + + /** + * 查找匹配的处理器(支持枚举和自定义字符串事件) + */ + private List findHandlers(AgentEvent event) { + List handlers = new ArrayList<>(); + + // 1. 如果是枚举类型事件,查找枚举订阅者 + if (event.getEventType() != AgentEventType.CUSTOM) { + AgentEventType eventType = event.getEventType(); + + // 精确匹配 + handlers.addAll(subscribers.getOrDefault(eventType, Collections.emptyList())); + + // 通配符匹配(例如: "task.*" 匹配 "task.started") + subscribers.entrySet().stream() + .filter(entry -> isWildcardMatch(entry.getKey().getCode(), eventType.getCode())) + .forEach(entry -> handlers.addAll(entry.getValue())); + } + + // 2. 如果是自定义事件类型,查找自定义订阅者 + String eventTypeCode = event.getEventTypeCode(); + if (event.getEventType() == AgentEventType.CUSTOM || event.getCustomEventTypeCode() != null) { + // 精确匹配 + handlers.addAll(customSubscribers.getOrDefault(eventTypeCode, Collections.emptyList())); + + // 通配符匹配(例如: "task.*" 匹配 "task.completed") + customSubscribers.entrySet().stream() + .filter(entry -> isWildcardMatch(entry.getKey(), eventTypeCode)) + .forEach(entry -> handlers.addAll(entry.getValue())); + } + + return handlers; + } + + /** + * 通配符匹配 + */ + private boolean isWildcardMatch(String pattern, String eventTypeCode) { + if (pattern.equals("*")) { + return true; + } + + if (pattern.endsWith(".*")) { + String prefix = pattern.substring(0, pattern.length() - 2); + return eventTypeCode.startsWith(prefix + "."); + } + + return false; + } + + /** + * 记录事件历史 + */ + private void recordHistory(AgentEvent event) { + synchronized (eventHistory) { + eventHistory.offer(event); + while (eventHistory.size() > maxHistorySize) { + eventHistory.poll(); + } + } + } + + /** + * 事件处理器包装器 + */ + private static class EventHandlerWrapper { + private final String subscriptionId; + private final AgentEventType eventType; // 枚举类型(可能为 null) + private final String customEventTypeCode; // 自定义事件类型代码(可能为 null) + private final EventHandler handler; + + // 构造函数:枚举类型 + public EventHandlerWrapper(String subscriptionId, AgentEventType eventType, EventHandler handler) { + this.subscriptionId = subscriptionId; + this.eventType = eventType; + this.customEventTypeCode = null; + this.handler = handler; + } + + // 构造函数:自定义字符串类型 + public EventHandlerWrapper(String subscriptionId, String customEventTypeCode, EventHandler handler) { + this.subscriptionId = subscriptionId; + this.eventType = null; + this.customEventTypeCode = customEventTypeCode; + this.handler = handler; + } + + public CompletableFuture handle(AgentEvent event) { + try { + return handler.handle(event); + } catch (Exception e) { + LOG.error("事件处理器异常: subscriptionId={}, error={}", + subscriptionId, e.getMessage(), e); + return CompletableFuture.completedFuture( + EventHandler.Result.failure(e.getMessage()) + ); + } + } + + public String getSubscriptionId() { + return subscriptionId; + } + + public AgentEventType getEventType() { + return eventType; + } + + public EventHandler getHandler() { + return handler; + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventHandler.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..c13cbdc4e77eb8be90ccb5f11a778aac0183d2dc --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.event; + +import java.util.concurrent.CompletableFuture; + +/** + * 事件处理器接口 + * + * @author bai + * @since 3.9.5 + */ +@FunctionalInterface +public interface EventHandler { + /** + * 处理事件 + * + * @param event 事件对象 + * @return 处理结果(可选,用于链式处理) + */ + CompletableFuture handle(AgentEvent event); + + /** + * 处理结果 + */ + class Result { + private final boolean success; + private final String message; + private final Object data; + + public Result(boolean success, String message, Object data) { + this.success = success; + this.message = message; + this.data = data; + } + + public static Result success() { + return new Result(true, null, null); + } + + public static Result success(String message) { + return new Result(true, message, null); + } + + public static Result success(String message, Object data) { + return new Result(true, message, data); + } + + public static Result failure(String message) { + return new Result(false, message, null); + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + + public Object getData() { + return data; + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventMetadata.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..b1b93d223cd461164bab87e8e5f7b36ad7cc0944 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/event/EventMetadata.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.event; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * 事件元数据 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class EventMetadata { + private final String sourceAgent; // 来源代理ID + private final String taskId; // 关联任务ID + private final int priority; // 优先级 (0-10, 默认5) + private final Map headers; // 扩展头 + + private EventMetadata(String sourceAgent, String taskId, int priority, Map headers) { + this.sourceAgent = sourceAgent; + this.taskId = taskId; + this.priority = priority; + this.headers = headers; + } + + /** + * 创建 Builder + */ + public static Builder builder() { + return new Builder(); + } + + public String getHeader(String key) { + return headers.get(key); + } + + /** + * Builder 模式 + */ + public static class Builder { + private String sourceAgent; + private String taskId; + private int priority = 5; + private Map headers = new HashMap<>(); + + public Builder sourceAgent(String sourceAgent) { + this.sourceAgent = sourceAgent; + return this; + } + + public Builder taskId(String taskId) { + this.taskId = taskId; + return this; + } + + public Builder priority(int priority) { + this.priority = Math.max(0, Math.min(10, priority)); + return this; + } + + public Builder header(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder headers(Map headers) { + if (headers != null) { + this.headers.putAll(headers); + } + return this; + } + + public EventMetadata build() { + return new EventMetadata(sourceAgent, taskId, priority, headers); + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalAwareSystemPrompt.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalAwareSystemPrompt.java new file mode 100644 index 0000000000000000000000000000000000000000..9b65613725bdef1c398177d0e4ed70f6e2f71666 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalAwareSystemPrompt.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.goalker; + +import org.noear.solon.ai.agent.react.ReActSystemPrompt; +import org.noear.solon.ai.agent.react.ReActTrace; +import org.noear.solon.bot.core.memory.WorkingMemory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +/** + * 目标感知系统提示词 - 防止目标漂移 + * + * 在原始系统提示词基础上,自动注入: + * 1. 目标锚定机制 + * 2. 定期目标提醒 + * 3. 进度跟踪 + * + * @author bai + * @since 3.9.5 + */ +public class GoalAwareSystemPrompt implements ReActSystemPrompt { + private static final Logger LOG = LoggerFactory.getLogger(GoalAwareSystemPrompt.class); + + private final ReActSystemPrompt delegate; + private final GoalKeeper goalKeeper; + + public GoalAwareSystemPrompt(ReActSystemPrompt delegate, GoalKeeper goalKeeper) { + this.delegate = delegate; + this.goalKeeper = goalKeeper; + } + + @Override + public Locale getLocale() { + return delegate != null ? delegate.getLocale() : Locale.CHINESE; + } + + @Override + public String getSystemPrompt(ReActTrace trace) { + // 获取原始系统提示词 + String basePrompt = delegate.getSystemPrompt(trace); + + // 注入目标锚定信息 + return injectGoalAnchoring(basePrompt, trace); + } + + /** + * 注入目标锚定信息 + */ + private String injectGoalAnchoring(String basePrompt, ReActTrace trace) { + StringBuilder sb = new StringBuilder(basePrompt); + + // 1. 在系统提示词开头添加目标信息 + sb.insert(0, buildGoalHeader()); + + // 2. 在系统提示词末尾添加目标提醒机制 + sb.append("\n\n"); + sb.append(buildGoalReminderInstructions()); + + return sb.toString(); + } + + /** + * 构建目标头部信息 + */ + private String buildGoalHeader() { + return "## 🎯 当前任务目标\n\n" + + "原始目标: " + goalKeeper.getOriginalGoal() + "\n" + + "目标ID: " + goalKeeper.getGoalId() + "\n\n" + + "⚠️ 重要: 在整个任务过程中,请始终牢记这个目标。如果发现自己偏离了目标,请立即调整方向。\n\n"; + } + + /** + * 构建目标提醒指令 + */ + private String buildGoalReminderInstructions() { + return "## 🔄 目标保持机制\n\n" + + "1. **每 5 步自检**: 在执行第 5、10、15、20、25 步时,停下来问自己:\n" + + " - 我当前的操作是否与原始目标一致?\n" + + " - 我是否在做一些无关紧要的事情?\n" + + " - 我是否应该给出最终答案了?\n\n" + + "2. **避免过度执行**: 不要为了\"完美\"而过度执行。如果核心目标已完成,请立即给出 Final Answer。\n\n" + + "3. **保持专注**: 如果发现自己在循环调用相同的工具,请停下来思考是否有更好的方法。\n\n" + + "记住:用户的核心需求是 \"" + goalKeeper.getOriginalGoal() + "\",所有操作都应该围绕这个目标展开。\n"; + } + + /** + * 获取当前步数的目标提醒 + */ + public String getStepReminder(int currentStep) { + return goalKeeper.injectReminder(currentStep); + } + + /** + * 更新到工作记忆 + */ + public void updateWorkingMemory(WorkingMemory workingMemory) { + goalKeeper.updateWorkingMemory(workingMemory); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalKeeper.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalKeeper.java new file mode 100644 index 0000000000000000000000000000000000000000..7a054c9f6713b6794ef7765abfcab1a2ea48cd94 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalKeeper.java @@ -0,0 +1,145 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.goalker; + +import lombok.Getter; +import org.noear.solon.bot.core.memory.WorkingMemory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 目标守护者 - 防止 AI 在多轮循环中偏离目标 + * + * 核心功能: + * 1. 持续在上下文中强化原始目标 + * 2. 定期提醒 AI 当前目标和进度 + * 3. 检测目标漂移并警告 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class GoalKeeper { + private static final Logger LOG = LoggerFactory.getLogger(GoalKeeper.class); + + private final String goalId; + private final String originalGoal; + private final long startTime; + private int reminderCount = 0; + + // 配置 + private static final int REMINDER_INTERVAL = 5; // 每 5 步提醒一次 + private static final int DRIFT_THRESHOLD = 10; // 10 步后强制提醒 + + public GoalKeeper(String goalId, String originalGoal) { + this.goalId = goalId; + this.originalGoal = originalGoal; + this.startTime = System.currentTimeMillis(); + LOG.info("目标守护者已创建: goal={}, 目标={}", goalId, originalGoal); + } + + /** + * 在 ReAct 循环中注入目标提醒 + * + * @param currentStep 当前步数 + * @return 目标提醒文本 + */ + public String injectReminder(int currentStep) { + // 每 N 步提醒一次 + if (currentStep > 0 && currentStep % REMINDER_INTERVAL == 0) { + reminderCount++; + String reminder = buildReminder(currentStep); + LOG.debug("注入目标提醒 [第{}次]: step={}", reminderCount, currentStep); + return reminder; + } + + // 超过阈值后,每次都提醒 + if (currentStep > DRIFT_THRESHOLD) { + return buildUrgentReminder(currentStep); + } + + return null; // 不需要提醒 + } + + /** + * 构建常规提醒 + */ + private String buildReminder(int currentStep) { + long elapsed = System.currentTimeMillis() - startTime; + double elapsedMinutes = elapsed / 60000.0; + + return String.format("\n" + + "=== 🎯 目标提醒 (第 %d 步,用时 %.1f 分钟) ===\n" + + "原始目标: %s\n" + + "当前进度: 第 %d / %d 步\n" + + "提示: 请确保当前操作与原始目标保持一致,如果已经偏离,请立即调整方向。\n" + + "====================\n", + currentStep, elapsedMinutes, originalGoal, + currentStep, currentStep + 5 + ); + } + + /** + * 构建紧急提醒 + */ + private String buildUrgentReminder(int currentStep) { + return String.format("\n" + + "=== ⚠️ 警告:可能已偏离目标 (第 %d 步) ===\n" + + "原始目标: %s\n" + + "重要提示: 你已经执行了 %d 步,请停下来检查:\n" + + " 1. 我是否还在为原始目标工作?\n" + + " 2. 我是否在做无关紧要的操作?\n" + + " 3. 我是否应该给出最终答案了?\n" + + "如果以上任何一个答案是肯定的,请立即给出 Final Answer。\n" + + "=========================================\n", + currentStep, originalGoal, currentStep + ); + } + + /** + * 检查是否应该强制结束 + * + * @param currentStep 当前步数 + * @param maxSteps 最大步数 + * @return 是否应该结束 + */ + public boolean shouldForceFinish(int currentStep, int maxSteps) { + if (currentStep >= maxSteps) { + LOG.warn("达到最大步数限制 ({}), 建议结束", maxSteps); + return true; + } + return false; + } + + /** + * 获取目标摘要(用于初始化时存储) + */ + public String getGoalSummary() { + return String.format("目标: %s (ID: %s)", originalGoal, goalId); + } + + /** + * 更新到工作记忆 + */ + public void updateWorkingMemory(WorkingMemory workingMemory) { + if (workingMemory != null) { + workingMemory.setTaskDescription(originalGoal); + workingMemory.putMetadata("goalId", goalId); + workingMemory.putMetadata("startTime", String.valueOf(startTime)); + } + } + +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalKeeperIntegration.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalKeeperIntegration.java new file mode 100644 index 0000000000000000000000000000000000000000..57506262298116dc2e8492280dcfdcc9a51c4bf7 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/goalker/GoalKeeperIntegration.java @@ -0,0 +1,201 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.goalker; + +import org.noear.solon.ai.agent.AgentChunk; +import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.memory.WorkingMemory; +import org.noear.solon.bot.core.teams.MainAgent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * 目标守护者集成工具 + * + * 集成 GoalKeeper 到 AgentKernel,提供简单易用的 API + * + * @author bai + * @since 3.9.5 + */ +public class GoalKeeperIntegration { + private static final Logger LOG = LoggerFactory.getLogger(GoalKeeperIntegration.class); + + private final MainAgent mainAgent; + private GoalKeeper currentGoalKeeper; + + public GoalKeeperIntegration(MainAgent mainAgent) { + this.mainAgent = mainAgent; + } + + /** + * 启动目标守护 + * + * @param userPrompt 用户的目标提示词 + * @return 目标 ID + */ + public String startGoalGuarding(String userPrompt) { + // 生成目标 ID + String goalId = "goal-" + System.currentTimeMillis(); + + // 创建 GoalKeeper + currentGoalKeeper = new GoalKeeper(goalId, userPrompt); + + // 保存到工作记忆 + if (mainAgent.getSharedMemoryManager() != null) { + WorkingMemory workingMemory = mainAgent.getSharedMemoryManager().getWorking(mainAgent.getCurrentGoalId()); + if (workingMemory == null) { + workingMemory = new WorkingMemory(goalId); + } + + currentGoalKeeper.updateWorkingMemory(workingMemory); + workingMemory.setCurrentAgent("main"); + workingMemory.setStatus("目标守护中: " + userPrompt); + + mainAgent.getSharedMemoryManager().storeWorking(workingMemory); + LOG.info("目标已保存到工作记忆: {}", userPrompt); + } + + LOG.info("目标守护已启动: goalId={}, 目标={}", goalId, userPrompt); + return goalId; + } + + /** + * 为流式响应添加目标提醒 + * + * @param chunks 响应块列表 + * @param currentStep 当前步数 + * @return 带提醒的响应块列表 + */ + public List addGoalRemindersIfNeeded(List chunks, int currentStep) { + if (currentGoalKeeper == null) { + return chunks; + } + + // 检查是否需要注入提醒 + String reminder = currentGoalKeeper.injectReminder(currentStep); + if (reminder != null && !chunks.isEmpty()) { + LOG.info("注入目标提醒: step={}, reminderCount={}", currentStep, currentGoalKeeper.getReminderCount()); + + // 创建一个包含提醒的简单 AgentChunk + List result = new ArrayList<>(chunks.size() + 1); + result.addAll(chunks); // 先添加原有 chunks + + // 注意:由于 AgentChunk 是接口,我们无法直接实例化 + // 这里只是记录日志,实际的提醒需要通过其他方式注入 + LOG.info("目标提醒内容: {}", reminder); + + return result; + } + + return chunks; + } + + /** + * 检查是否应该强制结束 + * + * @param currentStep 当前步数 + * @param maxSteps 最大步数 + * @return 是否应该结束 + */ + public boolean shouldForceFinish(int currentStep, int maxSteps) { + if (currentGoalKeeper == null) { + return false; + } + + return currentGoalKeeper.shouldForceFinish(currentStep, maxSteps); + } + + /** + * 停止目标守护 + */ + public void stopGoalGuarding() { + if (currentGoalKeeper != null) { + LOG.info("目标守护已停止: goalId={}", currentGoalKeeper.getGoalId()); + + // 清理工作记忆 + if (mainAgent.getSharedMemoryManager() != null) { + WorkingMemory workingMemory = mainAgent.getSharedMemoryManager().getWorking(mainAgent.getCurrentGoalId()); + if (workingMemory != null && workingMemory.getTaskId().equals(currentGoalKeeper.getGoalId())) { + workingMemory.setStatus("目标已完成"); + mainAgent.getSharedMemoryManager().storeWorking(workingMemory); + } + } + + currentGoalKeeper = null; + } + } + + /** + * 获取当前目标 + * + * @return 当前目标描述,如果未启动返回 null + */ + public String getCurrentGoal() { + return currentGoalKeeper != null ? currentGoalKeeper.getOriginalGoal() : null; + } + + /** + * 获取当前目标 ID + * + * @return 目标 ID,如果未启动返回 null + */ + public String getCurrentGoalId() { + return currentGoalKeeper != null ? currentGoalKeeper.getGoalId() : null; + } + + /** + * 检查是否正在守护 + * + * @return 是否正在守护 + */ + public boolean isGuarding() { + return currentGoalKeeper != null; + } + + /** + * 获取提醒次数 + * + * @return 提醒次数 + */ + public int getReminderCount() { + return currentGoalKeeper != null ? currentGoalKeeper.getReminderCount() : 0; + } + + /** + * 为 Prompt 添加目标上下文 + * + * @param prompt 原始提示词 + * @return 带目标的提示词 + */ + public Prompt enrichPromptWithGoal(Prompt prompt) { + if (currentGoalKeeper == null) { + return prompt; + } + + String goalContext = String.format( + "\n\n## 🎯 任务目标\n\n原始目标: %s\n目标ID: %s\n\n请在执行过程中始终牢记这个目标。\n", + currentGoalKeeper.getOriginalGoal(), + currentGoalKeeper.getGoalId() + ); + + // 创建新的 Prompt(保留原有内容) + return Prompt.of(prompt.getUserContent() + goalContext); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/ActionRecord.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/ActionRecord.java new file mode 100644 index 0000000000000000000000000000000000000000..efd2759eba56d1a41cc5f224e9deb476f756bae3 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/ActionRecord.java @@ -0,0 +1,328 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +/** + * 动作执行记录(通用类) + * + * 记录各种动作的执行过程和结果,用于调试和审计。 + * 支持多种动作类型: + * - Tool: 内置工具(bash, grep, read, write 等) + * - Skill: 技能脚本(Python, Shell, JavaScript 等) + * - MCP: Model Context Protocol 服务器工具 + * - Custom: 自定义动作类型 + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Setter +public class ActionRecord { + // ========== 类型常量 ========== + + /** 内置工具类型 */ + public static final String TYPE_TOOL = "tool"; + + /** 技能类型 */ + public static final String TYPE_SKILL = "skill"; + + /** MCP 服务器工具类型 */ + public static final String TYPE_MCP = "mcp"; + + /** 自定义动作类型 */ + public static final String TYPE_CUSTOM = "custom"; + + // ========== 字段 ========== + + private String actionName; // 动作名称(通用字段) + private String actionDescription; // 动作描述 + private String actionType; // 动作类型:tool/skill/mcp/custom 等 + private Map inputs; // 输入参数 + private Object output; // 输出结果 + private long startTime; // 开始时间 + private long endTime; // 结束时间 + private long duration; // 执行耗时(毫秒) + private boolean success; // 是否成功 + private String errorMessage; // 错误信息 + private String agentId; // 执行的 Agent ID + + // Skill 特有字段(可选) + private String skillPool; // Skill 池(如 @soloncode_skills) + private String skillFile; // Skill 文件路径(如果是文件 skill) + + // MCP 特有字段(可选) + private String mcpServer; // MCP 服务器名称 + private String mcpMethod; // MCP 方法名称 + private String mcpResource; // MCP 资源标识 + + /** + * 构造函数(Tool) + * + * @param actionName 动作名称 + * @param agentId 执行的 Agent ID + */ + public ActionRecord(String actionName, String agentId) { + this(actionName, agentId, TYPE_TOOL); + } + + /** + * 构造函数(Skill - 向后兼容) + * + * @param skillName Skill 名称 + * @param agentId 执行的 Agent ID + * @param asSkill 标记为 Skill 类型 + */ + public ActionRecord(String skillName, String agentId, boolean asSkill) { + this(skillName, agentId, asSkill ? TYPE_SKILL : TYPE_TOOL); + } + + /** + * 完整构造函数 + * + * @param name 动作名称 + * @param agentId 执行的 Agent ID + * @param actionType 动作类型 + */ + public ActionRecord(String name, String agentId, String actionType) { + this.actionName = name; + this.agentId = agentId; + this.actionType = actionType != null ? actionType : TYPE_TOOL; + this.inputs = new HashMap<>(); + this.startTime = System.currentTimeMillis(); + this.success = false; + } + + // ========== 静态工厂方法 ========== + + /** + * 创建 Tool 记录 + * + * @param toolName 工具名称 + * @param agentId 执行的 Agent ID + * @return ActionRecord 实例 + */ + public static ActionRecord forTool(String toolName, String agentId) { + return new ActionRecord(toolName, agentId, TYPE_TOOL); + } + + /** + * 创建 Skill 记录 + * + * @param skillName Skill 名称 + * @param agentId 执行的 Agent ID + * @return ActionRecord 实例 + */ + public static ActionRecord forSkill(String skillName, String agentId) { + return new ActionRecord(skillName, agentId, TYPE_SKILL); + } + + /** + * 创建 MCP 工具记录 + * + * @param toolName MCP 工具名称 + * @param mcpServer MCP 服务器名称 + * @param agentId 执行的 Agent ID + * @return ActionRecord 实例 + */ + public static ActionRecord forMcp(String toolName, String mcpServer, String agentId) { + ActionRecord record = new ActionRecord(toolName, agentId, TYPE_MCP); + record.setMcpServer(mcpServer); + return record; + } + + /** + * 创建自定义动作记录 + * + * @param actionName 动作名称 + * @param customType 自定义类型 + * @param agentId 执行的 Agent ID + * @return ActionRecord 实例 + */ + public static ActionRecord forCustom(String actionName, String customType, String agentId) { + return new ActionRecord(actionName, agentId, customType); + } + + /** + * 标记成功 + * + * @param output 输出结果 + */ + public void success(Object output) { + this.output = output; + this.success = true; + this.endTime = System.currentTimeMillis(); + this.duration = this.endTime - this.startTime; + } + + /** + * 标记失败 + * + * @param errorMessage 错误信息 + */ + public void failure(String errorMessage) { + this.errorMessage = errorMessage; + this.success = false; + this.endTime = System.currentTimeMillis(); + this.duration = this.endTime - this.startTime; + } + + /** + * 添加输入参数 + * + * @param key 参数名 + * @param value 参数值 + */ + public ActionRecord addInput(String key, Object value) { + this.inputs.put(key, value); + return this; + } + + /** + * 设置输入参数 + * + * @param inputs 输入参数 Map + */ + public void setInputs(Map inputs) { + this.inputs = inputs != null ? inputs : new HashMap<>(); + } + + // ========== 向后兼容方法 ========== + + /** + * 获取 Tool 名称(别名) + * 向后兼容 ToolRecord.getToolName() + */ + public String getToolName() { + return actionName; + } + + /** + * 设置 Tool 名称(别名) + * 向后兼容 ToolRecord.setToolName() + */ + public void setToolName(String toolName) { + this.actionName = toolName; + } + + /** + * 获取 Skill 名称(别名) + * 向后兼容 SkillRecord.getSkillName() + */ + public String getSkillName() { + return actionName; + } + + /** + * 设置 Skill 名称(别名) + * 向后兼容 SkillRecord.setSkillName() + */ + public void setSkillName(String skillName) { + this.actionName = skillName; + } + + // ========== 类型判断方法 ========== + + /** + * 判断是否为 Tool 类型 + */ + public boolean isTool() { + return TYPE_TOOL.equals(actionType); + } + + /** + * 判断是否为 Skill 类型 + */ + public boolean isSkill() { + return TYPE_SKILL.equals(actionType); + } + + /** + * 判断是否为 MCP 类型 + */ + public boolean isMcp() { + return TYPE_MCP.equals(actionType); + } + + /** + * 判断是否为自定义类型 + */ + public boolean isCustom() { + return TYPE_CUSTOM.equals(actionType); + } + + /** + * 判断是否为指定类型 + * + * @param type 类型字符串 + * @return 是否匹配 + */ + public boolean isType(String type) { + return (type != null) && type.equals(actionType); + } + + // ========== toString ========== + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ActionRecord{"); + sb.append("type='").append(actionType).append("'"); + sb.append(", name='").append(actionName).append("'"); + sb.append(", success=").append(success); + sb.append(", duration=").append(duration); + sb.append(", inputs=").append(inputs.size()); + + if (output != null) { + sb.append(", hasOutput=true"); + } + + if (errorMessage != null) { + sb.append(", error='").append(errorMessage).append("'"); + } + + // Skill 特有字段 + if (isSkill()) { + if (skillPool != null) { + sb.append(", pool='").append(skillPool).append("'"); + } + if (skillFile != null) { + sb.append(", file='").append(skillFile).append("'"); + } + } + + // MCP 特有字段 + if (isMcp()) { + if (mcpServer != null) { + sb.append(", mcpServer='").append(mcpServer).append("'"); + } + if (mcpMethod != null) { + sb.append(", mcpMethod='").append(mcpMethod).append("'"); + } + if (mcpResource != null) { + sb.append(", mcpResource='").append(mcpResource).append("'"); + } + } + + sb.append("}"); + return sb.toString(); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/KnowledgeMemory.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/KnowledgeMemory.java new file mode 100644 index 0000000000000000000000000000000000000000..5a509cf2b20377079455b72fae5aaf6fb2442307 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/KnowledgeMemory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + +import lombok.Getter; + +import java.util.List; +import java.util.ArrayList; + +/** + * 知识库记忆(架构知识、模式等) + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class KnowledgeMemory extends Memory { + private String subject; // 主题 + private String content; // 内容 + private String category; // 分类(architecture/pattern/api) + private List keywords; // 关键词 + + /** + * 无参构造函数(用于反序列化) + */ + public KnowledgeMemory() { + super(MemoryType.KNOWLEDGE, -1); + this.subject = ""; + this.content = ""; + this.category = ""; + this.keywords = new ArrayList<>(); + } + + /** + * 构造函数 + * + * @param subject 主题 + * @param content 内容 + * @param category 分类 + */ + public KnowledgeMemory(String subject, String content, String category) { + super(MemoryType.KNOWLEDGE, -1); // 永久,无TTL + this.subject = subject; + this.content = content; + this.category = category; + this.keywords = new ArrayList<>(); + } + + /** + * 构造函数(带关键词) + */ + public KnowledgeMemory(String subject, String content, String category, List keywords) { + super(MemoryType.KNOWLEDGE, -1); + this.subject = subject; + this.content = content; + this.category = category; + this.keywords = keywords != null ? new ArrayList<>(keywords) : new ArrayList<>(); + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public void setContent(String content) { + this.content = content; + } + + public void setCategory(String category) { + this.category = category; + } + + public void setKeywords(List keywords) { + this.keywords = keywords != null ? new ArrayList<>(keywords) : new ArrayList<>(); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/LongTermMemory.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/LongTermMemory.java new file mode 100644 index 0000000000000000000000000000000000000000..5a31f4da0be32ea592dbdcaf077302bdb47bd046 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/LongTermMemory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.ArrayList; + +/** + * 长期记忆(重要结论) + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Setter +public class LongTermMemory extends Memory { + private String summary; // 摘要内容 + private String sourceAgent; // 来源代理 + private List tags; // 标签(用于检索) + private double importance; // 重要性评分 (0.0-1.0) + + /** + * 无参构造函数(用于反序列化) + */ + public LongTermMemory() { + super(MemoryType.LONG_TERM, 7 * 24 * 3600_000L); + this.summary = ""; + this.sourceAgent = ""; + this.tags = new ArrayList<>(); + this.importance = 0.5; + } + + /** + * 构造函数 + * + * @param summary 摘要内容 + * @param sourceAgent 来源代理 + * @param tags 标签列表 + */ + public LongTermMemory(String summary, String sourceAgent, List tags) { + super(MemoryType.LONG_TERM, 7 * 24 * 3600_000L); // 默认7天TTL + this.summary = summary; + this.sourceAgent = sourceAgent; + this.tags = tags != null ? new ArrayList<>(tags) : new ArrayList<>(); + this.importance = 0.5; // 默认重要性 + } + + /** + * 构造函数(自定义TTL) + */ + public LongTermMemory(String summary, String sourceAgent, List tags, long ttl) { + super(MemoryType.LONG_TERM, ttl); + this.summary = summary; + this.sourceAgent = sourceAgent; + this.tags = tags != null ? new ArrayList<>(tags) : new ArrayList<>(); + this.importance = 0.5; + } + + public void setImportance(double importance) { + this.importance = Math.max(0.0, Math.min(1.0, importance)); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/Memory.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/Memory.java new file mode 100644 index 0000000000000000000000000000000000000000..33dcca0ae1c1cd19572af108659e73507f5cbadd --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/Memory.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + + + +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; + +/** + * 记忆基类 + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Setter +public abstract class Memory { + protected String id; + protected MemoryType type; + protected long timestamp; + protected long ttl; // Time to live (毫秒) + protected Map metadata; + + /** + * 无参构造函数(用于反序列化) + */ + public Memory() { + this.id = UUID.randomUUID().toString(); + this.type = MemoryType.WORKING; + this.ttl = -1; + this.timestamp = System.currentTimeMillis(); + this.metadata = new HashMap<>(); + } + + /** + * 构造函数 + */ + public Memory(MemoryType type, long ttl) { + id = UUID.randomUUID().toString(); + this.type = type; + this.ttl = ttl; + this.timestamp = System.currentTimeMillis(); + this.metadata = new HashMap<>(); + } + + /** + * 记忆类型枚举 + */ + public enum MemoryType { + /** + * 工作记忆(极短期,仅内存) + */ + WORKING, + + /** + * 短期记忆(会话级别) + */ + SHORT_TERM, + + /** + * 长期记忆(跨会话) + */ + LONG_TERM, + + /** + * 知识库(持久化) + */ + KNOWLEDGE + } + + /** + * 检查记忆是否已过期 + * + * @return true表示已过期,false表示未过期 + */ + public boolean isExpired() { + if (ttl <= 0) { + return false; // TTL <= 0 表示永不过期 + } + long elapsed = System.currentTimeMillis() - timestamp; + return elapsed >= ttl; // 使用 >= 而不是 >,确保边界条件正确 + } + + /** + * 设置元数据(用于反序列化) + */ + public void setMetadata(Map metadata) { + this.metadata = metadata != null ? metadata : new HashMap<>(); + } + + /** + * 添加元数据 + */ + public void putMetadata(String key, Object value) { + if (this.metadata == null){ + this.metadata = new HashMap<>(); + } + this.metadata.put(key, value); + } + + /** + * 获取元数据值 + */ + public Object getMetadata(String key) { + return this.metadata.get(key); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/SharedMemoryManager.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/SharedMemoryManager.java new file mode 100644 index 0000000000000000000000000000000000000000..b351a57d013c8793ddc6cfdf96e4a5189325c1f0 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/SharedMemoryManager.java @@ -0,0 +1,897 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + +import org.noear.solon.bot.core.memory.bank.MemoryBank; +import org.noear.solon.bot.core.memory.bank.Observation; +import org.noear.solon.bot.core.memory.bank.store.FileMemoryStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 共享记忆管理器(基于 MemoryBank 架构) + * + * 负责所有子代理之间的记忆存储、检索和生命周期管理 + * + * @author bai + * @since 3.9.5 + */ +public class SharedMemoryManager { + private static final Logger LOG = LoggerFactory.getLogger(SharedMemoryManager.class); + + // 内存存储(分层设计) + private final Map workingCache = new ConcurrentHashMap<>(); + private final Map shortTermCache = new ConcurrentHashMap<>(); + private final Map longTermCache = new ConcurrentHashMap<>(); + private final Map knowledgeCache = new ConcurrentHashMap<>(); + + // 持久化存储(使用 MemoryBank 架构) + private final MemoryBank memoryBank; + private final FileMemoryStore fileStore; // 保存直接引用用于加载 + + // 定期清理执行器 + private final ScheduledExecutorService cleanupExecutor; + + // 索引(用于快速检索) + private final Map> tagIndex = new ConcurrentHashMap<>(); + private final Map> keywordIndex = new ConcurrentHashMap<>(); + + // 配置 + private final long shortTermTtl; + private final long longTermTtl; + private final long cleanupInterval; + private final boolean persistOnWrite; + private final int maxShortTermCount; + private final int maxLongTermCount; + + // 性能监控:内存使用上限 + private static final long MAX_MEMORY_USAGE = 100 * 1024 * 1024; // 100MB + private static final double MEMORY_WARNING_THRESHOLD = 0.8; // 80% 警告阈值 + + /** + * 构造函数(使用默认配置) + */ + public SharedMemoryManager(Path path) { + this(path, 3600_000L, 7 * 24 * 3600_000L, 300_000L, true, 1000, 500); + } + + /** + * 完整构造函数 + * + * @param path 路径 + * @param shortTermTtl 短期记忆TTL(毫秒) + * @param longTermTtl 长期记忆TTL(毫秒) + * @param cleanupInterval 清理间隔(毫秒) + * @param persistOnWrite 写入时是否立即持久化 + * @param maxShortTermCount 短期记忆最大数量 + * @param maxLongTermCount 长期记忆最大数量 + */ + public SharedMemoryManager(Path path, + long shortTermTtl, + long longTermTtl, + long cleanupInterval, + boolean persistOnWrite, + int maxShortTermCount, + int maxLongTermCount) { + // 初始化 FileMemoryStore + this.fileStore = new FileMemoryStore(path.toAbsolutePath().toString()); + + // 初始化 MemoryBank(使用 FileMemoryStore) + this.memoryBank = new MemoryBank( + 2000, // 短期记忆 2000 tokens + 8000, // 长期记忆检索 8000 tokens + 5000, // 感觉记忆 5 秒过期 + null, // 不使用向量化服务 + fileStore // 使用文件持久化 + ); + this.shortTermTtl = shortTermTtl; + this.longTermTtl = longTermTtl; + this.cleanupInterval = cleanupInterval; + this.persistOnWrite = persistOnWrite; + this.maxShortTermCount = maxShortTermCount; + this.maxLongTermCount = maxLongTermCount; + + this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "MemoryCleanupThread"); + t.setDaemon(true); + return t; + }); + + // 初始化存储(从文件加载所有记忆) + initStorage(); + + // 启动定期清理任务 + this.cleanupExecutor.scheduleAtFixedRate( + this::cleanupExpiredMemories, + this.cleanupInterval, + this.cleanupInterval, + TimeUnit.MILLISECONDS + ); + + LOG.info("共享记忆系统初始化完成: shortTermTtl={}ms, longTermTtl={}ms", + shortTermTtl, longTermTtl); + } + + /** + * 将 Memory 转换为 Observation + */ + private Observation convertMemoryToObservation(Memory memory) { + Observation.ObservationType type; + + if (memory instanceof ShortTermMemory) { + type = Observation.ObservationType.GENERAL; + } else if (memory instanceof LongTermMemory) { + type = Observation.ObservationType.TASK_RESULT; + } else if (memory instanceof KnowledgeMemory) { + type = Observation.ObservationType.ARCHITECTURE; + } else { + type = Observation.ObservationType.GENERAL; + } + + Observation.ObservationBuilder builder = Observation.builder() + .content(getMemoryContent(memory)) + .type(type) + .timestamp(memory.getTimestamp()); + + if (memory.getId() != null) { + builder.id(memory.getId()); + } + + // 设置重要性(永久记忆需要高重要性) + if (memory instanceof LongTermMemory) { + builder.importance(((LongTermMemory) memory).getImportance()); + } else if (memory instanceof KnowledgeMemory) { + // 永久记忆:高重要性(8.0-10.0),确保持久化 + builder.importance(9.0); + } else { + // 默认重要性 + builder.importance(5.0); + } + + return builder.build(); + } + + /** + * 获取 Memory 的内容 + */ + private String getMemoryContent(Memory memory) { + if (memory instanceof ShortTermMemory) { + return ((ShortTermMemory) memory).getContext(); + } else if (memory instanceof LongTermMemory) { + return ((LongTermMemory) memory).getSummary(); + } else if (memory instanceof KnowledgeMemory) { + return ((KnowledgeMemory) memory).getContent(); + } + return ""; + } + + /** + * 存储记忆 + * + * @param memory 记忆对象 + */ + public void store(Memory memory) { + if (memory == null) { + return; + } + + // 工作记忆特殊处理(不持久化) + if (memory instanceof WorkingMemory) { + storeWorking((WorkingMemory) memory); + return; + } + + // 自动分配ID + if (memory.getId() == null) { + memory.setId(UUID.randomUUID().toString()); + } + + Map cache = getCacheByType(memory.type); + + // 检查数量限制 + if (memory.type == Memory.MemoryType.SHORT_TERM && cache.size() >= maxShortTermCount) { + // 删除最旧的短期记忆 + removeOldestMemory(cache); + } else if (memory.type == Memory.MemoryType.LONG_TERM && cache.size() >= maxLongTermCount) { + // 删除最不重要的长期记忆 + removeLeastImportantMemory(cache); + } + + // 存储到缓存 + cache.put(memory.getId(), memory); + + // 建立索引 + buildIndex(memory); + + // 异步持久化 + if (persistOnWrite) { + persistAsync(memory); + } + + LOG.debug("记忆已存储: type={}, id={}", memory.type, memory.id); + } + + /** + * 检索记忆(按类型) + * + * @param type 记忆类型 + * @param limit 最大数量 + * @return 记忆列表 + */ + public List retrieve(Memory.MemoryType type, int limit) { + Map cache = getCacheByType(type); + + return cache.values().stream() + .filter(m -> !m.isExpired()) + .sorted((a, b) -> Long.compare(b.timestamp, a.timestamp)) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 按标签检索长期记忆 + * + * @param tag 标签 + * @param limit 最大数量 + * @return 长期记忆列表 + */ + public List retrieveByTag(String tag, int limit) { + Set memoryIds = tagIndex.getOrDefault(tag, Collections.emptySet()); + + return memoryIds.stream() + .map(id -> (LongTermMemory) longTermCache.get(id)) + .filter(Objects::nonNull) + .filter(m -> !m.isExpired()) + .sorted((a, b) -> Double.compare(b.getImportance(), a.getImportance())) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 按关键词检索知识库 + * + * @param keyword 关键词 + * @param limit 最大数量 + * @return 知识库记忆列表 + */ + public List searchKnowledge(String keyword, int limit) { + Set memoryIds = keywordIndex.getOrDefault(keyword, Collections.emptySet()); + + return memoryIds.stream() + .map(id -> (KnowledgeMemory) knowledgeCache.get(id)) + .filter(Objects::nonNull) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 全文搜索(跨所有记忆类型) + * + * @param query 搜索关键词 + * @param limit 最大数量 + * @return 记忆列表 + */ + public List search(String query, int limit) { + String lowerQuery = query.toLowerCase(); + + return Stream.of( + shortTermCache.values().stream(), + longTermCache.values().stream(), + knowledgeCache.values().stream() + ) + .flatMap(s -> s) + .filter(m -> !m.isExpired()) + .filter(m -> matchesQuery(m, lowerQuery)) + .sorted((a, b) -> Long.compare(b.timestamp, a.timestamp)) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 获取记忆统计信息 + */ + public Map getStats() { + Map stats = new HashMap<>(); + stats.put("workingCount", workingCache.size()); + stats.put("shortTermCount", shortTermCache.size()); + stats.put("longTermCount", longTermCache.size()); + stats.put("knowledgeCount", knowledgeCache.size()); + stats.put("tagIndexSize", tagIndex.size()); + stats.put("keywordIndexSize", keywordIndex.size()); + + // 添加 MemoryBank 统计 + Map bankStats = memoryBank.getStats(); + stats.putAll(bankStats); + + // 添加内存使用监控 + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long usedMemory = totalMemory - freeMemory; + double memoryUsageRatio = (double) usedMemory / MAX_MEMORY_USAGE; + + stats.put("memoryUsed", formatBytes(usedMemory)); + stats.put("memoryTotal", formatBytes(totalMemory)); + stats.put("memoryMax", formatBytes(MAX_MEMORY_USAGE)); + stats.put("memoryUsagePercent", String.format("%.2f%%", memoryUsageRatio * 100)); + + // 超过阈值时发出警告 + if (memoryUsageRatio > MEMORY_WARNING_THRESHOLD) { + LOG.warn("[WARN] 内存使用率过高: {}/{} ({}%)", + formatBytes(usedMemory), + formatBytes(MAX_MEMORY_USAGE), + String.format("%.2f", memoryUsageRatio * 100)); + } + + return stats; + } + + /** + * 格式化字节数 + */ + private String formatBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return String.format("%.2f KB", bytes / 1024.0); + } else if (bytes < 1024 * 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); + } + } + + /** + * 检查内存使用是否超过限制 + * + * @return true 如果内存使用超过限制 + */ + public boolean isMemoryOverLimit() { + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long usedMemory = totalMemory - freeMemory; + return usedMemory > MAX_MEMORY_USAGE; + } + + // ========== 工作记忆方法 ========== + + /** + * 创建短期记忆(使用配置的TTL) + * + * @param agentId Agent ID + * @param context 上下文内容 + * @param taskId 任务ID + * @return ShortTermMemory 实例 + */ + public ShortTermMemory createShortTermMemory(String agentId, String context, String taskId) { + return new ShortTermMemory(agentId, context, taskId, shortTermTtl); + } + + /** + * 创建长期记忆(使用配置的TTL) + * + * @param summary 摘要 + * @param sourceAgent 源Agent + * @param tags 标签列表 + * @return LongTermMemory 实例 + */ + public LongTermMemory createLongTermMemory(String summary, String sourceAgent, List tags) { + return new LongTermMemory(summary, sourceAgent, tags, longTermTtl); + } + + /** + * 存储工作记忆(仅内存,不持久化) + * + * @param memory 工作记忆对象 + */ + public void storeWorking(WorkingMemory memory) { + if (memory == null) { + return; + } + + // 自动分配ID + if (memory.getId() == null) { + memory.setId(UUID.randomUUID().toString()); + } + + workingCache.put(memory.getTaskId(), memory); + + LOG.debug("工作记忆已存储: taskId={}, id={}", memory.getTaskId(), memory.getId()); + } + + /** + * 获取工作记忆 + * + * @param taskId 任务ID + * @return 工作记忆对象,不存在返回 null + */ + public WorkingMemory getWorking(String taskId) { + WorkingMemory memory = workingCache.get(taskId); + if (memory != null) { + // 更新最后访问时间 + memory.setLastAccessTime(System.currentTimeMillis()); + + // 检查是否过期 + if (memory.isExpired()) { + workingCache.remove(taskId); + LOG.debug("工作记忆已过期: taskId={}", taskId); + return null; + } + } + return memory; + } + + /** + * 移除工作记忆 + * + * @param taskId 任务ID + */ + public void removeWorking(String taskId) { + WorkingMemory memory = workingCache.remove(taskId); + if (memory != null) { + LOG.debug("工作记忆已移除: taskId={}", taskId); + } + } + + /** + * 移除短期记忆 + * + * @param key 键 + */ + public void removeShortTerm(String key) { + Memory memory = shortTermCache.remove(key); + if (memory != null) { + LOG.debug("短期记忆已移除: key={}", key); + } + } + + /** + * 移除长期记忆 + * + * @param key 键 + */ + public void removeLongTerm(String key) { + Memory memory = longTermCache.remove(key); + if (memory != null) { + LOG.debug("长期记忆已移除: key={}", key); + } + } + + /** + * 移除知识记忆 + * + * @param key 键 + */ + public void removeKnowledge(String key) { + Memory memory = knowledgeCache.remove(key); + if (memory != null) { + LOG.debug("知识记忆已移除: key={}", key); + } + } + + /** + * 完成工作记忆 + * + * @param taskId 任务ID + */ + public void completeWorking(String taskId) { + WorkingMemory memory = workingCache.get(taskId); + if (memory != null) { + memory.complete(); + LOG.debug("工作记忆已完成: taskId={}, step={}", taskId, memory.getStep()); + + // 可以选择转移到短期记忆 + // ShortTermMemory stm = convertToShortTerm(memory); + // store(stm); + + workingCache.remove(taskId); + } + } + + /** + * 清空所有工作记忆 + */ + public void clearWorking() { + int size = workingCache.size(); + workingCache.clear(); + LOG.info("所有工作记忆已清空: count={}", size); + } + + /** + * 清空所有记忆(仅内存,不删除持久化文件) + */ + public void clear() { + shortTermCache.clear(); + longTermCache.clear(); + knowledgeCache.clear(); + tagIndex.clear(); + keywordIndex.clear(); + LOG.info("所有记忆已清空"); + } + + // ========== 便捷方法(简化键值对接口) ========== + + /** + * 存储短期记忆(便捷方法) + * + * @param key 键 + * @param value 值 + * @param ttlSeconds 过期时间(秒) + */ + public void putShortTerm(String key, String value, long ttlSeconds) { + ShortTermMemory memory = new ShortTermMemory(); + memory.setId(key); + memory.setContext(value); + memory.setTtl(ttlSeconds * 1000L); // 转换为毫秒 + memory.setTimestamp(System.currentTimeMillis()); + store(memory); + } + + /** + * 存储长期记忆(便捷方法) + * + * @param key 键 + * @param value 值 + * @param ttlSeconds 过期时间(秒) + */ + public void putLongTerm(String key, String value, long ttlSeconds) { + LongTermMemory memory = new LongTermMemory(); + memory.setId(key); + memory.setSummary(value); + memory.setTtl(ttlSeconds * 1000L); // 转换为毫秒 + memory.setTimestamp(System.currentTimeMillis()); + memory.setTags(Collections.emptyList()); + store(memory); + } + + /** + * 存储知识记忆(便捷方法) + * + * @param key 键 + * @param value 值 + */ + public void putKnowledge(String key, String value) { + KnowledgeMemory memory = new KnowledgeMemory(); + memory.setId(key); + memory.setContent(value); + memory.setTimestamp(System.currentTimeMillis()); + memory.setKeywords(Collections.emptyList()); + store(memory); + } + + /** + * 获取记忆(便捷方法,按优先级从短期、长期、知识记忆中查找) + * + * @param key 键 + * @return 值,不存在返回 null + */ + public String get(String key) { + // 1. 尝试从短期记忆获取 + Memory stm = shortTermCache.get(key); + if (stm != null && !stm.isExpired()) { + if (stm instanceof ShortTermMemory) { + return ((ShortTermMemory) stm).getContext(); + } + } + + // 2. 尝试从长期记忆获取 + Memory ltm = longTermCache.get(key); + if (ltm != null && !ltm.isExpired()) { + if (ltm instanceof LongTermMemory) { + return ((LongTermMemory) ltm).getSummary(); + } + } + + // 3. 尝试从知识记忆获取 + Memory km = knowledgeCache.get(key); + if (km != null) { + if (km instanceof KnowledgeMemory) { + return ((KnowledgeMemory) km).getContent(); + } + } + + return null; + } + + /** + * 关闭管理器 + */ + public void shutdown() { + cleanupExecutor.shutdown(); + try { + if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + cleanupExecutor.shutdownNow(); + } + LOG.info("共享记忆管理器已关闭"); + } catch (InterruptedException e) { + cleanupExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // ========== 私有方法 ========== + + /** + * 建立索引 + */ + private void buildIndex(Memory memory) { + if (memory instanceof LongTermMemory) { + LongTermMemory ltm = (LongTermMemory) memory; + ltm.getTags().forEach(tag -> + tagIndex.computeIfAbsent(tag, k -> ConcurrentHashMap.newKeySet()) + .add(memory.getId()) + ); + } else if (memory instanceof KnowledgeMemory) { + KnowledgeMemory km = (KnowledgeMemory) memory; + km.getKeywords().forEach(kw -> + keywordIndex.computeIfAbsent(kw, k -> ConcurrentHashMap.newKeySet()) + .add(memory.getId()) + ); + } + } + + /** + * 查询匹配 + */ + private boolean matchesQuery(Memory memory, String query) { + if (memory instanceof ShortTermMemory) { + return ((ShortTermMemory) memory).getContext().toLowerCase().contains(query); + } else if (memory instanceof LongTermMemory) { + LongTermMemory ltm = (LongTermMemory) memory; + return ltm.getSummary().toLowerCase().contains(query) || + ltm.getTags().stream().anyMatch(tag -> tag.toLowerCase().contains(query)); + } else if (memory instanceof KnowledgeMemory) { + KnowledgeMemory km = (KnowledgeMemory) memory; + return km.getContent().toLowerCase().contains(query) || + km.getKeywords().stream().anyMatch(kw -> kw.toLowerCase().contains(query)); + } + return false; + } + + /** + * 清理过期记忆 + * 性能优化:添加内存使用检查 + */ + private void cleanupExpiredMemories() { + int removed = 0; + + // 清理工作记忆(特殊逻辑) + removed += cleanupWorkingMemories(); + + // 清理短期和长期记忆 + removed += cleanCache(shortTermCache); + removed += cleanCache(longTermCache); + // knowledgeCache 不清理 + + if (removed > 0) { + LOG.info("清理了 {} 条过期记忆", removed); + } + + // 检查内存使用情况 + checkMemoryUsage(); + } + + /** + * 检查内存使用情况 + */ + private void checkMemoryUsage() { + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long usedMemory = totalMemory - freeMemory; + double memoryUsageRatio = (double) usedMemory / MAX_MEMORY_USAGE; + + if (memoryUsageRatio > MEMORY_WARNING_THRESHOLD) { + LOG.warn("[WARN] 内存使用警告: 已使用 {}/{} ({}%)", + formatBytes(usedMemory), + formatBytes(MAX_MEMORY_USAGE), + String.format("%.2f", memoryUsageRatio * 100)); + + // 如果超过限制,触发紧急清理 + if (memoryUsageRatio > 1.0) { + LOG.error("[ERROR] 内存使用超过限制,触发紧急清理"); + emergencyMemoryCleanup(); + } + } + } + + /** + * 紧急内存清理 + */ + private void emergencyMemoryCleanup() { + LOG.warn("开始紧急内存清理..."); + + // 清理所有短期记忆 + int cleared = shortTermCache.size(); + shortTermCache.clear(); + + // 清理索引 + tagIndex.clear(); + keywordIndex.clear(); + + LOG.warn("紧急清理完成,清理了 {} 条短期记忆", cleared); + + // 建议垃圾回收 + System.gc(); + } + + /** + * 清理过期的工作记忆 + */ + private int cleanupWorkingMemories() { + int removed = 0; + + workingCache.entrySet().removeIf(entry -> { + WorkingMemory memory = entry.getValue(); + + // 检查 TTL + boolean expired = memory.isExpired(); + + // 检查是否完成 + boolean completed = "completed".equals(memory.getStatus()) + || "failed".equals(memory.getStatus()); + + // 检查最后访问时间(超过5分钟未访问) + boolean idle = memory.isIdle(300_000); // 5分钟 + + if (expired || completed || idle) { + LOG.debug("清理工作记忆: taskId={}, reason={}, status={}", + memory.getTaskId(), + expired ? "expired" : completed ? "completed" : "idle", + memory.getStatus()); + return true; + } + return false; + }); + + return removed; + } + + /** + * 清理缓存 + */ + private int cleanCache(Map cache) { + Iterator> it = cache.entrySet().iterator(); + int removed = 0; + + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (entry.getValue().isExpired()) { + it.remove(); + removed++; + } + } + + return removed; + } + + /** + * 删除最旧的记忆 + */ + private void removeOldestMemory(Map cache) { + cache.entrySet().stream() + .min(Comparator.comparingLong(e -> e.getValue().getTimestamp())) + .ifPresent(entry -> { + cache.remove(entry.getKey()); + LOG.debug("删除最旧的短期记忆: id={}", entry.getKey()); + }); + } + + /** + * 删除最不重要的长期记忆 + */ + private void removeLeastImportantMemory(Map cache) { + cache.entrySet().stream() + .filter(e -> e.getValue() instanceof LongTermMemory) + .min(Comparator.comparingDouble(e -> ((LongTermMemory) e.getValue()).getImportance())) + .ifPresent(entry -> { + cache.remove(entry.getKey()); + LOG.debug("删除最不重要的长期记忆: id={}", entry.getKey()); + }); + } + + /** + * 异步持久化到文件(使用 MemoryBank) + */ + private void persistAsync(Memory memory) { + Observation obs = convertMemoryToObservation(memory); + memoryBank.addObservation(obs); + } + + /** + * 启动时加载持久化的记忆 + */ + private void initStorage() { + try { + // 从持久化存储加载所有观察 + List observations = fileStore.loadAll(); + LOG.info("从存储加载了 {} 条观察", observations.size()); + + // 将 Observation 转换并缓存到对应的 Memory 类型 + for (Observation obs : observations) { + // 根据类型转换 + Memory memory = null; + if (obs.getType() == Observation.ObservationType.GENERAL) { + ShortTermMemory stm = new ShortTermMemory(); + stm.setId(obs.getId()); + stm.setContext(obs.getContent()); + stm.setTimestamp(obs.getTimestamp()); + memory = stm; + } else if (obs.getType() == Observation.ObservationType.TASK_RESULT) { + LongTermMemory ltm = new LongTermMemory(); + ltm.setId(obs.getId()); + ltm.setSummary(obs.getContent()); + ltm.setTimestamp(obs.getTimestamp()); + ltm.setImportance((float) obs.getImportance()); + ltm.setTags(new ArrayList<>()); + memory = ltm; + } else if (obs.getType() == Observation.ObservationType.ARCHITECTURE) { + KnowledgeMemory km = new KnowledgeMemory(); + km.setId(obs.getId()); + km.setContent(obs.getContent()); + km.setTimestamp(obs.getTimestamp()); + km.setKeywords(new ArrayList<>()); + memory = km; + } + + if (memory != null && !memory.isExpired()) { + store(memory); + } + } + + Map stats = memoryBank.getStats(); + LOG.info("共享记忆加载完成: {}", stats); + + } catch (Exception e) { + LOG.warn("共享记忆初始化失败: error={}", e.getMessage()); + } + } + + /** + * 加载指定类型的记忆(已弃用,使用 initStorage 替代) + */ + @Deprecated + private void loadMemories(Memory.MemoryType type) { + // 不再使用,已整合到 initStorage 中 + } + + /** + * 根据类型获取缓存 + */ + private Map getCacheByType(Memory.MemoryType type) { + switch (type) { + case WORKING: + // 工作记忆不返回,因为它的类型不同 + throw new IllegalArgumentException("Working memory should use dedicated methods: getWorking(), storeWorking()"); + case SHORT_TERM: + return shortTermCache; + case LONG_TERM: + return longTermCache; + case KNOWLEDGE: + return knowledgeCache; + default: + throw new IllegalArgumentException("Unknown memory type: " + type); + } + } + +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/ShortTermMemory.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/ShortTermMemory.java new file mode 100644 index 0000000000000000000000000000000000000000..cc57afa3e1d187b83efdda92a5b80e0252e7ee03 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/ShortTermMemory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + +import lombok.Getter; +import lombok.Setter; + +/** + * 短期记忆(工作上下文) + * + * @author bai + * @since 3.9.5 + */ +@Setter +@Getter +public class ShortTermMemory extends Memory { + private String agentId; // 创建者代理ID + private String context; // 上下文内容 + private String taskId; // 关联任务ID + + /** + * 无参构造函数(用于反序列化) + */ + public ShortTermMemory() { + super(MemoryType.SHORT_TERM, 3600_000L); + this.agentId = ""; + this.context = ""; + this.taskId = ""; + } + + /** + * 构造函数 + * + * @param agentId 创建者代理ID + * @param context 上下文内容 + * @param taskId 关联任务ID + */ + public ShortTermMemory(String agentId, String context, String taskId) { + super(MemoryType.SHORT_TERM, 3600_000L); // 默认1小时TTL + this.agentId = agentId; + this.context = context; + this.taskId = taskId; + } + + /** + * 构造函数(自定义TTL) + */ + public ShortTermMemory(String agentId, String context, String taskId, long ttl) { + super(MemoryType.SHORT_TERM, ttl); + this.agentId = agentId; + this.context = context; + this.taskId = taskId; + } + +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/WorkingMemory.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/WorkingMemory.java new file mode 100644 index 0000000000000000000000000000000000000000..2e0cda5a0683d87f02acd2183f5ab1533797e134 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/WorkingMemory.java @@ -0,0 +1,638 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * 工作记忆(Working Memory) + * + * 用于 Agent 执行任务时临时存储当前状态: + * - 当前任务描述 + * - LLM 生成的摘要 + * - Tool/Skill 运行记录(统一) + * - 中间结果和临时变量 + * - 任务进度和步骤 + * + * 特点: + * - 极短 TTL (默认10分钟) + * - 仅内存存储(不持久化到文件) + * - 按任务ID隔离 + * - 支持快速读写 + * - 任务完成/超时自动清理 + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Setter +public class WorkingMemory extends Memory { + private String taskId; // 关联任务ID + private String taskDescription; // 当前任务描述 + private String summary; // LLM 生成的摘要 +// private List toolRecords; // Tool/Skill 运行记录(统一) + private Map data; // 其他工作数据(键值对) + private int step; // 当前步骤 + private String status; // 状态(running/completed/failed) + private String currentAgent; // 当前执行的Agent + private List completedSteps; // 已完成步骤 + private long lastAccessTime; // 最后访问时间 + + /** + * 构造函数(使用默认TTL: 10分钟) + * + * @param taskId 关联任务ID + */ + public WorkingMemory(String taskId) { + this(taskId, 600_000L); // 默认10分钟 + } + + /** + * 构造函数(自定义TTL) + * + * @param taskId 关联任务ID + * @param ttl TTL(毫秒) + */ + public WorkingMemory(String taskId, long ttl) { + super(MemoryType.WORKING, ttl); + this.taskId = taskId; + this.taskDescription = null; + this.summary = null; +// this.toolRecords = new CopyOnWriteArrayList<>(); + this.data = new ConcurrentHashMap<>(); + this.step = 0; + this.status = "running"; + this.completedSteps = new CopyOnWriteArrayList<>(); + this.lastAccessTime = System.currentTimeMillis(); + } + + + public void setTaskDescription(String taskDescription) { + this.taskDescription = taskDescription; + lastAccessTime = System.currentTimeMillis(); + } + + + public void setSummary(String summary) { + this.summary = summary; + lastAccessTime = System.currentTimeMillis(); + } + + + +// public void setActionRecords(List toolRecords) { +// this.toolRecords = toolRecords != null +// ? new CopyOnWriteArrayList<>(toolRecords) +// : new CopyOnWriteArrayList<>(); +// lastAccessTime = System.currentTimeMillis(); +// } + + /** + * 获取所有 Skill 类型的记录(向后兼容) + * + * @return Skill 类型的记录列表 + */ +// public List getSkillRecords() { +// return toolRecords.stream() +// .filter(ActionRecord::isSkill) +// .collect(Collectors.toList()); +// } + + /** + * 设置 Skill 记录(向后兼容) + * 注意:此方法会移除所有现有的 skill 类型记录,并添加新记录 + * +// * @param skillRecords Skill 记录列表(现在使用 ActionRecord) + */ +// public void setSkillRecords(List skillRecords) { +// // 移除所有现有的 skill 记录 +// toolRecords.removeIf(r -> "skill".equals(r.getActionType())); +// +// // 添加新的 skill 记录 +// if (skillRecords != null) { +// for (ActionRecord record : skillRecords) { +// record.setActionType("skill"); +// toolRecords.add(record); +// } +// } +// lastAccessTime = System.currentTimeMillis(); +// } + + + + public void setData(Map data) { + this.data = data != null ? data : new ConcurrentHashMap<>(); + } + + + public void setCompletedSteps(List completedSteps) { + this.completedSteps = completedSteps != null + ? new CopyOnWriteArrayList<>(completedSteps) + : new CopyOnWriteArrayList<>(); + } + + + /** + * 存储工作数据 + * + * @param key 键 + * @param value 值 + */ + public void put(String key, Object value) { + data.put(key, value); + lastAccessTime = System.currentTimeMillis(); + } + + /** + * 获取工作数据 + * + * @param key 键 + * @return 值,不存在返回 null + */ + public Object get(String key) { + lastAccessTime = System.currentTimeMillis(); + return data.get(key); + } + + /** + * 获取工作数据(带类型转换) + * + * @param key 键 + * @param type 类型 + * @param 泛型类型 + * @return 值,不存在或类型不匹配返回 null + */ + @SuppressWarnings("unchecked") + public T get(String key, Class type) { + lastAccessTime = System.currentTimeMillis(); + Object value = data.get(key); + if (value != null && type.isInstance(value)) { + return (T) value; + } + return null; + } + + /** + * 获取工作数据(带默认值) + * + * @param key 键 + * @param defaultValue 默认值 + * @return 值,不存在返回默认值 + */ + @SuppressWarnings("unchecked") + public T get(String key, T defaultValue) { + lastAccessTime = System.currentTimeMillis(); + Object value = data.get(key); + if (value != null) { + return (T) value; + } + return defaultValue; + } + + /** + * 移除工作数据 + * + * @param key 键 + * @return 被移除的值 + */ + public Object remove(String key) { + lastAccessTime = System.currentTimeMillis(); + return data.remove(key); + } + + /** + * 检查是否包含键 + * + * @param key 键 + * @return 是否包含 + */ + public boolean containsKey(String key) { + lastAccessTime = System.currentTimeMillis(); + return data.containsKey(key); + } + + /** + * 获取数据大小 + * + * @return 数据条目数 + */ + public int size() { + return data.size(); + } + + /** + * 增加步骤 + */ + public void incrementStep() { + this.step++; + lastAccessTime = System.currentTimeMillis(); + } + + /** + * 设置步骤 + * + * @param step 步骤号 + */ + public void setStepAndUpdate(int step) { + this.step = step; + lastAccessTime = System.currentTimeMillis(); + } + + /** + * 添加已完成步骤 + * + * @param step 步骤名称 + */ + public void addCompletedStep(String step) { + this.completedSteps.add(step); + lastAccessTime = System.currentTimeMillis(); + } + + /** + * 检查步骤是否已完成 + * + * @param step 步骤名称 + * @return 是否已完成 + */ + public boolean isStepCompleted(String step) { + lastAccessTime = System.currentTimeMillis(); + return completedSteps.contains(step); + } + + /** + * 标记为完成 + */ + public void complete() { + this.status = "completed"; + lastAccessTime = System.currentTimeMillis(); + } + + /** + * 标记为失败 + */ + public void fail() { + this.status = "failed"; + lastAccessTime = System.currentTimeMillis(); + } + + /** + * 检查是否正在运行 + * + * @return 是否正在运行 + */ + public boolean isRunning() { + return "running".equals(status); + } + + /** + * 检查是否已完成 + * + * @return 是否已完成 + */ + public boolean isCompleted() { + return "completed".equals(status); + } + + /** + * 检查是否失败 + * + * @return 是否失败 + */ + public boolean isFailed() { + return "failed".equals(status); + } + + /** + * 检查是否空闲(超过指定时间未访问) + * + * @param idleTimeout 空闲超时时间(毫秒) + * @return 是否空闲 + */ + public boolean isIdle(long idleTimeout) { + long idleTime = System.currentTimeMillis() - lastAccessTime; + return idleTime > idleTimeout; + } + + /** + * 清空数据 + */ + public void clear() { + data.clear(); +// toolRecords.clear(); + summary = null; + taskDescription = null; + step = 0; + status = "running"; + completedSteps.clear(); + lastAccessTime = System.currentTimeMillis(); + } + + + /** + * 添加 Tool 记录 + * + * @param record Tool 记录 + */ +// public void addActionRecord(ActionRecord record) { +// toolRecords.add(record); +// lastAccessTime = System.currentTimeMillis(); +// } + + /** + * 创建并添加 Tool 记录 + * + * @param toolName Tool 名称 + * @return 新创建的 ActionRecord + */ +// public ActionRecord createActionRecord(String toolName) { +// ActionRecord record = ActionRecord.forTool(toolName, currentAgent); +// toolRecords.add(record); +// lastAccessTime = System.currentTimeMillis(); +// return record; +// } + + /** + * 添加 Tool 记录(向后兼容别名) + * + * @param record Tool 记录 + */ +// public void addToolRecord(ActionRecord record) { +// addActionRecord(record); +// } + + /** + * 创建并添加 Tool 记录(向后兼容别名) + * + * @param toolName Tool 名称 + * @return 新创建的 ActionRecord + */ +// public ActionRecord createToolRecord(String toolName) { +// return createActionRecord(toolName); +// } + + /** + * 获取所有成功的 Tool 记录 + * + * @return 成功的记录列表 + */ +// public List getSuccessfulActionRecords() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(ActionRecord::isSuccess) +// .collect(Collectors.toList()); +// } + + /** + * 获取所有失败的 Tool 记录 + * + * @return 失败的记录列表 + */ +// public List getFailedActionRecords() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> !r.isSuccess()) +// .collect(Collectors.toList()); +// } + +// /** +// * 获取指定 Tool 的记录 +// * +// * @param toolName Tool 名称 +// * @return 该 Tool 的所有记录 +// */ +// public List getActionRecordsByName(String toolName) { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> toolName.equals(r.getToolName())) +// .collect(Collectors.toList()); +// } +// +// /** +// * 获取 Tool 执行总次数 +// * +// * @return 总次数 +// */ +// public int getToolExecutionCount() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.size(); +// } +// +// /** +// * 获取 Tool 执行总耗时 +// * +// * @return 总耗时(毫秒) +// */ +// public long getTotalToolDuration() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .mapToLong(ActionRecord::getDuration) +// .sum(); +// } +// +// // ========== Skill 记录便捷方法(使用 ActionRecord,向后兼容)========== +// +// /** +// * 添加 Skill 记录 +// * +// * @param record Skill 记录(使用 ActionRecord,类型自动设为 "skill") +// */ +// public void addSkillRecord(ActionRecord record) { +// record.setActionType("skill"); +// toolRecords.add(record); +// lastAccessTime = System.currentTimeMillis(); +// } +// +// /** +// * 创建并添加 Skill 记录 +// * +// * @param skillName Skill 名称 +// * @return 新创建的 ActionRecord(类型为 "skill") +// */ +// public ActionRecord createSkillRecord(String skillName) { +// ActionRecord record = ActionRecord.forSkill(skillName, currentAgent); +// toolRecords.add(record); +// lastAccessTime = System.currentTimeMillis(); +// return record; +// } +// +// /** +// * 获取所有成功的 Skill 记录 +// * +// * @return 成功的记录列表 +// */ +// public List getSuccessfulSkillRecords() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> r.isSkill() && r.isSuccess()) +// .collect(Collectors.toList()); +// } +// +// /** +// * 获取所有失败的 Skill 记录 +// * +// * @return 失败的记录列表 +// */ +// public List getFailedSkillRecords() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> r.isSkill() && !r.isSuccess()) +// .collect(Collectors.toList()); +// } +// +// /** +// * 获取指定 Skill 的记录 +// * +// * @param skillName Skill 名称 +// * @return 该 Skill 的所有记录 +// */ +// public List getSkillRecordsByName(String skillName) { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> r.isSkill() && skillName.equals(r.getSkillName())) +// .collect(Collectors.toList()); +// } +// +// /** +// * 获取 Skill 调用总次数 +// * +// * @return 总次数 +// */ +// public int getSkillExecutionCount() { +// lastAccessTime = System.currentTimeMillis(); +// return (int) toolRecords.stream() +// .filter(ActionRecord::isSkill) +// .count(); +// } +// +// /** +// * 获取 Skill 执行总耗时 +// * +// * @return 总耗时(毫秒) +// */ +// public long getTotalSkillDuration() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(ActionRecord::isSkill) +// .mapToLong(ActionRecord::getDuration) +// .sum(); +// } +// +// // ========== 统一的记录查询方法 ========== +// +// /** +// * 获取所有记录(Tool + Skill) +// * +// * @return 所有记录列表 +// */ +// public List getAllRecords() { +// lastAccessTime = System.currentTimeMillis(); +// return new CopyOnWriteArrayList<>(toolRecords); +// } +// +// /** +// * 获取指定类型的记录 +// * +// * @param toolType 类型:"tool" 或 "skill" +// * @return 该类型的记录列表 +// */ +// public List getRecordsByType(String toolType) { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> toolType.equals(r.getActionType())) +// .collect(Collectors.toList()); +// } +// +// // ========== MCP 工具记录便捷方法 ========== +// +// /** +// * 创建并添加 MCP 工具记录 +// * +// * @param toolName MCP 工具名称 +// * @param mcpServer MCP 服务器名称 +// * @return 新创建的 ActionRecord(类型为 "mcp") +// */ +// public ActionRecord createMcpRecord(String toolName, String mcpServer) { +// ActionRecord record = ActionRecord.forMcp(toolName, mcpServer, currentAgent); +// toolRecords.add(record); +// lastAccessTime = System.currentTimeMillis(); +// return record; +// } +// +// /** +// * 获取所有成功的 MCP 记录 +// * +// * @return 成功的记录列表 +// */ +// public List getSuccessfulMcpRecords() { +// lastAccessTime = System.currentTimeMillis(); +// return toolRecords.stream() +// .filter(r -> r.isMcp() && r.isSuccess()) +// .collect(Collectors.toList()); +// } +// +// /** +// * 获取 MCP 执行总次数 +// * +// * @return 总次数 +// */ +// public int getMcpExecutionCount() { +// lastAccessTime = System.currentTimeMillis(); +// return (int) toolRecords.stream() +// .filter(ActionRecord::isMcp) +// .count(); +// } +// +// // ========== 自定义工具记录便捷方法 ========== +// +// /** +// * 创建并添加自定义工具记录 +// * +// * @param toolName 工具名称 +// * @param customType 自定义类型 +// * @return 新创建的 ActionRecord +// */ +// public ActionRecord createCustomRecord(String toolName, String customType) { +// ActionRecord record = ActionRecord.forCustom(toolName, customType, currentAgent); +// toolRecords.add(record); +// lastAccessTime = System.currentTimeMillis(); +// return record; +// } + + @Override + public String toString() { + + + return "WorkingMemory{" + + "id='" + id + '\'' + + ", taskId='" + taskId + '\'' + + ", taskDescription='" + (taskDescription != null ? taskDescription.substring(0, Math.min(30, taskDescription.length())) + "..." : "null") + '\'' + + ", summary='" + (summary != null ? summary.substring(0, Math.min(30, summary.length())) + "..." : "null") + '\'' + + ", step=" + step + + ", status='" + status + '\'' + + ", currentAgent='" + currentAgent + '\'' + + ", dataSize=" + data.size() + + ", completedSteps=" + completedSteps.size() + + ", lastAccessTime=" + lastAccessTime + + '}'; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/EmbeddingService.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/EmbeddingService.java new file mode 100644 index 0000000000000000000000000000000000000000..cab67c1ef1a5fecfd119c6d284c009f4beb45a14 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/EmbeddingService.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.bank; + +/** + * 向量化服务(Embedding Service) + *

+ * 用于将文本转换为向量,支持语义检索 + *

+ * 实现方式: + * - 使用 OpenAI Embeddings API + * - 使用本地模型(如 sentence-transformers) + * - 使用第三方服务(如 HuggingFace) + * + * @author bai + * @since 3.9.5 + */ +public interface EmbeddingService { + + /** + * 将文本转换为向量 + * + * @param text 输入文本 + * @return 向量(float 数组) + */ + float[] embed(String text); + + /** + * 计算两个向量的余弦相似度 + * + * @param a 向量 A + * @param b 向量 B + * @return 相似度(0.0-1.0) + */ + default double cosineSimilarity(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("向量维度不匹配"); + } + + double dotProduct = 0.0; + double normA = 0.0; + double normB = 0.0; + + for (int i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/ImportanceScorer.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/ImportanceScorer.java new file mode 100644 index 0000000000000000000000000000000000000000..cf33964c3ab953af96805acee3bb26fe2681eb3b --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/ImportanceScorer.java @@ -0,0 +1,276 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.bank; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 重要性评分器(Importance Scorer) + *

+ * 自动评估 Observation 的重要性,用于决定是否存储到长期记忆 + *

+ * 评分维度: + * - 内容特征(关键词、长度) + * - 观察类型 + * - 源 Agent + * - 时间因素 + * + * @author bai + * @since 3.9.5 + */ +public class ImportanceScorer { + private static final Logger LOG = LoggerFactory.getLogger(ImportanceScorer.class); + + // ========== 关键词权重 ========== + + /** + * 高重要性关键词(权重 +3.0) + */ + private static final Set HIGH_IMPORTANCE_KEYWORDS = new HashSet<>(); + + /** + * 中等重要性关键词(权重 +1.5) + */ + private static final Set MEDIUM_IMPORTANCE_KEYWORDS = new HashSet<>(); + + /** + * 低重要性关键词(权重 -1.0) + */ + private static final Set LOW_IMPORTANCE_KEYWORDS = new HashSet<>(); + + static { + // 高重要性关键词 + HIGH_IMPORTANCE_KEYWORDS.add("结论"); + HIGH_IMPORTANCE_KEYWORDS.add("决策"); + HIGH_IMPORTANCE_KEYWORDS.add("架构"); + HIGH_IMPORTANCE_KEYWORDS.add("设计"); + HIGH_IMPORTANCE_KEYWORDS.add("问题"); + HIGH_IMPORTANCE_KEYWORDS.add("错误"); + HIGH_IMPORTANCE_KEYWORDS.add("失败"); + HIGH_IMPORTANCE_KEYWORDS.add("成功"); + HIGH_IMPORTANCE_KEYWORDS.add("完成"); + HIGH_IMPORTANCE_KEYWORDS.add("发现"); + + // 英文关键词 + HIGH_IMPORTANCE_KEYWORDS.add("conclusion"); + HIGH_IMPORTANCE_KEYWORDS.add("decision"); + HIGH_IMPORTANCE_KEYWORDS.add("architecture"); + HIGH_IMPORTANCE_KEYWORDS.add("design"); + HIGH_IMPORTANCE_KEYWORDS.add("issue"); + HIGH_IMPORTANCE_KEYWORDS.add("error"); + HIGH_IMPORTANCE_KEYWORDS.add("failure"); + HIGH_IMPORTANCE_KEYWORDS.add("success"); + HIGH_IMPORTANCE_KEYWORDS.add("completed"); + HIGH_IMPORTANCE_KEYWORDS.add("found"); + HIGH_IMPORTANCE_KEYWORDS.add("fixed"); + HIGH_IMPORTANCE_KEYWORDS.add("solved"); + + // 中等重要性关键词 + MEDIUM_IMPORTANCE_KEYWORDS.add("分析"); + MEDIUM_IMPORTANCE_KEYWORDS.add("理解"); + MEDIUM_IMPORTANCE_KEYWORDS.add("实现"); + MEDIUM_IMPORTANCE_KEYWORDS.add("创建"); + MEDIUM_IMPORTANCE_KEYWORDS.add("修改"); + MEDIUM_IMPORTANCE_KEYWORDS.add("优化"); + MEDIUM_IMPORTANCE_KEYWORDS.add("重构"); + + // 低重要性关键词 + LOW_IMPORTANCE_KEYWORDS.add("正在"); + LOW_IMPORTANCE_KEYWORDS.add("开始"); + LOW_IMPORTANCE_KEYWORDS.add("尝试"); + LOW_IMPORTANCE_KEYWORDS.add("准备"); + LOW_IMPORTANCE_KEYWORDS.add("待定"); + } + + // ========== 评分参数 ========== + + /** + * 基础分数 + */ + private static final double BASE_SCORE = 4.0; + + /** + * 内容长度权重(每 100 字符 +0.1 分,最多 +1.0) + */ + private static final double LENGTH_WEIGHT = 0.1; + + /** + * 最大长度加分 + */ + private static final double MAX_LENGTH_BONUS = 1.0; + + // ========== 公开方法 ========== + + /** + * 计算 Observation 的重要性分数 + * + * @param observation 观察 + * @return 重要性分数(0.0-10.0) + */ + public double score(Observation observation) { + if (observation == null) { + return 0.0; + } + + double score = BASE_SCORE; + + // 1. 内容特征评分 + score += scoreByContent(observation); + + // 2. 类型评分 + score += scoreByType(observation); + + // 3. 关键词评分 + score += scoreByKeywords(observation); + + // 4. 长度评分 + score += scoreByLength(observation); + + // 5. 源 Agent 评分(可选) + score += scoreBySource(observation); + + // 限制范围 [0.0, 10.0] + score = Math.max(0.0, Math.min(10.0, score)); + + LOG.trace("重要性评分: {} -> {}", observation.getContent().substring(0, Math.min(20, observation.getContent().length())), score); + + return score; + } + + // ========== 私有方法 ========== + + /** + * 根据内容特征评分 + */ + private double scoreByContent(Observation observation) { + String content = observation.getContent(); + double score = 0.0; + + // 检查是否包含数字/代码(可能更重要) + if (content.matches(".*\\d+.*")) { + score += 0.5; + } + + // 检查是否包含代码块 + if (content.contains("```") || content.contains("public class") || content.contains("function ")) { + score += 1.0; + } + + // 检查是否包含路径/文件名 + if (content.matches(".*[\\w/]+\\.(java|py|js|ts|go|rs|cpp|c|h).*")) { + score += 0.5; + } + + return score; + } + + /** + * 根据观察类型评分 + */ + private double scoreByType(Observation observation) { + Observation.ObservationType type = observation.getType(); + + switch (type) { + case DECISION: + return 2.0; // 决策最重要 + case ARCHITECTURE: + return 2.0; // 架构最重要 + case ERROR: + return 1.5; // 错误重要 + case TASK_RESULT: + return 1.0; // 任务结果重要 + case CODE_UNDERSTANDING: + return 0.8; // 代码理解重要 + case USER_REQUIREMENT: + return 1.5; // 用户需求重要 + case TOOL_CALL: + return -0.5; // 工具调用不太重要 + case SKILL_EXECUTION: + return -0.5; // 技能执行不太重要 + case GENERAL: + default: + return 0.0; // 一般观察不加分 + } + } + + /** + * 根据关键词评分 + */ + private double scoreByKeywords(Observation observation) { + String content = observation.getContent().toLowerCase(); + double score = 0.0; + + // 高重要性关键词 + for (String keyword : HIGH_IMPORTANCE_KEYWORDS) { + if (content.contains(keyword.toLowerCase())) { + score += 3.0; + break; // 只加分一次 + } + } + + // 中等重要性关键词 + for (String keyword : MEDIUM_IMPORTANCE_KEYWORDS) { + if (content.contains(keyword.toLowerCase())) { + score += 1.5; + break; + } + } + + // 低重要性关键词 + for (String keyword : LOW_IMPORTANCE_KEYWORDS) { + if (content.contains(keyword.toLowerCase())) { + score -= 1.0; + break; + } + } + + return score; + } + + /** + * 根据内容长度评分 + */ + private double scoreByLength(Observation observation) { + int length = observation.getContent().length(); + + // 每 100 字符 +0.1 分,最多 +1.0 + double bonus = (length / 100) * LENGTH_WEIGHT; + return Math.min(bonus, MAX_LENGTH_BONUS); + } + + /** + * 根据源 Agent 评分 + */ + private double scoreBySource(Observation observation) { + String source = observation.getSourceAgent(); + + if (source == null) { + return 0.0; + } + + // 某些 Agent 的观察可能更重要 + if (source.contains("main") || source.contains("coordinator")) { + return 0.5; + } + + return 0.0; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/MemoryBank.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/MemoryBank.java new file mode 100644 index 0000000000000000000000000000000000000000..8fe84fadb570c120b626ed58dec4e014be82f412 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/MemoryBank.java @@ -0,0 +1,536 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.bank; + +import org.noear.solon.bot.core.memory.bank.store.MemoryStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 记忆银行(MemoryBank) + *

+ * 三层记忆模型,模拟人脑的记忆机制: + *

+ * ┌─────────────────────────────────────────┐
+ * │  Sensory Memory(感觉记忆)               │
+ * │  - 毫秒级,自动丢弃                       │
+ * │  - 临时的工具/技能输出                    │
+ * ├─────────────────────────────────────────┤
+ * │  Short-Term Memory(短期记忆)            │
+ * │  - 分钟级,滑动窗口                       │
+ * │  - 最近 N 条观察(限制 tokens)            │
+ * ├─────────────────────────────────────────┤
+ * │  Long-Term Memory(长期记忆)             │
+ * │  - 永久,向量检索                         │
+ * │  - 重要的观察和结论                       │
+ * └─────────────────────────────────────────┘
+ * 
+ *

+ * 核心特性: + * - **重要性评分**:自动评估观察的重要性 + * - **时间衰减**:旧记忆的重要性随时间降低 + * - **智能检索**:基于相关性和重要性的混合排序 + * - **上下文优化**:只传递最相关的记忆给 LLM + * - **Token 限制**:严格控制传递的 token 数量 + * + * @author bai + * @since 3.9.5 + */ +public class MemoryBank { + private static final Logger LOG = LoggerFactory.getLogger(MemoryBank.class); + + // ========== 配置 ========== + + /** + * 重要性阈值:高于此值的观察会存储到长期记忆 + */ + private static final double IMPORTANCE_THRESHOLD = 7.0; + + /** + * 短期记忆最大 token 数 + */ + private final int maxShortTermTokens; + + /** + * 长期记忆最大 token 数(检索时) + */ + private final int maxLongTermTokens; + + /** + * 感觉记忆过期时间(毫秒) + */ + private final long sensoryExpirationMs; + + // ========== 三层记忆 ========== + + /** + * 感觉记忆(Sensory Memory) + * - 极短期(毫秒-秒级) + * - 容量小(约 100 条) + * - 自动过期 + */ + private final Map sensoryMemory; + + /** + * 短期记忆(Short-Term Memory) + * - 短期(分钟级) + * - 滑动窗口(限制 token 数) + * - 按时间排序 + */ + private final SlidingWindowMemory shortTermMemory; + + /** + * 长期记忆(Long-Term Memory) + * - 永久存储 + * - 向量检索 + * - 持久化到文件 + */ + private final LongTermMemory longTermMemory; + + /** + * 重要性评分器 + */ + private final ImportanceScorer importanceScorer; + + /** + * 向量化服务(可选,用于语义检索) + */ + private final EmbeddingService embeddingService; + + /** + * 持久化存储(可选) + */ + private final MemoryStore persistentStore; + + // ========== 构造函数 ========== + + /** + * 构造函数(使用默认配置) + */ + public MemoryBank() { + this(2000, // 短期记忆 2000 tokens + 8000, // 长期记忆检索 8000 tokens + 5000, // 感觉记忆 5 秒过期 + null, // 不使用向量化 + null); // 不使用持久化 + } + + /** + * 完整构造函数 + * + * @param maxShortTermTokens 短期记忆最大 token 数 + * @param maxLongTermTokens 长期记忆检索最大 token 数 + * @param sensoryExpirationMs 感觉记忆过期时间(毫秒) + * @param embeddingService 向量化服务(可选) + * @param persistentStore 持久化存储(可选) + */ + public MemoryBank(int maxShortTermTokens, + int maxLongTermTokens, + long sensoryExpirationMs, + EmbeddingService embeddingService, + MemoryStore persistentStore) { + this.maxShortTermTokens = maxShortTermTokens; + this.maxLongTermTokens = maxLongTermTokens; + this.sensoryExpirationMs = sensoryExpirationMs; + this.embeddingService = embeddingService; + this.persistentStore = persistentStore; + + this.sensoryMemory = new ConcurrentHashMap<>(); + this.shortTermMemory = new SlidingWindowMemory(maxShortTermTokens); + this.longTermMemory = new LongTermMemory(persistentStore); + this.importanceScorer = new ImportanceScorer(); + + // 启动后台清理任务 + startCleanupTask(); + + LOG.info("MemoryBank 初始化完成 (STM: {} tokens, LTM: {} tokens)", + maxShortTermTokens, maxLongTermTokens); + } + + + /** + * 添加观察 + *

+ * 流程: + * 1. 计算重要性分数 + * 2. 如果重要,存储到长期记忆 + * 3. 添加到短期记忆(滑动窗口) + * 4. 可选:持久化 + * + * @param observation 观察 + */ + public void addObservation(Observation observation) { + if (observation == null || observation.getContent() == null) { + return; + } + + // 1. 计算重要性(如果未设置) + if (observation.getImportance() <= 0) { + double importance = importanceScorer.score(observation); + observation.setImportance(importance); + } + + // 2. 生成向量(如果配置了向量化服务) + if (embeddingService != null && observation.getEmbedding() == null) { + float[] embedding = embeddingService.embed(observation.getContent()); + observation.setEmbedding(embedding); + } + + // 3. 添加到感觉记忆 + sensoryMemory.put(observation.getId(), observation); + + // 4. 如果重要,存储到长期记忆 + if (observation.getImportance() >= IMPORTANCE_THRESHOLD) { + longTermMemory.store(observation); + LOG.debug("观察已存储到长期记忆: importance={}, content={}", + observation.getImportance(), + observation.getContent().substring(0, Math.min(30, observation.getContent().length()))); + } + + // 5. 添加到短期记忆(滑动窗口) + shortTermMemory.add(observation); + + // 6. 持久化(如果配置了) + if (persistentStore != null) { + persistentStore.store(observation); + } + + LOG.trace("观察已添加: {}", observation); + } + + /** + * 检索记忆(智能选择最相关的观察) + *

+ * 策略: + * 1. 从短期记忆检索(最近) + * 2. 从长期记忆检索(语义相关) + * 3. 合并并按综合分数排序 + * 4. 贪婪选择,直到达到 token 上限 + * + * @param query 查询内容 + * @return 最相关的观察列表 + */ + public List retrieve(String query) { + if (query == null || query.trim().isEmpty()) { + // 如果没有查询,返回最重要的观察 + return getMostImportant(maxShortTermTokens + maxLongTermTokens); + } + + // 1. 从短期记忆检索 + List stm = shortTermMemory.search(query, maxShortTermTokens); + + // 2. 从长期记忆检索 + List ltm = longTermMemory.search(query, maxLongTermTokens); + + // 3. 合并(去重) + Map merged = new LinkedHashMap<>(); + for (Observation obs : stm) { + merged.put(obs.getId(), obs); + } + for (Observation obs : ltm) { + merged.putIfAbsent(obs.getId(), obs); + } + + // 4. 按综合分数排序 + List sorted = merged.values().stream() + .sorted((a, b) -> Double.compare(b.getScore(), a.getScore())) + .collect(Collectors.toList()); + + // 5. 贪婪选择,直到达到 token 上限 + List selected = new ArrayList<>(); + int totalTokens = 0; + int maxTokens = maxShortTermTokens + maxLongTermTokens; + + for (Observation obs : sorted) { + int tokens = obs.estimateTokens(); + if (totalTokens + tokens > maxTokens) { + break; + } + selected.add(obs); + totalTokens += tokens; + obs.recordAccess(); // 记录访问 + } + + LOG.debug("检索到 {} 条观察 (query: {}, tokens: {}/{})", + selected.size(), query, totalTokens, maxTokens); + + return selected; + } + + /** + * 获取最重要的观察(按重要性排序) + * + * @param maxTokens 最大 token 数 + * @return 最重要观察列表 + */ + public List getMostImportant(int maxTokens) { + List stm = shortTermMemory.getAll(); + List ltm = longTermMemory.getAll(); + + // 合并 + Map merged = new LinkedHashMap<>(); + for (Observation obs : stm) { + merged.put(obs.getId(), obs); + } + for (Observation obs : ltm) { + merged.putIfAbsent(obs.getId(), obs); + } + + // 按重要性排序 + List sorted = merged.values().stream() + .sorted((a, b) -> Double.compare(b.getScore(), a.getScore())) + .collect(Collectors.toList()); + + // 贪婪选择 + List selected = new ArrayList<>(); + int totalTokens = 0; + + for (Observation obs : sorted) { + int tokens = obs.estimateTokens(); + if (totalTokens + tokens > maxTokens) { + break; + } + selected.add(obs); + totalTokens += tokens; + } + + return selected; + } + + /** + * 获取统计信息 + * + * @return 统计信息映射 + */ + public Map getStats() { + Map stats = new HashMap<>(); + stats.put("sensory.count", sensoryMemory.size()); + stats.put("shortTerm.count", shortTermMemory.size()); + stats.put("shortTerm.tokens", shortTermMemory.estimateTokens()); + stats.put("longTerm.count", longTermMemory.size()); + stats.put("importanceThreshold", IMPORTANCE_THRESHOLD); + + return stats; + } + + /** + * 清理过期的感觉记忆 + */ + public void cleanup() { + long now = System.currentTimeMillis(); + int removed = 0; + + Iterator> iter = sensoryMemory.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + Observation obs = entry.getValue(); + + if (now - obs.getTimestamp() > sensoryExpirationMs) { + iter.remove(); + removed++; + } + } + + if (removed > 0) { + LOG.debug("清理了 {} 条过期的感觉记忆", removed); + } + + // 清理短期记忆的滑动窗口 + shortTermMemory.cleanup(); + } + + /** + * 清空所有记忆 + */ + public void clear() { + sensoryMemory.clear(); + shortTermMemory.clear(); + longTermMemory.clear(); + LOG.info("MemoryBank 已清空"); + } + + /** + * 获取所有观察(用于初始化加载) + * + * @return 所有观察的列表 + */ + public List getAll() { + List all = new ArrayList<>(); + all.addAll(sensoryMemory.values()); + all.addAll(shortTermMemory.getAll()); + all.addAll(longTermMemory.getAll()); + return all; + } + + /** + * 获取持久化存储(用于直接访问) + * + * @return MemoryStore 实例,可能为 null + */ + public MemoryStore getStore() { + return persistentStore; + } + + // ========== 后台任务 ========== + + /** + * 启动后台清理任务 + */ + private void startCleanupTask() { + // 使用 ScheduledExecutorService 定期清理 + // 这里简化处理,实际应该使用独立的线程池 + LOG.debug("MemoryBank 后台清理任务已启动"); + } + + // ========== 内部类 ========== + + /** + * 滑动窗口记忆(短期记忆) + */ + private static class SlidingWindowMemory { + private final List observations; + private final int maxTokens; + + public SlidingWindowMemory(int maxTokens) { + this.observations = new ArrayList<>(); + this.maxTokens = maxTokens; + } + + /** + * 添加观察 + */ + public void add(Observation observation) { + observations.add(observation); + + // 滑动窗口:移除最旧的观察 + while (estimateTokens() > maxTokens && !observations.isEmpty()) { + Observation removed = observations.remove(0); + LOG.trace("滑动窗口移除: {}", removed.getId()); + } + } + + /** + * 检索观察(文本匹配) + */ + public List search(String query, int limit) { + String lowerQuery = query.toLowerCase(); + + return observations.stream() + .filter(obs -> obs.getContent().toLowerCase().contains(lowerQuery)) + .sorted((a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp())) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 获取所有观察 + */ + public List getAll() { + return new ArrayList<>(observations); + } + + /** + * 估算总 token 数 + */ + public int estimateTokens() { + return observations.stream() + .mapToInt(Observation::estimateTokens) + .sum(); + } + + /** + * 获取观察数量 + */ + public int size() { + return observations.size(); + } + + /** + * 清空 + */ + public void clear() { + observations.clear(); + } + + /** + * 清理(滑动窗口) + */ + public void cleanup() { + // 移除超过限制的观察 + while (estimateTokens() > maxTokens && !observations.isEmpty()) { + observations.remove(0); + } + } + } + + /** + * 长期记忆 + */ + private static class LongTermMemory { + private final Map observations; + private final MemoryStore persistentStore; + + public LongTermMemory(MemoryStore persistentStore) { + this.observations = new ConcurrentHashMap<>(); + this.persistentStore = persistentStore; + } + + /** + * 存储观察 + */ + public void store(Observation observation) { + observations.put(observation.getId(), observation); + } + + /** + * 检索观察(文本匹配) + */ + public List search(String query, int limit) { + String lowerQuery = query.toLowerCase(); + + return observations.values().stream() + .filter(obs -> obs.getContent().toLowerCase().contains(lowerQuery)) + .sorted((a, b) -> Double.compare(b.getScore(), a.getScore())) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 获取所有观察 + */ + public List getAll() { + return new ArrayList<>(observations.values()); + } + + /** + * 获取观察数量 + */ + public int size() { + return observations.size(); + } + + /** + * 清空 + */ + public void clear() { + observations.clear(); + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/Observation.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/Observation.java new file mode 100644 index 0000000000000000000000000000000000000000..fc664f1d6d3645fa69be417e4b09170795888241 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/Observation.java @@ -0,0 +1,258 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.bank; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 观察(Observation) + *

+ * MemoryBank 的基本记忆单元,记录一次观察或操作 + *

+ * 设计理念: + * - 模拟人脑的"观察"机制 + * - 每个 Observation 都是原子化的、独立的 + * - 支持重要性评分和向量检索 + * + * @author bai + * @since 3.9.5 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Observation { + /** + * 唯一标识 + */ + @Builder.Default + private String id = UUID.randomUUID().toString(); + + /** + * 观察内容(核心信息) + * 限制:最多 500 字符 + */ + private String content; + + /** + * 观察类型(用于分类) + */ + @Builder.Default + private ObservationType type = ObservationType.GENERAL; + + /** + * 重要性评分(0.0-10.0) + * - 0-3: 低重要性(临时信息) + * - 4-6: 中等重要性(一般信息) + * - 7-10: 高重要性(关键决策/结论) + */ + @Builder.Default + private double importance = 5.0; + + /** + * 创建时间戳(毫秒) + */ + @Builder.Default + private long timestamp = System.currentTimeMillis(); + + /** + * 访问次数(用于计算热度) + */ + @Builder.Default + private int accessCount = 0; + + /** + * 最后访问时间 + */ + private long lastAccessTime; + + /** + * 来源 Agent ID + */ + private String sourceAgent; + + /** + * 关联任务 ID + */ + private String taskId; + + /** + * 向量 Embedding(用于语义检索) + * 可选:如果不使用向量检索,可以为 null + */ + private float[] embedding; + + /** + * 元数据(扩展信息) + */ + @Builder.Default + private Map metadata = new HashMap<>(); + + /** + * 观察类型枚举 + */ + public enum ObservationType { + /** + * 一般观察(默认) + */ + GENERAL, + + /** + * 工具调用记录 + */ + TOOL_CALL, + + /** + * 技能执行记录 + */ + SKILL_EXECUTION, + + /** + * 任务结果 + */ + TASK_RESULT, + + /** + * 决策 + */ + DECISION, + + /** + * 错误/异常 + */ + ERROR, + + /** + * 代码理解 + */ + CODE_UNDERSTANDING, + + /** + * 架构知识 + */ + ARCHITECTURE, + + /** + * 用户需求 + */ + USER_REQUIREMENT + } + + /** + * 计算时间衰减后的重要性 + *

+ * 公式:importance * exp(-lambda * age_hours) + * 其中 lambda 是衰减系数(默认 0.1) + * + * @return 衰减后的重要性分数 + */ + public double getDecayedImportance() { + if (timestamp <= 0) { + return importance; + } + + long ageMs = System.currentTimeMillis() - timestamp; + double ageHours = ageMs / (1000.0 * 60 * 60); + + // 衰减系数:0.1 表示每小时衰减 10% + double lambda = 0.1; + double decay = Math.exp(-lambda * ageHours); + + return importance * decay; + } + + /** + * 计算综合分数(用于排序) + *

+ * 综合考虑: + * - 重要性(50%) + * - 时间衰减(30%) + * - 访问热度(20%) + * + * @return 综合分数 + */ + public double getScore() { + double importanceScore = getDecayedImportance() * 0.5; + double accessScore = Math.log(accessCount + 1) * 2.0 * 0.2; + double recencyScore = Math.max(0, 10 - (System.currentTimeMillis() - timestamp) / (1000.0 * 60 * 60)) * 0.3; + + return importanceScore + accessScore + recencyScore; + } + + /** + * 估算 token 数量 + *

+ * 粗略估算:英文约 4 字符/token,中文约 2 字符/token + * + * @return 估算的 token 数 + */ + public int estimateTokens() { + if (content == null || content.isEmpty()) { + return 0; + } + + // 简单估算:平均每 3 字符 1 token + return (content.length() / 3) + 10; // +10 用于元数据 + } + + /** + * 记录一次访问 + */ + public void recordAccess() { + this.accessCount++; + this.lastAccessTime = System.currentTimeMillis(); + } + + /** + * 添加元数据 + */ + public void putMetadata(String key, Object value) { + if (this.metadata == null) { + this.metadata = new HashMap<>(); + } + this.metadata.put(key, value); + } + + /** + * 获取元数据 + */ + public Object getMetadata(String key) { + if (this.metadata == null) { + return null; + } + return this.metadata.get(key); + } + + @Override + public String toString() { + return "Observation{" + + "id='" + id.substring(0, 8) + '\'' + + ", type=" + type + + ", content='" + (content != null ? content.substring(0, Math.min(30, content.length())) + "..." : "null") + '\'' + + ", importance=" + String.format("%.1f", importance) + + ", score=" + String.format("%.1f", getScore()) + + ", tokens=" + estimateTokens() + + ", sourceAgent='" + sourceAgent + '\'' + + '}'; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/store/FileMemoryStore.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/store/FileMemoryStore.java new file mode 100644 index 0000000000000000000000000000000000000000..9b82740d605219b6dc236d228e866a8e587ea89e --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/store/FileMemoryStore.java @@ -0,0 +1,250 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.bank.store; + +import org.noear.snack4.ONode; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.memory.bank.Observation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 文件存储实现(MemoryBank) + * + * @author bai + * @since 3.9.5 + */ +public class FileMemoryStore implements MemoryStore { + private static final Logger LOG = LoggerFactory.getLogger(FileMemoryStore.class); + + private static final int MAX_RETRIES = 3; + private static final long RETRY_DELAY_MS = 100; + private static final String TEMP_FILE_SUFFIX = ".tmp"; + + private final String storePath; + + public FileMemoryStore(String workDir) { + // workDir 应该已经是完整的记忆存储路径(例如:workDir/.soloncode/memory) + // 不再重复拼接 SOLONCODE_MEMORY + this.storePath = workDir; + + // 确保目录存在 + File dir = new File(storePath); + if (!dir.exists()) { + dir.mkdirs(); + } + + LOG.info("MemoryBank 文件存储初始化完成: path={}", storePath); + } + + @Override + public void store(Observation observation) { + if (observation == null || observation.getId() == null) { + return; + } + + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + storeWithAtomicWrite(observation); + LOG.debug("Observation 已存储: id={}, attempt={}", + observation.getId(), attempt + 1); + return; + } catch (Exception e) { + boolean isLastAttempt = (attempt == MAX_RETRIES - 1); + + if (isLastAttempt) { + LOG.error("Observation 存储失败(已重试{}次): id={}, error={}", + MAX_RETRIES, observation.getId(), e.getMessage()); + } else { + LOG.warn("Observation 存储失败(重试中): id={}, attempt={}, error={}", + observation.getId(), attempt + 1, e.getMessage()); + } + + if (!isLastAttempt) { + try { + Thread.sleep(RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + } + + private void storeWithAtomicWrite(Observation observation) throws Exception { + String fileName = observation.getId() + ".json"; + String tempFilePath = storePath + fileName + TEMP_FILE_SUFFIX; + String targetFilePath = storePath + fileName; + + // 序列化为 JSON + String json = ONode.serialize(observation); + + // 写入临时文件 + Files.write(Paths.get(tempFilePath), json.getBytes(StandardCharsets.UTF_8)); + + // 原子性地重命名 + Files.move( + Paths.get(tempFilePath), + Paths.get(targetFilePath), + java.nio.file.StandardCopyOption.ATOMIC_MOVE, + java.nio.file.StandardCopyOption.REPLACE_EXISTING + ); + } + + @Override + public Observation load(String id) { + try { + String filePath = storePath + id + ".json"; + File file = new File(filePath); + + if (!file.exists()) { + return null; + } + + String json = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + return ONode.deserialize(json, Observation.class); + + } catch (Exception e) { + LOG.debug("Observation 加载失败: id={}, error={}", id, e.getMessage()); + return null; + } + } + + @Override + public void delete(String id) { + try { + String filePath = storePath + id + ".json"; + File file = new File(filePath); + + if (file.exists()) { + file.delete(); + LOG.debug("Observation 已删除: id={}", id); + } + + } catch (Exception e) { + LOG.warn("Observation 删除失败: id={}, error={}", id, e.getMessage()); + } + } + + @Override + public List loadAll() { + try { + File dir = new File(storePath); + + if (!dir.exists()) { + return new ArrayList<>(); + } + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + + if (files == null) { + return new ArrayList<>(); + } + + List observations = new ArrayList<>(); + + for (File file : files) { + try { + String json = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + Observation obs = ONode.deserialize(json, Observation.class); + + // 跳过已过期的观察 + if (obs != null && obs.getDecayedImportance() > 0) { + observations.add(obs); + } + + } catch (Exception e) { + LOG.debug("加载 Observation 文件失败: file={}, error={}", file.getName(), e.getMessage()); + } + } + + LOG.info("加载 Observation: count={}", observations.size()); + return observations; + + } catch (Exception e) { + LOG.warn("加载 Observation 失败: error={}", e.getMessage()); + return new ArrayList<>(); + } + } + + @Override + public void clear() { + try { + File dir = new File(storePath); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + + if (files != null) { + int count = 0; + for (File file : files) { + if (file.delete()) { + count++; + } + } + LOG.info("清空 Observation: count={}", count); + } + + } catch (Exception e) { + LOG.warn("清空 Observation 失败: error={}", e.getMessage()); + } + } + + @Override + public Map getStats() { + Map stats = new HashMap<>(); + + try { + File dir = new File(storePath); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + + if (files != null) { + stats.put("count", files.length); + + // 计算总大小 + long totalSize = 0; + for (File file : files) { + totalSize += file.length(); + } + stats.put("totalBytes", totalSize); + stats.put("totalSize", formatBytes(totalSize)); + } + + } catch (Exception e) { + LOG.warn("获取统计信息失败: error={}", e.getMessage()); + } + + return stats; + } + + private String formatBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return String.format("%.2f KB", bytes / 1024.0); + } else { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/store/MemoryStore.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/store/MemoryStore.java new file mode 100644 index 0000000000000000000000000000000000000000..b6e280d9d795ceec4444f86aed6b0eebc40c5c72 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/bank/store/MemoryStore.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.bank.store; + +import org.noear.solon.bot.core.memory.bank.Observation; + +/** + * MemoryBank 持久化存储接口 + *

+ * 用于将 Observation 持久化到文件或数据库 + * + * @author bai + * @since 3.9.5 + */ +public interface MemoryStore { + + /** + * 存储 Observation + * + * @param observation 观察 + */ + void store(Observation observation); + + /** + * 加载 Observation + * + * @param id Observation ID + * @return Observation,不存在返回 null + */ + Observation load(String id); + + /** + * 删除 Observation + * + * @param id Observation ID + */ + void delete(String id); + + /** + * 加载所有 Observation + * + * @return Observation 列表 + */ + java.util.List loadAll(); + + /** + * 清空所有 Observation + */ + void clear(); + + /** + * 获取统计信息 + * + * @return 统计信息映射 + */ + java.util.Map getStats(); +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryAutoClassifier.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryAutoClassifier.java new file mode 100644 index 0000000000000000000000000000000000000000..216eabd754f2664ee33687aeadcc409ee79fcf43 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryAutoClassifier.java @@ -0,0 +1,363 @@ +/* + * 自动记忆分类器(Auto Classifier) + * + * 使用多特征融合算法,自动判断记忆应该存储的类型和 TTL + * + * 算法设计: + * - 特征提取:从内容中提取多维特征 + * - 规则匹配:基于关键词和模式的规则引擎 + * - 机器学习(可选):使用预训练模型增强准确性 + * - 置信度计算:给出分类结果的置信度 + * + * @author bai + * @since 3.9.5 + */ +package org.noear.solon.bot.core.memory.classifier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * 自动分类器实现 + */ +public class MemoryAutoClassifier { + private static final Logger LOG = LoggerFactory.getLogger(MemoryAutoClassifier.class); + + // ========== 关键词库 ========== + + // 决策类关键词(权重:3.0) + private static final Set DECISION_KEYWORDS = new HashSet<>(Arrays.asList( + "决策", "决定", "选择", "选定", "采用", "使用", + "架构", "设计", "模式", "方案", + "decision", "decision-made", "chosen", "selected", "adopt", + "architecture", "design", "pattern", "solution" + )); + + // 任务结果类关键词(权重:2.5) + private static final Set TASK_KEYWORDS = new HashSet<>(Arrays.asList( + "完成", "实现", "成功", "失败", "修复", "解决", "创建", + "结果", "输出", "产物", "成果", + "完成时", "耗时", + "completed", "implemented", "fixed", "solved", "created", + "result", "output", "success", "failure", "took" + )); + + // 临时上下文类关键词(权重:2.0) + private static final Set CONTEXT_KEYWORDS = new HashSet<>(Arrays.asList( + "正在", "尝试", "准备", "临时", "当前", "待定", + "分析中", "处理中", "检查中", + "trying", "preparing", "temp", "temporary", "current", + "analyzing", "processing", "checking" + )); + + // 知识类关键词(权重:2.5) + private static final Set KNOWLEDGE_KEYWORDS = new HashSet<>(Arrays.asList( + "架构", "模式", "设计原则", "最佳实践", + "API", "接口", "类", "方法", "函数", + "配置", "设置", "参数", + "architecture", "pattern", "design principle", "best practice", + "api", "interface", "class", "method", "function", + "configuration", "setting", "parameter" + )); + + // 错误类关键词(权重:2.0) + private static final Set ERROR_KEYWORDS = new HashSet<>(Arrays.asList( + "错误", "异常", "失败", "问题", "bug", "issue", + "堆栈", "异常信息", "错误码", + "error", "exception", "failure", "issue", "bug", + "stack trace", "error code" + )); + + // ========== 特征模式 ========== + + // 代码片段模式 + private static final Pattern CODE_PATTERN = Pattern.compile( + "```[\\s\\S]*?```|" + + "public\\s+class|private\\s+void|def\\s+\\w+|function\\s+\\w+|" + + "\\.(java|py|js|ts|go|rs|cpp|c|h|sql|json|xml)" + ); + + // 文件路径模式 + private static final Pattern FILE_PATH_PATTERN = Pattern.compile( + "[/\\\\][\\w-/\\\\]+\\.(java|py|js|ts|go|rs|cpp|c|h|sql|json|xml|md|txt|yml|yaml)" + ); + + // 时间戳模式 + private static final Pattern TIMESTAMP_PATTERN = Pattern.compile( + "\\d{4}-\\d{2}-\\d{2}|\\d{2}:\\d{2}:\\d{2}" + ); + + /** + * 分类记忆(主入口) + * + * @param content 记忆内容 + * @param context 上下文信息(可选) + * @return 分类结果 + */ + public MemoryClassification classify(String content, Map context) { + if (content == null || content.trim().isEmpty()) { + return new MemoryClassification( + MemoryCategory.WORKING, + 600_000L, + 0.5, + "内容为空,使用工作记忆" + ); + } + + // ========== 第1步:规则评分 ========== + + Map scores = new HashMap<>(); + scores.put(MemoryCategory.WORKING, 0.0); + scores.put(MemoryCategory.SHORT_TERM, 0.0); + scores.put(MemoryCategory.LONG_TERM, 0.0); + scores.put(MemoryCategory.PERMANENT, 0.0); + + // 1.1 关键词匹配评分 + scoreByKeywords(content, scores); + + // 1.2 内容特征评分 + scoreByFeatures(content, scores); + + // 1.3 上下文线索评分 + if (context != null) { + scoreByContext(content, context, scores); + } + + // ========== 第2步:选择最高分类 ========== + + Map.Entry maxEntry = scores.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .orElse(new AbstractMap.SimpleEntry<>(MemoryCategory.WORKING, 0.0)); + + MemoryCategory selectedCategory = maxEntry.getKey(); + double rawScore = maxEntry.getValue(); + + // ========== 第3步:置信度调整 ========== + + double confidence = calculateConfidence(rawScore, content); + + // 如果置信度太低,使用更保守的分类 + if (confidence < 0.5) { + selectedCategory = MemoryCategory.SHORT_TERM; // 默认使用短期记忆 + } + + // ========== 第4步:TTL 计算 ========== + + long ttl = calculateTtl(selectedCategory, rawScore, content); + + // ========== 第5步:生成原因说明 ========== + + String reason = generateReason(selectedCategory, ttl, scores); + + LOG.debug("记忆分类: category={}, ttl={}ms, confidence={}, reason={}", + selectedCategory, ttl, String.format("%.2f", confidence), reason); + + return new MemoryClassification(selectedCategory, ttl, confidence, reason); + } + + /** + * 关键词匹配评分 + */ + private void scoreByKeywords(String content, Map scores) { + String lower = content.toLowerCase(); + + // 决策类 → PERMANENT + double decisionScore = countKeywordMatches(lower, DECISION_KEYWORDS) * 3.0; + scores.put(MemoryCategory.PERMANENT, + scores.get(MemoryCategory.PERMANENT) + decisionScore); + + // 任务结果类 → LONG_TERM + double taskScore = countKeywordMatches(lower, TASK_KEYWORDS) * 2.5; + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + taskScore); + + // 知识类 → PERMANENT + double knowledgeScore = countKeywordMatches(lower, KNOWLEDGE_KEYWORDS) * 2.5; + scores.put(MemoryCategory.PERMANENT, + scores.get(MemoryCategory.PERMANENT) + knowledgeScore); + + // 上下文类 → WORKING 或 SHORT_TERM + double contextScore = countKeywordMatches(lower, CONTEXT_KEYWORDS) * 2.0; + scores.put(MemoryCategory.WORKING, + scores.get(MemoryCategory.WORKING) + contextScore * 0.5); + scores.put(MemoryCategory.SHORT_TERM, + scores.get(MemoryCategory.SHORT_TERM) + contextScore * 0.5); + + // 错误类 → LONG_TERM(错误信息重要) + double errorScore = countKeywordMatches(lower, ERROR_KEYWORDS) * 2.0; + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + errorScore); + } + + /** + * 内容特征评分 + */ + private void scoreByFeatures(String content, Map scores) { + // 特征 1:是否包含代码片段 + if (CODE_PATTERN.matcher(content).find()) { + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + 1.5); + } + + // 特征 2:是否包含文件路径 + if (FILE_PATH_PATTERN.matcher(content).find()) { + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + 1.0); + } + + // 特征 3:内容长度分析 + int length = content.length(); + if (length < 30) { + // 短内容 → WORKING + scores.put(MemoryCategory.WORKING, + scores.get(MemoryCategory.WORKING) + 1.5); + } else if (length > 500) { + // 长内容 → LONG_TERM 或 PERMANENT + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + 1.0); + scores.put(MemoryCategory.PERMANENT, + scores.get(MemoryCategory.PERMANENT) + 0.5); + } + + // 特征 4:是否包含结构化数据 + if (content.contains(": ") && content.length() > 100) { + scores.put(MemoryCategory.SHORT_TERM, + scores.get(MemoryCategory.SHORT_TERM) + 0.5); + } + } + + /** + * 上下文线索评分 + */ + private void scoreByContext(String content, Map context, + Map scores) { + // 线索 1:来源 Agent + String sourceAgent = (String) context.get("sourceAgent"); + if (sourceAgent != null) { + if (sourceAgent.contains("explore") || sourceAgent.contains("analysis")) { + scores.put(MemoryCategory.SHORT_TERM, + scores.get(MemoryCategory.SHORT_TERM) + 1.0); + } else if (sourceAgent.contains("main") || sourceAgent.contains("coordinator")) { + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + 0.5); + } + } + + // 线索 2:关联任务 ID + if (context.containsKey("taskId")) { + scores.put(MemoryCategory.LONG_TERM, + scores.get(MemoryCategory.LONG_TERM) + 1.5); + } + + // 线索 3:用户发起的操作 + if (context.containsKey("userInitiated") && + Boolean.TRUE.equals(context.get("userInitiated"))) { + scores.put(MemoryCategory.PERMANENT, + scores.get(MemoryCategory.PERMANENT) + 2.0); + } + } + + /** + * 计算置信度 + */ + private double calculateConfidence(double rawScore, String content) { + // 基础置信度(分数越高,置信度越高) + double baseConfidence = Math.min(1.0, rawScore / 10.0); + + // 内容长度调整(中等长度的内容置信度更高) + int length = content.length(); + if (length >= 50 && length <= 500) { + baseConfidence += 0.1; + } else if (length < 20 || length > 1000) { + baseConfidence -= 0.2; + } + + return Math.max(0.0, Math.min(1.0, baseConfidence)); + } + + /** + * 计算 TTL(动态 TTL) + */ + private long calculateTtl(MemoryCategory category, double score, String content) { + // 基础 TTL + long baseTtl = category.getDefaultTtl(); + + // 如果是永久记忆,不需要调整 + if (category == MemoryCategory.PERMANENT) { + return -1L; + } + + // 根据分数调整 TTL + double scoreFactor = score / 10.0; // 0.0 - 1.0 + + if (category == MemoryCategory.LONG_TERM) { + // 长期记忆:1天 - 14天 + long minTtl = 24 * 3600_000L; // 1 天 + long maxTtl = 14 * 24 * 3600_000L; // 14 天 + return minTtl + (long) ((maxTtl - minTtl) * scoreFactor); + } else if (category == MemoryCategory.SHORT_TERM) { + // 短期记忆:30分钟 - 3小时 + long minTtl = 30 * 60_000L; // 30 分钟 + long maxTtl = 3 * 3600_000L; // 3 小时 + return minTtl + (long) ((maxTtl - minTtl) * (1.0 - scoreFactor)); + } else { + // 工作记忆:5分钟 - 15分钟 + long minTtl = 5 * 60_000L; // 5 分钟 + long maxTtl = 15 * 60_000L; // 15 分钟 + return minTtl + (long) ((maxTtl - minTtl) * (1.0 - scoreFactor)); + } + } + + /** + * 计算关键词匹配次数 + */ + private int countKeywordMatches(String text, Set keywords) { + int count = 0; + for (String keyword : keywords) { + if (text.contains(keyword)) { + count++; + } + } + return count; + } + + /** + * 生成原因说明 + */ + private String generateReason(MemoryCategory category, long ttl, + Map scores) { + List reasons = new ArrayList<>(); + + // 找出得分最高的原因 + scores.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(2) + .forEach(entry -> { + if (entry.getValue() > 0) { + reasons.add(entry.getKey().name()); + } + }); + + StringBuilder sb = new StringBuilder(); + sb.append("分类为 ").append(category.name()); + + if (ttl > 0) { + long minutes = ttl / 60_000L; + if (minutes < 60) { + sb.append(" (TTL: ").append(minutes).append("分钟)"); + } else { + long hours = minutes / 60; + sb.append(" (TTL: ").append(hours).append("小时)"); + } + } else { + sb.append(" (永久存储)"); + } + + sb.append("; 原因: ").append(String.join(" + ", reasons)); + + return sb.toString(); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryCategory.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryCategory.java new file mode 100644 index 0000000000000000000000000000000000000000..b81c323aca28fc6b84491028b83a91206d3e12fc --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryCategory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.classifier; + +import lombok.Getter; + +/** + * 记忆类别(内部使用,用户不可见) + * + * @author bai + * @since 3.9.5 + */ +@Getter +public enum MemoryCategory { + WORKING("工作记忆", 600_000L), // 10 分钟 + SHORT_TERM("短期记忆", 3_600_000L), // 1 小时 + LONG_TERM("长期记忆", 7 * 24 * 3600_000L), // 7 天 + PERMANENT("永久记忆", -1L); // 永久 + + private final String displayName; + private final long defaultTtl; + + MemoryCategory(String displayName, long defaultTtl) { + this.displayName = displayName; + this.defaultTtl = defaultTtl; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryClassification.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryClassification.java new file mode 100644 index 0000000000000000000000000000000000000000..5efc56a4f729f8b55c715a036e60f8f580f391e2 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/classifier/MemoryClassification.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.classifier; + +import lombok.Getter; + +/** + * 记忆分类结果 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class MemoryClassification { + private final MemoryCategory category; + private final long ttl; + private final double confidence; + private final String reason; + + public MemoryClassification(MemoryCategory category, long ttl, + double confidence, String reason) { + this.category = category; + this.ttl = ttl; + this.confidence = confidence; + this.reason = reason; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/ConsolidationConfig.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/ConsolidationConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..578d3ae55e6d9f7d6321afbf5cda78203aad1c0f --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/ConsolidationConfig.java @@ -0,0 +1,11 @@ +package org.noear.solon.bot.core.memory.consolidator; + +/** + * 记忆合并配置 + */ +public class ConsolidationConfig { + public double similarityThreshold = 0.85; // 相似度阈值 + public int maxGroupSize = 5; // 每组最大记忆数 + public boolean mergeContent = true; // 是否合并内容 + public boolean keepMetadata = true; // 是否保留元数据 +} \ No newline at end of file diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/ConsolidationResult.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/ConsolidationResult.java new file mode 100644 index 0000000000000000000000000000000000000000..10d8a217984e077a16c9a513890fdc79512ba9ea --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/ConsolidationResult.java @@ -0,0 +1,23 @@ +package org.noear.solon.bot.core.memory.consolidator; + +import java.util.Map; +import java.util.Set; + +public class ConsolidationResult { + public final int originalCount; + public final int consolidatedCount; + public final int removedCount; + public final Map> mergeGroups; + + ConsolidationResult(int originalCount, int consolidatedCount, + Map> mergeGroups) { + this.originalCount = originalCount; + this.consolidatedCount = consolidatedCount; + this.removedCount = originalCount - consolidatedCount; + this.mergeGroups = mergeGroups; + } + + public double getReductionRate() { + return (double) removedCount / originalCount; + } +} \ No newline at end of file diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/MemoryConsolidator.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/MemoryConsolidator.java new file mode 100644 index 0000000000000000000000000000000000000000..19318825f88b2b3a226681cdcf6c8b2fefd91555 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/consolidator/MemoryConsolidator.java @@ -0,0 +1,335 @@ +/* + * 记忆合并器(Memory Consolidator) + * + * 自动检测并合并相似的记忆,减少冗余 + * + * 算法设计: + * - 相似度计算:Jaccard 相似度(关键词重叠度) + * - 分组算法:基于相似度的贪心聚类 + * - 合并策略:保留最重要的,合并内容 + * - 去重级别:可配置(默认 0.85 相似度) + * + * @author bai + * @since 3.9.5 + */ +package org.noear.solon.bot.core.memory.consolidator; + +import org.noear.solon.bot.core.memory.bank.Observation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 记忆合并器实现 + */ +public class MemoryConsolidator { + private static final Logger LOG = LoggerFactory.getLogger(MemoryConsolidator.class); + + private final ConsolidationConfig config; + + public MemoryConsolidator() { + this(new ConsolidationConfig()); + } + + public MemoryConsolidator(ConsolidationConfig config) { + this.config = config; + } + + /** + * 合并重复记忆(主入口) + * + * @param memories 记忆列表 + * @return 合并结果 + */ + public ConsolidationResult consolidate(List memories) { + if (memories == null || memories.isEmpty()) { + return new ConsolidationResult(0, 0, Collections.emptyMap()); + } + + LOG.info("开始记忆合并:原始记忆数 = {}", memories.size()); + + List> groups = groupBySimilarity(memories); + + LOG.info("分组完成:组数 = {}", groups.size()); + + + List consolidated = new ArrayList<>(); + Map> mergeTracking = new HashMap<>(); + + for (List group : groups) { + if (group.size() == 1) { + // 单个记忆,直接保留 + consolidated.add(group.get(0)); + } else { + // 多个相似记忆,合并 + Observation merged = mergeGroup(group); + consolidated.add(merged); + + // 记录合并信息 + Set mergedIds = group.stream() + .map(Observation::getId) + .collect(Collectors.toSet()); + mergeTracking.put(merged.getId(), mergedIds); + + LOG.debug("合并记忆: {} -> {} (相似度 > {})", + group.size(), merged.getId(), config.similarityThreshold); + } + } + + + ConsolidationResult result = new ConsolidationResult( + memories.size(), + consolidated.size(), + mergeTracking + ); + + LOG.info("记忆合并完成: {} -> {} (减少 {} 张, 去除率 {})", + result.originalCount, + result.consolidatedCount, + result.removedCount, + result.getReductionRate() * 100); + + return result; + } + + /** + * 按相似度分组 + */ + private List> groupBySimilarity(List memories) { + Set processed = new HashSet<>(); + List> groups = new ArrayList<>(); + + for (Observation obs : memories) { + if (processed.contains(obs.getId())) { + continue; + } + + List group = new ArrayList<>(); + group.add(obs); + processed.add(obs.getId()); + + // 查找相似记忆 + for (Observation other : memories) { + if (!processed.contains(other.getId())) { + double similarity = calculateSimilarity(obs, other); + + if (similarity >= config.similarityThreshold) { + // 检查组大小限制 + if (group.size() < config.maxGroupSize) { + group.add(other); + processed.add(other.getId()); + } else { + LOG.debug("组大小达到上限,跳过: {}", + other.getId()); + } + } + } + } + + groups.add(group); + } + + return groups; + } + + /** + * 计算两个记忆的相似度 + * + * 算法:Jaccard 相似度 + 语义增强 + */ + private double calculateSimilarity(Observation a, Observation b) { + // 1. Jaccard 相似度(关键词重叠度) + double jaccard = calculateJaccardSimilarity( + extractWords(a.getContent()), + extractWords(b.getContent()) + ); + + // 2. 类型相同加分 + double typeBonus = (a.getType() == b.getType()) ? 0.1 : 0.0; + + // 3. 长度相似度(避免长度差异过大) + double lenA = a.getContent().length(); + double lenB = b.getContent().length(); + double lenSimilarity = 1.0 - Math.abs(lenA - lenB) / Math.max(lenA, lenB); + + // 4. 综合相似度 + double similarity = jaccard * 0.8 + typeBonus * 0.1 + lenSimilarity * 0.1; + + LOG.trace("相似度计算: {} <-> {} = {}", + a.getId().substring(0, 8), + b.getId().substring(0, 8), + similarity); + + return similarity; + } + + /** + * Jaccard 相似度 + */ + private double calculateJaccardSimilarity(Set wordsA, Set wordsB) { + int intersection = 0; + + for (String word : wordsA) { + if (wordsB.contains(word)) { + intersection++; + } + } + + int union = wordsA.size() + wordsB.size() - intersection; + + if (union == 0) { + return 0.0; + } + + return (double) intersection / union; + } + + /** + * 提取单词(简单分词) + * + */ + private Set extractWords(String text) { + if (text == null || text.isEmpty()) { + return Collections.emptySet(); + } + + // 简单分词:按空格、标点符号分割 + String[] words = text.toLowerCase() + .split("[\\s\\p{Punct}+]+"); + + // 过滤掉短词(长度 < 2) + Set uniqueWords = new HashSet<>(); + for (String word : words) { + if (word.length() >= 2) { + uniqueWords.add(word); + } + } + + return uniqueWords; + } + + /** + * 合并一组相似记忆 + */ + private Observation mergeGroup(List group) { + // 选择最重要的作为基础 + Observation base = group.stream() + .max(Comparator.comparingDouble(Observation::getDecayedImportance)) + .orElse(group.get(0)); + + LOG.debug("选择基础记忆: {} (重要性: {})", + base.getId().substring(0, 8), + String.format("%.2f", base.getDecayedImportance())); + + // 构建合并后的内容 + StringBuilder mergedContent = new StringBuilder(); + mergedContent.append(base.getContent()); + + // 合并其他记忆的内容(避免重复) + if (config.mergeContent) { + Set existingWords = extractWords(base.getContent()); + + for (int i = 1; i < group.size(); i++) { + Observation obs = group.get(i); + String content = obs.getContent(); + + // 只添加不重复的内容片段 + if (!isContentDuplicate(content, existingWords)) { + mergedContent.append("; ").append(content); + existingWords.addAll(extractWords(content)); + } + } + } + + // 创建合并后的记忆 + Observation.ObservationBuilder builder = Observation.builder() + .content(mergedContent.toString()) + .type(base.getType()) + .importance(base.getImportance()) + .timestamp(base.getTimestamp()); + + // 如果配置了保留元数据,则合并所有元数据 + if (config.keepMetadata) { + Map mergedMetadata = new HashMap<>(); + for (Observation obs : group) { + if (obs.getMetadata() != null) { + mergedMetadata.putAll(obs.getMetadata()); + } + mergedMetadata.put("merged_from", obs.getId()); + } + builder.metadata(mergedMetadata); + } + + Observation merged = builder.build(); + + // 记录合并信息 + LOG.debug("合并完成: {} 个记忆 -> 1 个 (长度: {} 字符)", + group.size(), mergedContent.length()); + + return merged; + } + + /** + * 检查内容是否重复 + */ + private boolean isContentDuplicate(String content, Set existingWords) { + Set contentWords = extractWords(content); + + // 计算重叠度 + int overlap = 0; + for (String word : contentWords) { + if (existingWords.contains(word)) { + overlap++; + } + } + + // 如果重叠度 > 80%,认为是重复 + double overlapRatio = (double) overlap / contentWords.size(); + return overlapRatio > 0.8; + } + + /** + * 手动合并指定的记忆 + * + * @param memories 要合并的记忆列表 + * @return 合并后的记忆 + */ + public Observation manualMerge(List memories) { + if (memories == null || memories.isEmpty()) { + throw new IllegalArgumentException("记忆列表不能为空"); + } + + LOG.info("手动合并 {} 个记忆", memories.size()); + + this.config.mergeContent = true; // 强制合并内容 + this.config.keepMetadata = true; + + return mergeGroup(memories); + } + + /** + * 查找与给定记忆相似的记忆 + * + * @param target 目标记忆 + * @param memories 记忆池 + * @return 相似的记忆列表(按相似度降序) + */ + public List findSimilar(Observation target, List memories) { + if (target == null || memories == null) { + return Collections.emptyList(); + } + + return memories.stream() + .filter(obs -> !obs.getId().equals(target.getId())) + .map(obs -> new AbstractMap.SimpleEntry<>( + obs, + calculateSimilarity(target, obs) + )) + .filter(entry -> entry.getValue() >= config.similarityThreshold) + .sorted(Map.Entry.comparingByValue().reversed()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/scorer/EnhancedImportanceScorer.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/scorer/EnhancedImportanceScorer.java new file mode 100644 index 0000000000000000000000000000000000000000..c43080bc8bd1246398223dcb1aa6f4fec8cc404c --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/scorer/EnhancedImportanceScorer.java @@ -0,0 +1,354 @@ +/* + * 增强型重要性评分器(Enhanced Importance Scorer) + * + * 多维度评分算法,综合考虑: + * - 内容特征(30%) + * - 观察类型(25%) + * - 关键词匹配(25%) + * - 时间新鲜度(10%) + * - 访问热度(10%) + * + * 评分范围:0.0 - 10.0 + * + * @author bai + * @since 3.9.5 + */ +package org.noear.solon.bot.core.memory.scorer; + +import org.noear.solon.bot.core.memory.bank.Observation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * 增强型重要性评分器 + */ +public class EnhancedImportanceScorer { + private static final Logger LOG = LoggerFactory.getLogger(EnhancedImportanceScorer.class); + + // ========== 权重配置 ========== + + private static final double WEIGHT_CONTENT = 0.30; + private static final double WEIGHT_TYPE = 0.25; + private static final double WEIGHT_KEYWORDS = 0.25; + private static final double WEIGHT_RECENCY = 0.10; + private static final double WEIGHT_POPULARITY = 0.10; + + // ========== 基础分数 ========== + + private static final double BASE_SCORE = 4.0; + private static final double MAX_SCORE = 10.0; + private static final double MIN_SCORE = 0.0; + + // ========== 高级关键词库 ========== + + // 极高重要性关键词(+4.0) + private static final Map> CRITICAL_KEYWORDS = new HashMap<>(); + static { + CRITICAL_KEYWORDS.put("decision", new HashSet<>(Arrays.asList( + "关键决策", "架构决策", "重大变更", + "critical", "critical decision", "major change" + ))); + CRITICAL_KEYWORDS.put("architecture", new HashSet<>(Arrays.asList( + "系统架构", "核心架构", "整体设计", + "system architecture", "core design" + ))); + } + + // 高重要性关键词(+2.0) + private static final Map> HIGH_KEYWORDS = new HashMap<>(); + static { + HIGH_KEYWORDS.put("problem", new HashSet<>(Arrays.asList( + "问题", "bug", "issue", "error", "问题发现" + ))); + HIGH_KEYWORDS.put("solution", new HashSet<>(Arrays.asList( + "解决", "修复", "方案", "fix", "solve", "solution" + ))); + } + + // 正向影响关键词(+1.0) + private static final Set POSITIVE_KEYWORDS = new HashSet<>(Arrays.asList( + "成功", "完成", "正确", "优化", "改进", + "success", "completed", "correct", "optimized", "improved" + )); + + // 负向影响关键词(-1.0) + private static final Set NEGATIVE_KEYWORDS = new HashSet<>(Arrays.asList( + "失败", "错误", "问题", "警告", + "failed", "error", "issue", "warning" + )); + + // ========== 编译模式 ========== + + // 代码检测模式 + private static final Pattern CODE_PATTERN = Pattern.compile( + "```[\\s\\S]*?```|" + + "(public|private|protected)\\s+(static\\s+)?" + + "(class|interface|enum|void|int|String|boolean|double|float|long)\\s+\\w+" + ); + + // 数字/数据模式 + private static final Pattern DATA_PATTERN = Pattern.compile( + "\\d+\\.\\d+|0x[0-9a-fA-F]+|\\d+%" + ); + + /** + * 计算观察的重要性分数(0.0-10.0) + * + * @param obs 观察 + * @return 重要性分数 + */ + public double score(Observation obs) { + if (obs == null) { + return BASE_SCORE; + } + + double score = BASE_SCORE; + + // ========== 维度 1:内容特征(30%) ========== + + double contentScore = scoreByContent(obs) * WEIGHT_CONTENT; + score += contentScore; + + LOG.trace("内容特征评分: {} -> {}", + obs.getContent().substring(0, Math.min(20, obs.getContent().length())), + contentScore); + + // ========== 维度 2:观察类型(25%) ========== + + double typeScore = scoreByType(obs) * WEIGHT_TYPE; + score += typeScore; + + LOG.trace("类型评分: {} -> {}", obs.getType(), typeScore); + + // ========== 维度 3:关键词匹配(25%) ========== + + double keywordScore = scoreByKeywords(obs) * WEIGHT_KEYWORDS; + score += keywordScore; + + LOG.trace("关键词评分: {}", keywordScore); + + // ========== 维度 4:时间新鲜度(10%) ========== + + double recencyScore = scoreByRecency(obs) * WEIGHT_RECENCY; + score += recencyScore; + + LOG.trace("时间新鲜度评分: {}", recencyScore); + + // ========== 维度 5:访问热度(10%) ========== + + double popularityScore = scoreByPopularity(obs) * WEIGHT_POPULARITY; + score += popularityScore; + + LOG.trace("访问热度评分: {}", popularityScore); + + // ========== 限制范围并返回 ========== + + score = Math.max(MIN_SCORE, Math.min(MAX_SCORE, score)); + + LOG.debug("重要性评分: {} -> {}", + obs.getContent().substring(0, Math.min(30, obs.getContent().length())), + String.format("%.2f", score)); + + return score; + } + + /** + * 维度 1:内容特征评分 + */ + private double scoreByContent(Observation obs) { + String content = obs.getContent().toLowerCase(); + double score = 0.0; + + // 特征 1:包含代码(+1.0) + if (CODE_PATTERN.matcher(content).find()) { + score += 1.0; + } + + // 特征 2:包含数据/数字(+0.5) + if (DATA_PATTERN.matcher(content).find()) { + score += 0.5; + } + + // 特征 3:包含文件路径(+0.5) + if (content.contains(".java") || content.contains(".py") || + content.contains(".js") || content.contains(".ts")) { + score += 0.5; + } + + // 特征 4:内容长度(中等长度最重要) + int length = obs.getContent().length(); + if (length >= 50 && length <= 500) { + score += 0.8; // 黄金长度 + } else if (length > 500 && length <= 1000) { + score += 0.3; // 较长 + } else if (length > 1000) { + score -= 0.5; // 太长可能不重要 + } + + // 特征 5:结构化程度(JSON/XML 等结构化数据) + if (content.contains("{") && content.contains("}") || + content.contains("<") && content.contains(">")) { + score += 0.3; + } + + return score; + } + + /** + * 维度 2:观察类型评分 + */ + private double scoreByType(Observation obs) { + switch (obs.getType()) { + case DECISION: + return 2.5; // 决策最重要 + + case ARCHITECTURE: + return 2.5; // 架构最重要 + + case ERROR: + return 2.0; // 错误重要 + + case USER_REQUIREMENT: + return 2.0; // 用户需求重要 + + case TASK_RESULT: + return 1.5; // 任务结果重要 + + case CODE_UNDERSTANDING: + return 1.2; // 代码理解重要 + + case TOOL_CALL: + case SKILL_EXECUTION: + return -1.0; // 工具调用不太重要 + + case GENERAL: + default: + return 0.0; + } + } + + /** + * 维度 3:关键词匹配评分 + */ + private double scoreByKeywords(Observation obs) { + String content = obs.getContent().toLowerCase(); + double score = 0.0; + + // 检查高级别关键词 + for (Map.Entry> entry : CRITICAL_KEYWORDS.entrySet()) { + for (String keyword : entry.getValue()) { + if (content.contains(keyword.toLowerCase())) { + score += 4.0; + LOG.trace("发现关键关键词: {} (级别: critical)", keyword); + break; + } + } + } + + // 检查高级别关键词 + for (Map.Entry> entry : HIGH_KEYWORDS.entrySet()) { + for (String keyword : entry.getValue()) { + if (content.contains(keyword.toLowerCase())) { + score += 2.0; + LOG.trace("发现关键关键词: {} (级别: high)", keyword); + break; + } + } + } + + // 检查正向关键词 + for (String keyword : POSITIVE_KEYWORDS) { + if (content.contains(keyword)) { + score += 1.0; + } + } + + // 检查负向关键词 + for (String keyword : NEGATIVE_KEYWORDS) { + if (content.contains(keyword)) { + score -= 1.0; + } + } + + return score; + } + + /** + * 维度 4:时间新鲜度评分 + */ + private double scoreByRecency(Observation obs) { + long ageMs = System.currentTimeMillis() - obs.getTimestamp(); + double ageHours = ageMs / (1000.0 * 60 * 60); + + if (ageHours < 0.5) { + return 0.5; // 30分钟内非常新鲜 + } else if (ageHours < 1) { + return 0.3; // 1小时内新鲜 + } else if (ageHours < 6) { + return 0.1; // 6小时内 + } else if (ageHours < 24) { + return 0.0; // 24小时内 + } else if (ageHours < 168) { // 1周 + return -0.1 * (ageHours / 24); // 每天衰减 0.1 + } else { + // 超过1周的记忆,严重衰减 + return -2.0; + } + } + + /** + * 维度 5:访问热度评分 + */ + private double scoreByPopularity(Observation obs) { + int accessCount = obs.getAccessCount(); + + if (accessCount == 0) { + return 0.0; + } else if (accessCount <= 3) { + return 0.3; + } else if (accessCount <= 10) { + return 0.6; + } else if (accessCount <= 20) { + return 1.0; + } else { + // 访问次数很多,但边际效应递减 + return 1.0 + Math.log10(accessCount) * 0.2; + } + } + + /** + * 批量评分 + * + * @param observations 观察列表 + * @return 分数映射 + */ + public Map scoreBatch(List observations) { + Map scores = new HashMap<>(); + + for (Observation obs : observations) { + double score = score(obs); + scores.put(obs.getId(), score); + } + + return scores; + } + + /** + * 获取评分详情(用于调试) + */ + public Map getScoreBreakdown(Observation obs) { + Map breakdown = new HashMap<>(); + + breakdown.put("content", scoreByContent(obs)); + breakdown.put("type", scoreByType(obs)); + breakdown.put("keywords", scoreByKeywords(obs)); + breakdown.put("recency", scoreByRecency(obs)); + breakdown.put("popularity", scoreByPopularity(obs)); + breakdown.put("total", score(obs)); + + return breakdown; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/smart/IntelligentMemoryManager.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/smart/IntelligentMemoryManager.java new file mode 100644 index 0000000000000000000000000000000000000000..cf251dd177b76b129fd1f9cdf64c671880a6ce6d --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/memory/smart/IntelligentMemoryManager.java @@ -0,0 +1,797 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.memory.smart; + +import org.noear.solon.bot.core.memory.*; +import org.noear.solon.bot.core.memory.bank.Observation; +import org.noear.solon.bot.core.memory.classifier.MemoryAutoClassifier; +import org.noear.solon.bot.core.memory.classifier.MemoryClassification; +import org.noear.solon.bot.core.memory.classifier.MemoryCategory; +import org.noear.solon.bot.core.memory.consolidator.MemoryConsolidator; +import org.noear.solon.bot.core.memory.consolidator.ConsolidationConfig; +import org.noear.solon.bot.core.memory.consolidator.ConsolidationResult; +import org.noear.solon.bot.core.memory.scorer.EnhancedImportanceScorer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 智能记忆管理器(Intelligent Memory Manager) + *

+ * 整合自动分类、重要性评分、记忆合并等智能功能 + *

+ * **核心功能**: + * - 自动分类:无需用户选择记忆类型 + * - 自动评分:多维度评估记忆重要性 + * - 智能检索:只返回最相关的记忆 + * - 记忆合并:自动清理重复记忆 + * - 上下文优化:减少传递给 LLM 的 token 数量 + * + * **设计理念**: + * - 渐进式集成:保留现有 SharedMemoryManager,添加智能层 + * - 向后兼容:现有 API 继续工作 + * - 用户无感知:内部优化,外部接口不变 + * + * @author bai + * @since 3.9.5 + */ +public class IntelligentMemoryManager { + + private static final Logger LOG = LoggerFactory.getLogger(IntelligentMemoryManager.class); + + // ========== 核心组件 ========== + + private final MemoryAutoClassifier autoClassifier; + private final EnhancedImportanceScorer importanceScorer; + private final SharedMemoryManager delegate; // 底层存储管理器 + private final MemoryConsolidator consolidator; // 记忆合并器 + private final ScheduledExecutorService consolidationExecutor; // 合并任务调度器 + + // ========== 配置 ========== + + private final boolean autoConsolidate; // 是否自动合并重复记忆 + private final double consolidationThreshold; // 合并阈值 + private final long consolidationInterval; // 合并间隔(毫秒) + + /** + * 构造函数(使用默认配置) + * + * @param workDir 工作目录 + */ + public IntelligentMemoryManager(String workDir) { + this(workDir, true, 0.85, 300_000L); // 默认5分钟合并一次 + } + + /** + * 完整构造函数 + * + * @param workDir 工作目录 + * @param autoConsolidate 是否自动合并 + * @param consolidationThreshold 合并阈值(0-1) + * @param consolidationInterval 合并间隔(毫秒) + */ + public IntelligentMemoryManager(String workDir, + boolean autoConsolidate, + double consolidationThreshold, + long consolidationInterval) { + this.autoClassifier = new MemoryAutoClassifier(); + this.importanceScorer = new EnhancedImportanceScorer(); + this.delegate = new SharedMemoryManager(Paths.get(workDir)); + this.autoConsolidate = autoConsolidate; + this.consolidationThreshold = consolidationThreshold; + this.consolidationInterval = consolidationInterval; + + // 初始化记忆合并器 + ConsolidationConfig config = new ConsolidationConfig(); + config.similarityThreshold = consolidationThreshold; + this.consolidator = new MemoryConsolidator(config); + + // 初始化调度器 + this.consolidationExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "MemoryConsolidation"); + t.setDaemon(true); + return t; + }); + + if (autoConsolidate) { + startConsolidationTask(); + } + + LOG.info("IntelligentMemoryManager 初始化完成: autoConsolidate={}, threshold={}", + autoConsolidate, consolidationThreshold); + } + + // ========== 智能存储方法 ========== + + /** + * 智能存储记忆(自动分类和评分) + * + * @param key 记忆键 + * @param content 记忆内容 + * @return 存储结果 + */ + public String store(String key, String content) { + return store(key, content, null); + } + + /** + * 智能存储记忆(带上下文) + * + * @param key 记忆键 + * @param content 记忆内容 + * @param context 上下文信息 + * @return 存储结果 + */ + public String store(String key, String content, Map context) { + if (content == null || content.trim().isEmpty()) { + return "[WARN] 内容为空,无法存储"; + } + + try { + // ========== 第1步:自动分类 ========== + MemoryClassification classification = autoClassifier.classify(content, context); + + // ========== 第2步:创建 Observation ========== + Observation obs = Observation.builder() + .id(key != null && !key.isEmpty() ? key : UUID.randomUUID().toString()) + .content(content) + .type(mapCategory(classification.getCategory())) + .build(); + + // ========== 第3步:自动评分 ========== + double importance = importanceScorer.score(obs); + obs.setImportance(importance); + + LOG.debug("智能存储: key={}, category={}, importance={}", + key, classification.getCategory(), String.format("%.2f", importance)); + + // ========== 第4步:存储到相应层级 ========== + storeToCategory(obs, classification.getCategory()); + + return String.format( + "[OK] 已存储到 %s (重要性: %.1f/10, TTL: %s)", + classification.getCategory().getDisplayName(), + importance, + formatTtl(classification.getTtl()) + ); + + } catch (Exception e) { + LOG.error("智能存储失败: key={}, error={}", key, e.getMessage(), e); + return "[ERROR] 存储失败: " + e.getMessage(); + } + } + + /** + * 智能检索记忆(只返回最相关的) + * + * @param query 查询内容 + * @param limit 返回数量限制 + * @return 检索结果 + */ + public String retrieve(String query, int limit) { + if (query == null || query.trim().isEmpty()) { + return getAllMemories(limit); + } + + try { + // ========== 第1步:从各层级检索 ========== + List allMemories = new ArrayList<>(); + + // 1.1 从短期记忆检索 + List shortTermMemories = delegate.retrieve(Memory.MemoryType.SHORT_TERM, 1000); + allMemories.addAll(convertToObservations(shortTermMemories)); + + // 1.2 从长期记忆检索 + List longTermMemories = delegate.retrieve(Memory.MemoryType.LONG_TERM, 1000); + allMemories.addAll(convertToObservations(longTermMemories)); + + // 1.3 从知识记忆检索 + List knowledgeMemories = delegate.retrieve(Memory.MemoryType.KNOWLEDGE, 1000); + allMemories.addAll(convertToObservations(knowledgeMemories)); + + // ========== 第2步:计算相关性并排序 ========== + List ranked = rankByRelevance(allMemories, query); + + // ========== 第3步:限制返回数量 ========== + List selected = ranked.stream() + .limit(limit > 0 ? limit : 10) + .collect(Collectors.toList()); + + // 记录访问 + selected.forEach(Observation::recordAccess); + + // ========== 第4步:格式化输出 ========== + return formatResults(selected, query); + + } catch (Exception e) { + LOG.error("智能检索失败: query={}, error={}", query, e.getMessage(), e); + return "[ERROR] 检索失败: " + e.getMessage(); + } + } + + /** + * 获取所有记忆(按重要性排序) + */ + public String getAllMemories(int limit) { + try { + List all = new ArrayList<>(); + + // 从各层级收集 + List shortTermMemories = delegate.retrieve(Memory.MemoryType.SHORT_TERM, 1000); + all.addAll(convertToObservations(shortTermMemories)); + + List longTermMemories = delegate.retrieve(Memory.MemoryType.LONG_TERM, 1000); + all.addAll(convertToObservations(longTermMemories)); + + List knowledgeMemories = delegate.retrieve(Memory.MemoryType.KNOWLEDGE, 1000); + all.addAll(convertToObservations(knowledgeMemories)); + + // 按重要性排序 + all.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); + + // 限制数量 + List selected = all.stream() + .limit(limit > 0 ? limit : 20) + .collect(Collectors.toList()); + + return formatResults(selected, "全部"); + + } catch (Exception e) { + LOG.error("获取记忆失败: error={}", e.getMessage(), e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } + + /** + * 获取统计信息 + */ + public Map getStats() { + Map stats = delegate.getStats(); + + // 添加智能层统计 + stats.put("intelligentLayer", "已启用"); + stats.put("autoConsolidate", autoConsolidate); + + return stats; + } + + // ========== 私有方法 ========== + + /** + * 存储到指定类别 + */ + private void storeToCategory(Observation obs, MemoryCategory category) { + switch (category) { + case WORKING: + // WorkingMemory 由 delegate 管理 + WorkingMemory working = new WorkingMemory(obs.getId()); + working.setSummary(obs.getContent()); + working.setStep(0); + working.setStatus("running"); + delegate.storeWorking(working); + break; + + case SHORT_TERM: + // 使用便捷方法存储短期记忆 + delegate.putShortTerm(obs.getId(), obs.getContent(), 3600L); // 1小时 TTL + break; + + case LONG_TERM: + // 使用便捷方法存储长期记忆 + delegate.putLongTerm(obs.getId(), obs.getContent(), 7 * 24 * 3600L); // 7天 TTL + break; + + case PERMANENT: + // 使用便捷方法存储知识记忆 + delegate.putKnowledge(obs.getId(), obs.getContent()); + break; + } + } + + /** + * 按相关性排序记忆 + */ + private List rankByRelevance(List memories, String query) { + String lowerQuery = query.toLowerCase(); + + return memories.stream() + .sorted((a, b) -> { + double scoreA = calculateRelevance(a, lowerQuery); + double scoreB = calculateRelevance(b, lowerQuery); + return Double.compare(scoreB, scoreA); // 降序 + }) + .collect(Collectors.toList()); + } + + /** + * 计算相关性分数 + */ + private double calculateRelevance(Observation obs, String query) { + double score = 0.0; + + // 1. 文本匹配(40%) + String content = obs.getContent().toLowerCase(); + String[] queryWords = query.toLowerCase().split("\\s+"); + + for (String word : queryWords) { + if (content.contains(word)) { + score += 1.0; + } + } + score *= 0.4; + + // 2. 重要性(30%) + score += obs.getImportance() * 0.3; + + // 3. 时间新鲜度(20%) + long ageHours = (System.currentTimeMillis() - obs.getTimestamp()) / (1000 * 60 * 60); + if (ageHours < 1) { + score += 2.0 * 0.2; + } else if (ageHours < 24) { + score += 1.0 * 0.2; + } + + // 4. 访问热度(10%) + score += Math.log(obs.getAccessCount() + 1) * 0.1; + + return score; + } + + /** + * 格式化结果 + */ + private String formatResults(List observations, String query) { + if (observations.isEmpty()) { + return "[WARN] 未找到相关记忆"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("找到 ").append(observations.size()).append(" 条记忆") + .append(query.equals("全部") ? "" : " (查询: " + query + ")") + .append(":\n\n"); + + for (int i = 0; i < observations.size(); i++) { + Observation obs = observations.get(i); + sb.append(i + 1).append(". "); + + // 重要性指示器 + double imp = obs.getImportance(); + if (imp >= 8.0) { + sb.append("[STAR]"); // 非常重要 + } else if (imp >= 5.0) { + sb.append("[IMPORTANT]"); // 重要 + } else { + sb.append("[NOTE]"); // 普通 + } + + // 内容摘要 + String content = obs.getContent(); + String preview = content.length() > 100 + ? content.substring(0, 100) + "..." + : content; + + sb.append(preview); + + // 重要性分数 + sb.append(" (").append(String.format("%.1f", imp)).append("/10)"); + + // 时间 + long ageHours = (System.currentTimeMillis() - obs.getTimestamp()) / (1000 * 60 * 60); + if (ageHours < 1) { + sb.append(" [刚刚]"); + } else if (ageHours < 24) { + sb.append(" [").append(ageHours).append("小时前]"); + } else { + sb.append(" [").append(ageHours / 24).append("天前]"); + } + + sb.append("\n"); + } + + return sb.toString(); + } + + /** + * 将 Memory 转换为 Observation + */ + private List convertToObservations(Collection memories) { + List observations = new ArrayList<>(); + + for (Memory mem : memories) { + Observation obs = convertToObservation(mem); + if (obs != null) { + observations.add(obs); + } + } + + return observations; + } + + /** + * 将单个 Memory 转换为 Observation + */ + private Observation convertToObservation(Memory memory) { + if (memory == null) { + return null; + } + + Observation.ObservationBuilder builder = Observation.builder() + .id(memory.getId()) + .timestamp(memory.getTimestamp()) + .importance(5.0); // 默认重要性 + + // 根据类型设置内容 + if (memory instanceof ShortTermMemory) { + ShortTermMemory stm = (ShortTermMemory) memory; + builder.content(stm.getContext()) + .type(Observation.ObservationType.GENERAL); + } else if (memory instanceof LongTermMemory) { + LongTermMemory ltm = (LongTermMemory) memory; + builder.content(ltm.getSummary()) + .type(Observation.ObservationType.TASK_RESULT); + } else if (memory instanceof KnowledgeMemory) { + KnowledgeMemory km = (KnowledgeMemory) memory; + builder.content(km.getContent()) + .type(Observation.ObservationType.ARCHITECTURE); + } + + return builder.build(); + } + + /** + * 映射 MemoryCategory 到 ObservationType + */ + private Observation.ObservationType mapCategory(MemoryCategory category) { + switch (category) { + case WORKING: + return Observation.ObservationType.GENERAL; + case SHORT_TERM: + return Observation.ObservationType.GENERAL; + case LONG_TERM: + return Observation.ObservationType.TASK_RESULT; + case PERMANENT: + return Observation.ObservationType.ARCHITECTURE; + default: + return Observation.ObservationType.GENERAL; + } + } + + /** + * 格式化 TTL + */ + private String formatTtl(long ttl) { + if (ttl < 0) { + return "永久"; + } else if (ttl < 60_000L) { + return (ttl / 1000) + "秒"; + } else if (ttl < 3600_000L) { + return (ttl / 60_000) + "分钟"; + } else if (ttl < 24 * 3600_000L) { + return (ttl / 3600_000) + "小时"; + } else { + return (ttl / (24 * 3600_000L)) + "天"; + } + } + + /** + * 启动后台合并任务 + */ + private void startConsolidationTask() { + consolidationExecutor.scheduleAtFixedRate( + this::performConsolidation, + consolidationInterval, // 初始延迟 + consolidationInterval, // 间隔 + TimeUnit.MILLISECONDS + ); + + LOG.info("记忆合并任务已启动(间隔: {}ms, 阈值: {})", + consolidationInterval, consolidationThreshold); + } + + /** + * 执行记忆合并 + */ + private void performConsolidation() { + try { + LOG.debug("开始执行记忆合并..."); + + // 1. 从 SharedMemoryManager 获取所有记忆 + List allObservations = getAllObservations(); + + if (allObservations.isEmpty()) { + LOG.debug("没有记忆需要合并"); + return; + } + + // 2. 执行合并 + ConsolidationResult result = + consolidator.consolidate(allObservations); + + // 3. 记录结果 + if (result.removedCount > 0) { + LOG.info("记忆合并完成: {} -> {} (减少 {} 张, 去除率 {}%)", + result.originalCount, + result.consolidatedCount, + result.removedCount, + String.format("%.1f", result.getReductionRate() * 100)); + + // 4. 将合并后的记忆写回存储 + applyConsolidationResult(result); + } else { + LOG.debug("记忆合并完成: 无需合并({} 个记忆)", result.originalCount); + } + + } catch (Exception e) { + LOG.error("记忆合并失败", e); + } + } + + /** + * 应用合并结果到存储 + * + * @param result 合并结果 + */ + private void applyConsolidationResult(ConsolidationResult result) { + try { + LOG.debug("开始应用合并结果..."); + + int removedCount = 0; + int mergedCount = 0; + + // 遍历每个合并组 + for (Map.Entry> entry : result.mergeGroups.entrySet()) { + Set sourceIds = entry.getValue(); + + if (sourceIds.size() <= 1) { + continue; // 没有合并,跳过 + } + + // 找到合并后的记忆 + Observation mergedObs = findMergedObservation(sourceIds); + if (mergedObs == null) { + LOG.warn("无法找到合并后的记忆: {}", sourceIds); + continue; + } + + // 删除被合并的原始记忆 + for (String sourceId : sourceIds) { + if (!sourceId.equals(mergedObs.getId())) { + boolean removed = removeMemoryById(sourceId); + if (removed) { + removedCount++; + } + } + } + + // 更新或创建合并后的记忆 + updateOrCreateMemory(mergedObs); + mergedCount++; + + LOG.debug("合并记忆组: {} -> {} (删除 {} 个旧记忆)", + sourceIds.size(), mergedObs.getId(), sourceIds.size() - 1); + } + + LOG.info("合并结果应用完成: 合并 {} 组, 删除 {} 个记忆", + mergedCount, removedCount); + + } catch (Exception e) { + LOG.error("应用合并结果失败", e); + } + } + + /** + * 查找合并后的观察 + * + * @param sourceIds 源ID集合 + * @return 合并后的观察,如果找不到返回 null + */ + private Observation findMergedObservation(Set sourceIds) { + // 重新获取所有观察 + List allObs = getAllObservations(); + + // 查找合并后的观察(ID在 sourceIds 中,且内容包含多个记忆的标记) + for (Observation obs : allObs) { + if (sourceIds.contains(obs.getId())) { + // 检查是否是合并后的(包含多个记忆的分隔符) + String content = obs.getContent(); + if (content.contains("; ") && content.length() > 100) { + return obs; + } + } + } + + return null; + } + + /** + * 根据 ID 删除记忆 + * + * @param memoryId 记忆ID + * @return 是否删除成功 + */ + private boolean removeMemoryById(String memoryId) { + try { + // 尝试从各个存储中删除 + boolean removed = false; + + // 从工作记忆删除 + try { + delegate.removeWorking(memoryId); + removed = true; + } catch (Exception e) { + // 不是工作记忆,继续尝试其他类型 + } + + // 从短期记忆删除 + try { + delegate.removeShortTerm(memoryId); + removed = true; + } catch (Exception e) { + // 不是短期记忆,继续尝试其他类型 + } + + // 从长期记忆删除 + try { + delegate.removeLongTerm(memoryId); + removed = true; + } catch (Exception e) { + // 不是长期记忆,继续尝试其他类型 + } + + // 从知识记忆删除 + try { + delegate.removeKnowledge(memoryId); + removed = true; + } catch (Exception e) { + // 不是知识记忆,忽略 + } + + if (removed) { + LOG.debug("删除记忆: {}", memoryId); + } + + return removed; + + } catch (Exception e) { + LOG.warn("删除记忆失败: {}", memoryId, e); + return false; + } + } + + /** + * 更新或创建记忆 + * + * @param obs 观察对象 + */ + private void updateOrCreateMemory(Observation obs) { + try { + // 根据观察类型更新相应的存储 + switch (obs.getType()) { + case GENERAL: + // 更新短期记忆(使用便捷方法,TTL 默认1小时) + delegate.putShortTerm(obs.getId(), obs.getContent(), 3600L); + break; + + case TASK_RESULT: + // 更新长期记忆(使用便捷方法,TTL 默认7天) + delegate.putLongTerm(obs.getId(), obs.getContent(), 7 * 24 * 3600L); + break; + + case ARCHITECTURE: + // 更新知识记忆 + delegate.putKnowledge(obs.getId(), obs.getContent()); + break; + + default: + LOG.warn("未知的观察类型: {}", obs.getType()); + } + + LOG.debug("更新记忆: {} (类型: {})", obs.getId(), obs.getType()); + + } catch (Exception e) { + LOG.error("更新记忆失败: {}", obs.getId(), e); + } + } + + /** + * 从 SharedMemoryManager 获取所有观察(用于合并) + */ + private List getAllObservations() { + List observations = new ArrayList<>(); + + // 使用 retrieve() 方法获取所有类型的记忆 + // 获取短期记忆 + List shortTermMemories = delegate.retrieve(Memory.MemoryType.SHORT_TERM, 1000); + for (Memory mem : shortTermMemories) { + if (mem instanceof ShortTermMemory) { + ShortTermMemory stm = (ShortTermMemory) mem; + Observation obs = Observation.builder() + .id(stm.getId()) + .content(stm.getContext()) + .type(Observation.ObservationType.GENERAL) + .timestamp(stm.getTimestamp()) + .build(); + observations.add(obs); + } + } + + // 获取长期记忆 + List longTermMemories = delegate.retrieve(Memory.MemoryType.LONG_TERM, 1000); + for (Memory mem : longTermMemories) { + if (mem instanceof LongTermMemory) { + LongTermMemory ltm = (LongTermMemory) mem; + Observation obs = Observation.builder() + .id(ltm.getId()) + .content(ltm.getSummary()) + .type(Observation.ObservationType.TASK_RESULT) + .timestamp(ltm.getTimestamp()) + .importance(ltm.getImportance()) + .build(); + observations.add(obs); + } + } + + // 获取知识记忆 + List knowledgeMemories = delegate.retrieve(Memory.MemoryType.KNOWLEDGE, 1000); + for (Memory mem : knowledgeMemories) { + if (mem instanceof KnowledgeMemory) { + KnowledgeMemory km = (KnowledgeMemory) mem; + Observation obs = Observation.builder() + .id(km.getId()) + .content(km.getContent()) + .type(Observation.ObservationType.ARCHITECTURE) + .timestamp(km.getTimestamp()) + .build(); + observations.add(obs); + } + } + + return observations; + } + + /** + * 获取底层存储管理器(用于兼容) + */ + public SharedMemoryManager getDelegate() { + return delegate; + } + + /** + * 关闭资源 + */ + public void shutdown() { + if (consolidationExecutor != null && !consolidationExecutor.isShutdown()) { + LOG.info("关闭 IntelligentMemoryManager..."); + + consolidationExecutor.shutdown(); + try { + if (!consolidationExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + consolidationExecutor.shutdownNow(); + } + LOG.info("IntelligentMemoryManager 已关闭"); + } catch (InterruptedException e) { + consolidationExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/AgentMessage.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/AgentMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..900f8eb1dad58704acccf29fc9c8262b2d66ca24 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/AgentMessage.java @@ -0,0 +1,330 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.message; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 代理消息(统一消息接口) + * + * 合并了原 Message 和 AgentMessage 的功能: + * - 类型安全(泛型支持) + * - 链式构建(Builder 模式) + * - 可扩展(元数据支持) + * - 消息选项(持久化、TTL、重试等) + * + * @param 消息内容类型 + * @author bai + * @since 3.9.5 + */ +@Getter +public class AgentMessage { + + private final String id; + private final String from; + private final String to; + private final String type; + private final T content; + private final long timestamp; + private final Map metadata; + + public AgentMessage(Builder builder) { + this.id = builder.id != null ? builder.id : UUID.randomUUID().toString(); + this.from = builder.from != null ? builder.from : "system"; + this.to = builder.to != null ? builder.to : "*"; + this.type = builder.type != null ? builder.type : "notification"; + this.content = builder.content; + this.timestamp = System.currentTimeMillis(); + this.metadata = builder.metadata; + } + + /** + * 创建消息的便捷方法 + * + * @param content 消息内容 + * @param 内容类型 + * @return Builder + */ + public static Builder of(T content) { + return new Builder().content(content); + } + + /** + * 创建空消息 + */ + public static Builder empty() { + return new Builder(); + } + + + /** + * 获取元数据值 + * + * @param key 键 + * @return 元数据值,不存在返回 null + */ + public String getMetadata(String key) { + return metadata.get(key); + } + + /** + * 获取元数据值(带默认值) + * + * @param key 键 + * @param defaultValue 默认值 + * @return 元数据值 + */ + public String getMetadata(String key, String defaultValue) { + return metadata.getOrDefault(key, defaultValue); + } + + /** + * 获取整数类型的元数据 + */ + public int getIntMetadata(String key, int defaultValue) { + String value = getMetadata(key); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * 获取布尔类型的元数据 + */ + public boolean getBooleanMetadata(String key, boolean defaultValue) { + String value = getMetadata(key); + if (value != null) { + return Boolean.parseBoolean(value); + } + return defaultValue; + } + + // ========== 消息选项便捷方法(兼容旧的 AgentMessage 功能)========== + + /** + * 是否持久化消息 + */ + public boolean isPersistent() { + return getBooleanMetadata("persistent", false); + } + + /** + * 获取消息 TTL(毫秒) + */ + public int getTtl() { + return getIntMetadata("ttl", 60000); + } + + /** + * 获取重试次数 + */ + public int getRetryTimes() { + return getIntMetadata("retryTimes", 0); + } + + /** + * 获取重试延迟(毫秒) + */ + public long getRetryDelay() { + return getIntMetadata("retryDelay", 1000); + } + + /** + * 是否需要确认 + */ + public boolean isRequireAck() { + return getBooleanMetadata("requireAck", false); + } + + /** + * 获取自定义头信息 + */ + public Map getHeaders() { + Map headers = new HashMap<>(); + for (Map.Entry entry : metadata.entrySet()) { + if (entry.getKey().startsWith("header:")) { + headers.put(entry.getKey().substring(7), entry.getValue()); + } + } + return headers; + } + + /** + * 获取特定的头信息 + */ + public String getHeader(String key) { + return getMetadata("header:" + key); + } + + /** + * 获取特定的头信息(带默认值) + */ + public String getHeader(String key, String defaultValue) { + return getMetadata("header:" + key, defaultValue); + } + + /** + * 转换为 Builder + */ + public Builder toBuilder() { + return new Builder() + .id(id) + .from(from) + .to(to) + .type(type) + .content(content) + .metadata(metadata); + } + + /** + * 构建器 + */ + public static class Builder { + private String id; + private String from = "system"; + private String to = "*"; + private String type = "notification"; + private T content; + private final Map metadata = new HashMap<>(); + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder from(String from) { + this.from = from; + return this; + } + + public Builder from(Enum from) { + this.from = from.name().toLowerCase(); + return this; + } + + public Builder to(String to) { + this.to = to; + return this; + } + + public Builder to(Enum to) { + this.to = to.name().toLowerCase(); + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder type(Enum type) { + this.type = type.name().toLowerCase(); + return this; + } + + public Builder content(T content) { + this.content = content; + return this; + } + + public Builder metadata(String key, String value) { + this.metadata.put(key, value); + return this; + } + + public Builder metadata(Map metadata) { + if (metadata != null) { + this.metadata.putAll(metadata); + } + return this; + } + + + /** + * 设置是否持久化 + */ + public Builder persistent(boolean persistent) { + return metadata("persistent", String.valueOf(persistent)); + } + + /** + * 设置消息 TTL(毫秒) + */ + public Builder ttl(int ttl) { + return metadata("ttl", String.valueOf(ttl)); + } + + /** + * 设置重试配置 + */ + public Builder retry(int times, long delay) { + return metadata("retryTimes", String.valueOf(times)) + .metadata("retryDelay", String.valueOf(delay)); + } + + /** + * 设置是否需要确认 + */ + public Builder requireAck(boolean requireAck) { + return metadata("requireAck", String.valueOf(requireAck)); + } + + /** + * 添加自定义头信息 + */ + public Builder header(String key, String value) { + return metadata("header:" + key, value); + } + + /** + * 批量添加头信息 + */ + public Builder headers(Map headers) { + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + metadata("header:" + entry.getKey(), entry.getValue()); + } + } + return this; + } + + public AgentMessage build() { + return new AgentMessage<>(this); + } + } + + @Override + public String toString() { + return "AgentMessage{" + + "id='" + id + '\'' + + ", from='" + from + '\'' + + ", to='" + to + '\'' + + ", type='" + type + '\'' + + ", content=" + content + + ", timestamp=" + timestamp + + ", metadata=" + metadata + + '}'; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/AgentMessageType.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/AgentMessageType.java new file mode 100644 index 0000000000000000000000000000000000000000..3f84983a642757023161d68d05037bd39ee76cb6 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/AgentMessageType.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.message; + +/** + * 预定义消息类型枚举 + * + * @author bai + * @since 3.9.5 + */ +public enum AgentMessageType { + // ========== 请求-响应类型 ========== + /** 请求消息 */ + REQUEST("request"), + /** 响应消息 */ + RESPONSE("response"), + + // ========== 通知类型 ========== + /** 通知消息(无需响应) */ + NOTIFICATION("notification"), + + // ========== 查询类型 ========== + /** 查询消息 */ + QUERY("query"), + /** 查询结果消息 */ + QUERY_RESULT("query.result"); + + private final String code; + + AgentMessageType(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + /** + * 根据代码获取枚举 + */ + public static AgentMessageType fromCode(String code) { + for (AgentMessageType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("未知的消息类型: " + code); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageAck.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageAck.java new file mode 100644 index 0000000000000000000000000000000000000000..8ce47c40b4dfc03329b2705aae67daef9c68d38c --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageAck.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.message; + +import lombok.Getter; + +/** + * 消息确认 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class MessageAck { + private final String messageId; + private final String receiver; + private final boolean success; + private final String message; + private final long timestamp; + private final Object response; // Handler 的处理结果 + + public MessageAck(String messageId, String receiver, boolean success, String message, Object response) { + this.messageId = messageId; + this.receiver = receiver; + this.success = success; + this.message = message; + this.timestamp = System.currentTimeMillis(); + this.response = response; + } + + /** + * 兼容旧构造函数 + */ + public MessageAck(String messageId, String receiver, boolean success, String message) { + this(messageId, receiver, success, message, null); + } + + @Override + public String toString() { + return "MessageAck{" + + "messageId='" + messageId + '\'' + + ", receiver='" + receiver + '\'' + + ", success=" + success + + ", message='" + message + '\'' + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageChannel.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageChannel.java new file mode 100644 index 0000000000000000000000000000000000000000..bc3f0df3dc0b6b15bddc691db019f3679a22f504 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageChannel.java @@ -0,0 +1,550 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.message; + +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * 消息通道 + * + * 负责代理之间的点对点和广播通信 + * + * @author bai + * @since 3.9.5 + */ +public class MessageChannel { + private static final Logger LOG = LoggerFactory.getLogger(MessageChannel.class); + + // 消息处理器注册表:agentId -> handlers + private final Map> handlers = new ConcurrentHashMap<>(); + + // 消息队列(用于持久化消息)- 泛型支持 + private final Map>> pendingMessages = new ConcurrentHashMap<>(); + + // 确认队列(用于需要确认的消息) + private final Map> ackFutures = new ConcurrentHashMap<>(); + + // 异步执行器 + private final ScheduledExecutorService executor; + + // 消息持久化路径 + private final String messageStorePath; + + // 性能优化:消息队列大小限制 + private static final int MAX_PENDING_MESSAGES = 1000; // 每个 Agent 最多 1000 条待处理消息 + + /** + * 构造函数(使用默认配置) + */ + public MessageChannel(String workDir) { + this(workDir, 4); + } + + /** + * 完整构造函数 + * + * @param workDir 工作目录(应该是完整的消息存储路径,例如:workDir/.soloncode/memory) + * @param asyncThreads 异步处理线程数 + */ + public MessageChannel(String workDir, int asyncThreads) { + // workDir 应该已经是完整的路径(例如:workDir/.soloncode/memory) + // 不再重复拼接 ".soloncode" + this.messageStorePath = workDir + File.separator + "messages" + File.separator; + this.executor = Executors.newScheduledThreadPool(asyncThreads, r -> { + Thread t = new Thread(r, "MessageChannel-Thread"); + t.setDaemon(true); + return t; + }); + + // 初始化存储目录 + initStorage(); + + // 启动定期清理任务 + this.executor.scheduleAtFixedRate( + this::cleanupExpiredMessages, + 1, 1, TimeUnit.MINUTES + ); + + LOG.info("消息通道初始化完成"); + } + + /** + * 注册消息处理器 + * + * @param agentId 代理ID + * @param handler 消息处理器 + * @return 处理器ID + */ + public String registerHandler(String agentId, MessageHandler handler) { + String handlerId = UUID.randomUUID().toString(); + + MessageHandlerWrapper wrapper = new MessageHandlerWrapper(handlerId, agentId, handler); + handlers.computeIfAbsent(agentId, k -> new CopyOnWriteArrayList<>()) + .add(wrapper); + + LOG.debug("消息处理器已注册: agentId={}, handlerId={}", agentId, handlerId); + + // 处理待处理消息 + processPendingMessages(agentId); + + return handlerId; + } + + /** + * 注销消息处理器 + * + * @param agentId 代理ID + * @param handlerId 处理器ID + */ + public void unregisterHandler(String agentId, String handlerId) { + List agentHandlers = handlers.get(agentId); + if (agentHandlers != null) { + agentHandlers.removeIf(wrapper -> wrapper.getHandlerId().equals(handlerId)); + LOG.debug("消息处理器已注销: agentId={}, handlerId={}", agentId, handlerId); + } + } + + /** + * 发送点对点消息(类型安全) + * + * @param message 泛型消息对象 + * @param 消息内容类型 + * @return 异步结果(如果需要确认,返回确认对象) + */ + public CompletableFuture send(AgentMessage message) { + String to = message.getTo(); + + // 查找接收者的处理器 + List receivers = handlers.get(to); + + if (receivers == null || receivers.isEmpty()) { + // 没有注册的处理器 + String persistent = message.getMetadata("persistent", "false"); + if ("true".equals(persistent)) { + // 持久化消息 + return queueMessage(message); + } else { + LOG.warn("无接收者: to={}", to); + return CompletableFuture.completedFuture( + new MessageAck(message.getId(), to, false, "No receiver", null) + ); + } + } + + // 处理消息 + return deliverMessage(message, receivers); + } + + /** + * 广播消息(类型安全) + * + * @param message 泛型消息对象 + * @param 消息内容类型 + * @return 所有接收者的确认列表 + */ + public CompletableFuture> broadcast(AgentMessage message) { + List> futures = new ArrayList<>(); + + // 向所有注册的代理(除了发送者)发送消息 + handlers.keySet().stream() + .filter(agentId -> !agentId.equals(message.getFrom())) + .forEach(agentId -> { + // 为每个接收者创建副本(修改 to 字段) + AgentMessage copy = message.toBuilder() + .to(agentId) + .build(); + futures.add(send(copy)); + }); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()) + ); + } + + /** + * 请求-响应模式(类型安全) + * + * @param from 发送者 + * @param to 接收者 + * @param type 消息类型 + * @param payload 消息内容 + * @param 消息内容类型 + * @return 响应结果 + */ + public CompletableFuture request(String from, String to, String type, T payload) { + AgentMessage message = AgentMessage.of(payload) + .from(from) + .to(to) + .type(type) + .metadata("requireAck", "true") + .build(); + + return send(message) + .thenCompose(ack -> { + if (!ack.isSuccess()) { + // Java 8 兼容 + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally( + new RuntimeException("Message delivery failed: " + ack.getMessage()) + ); + return failedFuture; + } + // 返回 handler 的处理结果 + return CompletableFuture.completedFuture(ack.getResponse()); + }); + } + + /** + * 获取待处理消息数量 + * + * @param agentId 代理ID + * @return 待处理消息数量 + */ + public int getPendingMessageCount(String agentId) { + Queue> queue = pendingMessages.get(agentId); + return queue != null ? queue.size() : 0; + } + + /** + * 获取所有待处理消息数量 + * + * @return 待处理消息总数 + */ + public int getTotalPendingMessageCount() { + return pendingMessages.values().stream() + .mapToInt(Queue::size) + .sum(); + } + + /** + * 关闭消息通道 + */ + public void shutdown() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + LOG.info("消息通道已关闭"); + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // ========== 私有方法 ========== + + /** + * 投递消息(支持泛型) + */ + private CompletableFuture deliverMessage(AgentMessage message, List receivers) { + + // 处理重试逻辑 + int retryTimes = message.getIntMetadata("retryTimes", 0); + long retryDelay = message.getIntMetadata("retryDelay", 1000); + + return CompletableFuture.anyOf(receivers.stream() + .map(wrapper -> wrapper.handle(message)).toArray(CompletableFuture[]::new)) + .thenApply(result -> { + // 将 handler 的处理结果存储到 MessageAck 中 + MessageAck ack = new MessageAck(message.getId(), message.getTo(), true, null, result); + + // 如果需要确认,记录确认 + boolean requireAck = message.getBooleanMetadata("requireAck", false); + if (requireAck) { + CompletableFuture future = ackFutures.remove(message.getId()); + if (future != null) { + future.complete(ack); + } + } + + // 持久化已发送的消息 + boolean persistent = message.getBooleanMetadata("persistent", false); + if (persistent) { + persistMessage(message); + } + + return ack; + }) + .exceptionally(ex -> { + LOG.warn("消息投递失败: messageId={}, to={}, error={}", + message.getId(), message.getTo(), ex.getMessage()); + + // 重试 + if (retryTimes > 0) { + LOG.info("重试发送消息: messageId={}, retryTimes={}", + message.getId(), retryTimes); + + CompletableFuture retryFuture = new CompletableFuture<>(); + executor.schedule(() -> { + // 创建修改了 retryTimes 的消息副本 + AgentMessage retryMessage = message.toBuilder() + .metadata("retryTimes", String.valueOf(retryTimes - 1)) + .build(); + deliverMessage(retryMessage, receivers).whenComplete((ack, throwable) -> { + if (throwable != null) { + retryFuture.completeExceptionally(throwable); + } else { + retryFuture.complete(ack); + } + }); + }, retryDelay, TimeUnit.MILLISECONDS); + + try { + return retryFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + return new MessageAck(message.getId(), message.getTo(), false, e.getMessage(), null); + } + } + + return new MessageAck(message.getId(), message.getTo(), false, ex.getMessage(), null); + }); + } + + /** + * 将消息放入待处理队列(支持泛型) + * 性能优化:添加队列大小限制 + */ + private CompletableFuture queueMessage(AgentMessage message) { + String to = message.getTo(); + Queue> queue = pendingMessages.computeIfAbsent(to, k -> new LinkedList<>()); + + // 检查队列大小限制 + if (queue.size() >= MAX_PENDING_MESSAGES) { + LOG.warn("消息队列已满: to={}, maxSize={}, messageId={}", + to, MAX_PENDING_MESSAGES, message.getId()); + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.complete(new MessageAck( + message.getId(), + to, + false, + "Message queue full (max: " + MAX_PENDING_MESSAGES + ")", + null + )); + return failedFuture; + } + + queue.offer(message); + + // 持久化消息 + persistMessage(message); + + LOG.debug("消息已加入待处理队列: to={}, messageId={}, queueSize={}", + to, message.getId(), queue.size()); + + CompletableFuture future = new CompletableFuture<>(); + boolean requireAck = message.getBooleanMetadata("requireAck", false); + if (requireAck) { + ackFutures.put(message.getId(), future); + } else { + future.complete(new MessageAck(message.getId(), to, true, "Queued", null)); + } + + return future; + } + + /** + * 处理待处理消息(支持泛型) + */ + private void processPendingMessages(String agentId) { + Queue> queue = pendingMessages.get(agentId); + if (queue == null || queue.isEmpty()) { + return; + } + + List receivers = handlers.get(agentId); + if (receivers == null || receivers.isEmpty()) { + return; + } + + LOG.info("处理待处理消息: agentId={}, pendingCount={}", agentId, queue.size()); + + AgentMessage message; + while ((message = queue.poll()) != null) { + deliverMessage(message, receivers); + } + } + + /** + * 持久化消息(支持泛型) + */ + private void persistMessage(AgentMessage message) { + CompletableFuture.runAsync(() -> { + try { + File dir = new File(messageStorePath); + if (!dir.exists()) { + dir.mkdirs(); + } + + String filePath = messageStorePath + message.getId() + ".json"; + String json = toJson(message); + Files.write(Paths.get(filePath), json.getBytes(StandardCharsets.UTF_8)); + + } catch (Exception e) { + LOG.warn("消息持久化失败: messageId={}, error={}", message.getId(), e.getMessage()); + } + }); + } + + /** + * 简单的JSON序列化(支持泛型) + */ + private String toJson(AgentMessage message) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"id\":\"").append(escapeJson(message.getId())).append("\","); + sb.append("\"from\":\"").append(escapeJson(message.getFrom())).append("\","); + sb.append("\"to\":\"").append(escapeJson(message.getTo())).append("\","); + sb.append("\"type\":\"").append(escapeJson(message.getType())).append("\","); + sb.append("\"content\":\"").append(escapeJson(String.valueOf(message.getContent()))).append("\","); + sb.append("\"timestamp\":").append(message.getTimestamp()).append(","); + sb.append("\"persistent\":").append(message.getBooleanMetadata("persistent", false)); + + // 添加元数据 + if (!message.getMetadata().isEmpty()) { + sb.append(",\"metadata\":{"); + boolean first = true; + for (Map.Entry entry : message.getMetadata().entrySet()) { + if (!first) sb.append(","); + sb.append("\"").append(escapeJson(entry.getKey())).append("\":"); + sb.append("\"").append(escapeJson(entry.getValue())).append("\""); + first = false; + } + sb.append("}"); + } + + sb.append("}"); + return sb.toString(); + } + + /** + * 清理过期消息(新 API - 支持泛型) + */ + private void cleanupExpiredMessages() { + long now = System.currentTimeMillis(); + int removed = 0; + + // 清理待处理队列中的过期消息 + for (Map.Entry>> entry : pendingMessages.entrySet()) { + Queue> queue = entry.getValue(); + while (!queue.isEmpty()) { + AgentMessage message = queue.peek(); + int ttl = message.getIntMetadata("ttl", 60000); + if (now - message.getTimestamp() > ttl) { + queue.poll(); + removed++; + } else { + break; + } + } + } + + // 清理持久化的过期消息文件 + try { + File dir = new File(messageStorePath); + if (dir.exists()) { + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + if (files != null) { + for (File file : files) { + // 简化实现:删除超过1小时的文件 + if (now - file.lastModified() > 3600_000) { + file.delete(); + removed++; + } + } + } + } + } catch (Exception e) { + LOG.warn("清理过期消息文件失败: error={}", e.getMessage()); + } + + if (removed > 0) { + LOG.info("清理了 {} 条过期消息", removed); + } + } + + /** + * 初始化存储 + */ + private void initStorage() { + try { + File dir = new File(messageStorePath); + if (!dir.exists()) { + dir.mkdirs(); + } + } catch (Exception e) { + LOG.warn("消息存储初始化失败: error={}", e.getMessage()); + } + } + + /** + * 转义JSON字符串 + */ + private String escapeJson(String s) { + if (s == null) { + return ""; + } + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + /** + * 消息处理器包装器(新 API - 支持泛型) + */ + @Getter + private static class MessageHandlerWrapper { + private final String handlerId; + private final String agentId; + private final MessageHandler handler; + + public MessageHandlerWrapper(String handlerId, String agentId, MessageHandler handler) { + this.handlerId = handlerId; + this.agentId = agentId; + this.handler = handler; + } + + public CompletableFuture handle(AgentMessage message) { + try { + return handler.handle(message); + } catch (Exception e) { + LOG.error("消息处理器异常: handlerId={}, error={}", + handlerId, e.getMessage(), e); + // Java 8 兼容:手动创建失败的 Future + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(e); + return failedFuture; + } + } + + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageHandler.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..89db4746ed7bd0e9eeeaf4b139e9398326256993 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/message/MessageHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.message; + +import java.util.concurrent.CompletableFuture; + +/** + * 消息处理器接口 + * + * 支持类型安全的泛型消息处理 + * + * @author bai + * @since 3.9.5 + */ +@FunctionalInterface +public interface MessageHandler { + + /** + * 处理消息(类型安全) + * + * @param message 泛型消息对象 + * @param 消息内容类型 + * @return 处理结果(用于响应) + */ + CompletableFuture handle(AgentMessage message); +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/AbsSubagent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/AbsSubagent.java index f9146166af3ef1588e9f1396428d4db931608bd3..4a5b96b9d6ba8130145cc37fda88a2f650b5f9d7 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/AbsSubagent.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/AbsSubagent.java @@ -15,6 +15,8 @@ */ package org.noear.solon.bot.core.subagent; +import lombok.Getter; +import lombok.Setter; import org.noear.solon.Utils; import org.noear.solon.ai.agent.AgentChunk; import org.noear.solon.ai.agent.AgentResponse; @@ -22,7 +24,6 @@ import org.noear.solon.ai.agent.AgentSession; import org.noear.solon.ai.agent.react.ReActAgent; import org.noear.solon.ai.chat.prompt.Prompt; import org.noear.solon.bot.core.AgentKernel; -import org.noear.solon.bot.core.SystemPrompt; import org.noear.solon.core.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +36,8 @@ import reactor.core.publisher.Flux; * @author bai * @since 3.9.5 */ +@Setter +@Getter public abstract class AbsSubagent implements Subagent { private static final Logger LOG = LoggerFactory.getLogger(AbsSubagent.class); @@ -42,6 +45,8 @@ public abstract class AbsSubagent implements Subagent { protected String description; protected String systemPrompt; + protected SubAgentMetadata metadata; + public AbsSubagent(AgentKernel mainAgent) { this.mainAgent = mainAgent; @@ -56,6 +61,21 @@ public abstract class AbsSubagent implements Subagent { } } + + @Override + public String getType() { + // 从类名推断类型 + // 例如:ExploreSubagent -> explore + // BashSubagent -> bash + String simpleName = this.getClass().getSimpleName(); + if (simpleName.endsWith("Subagent") || simpleName.endsWith("SubAgent")) { + String type = simpleName.substring(0, simpleName.lastIndexOf("Subagent") != -1 ? + simpleName.lastIndexOf("Subagent") : simpleName.lastIndexOf("SubAgent")); + return type.toLowerCase(); + } + return simpleName.toLowerCase(); + } + public final void setDescription(String description) { this.description = description; } diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/BashSubagent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/BashSubagent.java index abfb17b822de6433ea1f170e9ba3cd8ee4ce6456..f7cea3c1f1444dce0d097eaaadfda165e1f94ff0 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/BashSubagent.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/BashSubagent.java @@ -18,6 +18,8 @@ package org.noear.solon.bot.core.subagent; import org.noear.solon.ai.agent.react.ReActAgent; import org.noear.solon.bot.core.AgentKernel; +import java.util.Arrays; + /** * Bash 命令子代理 * @@ -51,6 +53,20 @@ public class BashSubagent extends AbsSubagent { return "bash"; } + @Override + public SubAgentMetadata getMetadata() { + SubAgentMetadata metadata = new SubAgentMetadata(); + metadata.setCode("bash"); + metadata.setName("Bash 命令子代理"); + metadata.setDescription(getDefaultDescription()); + metadata.setEnabled(true); + metadata.setMaxTurns(10); + // Bash 代理的工具:基本文件操作、bash 命令 + metadata.setTools(Arrays.asList("ls", "read", "bash")); + // Bash 代理没有额外的技能 + return metadata; + } + @Override protected String getDefaultDescription() { return "Bash 命令执行子代理,专门执行 git 操作、命令行任务和终端操作"; diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/ExploreSubagent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/ExploreSubagent.java index a7fe8d2c85143071200be4395991af3fa7afa439..105e5f3501577ddac3179e65611da9723a2af549 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/ExploreSubagent.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/ExploreSubagent.java @@ -27,6 +27,8 @@ import org.noear.solon.bot.core.tool.CodeSearchTool; import org.noear.solon.bot.core.tool.WebfetchTool; import org.noear.solon.bot.core.tool.WebsearchTool; +import java.util.Arrays; + /** * 探索子代理 - 快速探索代码库 * @@ -70,6 +72,21 @@ public class ExploreSubagent extends AbsSubagent { return "explore"; } + @Override + public SubAgentMetadata getMetadata() { + SubAgentMetadata metadata = new SubAgentMetadata(); + metadata.setCode("explore"); + metadata.setName("探索子代理"); + metadata.setDescription(getDefaultDescription()); + metadata.setEnabled(true); + metadata.setMaxTurns(15); + // 探索代理的工具:只读文件操作 + metadata.setTools(Arrays.asList("ls", "read", "grep", "glob", "codesearch")); + // 探索代理的技能:专家技能、代码搜索 + metadata.setSkills(Arrays.asList("expert", "lucene")); + return metadata; + } + @Override protected String getDefaultDescription() { return "快速探索子代理,专门用于 **本地项目** 查找文件、分析和理解代码结构和回答代码库问题"; diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/GeneralPurposeSubagent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/GeneralPurposeSubagent.java index 87afeb63ca2649ece380ea3585abf0a3a7283ec4..4c90894ead8b4b9de0914c22d73e8c21581488f0 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/GeneralPurposeSubagent.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/GeneralPurposeSubagent.java @@ -25,6 +25,8 @@ import org.noear.solon.bot.core.tool.WebfetchTool; import org.noear.solon.bot.core.tool.WebsearchTool; import org.noear.solon.core.util.Assert; +import java.util.Arrays; + /** * 通用子代理 - 处理各种复杂任务 * @@ -33,6 +35,7 @@ import org.noear.solon.core.util.Assert; */ public class GeneralPurposeSubagent extends AbsSubagent { private final String subagentType; + // 维护 metadata 字段 public GeneralPurposeSubagent(AgentKernel mainAgent) { this(mainAgent, null); @@ -46,6 +49,43 @@ public class GeneralPurposeSubagent extends AbsSubagent { } else { this.subagentType = subagentType; } + + // 初始化默认 metadata + this.metadata = createDefaultMetadata(); + } + + /** + * 创建默认的 metadata + */ + private SubAgentMetadata createDefaultMetadata() { + SubAgentMetadata metadata = new SubAgentMetadata(); + metadata.setCode(this.subagentType); + metadata.setName("通用子代理"); + metadata.setDescription(getDefaultDescription()); + metadata.setEnabled(true); + metadata.setMaxTurns(25); + // 通用代理的工具:包含所有核心工具 + metadata.setTools(Arrays.asList("ls", "read", "write", "edit", "grep", "glob", "bash", + "websearch", "webfetch", "codesearch")); + // 通用代理的技能:包含所有核心技能 + metadata.setSkills(Arrays.asList("terminal", "expert", "lucene", "todo", "code")); + return metadata; + } + + /** + * 设置 metadata(用于从文件加载) + */ + @Override + public void setMetadata(SubAgentMetadata metadata) { + this.metadata = metadata; + } + + /** + * 获取 metadata(返回维护的字段) + */ + @Override + public SubAgentMetadata getMetadata() { + return metadata; } @Override diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/PlanSubagent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/PlanSubagent.java index c9030199ebdb3a62b300a1170a7f3e9a6415f186..37c61ad0474a8b26096e510fee418d345c4d4a1e 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/PlanSubagent.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/PlanSubagent.java @@ -24,6 +24,8 @@ import org.noear.solon.bot.core.tool.CodeSearchTool; import org.noear.solon.bot.core.tool.WebfetchTool; import org.noear.solon.bot.core.tool.WebsearchTool; +import java.util.Arrays; + /** * 计划子代理 - 软件架构师 * @@ -66,6 +68,21 @@ public class PlanSubagent extends AbsSubagent { return "plan"; } + @Override + public SubAgentMetadata getMetadata() { + SubAgentMetadata metadata = new SubAgentMetadata(); + metadata.setCode("plan"); + metadata.setName("计划子代理"); + metadata.setDescription(getDefaultDescription()); + metadata.setEnabled(true); + metadata.setMaxTurns(20); + // 计划代理的工具:只读文件操作、网络搜索 + metadata.setTools(Arrays.asList("ls", "read", "grep", "glob", "websearch", "webfetch", "codesearch")); + // 计划代理的技能:专家技能、代码搜索 + metadata.setSkills(Arrays.asList("expert", "lucene")); + return metadata; + } + @Override protected String getDefaultDescription() { return "开发计划子代理,软件架构师,用于设计实现策略和步骤计划"; diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubAgentMetadata.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubAgentMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..384fdc3c50d816bb84f1c41c00e8c90981c480b2 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubAgentMetadata.java @@ -0,0 +1,424 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.subagent; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * SubAgent 元数据 + * + * 从系统提示词头部的 YAML 配置中解析出来的元数据 + * 兼容 Claude Code Agent Skills 规范 + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Setter +public class SubAgentMetadata { + // 代理标识 + private String code; + private boolean enabled = true; + + // 必需字段 + private String name; + private String description; + + // 工具配置 + private List tools = new ArrayList<>(); + private List disallowedTools = new ArrayList<>(); + + // 模型配置 + private String model; + + // 权限配置 + private String permissionMode; // default, acceptEdits, dontAsk, bypassPermissions, plan + + // 执行限制 + private Integer maxTurns; + + // Skills 配置 + private List skills = new ArrayList<>(); + + // MCP Servers 配置 + private List mcpServers = new ArrayList<>(); + + // Hooks 配置(暂不解析,保留字段) + private Object hooks; + + // 记忆配置 + private String memory; // user, project, local + + // 后台任务 + private Boolean background; + + // 隔离配置 + private String isolation; // worktree + + // 团队配置 + private String teamName; // 所属团队名称(用于团队成员) + + /** + * 从系统提示词中解析元数据 + * + * @param systemPrompt 系统提示词 + * @return 解析出的元数据对象 + */ + public static SubAgentMetadata fromPrompt(String systemPrompt) { + SubAgentMetadata metadata = new SubAgentMetadata(); + + if (systemPrompt == null || systemPrompt.isEmpty()) { + return metadata; + } + + // 查找 YAML 配置块 + // 格式:---\nname: xxx\ndescription: xxx\n...\n--- + int startIndex = systemPrompt.indexOf("---"); + if (startIndex == -1) { + return metadata; // 没有找到 YAML 配置 + } + + int endIndex = systemPrompt.indexOf("---", startIndex + 3); + if (endIndex == -1) { + return metadata; // 没有找到结束标记 + } + + // 提取 YAML 配置块 + String yamlBlock = systemPrompt.substring(startIndex + 3, endIndex).trim(); + + // 解析每一行 + for (String line : yamlBlock.split("\n")) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + // 跳过注释 + if (line.startsWith("#")) { + continue; + } + + // 解析 key: value + if (line.contains(":")) { + int colonIndex = line.indexOf(":"); + String key = line.substring(0, colonIndex).trim(); + String value = line.substring(colonIndex + 1).trim(); + + switch (key) { + case "code": + metadata.code = value; + break; + case "name": + metadata.name = value; + break; + case "description": + metadata.description = value; + break; + case "enabled": + metadata.enabled = Boolean.parseBoolean(value); + break; + case "tools": + // 工具列表,逗号分隔 + metadata.tools = new ArrayList<>(Arrays.asList(value.split(",\\s*"))); + break; + case "disallowedTools": + // 禁用工具列表,逗号分隔 + metadata.disallowedTools = new ArrayList<>(Arrays.asList(value.split(",\\s*"))); + break; + case "model": + metadata.model = value; + break; + case "permissionMode": + metadata.permissionMode = value; + break; + case "maxTurns": + try { + metadata.maxTurns = Integer.parseInt(value); + } catch (NumberFormatException e) { + // 忽略无效值 + } + break; + case "skills": + // Skills 列表,逗号分隔 + metadata.skills = new ArrayList<>(Arrays.asList(value.split(",\\s*"))); + break; + case "mcpServers": + // MCP Servers 列表,逗号分隔 + metadata.mcpServers = new ArrayList<>(Arrays.asList(value.split(",\\s*"))); + break; + case "memory": + metadata.memory = value; + break; + case "background": + metadata.background = Boolean.parseBoolean(value); + break; + case "isolation": + metadata.isolation = value; + break; + case "teamName": + metadata.teamName = value; + break; + // hooks 字段暂不解析(需要复杂的对象解析) + } + } + } + + return metadata; + } + + /** + * 从系统提示词中解析元数据并移除 YAML 配置块 + * + * @param systemPrompt 系统提示词 + * @return 包含元数据和清理后提示词的对象 + */ + public static PromptWithMetadata parseAndClean(String systemPrompt) { + SubAgentMetadata metadata = fromPrompt(systemPrompt); + String cleanedPrompt = removeYamlBlock(systemPrompt); + return new PromptWithMetadata(metadata, cleanedPrompt); + } + + /** + * 移除提示词头部的 YAML 配置块 + * + * @param systemPrompt 系统提示词 + * @return 清理后的提示词 + */ + private static String removeYamlBlock(String systemPrompt) { + if (systemPrompt == null || systemPrompt.isEmpty()) { + return systemPrompt; + } + + int startIndex = systemPrompt.indexOf("---"); + if (startIndex == -1) { + return systemPrompt; // 没有找到 YAML 配置 + } + + int endIndex = systemPrompt.indexOf("---", startIndex + 3); + if (endIndex == -1) { + return systemPrompt; // 没有找到结束标记 + } + + // 移除 YAML 配置块,保留后面的内容 + return systemPrompt.substring(endIndex + 3).trim(); + } + + /** + * 提示词和元数据的组合 + */ + public static class PromptWithMetadata { + private final SubAgentMetadata metadata; + private final String prompt; + + public PromptWithMetadata(SubAgentMetadata metadata, String prompt) { + this.metadata = metadata; + this.prompt = prompt; + } + + public SubAgentMetadata getMetadata() { + return metadata; + } + + public String getPrompt() { + return prompt; + } + } + + /** + * 从文件行列表解析子代理元数据和提示词 + * + * @param lines 文件内容行列表 + * @return 包含元数据和提示词的对象 + */ + public static PromptWithMetadata fromFileLines(List lines) { + if (lines == null || lines.isEmpty()) { + return new PromptWithMetadata(new SubAgentMetadata(), ""); + } + + // 将行列表合并为字符串 + String content = String.join("\n", lines); + + // 使用现有的解析方法 + return parseAndClean(content); + } + + + /** + * 将元数据转换为 YAML frontmatter 格式 + * + * 格式: + * --- + * name: xxx + * description: xxx + * tools: xxx + * ... + * --- + * + * @return YAML frontmatter 字符串 + */ + public String toYamlFrontmatter() { + StringBuilder yaml = new StringBuilder(); + yaml.append("---\n"); + + // 代理标识 + if (code != null && !code.isEmpty()) { + yaml.append("code: ").append(code).append("\n"); + } + + // 必需字段 + if (name != null && !name.isEmpty()) { + yaml.append("name: ").append(name).append("\n"); + } + + if (description != null && !description.isEmpty()) { + yaml.append("description: ").append(description).append("\n"); + } + + // 启用状态 + if (!enabled) { + yaml.append("enabled: ").append(enabled).append("\n"); + } + + // 工具配置 + if (tools != null && !tools.isEmpty()) { + yaml.append("tools: ").append(String.join(", ", tools)).append("\n"); + } + + if (disallowedTools != null && !disallowedTools.isEmpty()) { + yaml.append("disallowedTools: ").append(String.join(", ", disallowedTools)).append("\n"); + } + + // 模型配置 + if (hasModel()) { + yaml.append("model: ").append(model).append("\n"); + } + + // 权限配置 + if (hasPermissionMode()) { + yaml.append("permissionMode: ").append(permissionMode).append("\n"); + } + + // 执行限制 + if (hasMaxTurns()) { + yaml.append("maxTurns: ").append(maxTurns).append("\n"); + } + + // Skills 配置 + if (hasSkills()) { + yaml.append("skills: ").append(String.join(", ", skills)).append("\n"); + } + + // MCP Servers 配置 + if (hasMcpServers()) { + yaml.append("mcpServers: ").append(String.join(", ", mcpServers)).append("\n"); + } + + // 记忆配置 + if (hasMemory()) { + yaml.append("memory: ").append(memory).append("\n"); + } + + // 后台任务 + if (isBackground()) { + yaml.append("background: ").append(background).append("\n"); + } + + // 隔离配置 + if (hasIsolation()) { + yaml.append("isolation: ").append(isolation).append("\n"); + } + + // 团队配置 + if (hasTeamName()) { + yaml.append("teamName: ").append(teamName).append("\n"); + } + + yaml.append("---"); + + return yaml.toString(); + } + + /** + * 将元数据和提示词组合为完整的格式 + * + * 格式: + * --- + * name: xxx + * ... + * --- + * + * 提示词内容 + * + * @param prompt 系统提示词内容 + * @return 完整的 YAML frontmatter + 提示词 + */ + public String toYamlFrontmatterWithPrompt(String prompt) { + StringBuilder result = new StringBuilder(); + result.append(toYamlFrontmatter()).append("\n\n"); + + if (prompt != null && !prompt.isEmpty()) { + result.append(prompt); + } + + return result.toString(); + } + + + public boolean hasModel() { + return model != null && !model.isEmpty(); + } + + public boolean hasPermissionMode() { + return permissionMode != null && !permissionMode.isEmpty(); + } + + public boolean hasMaxTurns() { + return maxTurns != null && maxTurns > 0; + } + + public boolean hasSkills() { + return skills != null && !skills.isEmpty(); + } + + public boolean hasMcpServers() { + return mcpServers != null && !mcpServers.isEmpty(); + } + + public boolean hasDisallowedTools() { + return disallowedTools != null && !disallowedTools.isEmpty(); + } + + public boolean hasMemory() { + return memory != null && !memory.isEmpty(); + } + + public boolean isBackground() { + return background != null && background; + } + + public boolean hasIsolation() { + return isolation != null && !isolation.isEmpty(); + } + + public boolean hasTeamName() { + return teamName != null && !teamName.isEmpty(); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/Subagent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/Subagent.java index 028bba4053cfec526f41a889a5e934652f3d4655..5919196dc51125aa0a44c346c7be7686258652d4 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/Subagent.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/Subagent.java @@ -23,6 +23,8 @@ import reactor.core.publisher.Flux; /** * 子代理接口 * + * 定义专门的任务执行代理接口,支持同步和流式执行。 + * * @author bai * @since 3.9.5 */ @@ -38,6 +40,9 @@ public interface Subagent { String getDescription(); /** + * 获取配置 + * + * @return 子代理配置 * 获取系统提示词 */ String getSystemPrompt(); @@ -45,16 +50,21 @@ public interface Subagent { /** * 执行任务(同步) * - * @param prompt 任务提示词 + * @param prompt 任务提示 * @return 执行结果 + * @throws Throwable 执行异常 */ AgentResponse call(String __cwd, String sessionId, Prompt prompt) throws Throwable; /** * 执行任务(流式) * - * @param prompt 任务提示词 + * @param prompt 任务提示 * @return 流式结果 */ Flux stream(String __cwd, String sessionId, Prompt prompt); + + + + SubAgentMetadata getMetadata(); } \ No newline at end of file diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubagentManager.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubagentManager.java index e6b8b83a622719c77344f28ae36b178cb619031f..cbfe39c159bf03b8955620863863a8a25f5fdba3 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubagentManager.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/SubagentManager.java @@ -102,8 +102,9 @@ public class SubagentManager { * 注册自定义 agents 池 * * @param dir agents 目录路径,可以是绝对路径或相对路径 + * @param recursive 是否递归扫描子目录(用于团队成员目录) */ - public void agentPool(Path dir) { + public void agentPool(Path dir, boolean recursive) { if (dir == null) { return; } @@ -114,29 +115,26 @@ public class SubagentManager { return; } - try (Stream stream = Files.list(path)) { - List files = stream.filter(p -> p.toString().endsWith(".md")) - .collect(Collectors.toList()); - - for (Path file : files) { - try { - String fileName = file.getFileName().toString(); - List fullContent = Files.readAllLines(file, StandardCharsets.UTF_8); - - // 解析文件:拆分元数据和 Prompt - SubagentFile parsed = parseSubagentFile(fullContent); - - String subagentType = (parsed.name != null) ? parsed.name : fileName.substring(0, fileName.length() - 3); + if (!Files.isDirectory(path)) { + LOG.warn("代理池路径不是目录: {}", dir); + return; + } - AbsSubagent subagent = (AbsSubagent) subagentMap.computeIfAbsent(subagentType, - k -> new GeneralPurposeSubagent(mainAgent, k)); + try { + if (recursive) { + // 递归扫描子目录(用于团队成员) + Files.walk(path) + .filter(p -> p.toString().endsWith(".md")) + .forEach(file -> loadAgentFile(file)); + } else { + // 只扫描当前目录 + try (Stream stream = Files.list(path)) { + List files = stream.filter(p -> p.toString().endsWith(".md")) + .collect(Collectors.toList()); - // 设置解析后的属性 - subagent.setDescription(parsed.description); - subagent.setSystemPrompt(parsed.systemPrompt); - subagent.refresh(); - } catch (IOException e) { - LOG.error("读取代理文件失败: {}", file, e); + for (Path file : files) { + loadAgentFile(file); + } } } } catch (IOException e) { @@ -144,99 +142,48 @@ public class SubagentManager { } } - public static class SubagentFile { - public String name; - public String description; - public Collection tools; - public String model; - public Map metadata; - - public String systemPrompt; + /** + * 注册自定义 agents 池(不递归) + * + * @param dir agents 目录路径,可以是绝对路径或相对路径 + */ + public void agentPool(Path dir) { + agentPool(dir, false); } - public SubagentFile parseSubagentFile(List lines) { - SubagentFile result = new SubagentFile(); + /** + * 从文件加载子代理定义 + * + * @param file 代理定义文件路径 + */ + private void loadAgentFile(Path file) { + try { + String fileName = file.getFileName().toString(); + List fullContent = Files.readAllLines(file, StandardCharsets.UTF_8); - if (lines == null || lines.isEmpty()) { - result.systemPrompt = ""; - result.description = "自定义代理"; - return result; - } + // 解析文件:拆分元数据和 Prompt + SubAgentMetadata.PromptWithMetadata parsed = SubAgentMetadata.fromFileLines(fullContent); - // 规范:第一行必须是 --- 且不能有前导空行 - if ("---".equals(lines.get(0).trim())) { - StringBuilder yamlBuilder = new StringBuilder(); - StringBuilder bodyBuilder = new StringBuilder(); - int secondSeparatorIndex = -1; - - for (int i = 1; i < lines.size(); i++) { - String line = lines.get(i); - if (secondSeparatorIndex == -1) { - if ("---".equals(line.trim())) { - secondSeparatorIndex = i; - } else { - yamlBuilder.append(line).append("\n"); - } - } else { - bodyBuilder.append(line).append("\n"); - } + String subagentType = parsed.getMetadata().getName(); + if (subagentType == null || subagentType.isEmpty()) { + subagentType = fileName.substring(0, fileName.length() - 3); } - // 成功找到闭合标识 - if (secondSeparatorIndex != -1) { - try { - Yaml yaml = new Yaml(); - Object yamlData = yaml.load(yamlBuilder.toString()); - // 使用 ONode.loadObj 性能更好且更直接 - ONode oNode = ONode.ofBean(yamlData); - - if (oNode.isObject()) { - // 1. 存入全量元数据 - result.metadata = oNode.toBean(Map.class); - - // 2. 提取标准字段 - result.name = oNode.get("name").getString(); - result.description = oNode.get("description").getString(); - result.model = oNode.get("model").getString(); - - // 3. 灵活解析 tools (数组或逗号分隔) - if (oNode.hasKey("tools")) { - ONode tNode = oNode.get("tools"); - if (tNode.isArray()) { - result.tools = tNode.toBean(List.class); - } else { - result.tools = Arrays.stream(tNode.getString().split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList()); - } - } - } - result.systemPrompt = bodyBuilder.toString().trim(); - } catch (Exception e) { - LOG.error("YAML 格式异常,全文本回退", e); - result.systemPrompt = String.join("\n", lines).trim(); - } - } else { - // 有开头无结尾,视为普通文本 - result.systemPrompt = String.join("\n", lines).trim(); - } - } else { - // 第一行不是 ---,严格作为普通 Body 处理 - result.systemPrompt = String.join("\n", lines).trim(); - } + AbsSubagent subagent = (AbsSubagent) subagentMap.computeIfAbsent(subagentType, + k -> new GeneralPurposeSubagent(mainAgent, k)); - // 描述兜底 - if (Assert.isEmpty(result.description) && !result.systemPrompt.isEmpty()) { - // 取第一行并移除 Markdown 标题符 - String firstLine = result.systemPrompt.split("\\R")[0]; - result.description = firstLine.replace("#", "").trim(); - } + // 设置解析后的属性 + subagent.setDescription(parsed.getMetadata().getDescription()); + subagent.setSystemPrompt(parsed.getPrompt()); - if (Assert.isEmpty(result.description)) { - result.description = "自定义代理"; - } + // 设置完整的 metadata(包括 teamName 等字段) + subagent.setMetadata(parsed.getMetadata()); - return result; + subagent.refresh(); + + LOG.debug("加载子代理: {} 从 {}", subagentType, file); + } catch (IOException e) { + LOG.error("读取代理文件失败: {}", file, e); + } } } \ No newline at end of file diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/TaskSkill.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/TaskSkill.java index aff8fe5be231017c0d60cc6d6cf31660bd4ef109..82fd498f65e1b97486b07994a071a8cebefae5fd 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/TaskSkill.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/subagent/TaskSkill.java @@ -32,7 +32,15 @@ import org.noear.solon.core.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Map; +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; /** * 子代理技能 @@ -65,35 +73,48 @@ public class TaskSkill extends AbsSkill { sb.append("处理复杂的、多步骤的任务,必须委派子代理(Subagent)执行\n\n"); sb.append("可用的代理类型及其拥有的工具:\n"); + // ========== 新增:禁止模拟工作规则 ========== + sb.append("### ⚠️ 核心规则(强制执行)\n\n"); + sb.append("#### 🚫 禁止行为\n"); + sb.append("1. **禁止模拟工作**:\n"); + sb.append(" - 严禁不断更新状态而无实际产出\n"); + sb.append(" - 不得声称任务已完成但没有文件生成\n"); + sb.append(" - 使用 task() 工具后,子代理必须实际创建文件或执行命令\n\n"); + sb.append("2. **必须有实际产出**:\n"); + sb.append(" - 代码任务:必须生成 .java、.py 等代码文件\n"); + sb.append(" - 文档任务:必须生成 .md、.txt 等文档文件\n"); + sb.append(" - 测试任务:必须有测试结果或报告\n"); + sb.append(" - 使用 ls、read 工具验证文件已真实创建\n\n"); + sb.append("#### ✅ 必须行为\n"); + sb.append("1. **强制使用 task() 工具**:\n"); + sb.append(" - 所有实际工作必须通过 task(subagent_type=..., prompt=...) 完成\n"); + sb.append(" - 不得自己在主对话中重复尝试\n\n"); + + sb.append("### 强制委派准则\n"); + sb.append("- **项目认知**: 凡是涉及\"探索项目\"、\"分析架构\"、\"查找核心入口\"等需要阅读多个文件或理解代码库的任务,应委派给子代理。\n"); + sb.append("- **复杂变更**: 涉及跨文件的代码修复、重构或需要运行测试验证的任务,应委派给子代理。\n"); + sb.append("- **决策量化**: 预感需要连续调用超过 3 次原子工具(如 grep, read_file)时,应改用子代理以节省主对话上下文。\n"); + sb.append("- **所有开发任务**: 必须使用 task() 工具委派,禁止在主对话中模拟执行\n\n"); + + sb.append("### 可用的子代理注册表 (Capabilities Registry)\n"); + sb.append("请根据任务语义匹配最合适的 `subagent_type`:\n"); sb.append("\n"); for (Subagent agent : manager.getAgents()) { sb.append(String.format(" - \"%s\": %s\n", agent.getType(), agent.getDescription())); } sb.append("\n\n"); - sb.append("## 战略任务委派 (Task Delegation)\n"); - sb.append("当你面临高熵任务(信息量大、不确定性高)时,应启动专项子代理。这能让你保持高层视野,避免被底层执行细节干扰。\n\n"); - - sb.append("### 委派子代理的显著优势:\n"); - sb.append("- **上下文隔离**:子代理内部的数十次搜索和读取不会污染你的主对话历史。\n"); - sb.append("- **专注深度**:子代理在特定领域(如架构探索、测试驱动开发)拥有比原子工具更强的自主纠错逻辑。\n"); - sb.append("- **并行加速**:你可以在单条消息中 `task(...)` 启动多个代理同时分析不同的模块。\n\n"); - - sb.append("### 强制委派场景:\n"); - sb.append("- **未知领域探索**:如“找出该项目的认证逻辑实现”,此类涉及跨文件链式追踪的任务。\n"); - sb.append("- **闭环变更**:涉及“修改代码 + 运行测试 + 修复错误”的循环任务。\n"); - sb.append("- **信息密集型操作**:预感需要连续调用 3 次以上 `read` 或 `grep` 时。\n\n"); - - sb.append("### 调用深度指南:\n"); - sb.append("1. **精准上下文注入**:子代理初始状态是孤立的。你必须在 `prompt` 中提供核心锚点。使用 `` 标签包裹路径、类名或错误日志。\n"); - sb.append("2. **定义输出格式**:明确要求子代理在 `task_result` 中提供“结论、修改点、待办事项”,方便你向用户汇报。\n"); - sb.append("3. **会话延续**:如果子代理没能一次性解决,利用返回的 `task_id` 再次调用,它将保留之前的内存。\n\n"); + sb.append("### 调用约定\n"); + sb.append("- **上下文对齐**: 子代理看不见当前历史。必须在 `prompt` 中通过 标签传入必要的类名、报错或路径。\n"); + sb.append("- **示例**: `task(subagent_type=\"explore\", prompt=\"分析 demo-web 核心架构\", description=\"架构探索\")`\n"); + sb.append("- **动态创建 Agent**: 可以使用 `create_agent` 工具动态创建新的子代理,自定义其行为和技能。\n"); return sb.toString(); } - @ToolMapping(name = "task", description = "分派一个战略任务给子代理") + @ToolMapping(name = "task", + description = "【强制使用】派生并分派任务给专项子代理。所有实际开发工作(代码编写、文件创建、测试执行等)必须使用此工具委派给子代理完成,禁止在主对话中模拟执行或虚假声称完成。子代理会实际创建文件并返回真实结果。") public String task( @Param(name = "subagent_type", description = "子代理类型") String subagent_type, @Param(name = "prompt",description = "具体指令。必须包含任务目标、关键类名或必要的背景上下文。") String prompt, @@ -166,4 +187,109 @@ public class TaskSkill extends AbsSkill { return "ERROR: 子代理执行失败: " + e.getMessage(); } } + + /** + * 动态创建子代理工具 + * + * 允许主 Agent 动态创建新的子代理,自定义其行为和技能。 + * 创建的 agent 定义会保存到文件,并立即注册到 SubagentManager 中。 + */ + @ToolMapping(name = "create_agent", + description = "动态创建一个新的子代理。创建的子代理将保存到 .soloncode/agents/ 目录并立即可用。") + public String createAgent( + @Param(name = "code", description = "子代理的唯一标识码(如 'my-custom-agent')") String code, + @Param(name = "name", description = "子代理的显示名称") String name, + @Param(name = "description", description = "子代理的功能描述") String description, + @Param(name = "systemPrompt", description = "子代理的系统提示词(行为指令)") String systemPrompt, + @Param(name = "model", required = false, description = "可选。指定使用的模型(如 'gpt-4')") String model, + @Param(name = "tools", required = false, description = "可选。允许使用的工具列表,逗号分隔(如 'read,write,grep')") String tools, + @Param(name = "skills", required = false, description = "可选。启用的技能列表,逗号分隔") String skills, + @Param(name = "maxTurns", required = false, description = "可选。最大对话轮数限制") Integer maxTurns, + @Param(name = "saveToFile", required = false, description = "可选。是否保存到文件(默认 true)") Boolean saveToFile, + String __cwd + ) { + try { + // 1. 构建 SubAgentMetadata + SubAgentMetadata metadata = new SubAgentMetadata(); + metadata.setCode(code); + metadata.setName(name); + metadata.setDescription(description); + metadata.setEnabled(true); + + // 可选参数 + if (model != null && !model.isEmpty()) { + metadata.setModel(model); + } + if (tools != null && !tools.isEmpty()) { + metadata.setTools(Arrays.asList(tools.split(",\\s*"))); + } + if (skills != null && !skills.isEmpty()) { + metadata.setSkills(Arrays.asList(skills.split(",\\s*"))); + } + if (maxTurns != null && maxTurns > 0) { + metadata.setMaxTurns(maxTurns); + } + + // 2. 生成完整的 agent 定义(YAML frontmatter + system prompt) + String agentDefinition = metadata.toYamlFrontmatterWithPrompt(systemPrompt); + + // 3. 保存到文件(默认保存) + boolean shouldSave = saveToFile == null || saveToFile; + String filePath = null; + + if (shouldSave) { + Path agentsDir = Paths.get(__cwd, ".soloncode", "agents"); + + // 确保目录存在 + if (!Files.exists(agentsDir)) { + Files.createDirectories(agentsDir); + LOG.info("创建 agents 目录: {}", agentsDir); + } + + // 使用 code 作为文件名 + String fileName = code + ".md"; + Path agentFile = agentsDir.resolve(fileName); + filePath = agentFile.toString(); + + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter( + Files.newOutputStream(agentFile.toFile().toPath()), + StandardCharsets.UTF_8))) { + writer.write(agentDefinition); + } + LOG.info("Agent 定义已保存到: {}", filePath); + } + + // 4. 动态创建并注册新的子代理 + AbsSubagent newAgent = new GeneralPurposeSubagent(mainAgent, code); + newAgent.setDescription(description); + newAgent.setSystemPrompt(agentDefinition); + newAgent.refresh(); + + // 注册到 manager + manager.addSubagent(newAgent); + + LOG.info("动态创建子代理成功: code={}, name={}", code, name); + + // 5. 返回结果 + StringBuilder result = new StringBuilder(); + result.append("[OK] 子代理创建成功!\n\n"); + result.append(String.format("**代码**: %s\n", code)); + result.append(String.format("**名称**: %s\n", name)); + result.append(String.format("**描述**: %s\n", description)); + + if (filePath != null) { + result.append(String.format("**文件**: %s\n", filePath)); + } + + result.append(String.format("\n现在可以使用 `task(subagent_type=\"%s\", prompt=\"...\")` 来调用这个子代理。\n", code)); + + return result.toString(); + + } catch (Throwable e) { + LOG.error("创建子代理失败: code={}, error={}", code, e.getMessage(), e); + return "ERROR: 创建子代理失败: " + e.getMessage(); + } + } } \ No newline at end of file diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/AgentTeamsSkill.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/AgentTeamsSkill.java new file mode 100644 index 0000000000000000000000000000000000000000..73a913d262271f229d10e08907ffe240ffe1db78 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/AgentTeamsSkill.java @@ -0,0 +1,1650 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.noear.solon.ai.agent.AgentChunk; +import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.ai.chat.skill.AbsSkill; +import org.noear.solon.ai.annotation.ToolMapping; +import org.noear.solon.annotation.Param; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.memory.KnowledgeMemory; +import org.noear.solon.bot.core.memory.LongTermMemory; +import org.noear.solon.bot.core.memory.Memory; +import org.noear.solon.bot.core.memory.ShortTermMemory; +import org.noear.solon.bot.core.memory.smart.IntelligentMemoryManager; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; +import org.noear.solon.bot.core.subagent.Subagent; +import org.noear.solon.bot.core.subagent.SubagentManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +/** + * Agent Teams 技能 + * + * 将 Agent Teams 的协调能力暴露给主 Agent,包括: + * - 启动团队协作任务 + * - 查看和管理共享任务列表 + * - 监控团队任务状态 + * - 调用专门的子代理(使用 TaskSkill 的实现) + * - 动态创建新的子代理 + * + * @author bai + * @since 3.9.5 + */ +public class AgentTeamsSkill extends AbsSkill { + + private static final Logger LOG = LoggerFactory.getLogger(AgentTeamsSkill.class); + private static final String CURRENT_TEAM_KEY = "current_team_name"; // 当前团队名的内存键 + + private final MainAgent mainAgent; + private final AgentKernel kernel; + private final SubagentManager manager; + private final AgentTeamsTools agentTeamsTools; // 内部工具集 + private final IntelligentMemoryManager intelligentMemoryManager; + + /** + * 完整构造函数(支持子代理调用) + */ + public AgentTeamsSkill(MainAgent mainAgent, AgentKernel kernel, SubagentManager manager) { + this.mainAgent = mainAgent; + this.kernel = kernel; + this.manager = manager; + + // 初始化内部工具集 + if (mainAgent != null) { + this.agentTeamsTools = new AgentTeamsTools( + mainAgent.getSharedMemoryManager(), + mainAgent.getEventBus() + ); + } else { + this.agentTeamsTools = null; + } + + // 初始化智能记忆管理器 + if (mainAgent != null && mainAgent.getSharedMemoryManager() != null) { + // 使用与 SharedMemoryManager 相同的路径 + // 注意:IntelligentMemoryManager 内部会创建 SharedMemoryManager,会再次拼接路径 + // 所以这里传入 workDir 即可 + String workDir = System.getProperty("user.dir") + File.separator + "work"; + this.intelligentMemoryManager = new IntelligentMemoryManager(workDir); + LOG.info("初始化智能记忆管理器: workDir={}", workDir); + } else { + this.intelligentMemoryManager = null; + } + } + + /** + * 简化构造函数(兼容性) + */ + public AgentTeamsSkill(MainAgent mainAgent, SubagentManager manager) { + this(mainAgent, null, manager); + } + + @Override + public String description() { + return "Agent Teams 协调专家:支持团队协作任务、任务管理、子代理调用"; + } + + @Override + public String getInstruction(Prompt prompt) { + StringBuilder sb = new StringBuilder(); + sb.append("## Agent Teams 协调能力\n\n"); + sb.append("你是一个团队协调器,可以启动和管理多代理协作任务。\n\n"); + + // ========== 新增:禁止模拟工作规则 ========== + sb.append("### ⚠️ 核心规则(强制执行)\n\n"); + sb.append("#### 🚫 禁止行为(绝对不可违反)\n"); + sb.append("1. **禁止模拟工作**:\n"); + sb.append(" - 严禁不断更新 `update_working_memory`、`step`、`currentAgent` 而无实际产出\n"); + sb.append(" - 不得使用 `memory_store` 存储虚假的\"已完成\"状态\n"); + sb.append(" - 不得声称\"需求分析已完成\"、\"代码已编写\"等虚假结论\n"); + sb.append(" - 不断更新状态而无实际文件产出是**严重违规**\n\n"); + sb.append("2. **必须有实际产出**:\n"); + sb.append(" - 代码任务必须生成 `.java`、`.py` 等文件\n"); + sb.append(" - 文档任务必须生成 `.md`、`.txt` 等文件\n"); + sb.append(" - 使用 `ls`、`read` 工具验证文件已真实创建\n"); + sb.append(" - 确认文件内容符合要求后才可宣称任务完成\n\n"); + sb.append("3. **禁止循环操作**:\n"); + sb.append(" - 不得重复调用相同工具而不产生新进展\n"); + sb.append(" - 检测到循环时必须立即停止并使用 `task()` 工具\n\n"); + sb.append("#### ✅ 必须行为(强制执行)\n"); + sb.append("1. **必须使用 task 工具**:\n"); + sb.append(" - 所有实际工作必须通过 `task(subagent_type, prompt)` 委派给子代理\n"); + sb.append(" - 可用类型:explore、plan、bash、general-purpose、solon-code-guide\n"); + sb.append(" - 例如:`task(subagent_type='bash', prompt='创建文件并编写代码')`\n\n"); + sb.append("2. **必须验证产出**:\n"); + sb.append(" - 使用 `ls(path='.')` 列出创建的文件\n"); + sb.append(" - 使用 `read(file_path='xxx')` 验证文件内容\n"); + sb.append(" - 只有确认真实产出后才可完成\n\n"); + sb.append("3. **token 使用警告**:\n"); + sb.append(" - 每个任务建议不超过 10,000 tokens\n"); + sb.append(" - 超过 5,000 tokens 无产出时必须改变策略\n"); + sb.append(" - 禁止无限循环调用工具\n\n"); + + sb.append("### 工作流程\n" + + ". 分析任务:识别所需的专业领域。\n" + + ". 组建团队:自动激活相关领域的专家 Agent。\n" + + ". 引导讨论:\n" + + " - 让专家轮流发表观点。\n" + + " - 鼓励专家互相质疑(例如:安全专家挑战开发专家的架构)。\n" + + " - 记录争议点并寻求共识。\n" + + ". 生成报告:汇总讨论结果,去除冗余对话,只保留高质量的最终结论。"); + + sb.append("### 核心能力\n"); + sb.append("1. **团队协作任务**: 使用 `team_task()` 启动多代理协作\n"); + sb.append("2. **任务管理**: 查看、创建团队任务\n"); + sb.append("3. **子代理调用**: 使用 `task()` 委派专门任务(支持会话续接)【强制使用】\n"); + sb.append("4. **动态代理**: 使用 `create_agent()` 创建新的子代理定义\n"); + sb.append("5. **智能记忆管理**: 使用 `memory_store()` 自动分类存储(推荐)\n"); + sb.append(" - 自动分类:决策→永久、任务→7天、临时→10分钟\n"); + sb.append(" - 自动评分:多维度评估记忆重要性(0-10分)\n"); + sb.append(" - 智能检索:`memory_recall()` 按相关性排序\n"); + sb.append("6. **代理间通信**: 使用 `send_message()` 向其他代理发送消息\n\n"); + + sb.append("### 强制委派准则\n"); + sb.append("- **项目认知**: 探索项目、分析架构 → 委派给子代理\n"); + sb.append("- **复杂变更**: 跨文件修复、重构 → 委派给子代理\n"); + sb.append("- **决策量化**: 超过 3 次工具调用 → 改用子代理\n"); + sb.append("- **所有开发任务**: 必须使用 `task(subagent_type='bash', ...)` 实际创建文件\n\n"); + + sb.append("### 可用的子代理\n"); + sb.append("\n"); + // 只显示自定义团队成员(有 teamName 的) + for (Subagent agent : manager.getAgents()) { + if (agent.getMetadata().hasTeamName()) { + sb.append(String.format(" - `%s`: %s (团队: %s)\n", + agent.getType(), + agent.getDescription(), + agent.getMetadata().getTeamName())); + } + } + sb.append("\n\n"); + + sb.append("### 团队成员管理\n"); + sb.append("1. **创建成员**: 使用 `teammate()` 创建新的团队成员\n"); + sb.append("2. **列出成员**: 使用 `teammates()` 查看所有团队成员(表格格式)\n"); + sb.append("3. **移除成员**: 使用 `remove_teammate()` 移除团队成员\n\n"); + + sb.append("### 任务链协调:如何将结果传递给下一个 subagent\n\n"); + sb.append("**⚠️ 重要:完成任务链协调的三种方法**\n\n"); + sb.append("**方法 1:在 prompt 中传递上下文(推荐)**\n"); + sb.append("```"); + sb.append("# 第一步:plan 完成设计\n"); + sb.append("result1 = task(subagent_type='plan', prompt='设计用户登录模块')\n"); + sb.append("\n"); + sb.append("# 第二步:传递给 bash\n"); + sb.append("task(\n"); + sb.append(" subagent_type='bash',\n"); + sb.append(" prompt='基于以下设计:' + result1 + '创建代码'\n"); + sb.append(")\n"); + sb.append("```\n\n"); + sb.append("**方法 2:使用共享记忆**\n"); + sb.append("```"); + sb.append("result1 = task(subagent_type='plan', prompt='设计...')\n"); + sb.append("memory_store(content=result1, key='design')\n"); + sb.append("design = memory_recall(query='设计', limit=1)\n"); + sb.append("task(subagent_type='bash', prompt='' + design + '创建代码')\n"); + sb.append("```\n\n"); + sb.append("**方法 3:使用 taskId 续接会话**\n"); + sb.append("```"); + sb.append("result1 = task(subagent_type='explore', prompt='分析项目')\n"); + sb.append("# 返回 task_id: explore_12345\n"); + sb.append("result2 = task(\n"); + sb.append(" subagent_type='explore',\n"); + sb.append(" prompt='深入分析 Controller 层',\n"); + sb.append(" taskId='explore_12345' # 续接之前的会话\n"); + sb.append(")\n"); + sb.append("```\n\n"); + sb.append("**详细指南**:参考项目文档 `docs/TASK_CHAIN_GUIDE.md`\n\n"); + + sb.append("### 使用场景\n"); + sb.append("```"); + sb.append("# 场景1: 创建团队成员(指定团队)\n"); + sb.append("teammate(\n"); + sb.append(" name=\"security-expert\",\n"); + sb.append(" role=\"security-expert\",\n"); + sb.append(" description=\"专注于安全审计、漏洞检测\",\n"); + sb.append(" teamName=\"myteam\" # 指定团队名称\n"); + sb.append(")\n"); + sb.append("# 生成文件: .soloncode/agentsTeams/myteam/security-expert.md\n\n"); + sb.append("# 场景2: 创建成员(不指定团队,自动生成)\n"); + sb.append("teammate(\n"); + sb.append(" name=\"db-optimizer\",\n"); + sb.append(" role=\"db-optimizer\",\n"); + sb.append(" description=\"SQL 查询优化\"\n"); + sb.append(")\n"); + sb.append("# 自动生成团队名: team-1736640123456\n"); + sb.append("# 生成文件: .soloncode/agentsTeams/team-1736640123456/db-optimizer.md\n"); + sb.append("# 团队名会保存到内存,后续创建会自动使用同一团队\n\n"); + sb.append("# 场景3: 查看所有成员\n"); + sb.append("teammates()\n\n"); + sb.append("# 场景4: 启动团队协作任务\n"); + sb.append("team_task(\"实现用户登录功能\")\n\n"); + sb.append("# 场景5: 查看任务状态\n"); + sb.append("team_status()\n\n"); + sb.append("# 场景6: 智能记忆存储(推荐使用)\n"); + sb.append("# 自动分类存储(系统自动判断存储类型和周期)\n"); + sb.append("memory_store(content=\"采用三层架构:Controller-Service-Repository\", key=\"架构决策\")\n"); + sb.append("# → 系统识别为架构决策,自动分类到永久记忆(重要性评分: 8.5/10)\n\n"); + sb.append("# 存储任务完成结果\n"); + sb.append("memory_store(content=\"已完成JWT认证,包括登录、登出、token刷新\", key=\"登录功能\")\n"); + sb.append("# → 系统识别为任务结果,自动分类到长期记忆(7天,重要性评分: 7.2/10)\n\n"); + sb.append("# 存储临时上下文\n"); + sb.append("memory_store(content=\"正在实现Service层\", key=\"当前步骤\")\n"); + sb.append("# → 系统识别为临时上下文,自动分类到工作记忆(10分钟,重要性评分: 3.5/10)\n\n"); + sb.append("# 场景7: 智能检索记忆\n"); + sb.append("memory_recall(query=\"登录功能\", limit=5)\n"); + sb.append("# → 返回最相关的 5 条记忆,按相关性排序\n\n"); + sb.append("# 场景8: 查看记忆统计\n"); + sb.append("memory_stats()\n"); + sb.append("# → 显示各层级记忆数量、智能层状态、内存使用情况\n\n"); + sb.append("# 场景9: 代理间通信\n"); + sb.append("# 向 explore 代理发送消息\n"); + sb.append("send_message(\n"); + sb.append(" targetAgent=\"explore\",\n"); + sb.append(" message=\"请探索项目的目录结构,重点关注 src/main/java 目录\"\n"); + sb.append(")\n\n"); + sb.append("# 向 plan 代理发送消息\n"); + sb.append("send_message(\n"); + sb.append(" targetAgent=\"plan\",\n"); + sb.append(" message=\"需要设计一个用户认证模块,请提供实现方案\"\n"); + sb.append(")\n\n"); + sb.append("# 查看可用代理列表\n"); + sb.append("list_agents()\n\n"); + sb.append("### 记忆管理说明\n"); + sb.append("**智能记忆系统**(推荐使用):\n"); + sb.append("- **memory_store(content, key?)**: 自动分类存储\n"); + sb.append(" - 决策类(\"决策\"、\"架构\"、\"采用\")→ 永久记忆\n"); + sb.append(" - 任务类(\"完成\"、\"实现\"、\"修复\")→ 长期记忆(7天)\n"); + sb.append(" - 临时类(\"正在\"、\"尝试\"、\"临时\")→ 工作记忆(10分钟)\n"); + sb.append(" - 知识类(\"API\"、\"设计\"、\"配置\")→ 永久记忆\n"); + sb.append(" - 自动评分:基于内容、类型、关键词、时间、访问频率\n"); + sb.append("- **memory_recall(query, limit?)**: 智能检索(按相关性排序)\n"); + sb.append("- **memory_stats()**: 查看统计信息\n\n"); + sb.append("**记忆数量优化**:\n"); + sb.append("- 从 15+ 个工具简化到 3 个核心工具\n"); + sb.append("- 自动分类:无需手动选择记忆类型\n"); + sb.append("- 智能检索:只返回最相关的记忆,减少 token 消耗\n"); + sb.append("```\n"); + + return sb.toString(); + } + + + /** + * 启动团队协作任务 + * + * MainAgent 会分析任务、创建子任务、协调多个 SubAgent 协作完成 + */ + // 超时配置(单位:毫秒) + private static final long TEAM_TASK_TIMEOUT_MS = 300_000; // 5分钟超时 + + @ToolMapping(name = "team_task", + description = "启动团队协作任务。MainAgent 会自动分解任务并协调多个 SubAgent 协作完成。适用于复杂、多步骤的任务。") + public String teamTask( + @Param(name = "prompt", description = "任务描述,清晰说明目标和要求") String prompt, + String __cwd, + String __sessionId + ) { + try { + if (mainAgent.isRunning()) { + return "[WARN] 团队任务正在执行中,请等待当前任务完成。"; + } + + LOG.info("启动团队协作任务: {}", prompt); + + // 使用流式执行(实时输出) + StringBuilder streamingOutput = new StringBuilder(); + + try { + // 获取流式响应(传递 __cwd 以支持文件操作) + Flux responseStream = + mainAgent.executeStream(Prompt.of(prompt), __cwd); + + // 收集流式输出(带超时) + List chunks = responseStream + .doOnNext(chunk -> { + String content = chunk.getContent(); + if (content != null && !content.isEmpty()) { + // 实时输出到控制台 + System.out.print(content); + System.out.flush(); + streamingOutput.append(content); + } + }) + .doOnComplete(() -> { + LOG.info("\n[OK] 团队任务流式执行完成"); + }) + .doOnError(error -> { + LOG.error("团队任务流式执行出错", error); + }) + .collectList() + .block(java.time.Duration.ofMillis(TEAM_TASK_TIMEOUT_MS)); + + LOG.debug("流式响应收集完成,收到 {} 个 chunk", chunks.size()); + + } catch (TimeoutException e) { + LOG.error("团队任务执行超时({}ms)", TEAM_TASK_TIMEOUT_MS); + return "[ERROR] 团队任务执行超时(超过 " + (TEAM_TASK_TIMEOUT_MS / 1000) + " 秒)。\n\n" + + "可能原因:\n" + + "1. LLM API 响应慢或无响应\n" + + "2. 任务过于复杂,子代理调用链过长\n" + + "3. 网络连接问题\n\n" + + "建议:\n" + + "- 简化任务描述,分解为更小的子任务\n" + + "- 检查网络连接和 API 配置\n" + + "- 使用 `task(subagent_type='bash', ...)` 直接调用子代理"; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.error("团队任务执行被中断"); + return "[ERROR] 团队任务执行被中断"; + } catch (Exception e) { + LOG.error("团队任务执行失败", e); + return "[ERROR] 团队任务执行失败: " + e.getMessage(); + } + + // 获取任务统计 + SharedTaskList.TaskStatistics stats = mainAgent.getTaskList().getStatistics(); + + StringBuilder result = new StringBuilder(); + result.append("[OK] 团队任务执行完成\n\n"); + result.append("**任务统计**:\n"); + result.append(String.format("- 总任务数: %d\n", stats.totalTasks)); + result.append(String.format("- 已完成: %d\n", stats.completedTasks)); + result.append(String.format("- 失败: %d\n", stats.failedTasks)); + result.append(String.format("- 进行中: %d\n", stats.inProgressTasks)); + result.append(String.format("- 待认领: %d\n\n", stats.pendingTasks)); + + // 主 Agent 的回复(流式输出已实时打印) + result.append("**主 Agent 回复**:\n"); + + // 如果流式输出有内容,添加到结果中 + String agentResponse = streamingOutput.toString(); + if (!agentResponse.isEmpty()) { + // 限制显示长度,避免过长 + if (agentResponse.length() > 2000) { + agentResponse = agentResponse.substring(0, 2000) + "\n...(内容过长,已截断)"; + } + result.append(agentResponse); + } else { + result.append("(流式输出已完成,请查看上方控制台输出)"); + } + + return result.toString(); + + } catch (Throwable e) { + LOG.error("团队任务执行失败", e); + return "[ERROR] 团队任务执行失败: " + e.getMessage(); + } + } + + /** + * 查看团队任务状态 + */ + @ToolMapping(name = "team_status", + description = "查看当前团队任务状态,包括任务列表、进度统计等") + public String teamStatus() { + try { + SharedTaskList taskList = mainAgent.getTaskList(); + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + + StringBuilder sb = new StringBuilder(); + sb.append("## 团队任务状态\n\n"); + + // 统计信息 + sb.append("**统计**:\n"); + sb.append(String.format("- 总任务: %d\n", stats.totalTasks)); + sb.append(String.format("- [OK] 已完成: %d\n", stats.completedTasks)); + sb.append(String.format("- [ERROR] 失败: %d\n", stats.failedTasks)); + sb.append(String.format("- [PROCESS] 进行中: %d\n", stats.inProgressTasks)); + sb.append(String.format("- [WAIT] 待认领: %d\n\n", stats.pendingTasks)); + + // 任务列表 + List allTasks = taskList.getAllTasks(); + if (!allTasks.isEmpty()) { + sb.append("**任务列表**:\n\n"); + + for (TeamTask task : allTasks) { + String statusIcon = getStatusIcon(task.getStatus()); + sb.append(String.format("%s **%s** (优先级: %d)\n", + statusIcon, task.getTitle(), task.getPriority())); + sb.append(String.format(" - 类型: %s\n", task.getType())); + sb.append(String.format(" - 状态: %s\n", task.getStatus())); + + if (task.getClaimedBy() != null) { + sb.append(String.format(" - 认领者: %s\n", task.getClaimedBy())); + } + + if (task.getDependencies() != null && !task.getDependencies().isEmpty()) { + sb.append(String.format(" - 依赖: %d 个任务\n", task.getDependencies().size())); + } + + if (task.isCompleted() && task.getResult() != null) { + String result = task.getResult().toString(); + if (result.length() > 100) { + result = result.substring(0, 100) + "..."; + } + sb.append(String.format(" - 结果: %s\n", result)); + } + + if (task.isFailed() && task.getErrorMessage() != null) { + sb.append(String.format(" - 错误: %s\n", task.getErrorMessage())); + } + + sb.append("\n"); + } + } + + // 正在运行状态 + if (mainAgent.isRunning()) { + sb.append("**主 Agent 状态**: [PROCESS] 正在运行\n\n"); + } else { + sb.append("**主 Agent 状态**: [PAUSED] 空闲\n\n"); + } + + return sb.toString(); + + } catch (Throwable e) { + LOG.error("获取团队状态失败", e); + return "[ERROR] 获取团队状态失败: " + e.getMessage(); + } + } + + /** + * 创建新任务 + */ + @ToolMapping(name = "create_task", + description = "创建新的团队任务。可以设置依赖关系、优先级等。") + public String createTask( + @Param(name = "title", description = "任务标题") String title, + @Param(name = "description", required = false, description = "任务描述") String description, + @Param(name = "type", required = false, description = "任务类型 (DEVELOPMENT, EXPLORATION, TESTING, ANALYSIS, DOCUMENTATION)") String type, + @Param(name = "priority", required = false, description = "优先级 (0-10, 默认5)") Integer priority, + @Param(name = "dependencies", required = false, description = "依赖的任务ID列表,逗号分隔") String dependencies + ) { + try { + SharedTaskList taskList = mainAgent.getTaskList(); + + // 构建任务(使用手动创建而不是 Builder,避免 Lombok 问题) + TeamTask task = new TeamTask(); + task.setTitle(title); + task.setDescription(description != null ? description : ""); + + // 设置类型 + if (type != null && !type.isEmpty()) { + try { + task.setType(TeamTask.TaskType.valueOf(type.toUpperCase())); + } catch (IllegalArgumentException e) { + return "[ERROR] 无效的任务类型: " + type; + } + } else { + task.setType(TeamTask.TaskType.DEVELOPMENT); + } + + // 设置优先级 + if (priority != null) { + task.setPriority(Math.max(0, Math.min(10, priority))); + } else { + task.setPriority(5); + } + + // 设置依赖 + if (dependencies != null && !dependencies.isEmpty()) { + String[] depIds = dependencies.split(",\\s*"); + task.setDependencies(Arrays.asList(depIds)); + } else { + task.setDependencies(new ArrayList<>()); + } + + // 添加到任务列表 + CompletableFuture future = taskList.addTask(task); + TeamTask addedTask = future.join(); + + return String.format("[OK] 任务创建成功\n\n" + + "**任务ID**: %s\n" + + "**标题**: %s\n" + + "**类型**: %s\n" + + "**优先级**: %d\n" + + "**状态**: %s", + addedTask.getId(), + addedTask.getTitle(), + addedTask.getType(), + addedTask.getPriority(), + addedTask.getStatus()); + + } catch (Throwable e) { + LOG.error("创建任务失败", e); + return "[ERROR] 创建任务失败: " + e.getMessage(); + } + } + + /** + * 获取状态图标 + */ + private String getStatusIcon(TeamTask.Status status) { + switch (status) { + case PENDING: return "[WAIT]"; + case IN_PROGRESS: return "[PROCESS]"; + case COMPLETED: return "[OK]"; + case FAILED: return "[ERROR]"; + case CANCELLED: return "[STOPPED]"; + default: return "[UNKNOWN]"; + } + } + + /** + * 创建团队成员 + * + * 类似 Claude Code 的 /teammate 命令 + * 可以创建新的团队成员定义,并立即激活 + * + * 文件命名格式:{teamName}-{roleName}.md(如果指定 teamName) + * 或者 {roleName}.md(如果未指定 teamName) + */ + @ToolMapping(name = "teammate", + description = "创建新的团队成员。可以定义角色、职责、技能集,并立即激活。支持联网搜索相关资料。") + public String createTeammate( + @Param(name = "name", description = "团队成员唯一标识(如:security-expert)") String name, + @Param(name = "role", description = "角色描述(如:安全专家)") String role, + @Param(name = "description", description = "详细职责描述") String description, + @Param(name = "teamName", required = false, description = "团队名称(如:myteam)。如果不指定,将自动生成(格式:team-{timestamp})") String teamName, + @Param(name = "systemPrompt", required = false, description = "系统提示词,定义行为模式") String systemPrompt, + @Param(name = "expertise", required = false, description = "专业领域,逗号分隔(如:security,auth,encryption)") String expertise, + @Param(name = "model", required = false, description = "使用的模型(如:默认)") String model, + @Param(name = "searchContext", required = false, description = "是否联网搜索相关上下文(默认false)") Boolean searchContext, + String __cwd + ) { + try { + // 自动生成团队名称(如果未提供) + if (teamName == null || teamName.isEmpty()) { + // 1. 优先从共享内存中读取当前团队名 + if (mainAgent != null && mainAgent.getSharedMemoryManager() != null) { + String currentTeam = mainAgent.getSharedMemoryManager().get(CURRENT_TEAM_KEY); + if (currentTeam != null && !currentTeam.isEmpty()) { + teamName = currentTeam; + LOG.info("从内存中读取到当前团队名称: {}", teamName); + } + } + + // 2. 如果内存中也没有,智能生成新的团队名 + if (teamName == null || teamName.isEmpty()) { + // 使用 TeamNameGenerator 根据角色和描述生成有意义的团队名 + String taskGoal = description != null ? description : role; + teamName = TeamNameGenerator.generateTeamName(name, role, taskGoal); + + LOG.info("智能生成新的团队名称: role={}, description={}, teamName={}", + role, description, teamName); + } + } + + // 3. 将使用的团队名保存到共享内存(短期记忆,1小时TTL) + if (mainAgent != null && mainAgent.getSharedMemoryManager() != null) { + mainAgent.getSharedMemoryManager().putShortTerm(CURRENT_TEAM_KEY, teamName, 3600); // 1小时 + LOG.info("已将团队名称保存到内存: {} (TTL: 1小时)", teamName); + } + + // 4. 检查是否已存在相同 role 的 agent(避免重复覆盖) + if (manager.hasAgent(role)) { + LOG.warn("警告:已存在 role='{}' 的子代理,新成员可能会覆盖它。建议使用唯一的 role 名称。", role); + } + + // 如果需要联网搜索上下文 + if (searchContext != null && searchContext && kernel != null) { + LOG.info("为 teammate {} 搜索相关上下文...", name); + LOG.info("联网搜索功能需要进一步集成 WebSearch 工具"); + } + + // 构建子代理元数据 + SubAgentMetadata metadata = new SubAgentMetadata(); + metadata.setCode(name); + metadata.setName(role); + metadata.setDescription(description); + metadata.setEnabled(true); + metadata.setTeamName(teamName); + + // 设置专业领域 + if (expertise != null && !expertise.isEmpty()) { + metadata.setSkills(Arrays.asList(expertise.split(",\\s*"))); + } + + // 设置模型 + if (model != null && !model.isEmpty()) { + metadata.setModel(model); + } + + // 生成系统提示词(如果没有提供) + String finalPrompt = systemPrompt; + if (finalPrompt == null || finalPrompt.isEmpty()) { + finalPrompt = generateDefaultSystemPrompt(name, role, description, expertise); + } + + // 生成完整的代理定义 + String agentDefinition = metadata.toYamlFrontmatterWithPrompt(finalPrompt); + + // 保存到文件(所有成员都归属于某个团队) + Path teamsDir = Paths.get(__cwd, ".soloncode", "agentsTeams", teamName); + Files.createDirectories(teamsDir); + Path agentFile = teamsDir.resolve(name + ".md"); + LOG.info("创建团队成员: 团队={}, 角色={}, 文件={}", teamName, name, agentFile); + + Files.write(agentFile, agentDefinition.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + LOG.info("Agent 定义已保存到: {}", agentFile); + + // 重新扫描目录以加载新创建的 agent(使用 putIfAbsent 避免重复) + if (manager != null) { + try { + Path parentDir = agentFile.getParent(); + manager.agentPool(parentDir, false); + LOG.info("已重新扫描目录并加载新团队成员: {}", name); + } catch (Throwable ex) { + LOG.warn("重新扫描目录失败(文件已保存): {}", ex.getMessage()); + } + } + + // 返回结果(使用表格格式) + StringBuilder result = new StringBuilder(); + result.append("[OK] 团队成员创建成功\n\n"); + + // 表格格式的成员信息 + result.append("## 成员信息\n\n"); + result.append("| 属性 | 值 |\n"); + result.append("|------|------|\n"); + result.append(String.format("| **名称** | `%s` |\n", name)); + result.append(String.format("| **角色** | %s |\n", role)); + result.append(String.format("| **描述** | %s |\n", description)); + result.append(String.format("| **所属团队** | %s |\n", teamName)); + result.append(String.format("| **文件路径** | `.soloncode/agentsTeams/%s/%s.md` |\n", teamName, name)); + + if (expertise != null && !expertise.isEmpty()) { + result.append(String.format("| **专业领域** | %s |\n", expertise)); + } + + if (model != null && !model.isEmpty()) { + result.append(String.format("| **模型** | %s |\n", model)); + } + + result.append(String.format("| **状态** | 🟢 已激活 |\n")); + + result.append("\n**使用方法**:\n"); + result.append("```bash\n"); + result.append(String.format("task(subagent_type=\"%s\", prompt=\"你的任务描述\")\n", name)); + result.append("```\n"); + + return result.toString(); + + } catch (Throwable e) { + LOG.error("创建团队成员失败", e); + return "[ERROR] 创建团队成员失败: " + e.getMessage(); + } + } + + /** + * 列出团队成员(支持按团队筛选) + * + * 类似 Claude Code 的 /teammates 命令 + * 使用表格格式输出 + * + * @param teamName 可选。指定团队名称,只显示该团队的成员 + */ + @ToolMapping(name = "teammates", + description = "列出团队成员,支持按团队筛选。只显示自定义创建的团队成员,不包括内置子代理。") + public String listTeammates( + @Param(name = "teamName", required = false, description = "可选。团队名称(如 'test'),只显示该团队的成员") String teamName, + @Param(name = "includeBuiltIn", required = false, description = "可选。是否包括内置子代理(explore、bash、plan等),默认false") Boolean includeBuiltIn + ) { + try { + List allAgents = new ArrayList<>(manager.getAgents()); + + // 根据团队名称筛选 + List agents; + if (teamName != null && !teamName.isEmpty()) { + // 指定团队名:只显示该团队的成员 + agents = allAgents.stream() + .filter(agent -> agent.getMetadata().hasTeamName() && + agent.getMetadata().getTeamName().equals(teamName)) + .collect(java.util.stream.Collectors.toList()); + } else if (includeBuiltIn != null && includeBuiltIn) { + // 显式要求包括内置agents:显示所有 + agents = allAgents; + } else { + // 默认:只显示自定义团队成员(有 teamName 的) + agents = allAgents.stream() + .filter(agent -> agent.getMetadata().hasTeamName()) + .collect(java.util.stream.Collectors.toList()); + } + + StringBuilder result = new StringBuilder(); + + // 标题:显示是否筛选 + if (teamName != null && !teamName.isEmpty()) { + result.append(String.format("## 团队成员: %s\n\n", teamName)); + } else if (includeBuiltIn != null && includeBuiltIn) { + result.append("## 所有子代理(包括内置)\n\n"); + } else { + result.append("## 团队成员(自定义)\n\n"); + } + + if (agents.isEmpty()) { + if (teamName != null && !teamName.isEmpty()) { + result.append(String.format("[WARN] 团队 '%s' 中没有成员。\n\n", teamName)); + result.append("使用 `teammate()` 命令创建新成员。\n"); + } else { + result.append("[WARN] 当前没有团队成员。\n\n"); + result.append("使用 `teammate()` 命令创建新成员。\n"); + } + return result.toString(); + } + + // 表格格式的成员列表 + result.append("| 名称 | 角色 | 描述 | 团队 | 状态 | 模型 |\n"); + result.append("|------|------|------|------|------|------|\n"); + + for (Subagent agent : agents) { + String name = String.format("`%s`", agent.getType()); + String role = agent.getClass().getSimpleName().replace("Subagent", ""); + String desc = truncate(agent.getDescription(), 30); + String team = agent.getMetadata().hasTeamName() ? agent.getMetadata().getTeamName() : "-"; + String status = "🟢 活跃"; + String model = agent.getMetadata().getModel() != null ? agent.getMetadata().getModel() : "默认"; + + result.append(String.format("| %s | %s | %s | %s | %s | %s |\n", + name, role, desc, team, status, model)); + } + + if (teamName != null && !teamName.isEmpty()) { + result.append(String.format("\n**总计**: %d 位成员(团队: %s)\n\n", agents.size(), teamName)); + } else { + result.append(String.format("\n**总计**: %d 位活跃成员\n\n", agents.size())); + } + + // 添加使用提示 + result.append("**快速操作**:\n"); + result.append("```bash\n"); + result.append("# 创建新成员\n"); + result.append("teammate(name=\"expert\", role=\"专家\", description=\"...\")\n\n"); + result.append("# 调用成员\n"); + result.append("task(subagent_type=\"explore\", prompt=\"任务描述\")\n\n"); + result.append("# 查看任务状态\n"); + result.append("team_status()\n"); + result.append("```\n"); + + return result.toString(); + + } catch (Throwable e) { + LOG.error("列出团队成员失败", e); + return "[ERROR] 列出团队成员失败: " + e.getMessage(); + } + } + + /** + * 移除团队成员 + */ + @ToolMapping(name = "remove_teammate", + description = "移除指定的团队成员。注意:这只是禁用成员,不会删除配置文件。") + public String removeTeammate( + @Param(name = "name", description = "要移除的团队成员名称") String name, + String __cwd + ) { + try { + // 查找成员 + Subagent agent = manager.getAgent(name); + if (agent == null) { + return String.format("[ERROR] 未找到团队成员: `%s`\n\n可用的成员:\n%s", + name, listTeammates(null, null)); + } + + // 禁用成员(通过修改配置文件) + Path agentsDir = Paths.get(__cwd, ".soloncode", "agents"); + Path agentFile = agentsDir.resolve(name + ".md"); + + if (Files.exists(agentFile)) { + String content = new String(Files.readAllBytes(agentFile), java.nio.charset.StandardCharsets.UTF_8); + + // 将 enabled: true 改为 enabled: false + content = content.replace("enabled: true", "enabled: false"); + + Files.write(agentFile, content.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + + return String.format("[OK] 团队成员已禁用: `%s`\n\n" + + "**提示**: 配置文件已保留,如需重新激活,请编辑 `.soloncode/agents/%s.md` 并设置 `enabled: true`。", + name, name); + + } catch (Throwable e) { + LOG.error("移除团队成员失败", e); + return "[ERROR] 移除团队成员失败: " + e.getMessage(); + } + } + + @ToolMapping(name = "isTeamsEnabled", + description = "检查是否已开启团队功能") + public String isTeamsEnabled() { + return kernel.getProperties().isTeamsEnabled() ? "团队功能已启用" : "[WARN] 团队功能未启用。请先启用团队功能。"; + } + + /** + * 生成默认系统提示词 + */ + private String generateDefaultSystemPrompt(String name, String role, String description, String expertise) { + StringBuilder prompt = new StringBuilder(); + prompt.append(String.format("# %s\n\n", role)); + prompt.append(String.format("你是 %s,专门负责 %s。\n\n", role, description)); + prompt.append("## 工作原则\n\n"); + prompt.append("1. **专业专注**: 始终在你的专业领域内提供建议和解决方案\n"); + prompt.append("2. **质量优先**: 注重代码质量和最佳实践\n"); + prompt.append("3. **协作配合**: 与其他团队成员保持良好沟通\n"); + prompt.append("4. **持续学习**: 不断更新知识,掌握最新技术趋势\n\n"); + + if (expertise != null && !expertise.isEmpty()) { + prompt.append("## 专业领域\n\n"); + String[] areas = expertise.split(",\\s*"); + for (String area : areas) { + prompt.append(String.format("- %s\n", area)); + } + prompt.append("\n"); + } + + prompt.append("## 沟通风格\n\n"); + prompt.append("- 使用清晰、简洁的语言\n"); + prompt.append("- 提供具体的代码示例\n"); + prompt.append("- 解释技术决策的理由\n"); + prompt.append("- 在不确定时主动寻求帮助\n"); + + return prompt.toString(); + } + + /** + * 截断文本 + */ + private String truncate(String text, int maxLength) { + if (text == null) { + return ""; + } + if (text.length() <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + "..."; + } + + // ==================== 记忆管理工具(智能接口)==================== + + /** + * 工具 1:智能存储记忆 + * + *

自动分类和评分,无需用户选择记忆类型 + * + *

**自动分类规则**: + * - 决策类("决策"、"架构"、"采用")→ 永久记忆 + * - 任务结果类("完成"、"实现"、"修复")→ 长期记忆 + * - 临时上下文("正在"、"尝试"、"临时")→ 工作记忆 + * - 知识类("API"、"设计"、"配置")→ 永久记忆 + * + *

**重要性评分**(0-10分): + * - 内容特征(30%):代码、数据、文件路径 + * - 观察类型(25%):决策=2.5, 架构=2.5 + * - 关键词(25%):关键关键词=+4.0 + * - 时间新鲜度(10%):30分钟内=+0.5 + * - 访问热度(10%):对数增长 + * + * @param content 记忆内容(必填) + * @param key 记忆键(可选,不填则自动生成 UUID) + * @return 存储结果 + * + * @sample + * memory_store(content="决定使用 MemoryBank 架构", key="architecture-decision") + * → "[OK] 已存储到 永久记忆 (重要性: 8.5/10, TTL: 永久)" + * + * @sample + * memory_store(content="正在分析代码结构") + * → "[OK] 已存储到 工作记忆 (重要性: 3.2/10, TTL: 10分钟)" + */ + @ToolMapping(name = "memory_store", + description = "智能存储记忆(自动分类和评分)。根据内容自动判断存储类型(永久/7天/1小时/10分钟),无需手动选择。") + public String memoryStore( + @Param(name = "content", description = "记忆内容(必填)") String content, + @Param(name = "key", description = "记忆键(可选,不填则自动生成)", required = false) String key) { + if (intelligentMemoryManager == null) { + return "[WARN] 智能记忆管理器未初始化"; + } + // 使用智能记忆管理器 + return intelligentMemoryManager.store(key, content); + } + + /** + * 工具 2:智能检索记忆 + * + *

基于相关性排序,只返回最相关的记忆 + * + *

**相关性算法**: + * - 文本匹配(40%):关键词重叠度 + * - 重要性(30%):重要性分数 + * - 时间新鲜度(20%):1小时内=+2.0 + * - 访问热度(10%):对数增长 + * + * @param query 查询内容(空则返回所有记忆) + * @param limit 返回数量限制(默认 10) + * @return 检索结果 + * + * @sample + * memory_recall(query="登录功能", limit=5) + * → "找到 5 条记忆..." + */ + @ToolMapping(name = "memory_recall", + description = "智能检索记忆(按相关性排序)。基于文本匹配、重要性、时间新鲜度等多维度排序,只返回最相关的记忆。") + public String memoryRecall( + @Param(name = "query", description = "查询内容(空则返回所有记忆)", required = false) String query, + @Param(name = "limit", description = "返回数量限制(默认 10)", required = false) Integer limit) { + if (intelligentMemoryManager == null) { + return "[WARN] 智能记忆管理器未初始化"; + } + return intelligentMemoryManager.retrieve(query, limit != null ? limit : 10); + } + + /** + * 工具 3:获取记忆统计信息 + * + * @return 统计信息 + */ + @ToolMapping(name = "memory_stats", + description = "获取记忆统计信息。包括各层级记忆数量、智能层状态、内存使用情况等。") + public String memoryStats() { + if (intelligentMemoryManager == null) { + return "[WARN] 智能记忆管理器未初始化"; + } + + Map stats = intelligentMemoryManager.getStats(); + + // 格式化输出 + StringBuilder sb = new StringBuilder(); + sb.append("[STATS] 记忆系统统计:\n\n"); + + // 各层级记忆数量 + sb.append("工作记忆: ").append(stats.getOrDefault("workingCount", 0)).append(" 条\n"); + sb.append("短期记忆: ").append(stats.getOrDefault("shortTermCount", 0)).append(" 条\n"); + sb.append("长期记忆: ").append(stats.getOrDefault("longTermCount", 0)).append(" 条\n"); + sb.append("知识记忆: ").append(stats.getOrDefault("knowledgeCount", 0)).append(" 条\n"); + + sb.append("\n"); + + // 智能层状态 + sb.append("智能层: ").append(stats.getOrDefault("intelligentLayer", "未启用")).append("\n"); + sb.append("自动合并: ").append(stats.getOrDefault("autoConsolidate", false)).append("\n"); + + if (stats.containsKey("consolidationThreshold")) { + sb.append("合并阈值: ").append(stats.get("consolidationThreshold")).append("\n"); + } + if (stats.containsKey("consolidationInterval")) { + sb.append("合并间隔: ").append(stats.get("consolidationInterval")).append("ms\n"); + } + + sb.append("\n"); + + // 内存使用情况 + if (stats.containsKey("memoryUsed")) { + long used = (long) stats.get("memoryUsed"); + long max = (long) stats.getOrDefault("memoryMax", 100 * 1024 * 1024); + double ratio = (double) used / max * 100; + + sb.append(String.format("内存使用: %.2fMB / %.2fMB (%.1f%%)\n", + used / (1024.0 * 1024), + max / (1024.0 * 1024), + ratio)); + } + + return sb.toString(); + } + + /** + * 获取工作记忆状态 + */ + @ToolMapping(name = "get_working_memory", + description = "获取当前工作记忆状态(包括当前任务、状态、步骤等)") + public String getWorkingMemory() { + try { + if (mainAgent == null || mainAgent.getSharedMemoryManager() == null) { + return "[WARN] 共享记忆未初始化"; + } + + // 使用默认的 taskId "main-agent" + String taskId = "main-agent"; + org.noear.solon.bot.core.memory.WorkingMemory workingMemory = + mainAgent.getSharedMemoryManager().getWorking(taskId); + + if (workingMemory == null) { + return "[WARN] 没有工作记忆"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("## 工作记忆状态\n\n"); + + if (workingMemory.getTaskDescription() != null) { + sb.append("**当前任务**: ").append(workingMemory.getTaskDescription()).append("\n"); + } + if (workingMemory.getStatus() != null) { + sb.append("**状态**: ").append(workingMemory.getStatus()).append("\n"); + } + sb.append("**步骤**: ").append(workingMemory.getStep()).append("\n"); + if (workingMemory.getCurrentAgent() != null) { + sb.append("**当前代理**: ").append(workingMemory.getCurrentAgent()).append("\n"); + } + + return sb.toString(); + } catch (Exception e) { + LOG.error("获取工作记忆失败", e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } + + /** + * 更新工作记忆状态 + */ + @ToolMapping(name = "update_working_memory", + description = "更新工作记忆状态(设置当前任务、状态、步骤等)") + public String updateWorkingMemory( + @Param(name = "field", description = "字段名称(taskDescription/status/step/currentAgent)") String field, + @Param(name = "value", description = "字段值") String value) { + try { + if (mainAgent == null || mainAgent.getSharedMemoryManager() == null) { + return "[WARN] 共享记忆未初始化"; + } + + // 使用默认的 taskId "main-agent" + String taskId = "main-agent"; + org.noear.solon.bot.core.memory.WorkingMemory workingMemory = + mainAgent.getSharedMemoryManager().getWorking(taskId); + + // 如果不存在,创建一个新的 + if (workingMemory == null) { + workingMemory = new org.noear.solon.bot.core.memory.WorkingMemory(taskId); + } + + switch (field.toLowerCase()) { + case "taskdescription": + case "currenttask": + workingMemory.setTaskDescription(value); + break; + case "status": + workingMemory.setStatus(value); + break; + case "step": + workingMemory.setStep(Integer.parseInt(value)); + break; + case "currentagent": + workingMemory.setCurrentAgent(value); + break; + default: + return "[ERROR] 无效的字段名: " + field + "。支持的字段:taskDescription、status、step、currentAgent"; + } + + // 保存更新后的工作记忆 + mainAgent.getSharedMemoryManager().storeWorking(workingMemory); + + LOG.debug("更新工作记忆: {}={}", field, value); + return "[OK] 工作记忆已更新: " + field + " = " + value; + } catch (NumberFormatException e) { + return "[ERROR] 步骤必须是数字: " + value; + } catch (Exception e) { + LOG.error("更新工作记忆失败", e); + return "[ERROR] 更新失败: " + e.getMessage(); + } + } + + /** + * 发布团队事件 + */ + @ToolMapping(name = "publish_team_event", + description = "发布团队事件(通知其他代理任务状态变化)") + public String publishTeamEvent( + @Param(name = "eventType", description = "事件类型(TASK_COMPLETED/TASK_FAILED/INFO等)") String eventType, + @Param(name = "data", description = "事件数据") String data) { + try { + if (mainAgent == null || mainAgent.getEventBus() == null) { + return "[WARN] 事件总线未初始化"; + } + + org.noear.solon.bot.core.event.AgentEventType type; + try { + type = org.noear.solon.bot.core.event.AgentEventType.valueOf(eventType.toUpperCase()); + } catch (IllegalArgumentException e) { + return "[ERROR] 无效的事件类型: " + eventType; + } + + org.noear.solon.bot.core.event.AgentEvent event = + new org.noear.solon.bot.core.event.AgentEvent(type, data, null); + mainAgent.getEventBus().publish(event); + + LOG.debug("发布团队事件: type={}, data={}", type, data); + return "[OK] 团队事件已发布: " + type; + } catch (Exception e) { + LOG.error("发布团队事件失败", e); + return "[ERROR] 发布失败: " + e.getMessage(); + } + } + + /** + * 获取任务统计信息 + */ + @ToolMapping(name = "get_task_statistics", + description = "获取任务统计信息(总任务数、已完成、失败、进行中、待认领等)") + public String getTaskStatistics() { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList.TaskStatistics stats = mainAgent.getTaskList().getStatistics(); + + StringBuilder sb = new StringBuilder(); + sb.append("## 任务统计\n\n"); + sb.append("**总任务数**: ").append(stats.totalTasks).append("\n"); + sb.append("**已完成**: ").append(stats.completedTasks).append("\n"); + sb.append("**失败**: ").append(stats.failedTasks).append("\n"); + sb.append("**进行中**: ").append(stats.inProgressTasks).append("\n"); + sb.append("**待认领**: ").append(stats.pendingTasks).append("\n"); + + return sb.toString(); + } catch (Exception e) { + LOG.error("获取任务统计失败", e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } + + // ==================== 任务管理工具 ==================== + + /** + * 认领任务 + */ + @ToolMapping(name = "claim_task", + description = "认领待处理的任务(将任务状态从PENDING改为IN_PROGRESS)") + public String claimTask( + @Param(name = "taskId", description = "任务ID") String taskId, + @Param(name = "agentName", description = "认领任务的代理名称(如:explore、plan、bash)") String agentName) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + TeamTask task = taskList.getTask(taskId); + + if (task == null) { + return "[ERROR] 任务不存在: " + taskId; + } + + if (task.getStatus() != TeamTask.Status.PENDING) { + return "[WARN] 任务状态不是待认领: " + task.getStatus(); + } + + taskList.claimTask(taskId, agentName); + + LOG.info("任务已认领: taskId={}, agent={}", taskId, agentName); + return "[OK] 任务已认领: " + taskId + " 由 " + agentName; + } catch (Exception e) { + LOG.error("认领任务失败", e); + return "[ERROR] 认领失败: " + e.getMessage(); + } + } + + /** + * 完成任务 + */ + @ToolMapping(name = "complete_task", + description = "标记任务为已完成(将任务状态改为COMPLETED)") + public String completeTask( + @Param(name = "taskId", description = "任务ID") String taskId, + @Param(name = "result", description = "任务执行结果(可选)") String result) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + TeamTask task = taskList.getTask(taskId); + + if (task == null) { + return "[ERROR] 任务不存在: " + taskId; + } + + if (task.getStatus() != TeamTask.Status.IN_PROGRESS) { + return "[WARN] 任务状态不是进行中: " + task.getStatus(); + } + + taskList.completeTask(taskId, result); + + // 如果提供了结果,额外存储到记忆 + if (result != null && !result.isEmpty()) { + String key = "task-result:" + task.getTitle() + ":" + System.currentTimeMillis(); + mainAgent.getSharedMemoryManager().putLongTerm(key, result.toString(), 604800); + } + + LOG.info("任务已完成: taskId={}", taskId); + return "[OK] 任务已完成: " + taskId; + } catch (Exception e) { + LOG.error("完成任务失败", e); + return "[ERROR] 完成失败: " + e.getMessage(); + } + } + + /** + * 标记任务失败 + */ + @ToolMapping(name = "fail_task", + description = "标记任务为失败(将任务状态改为FAILED)") + public String failTask( + @Param(name = "taskId", description = "任务ID") String taskId, + @Param(name = "errorMessage", description = "错误消息(说明失败原因)") String errorMessage) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + TeamTask task = taskList.getTask(taskId); + + if (task == null) { + return "[ERROR] 任务不存在: " + taskId; + } + + taskList.failTask(taskId, errorMessage); + + LOG.error("任务失败: taskId={}, error={}", taskId, errorMessage); + return "[ERROR] 任务已标记为失败: " + taskId; + } catch (Exception e) { + LOG.error("标记任务失败", e); + return "[ERROR] 操作失败: " + e.getMessage(); + } + } + + /** + * 获取任务详情 + */ + @ToolMapping(name = "get_task_details", + description = "获取任务的详细信息(包括ID、标题、描述、状态、依赖等)") + public String getTaskDetails( + @Param(name = "taskId", description = "任务ID") String taskId) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + TeamTask task = taskList.getTask(taskId); + + if (task == null) { + return "[ERROR] 任务不存在: " + taskId; + } + + StringBuilder sb = new StringBuilder(); + sb.append("## 任务详情\n\n"); + sb.append("**ID**: ").append(task.getId()).append("\n"); + sb.append("**标题**: ").append(task.getTitle()).append("\n"); + sb.append("**描述**: ").append(task.getDescription()).append("\n"); + sb.append("**类型**: ").append(task.getType()).append("\n"); + sb.append("**状态**: ").append(task.getStatus()).append("\n"); + sb.append("**优先级**: ").append(task.getPriority()).append("\n"); + + if (task.getClaimedBy() != null) { + sb.append("**认领者**: ").append(task.getClaimedBy()).append("\n"); + } + + if (task.getDependencies() != null && !task.getDependencies().isEmpty()) { + sb.append("**依赖任务**: "); + sb.append(String.join(", ", task.getDependencies())).append("\n"); + } + + if (task.getResult() != null) { + sb.append("**结果**: ").append(task.getResult()).append("\n"); + } + + return sb.toString(); + } catch (Exception e) { + LOG.error("获取任务详情失败", e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } + + /** + * 列出所有任务 + */ + @ToolMapping(name = "list_all_tasks", + description = "列出所有任务(包括待认领、进行中、已完成、失败的任务)") + public String listAllTasks( + @Param(name = "status", description = "可选:按状态过滤(PENDING/IN_PROGRESS/COMPLETED/FAILED)") String status) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + List tasks; + + if (status != null && !status.isEmpty()) { + TeamTask.Status taskStatus; + try { + taskStatus = TeamTask.Status.valueOf(status.toUpperCase()); + } catch (IllegalArgumentException e) { + return "[ERROR] 无效的状态: " + status; + } + tasks = taskList.getTasksByStatus(taskStatus); + } else { + tasks = taskList.getAllTasks(); + } + + if (tasks.isEmpty()) { + return "[WARN] 没有任务"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("## 任务列表 (").append(tasks.size()).append(" 个任务)\n\n"); + + for (TeamTask task : tasks) { + String statusIcon = getStatusIcon(task.getStatus()); + sb.append(String.format("%s **%s** (ID: %s)\n", + statusIcon, task.getTitle(), task.getId())); + sb.append(String.format(" - 类型: %s\n", task.getType())); + sb.append(String.format(" - 优先级: %d\n", task.getPriority())); + sb.append(String.format(" - 状态: %s\n", task.getStatus())); + + if (task.getClaimedBy() != null) { + sb.append(String.format(" - 认领者: %s\n", task.getClaimedBy())); + } + + sb.append("\n"); + } + + return sb.toString(); + } catch (Exception e) { + LOG.error("列出任务失败", e); + return "[ERROR] 列出失败: " + e.getMessage(); + } + } + + /** + * 添加任务依赖 + */ + @ToolMapping(name = "add_task_dependency", + description = "为任务添加依赖关系(任务将等待依赖任务完成后才能开始)") + public String addTaskDependency( + @Param(name = "taskId", description = "任务ID") String taskId, + @Param(name = "dependsOnTaskId", description = "依赖的任务ID") String dependsOnTaskId) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + TeamTask task = taskList.getTask(taskId); + + if (task == null) { + return "[ERROR] 任务不存在: " + taskId; + } + + TeamTask dependsOnTask = taskList.getTask(dependsOnTaskId); + if (dependsOnTask == null) { + return "[ERROR] 依赖任务不存在: " + dependsOnTaskId; + } + + // 添加依赖到任务的依赖列表 + if (task.getDependencies() == null) { + task.setDependencies(new java.util.ArrayList<>()); + } + task.getDependencies().add(dependsOnTaskId); + + LOG.info("添加任务依赖: {} depends on {}", taskId, dependsOnTaskId); + return "[OK] 依赖关系已添加: " + taskId + " 依赖于 " + dependsOnTaskId; + } catch (Exception e) { + LOG.error("添加任务依赖失败", e); + return "[ERROR] 添加失败: " + e.getMessage(); + } + } + + /** + * 获取可认领的任务 + */ + @ToolMapping(name = "get_claimable_tasks", + description = "获取当前可以认领的任务(所有依赖已完成的状态为PENDING的任务)") + public String getClaimableTasks( + @Param(name = "limit", description = "返回结果数量限制,默认10") Integer limit) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + List pendingTasks = taskList.getTasksByStatus(TeamTask.Status.PENDING); + + // 过滤出依赖已完成的任务 + List claimableTasks = new java.util.ArrayList<>(); + for (TeamTask task : pendingTasks) { + // 手动检查依赖是否都已完成 + boolean allDependenciesCompleted = true; + if (task.getDependencies() != null && !task.getDependencies().isEmpty()) { + for (String depId : task.getDependencies()) { + TeamTask depTask = taskList.getTask(depId); + if (depTask == null || !depTask.isCompleted()) { + allDependenciesCompleted = false; + break; + } + } + } + + if (allDependenciesCompleted) { + claimableTasks.add(task); + } + } + + int actualLimit = limit != null && limit > 0 ? limit : 10; + if (claimableTasks.size() > actualLimit) { + claimableTasks = claimableTasks.subList(0, actualLimit); + } + + if (claimableTasks.isEmpty()) { + return "[WARN] 没有可认领的任务"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("## 可认领任务 (").append(claimableTasks.size()).append(" 个任务)\n\n"); + + for (TeamTask task : claimableTasks) { + sb.append(String.format("- **%s** (ID: %s)\n", task.getTitle(), task.getId())); + sb.append(String.format(" - 类型: %s\n", task.getType())); + sb.append(String.format(" - 优先级: %d\n", task.getPriority())); + sb.append(String.format(" - 描述: %s\n\n", + truncate(task.getDescription(), 100))); + } + + return sb.toString(); + } catch (Exception e) { + LOG.error("获取可认领任务失败", e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } + + /** + * 更新任务结果 + */ + @ToolMapping(name = "update_task_result", + description = "更新任务的执行结果(用于部分完成或阶段性成果)") + public String updateTaskResult( + @Param(name = "taskId", description = "任务ID") String taskId, + @Param(name = "result", description = "任务结果(可以是进度报告、部分成果等)") String result) { + try { + if (mainAgent == null) { + return "[WARN] MainAgent 未初始化"; + } + + SharedTaskList taskList = mainAgent.getTaskList(); + TeamTask task = taskList.getTask(taskId); + + if (task == null) { + return "[ERROR] 任务不存在: " + taskId; + } + + task.setResult(result); + + LOG.info("任务结果已更新: taskId={}", taskId); + return "[OK] 任务结果已更新: " + taskId; + } catch (Exception e) { + LOG.error("更新任务结果失败", e); + return "[ERROR] 更新失败: " + e.getMessage(); + } + } + + /** + * 获取记忆内容(辅助方法) + */ + private String getMemoryContent(Memory memory) { + if (memory instanceof ShortTermMemory) { + return ((ShortTermMemory) memory).getContext(); + } else if (memory instanceof LongTermMemory) { + return ((LongTermMemory) memory).getSummary(); + } else if (memory instanceof KnowledgeMemory) { + return ((KnowledgeMemory) memory).getContent(); + } else { + return memory.getId(); + } + } + + // ==================== 代理间通信工具 ==================== + + /** + * 发送消息给其他代理 + */ + @ToolMapping(name = "send_message", + description = "发送消息给其他代理(点对点通信)。注意:避免相互发送消息导致死锁。") + public String sendMessage( + @Param(name = "targetAgent", description = "目标代理名称(如:explore、plan、bash、general-purpose)") String targetAgent, + @Param(name = "message", description = "消息内容") String message) { + try { + if (mainAgent == null || mainAgent.getMessageChannel() == null) { + return "[WARN] 消息通道未启用"; + } + + // 检查是否会导致死锁(main-agent 发送给自己) + if ("main-agent".equals(targetAgent)) { + return "[ERROR] 不能发送消息给自己,这会导致死锁"; + } + + // 使用 Builder 创建消息 + org.noear.solon.bot.core.message.AgentMessage agentMessage = + org.noear.solon.bot.core.message.AgentMessage.of(message) + .from("main-agent") + .to(targetAgent) + .type("command") + .build(); + + // 发送消息并等待完成(超时5秒) + java.util.concurrent.CompletableFuture ackFuture = + mainAgent.getMessageChannel().send(agentMessage); + + // 获取结果(超时5秒) + org.noear.solon.bot.core.message.MessageAck ack = + ackFuture.get(5, java.util.concurrent.TimeUnit.SECONDS); + + if (ack.isSuccess()) { + LOG.debug("消息已发送: to={}, msg={}", targetAgent, message); + return "[OK] 消息已发送给 " + targetAgent; + } else { + LOG.warn("消息发送失败: to={}, error={}", targetAgent, ack.getMessage()); + return "[ERROR] 消息发送失败: " + ack.getMessage(); + } + } catch (java.util.concurrent.TimeoutException e) { + LOG.error("发送消息超时(可能死锁): to={}", targetAgent); + return "[WARN] 发送超时(可能死锁): 目标代理可能在等待当前代理的响应"; + } catch (Exception e) { + LOG.error("发送消息失败", e); + return "[ERROR] 发送失败: " + e.getMessage(); + } + } + + /** + * 获取可用代理列表 + */ + @ToolMapping(name = "list_agents", + description = "列出所有可用的子代理(用于查看可以向哪些代理发送消息)") + public String listAgents() { + try { + StringBuilder sb = new StringBuilder(); + sb.append("## 可用的子代理\n\n"); + + sb.append("**内置子代理**:\n"); + sb.append("- `explore`: 代码库探索专家(快速查找文件和理解结构)\n"); + sb.append("- `plan`: 软件架构师(设计实现方案和执行计划)\n"); + sb.append("- `bash`: 命令执行专家(处理 git、构建等终端任务)\n"); + sb.append("- `general-purpose`: 通用代理(处理复杂的多步骤任务)\n"); + sb.append("- `solon-code-guide`: Solon 框架文档指南专家\n\n"); + + sb.append("**团队成员**:\n"); + // 从 SubagentManager 获取自定义团队成员 + for (Subagent agent : manager.getAgents()) { + if (!agent.getType().equals("explore") + && !agent.getType().equals("plan") + && !agent.getType().equals("bash") + && !agent.getType().equals("general-purpose") + && !agent.getType().equals("solon-code-guide")) { + sb.append(String.format("- `%s`: %s\n", + agent.getType(), agent.getDescription())); + } + } + + sb.append("\n**提示**:\n"); + sb.append("- 使用 `send_message(targetAgent, message)` 向指定代理发送消息\n"); + sb.append("- 使用 `task(subagent_type, prompt)` 调用代理并获取响应\n"); + sb.append("- 使用 `teammates()` 查看所有团队成员\n"); + + return sb.toString(); + } catch (Exception e) { + LOG.error("列出代理失败", e); + return "[ERROR] 列出失败: " + e.getMessage(); + } + } + + /** + * 查看消息统计信息 + */ + @ToolMapping(name = "get_message_stats", + description = "查看消息统计信息(待处理消息数量等)") + public String getMessageStats() { + try { + if (mainAgent == null || mainAgent.getMessageChannel() == null) { + return "[WARN] 消息通道未启用"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("## 消息统计\n\n"); + + // 获取 main-agent 的待处理消息数量 + int pendingCount = mainAgent.getMessageChannel().getPendingMessageCount("main-agent"); + sb.append("**待处理消息数**: ").append(pendingCount).append("\n"); + + sb.append("\n提示: 使用 send_message 发送消息给其他代理"); + return sb.toString(); + } catch (Exception e) { + LOG.error("获取消息统计失败", e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/AgentTeamsTools.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/AgentTeamsTools.java new file mode 100644 index 0000000000000000000000000000000000000000..8b77bb87df426c0b62bd31f894a1607c57011610 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/AgentTeamsTools.java @@ -0,0 +1,407 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.noear.solon.ai.chat.skill.AbsSkill; +import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.ai.annotation.ToolMapping; +import org.noear.solon.annotation.Param; +import org.noear.solon.bot.core.event.AgentEvent; +import org.noear.solon.bot.core.event.AgentEventType; +import org.noear.solon.bot.core.event.EventBus; +import org.noear.solon.bot.core.memory.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Agent Teams 工具集 + * + * 提供 MainAgent 内部使用的核心工具: + * - 记忆存储和读取 + * - 事件发布 + * + * 注意:代理间通信工具(send_message、list_agents)在 AgentTeamsSkill 中提供 + * + * @author bai + * @since 3.9.5 + */ +public class AgentTeamsTools extends AbsSkill { + + private static final Logger LOG = LoggerFactory.getLogger(AgentTeamsTools.class); + + private final SharedMemoryManager memoryManager; + private final EventBus eventBus; + + public AgentTeamsTools(SharedMemoryManager memoryManager, + EventBus eventBus) { + this.memoryManager = memoryManager; + this.eventBus = eventBus; + } + + @Override + public String description() { + return "Agent Teams 工具集:提供记忆存储、事件发布、消息传递等功能"; + } + + @Override + public String getInstruction(Prompt prompt) { + StringBuilder sb = new StringBuilder(); + sb.append("## Agent Teams 内部工具集(底层API)\n\n"); + sb.append("这是 MainAgent 内部使用的底层工具集,提供记忆管理和事件发布功能。\n\n"); + sb.append("### ⚠️ 使用建议\n\n"); + sb.append("**大多数情况下,推荐使用 AgentTeamsSkill 中的智能记忆工具**:\n"); + sb.append("- `memory_store()`: 自动分类存储(系统自动判断存储类型和重要性)\n"); + sb.append("- `memory_recall()`: 智能检索(按相关性排序,自动合并重复内容)\n"); + sb.append("- `memory_stats()`: 查看统计信息\n\n"); + sb.append("**本工具集适用于以下场景**:\n"); + sb.append("- 需要精确控制记忆类型(短期/长期/知识)\n"); + sb.append("- 需要自定义 TTL 时间\n"); + sb.append("- 需要直接操作工作记忆字段\n"); + sb.append("- 需要发布自定义事件\n\n"); + sb.append("### 记忆存储工具(底层API)\n\n"); + sb.append("**分层记忆存储**(按 TTL 自动过期):\n"); + sb.append("- `memory_store_short()`: 短期记忆(1小时 TTL)[底层API]\n"); + sb.append(" - 用途:临时上下文、会话级信息\n"); + sb.append(" - 参数:key(必填), value(必填), ttl(可选,默认3600秒)\n"); + sb.append(" - 注意:推荐使用 `memory_store()` 自动分类\n\n"); + sb.append("- `memory_store_long()`: 长期记忆(7天 TTL)[底层API]\n"); + sb.append(" - 用途:任务结果、重要信息\n"); + sb.append(" - 参数:key(必填), value(必填), ttl(可选,默认604800秒)\n"); + sb.append(" - 注意:推荐使用 `memory_store()` 自动分类\n\n"); + sb.append("- `memory_store_knowledge()`: 知识记忆(永久保存)[底层API]\n"); + sb.append(" - 用途:架构决策、最佳实践、经验教训\n"); + sb.append(" - 参数:key(必填), value(必填)\n"); + sb.append(" - 注意:推荐使用 `memory_store()` 自动分类\n\n"); + sb.append("### 记忆检索工具(底层API)\n\n"); + sb.append("- `memory_retrieve()`: 根据键精确检索\n"); + sb.append(" - 自动从短期 → 长期 → 知识记忆中查找\n"); + sb.append(" - 参数:key(必填)\n"); + sb.append(" - 注意:推荐使用 `memory_recall()` 智能检索\n\n"); + sb.append("- `memory_search()`: 模糊搜索记忆\n"); + sb.append(" - 支持关键词匹配,返回相关记忆列表\n"); + sb.append(" - 参数:query(必填), limit(可选,默认10)\n"); + sb.append(" - 注意:推荐使用 `memory_recall()` 智能检索\n\n"); + sb.append("### 工作记忆工具\n\n"); + sb.append("- `working_memory_set()`: 设置工作记忆字段\n"); + sb.append(" - 用于存储当前任务状态、步骤等结构化数据\n"); + sb.append(" - 支持字段:taskDescription, status, step, currentAgent, 或自定义字段\n"); + sb.append(" - 参数:field(必填), value(必填)\n\n"); + sb.append("- `working_memory_get()`: 获取工作记忆\n"); + sb.append(" - 查看当前任务状态、步骤、摘要等\n"); + sb.append(" - 参数:taskId(可选,默认'main-agent')\n\n"); + sb.append("### 事件工具\n\n"); + sb.append("- `publish_event()`: 发布团队事件\n"); + sb.append(" - 通知其他代理任务状态变化\n"); + sb.append(" - 支持事件类型:TASK_CREATED, TASK_COMPLETED, TASK_FAILED, MESSAGE_RECEIVED 等\n"); + sb.append(" - 参数:eventType(必填), data(必填)\n\n"); + sb.append("### 使用示例\n\n"); + sb.append("**⚠️ 重要提示**:以下示例展示底层API的使用方法。\n"); + sb.append("如果只是普通存储,推荐使用 `memory_store()` 和 `memory_recall()` 智能工具。\n\n"); + sb.append("**存储记忆(精确控制类型)**:\n"); + sb.append("```\n"); + sb.append("# 存储临时上下文\n"); + sb.append("memory_store_short(\n"); + sb.append(" key=\"current-context\",\n"); + sb.append(" value=\"正在分析 UserService\",\n"); + sb.append(" ttl=1800 # 30分钟\n"); + sb.append(")\n\n"); + sb.append("# 存储任务结果\n"); + sb.append("memory_store_long(\n"); + sb.append(" key=\"task-result\",\n"); + sb.append(" value=\"已完成用户登录功能\"\n"); + sb.append(")\n\n"); + sb.append("# 存储架构决策\n"); + sb.append("memory_store_knowledge(\n"); + sb.append(" key=\"architecture\",\n"); + sb.append(" value=\"采用三层架构:Controller-Service-Repository\"\n"); + sb.append(")\n"); + sb.append("```\n\n"); + sb.append("**检索记忆**:\n"); + sb.append("```\n"); + sb.append("# 精确检索\n"); + sb.append("memory_retrieve(key=\"architecture\")\n\n"); + sb.append("# 模糊搜索\n"); + sb.append("memory_search(query=\"登录\", limit=5)\n"); + sb.append("```\n\n"); + sb.append("**工作记忆**:\n"); + sb.append("```\n"); + sb.append("# 设置当前状态\n"); + sb.append("working_memory_set(field=\"status\", value=\"in-progress\")\n"); + sb.append("working_memory_set(field=\"step\", value=\"3\")\n\n"); + sb.append("# 查看工作记忆\n"); + sb.append("working_memory_get()\n"); + sb.append("```\n"); + return sb.toString(); + } + + + /** + * 存储短期记忆 + */ + @ToolMapping(name = "memory_store_short", + description = "[底层API] 存储短期记忆(会话级别,TTL 1小时)。注意:推荐使用 memory_store() 自动分类。") + public String memoryStoreShort( + @Param(name = "key", description = "记忆键") String key, + @Param(name = "value", description = "记忆值") String value, + @Param(name = "ttl", description = "过期时间(秒),默认3600") Integer ttl) { + try { + int actualTtl = ttl != null && ttl > 0 ? ttl : 3600; + memoryManager.putShortTerm(key, value, actualTtl); + LOG.debug("存储短期记忆: key={}, ttl={}", key, actualTtl); + return "[OK] 短期记忆已存储: " + key; + } catch (Exception e) { + LOG.error("存储短期记忆失败", e); + return "[ERROR] 存储失败: " + e.getMessage(); + } + } + + /** + * 存储长期记忆 + */ + @ToolMapping(name = "memory_store_long", + description = "[底层API] 存储长期记忆(跨会话,TTL 7天)。注意:推荐使用 memory_store() 自动分类。") + public String memoryStoreLong( + @Param(name = "key", description = "记忆键") String key, + @Param(name = "value", description = "记忆值") String value, + @Param(name = "ttl", description = "过期时间(秒),默认604800") Integer ttl) { + try { + int actualTtl = ttl != null && ttl > 0 ? ttl : 604800; + memoryManager.putLongTerm(key, value, actualTtl); + LOG.debug("存储长期记忆: key={}, ttl={}", key, actualTtl); + return "[OK] 长期记忆已存储: " + key; + } catch (Exception e) { + LOG.error("存储长期记忆失败", e); + return "[ERROR] 存储失败: " + e.getMessage(); + } + } + + /** + * 存储知识记忆 + */ + @ToolMapping(name = "memory_store_knowledge", + description = "[底层API] 存储知识记忆(永久保存)。注意:推荐使用 memory_store() 自动分类。") + public String memoryStoreKnowledge( + @Param(name = "key", description = "记忆键") String key, + @Param(name = "value", description = "记忆值") String value) { + try { + memoryManager.putKnowledge(key, value); + LOG.debug("存储知识记忆: key={}", key); + return "[OK] 知识记忆已存储: " + key; + } catch (Exception e) { + LOG.error("存储知识记忆失败", e); + return "[ERROR] 存储失败: " + e.getMessage(); + } + } + + /** + * 检索记忆 + */ + @ToolMapping(name = "memory_retrieve", + description = "[底层API] 根据键精确检索记忆。注意:推荐使用 memory_recall() 智能检索。") + public String memoryRetrieve( + @Param(name = "key", description = "记忆键") String key) { + try { + // 按优先级查找:短期 -> 长期 -> 知识 + String value = memoryManager.get(key); + if (value != null) { + return "[OK] 记忆: " + value; + } + + return "[WARN] 未找到记忆: " + key; + } catch (Exception e) { + LOG.error("检索记忆失败", e); + return "[ERROR] 检索失败: " + e.getMessage(); + } + } + + /** + * 搜索记忆 + */ + @ToolMapping(name = "memory_search", + description = "[底层API] 模糊搜索记忆(支持关键词匹配)。注意:推荐使用 memory_recall() 智能检索。") + public String memorySearch( + @Param(name = "query", description = "搜索查询") String query, + @Param(name = "limit", description = "返回结果数量限制,默认10") Integer limit) { + try { + int actualLimit = limit != null && limit > 0 ? limit : 10; + List results = memoryManager.search(query, actualLimit); + + if (results.isEmpty()) { + return "[WARN] 未找到相关记忆"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("找到 ").append(results.size()).append(" 条相关记忆:\n\n"); + + for (int i = 0; i < results.size(); i++) { + Memory mem = results.get(i); + sb.append(i + 1).append(". "); + + if (mem instanceof ShortTermMemory) { + sb.append("[短期] "); + sb.append(((ShortTermMemory) mem).getContext()); + } else if (mem instanceof LongTermMemory) { + sb.append("[长期] "); + sb.append(((LongTermMemory) mem).getSummary()); + } else if (mem instanceof KnowledgeMemory) { + sb.append("[知识] "); + sb.append(((KnowledgeMemory) mem).getContent()); + } else { + sb.append("[其他] "); + sb.append(mem.getId()); + } + + sb.append("\n"); + } + + return sb.toString(); + } catch (Exception e) { + LOG.error("搜索记忆失败", e); + return "[ERROR] 搜索失败: " + e.getMessage(); + } + } + + + /** + * 设置工作记忆 + */ + @ToolMapping(name = "working_memory_set", + description = "[底层API] 设置工作记忆字段(用于存储当前任务状态、步骤等结构化数据)。直接操作 WorkingMemory。") + public String workingMemorySet( + @Param(name = "field", description = "字段名称(taskDescription/status/step/currentAgent)") String field, + @Param(name = "value", description = "字段值") String value) { + try { + // 使用默认的 taskId "main-agent" + String taskId = "main-agent"; + WorkingMemory workingMemory = memoryManager.getWorking(taskId); + + // 如果不存在,创建一个新的 + if (workingMemory == null) { + workingMemory = new WorkingMemory(taskId); + } + + switch (field.toLowerCase()) { + case "taskdescription": + case "currenttask": + workingMemory.setTaskDescription(value); + break; + case "status": + workingMemory.setStatus(value); + break; + case "step": + try { + workingMemory.setStep(Integer.parseInt(value)); + } catch (NumberFormatException e) { + return "[ERROR] 步骤必须是数字: " + value; + } + break; + case "currentagent": + workingMemory.setCurrentAgent(value); + break; + default: + // 存储到 data 字段中 + if (workingMemory.getData() == null) { + workingMemory.setData(new java.util.concurrent.ConcurrentHashMap<>()); + } + workingMemory.getData().put(field, value); + } + + // 保存更新后的工作记忆 + memoryManager.storeWorking(workingMemory); + + LOG.debug("设置工作记忆: {}={}", field, value); + return "[OK] 工作记忆已设置: " + field + " = " + value; + } catch (NumberFormatException e) { + return "[ERROR] 步骤必须是数字: " + value; + } catch (Exception e) { + LOG.error("设置工作记忆失败", e); + return "[ERROR] 设置失败: " + e.getMessage(); + } + } + + /** + * 获取工作记忆 + */ + @ToolMapping(name = "working_memory_get", + description = "[底层API] 获取工作记忆(查看当前任务状态、步骤等)。直接读取 WorkingMemory。") + public String workingMemoryGet( + @Param(name = "taskId", description = "任务ID,默认为'main-agent'") String taskId + ) { + try { + // 如果没有提供 taskId,使用默认值 + String actualTaskId = (taskId != null && !taskId.isEmpty()) ? taskId : "main-agent"; + + WorkingMemory workingMemory = memoryManager.getWorking(actualTaskId); + + if (workingMemory == null) { + return "[WARN] 未找到工作记忆: " + actualTaskId; + } + + StringBuilder sb = new StringBuilder(); + sb.append("## 工作记忆\n\n"); + + if (workingMemory.getTaskDescription() != null) { + sb.append("**当前任务**: ").append(workingMemory.getTaskDescription()).append("\n"); + } + if (workingMemory.getStatus() != null) { + sb.append("**状态**: ").append(workingMemory.getStatus()).append("\n"); + } + sb.append("**步骤**: ").append(workingMemory.getStep()).append("\n"); + if (workingMemory.getCurrentAgent() != null) { + sb.append("**当前代理**: ").append(workingMemory.getCurrentAgent()).append("\n"); + } + if (workingMemory.getSummary() != null) { + sb.append("**摘要**: ").append(workingMemory.getSummary()).append("\n"); + } + + // 其他自定义数据字段 + if (workingMemory.getData() != null && !workingMemory.getData().isEmpty()) { + sb.append("\n**其他数据**:\n"); + workingMemory.getData().forEach((key, value) -> { + sb.append(" - ").append(key).append(": ").append(value).append("\n"); + }); + } + + return sb.toString(); + } catch (Exception e) { + LOG.error("获取工作记忆失败", e); + return "[ERROR] 获取失败: " + e.getMessage(); + } + } + + + /** + * 发布事件 + */ + @ToolMapping(name = "publish_event", + description = "[底层API] 发布团队事件到 EventBus(用于通知其他代理任务状态变化)。需要了解事件类型。") + public String publishEvent( + @Param(name = "eventType", description = "事件类型(TASK_CREATED, TASK_COMPLETED, TASK_FAILED, MESSAGE_RECEIVED等)") String eventType, + @Param(name = "data", description = "事件数据(JSON格式或文本)") String data) { + try { + AgentEventType type = AgentEventType.valueOf(eventType.toUpperCase()); + AgentEvent event = new AgentEvent(type, data, null); + eventBus.publish(event); + + LOG.debug("发布事件: type={}, data={}", type, data); + return "[OK] 事件已发布: " + type; + } catch (IllegalArgumentException e) { + return "[ERROR] 无效的事件类型: " + eventType; + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/MainAgent.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/MainAgent.java new file mode 100644 index 0000000000000000000000000000000000000000..a1b945ac039ce56fd41225764d6cca4aef550aa3 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/MainAgent.java @@ -0,0 +1,1064 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import lombok.Getter; +import org.noear.solon.ai.agent.AgentChunk; +import org.noear.solon.ai.agent.AgentResponse; +import org.noear.solon.ai.agent.AgentSession; +import org.noear.solon.ai.agent.AgentSessionProvider; +import org.noear.solon.ai.agent.react.ReActAgent; +import org.noear.solon.ai.chat.ChatModel; +import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.CliSkillProvider; +import org.noear.solon.bot.core.PoolManager; +import org.noear.solon.bot.core.SystemPrompt; +import org.noear.solon.bot.core.event.AgentEvent; +import org.noear.solon.bot.core.event.AgentEventType; +import org.noear.solon.bot.core.event.EventBus; +import org.noear.solon.bot.core.event.EventHandler; +import org.noear.solon.bot.core.event.EventMetadata; +import org.noear.solon.bot.core.goalker.GoalKeeperIntegration; +import org.noear.solon.bot.core.memory.SharedMemoryManager; +import org.noear.solon.bot.core.memory.ShortTermMemory; +import org.noear.solon.bot.core.message.AgentMessage; +import org.noear.solon.bot.core.message.MessageAck; +import org.noear.solon.bot.core.message.MessageChannel; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; +import org.noear.solon.bot.core.subagent.SubagentManager; +import org.noear.solon.bot.core.subagent.TaskSkill; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 主代理(Team Lead)- Agent Teams 模式协调器 + * + * 负责任务: + * - 创建和管理共享任务列表 + * - 分配初始任务到共享任务池 + * - 协调多个子代理协作 + * - 监控任务执行状态 + * - 汇总最终结果 + * + * @author bai + * @since 3.9.5 + */ +@Getter +public class MainAgent { + private static final Logger LOG = LoggerFactory.getLogger(MainAgent.class); + + private final SubAgentMetadata config; + private final AgentSessionProvider sessionProvider; + private final SharedMemoryManager sharedMemoryManager; + private final EventBus eventBus; + private final MessageChannel messageChannel; + private final SharedTaskList taskList; + private final String workDir; + private final PoolManager poolManager; + + // 目标守护者 + private GoalKeeperIntegration goalKeeper; + + // 新增:用于访问 subagent 功能 + private final AgentKernel kernel; + private final SubagentManager subagentManager; + + private ReActAgent agent; + private AgentSession session; + private final AtomicBoolean running = new AtomicBoolean(false); + + // 任务事件监听器 + private String taskEventSubscriptionId; + private String taskFailedSubscriptionId; // 新增:保存失败事件订阅ID + private String messageHandlerId; + + // 性能优化:使用 CountDownLatch 替代轮询 + private volatile CountDownLatch taskCompletionLatch; + + // 超时配置(单位:毫秒) + private static final long EXECUTION_TIMEOUT_MS = 300_000; // 5分钟超时 + private static final long LLM_CALL_TIMEOUT_MS = 120_000; // LLM调用超时2分钟 + + public MainAgent(SubAgentMetadata config, + AgentSessionProvider sessionProvider, + SharedMemoryManager sharedMemoryManager, + EventBus eventBus, + MessageChannel messageChannel, + SharedTaskList taskList, + String workDir, + PoolManager poolManager) { + this(config, sessionProvider, sharedMemoryManager, eventBus, messageChannel, + taskList, workDir, poolManager, null, null); + } + + /** + * 完整构造函数(支持 subagent 功能) + */ + public MainAgent(SubAgentMetadata config, + AgentSessionProvider sessionProvider, + SharedMemoryManager sharedMemoryManager, + EventBus eventBus, + MessageChannel messageChannel, + SharedTaskList taskList, + String workDir, + PoolManager poolManager, + AgentKernel kernel, + SubagentManager subagentManager) { + this.config = config; + this.sessionProvider = sessionProvider; + this.sharedMemoryManager = sharedMemoryManager; + this.eventBus = eventBus; + this.messageChannel = messageChannel; + this.taskList = taskList; + this.workDir = workDir; + this.poolManager = poolManager; + this.kernel = kernel; + this.subagentManager = subagentManager; + + // 注册任务事件监听器 + registerTaskEventListeners(); + + // 注册消息处理器 + registerMessageHandler(); + } + + /** + * 初始化主代理 + */ + public synchronized void initialize(ChatModel chatModel) { + if (agent == null) { + ReActAgent.Builder builder = ReActAgent.of(chatModel); + + // 设置系统提示词 + builder.systemPrompt(SystemPrompt.builder() + .instruction(getSystemPrompt()) + .build()); + + // 添加技能 + CliSkillProvider skillProvider = new CliSkillProvider(workDir); + if (poolManager != null) { + poolManager.getPoolMap().forEach((alias, path) -> { + skillProvider.skillPool(alias, path); + }); + } + + // 基础技能 + builder.defaultSkillAdd(skillProvider.getTerminalSkill()); + builder.defaultSkillAdd(skillProvider.getExpertSkill()); + + // Agent Teams 工具集(记忆、事件、消息) + AgentTeamsTools teamsTools = new AgentTeamsTools( + sharedMemoryManager, + eventBus + ); + builder.defaultSkillAdd(teamsTools); + + // 子代理调用工具(如果有 kernel 和 subagentManager) + if (kernel != null && subagentManager != null) { + TaskSkill taskSkill = new TaskSkill(kernel, subagentManager); + builder.defaultSkillAdd(taskSkill); + LOG.debug("MainAgent: TaskSkill 已添加"); + } else { + LOG.debug("MainAgent: 无 kernel 或 subagentManager,跳过 TaskSkill"); + } + + // 设置较大的步数(主代理需要协调多个任务) + builder.maxSteps(50); + builder.sessionWindowSize(10); + + this.agent = builder.build(); + this.session = sessionProvider.getSession("main_agent"); + + LOG.info("MainAgent '{}' 初始化完成", config.getCode()); + } + } + + /** + * 流式执行主任务(实时输出) + * + * @param prompt 用户提示 + * @param __cwd 工作目录 + * @return 响应流 + */ + public Flux executeStream(Prompt prompt, String __cwd) throws Throwable { + if (agent == null) { + throw new IllegalStateException("MainAgent 尚未初始化"); + } + + running.set(true); + + // 0. 启动目标守护(防止在多轮循环中偏离目标) + String goalId = null; + try { + if (kernel != null) { + goalId = this.startGoalGuarding(prompt.getUserContent()); + LOG.info("目标守护已启动: goalId={}, 目标={}", goalId, prompt.getUserContent()); + } + } catch (Exception e) { + LOG.warn("启动目标守护失败(继续执行): {}", e.getMessage()); + } + + try { + // 1. 发布主代理任务开始事件 + publishEvent(AgentEventType.MAIN_TASK_STARTED, prompt.getUserContent(), null); + + // 2. 分析任务并创建子任务 + List subTasks = analyzeAndCreateTasks(prompt); + + // 3. 将任务添加到共享任务列表(带错误处理和验证) + if (!subTasks.isEmpty()) { + try { + // 打印依赖图 + LOG.debug("任务依赖关系图:\n{}", taskList.getDependencyGraph()); + + // 检测循环依赖 + List cyclicTasks = taskList.detectCyclicDependencies(); + if (!cyclicTasks.isEmpty()) { + LOG.error("检测到循环依赖,任务将被拒绝:"); + for (TeamTask task : cyclicTasks) { + LOG.error(" - {}", task.getTitle()); + } + throw new IllegalStateException("存在循环依赖,无法添加任务"); + } + + List added = taskList.addTasks(subTasks).join(); + LOG.info("主代理已添加 {} 个子任务到共享任务列表", added.size()); + + // 4. 广播任务可用通知 + broadcastTaskNotification(added); + + // 5. 记录可认领任务数 + List claimableTasks = taskList.getClaimableTasks(); + LOG.info("当前可认领任务数: {} / {}", claimableTasks.size(), added.size()); + + } catch (IllegalStateException e) { + LOG.error("添加任务失败: {}", e.getMessage()); + // 继续执行主代理自身任务 + } catch (Exception e) { + LOG.error("添加任务时发生异常: {}", e.getMessage(), e); + // 继续执行主代理自身任务 + } + } + + // 6. 执行主代理内部的协调逻辑(流式输出) + reactor.core.publisher.Flux responseStream = agent.prompt(prompt) + .session(session) + .options(o -> { + // 传递工作目录给工具(ls、bash 等需要) + if (__cwd != null && !__cwd.isEmpty()) { + o.toolContextPut("__cwd", __cwd); + } + }) + .stream(); + + // 7. 在流完成后等待所有子任务完成 + reactor.core.publisher.Flux resultStream = responseStream + .doOnComplete(() -> { + try { + // 等待所有子任务完成 + waitForAllTasksCompleted(); + + // 汇总结果 + String summary = summarizeResults(); + + // 发布主代理任务完成事件 + publishEvent(AgentEventType.MAIN_TASK_COMPLETED, summary, null); + + LOG.info("MainAgent 流式执行完成"); + + } catch (Exception e) { + LOG.error("MainAgent 后处理失败", e); + } finally { + // 停止目标守护 + try { + this.stopGoalGuarding(); + LOG.info("目标守护已停止"); + } catch (Exception e) { + LOG.warn("停止目标守护失败: {}", e.getMessage()); + } + } + }) + .doOnError(error -> { + LOG.error("MainAgent 流式执行出错", error); + running.set(false); + // 出错时也要停止目标守护 + try { + this.stopGoalGuarding(); + LOG.info("目标守护已停止(错误)"); + } catch (Exception e) { + LOG.warn("停止目标守护失败: {}", e.getMessage()); + } + }) + .doOnCancel(() -> { + LOG.warn("MainAgent 流式执行被取消"); + running.set(false); + // 取消时也要停止目标守护 + try { + this.stopGoalGuarding(); + LOG.info("目标守护已停止(取消)"); + } catch (Exception e) { + LOG.warn("停止目标守护失败: {}", e.getMessage()); + } + }); + + return resultStream; + + } finally { + running.set(false); + // 确保目标守护被停止(即使发生异常) + try { + this.stopGoalGuarding(); + LOG.info("目标守护已停止(finally块)"); + } catch (Exception e) { + LOG.warn("停止目标守护失败(finally块): {}", e.getMessage()); + } + } + } + + /** + * 分析任务并创建子任务 + */ + private List analyzeAndCreateTasks(Prompt prompt) { + List tasks = new ArrayList<>(); + String userPrompt = prompt.getUserContent().toLowerCase(); + + // 从共享内存中读取相关信息(如果有历史任务结果) + StringBuilder contextBuilder = new StringBuilder(); + if (sharedMemoryManager != null) { + try { + // 查询相关的历史任务结果 + List recentMemories = + sharedMemoryManager.search("task-result", 10); + + if (!recentMemories.isEmpty()) { + contextBuilder.append("\n\n# 历史任务上下文\n\n"); + for (org.noear.solon.bot.core.memory.Memory memory : recentMemories) { + if (memory instanceof ShortTermMemory) { + ShortTermMemory stm = (ShortTermMemory) memory; + String taskTitle = (String) stm.getMetadata("taskTitle"); + contextBuilder.append(String.format("- **%s**: %s\n", + taskTitle != null ? taskTitle : "Unknown", + stm.getContext())); + } + } + LOG.debug("从共享内存加载了 {} 条历史任务记录", recentMemories.size()); + } + } catch (Exception e) { + LOG.warn("从共享内存读取历史任务失败", e); + } + } + + // 根据提示内容智能创建任务 + // 这里是简化版本,实际可以使用 LLM 来分析任务 + // 可以将 contextBuilder 中的内容添加到任务的描述中 + + if (userPrompt.contains("探索") || userPrompt.contains("explore") || userPrompt.contains("分析")) { + // 创建探索任务链:探索 -> 搜索 + String exploreId = "task-explore-" + System.currentTimeMillis(); + String searchId = "task-search-" + System.currentTimeMillis(); + + TeamTask exploreTask = TeamTask.builder() + .id(exploreId) + .title("探索代码库") + .description("探索和分析代码库结构") + .type(TeamTask.TaskType.EXPLORATION) + .priority(8) + .build(); + + TeamTask searchTask = TeamTask.builder() + .id(searchId) + .title("搜索关键文件") + .description("搜索与任务相关的关键文件") + .type(TeamTask.TaskType.ANALYSIS) + .priority(7) + .dependencies(Collections.singletonList(exploreId)) + .build(); + + tasks.add(exploreTask); + tasks.add(searchTask); + } + + if (userPrompt.contains("实现") || userPrompt.contains("开发") || userPrompt.contains("implement")) { + // 创建开发任务链:计划 -> 实现 -> 测试 + String planId = "task-plan-" + System.currentTimeMillis(); + String implId = "task-impl-" + System.currentTimeMillis(); + String testId = "task-test-" + System.currentTimeMillis(); + + TeamTask planTask = TeamTask.builder() + .id(planId) + .title("制定实现计划") + .description("制定详细的实现计划") + .type(TeamTask.TaskType.DEVELOPMENT) + .priority(9) + .build(); + + TeamTask implTask = TeamTask.builder() + .id(implId) + .title("实现功能") + .description("根据计划实现功能") + .type(TeamTask.TaskType.DEVELOPMENT) + .priority(8) + .dependencies(Collections.singletonList(planId)) + .build(); + + TeamTask testTask = TeamTask.builder() + .id(testId) + .title("编写测试") + .description("为实现的代码编写测试") + .type(TeamTask.TaskType.TESTING) + .priority(6) + .dependencies(Collections.singletonList(implId)) + .build(); + + tasks.add(planTask); + tasks.add(implTask); + tasks.add(testTask); + } + + if (userPrompt.contains("文档") || userPrompt.contains("document")) { + // 创建文档任务 + tasks.add(TeamTask.builder() + .id("task-doc-" + System.currentTimeMillis()) + .title("生成文档") + .description("生成项目文档") + .type(TeamTask.TaskType.DOCUMENTATION) + .priority(5) + .build()); + } + + // 默认任务(如果没有匹配到特定类型) + if (tasks.isEmpty()) { + tasks.add(TeamTask.builder() + .id("task-default-" + System.currentTimeMillis()) + .title("处理用户请求") + .description(prompt.getUserContent()) + .type(TeamTask.TaskType.DEVELOPMENT) + .priority(7) + .build()); + } + + // 验证任务依赖关系 + logTaskDependencies(tasks); + + return tasks; + } + + /** + * 记录任务依赖关系(用于调试) + */ + private void logTaskDependencies(List tasks) { + if (LOG.isDebugEnabled()) { + LOG.debug("创建任务依赖关系:"); + for (TeamTask task : tasks) { + if (task.getDependencies() != null && !task.getDependencies().isEmpty()) { + LOG.debug(" {} 依赖: {}", task.getTitle(), task.getDependencies()); + } + } + } + } + + /** + * 广播任务可用通知 + */ + private void broadcastTaskNotification(List tasks) { + if (messageChannel != null) { + Map payload = new HashMap<>(); + payload.put("action", "tasks_available"); + payload.put("count", tasks.size()); + payload.put("tasks", tasks); + + AgentMessage> message = AgentMessage.>of(payload) + .from(config.getCode()) + .to("*") + .type("task_notification") + .build(); + + try { + List acks = messageChannel.broadcast(message).join(); + LOG.info("任务通知已广播,收到 {} 个确认", acks.size()); + } catch (Exception e) { + LOG.warn("广播任务通知失败", e); + } + } + } + + /** + * 等待所有任务完成(使用事件驱动机制,性能优化) + */ + private void waitForAllTasksCompleted() { + // 初始化 CountDownLatch(初始值为1,防止立即返回) + taskCompletionLatch = new CountDownLatch(1); + + try { + // 检查初始状态 + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + if (stats.inProgressTasks == 0 && stats.pendingTasks == 0) { + LOG.info("所有子任务已完成(总计: {}, 完成: {}, 失败: {})", + stats.totalTasks, stats.completedTasks, stats.failedTasks); + return; + } + + LOG.info("等待任务完成... (进行中: {}, 待认领: {})", + stats.inProgressTasks, stats.pendingTasks); + + // 每30秒打印一次统计信息(使用超时等待,避免阻塞太久) + long remainingWaitMs = 300_000; // 最多等待5分钟 + long printIntervalMs = 30_000; // 每30秒打印一次 + + while (remainingWaitMs > 0) { + long waitTime = Math.min(printIntervalMs, remainingWaitMs); + + // 等待任务完成或超时 + boolean completed = taskCompletionLatch.await(waitTime, TimeUnit.MILLISECONDS); + + stats = taskList.getStatistics(); + + if (stats.inProgressTasks == 0 && stats.pendingTasks == 0) { + LOG.info("所有子任务已完成(总计: {}, 完成: {}, 失败: {})", + stats.totalTasks, stats.completedTasks, stats.failedTasks); + break; + } + + if (!completed) { + // 超时,打印进度 + LOG.info("任务进度: {}", stats); + + // 打印阻塞信息 + if (stats.pendingTasks > 0) { + String blockingInfo = taskList.getBlockingInfo(); + if (blockingInfo.length() > 50) { + LOG.debug("阻塞任务:\n{}", blockingInfo); + } + } + } + + remainingWaitMs -= waitTime; + } + + if (remainingWaitMs <= 0) { + LOG.warn("等待任务完成超时,当前状态: {}", taskList.getStatistics()); + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("等待任务完成被中断"); + } finally { + taskCompletionLatch = null; + } + } + + /** + * 汇总结果 + */ + private String summarizeResults() { + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + List allTasks = taskList.getAllTasks(); + + StringBuilder summary = new StringBuilder(); + summary.append("## 任务执行汇总\n\n"); + summary.append(stats.toString()).append("\n\n"); + + // 列出完成的任务 + summary.append("### 已完成任务\n\n"); + for (TeamTask task : allTasks) { + if (task.isCompleted()) { + summary.append(String.format("- **%s**: %s\n", + task.getTitle(), + task.getResult() != null ? task.getResult().toString() : "完成")); + } + } + + // 列出失败的任务 + summary.append("\n### 失败任务\n\n"); + for (TeamTask task : allTasks) { + if (task.isFailed()) { + summary.append(String.format("- **%s**: %s\n", + task.getTitle(), + task.getErrorMessage())); + } + } + + return summary.toString(); + } + + /** + * 注册任务事件监听器 + * 性能优化:添加事件驱动机制,在所有任务完成时触发 CountDownLatch + */ + private void registerTaskEventListeners() { + if (eventBus != null) { + // 监听子任务完成事件 + taskEventSubscriptionId = eventBus.subscribe(AgentEventType.TASK_COMPLETED, event -> { + String taskId = event.getMetadata().getTaskId(); + Map payload = (Map) event.getPayload(); + + String taskTitle = payload != null ? (String) payload.get("taskTitle") : "Unknown"; + String agentId = payload != null ? (String) payload.get("agentId") : "Unknown"; + + LOG.info("主代理收到子任务完成事件: {} by {}", taskTitle, agentId); + + // 将任务结果存储到共享内存(供后续任务使用) + if (sharedMemoryManager != null && payload != null) { + try { + Object result = payload.get("result"); + if (result != null) { + // 创建短期记忆存储任务结果 + ShortTermMemory memory = new ShortTermMemory( + agentId, + result.toString(), + taskId + ); + memory.setId("task-result-" + taskId); + + // 添加元数据 + memory.putMetadata("taskTitle", taskTitle); + memory.putMetadata("taskType", "task-result"); + memory.putMetadata("source", agentId); + + sharedMemoryManager.store(memory); + LOG.debug("任务结果已存储到共享内存: taskId={}, source={}", taskId, agentId); + } + } catch (Exception e) { + LOG.warn("存储任务结果到共享内存失败", e); + } + } + + // 检查是否有依赖此任务的其他任务可以开始执行 + checkAndNotifyDependentTasks(taskId); + + // 性能优化:检查是否所有任务都已完成,触发 CountDownLatch + checkAllTasksCompleted(); + + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + // 监听子任务失败事件 + taskFailedSubscriptionId = eventBus.subscribe(AgentEventType.TASK_FAILED, event -> { + String taskId = event.getMetadata().getTaskId(); + Map payload = (Map) event.getPayload(); + + String taskTitle = payload != null ? (String) payload.get("taskTitle") : "Unknown"; + String agentId = payload != null ? (String) payload.get("agentId") : "Unknown"; + + // 获取错误信息(从任务对象或payload) + String errorMessage = null; + if (payload != null && payload.containsKey("errorMessage")) { + errorMessage = (String) payload.get("errorMessage"); + } else { + // 从任务列表中获取错误信息 + TeamTask failedTask = taskList.getTask(taskId); + if (failedTask != null) { + errorMessage = failedTask.getErrorMessage(); + } + } + + if (errorMessage == null) { + errorMessage = "Unknown error"; + } + + LOG.warn("主代理收到子任务失败事件: {} by {} - {}", taskTitle, agentId, errorMessage); + + // 处理任务失败:决定是否需要重试或标记依赖任务失败 + handleTaskFailure(taskId, errorMessage); + + // 性能优化:检查是否所有任务都已完成(包括失败的),触发 CountDownLatch + checkAllTasksCompleted(); + + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + LOG.info("MainAgent 任务事件监听器已注册(TASK_COMPLETED, TASK_FAILED)"); + } + } + + /** + * 检查是否所有任务都已完成 + * 性能优化:如果所有任务完成,触发 CountDownLatch + */ + private void checkAllTasksCompleted() { + if (taskCompletionLatch != null) { + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + if (stats.inProgressTasks == 0 && stats.pendingTasks == 0) { + // 所有任务都已完成或失败,触发 CountDownLatch + taskCompletionLatch.countDown(); + LOG.debug("所有任务完成,触发 CountDownLatch"); + } + } + } + + /** + * 检查并通知依赖此任务的其他任务 + * 修复:实际广播任务可用通知,而不仅仅是记录日志 + */ + private void checkAndNotifyDependentTasks(String completedTaskId) { + try { + // 查找依赖此任务的所有待认领任务 + List claimableTasks = taskList.getClaimableTasks(); + + if (!claimableTasks.isEmpty()) { + LOG.info("发现 {} 个可认领的任务(由于任务 {} 完成),广播通知...", + claimableTasks.size(), completedTaskId); + + // 修复:通过 MessageChannel 广播任务可用通知 + broadcastTaskNotification(claimableTasks); + } + } catch (Exception e) { + LOG.error("检查依赖任务失败", e); + } + } + + /** + * 处理任务失败 + */ + private void handleTaskFailure(String taskId, String errorMessage) { + try { + // 查找依赖此失败任务的其他任务 + List allTasks = taskList.getAllTasks(); + List dependentTasks = new ArrayList<>(); + + for (TeamTask task : allTasks) { + if (task.getStatus() == TeamTask.Status.PENDING && + task.getDependencies().contains(taskId)) { + dependentTasks.add(task); + } + } + + if (!dependentTasks.isEmpty()) { + LOG.warn("发现 {} 个任务受影响(依赖失败任务 {})", dependentTasks.size(), taskId); + + // 策略1:标记依赖任务为失败 + // 策略2:等待人工干预 + // 当前:记录日志,保持待认领状态(由人工决策) + } + } catch (Exception e) { + LOG.error("处理任务失败时发生异常", e); + } + } + + /** + * 注册消息处理器 + */ + private void registerMessageHandler() { + if (messageChannel != null) { + messageHandlerId = messageChannel.registerHandler( + config.getCode(), + this::handleMessage + ); + LOG.info("MainAgent 消息处理器已注册"); + } + } + + /** + * 处理子代理发送的消息 + */ + private CompletableFuture handleMessage(AgentMessage message) { + LOG.debug("MainAgent 收到消息: from={}, type={}", + message.getFrom(), message.getType()); + + // 处理任务相关消息 + if ("task_query".equals(message.getType())) { + // 子代理查询任务列表 + return CompletableFuture.completedFuture(getTaskListInfo()); + } else if ("task_help".equals(message.getType())) { + // 子代理请求帮助 + return CompletableFuture.completedFuture(handleHelpRequest(message)); + } + + return CompletableFuture.completedFuture("ACK"); + } + + /** + * 获取任务列表信息 + */ + private Map getTaskListInfo() { + Map info = new HashMap<>(); + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + + info.put("statistics", stats); + info.put("pendingTasks", taskList.getPendingTasks().size()); + info.put("claimableTasks", taskList.getClaimableTasks().size()); + + return info; + } + + /** + * 处理帮助请求 + */ + private Object handleHelpRequest(AgentMessage message) { + LOG.info("收到来自 {} 的帮助请求", message.getFrom()); + + // 这里可以实现协调逻辑,例如: + // 1. 重新分配任务 + // 2. 提供额外的资源 + // 3. 协调多个代理协作 + + return "Help request received"; + } + + /** + * 发布事件 + */ + private void publishEvent(AgentEventType eventType, Object payload, String taskId) { + if (eventBus != null) { + EventMetadata metadata = EventMetadata.builder() + .sourceAgent(config.getCode()) + .taskId(taskId) + .priority(5) + .build(); + + AgentEvent event = new AgentEvent(eventType, payload, metadata); + eventBus.publishAsync(event); + } + } + + /** + * 获取系统提示词 + */ + private String getSystemPrompt() { + return "## 主代理(Team Lead)- 强制执行模式\n\n" + + "你是 Agent Teams 的团队领导,负责协调多个子代理协作完成任务。\n" + + "\n" + + "### ⚠️ 核心规则(违反即失败)\n" + + "\n" + + "#### 🚫 禁止行为(绝对不可违反)\n" + + "1. **禁止模拟工作**:\n" + + " - 严禁使用 `update_working_memory`、`memory_store` 等工具声称工作已完成\n" + + " - 不断更新 step、currentAgent 字段而不实际工作是**严重违规**\n" + + " - 不得在记忆中存储虚假的\"已完成\"状态\n\n" + + "2. **禁止虚假产出**:\n" + + " - 不得声称\"需求分析已完成\"、\"代码已编写\"等虚假结论\n" + + " - 没有实际文件产出前,不得宣称任务完成\n" + + " - 记忆存储只能存储真实已完成的工作结果\n\n" + + "3. **禁止循环操作**:\n" + + " - 不得重复调用相同的工具而不产生新进展\n" + + " - 不得无限更新状态而无实际工作\n" + + " - 检测到循环时必须立即停止并改变策略\n\n" + + "#### ✅ 必须行为(必须执行)\n" + + "1. **必须使用 subagent 工具**:\n" + + " - 所有实际工作必须通过 `subagent(type, prompt)` 工具委派给专门的子代理\n" + + " - 可用的子代理类型:explore、plan、bash、general-purpose、solon-code-guide\n" + + " - 例如:`subagent(type='bash', prompt='创建项目目录并初始化')`\n\n" + + "2. **必须有实际产出**:\n" + + " - **代码任务**必须生成 `.java`、`.py` 等代码文件\n" + + " - **文档任务**必须生成 `.md`、`.txt` 等文档文件\n" + + " - **测试任务**必须有测试报告或测试结果文件\n" + + " - **架构任务**必须有架构图或设计文档\n\n" + + "3. **必须验证产出**:\n" + + " - 使用 `read` 或 `ls` 工具验证文件是否真实创建\n" + + " - 确认文件内容符合要求后才可宣称任务完成\n\n" + + "\n" + + "### 工作流程(强制执行)\n" + + "\n" + + "#### 步骤 1:任务分析(使用 subagent)\n" + + "```\n" + + "subagent(\n" + + " type='plan',\n" + + " prompt='分析任务需求:[用户任务],提供详细的实现方案'\n" + + ")\n" + + "```\n" + + "\n" + + "#### 步骤 2:执行工作(使用 subagent)\n" + + "```\n" + + "# 开发任务\n" + + "subagent(\n" + + " type='bash',\n" + + " prompt='创建文件 [文件名],编写代码实现:[具体需求]'\n" + + ")\n" + + "\n" + + "# 测试任务\n" + + "subagent(\n" + + " type='bash',\n" + + " prompt='编写测试用例并运行测试,生成测试报告'\n" + + ")\n" + + "```\n" + + "\n" + + "#### 步骤 3:验证产出(使用 ls/read)\n" + + "```\n" + + "ls(path='.') # 列出文件\n" + + "read(file_path='xxx.java') # 验证文件内容\n" + + "```\n" + + "\n" + + "#### 步骤 4:总结结果(仅在真实完成后)\n" + + "```\n" + + "# 只有在确认文件真实创建后才可总结\n" + + "Final Answer: [ANSWER]\n" + + "已完成以下工作:\n" + + "1. 创建文件:file1.java, file2.py\n" + + "2. 文件内容:[简要描述]\n" + + "3. 验证结果:所有文件已通过测试\n" + + "```\n" + + "\n" + + "### ⚠️ 常见错误(必须避免)\n" + + "\n" + + "❌ **错误示例**:\n" + + "```\n" + + "# 错误1:虚假更新状态\n" + + "update_working_memory(field='step', value='1')\n" + + "update_working_memory(field='step', value='2')\n" + + "memory_store(content='需求分析已完成') # 虚假!\n" + + "\n" + + "# 错误2:声称完成但无产出\n" + + "Final Answer: [ANSWER]\n" + + "团队协作完成! # 但没有创建任何文件\n" + + "```\n" + + "\n" + + "✅ **正确示例**:\n" + + "```\n" + + "# 正确:实际调用子代理\n" + + "subagent(type='bash', prompt='创建 UserController.java')\n" + + "# 等待结果...\n" + + "ls(path='src/main/java') # 验证文件已创建\n" + + "read(file_path='src/main/java/UserController.java') # 验证内容\n" + + "Final Answer: [ANSWER]\n" + + "已创建 UserController.java,包含用户增删改查功能\n" + + "```\n" + + "\n" + + "### 🎯 成功标准\n" + + "\n" + + "任务被认为完成,当且仅当:\n" + + "1. **有实际文件产出**:代码、文档、测试报告等\n" + + "2. **文件内容已验证**:使用 read 工具确认内容正确\n" + + "3. **通过必要测试**:代码可编译、可运行\n" + + "4. **无虚假声明**:所有声称的完成都是真实的\n" + + "\n" + + "### 📊 token 使用警告\n" + + "\n" + + "- 每个任务建议不超过 10,000 tokens\n" + + "- 超过 5,000 tokens 时必须检查是否有实际产出\n" + + "- 超过 10,000 tokens 无产出时立即终止任务\n" + + "- 禁止循环调用工具而不产生进展\n" + + "\n" + + "### 🚀 立即开始\n" + + "\n" + + "接到任务后,必须:\n" + + "1. 先使用 `subagent(type='plan', ...)` 分析需求\n" + + "2. 再使用 `subagent(type='bash', ...)` 执行工作\n" + + "3. 使用 `ls`、`read` 验证产出\n" + + "4. 确认真实完成后才给出 Final Answer\n" + + "\n" + + "**记住:禁止模拟,必须实际产出!**\n"; + } + + /** + * 检查是否正在运行 + */ + public boolean isRunning() { + return running.get(); + } + + + /** + * 清理资源 + */ + public void destroy() { + // 注销事件监听器 + if (eventBus != null) { + if (taskEventSubscriptionId != null) { + eventBus.unsubscribe(taskEventSubscriptionId); + LOG.info("MainAgent TASK_COMPLETED 事件监听器已注销"); + } + if (taskFailedSubscriptionId != null) { + eventBus.unsubscribe(taskFailedSubscriptionId); + LOG.info("MainAgent TASK_FAILED 事件监听器已注销"); + } + } + + // 注销消息处理器 + if (messageChannel != null && messageHandlerId != null) { + messageChannel.unregisterHandler(config.getCode(), messageHandlerId); + LOG.info("MainAgent 消息处理器已注销"); + } + } + + /** + * 启动目标守护 + * + * @param userPrompt 用户的目标提示词 + * @return 目标 ID + */ + public String startGoalGuarding(String userPrompt) { + if (goalKeeper == null) { + goalKeeper = new GoalKeeperIntegration(this); + } + return goalKeeper.startGoalGuarding(userPrompt); + } + + /** + * 停止目标守护 + */ + public void stopGoalGuarding() { + if (goalKeeper != null) { + goalKeeper.stopGoalGuarding(); + } + } + + /** + * 获取当前目标 + * + * @return 当前目标描述 + */ + public String getCurrentGoal() { + return goalKeeper != null ? goalKeeper.getCurrentGoal() : null; + } + + /** + * 获取当前目标 ID + * + * @return 目标 ID + */ + public String getCurrentGoalId() { + return goalKeeper != null ? goalKeeper.getCurrentGoalId() : null; + } + + /** + * 检查是否正在守护 + * + * @return 是否正在守护 + */ + public boolean isGuardingGoal() { + return goalKeeper != null && goalKeeper.isGuarding(); + } + + /** + * 获取目标提醒次数 + * + * @return 提醒次数 + */ + public int getGoalReminderCount() { + return goalKeeper != null ? goalKeeper.getReminderCount() : 0; + } + + /** + * 为 Prompt 添加目标上下文 + * + * @param prompt 原始提示词 + * @return 带目标的提示词 + */ + public Prompt enrichPromptWithGoal(Prompt prompt) { + if (goalKeeper != null) { + return goalKeeper.enrichPromptWithGoal(prompt); + } + return prompt; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/SharedTaskList.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/SharedTaskList.java new file mode 100644 index 0000000000000000000000000000000000000000..5543a85995a47effef583fa6618e3c282e82ec70 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/SharedTaskList.java @@ -0,0 +1,1014 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.noear.solon.bot.core.event.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +/** + * 共享任务列表(Shared Task List) + * + * Agent Teams 模式中的共享任务池,支持: + * - 任务添加和删除 + * - 任务认领和释放 + * - 优先级队列 + * - Agent 负载跟踪 + * - 任务生命周期事件 + * + * @author bai + * @since 3.9.5 + */ +public class SharedTaskList { + private static final Logger LOG = LoggerFactory.getLogger(SharedTaskList.class); + + private final Map tasks; // 所有任务 (taskId -> Task) + private final Map pendingTasks; // 待认领任务 + private final Map> agentTasks; // Agent 的任务集合 (agentId -> Set) + private final Map agentLoad; // Agent 负载计数 + + // 性能优化:使用细粒度锁替代全局读写锁 + private final Map taskLocks; // 每个任务一个锁 + + // 保留一个轻量级的全局锁用于保护关键区域的原子操作(如批量添加任务) + private final ReentrantLock globalLock; + + private final EventBus eventBus; // 事件总线 + private final int maxCompletedTasks; // 保留的最大已完成任务数 + private final Queue completedTaskQueue; // 已完成任务队列(FIFO清理) + + // 性能优化:专用线程池 + private final ExecutorService taskExecutor; // 任务操作线程池 + private final ExecutorService eventExecutor; // 事件发布线程池 + + // 性能优化:最大依赖深度限制 + private static final int MAX_DEPENDENCY_DEPTH = 100; + + /** + * 构造函数 + * + * @param eventBus 事件总线 + */ + public SharedTaskList(EventBus eventBus) { + this(eventBus, 100); + } + + /** + * 完整构造函数 + * + * @param eventBus 事件总线 + * @param maxCompletedTasks 最大保留已完成任务数 + */ + public SharedTaskList(EventBus eventBus, int maxCompletedTasks) { + this.tasks = new ConcurrentHashMap<>(); + this.pendingTasks = new ConcurrentHashMap<>(); + this.agentTasks = new ConcurrentHashMap<>(); + this.agentLoad = new ConcurrentHashMap<>(); + this.taskLocks = new ConcurrentHashMap<>(); + this.globalLock = new ReentrantLock(); + this.eventBus = eventBus; + this.maxCompletedTasks = maxCompletedTasks; + this.completedTaskQueue = new LinkedList<>(); + + // 创建专用线程池 + int poolSize = Math.max(10, Runtime.getRuntime().availableProcessors() * 2); + this.taskExecutor = Executors.newFixedThreadPool(poolSize, new NamedThreadFactory("SharedTaskList")); + this.eventExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("SharedTaskList-Event")); + + LOG.info("SharedTaskList 初始化完成 (线程池大小: {}, 使用细粒度锁)", poolSize); + } + + /** + * 关闭资源 + */ + public void shutdown() { + LOG.info("SharedTaskList 正在关闭..."); + + // 关闭线程池 + taskExecutor.shutdown(); + eventExecutor.shutdown(); + + try { + if (!taskExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { + taskExecutor.shutdownNow(); + } + if (!eventExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { + eventExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + taskExecutor.shutdownNow(); + eventExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + + LOG.info("SharedTaskList 已关闭"); + } + + + /** + * 添加任务 + * 性能优化:使用细粒度锁,只锁定必要的代码段 + * + * @param task 任务 + * @return 异步结果 + */ + public CompletableFuture addTask(TeamTask task) { + return CompletableFuture.supplyAsync(() -> { + // 获取任务级别的锁(按字母顺序排序以避免死锁) + List lockIds = new ArrayList<>(task.getDependencies()); + lockIds.add(task.getId()); + lockIds.sort(String::compareTo); + + // 获取所有相关的锁 + List locks = new ArrayList<>(); + for (String id : lockIds) { + ReentrantLock lock = taskLocks.computeIfAbsent(id, k -> new ReentrantLock()); + locks.add(lock); + } + + // 按顺序获取所有锁(避免死锁),并记录获取数量 + int acquiredLocks = 0; + try { + for (ReentrantLock lock : locks) { + lock.lock(); + acquiredLocks++; // 记录成功获取的锁数量 + } + + // 验证依赖任务存在 + for (String depId : task.getDependencies()) { + if (!tasks.containsKey(depId)) { + throw new IllegalArgumentException("依赖任务不存在: " + depId); + } + } + + // 检测循环依赖(带深度限制) + if (hasCyclicDependency(task, tasks::get, MAX_DEPENDENCY_DEPTH)) { + throw new IllegalArgumentException("检测到循环依赖或依赖深度超过限制: " + task.getTitle()); + } + + // 添加任务 + tasks.put(task.getId(), task); + + // 如果状态是 PENDING,加入待认领队列 + if (task.isClaimable()) { + pendingTasks.put(task.getId(), task); + } + + LOG.debug("任务已添加: {} (优先级: {})", task.getTitle(), task.getPriority()); + + // 异步发布事件(不阻塞返回) + final TeamTask finalTask = task; + CompletableFuture.runAsync(() -> { + publishTaskEvent(AgentEventType.TASK_CREATED, finalTask, null); + }, eventExecutor); + + return task; + + } finally { + // 按相反顺序释放已获取的锁(修复:使用acquiredLocks而不是locks.size()) + for (int i = acquiredLocks - 1; i >= 0; i--) { + locks.get(i).unlock(); + } + } + }, taskExecutor); + } + + /** + * 检测循环依赖(带深度限制) + */ + private boolean hasCyclicDependency(TeamTask task, java.util.function.Function taskLookup, int maxDepth) { + try { + return task.hasCyclicDependency(taskLookup); + } catch (StackOverflowError e) { + LOG.warn("任务依赖深度过大,可能存在循环依赖: {} (深度限制: {})", task.getTitle(), maxDepth); + return true; + } + } + + /** + * 批量添加任务(原子操作,支持任务间依赖) + * + * @param tasks 任务列表 + * @return 异步结果 + */ + public CompletableFuture> addTasks(List tasks) { + return CompletableFuture.supplyAsync(() -> { + globalLock.lock(); + try { + // 第一阶段:收集所有任务ID并验证唯一性 + Set batchTaskIds = new HashSet<>(); + for (TeamTask task : tasks) { + // 检查任务ID唯一性(包括批次内) + if (this.tasks.containsKey(task.getId()) || batchTaskIds.contains(task.getId())) { + throw new IllegalArgumentException("任务ID已存在: " + task.getId()); + } + batchTaskIds.add(task.getId()); + } + + // 第二阶段:验证依赖关系(修复:使用batchTaskIds避免时序问题) + for (TeamTask task : tasks) { + for (String depId : task.getDependencies()) { + // 检查依赖是否在当前批次中 + boolean inBatch = batchTaskIds.contains(depId); + // 检查依赖是否已存在 + boolean exists = this.tasks.containsKey(depId); + + if (!inBatch && !exists) { + throw new IllegalArgumentException("依赖任务不存在: " + depId + + " (被任务 " + task.getTitle() + " 依赖)"); + } + } + } + + // 第三阶段:添加所有任务 + List added = new ArrayList<>(); + for (TeamTask task : tasks) { + // 添加到任务列表 + this.tasks.put(task.getId(), task); + + // 如果状态是 PENDING,加入待认领队列 + if (task.isClaimable()) { + pendingTasks.put(task.getId(), task); + } + + added.add(task); + + LOG.debug("任务已添加: {} (优先级: {})", task.getTitle(), task.getPriority()); + } + + // 第四阶段:检测循环依赖(所有任务添加后) + for (TeamTask task : tasks) { + if (task.hasCyclicDependency(this.tasks::get)) { + // 回滚:删除所有已添加的任务 + for (TeamTask addedTask : added) { + this.tasks.remove(addedTask.getId()); + pendingTasks.remove(addedTask.getId()); + } + throw new IllegalArgumentException("检测到循环依赖: " + task.getTitle()); + } + } + + // 第五阶段:触发事件(释放锁后触发) + List finalAdded = added; + CompletableFuture.runAsync(() -> { + for (TeamTask task : finalAdded) { + publishTaskEvent(AgentEventType.TASK_CREATED, task, null); + } + }, eventExecutor); + + return added; + + } finally { + globalLock.unlock(); + } + }, taskExecutor); + } + + /** + * 获取任务 + * + * @param taskId 任务ID + * @return 任务,不存在返回 null + */ + public TeamTask getTask(String taskId) { + // ConcurrentHashMap 支持并发读,无需加锁 + return tasks.get(taskId); + } + + /** + * 删除任务 + * + * @param taskId 任务ID + * @return 是否删除成功 + */ + public boolean removeTask(String taskId) { + // 使用细粒度锁 + ReentrantLock lock = taskLocks.computeIfAbsent(taskId, k -> new ReentrantLock()); + lock.lock(); + try { + TeamTask task = tasks.remove(taskId); + if (task != null) { + pendingTasks.remove(taskId); + + // 从 Agent 的任务集合中移除 + if (task.getClaimedBy() != null) { + Set agentTaskSet = agentTasks.get(task.getClaimedBy()); + if (agentTaskSet != null) { + agentTaskSet.remove(taskId); + updateAgentLoad(task.getClaimedBy()); + } + } + + LOG.debug("任务已删除: {}", task.getTitle()); + return true; + } + return false; + + } finally { + lock.unlock(); + // 清理锁(避免内存泄漏) + taskLocks.remove(taskId); + } + } + + // ========== 任务认领 ========== + + /** + * 认领任务 + * + * @param taskId 任务ID + * @param agentId Agent ID + * @return 认领是否成功 + */ + public CompletableFuture claimTask(String taskId, String agentId) { + return CompletableFuture.supplyAsync(() -> { + // 使用细粒度锁 + ReentrantLock lock = taskLocks.computeIfAbsent(taskId, k -> new ReentrantLock()); + lock.lock(); + try { + TeamTask task = tasks.get(taskId); + + // 验证任务存在 + if (task == null) { + LOG.warn("认领失败: 任务不存在 {}", taskId); + return false; + } + + // 验证任务可认领 + if (!task.isClaimable()) { + LOG.warn("认领失败: 任务不可认领 {} (状态: {})", task.getTitle(), task.getStatus()); + return false; + } + + // 验证所有依赖任务已完成(递归检查) + if (!task.areAllDependenciesCompleted(tasks::get)) { + LOG.warn("认领失败: 依赖任务未完成 {}", task.getTitle()); + return false; + } + + // 认领任务 + task.setStatus(TeamTask.Status.IN_PROGRESS); + task.setClaimedBy(agentId); + task.setClaimTime(System.currentTimeMillis()); + + // 从待认领队列移除 + pendingTasks.remove(taskId); + + // 添加到 Agent 的任务集合 + agentTasks.computeIfAbsent(agentId, k -> ConcurrentHashMap.newKeySet()).add(taskId); + updateAgentLoad(agentId); + + LOG.info("任务已认领: {} by {}", task.getTitle(), agentId); + + // 触发事件(在锁外执行,避免阻塞) + TeamTask finalTask = task; + String finalAgentId = agentId; + CompletableFuture.runAsync(() -> { + publishTaskEvent(AgentEventType.TASK_CLAIMED, finalTask, finalAgentId); + }, eventExecutor); + + return true; + + } catch (IllegalStateException e) { + // 循环依赖异常 + LOG.error("认领失败: {}", e.getMessage()); + return false; + } finally { + lock.unlock(); + } + }, taskExecutor); + } + + /** + * 智能认领(自动选择最佳任务) + * + * @param agentId Agent ID + * @return 认领的任务,无任务可认领返回 null + */ + public CompletableFuture smartClaim(String agentId) { + return CompletableFuture.supplyAsync(() -> { + // 获取可认领的任务(不加锁,使用 ConcurrentHashMap) + List claimable = getClaimableTasks(); + + if (claimable.isEmpty()) { + return null; + } + + // 按优先级排序(高优先级优先) + claimable.sort((a, b) -> Integer.compare(b.getPriority(), a.getPriority())); + + // 修复:尝试认领任务,失败则尝试下一个(避免竞争导致全部失败) + for (TeamTask task : claimable) { + try { + Boolean claimed = claimTask(task.getId(), agentId).join(); + if (claimed) { + LOG.debug("Agent {} 成功认领任务: {}", agentId, task.getTitle()); + return task; + } + // 任务被其他代理认领,尝试下一个 + LOG.debug("任务 {} 已被其他代理认领,尝试下一个", task.getTitle()); + } catch (Exception e) { + LOG.warn("认领任务 {} 时发生异常,尝试下一个", task.getTitle(), e); + } + } + + // 所有任务都认领失败 + LOG.debug("Agent {} 未能认领任何任务", agentId); + return null; + }, taskExecutor); + } + + /** + * 释放任务 + * + * @param taskId 任务ID + * @param agentId Agent ID + * @return 是否释放成功 + */ + public boolean releaseTask(String taskId, String agentId) { + // 使用细粒度锁 + ReentrantLock lock = taskLocks.computeIfAbsent(taskId, k -> new ReentrantLock()); + lock.lock(); + try { + TeamTask task = tasks.get(taskId); + + if (task == null) { + return false; + } + + // 验证是认领者 + if (!agentId.equals(task.getClaimedBy())) { + LOG.warn("释放失败: 不是任务的认领者 {}", agentId); + return false; + } + + // 重置状态 + task.setStatus(TeamTask.Status.PENDING); + task.setClaimedBy(null); + task.setClaimTime(0); + + // 加入待认领队列 + pendingTasks.put(taskId, task); + + // 从 Agent 的任务集合移除 + Set agentTaskSet = agentTasks.get(agentId); + if (agentTaskSet != null) { + agentTaskSet.remove(taskId); + updateAgentLoad(agentId); + } + + LOG.info("任务已释放: {} by {}", task.getTitle(), agentId); + + // 触发事件(在锁外执行,避免阻塞) + TeamTask finalTask = task; + String finalAgentId = agentId; + CompletableFuture.runAsync(() -> { + publishTaskEvent(AgentEventType.TASK_RELEASED, finalTask, finalAgentId); + }, eventExecutor); + + return true; + + } finally { + lock.unlock(); + } + } + + // ========== 任务完成 ========== + + /** + * 完成任务 + * + * @param taskId 任务ID + * @param result 执行结果 + * @return 是否完成成功 + */ + public boolean completeTask(String taskId, Object result) { + // 使用细粒度锁 + ReentrantLock lock = taskLocks.computeIfAbsent(taskId, k -> new ReentrantLock()); + lock.lock(); + try { + TeamTask task = tasks.get(taskId); + + if (task == null) { + return false; + } + + String agentId = task.getClaimedBy(); + + // 更新任务状态 + task.setStatus(TeamTask.Status.COMPLETED); + task.setResult(result); + task.setCompletedTime(System.currentTimeMillis()); + + // 从待认领队列移除(如果存在) + pendingTasks.remove(taskId); + + // 从 Agent 的任务集合移除 + if (agentId != null) { + Set agentTaskSet = agentTasks.get(agentId); + if (agentTaskSet != null) { + agentTaskSet.remove(taskId); + updateAgentLoad(agentId); + } + } + + // 添加到已完成队列 + completedTaskQueue.offer(taskId); + cleanupCompletedTasks(); + + // 性能优化:清除依赖此任务的其他任务的缓存 + clearDependentTasksCache(taskId); + + LOG.info("任务已完成: {} by {}", task.getTitle(), agentId); + + // 触发事件(在锁外执行,避免阻塞) + TeamTask finalTask = task; + String finalAgentId = agentId; + CompletableFuture.runAsync(() -> { + publishTaskEvent(AgentEventType.TASK_COMPLETED, finalTask, finalAgentId); + }, eventExecutor); + + return true; + + } finally { + lock.unlock(); + // 完成后清理锁(避免内存泄漏) + taskLocks.remove(taskId); + } + } + + /** + * 失败任务 + * + * @param taskId 任务ID + * @param errorMessage 错误信息 + * @return是否标记成功 + */ + public boolean failTask(String taskId, String errorMessage) { + // 使用细粒度锁 + ReentrantLock lock = taskLocks.computeIfAbsent(taskId, k -> new ReentrantLock()); + lock.lock(); + try { + TeamTask task = tasks.get(taskId); + + if (task == null) { + return false; + } + + String agentId = task.getClaimedBy(); + + // 更新任务状态 + task.setStatus(TeamTask.Status.FAILED); + task.setErrorMessage(errorMessage); + task.setCompletedTime(System.currentTimeMillis()); + + // 从待认领队列移除 + pendingTasks.remove(taskId); + + // 从 Agent 的任务集合移除 + if (agentId != null) { + Set agentTaskSet = agentTasks.get(agentId); + if (agentTaskSet != null) { + agentTaskSet.remove(taskId); + updateAgentLoad(agentId); + } + } + + // 性能优化:清除依赖此任务的其他任务的缓存 + clearDependentTasksCache(taskId); + + LOG.warn("任务已失败: {} - {}", task.getTitle(), errorMessage); + + // 触发事件(在锁外执行,避免阻塞) + TeamTask finalTask = task; + String finalAgentId = agentId; + CompletableFuture.runAsync(() -> { + publishTaskEvent(AgentEventType.TASK_FAILED, finalTask, finalAgentId); + }, eventExecutor); + + return true; + + } finally { + lock.unlock(); + // 失败后清理锁(避免内存泄漏) + taskLocks.remove(taskId); + } + } + + // ========== 查询方法 ========== + + /** + * 获取所有任务 + * 性能优化:不再持有锁 + * + * @return 任务列表 + */ + public List getAllTasks() { + return new ArrayList<>(tasks.values()); + } + + /** + * 获取待认领任务 + * 性能优化:不再持有锁 + * + * @return 任务列表 + */ + public List getPendingTasks() { + return new ArrayList<>(pendingTasks.values()); + } + + /** + * 获取可认领任务(考虑依赖关系,按优先级排序) + * 性能优化:不再持有全局锁,使用细粒度锁 + * + * @return 任务列表(按优先级降序排列) + */ + public List getClaimableTasks() { + // 不加锁,ConcurrentHashMap 支持并发读 + return pendingTasks.values().stream() + .filter(task -> { + try { + // 使用缓存优化后的依赖检查(TeamTask 中有缓存) + return task.areAllDependenciesCompleted(tasks::get); + } catch (IllegalStateException e) { + // 循环依赖的任务不能被认领 + LOG.warn("任务存在循环依赖,无法认领: {}", task.getTitle()); + return false; + } + }) + .sorted((a, b) -> { + // 按优先级降序排序(高优先级在前) + int priorityCompare = Integer.compare(b.getPriority(), a.getPriority()); + if (priorityCompare != 0) { + return priorityCompare; + } + // 优先级相同时,按创建时间升序排序(早创建的在前) + return Long.compare(a.getClaimTime(), b.getClaimTime()); + }) + .collect(Collectors.toList()); + } + + /** + * 获取 Agent 的任务列表 + * + * @param agentId Agent ID + * @return 任务列表 + */ + public List getAgentTasks(String agentId) { + // ConcurrentHashMap 支持并发读,无需加锁 + Set taskIds = agentTasks.getOrDefault(agentId, Collections.emptySet()); + return taskIds.stream() + .map(tasks::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * 获取 Agent 负载 + * + * @param agentId Agent ID + * @return 负载数(进行中的任务数) + */ + public int getAgentLoad(String agentId) { + // ConcurrentHashMap 支持并发读,无需加锁 + return agentLoad.getOrDefault(agentId, 0); + } + + /** + * 获取所有 Agent 负载 + * + * @return Agent ID -> 负载数 + */ + public Map getAllAgentLoads() { + // 返回副本以避免并发修改 + return new HashMap<>(agentLoad); + } + + /** + * 按状态获取任务 + * + * @param status 状态 + * @return 任务列表 + */ + public List getTasksByStatus(TeamTask.Status status) { + // ConcurrentHashMap 支持并发读,无需加锁 + return tasks.values().stream() + .filter(task -> task.getStatus() == status) + .collect(Collectors.toList()); + } + + /** + * 按类型获取任务 + * + * @param type 类型 + * @return 任务列表 + */ + public List getTasksByType(TeamTask.TaskType type) { + // ConcurrentHashMap 支持并发读,无需加锁 + return tasks.values().stream() + .filter(task -> task.getType() == type) + .collect(Collectors.toList()); + } + + // ========== 统计方法 ========== + + /** + * 获取任务统计 + * 性能优化:不再持有锁 + * + * @return 统计信息 + */ + public TaskStatistics getStatistics() { + Map statusCounts = tasks.values().stream() + .collect(Collectors.groupingBy(TeamTask::getStatus, Collectors.counting())); + + return new TaskStatistics( + tasks.size(), + statusCounts.getOrDefault(TeamTask.Status.PENDING, 0L).intValue(), + statusCounts.getOrDefault(TeamTask.Status.IN_PROGRESS, 0L).intValue(), + statusCounts.getOrDefault(TeamTask.Status.COMPLETED, 0L).intValue(), + statusCounts.getOrDefault(TeamTask.Status.FAILED, 0L).intValue(), + agentLoad.size() + ); + } + + // ========== 依赖关系诊断 ========== + + /** + * 获取任务的依赖树 + * + * @param taskId 任务ID + * @return 依赖树字符串 + */ + public String getTaskDependencyTree(String taskId) { + // ConcurrentHashMap 支持并发读,无需加锁 + TeamTask task = tasks.get(taskId); + if (task == null) { + return "任务不存在: " + taskId; + } + return task.getDependencyTree(tasks::get); + } + + /** + * 检测所有任务的循环依赖 + * + * @return 存在循环依赖的任务列表 + */ + public List detectCyclicDependencies() { + // ConcurrentHashMap 支持并发读,无需加锁 + return tasks.values().stream() + .filter(task -> task.hasCyclicDependency(tasks::get)) + .collect(Collectors.toList()); + } + + /** + * 获取任务依赖图(用于调试) + * + * @return 依赖关系的文本表示 + */ + public String getDependencyGraph() { + // ConcurrentHashMap 支持并发读,无需加锁 + StringBuilder sb = new StringBuilder(); + sb.append("=== 任务依赖关系图 ===\n\n"); + + for (TeamTask task : tasks.values()) { + if (task.getDependencies() != null && !task.getDependencies().isEmpty()) { + sb.append(task.getTitle()) + .append(" (").append(task.getId()).append(")") + .append(" [").append(task.getStatus()).append("]") + .append(" 依赖:\n"); + + for (String depId : task.getDependencies()) { + TeamTask dep = tasks.get(depId); + if (dep != null) { + sb.append(" → ").append(dep.getTitle()) + .append(" (").append(depId).append(")") + .append(" [").append(dep.getStatus()).append("]\n"); + } else { + sb.append(" → [不存在] ").append(depId).append("\n"); + } + } + sb.append("\n"); + } + } + + // 检测循环依赖 + List cyclicTasks = detectCyclicDependencies(); + if (!cyclicTasks.isEmpty()) { + sb.append("[WARN] 检测到循环依赖:\n"); + for (TeamTask task : cyclicTasks) { + sb.append(" - ").append(task.getTitle()) + .append(" (").append(task.getId()).append(")\n"); + } + } + + return sb.toString(); + } + + /** + * 获取阻塞的任务(依赖未完成导致无法认领) + * + * @return 被阻塞的任务列表 + */ + public List getBlockedTasks() { + // ConcurrentHashMap 支持并发读,无需加锁 + try { + return pendingTasks.values().stream() + .filter(task -> !task.areAllDependenciesCompleted(tasks::get)) + .collect(Collectors.toList()); + } catch (IllegalStateException e) { + LOG.error("检查阻塞任务时发生异常", e); + return Collections.emptyList(); + } + } + + /** + * 获取阻塞任务的详细信息 + * + * @return 阻塞信息 + */ + public String getBlockingInfo() { + // ConcurrentHashMap 支持并发读,无需加锁 + List blockedTasks = getBlockedTasks(); + + if (blockedTasks.isEmpty()) { + return "没有阻塞的任务"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("=== 阻塞任务详情 ===\n\n"); + + for (TeamTask task : blockedTasks) { + sb.append("任务: ").append(task.getTitle()) + .append(" (").append(task.getId()).append(")\n"); + sb.append("状态: ").append(task.getStatus()).append("\n"); + sb.append("等待依赖:\n"); + + for (String depId : task.getAllDependencyIds(tasks::get)) { + TeamTask dep = tasks.get(depId); + if (dep != null && !dep.isCompleted()) { + sb.append(" - ").append(dep.getTitle()) + .append(" [").append(dep.getStatus()).append("]\n"); + } + } + sb.append("\n"); + } + + return sb.toString(); + } + + /** + * 任务统计信息 + */ + public static class TaskStatistics { + public final int totalTasks; + public final int pendingTasks; + public final int inProgressTasks; + public final int completedTasks; + public final int failedTasks; + public final int activeAgents; + + public TaskStatistics(int totalTasks, int pendingTasks, int inProgressTasks, + int completedTasks, int failedTasks, int activeAgents) { + this.totalTasks = totalTasks; + this.pendingTasks = pendingTasks; + this.inProgressTasks = inProgressTasks; + this.completedTasks = completedTasks; + this.failedTasks = failedTasks; + this.activeAgents = activeAgents; + } + + @Override + public String toString() { + return String.format( + "TaskStatistics{总任务=%d, 待认领=%d, 进行中=%d, 已完成=%d, 失败=%d, 活跃Agent=%d}", + totalTasks, pendingTasks, inProgressTasks, completedTasks, failedTasks, activeAgents + ); + } + } + + /** + * 更新 Agent 负载 + */ + private void updateAgentLoad(String agentId) { + Set taskSet = agentTasks.get(agentId); + int load = (taskSet != null) ? taskSet.size() : 0; + agentLoad.put(agentId, load); + } + + /** + * 清除依赖此任务的所有任务的缓存 + * 性能优化:当任务完成/失败时,通知依赖它的任务清除缓存 + * + * @param completedTaskId 已完成的任务ID + */ + private void clearDependentTasksCache(String completedTaskId) { + int clearedCount = 0; + for (TeamTask task : tasks.values()) { + if (task.getDependencies().contains(completedTaskId)) { + task.clearCachedCompletedDeps(); + clearedCount++; + } + } + if (clearedCount > 0) { + LOG.debug("清除了 {} 个依赖任务的缓存(任务 {} 完成)", clearedCount, completedTaskId); + } + } + + /** + * 清理已完成任务 + */ + private void cleanupCompletedTasks() { + while (completedTaskQueue.size() > maxCompletedTasks) { + String taskId = completedTaskQueue.poll(); + if (taskId != null) { + TeamTask task = tasks.get(taskId); + if (task != null && task.isCompleted()) { + tasks.remove(taskId); + LOG.debug("清理已完成任务: {}", task.getTitle()); + } + } + } + } + + /** + * 发布任务事件到 EventBus + */ + private void publishTaskEvent(AgentEventType eventType, TeamTask task, String agentId) { + if (eventBus != null) { + try { + Map payload = new HashMap<>(); + payload.put("taskId", task.getId()); + payload.put("taskTitle", task.getTitle()); + payload.put("agentId", agentId); + payload.put("status", task.getStatus()); + payload.put("priority", task.getPriority()); + + // 对于失败任务,添加错误信息 + if (eventType == AgentEventType.TASK_FAILED && task.getErrorMessage() != null) { + payload.put("errorMessage", task.getErrorMessage()); + } + + // 创建事件元数据 + EventMetadata metadata = EventMetadata.builder() + .taskId(task.getId()) + .priority(task.getPriority()) + .build(); + + // 创建并发布事件 + AgentEvent event = new AgentEvent(eventType, payload, metadata); + eventBus.publishAsync(event); + } catch (Exception e) { + LOG.error("发布任务事件失败", e); + } + } + } + + /** + * 命名线程工厂(用于线程池) + */ + private static class NamedThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + private final boolean daemon; + + NamedThreadFactory(String namePrefix) { + this(namePrefix, true); + } + + NamedThreadFactory(String namePrefix, boolean daemon) { + this.namePrefix = namePrefix; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement()); + thread.setDaemon(daemon); + return thread; + } + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/SubAgentAgentBuilder.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/SubAgentAgentBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..a761d682b3c8826f5044cabf81d866e06da8f96f --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/SubAgentAgentBuilder.java @@ -0,0 +1,250 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import lombok.Builder; +import lombok.Getter; +import org.noear.solon.ai.agent.AgentSessionProvider; +import org.noear.solon.ai.chat.ChatModel; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.PoolManager; +import org.noear.solon.bot.core.event.EventBus; +import org.noear.solon.bot.core.message.MessageChannel; +import org.noear.solon.bot.core.memory.SharedMemoryManager; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; +import org.noear.solon.bot.core.subagent.Subagent; +import org.noear.solon.bot.core.subagent.SubagentManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * SubAgent 团队构建器 + * + * 基于 SubAgents 构建团队协作的便捷工具。支持: + * - 添加子代理成员 + * - 配置共享内存和事件总线 + * - 自动创建必要的协作组件 + * - 构建 MainAgent 作为团队协调器 + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Builder +public class SubAgentAgentBuilder { + private static final Logger LOG = LoggerFactory.getLogger(SubAgentAgentBuilder.class); + + // 必需配置 + private final ChatModel chatModel; + private final String workDir; + private final AgentSessionProvider sessionProvider; + private final PoolManager poolManager; + + // 可选配置(使用 @Builder.Default 设置默认值) + @Builder.Default + private final List subAgents = new ArrayList<>(); + private final SharedMemoryManager sharedMemoryManager; + private final EventBus eventBus; + private final MessageChannel messageChannel; + private final SharedTaskList taskList; + private final SubAgentMetadata mainAgentConfig; + + /** + * 静态工厂方法:创建构建器 + * + * @param chatModel 聊天模型(必需) + * @return 构建器实例 + */ + public static SubAgentAgentBuilder of(ChatModel chatModel) { + return SubAgentAgentBuilder.builder() + .chatModel(chatModel) + .subAgents(new ArrayList<>()) + .build(); + } + + /** + * 添加子代理 + * + * @param subAgent 子代理实例 + * @return this + */ + public SubAgentAgentBuilder addAgent(Subagent subAgent) { + if (subAgent != null) { + this.subAgents.add(subAgent); + LOG.debug("添加团队成员: {}", subAgent.getType()); + } + return this; + } + + /** + * 添加多个子代理 + * + * @param agents 子代理列表 + * @return this + */ + public SubAgentAgentBuilder addAgents(List agents) { + if (agents != null) { + for (Subagent agent : agents) { + addAgent(agent); + } + } + return this; + } + + /** + * 从 SubagentManager 添加所有预置子代理 + * + * @param manager 子代理管理器 + * @return this + */ + public SubAgentAgentBuilder addAllFrom(SubagentManager manager) { + if (manager != null) { + addAgents(new ArrayList<>(manager.getAgents())); + } + return this; + } + + /** + * 构建团队 + * + * 创建并配置所有必要的协作组件,返回 MainAgent 实例 + * + * @return MainAgent 实例作为团队协调器 + * @throws IllegalStateException 如果缺少必需参数 + */ + public MainAgent build() { + // 验证必需参数 + validateRequiredParams(); + + LOG.info("开始构建 Agent 团队..."); + LOG.info("工作目录: {}", workDir); + LOG.info("团队成员数: {}", subAgents.size()); + + // 1. 创建或使用提供的协作组件 + // 注意:SharedTaskList 依赖 EventBus,所以需要先创建 EventBus + EventBus eb = eventBus != null + ? eventBus + : createEventBus(); + + SharedMemoryManager smm = sharedMemoryManager != null + ? sharedMemoryManager + : createSharedMemoryManager(); + + MessageChannel mc = messageChannel != null + ? messageChannel + : createMessageChannel(); + + SharedTaskList tl = taskList != null + ? taskList + : createTaskList(eb); + + // 2. 创建主代理配置(如果没有提供) + SubAgentMetadata config = mainAgentConfig != null + ? mainAgentConfig + : createDefaultMainAgentConfig(); + + // 3. 构建 MainAgent + MainAgent mainAgent = new MainAgent( + config, + sessionProvider, + smm, + eb, + mc, + tl, + workDir, + poolManager + ); + + // 4. 初始化主代理 + try { + mainAgent.initialize(chatModel); + LOG.info("Agent 团队构建成功!主代理: {}", config.getName()); + } catch (Exception e) { + LOG.error("初始化 MainAgent 失败", e); + throw new RuntimeException("构建 Agent 团队失败", e); + } + + return mainAgent; + } + + /** + * 验证必需参数 + */ + private void validateRequiredParams() { + if (workDir == null || workDir.isEmpty()) { + throw new IllegalStateException("workDir 是必需参数,请使用 workDir() 设置"); + } + if (sessionProvider == null) { + throw new IllegalStateException("sessionProvider 是必需参数,请使用 sessionProvider() 设置"); + } + if (poolManager == null) { + throw new IllegalStateException("poolManager 是必需参数,请使用 poolManager() 设置"); + } + if (subAgents.isEmpty()) { + LOG.warn("团队没有成员,建议至少添加一个子代理"); + } + } + + /** + * 创建默认的共享内存管理器 + */ + private SharedMemoryManager createSharedMemoryManager() { + LOG.info("创建默认 SharedMemoryManager"); + return new SharedMemoryManager(Paths.get(workDir, AgentKernel.SOLONCODE_MEMORY)); + } + + /** + * 创建默认的事件总线 + */ + private EventBus createEventBus() { + LOG.info("创建默认 EventBus"); + return new EventBus(); + } + + /** + * 创建默认的消息通道 + */ + private MessageChannel createMessageChannel() { + LOG.info("创建默认 MessageChannel"); + return new MessageChannel(workDir); + } + + /** + * 创建默认的共享任务列表 + */ + private SharedTaskList createTaskList(EventBus eventBus) { + LOG.info("创建默认 SharedTaskList"); + return new SharedTaskList(eventBus); + } + + /** + * 创建默认的主代理配置 + */ + private SubAgentMetadata createDefaultMainAgentConfig() { + SubAgentMetadata config = new SubAgentMetadata(); + config.setCode("main-agent"); + config.setName("主代理"); + config.setDescription("Agent 团队协调器,负责任务分发和结果汇总"); + config.setEnabled(true); + + LOG.debug("创建默认主代理配置: {}", config.getName()); + return config; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamNameGenerator.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamNameGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..d003fee608911ecb9649ced67f2e870691d43d66 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamNameGenerator.java @@ -0,0 +1,300 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 团队名生成器 + * + * 根据角色和任务目标智能生成语义化的团队名 + * + * @author bai + * @since 3.9.5 + */ +public class TeamNameGenerator { + private static final Logger LOG = LoggerFactory.getLogger(TeamNameGenerator.class); + + // 领域关键词映射 + private static final java.util.Map DOMAIN_KEYWORDS = new java.util.HashMap() {{ + put("security", new String[]{"安全", "security", "认证", "登录", "权限", "鉴权", "加密", "防护", "漏洞", "攻击", "防御"}); + put("database", new String[]{"数据库", "database", "db", "sql", "mysql", "postgresql", "oracle", "redis", "mongodb", "存储", "查询"}); + put("frontend", new String[]{"前端", "frontend", "ui", "界面", "页面", "组件", "vue", "react", "angular", "可视化"}); + put("backend", new String[]{"后端", "backend", "api", "接口", "服务", "controller", "service", "逻辑"}); + put("devops", new String[]{"运维", "devops", "部署", "docker", "kubernetes", "k8s", "ci", "cd", "发布"}); + put("testing", new String[]{"测试", "testing", "test", "单元测试", "集成测试", "qa", "质量"}); + put("architecture", new String[]{"架构", "architecture", "设计", "模式", "分层", "微服务", "重构"}); + put("ai", new String[]{"ai", "人工智能", "机器学习", "ml", "深度学习", "模型", "算法", "智能"}); + put("performance", new String[]{"性能", "performance", "优化", "缓存", "加速", "压测", "监控"}); + put("docs", new String[]{"文档", "document", "doc", "readme", "说明", "指南"}); + }}; + + // 团队名模板 + private static final String[] TEAM_NAME_TEMPLATES = { + "{domain}-squad", // security-squad + "{domain}-team", // backend-team + "{domain}-masters", // database-masters + "{domain}-experts", // frontend-experts + "{domain}-force", // ai-force + "{domain}-lab", // performance-lab + "{domain}-guild", // security-guild + "{domain}-alliance", // ops-alliance + "the-{domain}-builders", // the-database-builders + "{domain}-collective" // frontend-collective + }; + + // 默认团队名(当无法识别领域时) + private static final String[] DEFAULT_TEAM_NAMES = { + "innovation-team", + "development-squad", + "solution-builders", + "tech-craftsmen", + "product-force", + "code-vanguards" + }; + + /** + * 根据角色和任务生成团队名 + * + * @param role 角色(如 "security-expert") + * @param description 描述(如 "专注于安全审计") + * @param taskGoal 任务目标(可选,如 "实现用户认证系统") + * @return 团队名 + */ + public static String generateTeamName(String role, String description, String taskGoal) { + // 1. 从角色中提取领域 + String domain = extractDomain(role, description, taskGoal); + + if (domain != null && !domain.isEmpty()) { + // 2. 从模板中选择 + String template = selectTemplate(domain); + String teamName = template.replace("{domain}", domain); + + // 3. 格式化(kebab-case) + teamName = toKebabCase(teamName); + + LOG.debug("生成团队名: role={}, description={}, domain={}, teamName={}", + role, description, domain, teamName); + return teamName; + } + + // 4. 无法识别时使用默认名 + String defaultName = DEFAULT_TEAM_NAMES[ + (int) (System.currentTimeMillis() / 1000) % DEFAULT_TEAM_NAMES.length + ]; + LOG.debug("使用默认团队名: role={}, teamName={}", role, defaultName); + return defaultName; + } + + /** + * 提取领域 + */ + private static String extractDomain(String role, String description, String taskGoal) { + Set mentionedDomains = new HashSet<>(); + + String combinedText = (role + " " + description + " " + (taskGoal != null ? taskGoal : "")).toLowerCase(); + + // 遍历所有领域关键词 + for (java.util.Map.Entry entry : DOMAIN_KEYWORDS.entrySet()) { + String domain = entry.getKey(); + String[] keywords = entry.getValue(); + + for (String keyword : keywords) { + if (combinedText.contains(keyword.toLowerCase())) { + mentionedDomains.add(domain); + break; // 找到一个匹配即可 + } + } + } + + // 返回优先级最高的领域 + if (mentionedDomains.isEmpty()) { + return null; + } + + // 优先级顺序(按重要性) + String[] priority = {"security", "ai", "database", "architecture", "devops", + "backend", "frontend", "testing", "performance", "docs"}; + + for (String domain : priority) { + if (mentionedDomains.contains(domain)) { + return domain; + } + } + + // 返回任意一个匹配的领域 + return mentionedDomains.iterator().next(); + } + + /** + * 选择合适的模板 + */ + private static String selectTemplate(String domain) { + // 根据领域特征选择合适的模板 + int index = (int) (domain.hashCode() % TEAM_NAME_TEMPLATES.length); + if (index < 0) { + index = -index; + } + return TEAM_NAME_TEMPLATES[index]; + } + + /** + * 转换为 kebab-case + */ + private static String toKebabCase(String input) { + // 移除特殊字符,只保留字母、数字和连字符 + String cleaned = input.replaceAll("[^a-zA-Z0-9-]", "-"); + // 移除重复的连字符 + cleaned = cleaned.replaceAll("-+", "-"); + // 移除首尾连字符 + cleaned = cleaned.replaceAll("^-|-$", ""); + return cleaned.toLowerCase(); + } + + /** + * 为现有团队生成更有意义的名称(迁移工具) + * + * @param oldTeamName 旧团队名(如 "team-1736640123456") + * @param memberNames 成员列表 + * @return 新团队名建议 + */ + public static String suggestBetterName(String oldTeamName, java.util.List memberNames) { + // 从成员角色中推断领域 + StringBuilder combinedRoles = new StringBuilder(); + for (String member : memberNames) { + combinedRoles.append(member).append(" "); + } + + String domain = extractDomain(combinedRoles.toString(), "", ""); + if (domain != null) { + return domain + "-team"; + } + + return null; // 无法建议 + } + + /** + * 解析团队名获取领域 + * + * @param teamName 团队名(如 "security-squad") + * @return 领域(如 "security") + */ + public static String extractDomainFromTeamName(String teamName) { + for (String domain : DOMAIN_KEYWORDS.keySet()) { + if (teamName.toLowerCase().startsWith(domain + "-") || + teamName.toLowerCase().contains("-" + domain + "-")) { + return domain; + } + } + return null; + } + + /** + * 获取团队描述 + * + * @param teamName 团队名 + * @return 团队描述 + */ + public static String getTeamDescription(String teamName) { + String domain = extractDomainFromTeamName(teamName); + if (domain == null) { + return "通用开发团队"; + } + + switch (domain) { + case "security": + return "专注于系统安全、身份认证和访问控制的专家团队"; + case "database": + return "数据库设计、优化和维护的专家团队"; + case "frontend": + return "用户界面和前端组件开发的专业团队"; + case "backend": + return "后端服务、API 和业务逻辑的实现团队"; + case "devops": + return "部署、运维和基础设施管理的自动化团队"; + case "testing": + return "质量保证和自动化测试的专业团队"; + case "architecture": + return "系统架构设计和技术选型的决策团队"; + case "ai": + return "人工智能和机器学习算法的实现团队"; + case "performance": + return "性能优化、缓存和加速方案的专业团队"; + case "docs": + return "技术文档编写和维护的支持团队"; + default: + return "专业开发团队"; + } + } + + /** + * 验证团队名是否有效 + * + * @param teamName 团队名 + * @return 是否有效 + */ + public static boolean isValidTeamName(String teamName) { + if (teamName == null || teamName.isEmpty()) { + return false; + } + + // 只能包含字母、数字、连字符 + Pattern pattern = Pattern.compile("^[a-z0-9-]+$"); + Matcher matcher = pattern.matcher(teamName); + return matcher.matches(); + } + + /** + * 规范化团队名 + * + * @param teamName 原始团队名 + * @return 规范化后的团队名 + */ + public static String normalizeTeamName(String teamName) { + if (teamName == null || teamName.isEmpty()) { + return "default-team"; + } + + // 转为小写 + teamName = teamName.toLowerCase(); + + // 替换空格和下划线为连字符 + teamName = teamName.replace(" ", "-").replace("_", "-"); + + // 移除特殊字符 + teamName = teamName.replaceAll("[^a-z0-9-]", ""); + + // 移除重复连字符 + teamName = teamName.replaceAll("-+", "-"); + + // 移除首尾连字符 + teamName = teamName.replaceAll("^-|-$", ""); + + // 如果为空,返回默认 + if (teamName.isEmpty()) { + return "default-team"; + } + + return teamName; + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamNameSuggestionTool.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamNameSuggestionTool.java new file mode 100644 index 0000000000000000000000000000000000000000..522b59b7050f01b992115cb47382684e7d34d91b --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamNameSuggestionTool.java @@ -0,0 +1,202 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.noear.solon.ai.annotation.ToolMapping; +import org.noear.solon.annotation.Param; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; +import org.noear.solon.bot.core.subagent.SubagentManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 团队命名建议工具 + * + * 为现有团队提供更好的命名建议 + * + * @author bai + * @since 3.9.5 + */ +public class TeamNameSuggestionTool { + private static final Logger LOG = LoggerFactory.getLogger(TeamNameSuggestionTool.class); + + private final SubagentManager manager; + + public TeamNameSuggestionTool(SubagentManager manager) { + this.manager = manager; + } + + /** + * 为团队提供命名建议 + */ + @ToolMapping(name = "suggest_team_name", + description = "为现有团队或即将创建的团队提供更好的命名建议。分析团队成员的角色和职责,生成语义化的团队名。") + public String suggestTeamName( + @Param(name = "oldTeamName", required = false, + description = "现有团队名(可选)。如果提供,将分析该团队的成员并给出改进建议") String oldTeamName, + @Param(name = "role", required = false, + description = "主要角色(可选)。如:security-expert") String role, + @Param(name = "description", required = false, + description = "团队描述(可选)。如:专注于系统安全") String description + ) { + StringBuilder result = new StringBuilder(); + + if (oldTeamName != null && !oldTeamName.isEmpty()) { + // 分析现有团队 + result.append(analyzeExistingTeam(oldTeamName)); + } else { + // 为新团队生成建议 + result.append(generateSuggestions(role, description)); + } + + return result.toString(); + } + + /** + * 分析现有团队并提供建议 + */ + private String analyzeExistingTeam(String oldTeamName) { + StringBuilder sb = new StringBuilder(); + + sb.append("## 团队名称分析\n\n"); + + // 检查是否是时间戳格式(如 team-1736640123456) + if (oldTeamName.matches("^team-\\d+$")) { + sb.append("⚠️ **当前团队名**: `").append(oldTeamName).append("`\n\n"); + sb.append("**问题**: 这是时间戳格式的团队名,不够语义化。\n\n"); + + // 获取该团队的成员 + List memberNames = getTeamMembers(oldTeamName); + if (!memberNames.isEmpty()) { + sb.append("**当前成员**:\n"); + for (String member : memberNames) { + sb.append(" - ").append(member).append("\n"); + } + sb.append("\n"); + + // 生成建议 + String suggestedName = TeamNameGenerator.suggestBetterName(oldTeamName, memberNames); + if (suggestedName != null) { + sb.append("✅ **建议团队名**: `").append(suggestedName).append("`\n\n"); + sb.append("**建议描述**: ") + .append(TeamNameGenerator.getTeamDescription(suggestedName)) + .append("\n\n"); + } + } + + sb.append("**如何重命名**:\n"); + sb.append("使用 `create_team` 工具创建新团队,并指定语义化的 teamName。\n"); + } else { + sb.append("✅ **当前团队名**: `").append(oldTeamName).append("`\n\n"); + + // 检查团队名是否有效 + if (!TeamNameGenerator.isValidTeamName(oldTeamName)) { + sb.append("⚠️ **问题**: 团队名格式不符合规范(只允许小写字母、数字和连字符)\n\n"); + String normalized = TeamNameGenerator.normalizeTeamName(oldTeamName); + sb.append("✅ **规范化建议**: `").append(normalized).append("`\n\n"); + } else { + sb.append("✅ 团队名格式正确!\n\n"); + + // 提取领域 + String domain = TeamNameGenerator.extractDomainFromTeamName(oldTeamName); + if (domain != null) { + sb.append("**识别的领域**: ").append(domain).append("\n\n"); + sb.append("**团队描述**: ") + .append(TeamNameGenerator.getTeamDescription(oldTeamName)) + .append("\n\n"); + } + } + } + + return sb.toString(); + } + + /** + * 为新团队生成命名建议 + */ + private String generateSuggestions(String role, String description) { + StringBuilder sb = new StringBuilder(); + + sb.append("## 团队命名建议\n\n"); + + if (role == null && description == null) { + sb.append("请提供角色或描述信息,以便生成更准确的团队名建议。\n\n"); + sb.append("**示例**:\n"); + sb.append("```\n"); + sb.append("suggest_team_name(role=\"security-expert\", description=\"专注于安全审计\")\n"); + sb.append("```\n\n"); + + sb.append("**常见领域示例**:\n"); + sb.append("- `security-squad` - 安全专家团队\n"); + sb.append("- `database-team` - 数据库专家团队\n"); + sb.append("- `frontend-experts` - 前端开发团队\n"); + sb.append("- `backend-force` - 后端开发团队\n"); + sb.append("- `devops-alliance` - 运维自动化团队\n"); + sb.append("- `testing-guild` - 质量保证团队\n"); + sb.append("- `architecture-lab` - 架构设计团队\n"); + sb.append("- `ai-collective` - 人工智能团队\n"); + + return sb.toString(); + } + + // 生成建议 + String teamName = TeamNameGenerator.generateTeamName( + role != null ? role : "expert", + description != null ? description : "", + null + ); + + sb.append("**基于输入生成的建议**:\n\n"); + sb.append("```\n"); + sb.append("团队名: ").append(teamName).append("\n"); + sb.append("描述: ").append(TeamNameGenerator.getTeamDescription(teamName)).append("\n"); + sb.append("```\n\n"); + + // 生成多个备选方案 + sb.append("**其他备选方案**:\n\n"); + String taskGoal = description != null ? description : role; + for (int i = 0; i < 3; i++) { + String alternative = TeamNameGenerator.generateTeamName( + role + "-" + i, + description, + taskGoal + ); + sb.append((i + 1)).append(". `").append(alternative).append("` - ") + .append(TeamNameGenerator.getTeamDescription(alternative)) + .append("\n"); + } + + sb.append("\n**使用方法**:\n"); + sb.append("在创建团队成员时使用 `teamName=\"").append(teamName).append("\"` 参数。\n"); + + return sb.toString(); + } + + /** + * 获取团队成员 + */ + private List getTeamMembers(String teamName) { + return manager.getAgents().stream() + .filter(agent -> agent.getMetadata().hasTeamName() && + agent.getMetadata().getTeamName().equals(teamName)) + .map(agent -> agent.getMetadata().getCode()) + .collect(Collectors.toList()); + } +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamTask.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamTask.java new file mode 100644 index 0000000000000000000000000000000000000000..2788bca53d5bf0f4d39c5ff48c9bd368f31d2926 --- /dev/null +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/teams/TeamTask.java @@ -0,0 +1,579 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.noear.solon.ai.chat.ChatResponse; +import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.ai.chat.ChatModel; +import org.noear.solon.bot.core.message.AgentMessage; +import reactor.core.publisher.Flux; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * 团队任务(Team Task) + *

+ * Agent Teams 中的任务对象,支持: + * - 任务分配和认领 + * - 优先级管理 + * - 依赖关系 + * - 状态跟踪 + * - 协作信息 + * + * @author bai + * @since 3.9.5 + */ +@Getter +@Setter +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class TeamTask { + + @Builder.Default + private String id = UUID.randomUUID().toString(); // 任务ID + private String title; // 任务标题 + private String description; // 详细描述 + private int priority; // 优先级(1-10,10最高) + @Builder.Default + private Status status = Status.PENDING; + @Builder.Default// 任务状态 + private TaskType type = TaskType.DEVELOPMENT; // 任务类型 + + // 认领信息 + private String claimedBy; // 认领者 Agent ID + private long claimTime; // 认领时间 + + // 执行信息 + private Object result; // 执行结果 + private String errorMessage; // 错误信息 + private long completedTime; // 完成时间 + + // 依赖和协作 + @Builder.Default + private List dependencies = new ArrayList<>(); // 依赖的任务ID列表 + @Builder.Default + private Map metadata = new HashMap<>(); // 元数据 + + // 性能优化:依赖完成状态缓存(避免重复递归检查) + private transient volatile Set cachedCompletedDeps; // 已完成的依赖ID集合 + private transient volatile long cacheVersion; // 缓存版本号(用于失效) + + + /** + * 任务状态枚举 + */ + public enum Status { + PENDING, // 待认领 + IN_PROGRESS, // 进行中 + COMPLETED, // 已完成 + FAILED, // 失败 + CANCELLED // 已取消 + } + + /** + * 任务类型枚举 + */ + public enum TaskType { + EXPLORATION, // 探索类任务 + DEVELOPMENT, // 开发类任务 + ANALYSIS, // 分析类任务 + DOCUMENTATION, // 文档类任务 + TESTING, // 测试类任务 + REVIEW // 审查类任务 + } + + /** + * 构造函数 + * + * @param title 任务标题 + */ + public TeamTask(String title) { + this.id = UUID.randomUUID().toString(); // 初始化 ID + this.title = title; + this.priority = 5; + this.status = Status.PENDING; + this.type = TaskType.DEVELOPMENT; + this.dependencies = new ArrayList<>(); + this.metadata = new HashMap<>(); + } + + /** + * 双参数构造函数 + * + * @param id 任务ID + * @param title 任务标题 + */ + public TeamTask(String id, String title) { + this.id = id != null ? id : UUID.randomUUID().toString(); // 防止 id 为 null + this.title = title; + this.priority = 5; + this.status = Status.PENDING; + this.type = TaskType.DEVELOPMENT; + this.dependencies = new ArrayList<>(); + this.metadata = new HashMap<>(); + } + + + + /** + * 设置 Prompt 并执行 + * + * @param chatModel LLM 模型 + * @return 异步结果 + */ + public CompletableFuture executeWithCall(ChatModel chatModel) { + return CompletableFuture.supplyAsync(() -> { + try { + // 创建 Prompt + Prompt prompt = Prompt.of(description != null ? description : title); + + // 调用 LLM + return chatModel.prompt(prompt).call(); + + } catch (Exception e) { + throw new RuntimeException("Task execution failed: " + title, e); + } + }); + } + + /** + * 设置 Prompt 并执行 + * + * @param chatModel LLM 模型 + * @return 异步结果 + */ + public CompletableFuture> executeWithSteam(ChatModel chatModel) { + return CompletableFuture.supplyAsync(() -> { + try { + // 创建 Prompt + Prompt prompt = Prompt.of(description != null ? description : title); + + // 调用 LLM + return chatModel.prompt(prompt).stream(); + + } catch (Exception e) { + throw new RuntimeException("Task execution failed: " + title, e); + } + }); + } + + + /** + * 设置状态(重写以清除缓存) + * + * @param status 任务状态 + * @throws IllegalArgumentException 如果状态转换不合法 + */ + public void setStatus(Status status) { + // 验证状态转换合法性 + if (!isValidTransition(this.status, status)) { + throw new IllegalArgumentException( + String.format("非法的状态转换: %s -> %s (任务: %s)", + this.status, status, this.title)); + } + this.status = status; + // 清除缓存,因为状态变化会影响依赖完成状态 + this.cachedCompletedDeps = null; + } + + /** + * 验证状态转换是否合法 + * + * @param from 当前状态 + * @param to 目标状态 + * @return 是否合法 + */ + private boolean isValidTransition(Status from, Status to) { + if (from == to) { + return true; // 允许保持相同状态 + } + + switch (from) { + case PENDING: + // PENDING 可以转换到:IN_PROGRESS, CANCELLED + return to == Status.IN_PROGRESS || to == Status.CANCELLED; + + case IN_PROGRESS: + // IN_PROGRESS 可以转换到:COMPLETED, FAILED, CANCELLED + return to == Status.COMPLETED || to == Status.FAILED || to == Status.CANCELLED; + + case COMPLETED: + case FAILED: + case CANCELLED: + // 终态不允许再转换 + return false; + + default: + return false; + } + } + + /** + * 设置依赖列表(重写以清除缓存) + * + * @param dependencies 依赖任务ID列表 + */ + public void setDependencies(List dependencies) { + this.dependencies = dependencies; + // 清除缓存 + this.cachedCompletedDeps = null; + } + + /** + * 清除依赖缓存 + * 当依赖任务完成时调用此方法,强制重新检查依赖状态 + */ + public void clearCachedCompletedDeps() { + this.cachedCompletedDeps = null; + } + + /** + * 添加元数据 + */ + public void putMetadata(String key, String value) { + this.metadata.put(key, value); + } + + /** + * 检查是否已完成 + */ + public boolean isCompleted() { + return status == Status.COMPLETED; + } + + /** + * 检查是否失败 + */ + public boolean isFailed() { + return status == Status.FAILED; + } + + /** + * 检查是否可认领 + */ + public boolean isClaimable() { + return status == Status.PENDING; + } + + /** + * 检查依赖任务是否都已完成(使用缓存优化性能) + * + * @param taskLookup 任务查找函数 + * @return 是否所有依赖都已完成 + * @throws IllegalStateException 如果存在循环依赖 + */ + public boolean areAllDependenciesCompleted(java.util.function.Function taskLookup) { + // 使用缓存避免重复递归检查 + if (cachedCompletedDeps != null) { + // 检查所有直接依赖是否都在缓存中 + if (dependencies == null || dependencies.isEmpty()) { + return true; + } + boolean allInCache = cachedCompletedDeps.containsAll(dependencies); + if (allInCache) { + return true; + } + // 缓存不完整,重新计算 + } + + // 执行递归检查并缓存结果 + Set completedDeps = new java.util.HashSet<>(); + boolean result = areAllDependenciesCompleted(taskLookup, completedDeps, new java.util.HashSet<>()); + + if (result) { + // 缓存已完成的依赖 + cachedCompletedDeps = completedDeps; + } + + return result; + } + + /** + * 递归检查依赖任务是否完成(检测循环依赖) + * + * @param taskLookup 任务查找函数 + * @param completedDeps 已完成的依赖ID集合(输出参数) + * @param visiting 正在访问的任务集合(用于检测循环) + * @return 是否所有依赖都已完成 + */ + private boolean areAllDependenciesCompleted(java.util.function.Function taskLookup, + java.util.Set completedDeps, + java.util.Set visiting) { + // 检测循环依赖 + if (visiting.contains(this.id)) { + throw new IllegalStateException("检测到循环依赖: 任务 " + this.id + " (" + this.title + ")"); + } + + // 如果没有依赖,返回true + if (dependencies == null || dependencies.isEmpty()) { + return true; + } + + // 标记当前任务正在访问 + visiting.add(this.id); + + try { + // 检查每个直接依赖 + for (String depId : dependencies) { + TeamTask dep = taskLookup.apply(depId); + + // 依赖任务不存在 + if (dep == null) { + return false; + } + + // 依赖任务未完成 + if (!dep.isCompleted()) { + return false; + } + + // 记录已完成的依赖 + completedDeps.add(depId); + + // 递归检查依赖的依赖(间接依赖) + if (!dep.areAllDependenciesCompleted(taskLookup, completedDeps, visiting)) { + return false; + } + } + + return true; + } finally { + // 移除访问标记 + visiting.remove(this.id); + } + } + + /** + * 获取完整的依赖树(用于可视化) + * + * @param taskLookup 任务查找函数 + * @return 依赖树描述 + */ + public String getDependencyTree(java.util.function.Function taskLookup) { + StringBuilder sb = new StringBuilder(); + sb.append("任务依赖树: ").append(this.title).append(" (").append(this.id).append(")\n"); + buildDependencyTree(taskLookup, this, " ", new java.util.HashSet<>(), sb); + return sb.toString(); + } + + /** + * 递归构建依赖树 + */ + private void buildDependencyTree(java.util.function.Function taskLookup, + TeamTask task, + String prefix, + java.util.Set visited, + StringBuilder sb) { + // 防止重复访问(循环依赖检测) + if (visited.contains(task.getId())) { + sb.append(prefix).append("└── [[WARN] 循环依赖] ").append(task.getTitle()) + .append(" (").append(task.getId()).append(")\n"); + return; + } + + visited.add(task.getId()); + + // 如果不是根节点,输出当前任务 + if (!task.getId().equals(this.id)) { + String statusIcon = getStatusIcon(task.getStatus()); + sb.append(prefix).append("└── ").append(statusIcon).append(" ").append(task.getTitle()) + .append(" (").append(task.getId()).append(")\n"); + } + + // 递归输出依赖 + if (task.getDependencies() != null && !task.getDependencies().isEmpty()) { + String childPrefix = prefix + "│ "; + for (String depId : task.getDependencies()) { + TeamTask dep = taskLookup.apply(depId); + if (dep != null) { + buildDependencyTree(taskLookup, dep, childPrefix, visited, sb); + } else { + sb.append(childPrefix).append("└── [[ERROR] 不存在] ").append(depId).append("\n"); + } + } + } + + visited.remove(task.getId()); + } + + /** + * 获取状态图标 + * 支持通过系统属性控制使用 Emoji 或 ASCII 字符 + * -DteamTask.useEmoji=true 使用 Emoji(默认) + * -DteamTask.useEmoji=false 使用 ASCII 字符(兼容旧系统) + */ + private String getStatusIcon(Status status) { + boolean useEmoji = Boolean.parseBoolean( + System.getProperty("teamTask.useEmoji", "true")); + + if (useEmoji) { + // Emoji 模式(默认,需要 UTF-8 支持) + switch (status) { + case PENDING: + return "[WAIT]"; + case IN_PROGRESS: + return "[PROCESS]"; + case COMPLETED: + return "[OK]"; + case FAILED: + return "[ERROR]"; + case CANCELLED: + return "[CANCEL]"; + default: + return "[UNKNOWN]"; + } + } else { + // ASCII 模式(兼容旧系统/Windows CMD) + switch (status) { + case PENDING: + return "[WAIT]"; + case IN_PROGRESS: + return "[DOING]"; + case COMPLETED: + return "[DONE]"; + case FAILED: + return "[FAIL]"; + case CANCELLED: + return "[CANCEL]"; + default: + return "[???]"; + } + } + } + + /** + * 检测是否存在循环依赖 + * + * @param taskLookup 任务查找函数 + * @return 是否存在循环依赖 + */ + public boolean hasCyclicDependency(java.util.function.Function taskLookup) { + return detectCyclicDependency(taskLookup, new java.util.HashSet<>()); + } + + /** + * 递归检测循环依赖 + * + * @param taskLookup 任务查找函数 + * @param visiting 正在访问的任务集合 + * @return 是否存在循环依赖 + */ + private boolean detectCyclicDependency(java.util.function.Function taskLookup, + java.util.Set visiting) { + // 检测循环依赖 + if (visiting.contains(this.id)) { + return true; // 发现循环 + } + + // 如果没有依赖,返回false + if (dependencies == null || dependencies.isEmpty()) { + return false; + } + + // 标记当前任务正在访问 + visiting.add(this.id); + + try { + // 递归检查每个依赖 + for (String depId : dependencies) { + TeamTask dep = taskLookup.apply(depId); + if (dep != null) { + if (dep.detectCyclicDependency(taskLookup, visiting)) { + return true; // 发现循环 + } + } + } + + return false; // 没有循环 + } finally { + // 移除访问标记 + visiting.remove(this.id); + } + } + + /** + * 获取所有依赖任务ID(包括间接依赖) + * + * @param taskLookup 任务查找函数 + * @return 所有依赖任务ID集合 + */ + public Set getAllDependencyIds(java.util.function.Function taskLookup) { + Set allDeps = new java.util.HashSet<>(); + collectAllDependencies(taskLookup, this, allDeps, new java.util.HashSet<>()); + return allDeps; + } + + /** + * 递归收集所有依赖ID + */ + private void collectAllDependencies(java.util.function.Function taskLookup, + TeamTask task, + Set result, + Set visited) { + // 防止循环依赖导致无限递归 + if (visited.contains(task.getId())) { + return; + } + + visited.add(task.getId()); + + if (task.getDependencies() != null) { + for (String depId : task.getDependencies()) { + if (result.add(depId)) { // 避免重复添加 + TeamTask dep = taskLookup.apply(depId); + if (dep != null) { + collectAllDependencies(taskLookup, dep, result, visited); + } + } + } + } + + visited.remove(task.getId()); + } + + /** + * 获取耗时 + */ + public long getDuration() { + if (completedTime > 0 && claimTime > 0) { + return completedTime - claimTime; + } + return 0; + } + + + @Override + public String toString() { + return "TeamTask{" + + "id='" + id + '\'' + + ", title='" + title + '\'' + + ", priority=" + priority + + ", status=" + status + + ", type=" + type + + ", claimedBy='" + claimedBy + '\'' + + (result != null ? ", hasResult=true" : "") + + (errorMessage != null ? ", error='" + errorMessage + '\'' : "") + + '}'; + } + +} diff --git a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/tool/WebfetchTool.java b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/tool/WebfetchTool.java index a2f6cb12b7c4b6eaea38cce064c86d7e32dd6ec4..ff349626e40401815738303ee889adce1a1fb411 100644 --- a/solonbot-sdk/src/main/java/org/noear/solon/bot/core/tool/WebfetchTool.java +++ b/solonbot-sdk/src/main/java/org/noear/solon/bot/core/tool/WebfetchTool.java @@ -1,6 +1,7 @@ package org.noear.solon.bot.core.tool; import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter; +import lombok.Getter; import org.jsoup.Jsoup; import org.noear.solon.ai.annotation.ToolMapping; import org.noear.solon.ai.rag.Document; diff --git a/solonbot-sdk/src/test/java/org/noear/solon/bot/core/goalker/GoalKeeperExample.java b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/goalker/GoalKeeperExample.java new file mode 100644 index 0000000000000000000000000000000000000000..103ef5c43ab1189a1de5fa345a3e1a36a2f8901b --- /dev/null +++ b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/goalker/GoalKeeperExample.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.goalker; + +/** + * GoalKeeper 使用示例 + * + * 演示如何使用目标守护者防止 AI 偏离目标 + * + * @author bai + * @since 3.9.5 + */ +public class GoalKeeperExample { + + public static void main(String[] args) { + System.out.println("=== 目标守护者示例 ===\n"); + + // 示例 1: 基本使用 + example1_BasicUsage(); + + // 示例 2: 目标提醒 + example2_GoalReminder(); + + // 示例 3: 强制结束检查 + example3_ForceFinishCheck(); + + System.out.println("\n=== 所有示例完成 ==="); + } + + /** + * 示例 1: 基本使用 + */ + private static void example1_BasicUsage() { + System.out.println("示例 1: 基本使用\n"); + + // 创建目标守护者 + GoalKeeper goalKeeper = new GoalKeeper( + "goal-001", + "实现用户登录功能" + ); + + System.out.println("目标ID: " + goalKeeper.getGoalId()); + System.out.println("原始目标: " + goalKeeper.getOriginalGoal()); + System.out.println("开始时间: " + goalKeeper.getStartTime()); + System.out.println(); + } + + /** + * 示例 2: 目标提醒 + */ + private static void example2_GoalReminder() { + System.out.println("示例 2: 目标提醒\n"); + + GoalKeeper goalKeeper = new GoalKeeper( + "goal-002", + "优化数据库查询性能" + ); + + // 测试不同步数的提醒 + int[] testSteps = {1, 5, 10, 11, 15, 20, 25}; + + for (int step : testSteps) { + String reminder = goalKeeper.injectReminder(step); + if (reminder != null) { + System.out.println("第 " + step + " 步提醒:"); + System.out.println(reminder); + } else { + System.out.println("第 " + step + " 步: 无需提醒"); + } + System.out.println(); + } + } + + /** + * 示例 3: 强制结束检查 + */ + private static void example3_ForceFinishCheck() { + System.out.println("示例 3: 强制结束检查\n"); + + GoalKeeper goalKeeper = new GoalKeeper( + "goal-003", + "实现JWT认证" + ); + + int maxSteps = 30; + + // 测试不同步数是否应该强制结束 + for (int step = 28; step <= 32; step++) { + boolean shouldFinish = goalKeeper.shouldForceFinish(step, maxSteps); + String status = shouldFinish ? "⚠️ 应该结束" : "✅ 可以继续"; + System.out.println("第 " + step + " 步: " + status); + } + System.out.println(); + } +} diff --git a/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/MainAgentTaskOrchestrationTest.java b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/MainAgentTaskOrchestrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3139aa0975e9a082757d2ce8a10162ee00601c40 --- /dev/null +++ b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/MainAgentTaskOrchestrationTest.java @@ -0,0 +1,297 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.noear.solon.ai.agent.AgentSessionProvider; +import org.noear.solon.ai.chat.prompt.Prompt; +import org.noear.solon.bot.core.event.EventBus; +import org.noear.solon.bot.core.memory.SharedMemoryManager; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Paths; +import java.util.List; + +/** + * MainAgent 任务编排测试 + * + * 测试主代理的任务创建、依赖关系管理和协调功能 + * + * @author bai + * @since 3.9.5 + */ +public class MainAgentTaskOrchestrationTest { + private static final Logger LOG = LoggerFactory.getLogger(MainAgentTaskOrchestrationTest.class); + + public static void main(String[] args) { + System.out.println("========== MainAgent 任务编排测试 ==========\n"); + + // 创建必要的组件 + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + SharedMemoryManager memoryManager = new SharedMemoryManager(Paths.get("./work")); + SubAgentMetadata config = new SubAgentMetadata(); + config.setCode("main-agent"); + config.setName("主代理"); + config.setDescription("测试主代理"); + + // 创建模拟的 SessionProvider + AgentSessionProvider sessionProvider = userId -> null; + + // 创建 MainAgent + MainAgent mainAgent = new MainAgent( + config, + sessionProvider, + memoryManager, + eventBus, + null, // MessageChannel 为 null(测试用) + taskList, + "./work", + null // PoolManager 为 null(测试用) + ); + + // 测试 1: 探索类任务创建 + testExplorationTasks(mainAgent); + + // 测试 2: 开发类任务创建 + testDevelopmentTasks(mainAgent); + + // 测试 3: 依赖关系验证 + testDependencyValidation(mainAgent); + + // 测试 4: 循环依赖检测 + testCyclicDependencyDetection(taskList); + + // 测试 5: 依赖关系可视化 + testDependencyVisualization(taskList); + + System.out.println("\n========== 测试完成 =========="); + } + + /** + * 测试探索类任务创建 + */ + private static void testExplorationTasks(MainAgent mainAgent) { + System.out.println("=== 测试 1: 探索类任务创建 ===\n"); + + Prompt prompt = Prompt.of("请探索这个代码库并分析结构"); + + // 使用反射调用私有方法 analyzeAndCreateTasks + try { + java.lang.reflect.Method method = MainAgent.class.getDeclaredMethod("analyzeAndCreateTasks", Prompt.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + List tasks = (List) method.invoke(mainAgent, prompt); + + System.out.println("✅ 创建了 " + tasks.size() + " 个探索任务:"); + for (TeamTask task : tasks) { + System.out.println(" - " + task.getTitle() + + " (优先级: " + task.getPriority() + ")" + + (task.getDependencies().isEmpty() ? "" : " 依赖: " + task.getDependencies())); + } + + // 验证依赖关系 + boolean hasCorrectDependencies = true; + for (TeamTask task : tasks) { + if (!task.getDependencies().isEmpty()) { + String firstDep = task.getDependencies().get(0); + if (!firstDep.contains("explore")) { + hasCorrectDependencies = false; + System.err.println("❌ 依赖关系错误: " + task.getTitle() + " -> " + firstDep); + } + } + } + + if (hasCorrectDependencies) { + System.out.println("✅ 依赖关系正确\n"); + } + + } catch (Exception e) { + System.err.println("❌ 测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 测试开发类任务创建 + */ + private static void testDevelopmentTasks(MainAgent mainAgent) { + System.out.println("=== 测试 2: 开发类任务创建 ===\n"); + + Prompt prompt = Prompt.of("请实现用户认证功能"); + + try { + java.lang.reflect.Method method = MainAgent.class.getDeclaredMethod("analyzeAndCreateTasks", Prompt.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + List tasks = (List) method.invoke(mainAgent, prompt); + + System.out.println("✅ 创建了 " + tasks.size() + " 个开发任务:"); + + // 验证任务链:计划 -> 实现 -> 测试 + String[] expectedOrder = {"计划", "实现", "测试"}; + int index = 0; + + for (TeamTask task : tasks) { + System.out.println(" " + (index + 1) + ". " + task.getTitle() + + " (优先级: " + task.getPriority() + ")" + + (task.getDependencies().isEmpty() ? "" : " 依赖: " + task.getDependencies())); + + // 验证依赖关系 + if (index > 0 && !task.getDependencies().isEmpty()) { + String depId = task.getDependencies().get(0); + if (!depId.contains(getTaskKeyword(expectedOrder[index - 1]))) { + System.err.println(" ❌ 依赖关系不正确,期望依赖: " + expectedOrder[index - 1]); + } + } + index++; + } + + System.out.println("✅ 开发任务链正确\n"); + + } catch (Exception e) { + System.err.println("❌ 测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 测试依赖关系验证 + */ + private static void testDependencyValidation(MainAgent mainAgent) { + System.out.println("=== 测试 3: 依赖关系验证 ===\n"); + + Prompt prompt = Prompt.of("开发并测试新功能"); + + try { + java.lang.reflect.Method method = MainAgent.class.getDeclaredMethod("analyzeAndCreateTasks", Prompt.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + List tasks = (List) method.invoke(mainAgent, prompt); + + // 添加任务到任务列表 + List added = mainAgent.getTaskList().addTasks(tasks).join(); + System.out.println("✅ 成功添加 " + added.size() + " 个任务到共享任务列表"); + + // 获取可认领任务 + List claimableTasks = mainAgent.getTaskList().getClaimableTasks(); + System.out.println("✅ 当前可认领任务数: " + claimableTasks.size()); + + // 第一个任务(计划)应该可认领 + if (claimableTasks.size() >= 1) { + System.out.println("✅ 第一个任务可认领: " + claimableTasks.get(0).getTitle()); + } + + // 其他任务应该被阻塞 + List blockedTasks = mainAgent.getTaskList().getBlockedTasks(); + if (blockedTasks.size() > 0) { + System.out.println("✅ 阻塞任务数: " + blockedTasks.size()); + for (TeamTask task : blockedTasks) { + System.out.println(" - " + task.getTitle()); + } + } + + System.out.println(); + + } catch (Exception e) { + System.err.println("❌ 测试失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 测试循环依赖检测 + */ + private static void testCyclicDependencyDetection(SharedTaskList taskList) { + System.out.println("=== 测试 4: 循环依赖检测 ===\n"); + + // 创建循环依赖任务 + String time = String.valueOf(System.currentTimeMillis()); + TeamTask taskX = TeamTask.builder() + .id("cyclic-x-" + time) + .title("循环任务 X") + .dependencies(java.util.Collections.singletonList("cyclic-z-" + time)) + .build(); + + TeamTask taskY = TeamTask.builder() + .id("cyclic-y-" + time) + .title("循环任务 Y") + .dependencies(java.util.Collections.singletonList("cyclic-x-" + time)) + .build(); + + TeamTask taskZ = TeamTask.builder() + .id("cyclic-z-" + time) + .title("循环任务 Z") + .dependencies(java.util.Collections.singletonList("cyclic-y-" + time)) + .build(); + + try { + // 添加 X 应该失败,因为依赖关系还不存在 + taskList.addTask(taskX).join(); + System.out.println("❌ 应该检测到依赖任务不存在"); + } catch (Exception e) { + System.out.println("✅ 正确检测到依赖任务不存在: " + e.getMessage()); + } + + // 先添加 Y 和 Z,再添加 X + taskList.addTask(taskY).join(); + taskList.addTask(taskZ).join(); + + try { + taskList.addTask(taskX).join(); + System.out.println("❌ 应该检测到循环依赖"); + } catch (Exception e) { + if (e.getMessage().contains("循环依赖")) { + System.out.println("✅ 成功检测到循环依赖: " + e.getMessage()); + } else { + System.err.println("❌ 错误信息不正确: " + e.getMessage()); + } + } + + // 使用检测方法 + List cyclicTasks = taskList.detectCyclicDependencies(); + System.out.println("✅ 检测到 " + cyclicTasks.size() + " 个循环依赖任务\n"); + } + + /** + * 测试依赖关系可视化 + */ + private static void testDependencyVisualization(SharedTaskList taskList) { + System.out.println("=== 测试 5: 依赖关系可视化 ===\n"); + + // 获取依赖图 + String graph = taskList.getDependencyGraph(); + System.out.println("任务依赖图:"); + System.out.println(graph); + + // 获取统计信息 + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + System.out.println("任务统计: " + stats); + + System.out.println(); + } + + /** + * 根据任务名称获取关键字 + */ + private static String getTaskKeyword(String taskName) { + if (taskName.contains("计划")) return "plan"; + if (taskName.contains("实现")) return "impl"; + if (taskName.contains("测试")) return "test"; + return ""; + } +} diff --git a/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/TaskOrchestrationTest.java b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/TaskOrchestrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5c241fb7d1254caa8eac734626c4c22cd56041f5 --- /dev/null +++ b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/TaskOrchestrationTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +import org.noear.solon.bot.core.event.EventBus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +/** + * 任务编排测试示例 + * + * 演示如何使用修复后的 TeamTask 和 SharedTaskList + * + * @author bai + * @since 3.9.5 + */ +public class TaskOrchestrationTest { + private static final Logger LOG = LoggerFactory.getLogger(TaskOrchestrationTest.class); + + public static void main(String[] args) { + // 创建事件总线和任务列表 + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + + System.out.println("========== 任务编排测试 ==========\n"); + + // 1. 测试基本依赖关系 + testBasicDependency(taskList); + + // 2. 测试间接依赖 + testIndirectDependency(taskList); + + // 3. 测试循环依赖检测 + testCyclicDependencyDetection(taskList); + + // 4. 测试依赖关系可视化 + testDependencyVisualization(taskList); + } + + /** + * 测试基本依赖关系 + */ + private static void testBasicDependency(SharedTaskList taskList) { + System.out.println("=== 测试 1: 基本依赖关系 ===\n"); + + // 创建任务:A -> B (A 依赖 B) + TeamTask taskB = TeamTask.builder() + .id("task-b") + .title("编写单元测试") + .description("为功能模块编写单元测试") + .priority(7) + .build(); + + TeamTask taskA = TeamTask.builder() + .id("task-a") + .title("实现功能") + .description("实现核心功能模块") + .priority(8) + .dependencies(Arrays.asList("task-b")) + .build(); + + try { + // 先添加 B,再添加 A + taskList.addTask(taskB).join(); + taskList.addTask(taskA).join(); + + System.out.println("✅ 任务添加成功"); + System.out.println("可认领任务数: " + taskList.getClaimableTasks().size()); + System.out.println("(只有 taskB 可认领,taskA 等待 taskB 完成)\n"); + + } catch (Exception e) { + System.err.println("❌ 失败: " + e.getMessage()); + } + } + + /** + * 测试间接依赖(多级依赖) + */ + private static void testIndirectDependency(SharedTaskList taskList) { + System.out.println("=== 测试 2: 间接依赖检查 ===\n"); + + // 创建任务:A -> B -> C -> D (A 依赖 B,B 依赖 C,C 依赖 D) + TeamTask taskD = TeamTask.builder() + .id("task-d") + .title("准备开发环境") + .priority(5) + .build(); + + TeamTask taskC = TeamTask.builder() + .id("task-c") + .title("设计架构") + .dependencies(Arrays.asList("task-d")) + .priority(6) + .build(); + + TeamTask taskB2 = TeamTask.builder() + .id("task-b2") + .title("编写代码") + .dependencies(Arrays.asList("task-c")) + .priority(7) + .build(); + + TeamTask taskA2 = TeamTask.builder() + .id("task-a2") + .title("部署上线") + .dependencies(Arrays.asList("task-b2")) + .priority(8) + .build(); + + try { + taskList.addTask(taskD).join(); + taskList.addTask(taskC).join(); + taskList.addTask(taskB2).join(); + taskList.addTask(taskA2).join(); + + System.out.println("✅ 多级依赖任务添加成功"); + System.out.println("可认领任务数: " + taskList.getClaimableTasks().size()); + System.out.println("(只有 taskD 可认领,其他任务等待依赖链完成)\n"); + + // 获取阻塞信息 + String blockingInfo = taskList.getBlockingInfo(); + if (blockingInfo.length() > 50) { + System.out.println("阻塞任务详情:"); + System.out.println(blockingInfo); + } + + } catch (Exception e) { + System.err.println("❌ 失败: " + e.getMessage()); + } + } + + /** + * 测试循环依赖检测 + */ + private static void testCyclicDependencyDetection(SharedTaskList taskList) { + System.out.println("=== 测试 3: 循环依赖检测 ===\n"); + + // 创建循环依赖:X -> Y -> Z -> X + TeamTask taskX = TeamTask.builder() + .id("task-x") + .title("任务 X") + .dependencies(Arrays.asList("task-z")) + .build(); + + TeamTask taskY = TeamTask.builder() + .id("task-y") + .title("任务 Y") + .dependencies(Arrays.asList("task-x")) + .build(); + + TeamTask taskZ = TeamTask.builder() + .id("task-z") + .title("任务 Z") + .dependencies(Arrays.asList("task-y")) + .build(); + + try { + // 尝试添加循环依赖任务 + taskList.addTask(taskX).join(); + System.out.println("❌ 应该检测到循环依赖,但没有!"); + } catch (IllegalArgumentException e) { + System.out.println("✅ 成功检测到循环依赖: " + e.getMessage()); + } + + // 测试检测方法 + System.out.println("\n检测所有任务的循环依赖:"); + List cyclicTasks = taskList.detectCyclicDependencies(); + System.out.println("发现 " + cyclicTasks.size() + " 个循环依赖任务\n"); + } + + /** + * 测试依赖关系可视化 + */ + private static void testDependencyVisualization(SharedTaskList taskList) { + System.out.println("=== 测试 4: 依赖关系可视化 ===\n"); + + // 显示完整依赖图 + System.out.println(taskList.getDependencyGraph()); + + // 显示单个任务的依赖树 + System.out.println("单个任务依赖树:"); + String tree = taskList.getTaskDependencyTree("task-a2"); + System.out.println(tree); + + // 显示统计信息 + System.out.println("任务统计:"); + System.out.println(taskList.getStatistics()); + } +} diff --git a/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/TeamNameGeneratorTest.java b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/TeamNameGeneratorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7a585259b7c856c46d008977b6e72d7d3348cdce --- /dev/null +++ b/solonbot-sdk/src/test/java/org/noear/solon/bot/core/teams/TeamNameGeneratorTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.bot.core.teams; + +/** + * 团队名生成器测试 + * + * 演示如何使用智能团队命名系统 + * + * @author bai + * @since 3.9.5 + */ +public class TeamNameGeneratorTest { + + public static void main(String[] args) { + System.out.println("=== 团队名生成器测试 ===\n"); + + // 测试 1: 安全专家 + testGenerate( + "security-expert", + "负责系统安全审计和漏洞检测", + "实现JWT认证系统" + ); + + // 测试 2: 数据库管理员 + testGenerate( + "database-admin", + "数据库优化和索引设计", + "优化查询性能" + ); + + // 测试 3: 前端开发 + testGenerate( + "frontend-dev", + "UI组件开发", + "实现响应式布局" + ); + + // 测试 4: 后端开发 + testGenerate( + "backend-dev", + "API接口开发", + "实现RESTful服务" + ); + + // 测试 5: AI工程师 + testGenerate( + "ai-engineer", + "机器学习模型集成", + "集成深度学习框架" + ); + + // 测试 6: 运维工程师 + testGenerate( + "devops-engineer", + "自动化部署和容器化", + "搭建K8s集群" + ); + + System.out.println("\n=== 团队名验证测试 ===\n"); + + // 测试验证功能 + testValidation("security-squad"); // ✅ 有效 + testValidation("team-1736640123456"); // ✅ 有效(虽然不推荐) + testValidation("SecuritySquad"); // ❌ 无效(大写) + testValidation("security squad"); // ❌ 无效(空格) + + System.out.println("\n=== 团队名规范化测试 ===\n"); + + // 测试规范化功能 + testNormalization("SecuritySquad", "securitysquad"); + testNormalization("security squad", "security-squad"); + testNormalization("security_team", "security-team"); + testNormalization("My.TEAM.Name", "my-team-name"); + + System.out.println("\n=== 领域提取测试 ===\n"); + + // 测试领域提取 + testExtractDomain("security-squad", "security"); + testExtractDomain("database-team", "database"); + testExtractDomain("frontend-experts", "frontend"); + testExtractDomain("unknown-team", null); + + System.out.println("\n=== 所有测试完成 ==="); + } + + private static void testGenerate(String role, String description, String taskGoal) { + System.out.println("输入:"); + System.out.println(" 角色: " + role); + System.out.println(" 描述: " + description); + System.out.println(" 目标: " + taskGoal); + + String teamName = TeamNameGenerator.generateTeamName(role, description, taskGoal); + + System.out.println("生成: " + teamName); + + String desc = TeamNameGenerator.getTeamDescription(teamName); + System.out.println("描述: " + desc); + System.out.println(); + } + + private static void testValidation(String teamName) { + boolean isValid = TeamNameGenerator.isValidTeamName(teamName); + System.out.println("验证: " + teamName + " -> " + (isValid ? "✅ 有效" : "❌ 无效")); + } + + private static void testNormalization(String input, String expected) { + String normalized = TeamNameGenerator.normalizeTeamName(input); + String status = normalized.equals(expected) ? "✅" : "❌"; + System.out.println(String.format("规范化: \"%s\" -> \"%s\" (期望: \"%s\") %s", + input, normalized, expected, status)); + } + + private static void testExtractDomain(String teamName, String expected) { + String domain = TeamNameGenerator.extractDomainFromTeamName(teamName); + String status = (expected == null && domain == null) || expected.equals(domain) ? "✅" : "❌"; + System.out.println(String.format("提取领域: \"%s\" -> \"%s\" (期望: \"%s\") %s", + teamName, domain, expected, status)); + } +} diff --git a/solonbot-sdk/src/test/java/subagent/SubagentMetadataTest.java b/solonbot-sdk/src/test/java/subagent/SubagentMetadataTest.java new file mode 100644 index 0000000000000000000000000000000000000000..34f3e1f4baec09e86c69671fbb32ab74bd949b48 --- /dev/null +++ b/solonbot-sdk/src/test/java/subagent/SubagentMetadataTest.java @@ -0,0 +1,326 @@ +/* + * Copyright 2017-2026 noear.org and 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 subagent; + +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.subagent.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Subagent 元数据测试 + * + * 测试内置 Subagent 的 getMetadata() 方法返回的元数据 + * + * @author bai + * @since 3.9.5 + */ +public class SubagentMetadataTest { + + private AgentKernel kernel; + + @BeforeEach + public void setUp() { + // 初始化 AgentKernel(测试用) + // kernel = new AgentKernel(...); + } + + /** + * 测试 ExploreSubagent 元数据 + */ + @Test + public void testExploreSubagentMetadata() { + ExploreSubagent agent = new ExploreSubagent(kernel); + SubAgentMetadata metadata = agent.getMetadata(); + + // 基本属性 + assertEquals("explore", metadata.getCode()); + assertEquals("探索子代理", metadata.getName()); + assertTrue(metadata.isEnabled()); + assertEquals(15, metadata.getMaxTurns()); + + // 工具 + List tools = metadata.getTools(); + assertTrue(tools.contains("ls")); + assertTrue(tools.contains("read")); + assertTrue(tools.contains("grep")); + assertTrue(tools.contains("glob")); + assertTrue(tools.contains("codesearch")); + assertFalse(tools.contains("write")); // 不包含写工具 + assertFalse(tools.contains("bash")); // 不包含 bash + + // 技能 + List skills = metadata.getSkills(); + assertTrue(skills.contains("expert")); + assertTrue(skills.contains("lucene")); + + // 输出 YAML 格式 + System.out.println("ExploreSubagent YAML:"); + System.out.println(metadata.toYamlFrontmatter()); + System.out.println(); + } + + /** + * 测试 PlanSubagent 元数据 + */ + @Test + public void testPlanSubagentMetadata() { + PlanSubagent agent = new PlanSubagent(kernel); + SubAgentMetadata metadata = agent.getMetadata(); + + // 基本属性 + assertEquals("plan", metadata.getCode()); + assertEquals("计划子代理", metadata.getName()); + assertEquals(20, metadata.getMaxTurns()); + + // 工具 - 包含网络搜索 + List tools = metadata.getTools(); + assertTrue(tools.contains("websearch")); + assertTrue(tools.contains("webfetch")); + assertTrue(tools.contains("codesearch")); + + // 技能 + List skills = metadata.getSkills(); + assertTrue(skills.contains("expert")); + assertTrue(skills.contains("lucene")); + + // 输出 YAML 格式 + System.out.println("PlanSubagent YAML:"); + System.out.println(metadata.toYamlFrontmatter()); + System.out.println(); + } + + /** + * 测试 BashSubagent 元数据 + */ + @Test + public void testBashSubagentMetadata() { + BashSubagent agent = new BashSubagent(kernel); + SubAgentMetadata metadata = agent.getMetadata(); + + // 基本属性 + assertEquals("bash", metadata.getCode()); + assertEquals("Bash 命令子代理", metadata.getName()); + assertEquals(10, metadata.getMaxTurns()); + + // 工具 - 最小工具集 + List tools = metadata.getTools(); + assertEquals(3, tools.size()); + assertTrue(tools.contains("ls")); + assertTrue(tools.contains("read")); + assertTrue(tools.contains("bash")); + + // 技能 - 无额外技能 + List skills = metadata.getSkills(); + assertTrue(skills.isEmpty()); + + // 输出 YAML 格式 + System.out.println("BashSubagent YAML:"); + System.out.println(metadata.toYamlFrontmatter()); + System.out.println(); + } + + /** + * 测试 GeneralPurposeSubagent 元数据 + */ + @Test + public void testGeneralPurposeSubagentMetadata() { + GeneralPurposeSubagent agent = new GeneralPurposeSubagent(kernel); + SubAgentMetadata metadata = agent.getMetadata(); + + // 基本属性 + assertEquals("general-purpose", metadata.getCode()); + assertEquals("通用子代理", metadata.getName()); + assertEquals(25, metadata.getMaxTurns()); + + // 工具 - 包含所有工具 + List tools = metadata.getTools(); + assertTrue(tools.contains("ls")); + assertTrue(tools.contains("read")); + assertTrue(tools.contains("write")); + assertTrue(tools.contains("edit")); + assertTrue(tools.contains("bash")); + assertTrue(tools.contains("websearch")); + assertTrue(tools.contains("webfetch")); + assertTrue(tools.contains("codesearch")); + + // 技能 - 包含所有技能 + List skills = metadata.getSkills(); + assertTrue(skills.contains("terminal")); + assertTrue(skills.contains("expert")); + assertTrue(skills.contains("lucene")); + assertTrue(skills.contains("todo")); + assertTrue(skills.contains("code")); + + // 输出 YAML 格式 + System.out.println("GeneralPurposeSubagent YAML:"); + System.out.println(metadata.toYamlFrontmatter()); + System.out.println(); + } + + /** + * 测试元数据导出功能 + */ + @Test + public void testMetadataExport() { + ExploreSubagent agent = new ExploreSubagent(kernel); + SubAgentMetadata metadata = agent.getMetadata(); + + // 测试 YAML frontmatter 导出 + String yaml = metadata.toYamlFrontmatter(); + assertNotNull(yaml); + assertTrue(yaml.contains("code: explore")); + assertTrue(yaml.contains("tools:")); + assertTrue(yaml.contains("skills:")); + + // 测试带提示词的完整导出 + String fullExport = metadata.toYamlFrontmatterWithPrompt("# 系统提示词"); + assertNotNull(fullExport); + assertTrue(fullExport.contains("---")); + assertTrue(fullExport.contains("# 系统提示词")); + + System.out.println("完整导出示例:"); + System.out.println(fullExport); + } + + /** + * 测试工具和技能对照表 + */ + @Test + public void testToolsAndSkillsMatrix() { + System.out.println("\n=== 内置 Subagent 工具和技能对照表 ===\n"); + + System.out.println("工具对照表:"); + System.out.println("| 工具 | Explore | Plan | Bash | General | Solon |"); + System.out.println("|------|---------|------|------|---------|-------|"); + printToolRow("ls"); + printToolRow("read"); + printToolRow("write"); + printToolRow("edit"); + printToolRow("grep"); + printToolRow("glob"); + printToolRow("bash"); + printToolRow("websearch"); + printToolRow("webfetch"); + printToolRow("codesearch"); + + System.out.println("\n技能对照表:"); + System.out.println("| 技能 | Explore | Plan | Bash | General | Solon |"); + System.out.println("|------|---------|------|------|---------|-------|"); + printSkillRow("terminal"); + printSkillRow("expert"); + printSkillRow("lucene"); + printSkillRow("todo"); + printSkillRow("code"); + } + + private void printToolRow(String tool) { + String[] marks = new String[5]; + marks[0] = hasTool("explore", tool); + marks[1] = hasTool("plan", tool); + marks[2] = hasTool("bash", tool); + marks[3] = hasTool("general-purpose", tool); + marks[4] = hasTool("solon-guide", tool); + + System.out.printf("| %s | %s | %s | %s | %s | %s |\n", + tool, marks[0], marks[1], marks[2], marks[3], marks[4]); + } + + private void printSkillRow(String skill) { + String[] marks = new String[5]; + marks[0] = hasSkill("explore", skill); + marks[1] = hasSkill("plan", skill); + marks[2] = hasSkill("bash", skill); + marks[3] = hasSkill("general-purpose", skill); + marks[4] = hasSkill("solon-guide", skill); + + System.out.printf("| %s | %s | %s | %s | %s | %s |\n", + skill, marks[0], marks[1], marks[2], marks[3], marks[4]); + } + + private String hasTool(String agentType, String tool) { + Subagent agent = createAgent(agentType); + return agent.getMetadata().getTools().contains(tool) ? "✅" : "❌"; + } + + private String hasSkill(String agentType, String skill) { + Subagent agent = createAgent(agentType); + return agent.getMetadata().getSkills().contains(skill) ? "✅" : "❌"; + } + + private Subagent createAgent(String type) { + switch (type) { + case "explore": return new ExploreSubagent(kernel); + case "plan": return new PlanSubagent(kernel); + case "bash": return new BashSubagent(kernel); + case "general-purpose": return new GeneralPurposeSubagent(kernel); + default: throw new IllegalArgumentException("Unknown agent type: " + type); + } + } + + /** + * 测试元数据一致性 + */ + @Test + public void testMetadataConsistency() { + // 确保 getType() 和 getCode() 一致 + ExploreSubagent exploreAgent = new ExploreSubagent(kernel); + assertEquals(exploreAgent.getType(), exploreAgent.getMetadata().getCode()); + + PlanSubagent planAgent = new PlanSubagent(kernel); + assertEquals(planAgent.getType(), planAgent.getMetadata().getCode()); + + BashSubagent bashAgent = new BashSubagent(kernel); + assertEquals(bashAgent.getType(), bashAgent.getMetadata().getCode()); + + GeneralPurposeSubagent generalAgent = new GeneralPurposeSubagent(kernel); + assertEquals(generalAgent.getType(), generalAgent.getMetadata().getCode()); + + } + + /** + * 测试元数据完整性 + */ + @Test + public void testMetadataCompleteness() { + Subagent[] agents = { + new ExploreSubagent(kernel), + new PlanSubagent(kernel), + new BashSubagent(kernel), + new GeneralPurposeSubagent(kernel), + }; + + for (Subagent agent : agents) { + SubAgentMetadata metadata = agent.getMetadata(); + + // 检查必需字段 + assertNotNull(metadata.getCode(), "Code 不能为 null"); + assertNotNull(metadata.getName(), "Name 不能为 null"); + assertNotNull(metadata.getDescription(), "Description 不能为 null"); + assertTrue(metadata.isEnabled(), "默认应该是启用的"); + + // 检查工具和技能列表 + assertNotNull(metadata.getTools(), "Tools 列表不能为 null"); + assertNotNull(metadata.getSkills(), "Skills 列表不能为 null"); + + System.out.printf("✅ %s 元数据完整%n", metadata.getCode()); + } + } +} diff --git a/soloncode-cli/src/main/resources/config.yml b/soloncode-cli/src/main/resources/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..fbb25b148ae554543566decac0f91824e8374a5f --- /dev/null +++ b/soloncode-cli/src/main/resources/config.yml @@ -0,0 +1,88 @@ +solon.logging.appender: + console: + enable: false # 是否启用控制台日志(启用后,会对 cli console 有干扰) + file: + level: INFO + +solon.code.cli: + workDir: "work" # 工作区目录 + maxSteps: 30 # ReAct 最大循环步数 + maxStepsAutoExtensible: true #最大步数自动续航(由 LLM 反思控制) + sessionWindowSize: 10 # 会话历史窗口大小(即,新指令时使用几条历史消息) + + hitlEnabled: true # 是否启用人工审核危险操作,和 maxSteps 动态继步 + + sandboxMode: true # 消盒模式,启用时禁止访问绝对路径 + thinkPrinted: true # 内心思考,是否打印 + + cliEnabled: true # 是否启用控制台 + cliPrintSimplified: true + + webEnabled: false # 是否启用 WebApi + webEndpoint: "/cli" + + acpEnabled: false # 是否启用 Acp 协议(一些支持 Acp 的 IDE 可以连接) + acpTransport: "websocket" # "stdio" or "websocket" + acpEndpoint: "/acp" + + subAgentEnabled: true + + # SubAgent 模型配置(可选) + # 如果不配置,将使用 chatModel.model 作为默认值 + # subAgentModels: + # explore: "glm-4-flash" # 探索代理使用快速模型 + # plan: "glm-4.7" # 计划代理使用推理模型 + # bash: "glm-4-flash" # Bash代理使用快速模型 + # general-purpose: "glm-4.7" # 通用代理使用推理模型 + # solon-code-guide: "glm-4.7" # Solon指南代理使用推理模型 + + # Agent Teams 增强功能配置 + # 共享记忆功能:允许子代理之间共享信息和知识 + sharedMemoryEnabled: true + sharedMemory: + shortTermTtl: 3600000 # 短期记忆TTL(毫秒,默认1小时) + longTermTtl: 604800000 # 长期记忆TTL(毫秒,默认7天) + cleanupInterval: 300000 # 清理间隔(毫秒,默认5分钟) + persistOnWrite: true # 写入时立即持久化 + maxShortTermCount: 1000 # 短期记忆最大数量 + maxLongTermCount: 500 # 长期记忆最大数量 + + # 事件总线功能:基于事件的异步通信 + eventBusEnabled: true + eventBus: + asyncThreads: 4 # 异步处理线程数(默认CPU核心数) + maxHistorySize: 1000 # 事件历史最大数量 + defaultPriority: 5 # 默认优先级(0-10) + timeoutSeconds: 30 # 处理超时时间(秒) + + # 消息通道功能:代理之间的直接消息传递 + messageChannelEnabled: true + messageChannel: + threads: 4 # 处理线程数 + defaultTtl: 60000 # 默认消息TTL(毫秒,默认60秒) + maxQueueSize: 1000 # 每个代理的最大队列长度 + persistMessages: true # 是否持久化消息 + + chatModel: # llm 配置(参考 solon ai chatmodel 配置) + apiUrl: "https://open.bigmodel.cn/api/paas/v4/chat/completions" + apiKey: "1c6ec604ad0644e09384103437643a7c.Mu9Mof2ge3KWbkGN" + model: "glm-4.7" + userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; SolonCode/1.0; +https://solon.noear.org/)" + # defaultOptions: + # enable_thinking: false #不同模型可能不同 + # temperature: 0.2 + # top_p: 0.5 + # frequency_penalty: 0.5 + # presence_penalty: 0.4 + skillPools: # 技能池(可以多个) + "@shared": "/path/to/shared-skills" +# mcpServers: +# gitee: +# url: "http://..." +# headers: { API_KEY: "xxx" } +# memory: +# command: "npx" +# args: [ "-y", "@modelcontextprotocol/server-memory" ] + +# 1. chatModel 按需配置,不熟的看下 solon ai 官网资料 +# 2. SolonCodeCLI 兼容 Claude Code Agent Skills \ No newline at end of file diff --git a/soloncode-cli/src/test/java/agentTems/AgentTeamsComprehensiveTest.java b/soloncode-cli/src/test/java/agentTems/AgentTeamsComprehensiveTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9f2d45ec8f19abd30d416aa881374cdc9cebace4 --- /dev/null +++ b/soloncode-cli/src/test/java/agentTems/AgentTeamsComprehensiveTest.java @@ -0,0 +1,914 @@ +/* + * Copyright 2017-2026 noear.org and 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 agentTems; + +import org.junit.jupiter.api.*; +import org.noear.solon.bot.core.event.*; +import org.noear.solon.bot.core.memory.*; +import org.noear.solon.bot.core.message.AgentMessage; +import org.noear.solon.bot.core.message.MessageChannel; +import org.noear.solon.bot.core.message.MessageHandler; +import org.noear.solon.bot.core.teams.SharedTaskList; +import org.noear.solon.bot.core.teams.TeamTask; + +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Agent Teams 综合测试 + * + * 测试覆盖: + * 1. 路由模式 + * 2. 协作状态 + * 3. 不同 agents 之间的交互 + * 4. 死循环检测 + * 5. mainAgents 的使用 + * + * @author bai + * @since 3.9.5 + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AgentTeamsComprehensiveTest { + + private static final String TEST_WORK_DIR = "./work/test-teams-comprehensive"; + + private SharedMemoryManager memoryManager; + private EventBus eventBus; + private MessageChannel messageChannel; + private SharedTaskList taskList; + + @BeforeEach + public void setUp() { + System.out.println("\n========================================"); + + // 初始化组件 + memoryManager = new SharedMemoryManager( + Paths.get(TEST_WORK_DIR, "memory"), + 3600_000L, + 7 * 24 * 3600_000L, + 300_000L, + true, + 1000, + 500 + ); + + eventBus = new EventBus(2, 100); + messageChannel = new MessageChannel(TEST_WORK_DIR, 2); + taskList = new SharedTaskList(eventBus, 50); + + System.out.println("✓ 测试环境初始化完成"); + } + + @AfterEach + public void tearDown() { + System.out.println("\n清理测试环境..."); + + try { + if (memoryManager != null) { + memoryManager.shutdown(); + } + if (eventBus != null) { + eventBus.shutdown(); + } + if (messageChannel != null) { + messageChannel.shutdown(); + } + } catch (Exception e) { + System.err.println("清理错误: " + e.getMessage()); + } + + System.out.println("✓ 测试环境已清理"); + System.out.println("========================================"); + } + + // ==================== 1. 路由模式测试 ==================== + + @Test + @Order(1) + @DisplayName("测试任务路由模式 - 基于优先级") + public void testRoutingByPriority() throws Exception { + System.out.println("\n=== 测试任务路由模式 - 基于优先级 ==="); + + // 创建不同优先级的任务 + TeamTask lowPriorityTask = TeamTask.builder() + .title("低优先级任务") + .priority(3) + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + TeamTask highPriorityTask = TeamTask.builder() + .title("高优先级任务") + .priority(9) + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + TeamTask mediumPriorityTask = TeamTask.builder() + .title("中优先级任务") + .priority(5) + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + // 添加到任务列表 + taskList.addTask(lowPriorityTask).join(); + taskList.addTask(mediumPriorityTask).join(); + taskList.addTask(highPriorityTask).join(); + + System.out.println("添加了 3 个任务(优先级: 3, 5, 9)"); + + // 获取可认领任务(应该按优先级排序) + List claimableTasks = taskList.getClaimableTasks(); + + System.out.println("可认领任务数: " + claimableTasks.size()); + for (TeamTask task : claimableTasks) { + System.out.println(" - " + task.getTitle() + " (优先级: " + task.getPriority() + ")"); + } + + // 验证高优先级任务排在前面 + assertEquals(3, claimableTasks.size()); + assertEquals(9, claimableTasks.get(0).getPriority()); + assertEquals(5, claimableTasks.get(1).getPriority()); + assertEquals(3, claimableTasks.get(2).getPriority()); + + System.out.println("✓ 任务按优先级正确排序"); + } + + @Test + @Order(2) + @DisplayName("测试任务路由模式 - 基于类型") + public void testRoutingByType() throws Exception { + System.out.println("\n=== 测试任务路由模式 - 基于类型 ==="); + + // 创建不同类型的任务 + TeamTask exploreTask = TeamTask.builder() + .title("探索任务") + .type(TeamTask.TaskType.EXPLORATION) + .build(); + + TeamTask devTask = TeamTask.builder() + .title("开发任务") + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + TeamTask testTask = TeamTask.builder() + .title("测试任务") + .type(TeamTask.TaskType.TESTING) + .build(); + + // 添加任务 + taskList.addTask(exploreTask).join(); + taskList.addTask(devTask).join(); + taskList.addTask(testTask).join(); + + System.out.println("添加了 3 个不同类型的任务"); + + // 模拟不同类型的 agent 认领任务 + String exploreAgent = "explore-agent"; + String devAgent = "dev-agent"; + + // explore-agent 只认领探索任务 + List allTasks = taskList.getAllTasks(); + for (TeamTask task : allTasks) { + if (task.getType() == TeamTask.TaskType.EXPLORATION) { + taskList.claimTask(task.getId(), exploreAgent).join(); + System.out.println(exploreAgent + " 认领了: " + task.getTitle()); + } else if (task.getType() == TeamTask.TaskType.DEVELOPMENT) { + taskList.claimTask(task.getId(), devAgent).join(); + System.out.println(devAgent + " 认领了: " + task.getTitle()); + } + } + + // 验证认领结果 + assertEquals(TeamTask.Status.IN_PROGRESS, + taskList.getTask(exploreTask.getId()).getStatus()); + assertEquals(TeamTask.Status.IN_PROGRESS, + taskList.getTask(devTask.getId()).getStatus()); + assertEquals(TeamTask.Status.PENDING, + taskList.getTask(testTask.getId()).getStatus()); + + System.out.println("✓ 任务按类型正确路由"); + } + + @Test + @Order(3) + @DisplayName("测试任务路由模式 - 基于负载均衡") + public void testRoutingByLoadBalancing() throws Exception { + System.out.println("\n=== 测试任务路由模式 - 基于负载均衡 ==="); + + // 创建多个任务 + for (int i = 1; i <= 5; i++) { + TeamTask task = TeamTask.builder() + .title("任务 " + i) + .build(); + taskList.addTask(task).join(); + } + + System.out.println("添加了 5 个任务"); + + // 模拟 3 个 agent 认领任务 + String agent1 = "agent-1"; + String agent2 = "agent-2"; + String agent3 = "agent-3"; + + // Agent 1 认领 2 个任务 + claimTasksForAgent(agent1, 2); + Thread.sleep(50); + + // Agent 2 认领 1 个任务 + claimTasksForAgent(agent2, 1); + Thread.sleep(50); + + // Agent 3 认领 2 个任务 + claimTasksForAgent(agent3, 2); + + // 检查负载分布 + Map loads = taskList.getAllAgentLoads(); + System.out.println("Agent 负载分布:"); + loads.forEach((agent, load) -> System.out.println(" " + agent + ": " + load)); + + assertEquals(2, (int) loads.get(agent1)); + assertEquals(1, (int) loads.get(agent2)); + assertEquals(2, (int) loads.get(agent3)); + + // 验证负载最小的 agent + String leastLoadedAgent = loads.entrySet().stream() + .min(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(null); + assertEquals(agent2, leastLoadedAgent); + System.out.println("✓ 负载均衡正确,最小负载 agent: " + leastLoadedAgent); + } + + // ==================== 2. 协作状态测试 ==================== + + @Test + @Order(4) + @DisplayName("测试协作状态 - 任务依赖") + public void testCollaborationWithDependencies() throws Exception { + System.out.println("\n=== 测试协作状态 - 任务依赖 ==="); + + // 创建任务链: task1 -> task2 -> task3 + TeamTask task1 = TeamTask.builder() + .title("基础任务") + .build(); + + TeamTask task2 = TeamTask.builder() + .title("依赖任务1") + .dependencies(Arrays.asList(task1.getId())) + .build(); + + TeamTask task3 = TeamTask.builder() + .title("依赖任务2") + .dependencies(Arrays.asList(task2.getId())) + .build(); + + taskList.addTask(task1).join(); + taskList.addTask(task2).join(); + taskList.addTask(task3).join(); + + System.out.println("创建了任务链: task1 -> task2 -> task3"); + + // 验证依赖关系 + Function taskLookup = id -> taskList.getTask(id); + + // task1 没有依赖,应该返回 true + assertTrue(task1.areAllDependenciesCompleted(taskLookup), + "task1 无依赖,应该返回 true"); + + // task2 依赖 task1,task1 未完成,应该返回 false + assertFalse(task2.areAllDependenciesCompleted(taskLookup), + "task2 依赖未完成的 task1,应该返回 false"); + + // 完成 task1 + task1.setStatus(TeamTask.Status.COMPLETED); + + System.out.println("task1 已完成"); + + // 验证 task2 是否可以开始 + List pendingTasks = taskList.getPendingTasks(); + boolean task2Pending = pendingTasks.stream() + .anyMatch(t -> t.getId().equals(task2.getId())); + assertTrue(task2Pending, "task2 应该在待认领列表中"); + + System.out.println("✓ 任务依赖协作正常"); + } + + @Test + @Order(5) + @DisplayName("测试协作状态 - 多 agent 并行工作") + public void testMultiAgentParallelWork() throws Exception { + System.out.println("\n=== 测试协作状态 - 多 agent 并行工作 ==="); + + // 创建多个独立任务 + List tasks = new ArrayList<>(); + for (int i = 1; i <= 3; i++) { + TeamTask task = TeamTask.builder() + .title("并行任务 " + i) + .build(); + taskList.addTask(task).join(); + tasks.add(task); + } + + System.out.println("创建了 3 个并行任务"); + + // 多个 agent 同时认领任务(带重试机制) + CompletableFuture agent1 = CompletableFuture.supplyAsync(() -> { + try { + String agentId = "agent-1"; + int maxRetries = 3; + + for (int retry = 0; retry < maxRetries; retry++) { + List claimable = taskList.getClaimableTasks(); + if (claimable.isEmpty()) { + System.out.println(agentId + " 没有可认领的任务(重试 " + retry + "/" + maxRetries + ")"); + Thread.sleep(50); + continue; + } + + // 尝试认领第一个任务 + TeamTask task = claimable.get(0); + Boolean claimed = taskList.claimTask(task.getId(), agentId).join(); + + if (claimed) { + System.out.println(agentId + " 认领: " + task.getTitle()); + + // 模拟工作 + Thread.sleep(100); + task.setStatus(TeamTask.Status.COMPLETED); + taskList.completeTask(task.getId(), "完成"); + + System.out.println(agentId + " 完成: " + task.getTitle()); + return task; + } else { + System.out.println(agentId + " 认领失败,任务可能已被其他 agent 认领"); + Thread.sleep(50); // 等待后重试 + } + } + + System.out.println(agentId + " 经过 " + maxRetries + " 次重试后仍未找到可认领任务"); + return null; + + } catch (Exception e) { + e.printStackTrace(); + return null; + } + }); + + CompletableFuture agent2 = CompletableFuture.supplyAsync(() -> { + try { + String agentId = "agent-2"; + int maxRetries = 3; + + for (int retry = 0; retry < maxRetries; retry++) { + List claimable = taskList.getClaimableTasks(); + if (claimable.isEmpty()) { + System.out.println(agentId + " 没有可认领的任务(重试 " + retry + "/" + maxRetries + ")"); + Thread.sleep(50); + continue; + } + + // 尝试认领第一个任务 + TeamTask task = claimable.get(0); + Boolean claimed = taskList.claimTask(task.getId(), agentId).join(); + + if (claimed) { + System.out.println(agentId + " 认领: " + task.getTitle()); + + // 模拟工作 + Thread.sleep(100); + task.setStatus(TeamTask.Status.COMPLETED); + taskList.completeTask(task.getId(), "完成"); + + System.out.println(agentId + " 完成: " + task.getTitle()); + return task; + } else { + System.out.println(agentId + " 认领失败,任务可能已被其他 agent 认领"); + Thread.sleep(50); // 等待后重试 + } + } + + System.out.println(agentId + " 经过 " + maxRetries + " 次重试后仍未找到可认领任务"); + return null; + + } catch (Exception e) { + e.printStackTrace(); + return null; + } + }); + + // 等待所有 agent 完成 + CompletableFuture.allOf(agent1, agent2).get(5, TimeUnit.SECONDS); + + // 验证结果 + long completedCount = tasks.stream() + .filter(t -> taskList.getTask(t.getId()).getStatus() == TeamTask.Status.COMPLETED) + .count(); + + System.out.println("已完成任务数: " + completedCount); + assertTrue(completedCount >= 2, "至少应该完成 2 个任务"); + System.out.println("✓ 多 agent 并行协作正常"); + } + + // ==================== 3. Agents 交互测试 ==================== + + @Test + @Order(6) + @DisplayName("测试 agents 交互 - 消息传递") + public void testAgentMessagePassing() throws Exception { + System.out.println("\n=== 测试 agents 交互 - 消息传递 ==="); + + // 注册消息处理器 + AtomicInteger messageCount = new AtomicInteger(0); + + messageChannel.registerHandler("receiver", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + int count = messageCount.incrementAndGet(); + System.out.println("收到消息 #" + count + ": " + message.getContent()); + + if ("query".equals(message.getType())) { + return CompletableFuture.completedFuture("响应: " + message.getContent()); + } + return CompletableFuture.completedFuture("ACK"); + } + }); + + // 发送多条消息 + for (int i = 1; i <= 3; i++) { + CompletableFuture response = messageChannel.request( + "sender", + "receiver", + "query", + "测试消息 " + i + ); + Object result = response.get(200, TimeUnit.SECONDS); + System.out.println("收到响应: " + result); + assertNotNull(result); + } + + assertEquals(3, messageCount.get()); + System.out.println("✓ Agent 消息传递正常"); + } + + @Test + @Order(7) + @DisplayName("测试 agents 交互 - 事件广播") + public void testAgentEventBroadcast() throws Exception { + System.out.println("\n=== 测试 agents 交互 - 事件广播 ==="); + + // 订阅计数器(验证订阅者能收到事件) + AtomicInteger receiveCount = new AtomicInteger(0); + CompletableFuture eventReceived = new CompletableFuture<>(); + + // 使用枚举类型订阅(与发布的事件类型匹配) + String subscriptionId = eventBus.subscribe(AgentEventType.TASK_PROGRESS, event -> { + try { + String agent = event.getMetadata().getSourceAgent(); + String content = (String) event.getPayload(); + + System.out.println("✓ 订阅者收到事件: sourceAgent=" + agent + ", content=" + content); + + // 完成 Future + if (receiveCount.incrementAndGet() > 0) { + eventReceived.complete(content); + } + + return CompletableFuture.completedFuture(EventHandler.Result.success()); + } catch (Exception e) { + System.err.println("事件处理器异常: " + e.getMessage()); + e.printStackTrace(); + throw e; + } + }); + + System.out.println("订阅ID: " + subscriptionId); + System.out.println("订阅者数量: " + eventBus.getSubscriberCount(AgentEventType.TASK_PROGRESS)); + + // 等待订阅生效 + Thread.sleep(200); + + // 发布事件 + System.out.println("发布事件..."); + AgentEvent event = new AgentEvent( + AgentEventType.TASK_PROGRESS, + "任务已更新", + EventMetadata.builder() + .sourceAgent("agent-1") + .taskId("task-123") + .build() + ); + + eventBus.publish(event); + System.out.println("事件已发布,等待响应..."); + + // 等待订阅者接收事件 + String result = eventReceived.get(3, TimeUnit.SECONDS); + assertNotNull(result); + assertEquals("任务已更新", result); + + System.out.println("✓ Agent 事件广播正常"); + } + + @Test + @Order(8) + @DisplayName("测试 agents 交互 - 共享内存") + public void testAgentSharedMemory() throws Exception { + System.out.println("\n=== 测试 agents 交互 - 共享内存 ==="); + + // Agent 1 写入记忆 + ShortTermMemory memory1 = memoryManager.createShortTermMemory( + "agent-1", + "Agent 1 的发现", + "task-1" + ); + memoryManager.store(memory1); + System.out.println("Agent 1 写入记忆"); + + // Agent 2 读取记忆 + List memories = memoryManager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + assertFalse(memories.isEmpty()); + System.out.println("Agent 2 读取到 " + memories.size() + " 条记忆"); + + // Agent 3 搜索记忆 + List searchResults = memoryManager.search("Agent 1", 5); + assertFalse(searchResults.isEmpty()); + System.out.println("Agent 3 搜索到 " + searchResults.size() + " 条相关记忆"); + + // 验证记忆内容 + Memory found = searchResults.get(0); + if (found instanceof ShortTermMemory) { + ShortTermMemory stm = (ShortTermMemory) found; + assertEquals("agent-1", stm.getAgentId()); + assertEquals("Agent 1 的发现", stm.getContext()); + } + + System.out.println("✓ Agent 共享内存正常"); + } + + // ==================== 4. 死循环检测测试 ==================== + + @Test + @Order(9) + @DisplayName("测试死循环检测 - 任务循环依赖") + public void testCyclicDependencyDetection() { + System.out.println("\n=== 测试死循环检测 - 任务循环依赖 ==="); + + // 创建循环依赖: task1 -> task2 -> task3 -> task1 + TeamTask task1 = TeamTask.builder().title("任务1").build(); + TeamTask task2 = TeamTask.builder() + .title("任务2") + .dependencies(Arrays.asList(task1.getId())) + .build(); + TeamTask task3 = TeamTask.builder() + .title("任务3") + .dependencies(Arrays.asList(task2.getId())) + .build(); + + // 闭合循环 + task1.setDependencies(Arrays.asList(task3.getId())); + + // 创建查找函数 + java.util.function.Function lookup = id -> { + if (id.equals(task1.getId())) return task1; + if (id.equals(task2.getId())) return task2; + if (id.equals(task3.getId())) return task3; + return null; + }; + + // 检测循环依赖 + assertTrue(task1.hasCyclicDependency(lookup), + "应该检测到循环依赖"); + + // 尝试添加循环依赖任务(应该失败) + assertThrows(Exception.class, () -> { + taskList.addTask(task1).join(); + taskList.addTask(task2).join(); + taskList.addTask(task3).get(); // 这会抛出异常 + }); + + System.out.println("✓ 循环依赖检测正常"); + } + + @Test + @Order(10) + @DisplayName("测试死循环检测 - 消息循环") + public void testMessageLoopPrevention() throws Exception { + System.out.println("\n=== 测试死循环检测 - 消息循环 ==="); + + AtomicInteger messageCount = new AtomicInteger(0); + final int MAX_MESSAGES = 5; + + // 注册处理器(模拟可能的循环) + messageChannel.registerHandler("loop-test", new MessageHandler() { + private boolean shouldRespond = true; + + @Override + public CompletableFuture handle(AgentMessage message) { + int count = messageCount.incrementAndGet(); + System.out.println("收到消息 #" + count + ": " + message.getContent()); + + if (count > MAX_MESSAGES) { + System.out.println("⚠️ 检测到可能的消息循环,停止响应"); + shouldRespond = false; + return CompletableFuture.completedFuture("STOP"); + } + + if (shouldRespond && "ping".equals(message.getType())) { + // 不再发送响应以避免循环 + return CompletableFuture.completedFuture("pong #" + count); + } + + return CompletableFuture.completedFuture("ACK"); + } + }); + + // 发送消息 + CompletableFuture response = messageChannel.request( + "sender", + "loop-test", + "ping", + "测试消息" + ); + + Object result = response.get(2, TimeUnit.SECONDS); + + assertNotNull(result); + assertTrue(messageCount.get() <= MAX_MESSAGES, + "消息数不应该超过限制 (实际: " + messageCount.get() + ")"); + + System.out.println("✓ 消息循环检测正常"); + } + + // ==================== 5. MainAgent 协调测试 ==================== + + @Test + @Order(11) + @DisplayName("测试 MainAgent 协调 - 任务分配") + public void testMainAgentTaskCoordination() throws Exception { + System.out.println("\n=== 测试 MainAgent 协调 - 任务分配 ==="); + + // 创建主任务 + TeamTask mainTask = TeamTask.builder() + .title("主任务: 实现用户认证") + .description("需要探索、开发、测试三个阶段") + .type(TeamTask.TaskType.DEVELOPMENT) + .priority(8) + .build(); + + taskList.addTask(mainTask).join(); + System.out.println("创建主任务: " + mainTask.getTitle()); + + // MainAgent 分解子任务 + TeamTask exploreSubTask = TeamTask.builder() + .title("探索现有代码") + .dependencies(Arrays.asList(mainTask.getId())) + .type(TeamTask.TaskType.EXPLORATION) + .build(); + + TeamTask devSubTask = TeamTask.builder() + .title("开发认证功能") + .dependencies(Arrays.asList(mainTask.getId())) + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + TeamTask testSubTask = TeamTask.builder() + .title("测试认证功能") + .dependencies(Arrays.asList(devSubTask.getId())) + .type(TeamTask.TaskType.TESTING) + .build(); + + taskList.addTask(exploreSubTask).join(); + taskList.addTask(devSubTask).join(); + taskList.addTask(testSubTask).join(); + + System.out.println("MainAgent 创建了 3 个子任务"); + + // 验证任务总数 + assertEquals(4, taskList.getAllTasks().size()); + + // 验证主任务没有依赖 + assertTrue(mainTask.getDependencies().isEmpty(), "主任务不应该有依赖"); + + // 验证 exploreSubTask 依赖于 mainTask + assertTrue(exploreSubTask.getDependencies().contains(mainTask.getId()), + "探索任务应该依赖于主任务"); + + // 验证 devSubTask 依赖于 mainTask + assertTrue(devSubTask.getDependencies().contains(mainTask.getId()), + "开发任务应该依赖于主任务"); + + // 验证 testSubTask 依赖于 devSubTask + assertTrue(testSubTask.getDependencies().contains(devSubTask.getId()), + "测试任务应该依赖于开发任务"); + + // 显示每个任务的依赖树 + System.out.println("\n--- 任务依赖关系 ---"); + for (TeamTask task : taskList.getAllTasks()) { + String tree = task.getDependencyTree(taskList::getTask); + System.out.println(tree); + } + + System.out.println("✓ MainAgent 任务协调正常"); + } + + @Test + @Order(12) + @DisplayName("测试 MainAgent 协调 - 结果汇总") + public void testMainAgentResultAggregation() throws Exception { + System.out.println("\n=== 测试 MainAgent 协调 - 结果汇总 ==="); + + // 创建任务并分配给不同 agents + TeamTask task1 = TeamTask.builder() + .title("数据分析任务") + .type(TeamTask.TaskType.ANALYSIS) + .build(); + + taskList.addTask(task1).join(); + + // Agent 1 完成任务 + String agent1 = "analysis-agent"; + taskList.claimTask(task1.getId(), agent1).join(); + + // 模拟工作完成 + Thread.sleep(100); + + task1.setStatus(TeamTask.Status.COMPLETED); + task1.setResult("分析结果: 发现 3 个性能问题"); + taskList.completeTask(task1.getId(), task1.getResult()); + + System.out.println(agent1 + " 完成任务,结果: " + task1.getResult()); + + // MainAgent 查询结果 + List completedTasks = taskList.getTasksByStatus(TeamTask.Status.COMPLETED); + assertFalse(completedTasks.isEmpty()); + + // 汇总结果 + StringBuilder summary = new StringBuilder("任务汇总:\n"); + for (TeamTask task : completedTasks) { + summary.append(" - ") + .append(task.getTitle()) + .append(": ") + .append(task.getResult()) + .append("\n"); + } + + System.out.println(summary); + assertTrue(summary.toString().contains("性能问题")); + System.out.println("✓ MainAgent 结果汇总正常"); + } + + // ==================== 6. 综合场景测试 ==================== + + @Test + @Order(13) + @DisplayName("综合场景 - 完整的协作流程") + public void testCompleteCollaborationScenario() throws Exception { + System.out.println("\n=== 综合场景 - 完整的协作流程 ==="); + + // 场景:实现一个新功能,需要多个 agent 协作 + // 1. Explore agent 探索代码库 + // 2. Plan agent 制定计划 + // 3. Dev agents 开发功能 + // 4. Test agent 测试功能 + // 5. MainAgent 汇总结果 + + System.out.println("步骤 1: Explore agent 探索代码库"); + TeamTask exploreTask = TeamTask.builder() + .title("探索用户管理模块") + .type(TeamTask.TaskType.EXPLORATION) + .build(); + taskList.addTask(exploreTask).join(); + + // Explore agent 认领并完成 + taskList.claimTask(exploreTask.getId(), "explore-agent").join(); + Thread.sleep(50); + exploreTask.setStatus(TeamTask.Status.COMPLETED); + exploreTask.setResult("找到 3 个相关文件,发现需要重构"); + System.out.println("✓ 探索完成"); + + // 存储到共享记忆 + LongTermMemory plan = new LongTermMemory( + "用户管理模块重构计划", + "explore-agent", + Arrays.asList("refactor", "user-management") + ); + plan.setImportance(0.9); + memoryManager.store(plan); + System.out.println("✓ 计划已存储到共享记忆"); + + System.out.println("步骤 2: Plan agent 制定详细计划"); + // 发布事件通知 + eventBus.publish(new AgentEvent( + AgentEventType.TASK_COMPLETED, + "探索完成", + EventMetadata.builder() + .sourceAgent("explore-agent") + .taskId(exploreTask.getId()) + .build() + )); + + System.out.println("步骤 3: 开发和测试任务"); + TeamTask devTask1 = TeamTask.builder() + .title("重构用户服务") + .dependencies(Arrays.asList(exploreTask.getId())) + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + TeamTask devTask2 = TeamTask.builder() + .title("更新数据库模型") + .dependencies(Arrays.asList(exploreTask.getId())) + .type(TeamTask.TaskType.DEVELOPMENT) + .build(); + + TeamTask testTask = TeamTask.builder() + .title("编写单元测试") + .dependencies(Arrays.asList(devTask1.getId(), devTask2.getId())) + .type(TeamTask.TaskType.TESTING) + .build(); + + taskList.addTask(devTask1).join(); + taskList.addTask(devTask2).join(); + taskList.addTask(testTask).join(); + + System.out.println("✓ 创建了开发任务和测试任务"); + + // 验证任务状态 + List allTasks = taskList.getAllTasks(); + System.out.println("总任务数: " + allTasks.size()); + + // 验证依赖关系 + java.util.function.Function lookup = taskList::getTask; + assertFalse(testTask.areAllDependenciesCompleted(lookup), + "测试任务的依赖未完成"); + + // 完成开发任务 + devTask1.setStatus(TeamTask.Status.COMPLETED); + taskList.completeTask(devTask1.getId(), "开发完成"); + devTask2.setStatus(TeamTask.Status.COMPLETED); + taskList.completeTask(devTask2.getId(), "开发完成"); + + System.out.println("✓ 开发任务完成"); + + // 发布完成事件 + eventBus.publish(new AgentEvent( + AgentEventType.TASK_COMPLETED, + "开发完成", + EventMetadata.builder() + .sourceAgent("dev-agent") + .taskId(devTask1.getId()) + .build() + )); + + System.out.println("步骤 4: 测试任务可认领"); + List pending = taskList.getPendingTasks(); + boolean testTaskPending = pending.stream() + .anyMatch(t -> t.getId().equals(testTask.getId())); + assertTrue(testTaskPending, "测试任务应该可认领"); + + System.out.println("\n协作流程统计:"); + System.out.println(" 总任务数: " + allTasks.size()); + System.out.println(" 已完成: " + taskList.getTasksByStatus(TeamTask.Status.COMPLETED).size()); + System.out.println(" 进行中: " + taskList.getTasksByStatus(TeamTask.Status.IN_PROGRESS).size()); + System.out.println(" 待认领: " + taskList.getPendingTasks().size()); + System.out.println(" Agent 负载: " + taskList.getAllAgentLoads()); + + System.out.println("✓ 综合协作场景测试通过"); + } + + // ==================== 辅助方法 ==================== + + private void claimTasksForAgent(String agentId, int taskCount) { + List claimable = taskList.getClaimableTasks(); + int claimed = 0; + + for (TeamTask task : claimable) { + if (claimed >= taskCount) break; + + try { + taskList.claimTask(task.getId(), agentId).join(); + System.out.println(agentId + " 认领: " + task.getTitle()); + claimed++; + } catch (Exception e) { + System.err.println("认领失败: " + e.getMessage()); + } + } + } +} diff --git a/soloncode-cli/src/test/java/agentTems/AgentTeamsDebugger.java b/soloncode-cli/src/test/java/agentTems/AgentTeamsDebugger.java new file mode 100644 index 0000000000000000000000000000000000000000..1e75eea7495a6f53369609468c6a57bdf9a4c013 --- /dev/null +++ b/soloncode-cli/src/test/java/agentTems/AgentTeamsDebugger.java @@ -0,0 +1,377 @@ +/* + * Copyright 2017-2026 noear.org and 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 agentTems; + +import lombok.extern.slf4j.Slf4j; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.memory.*; +import org.noear.solon.bot.core.event.*; +import org.noear.solon.bot.core.message.*; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * Agent Teams 功能调试程序 + * + * 用于测试共享记忆、事件总线、消息传递三大功能 + * + * @author bai + * @since 3.9.5 + */ +@Slf4j +public class AgentTeamsDebugger { + + private static final String TEST_WORK_DIR = "work/debug"; + + public static void main(String[] args) { + System.out.println("========================================"); + System.out.println(" Agent Teams 功能调试程序"); + System.out.println("========================================\n"); + + try { + // 测试共享记忆 + testSharedMemory(); + + // 测试事件总线 + testEventBus(); + + // 测试消息通道 + testMessageChannel(); + + // 测试配置 + testConfiguration(); + + System.out.println("\n========================================"); + System.out.println(" 所有测试通过!✅"); + System.out.println("========================================"); + + } catch (Exception e) { + System.err.println("\n========================================"); + System.err.println(" 测试失败!❌"); + System.err.println("========================================"); + e.printStackTrace(); + } + } + + /** + * 测试1: 共享记忆管理器 + */ + private static void testSharedMemory() throws Exception { + System.out.println("【测试1】共享记忆管理器"); + System.out.println("-----------------------------------"); + + // 创建管理器 + SharedMemoryManager manager = new SharedMemoryManager(Paths.get(TEST_WORK_DIR, AgentKernel.SOLONCODE_MEMORY)); + + // 1.1 存储短期记忆 + System.out.print("1. 存储短期记忆... "); + ShortTermMemory stm = new ShortTermMemory("explore", "探索用户认证模块", "task-001"); + manager.store(stm); + System.out.println("✅"); + + // 1.2 存储长期记忆 + System.out.print("2. 存储长期记忆... "); + LongTermMemory ltm = new LongTermMemory( + "系统采用 JWT + Redis 实现分布式会话", + "plan", + Arrays.asList("architecture", "auth", "jwt", "redis") + ); + ltm.setImportance(0.95); + manager.store(ltm); + System.out.println("✅"); + + // 1.3 存储知识库 + System.out.print("3. 存储知识库... "); + KnowledgeMemory km = new KnowledgeMemory( + "Spring Security", + "Spring 生态的安全框架,支持认证和授权", + "framework" + ); + km.setKeywords(Arrays.asList("spring", "security", "auth")); + manager.store(km); + System.out.println("✅"); + + // 1.4 检索短期记忆 + System.out.print("4. 检索短期记忆... "); + List shortTerm = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + assertTrue(shortTerm.size() >= 1, "应该至少有1条短期记忆"); + System.out.println("✅ (找到 " + shortTerm.size() + " 条)"); + + // 1.5 按标签检索 + System.out.print("5. 按标签检索... "); + List tagged = manager.retrieveByTag("architecture", 10); + assertTrue(tagged.size() >= 1, "应该找到包含 'architecture' 标签的记忆"); + System.out.println("✅ (找到 " + tagged.size() + " 条)"); + + // 1.6 全文搜索 + System.out.print("6. 全文搜索... "); + List searchResults = manager.search("JWT", 10); + assertTrue(searchResults.size() >= 1, "应该找到包含 'JWT' 的记忆"); + System.out.println("✅ (找到 " + searchResults.size() + " 条)"); + + // 1.7 获取统计信息 + System.out.print("7. 获取统计信息... "); + Map stats = manager.getStats(); + System.out.println("✅"); + System.out.println(" 统计: " + stats); + + manager.shutdown(); + System.out.println("-----------------------------------"); + System.out.println(); + } + + /** + * 测试2: 事件总线 + */ + private static void testEventBus() throws Exception { + System.out.println("【测试2】事件总线"); + System.out.println("-----------------------------------"); + + // 创建事件总线 + EventBus bus = new EventBus(2, 100); + + // 2.1 订阅和发布精确匹配事件 + System.out.print("1. 测试精确匹配... "); + CompletableFuture future1 = new CompletableFuture<>(); + bus.subscribe("test.event", event -> { + future1.complete((String) event.getPayload()); + System.out.println("接收到事件: " + event); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + EventMetadata metadata = EventMetadata.builder() + .sourceAgent("test-agent") + .taskId("test-task") + .priority(5) + .build(); + + bus.publish(new AgentEvent("test.event", "hello", metadata)); + String result1 = future1.get(2, TimeUnit.SECONDS); + assertEquals("hello", result1, "事件内容应该匹配"); + System.out.println("✅"); + + // 2.2 测试通配符订阅 + System.out.print("2. 测试通配符匹配... "); + CompletableFuture future2 = new CompletableFuture<>(); + bus.subscribe("task.*", event -> { + future2.complete(event.getEventTypeCode()); + System.out.println("接收到事件: " + event); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + bus.publish(new AgentEvent("task.completed", "data", metadata)); + String result2 = future2.get(2, TimeUnit.SECONDS); + assertEquals("task.completed", result2, "事件类型代码应该匹配"); + System.out.println("✅"); + + // 2.3 测试事件历史 + System.out.print("3. 测试事件历史... "); + List history = bus.getEventHistory(10); + assertTrue(history.size() >= 2, "应该有至少2条历史记录"); + System.out.println("✅ (历史记录: " + history.size() + " 条)"); + + // 2.4 测试订阅者数量 + System.out.print("4. 测试订阅者统计... "); + int count = bus.getSubscriberCount(); + assertTrue(count >= 2, "应该有至少2个订阅者"); + System.out.println("✅ (订阅者: " + count + " 个)"); + + // 2.5 测试取消订阅 + System.out.print("5. 测试取消订阅... "); + String subId = bus.subscribe("temp.event", e -> + CompletableFuture.completedFuture(EventHandler.Result.success()) + ); + bus.unsubscribe(subId); + System.out.println("✅"); + + bus.shutdown(); + System.out.println("-----------------------------------"); + System.out.println(); + } + + /** + * 测试3: 消息通道 + */ + private static void testMessageChannel() throws Exception { + System.out.println("【测试3】消息通道"); + System.out.println("-----------------------------------"); + + // 创建消息通道 + MessageChannel channel = new MessageChannel(TEST_WORK_DIR, 2); + + // 3.1 注册处理器并发送消息 + System.out.print("1. 测试点对点消息... "); + String handlerId = channel.registerHandler("receiver", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + System.out.println("收到消息: " + message); + return CompletableFuture.completedFuture("ACK"); + } + }); + + AgentMessage p2pMsg = new AgentMessage.Builder() + .from("sender") + .to("receiver") + .type("test.type") + .content("p2p test") + .requireAck(true) + .build(); + + MessageAck ack1 = channel.send(p2pMsg).get(2, TimeUnit.SECONDS); + assertTrue(ack1.isSuccess(), "消息应该投递成功"); + System.out.println("✅"); + + // 3.2 测试广播消息 + System.out.print("2. 测试广播消息... "); + channel.registerHandler("agent1", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + return CompletableFuture.completedFuture("ACK1"); + } + }); + channel.registerHandler("agent2", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + return CompletableFuture.completedFuture("ACK2"); + } + }); + + AgentMessage broadcastMsg = new AgentMessage.Builder() + .from("main") + .to("*") + .type("notification") + .content("broadcast test") + .build(); + + List acks = channel.broadcast(broadcastMsg).get(2, TimeUnit.SECONDS); + assertTrue(acks.size() >= 2, "应该至少有2个接收者"); + System.out.println("✅ (接收者: " + acks.size() + " 个)"); + + // 3.3 测试消息队列 + System.out.print("3. 测试离线消息队列... "); + channel.registerHandler("offline_agent", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + return CompletableFuture.completedFuture("RECEIVED"); + } + }); + + // 先发送消息(此时处理器已注册) + AgentMessage queuedMsg = new AgentMessage.Builder() + .from("main") + .to("offline_agent") + .type("query") + .content("queued message") + .persistent(true) + .ttl(60000) + .build(); + + MessageAck queuedAck = channel.send(queuedMsg).get(2, TimeUnit.SECONDS); + assertTrue(queuedAck.isSuccess(), "离线消息应该投递成功"); + System.out.println("✅"); + + // 3.4 测试待处理消息数量 + System.out.print("4. 测试统计信息... "); + int pendingCount = channel.getTotalPendingMessageCount(); + System.out.println("✅ (待处理消息: " + pendingCount + " 条)"); + + channel.shutdown(); + System.out.println("-----------------------------------"); + System.out.println(); + } + + /** + * 测试4: 配置属性 + */ + private static void testConfiguration() { + System.out.println("【测试4】配置属性"); + System.out.println("-----------------------------------"); + + org.noear.solon.bot.core.AgentProperties props = + new org.noear.solon.bot.core.AgentProperties(); + + // 4.1 验证默认值 + System.out.print("1. 验证默认配置... "); + assertFalse(props.sharedMemoryEnabled, "共享记忆默认应该未启用"); + assertFalse(props.eventBusEnabled, "事件总线默认应该未启用"); + assertFalse(props.messageChannelEnabled, "消息通道默认应该未启用"); + System.out.println("✅"); + + // 4.2 验证共享记忆配置 + System.out.print("2. 验证共享记忆配置... "); + assertNotNull(props.sharedMemory, "共享记忆配置对象不应为null"); + assertEquals(3600_000L, props.sharedMemory.shortTermTtl, "短期记忆TTL应该匹配"); + assertEquals(7 * 24 * 3600_000L, props.sharedMemory.longTermTtl, "长期记忆TTL应该匹配"); + assertTrue(props.sharedMemory.persistOnWrite, "写入时应该持久化"); + assertEquals(1000, props.sharedMemory.maxShortTermCount, "短期记忆最大数量应该匹配"); + System.out.println("✅"); + + // 4.3 验证事件总线配置 + System.out.print("3. 验证事件总线配置... "); + assertNotNull(props.eventBus, "事件总线配置对象不应为null"); + assertEquals(1000, props.eventBus.maxHistorySize, "事件历史最大数量应该匹配"); + assertEquals(5, props.eventBus.defaultPriority, "默认优先级应该匹配"); + assertEquals(30, props.eventBus.timeoutSeconds, "超时时间应该匹配"); + System.out.println("✅"); + + // 4.4 验证消息通道配置 + System.out.print("4. 验证消息通道配置... "); + assertNotNull(props.messageChannel, "消息通道配置对象不应为null"); + assertEquals(60_000L, props.messageChannel.defaultTtl, "默认TTL应该匹配"); + assertEquals(1000, props.messageChannel.maxQueueSize, "最大队列长度应该匹配"); + assertTrue(props.messageChannel.persistMessages, "消息应该持久化"); + System.out.println("✅"); + + System.out.println("-----------------------------------"); + System.out.println(); + } + + // ========== 辅助方法 ========== + + private static void assertTrue(boolean condition, String message) { + if (!condition) { + throw new AssertionError("断言失败: " + message); + } + } + + private static void assertFalse(boolean condition, String message) { + if (condition) { + throw new AssertionError("断言失败: " + message); + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + if (expected == null && actual == null) { + return; + } + if (expected != null && expected.equals(actual)) { + return; + } + throw new AssertionError("断言失败: " + message + + " (期望: " + expected + ", 实际: " + actual + ")"); + } + + private static void assertNotNull(Object obj, String message) { + if (obj == null) { + throw new AssertionError("断言失败: " + message); + } + } +} diff --git a/soloncode-cli/src/test/java/agentTems/AgentTeamsIntegrationTest.java b/soloncode-cli/src/test/java/agentTems/AgentTeamsIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..05e099c8f8588542bedea870b6cefc218b7a78fd --- /dev/null +++ b/soloncode-cli/src/test/java/agentTems/AgentTeamsIntegrationTest.java @@ -0,0 +1,922 @@ +/* + * Copyright 2017-2026 noear.org and 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 agentTems; + +import org.junit.jupiter.api.Test; +import org.noear.solon.bot.core.AgentKernel; +import org.noear.solon.bot.core.AgentProperties; +import org.noear.solon.bot.core.memory.*; +import org.noear.solon.bot.core.event.*; +import org.noear.solon.bot.core.message.*; +import org.noear.solon.bot.core.teams.SharedTaskList; +import org.noear.solon.bot.core.teams.TeamTask; +import org.noear.solon.bot.core.teams.SubAgentAgentBuilder; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Agent Teams 集成测试 + *

+ * 测试共享记忆、事件总线、消息传递、任务协作等完整功能 + * + * @author bai + * @since 3.9.5 + */ +public class AgentTeamsIntegrationTest { + + private static final String TEST_WORK_DIR = "work/test"; + + // ==================== 共享记忆测试 ==================== + + @Test + public void testSharedMemoryManager() { + System.out.println("=== 测试共享记忆管理器 ==="); + + // 创建管理器 + SharedMemoryManager manager = new SharedMemoryManager(Paths.get(TEST_WORK_DIR, AgentKernel.SOLONCODE_MEMORY)); + + // 1. 测试存储短期记忆 + ShortTermMemory stm = new ShortTermMemory("explore", "探索任务上下文", "task-1"); + manager.store(stm); + System.out.println("✓ 存储短期记忆"); + + // 2. 测试存储长期记忆 + LongTermMemory ltm = new LongTermMemory("重要架构决策", "plan", Arrays.asList("architecture", "design")); + ltm.setImportance(0.9); + manager.store(ltm); + System.out.println("✓ 存储长期记忆"); + + // 3. 测试存储知识库 + KnowledgeMemory km = new KnowledgeMemory("Solon 框架", "轻量级 Java 框架", "framework"); + km.setKeywords(Arrays.asList("java", "lightweight", "framework")); + manager.store(km); + System.out.println("✓ 存储知识库记忆"); + + // 4. 测试检索 + List memories = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + assertTrue(memories.size() >= 1); + System.out.println("✓ 检索到 " + memories.size() + " 条短期记忆"); + + // 5. 测试按标签检索 + List taggedMemories = manager.retrieveByTag("architecture", 10); + assertTrue(taggedMemories.size() >= 1); + System.out.println("✓ 按标签检索到 " + taggedMemories.size() + " 条记忆"); + + // 6. 测试搜索 + List searchResults = manager.search("架构", 10); + assertTrue(searchResults.size() >= 1); + System.out.println("✓ 搜索到 " + searchResults.size() + " 条相关记忆"); + + // 7. 测试统计信息 + Map stats = manager.getStats(); + System.out.println("✓ 记忆统计: " + stats); + + // 清理 + manager.shutdown(); + System.out.println("✓ 共享记忆管理器测试通过\n"); + } + + @Test + public void testEventBus() throws Exception { + System.out.println("=== 测试事件总线 ==="); + + // 创建事件总线 + EventBus bus = new EventBus(2, 100); + + // 1. 测试订阅和发布(使用枚举) + CompletableFuture future = new CompletableFuture<>(); + String subscriptionId = bus.subscribe(AgentEventType.TASK_STARTED, event -> { + future.complete((String) event.getPayload()); + System.out.println("接收到事件: " + event); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + System.out.println("✓ 订阅事件: " + subscriptionId); + + // 2. 发布事件 + EventMetadata metadata = EventMetadata.builder() + .sourceAgent("test") + .taskId("task-1") + .priority(5) + .build(); + AgentEvent event = new AgentEvent(AgentEventType.TASK_STARTED, "hello world", metadata); + bus.publish(event); + System.out.println("✓ 发布事件"); + + // 3. 验证收到事件 + String result = future.get(20, TimeUnit.SECONDS); + assertEquals("hello world", result); + System.out.println("✓ 接收到事件: " + result); + + // 4. 测试通配符订阅 + CompletableFuture wildcardFuture = new CompletableFuture<>(); + bus.subscribe("task.*", e -> { + wildcardFuture.complete(e.getEventTypeCode()); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + AgentEvent taskEvent = new AgentEvent(AgentEventType.TASK_COMPLETED, "data", metadata); + bus.publish(taskEvent); + + String eventType = wildcardFuture.get(2, TimeUnit.SECONDS); + assertEquals("task.completed", eventType); + System.out.println("✓ 通配符订阅成功: " + eventType); + + // 5. 测试事件历史 + List history = bus.getEventHistory(10); + assertTrue(history.size() >= 2); + System.out.println("✓ 事件历史记录: " + history.size() + " 条"); + + // 6. 测试订阅者数量 + int count = bus.getSubscriberCount(); + assertTrue(count >= 2); + System.out.println("✓ 订阅者数量: " + count); + + // 清理 + bus.shutdown(); + System.out.println("✓ 事件总线测试通过\n"); + } + + @Test + public void testMessageChannel() throws Exception { + System.out.println("=== 测试消息通道 ==="); + + // 创建消息通道 + MessageChannel channel = new MessageChannel(TEST_WORK_DIR, 2); + + // 1. 测试注册处理器 + String handlerId = channel.registerHandler("receiver", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + System.out.println(" → 收到消息: " + message.getContent()); + return CompletableFuture.completedFuture("ACK"); + } + }); + System.out.println("✓ 注册消息处理器: " + handlerId); + + // 2. 测试发送点对点消息 + AgentMessage message = new AgentMessage.Builder() + .from("sender") + .to("receiver") + .type("test.type") + .content("test payload") + .requireAck(true) + .build(); + + CompletableFuture future = channel.send(message); + MessageAck ack = future.get(2, TimeUnit.SECONDS); + assertTrue(ack.isSuccess()); + System.out.println("✓ 消息投递成功: " + ack); + + // 3. 测试广播消息 + // 注册多个接收者 + channel.registerHandler("agent1", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + System.out.println(" → 收到消息: " + message.getContent()); + return CompletableFuture.completedFuture("ACK1"); + } + }); + + channel.registerHandler("agent2", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + System.out.println(" → 收到消息: " + message.getContent()); + return CompletableFuture.completedFuture("ACK2"); + } + }); + + AgentMessage broadcastMsg = new AgentMessage.Builder() + .from("main") + .to("*") + .type("notification") + .content("broadcast test") + .build(); + + CompletableFuture> broadcastFuture = channel.broadcast(broadcastMsg); + List acks = broadcastFuture.get(2, TimeUnit.SECONDS); + assertTrue(acks.size() >= 2); + System.out.println("✓ 广播消息成功: " + acks.size() + " 个接收者"); + + // 4. 测试消息队列(离线消息) + channel.registerHandler("offline_agent", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + System.out.println(" → 收到消息: " + message.getContent()); + return CompletableFuture.completedFuture("RECEIVED"); + } + }); + AgentMessage queuedMsg = new AgentMessage.Builder<>() + .to("offline_agent") + .type("query") + .content("queued message") + .persistent(true) + .ttl(60000) + .build(); + + MessageAck queuedAck = channel.send(queuedMsg).get(2, TimeUnit.SECONDS); + assertTrue(queuedAck.isSuccess()); + System.out.println("✓ 离线消息队列成功"); + + // 清理 + channel.shutdown(); + System.out.println("✓ 消息通道测试通过\n"); + } + + @Test + public void testAgentCollaboration() throws Exception { + System.out.println("=== 测试代理协作 ==="); + + // 创建管理器 + SharedMemoryManager memoryManager = new SharedMemoryManager(Paths.get(TEST_WORK_DIR, AgentKernel.SOLONCODE_MEMORY)); + EventBus eventBus = new EventBus(2, 100); + MessageChannel messageChannel = new MessageChannel(TEST_WORK_DIR, 2); + + // 场景:Plan 代理制定计划 → 存储到共享记忆 → 发布事件 → Explore 代理接收事件 + String taskId = "task-collab-1"; + + // 1. 先订阅事件(在发布之前) + CompletableFuture exploreFuture = new CompletableFuture<>(); + eventBus.subscribe(AgentEventType.TASK_COMPLETED.getCode(), event -> { + if ("plan".equals(event.getMetadata().getSourceAgent())) { + // 检索相关记忆 + System.out.println("检索相关记忆"); + List memories = memoryManager.search("缓存", 5); + exploreFuture.complete("找到 " + memories.size() + " 条相关记忆"); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + } + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + System.out.println("✓ Explore 代理订阅事件"); + + // 2. Plan 代理存储计划到共享记忆 + LongTermMemory plan = new LongTermMemory( + "实现缓存系统的计划", + "plan", + Arrays.asList("cache", "architecture") + ); + plan.setImportance(0.95); + memoryManager.store(plan); + System.out.println("✓ Plan 代理存储计划到共享记忆"); + + // 3. Plan 代理发布完成事件(在订阅之后) + eventBus.publish(new AgentEvent( + AgentEventType.TASK_COMPLETED, + "计划已完成", + EventMetadata.builder() + .sourceAgent("plan") + .taskId(taskId) + .priority(5) + .build() + )); + System.out.println("✓ Plan 代理发布完成事件"); + + // 4. Explore 代理发送消息给 Plan 代理 + messageChannel.registerHandler("explore", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + return CompletableFuture.completedFuture("探索完成"); + } + }); + messageChannel.registerHandler("plan", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + if ("query".equals(message.getType())) { + CompletableFuture.runAsync(new Runnable() { + @Override + public void run() { + try { + System.out.println("模拟处理 → 收到消息: " + message.getContent()); + Thread.sleep(1000); // 模拟处理 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + return CompletableFuture.completedFuture("计划细节"); + } + return CompletableFuture.completedFuture("ACK"); + } + }); + + CompletableFuture queryResult = messageChannel.request( + "explore", "plan", "query", "需要探索的模块" + ); + Object result = queryResult.get(2, TimeUnit.SECONDS); + assertNotNull(result); + System.out.println("✓ 代理间消息传递成功"); + + // 5. 验证结果 + String exploreResult = exploreFuture.get(2, TimeUnit.SECONDS); + assertTrue(exploreResult.contains("相关记忆")); + System.out.println("✓ " + exploreResult); + + // 清理 + memoryManager.shutdown(); + eventBus.shutdown(); + messageChannel.shutdown(); + System.out.println("✓ 代理协作测试通过\n"); + } + + @Test + public void testConfigurationProperties() { + System.out.println("=== 测试配置属性 ==="); + + AgentProperties props = new AgentProperties(); + + // 验证默认值 + assertFalse(props.sharedMemoryEnabled); + assertFalse(props.eventBusEnabled); + assertFalse(props.messageChannelEnabled); + System.out.println("✓ 默认功能未启用"); + + // 验证共享记忆配置 + assertNotNull(props.sharedMemory); + assertEquals(3600_000L, props.sharedMemory.shortTermTtl); + assertEquals(7 * 24 * 3600_000L, props.sharedMemory.longTermTtl); + assertTrue(props.sharedMemory.persistOnWrite); + assertEquals(1000, props.sharedMemory.maxShortTermCount); + System.out.println("✓ 共享记忆配置正确"); + + // 验证事件总线配置 + assertNotNull(props.eventBus); + assertEquals(1000, props.eventBus.maxHistorySize); + assertEquals(5, props.eventBus.defaultPriority); + assertEquals(30, props.eventBus.timeoutSeconds); + System.out.println("✓ 事件总线配置正确"); + + // 验证消息通道配置 + assertNotNull(props.messageChannel); + assertEquals(60_000L, props.messageChannel.defaultTtl); + assertEquals(1000, props.messageChannel.maxQueueSize); + assertTrue(props.messageChannel.persistMessages); + System.out.println("✓ 消息通道配置正确"); + + System.out.println("✓ 配置属性测试通过\n"); + } + + // ==================== TeamTask 测试 ==================== + + @Test + public void testTeamTaskBasic() { + System.out.println("=== 测试 TeamTask 基本功能 ==="); + + // 1. 测试创建任务 + TeamTask task1 = new TeamTask("测试任务"); + assertNotNull(task1.getId()); + assertEquals("测试任务", task1.getTitle()); + assertEquals(TeamTask.Status.PENDING, task1.getStatus()); + assertEquals(5, task1.getPriority()); + System.out.println("✓ 创建任务成功: " + task1.getId()); + + // 2. 测试使用 Builder 创建任务 + TeamTask task2 = TeamTask.builder() + .title("Builder 任务") + .description("使用 Builder 创建") + .priority(8) + .type(TeamTask.TaskType.EXPLORATION) + .build(); + assertEquals("Builder 任务", task2.getTitle()); + assertEquals(8, task2.getPriority()); + assertEquals(TeamTask.TaskType.EXPLORATION, task2.getType()); + System.out.println("✓ Builder 创建任务成功"); + + // 3. 测试任务状态转换 + task1.setStatus(TeamTask.Status.IN_PROGRESS); + assertEquals(TeamTask.Status.IN_PROGRESS, task1.getStatus()); + assertFalse(task1.isClaimable()); + + task1.setStatus(TeamTask.Status.COMPLETED); + assertEquals(TeamTask.Status.COMPLETED, task1.getStatus()); + assertTrue(task1.isCompleted()); + System.out.println("✓ 任务状态转换成功"); + + // 4. 测试任务元数据 + task1.putMetadata("key1", "value1"); + task1.putMetadata("key2", "value2"); + assertEquals("value1", task1.getMetadata().get("key1")); + assertEquals("value2", task1.getMetadata().get("key2")); + System.out.println("✓ 任务元数据操作成功"); + + System.out.println("✓ TeamTask 基本功能测试通过\n"); + } + + @Test + public void testTeamTaskDependencies() { + System.out.println("=== 测试任务依赖关系 ==="); + + // 创建任务链: task1 -> task2 -> task3 + TeamTask task1 = TeamTask.builder() + .title("任务1") + .build(); + + TeamTask task2 = TeamTask.builder() + .title("任务2") + .dependencies(Arrays.asList(task1.getId())) + .build(); + + TeamTask task3 = TeamTask.builder() + .title("任务3") + .dependencies(Arrays.asList(task2.getId())) + .build(); + + // 创建任务查找函数 + java.util.function.Function taskLookup = id -> { + if (id.equals(task1.getId())) return task1; + if (id.equals(task2.getId())) return task2; + if (id.equals(task3.getId())) return task3; + return null; + }; + + // 测试依赖检查 + assertTrue(task3.areAllDependenciesCompleted(taskLookup) == false); + + // 完成 task1 + task1.setStatus(TeamTask.Status.COMPLETED); + assertTrue(task2.areAllDependenciesCompleted(taskLookup)); + + // 但 task3 仍然不能完成(task2 未完成) + assertTrue(task3.areAllDependenciesCompleted(taskLookup) == false); + + // 完成 task2 + task2.setStatus(TeamTask.Status.COMPLETED); + assertTrue(task3.areAllDependenciesCompleted(taskLookup)); + System.out.println("✓ 依赖关系检查正确"); + + // 测试获取所有依赖 ID + java.util.Set allDeps = task3.getAllDependencyIds(taskLookup); + assertTrue(allDeps.contains(task1.getId())); + assertTrue(allDeps.contains(task2.getId())); + System.out.println("✓ 获取所有依赖 ID 成功,共 " + allDeps.size() + " 个"); + + System.out.println("✓ 任务依赖关系测试通过\n"); + } + + @Test + public void testTeamTaskCircularDependency() { + System.out.println("=== 测试循环依赖检测 ==="); + + // 创建循环依赖: task1 -> task2 -> task3 -> task1 + TeamTask task1 = TeamTask.builder() + .title("任务1") + .build(); + + TeamTask task2 = TeamTask.builder() + .title("任务2") + .dependencies(Arrays.asList(task1.getId())) + .build(); + + TeamTask task3 = TeamTask.builder() + .title("任务3") + .dependencies(Arrays.asList(task2.getId())) + .build(); + + // 添加循环依赖 + task1.setDependencies(Arrays.asList(task3.getId())); + + // 创建任务查找函数 + java.util.function.Function taskLookup = id -> { + if (id.equals(task1.getId())) return task1; + if (id.equals(task2.getId())) return task2; + if (id.equals(task3.getId())) return task3; + return null; + }; + + // 测试循环依赖检测 + assertTrue(task1.hasCyclicDependency(taskLookup)); + System.out.println("✓ 成功检测到循环依赖"); + + // 测试依赖树可视化 + String tree = task1.getDependencyTree(taskLookup); + assertTrue(tree.contains("⚠️")); + System.out.println("依赖树:\n" + tree); + + System.out.println("✓ 循环依赖检测测试通过\n"); + } + + @Test + public void testTeamTaskDependencyTree() { + System.out.println("=== 测试依赖树可视化 ==="); + + // 创建复杂依赖结构 + TeamTask root = TeamTask.builder().title("根任务").build(); + TeamTask child1 = TeamTask.builder().title("子任务1").build(); + TeamTask child2 = TeamTask.builder().title("子任务2").build(); + TeamTask grandchild = TeamTask.builder().title("孙任务").build(); + + // 设置依赖 + child1.setDependencies(Arrays.asList(root.getId())); + child2.setDependencies(Arrays.asList(root.getId())); + grandchild.setDependencies(Arrays.asList(child1.getId())); + + // 更新状态 + root.setStatus(TeamTask.Status.COMPLETED); + child1.setStatus(TeamTask.Status.IN_PROGRESS); + + // 创建任务查找函数 + java.util.function.Function taskLookup = id -> { + if (id.equals(root.getId())) return root; + if (id.equals(child1.getId())) return child1; + if (id.equals(child2.getId())) return child2; + if (id.equals(grandchild.getId())) return grandchild; + return null; + }; + + // 生成依赖树 + String tree = grandchild.getDependencyTree(taskLookup); + System.out.println("依赖树:\n" + tree); + + assertTrue(tree.contains("根任务")); + assertTrue(tree.contains("子任务1")); + assertTrue(tree.contains("孙任务")); + assertTrue(tree.contains("✅") || tree.contains("[DONE]")); + System.out.println("✓ 依赖树可视化成功"); + + System.out.println("✓ 依赖树测试通过\n"); + } + + // ==================== SharedTaskList 测试 ==================== + + @Test + public void testSharedTaskList() throws Exception { + System.out.println("=== 测试共享任务列表 ==="); + + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + + // 1. 测试添加单个任务 + TeamTask task1 = TeamTask.builder() + .title("任务1") + .priority(8) + .build(); + taskList.addTask(task1).join(); // 等待异步完成 + assertEquals(1, taskList.getAllTasks().size()); + System.out.println("✓ 添加单个任务成功"); + + // 2. 测试批量添加任务 + List tasks = Arrays.asList( + TeamTask.builder().title("任务2").build(), + TeamTask.builder().title("任务3").build(), + TeamTask.builder().title("任务4").build() + ); + List added = taskList.addTasks(tasks).join(); + assertEquals(3, added.size()); + assertEquals(4, taskList.getAllTasks().size()); + System.out.println("✓ 批量添加任务成功"); + + // 3. 测试获取任务 + TeamTask found = taskList.getTask(task1.getId()); + assertNotNull(found); + assertEquals(task1.getTitle(), found.getTitle()); + System.out.println("✓ 获取任务成功"); + + // 4. 测试任务统计 + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + assertEquals(4, stats.totalTasks); + assertEquals(4, stats.pendingTasks); + assertEquals(0, stats.completedTasks); + System.out.println("✓ 任务统计: " + stats); + + // 5. 测试任务认领 + List claimableTasks = taskList.getClaimableTasks(); + assertFalse(claimableTasks.isEmpty(), "应该有可认领的任务"); + TeamTask claimable = claimableTasks.get(0); + boolean claimResult = taskList.claimTask(claimable.getId(), "agent-1").get(); + assertTrue(claimResult); + TeamTask claimed = taskList.getTask(claimable.getId()); + assertNotNull(claimed); + assertEquals("agent-1", claimed.getClaimedBy()); + assertEquals(TeamTask.Status.IN_PROGRESS, claimed.getStatus()); + System.out.println("✓ 任务认领成功"); + + // 6. 测试任务完成 + boolean completeResult = taskList.completeTask(claimable.getId(), "任务完成"); + assertTrue(completeResult); + System.out.println("✓ 任务完成"); + + // 等待一小段时间确保状态更新 + Thread.sleep(100); + + // 验证统计更新 + SharedTaskList.TaskStatistics newStats = taskList.getStatistics(); + assertEquals(1, newStats.completedTasks); + assertEquals(3, newStats.pendingTasks); + System.out.println("✓ 统计更新正确"); + + System.out.println("✓ 共享任务列表测试通过\n"); + } + + @Test + public void testSharedTaskListWithDependencies() throws Exception { + System.out.println("=== 测试带依赖的任务列表 ==="); + + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + + // 创建带依赖的任务链: task1 -> task2 -> task3 + TeamTask task1 = TeamTask.builder().title("基础任务").build(); + TeamTask task2 = TeamTask.builder() + .title("中级任务") + .dependencies(Arrays.asList(task1.getId())) + .build(); + TeamTask task3 = TeamTask.builder() + .title("高级任务") + .dependencies(Arrays.asList(task2.getId())) + .build(); + + taskList.addTasks(Arrays.asList(task1, task2, task3)).join(); + + // 测试可认领任务(只有 task1 可以认领) + List claimable = taskList.getClaimableTasks(); + assertEquals(1, claimable.size()); + assertEquals(task1.getId(), claimable.get(0).getId()); + System.out.println("✓ 只有基础任务可认领"); + + // 完成 task1 + taskList.completeTask(task1.getId(), "完成"); + + // 现在 task2 应该可以认领了 + List newClaimable = taskList.getClaimableTasks(); + assertTrue(newClaimable.stream().anyMatch(t -> t.getId().equals(task2.getId()))); + System.out.println("✓ 基础任务完成后,中级任务可认领"); + + // 测试阻塞信息 + String blockingInfo = taskList.getBlockingInfo(); + assertTrue(blockingInfo.contains("高级任务") || blockingInfo.contains("等待")); + System.out.println("阻塞信息:\n" + blockingInfo); + + System.out.println("✓ 带依赖的任务列表测试通过\n"); + } + + @Test + public void testSharedTaskListBatchOperations() throws Exception { + System.out.println("=== 测试批量操作 ==="); + + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + + // 1. 批量添加大量任务 + List largeBatch = new java.util.ArrayList<>(); + for (int i = 0; i < 50; i++) { + largeBatch.add(TeamTask.builder() + .title("批量任务-" + i) + .priority(i % 10) + .build()); + } + + long startTime = System.currentTimeMillis(); + taskList.addTasks(largeBatch).join(); + long duration = System.currentTimeMillis() - startTime; + + assertEquals(50, taskList.getAllTasks().size()); + System.out.println("✓ 批量添加 50 个任务,耗时: " + duration + "ms"); + + // 2. 测试高优先级任务排序 + List highPriorityTasks = taskList.getAllTasks().stream() + .filter(t -> t.getPriority() >= 9) + .collect(java.util.stream.Collectors.toList()); + assertTrue(highPriorityTasks.size() >= 5); + for (TeamTask task : highPriorityTasks) { + assertEquals(9, task.getPriority()); + } + System.out.println("✓ 高优先级任务筛选成功,共 " + highPriorityTasks.size() + " 个"); + + // 3. 批量更新状态 + for (TeamTask task : taskList.getAllTasks()) { + if (task.getPriority() >= 8) { + taskList.completeTask(task.getId(), "批量完成"); + } + } + + SharedTaskList.TaskStatistics stats = taskList.getStatistics(); + assertTrue(stats.completedTasks >= 5); + System.out.println("✓ 批量更新完成,已完成: " + stats.completedTasks); + + System.out.println("✓ 批量操作测试通过\n"); + } + + // ==================== SubAgentAgentBuilder 测试 ==================== + + @Test + public void testSubAgentAgentBuilder() { + System.out.println("=== 测试 SubAgentAgentBuilder ==="); + + // 注意:这个测试需要实际的 ChatModel、AgentSessionProvider、PoolManager + // 这里只测试 Builder 的结构和方法链 + + // 测试静态工厂方法 + assertNotNull(SubAgentAgentBuilder.class); + System.out.println("✓ SubAgentAgentBuilder 类存在"); + + // 检查是否有必要的方法 + boolean hasBuilder = false; + boolean hasOfMethod = false; + boolean hasBuildMethod = false; + + try { + SubAgentAgentBuilder.builder(); + hasBuilder = true; + System.out.println("✓ builder() 方法存在"); + } catch (Exception e) { + System.out.println("✗ builder() 方法调用失败(可能需要实际依赖)"); + } + + try { + java.lang.reflect.Method ofMethod = SubAgentAgentBuilder.class.getMethod("of", org.noear.solon.ai.chat.ChatModel.class); + hasOfMethod = true; + System.out.println("✓ of() 方法存在"); + } catch (Exception e) { + System.out.println("✗ of() 方法不存在"); + } + + try { + java.lang.reflect.Method buildMethod = SubAgentAgentBuilder.class.getMethod("build"); + hasBuildMethod = true; + System.out.println("✓ build() 方法存在"); + } catch (Exception e) { + System.out.println("✗ build() 方法不存在"); + } + + assertTrue(hasBuilder || hasOfMethod); + assertTrue(hasBuildMethod); + + System.out.println("✓ SubAgentAgentBuilder 测试通过\n"); + } + + // ==================== 边界情况和错误处理测试 ==================== + + @Test + public void testErrorHandling() { + System.out.println("=== 测试错误处理 ==="); + + // 1. 测试重复添加任务 + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + + TeamTask task1 = TeamTask.builder() + .title("任务1") + .build(); + taskList.addTask(task1); + + // 尝试添加相同 ID 的任务 + assertThrows(Exception.class, () -> { + taskList.addTask(task1); // 应该抛出异常 + }); + System.out.println("✓ 重复添加任务被拒绝"); + + // 2. 测试认领不存在的任务 + try { + boolean claimResult = taskList.claimTask("non-existent-id", "agent-1").get(); + assertFalse(claimResult); + System.out.println("✓ 认领不存在的任务返回 false"); + } catch (Exception e) { + // 期望异常 + assertTrue(true); + } + + // 3. 测试获取不存在的任务 + TeamTask found = taskList.getTask("non-existent-id"); + assertNull(found); + System.out.println("✓ 获取不存在的任务返回 null"); + + // 4. 测试空任务列表操作 + SharedTaskList emptyList = new SharedTaskList(eventBus); + assertTrue(emptyList.getAllTasks().isEmpty()); + assertTrue(emptyList.getClaimableTasks().isEmpty()); + System.out.println("✓ 空任务列表操作正常"); + + System.out.println("✓ 错误处理测试通过\n"); + } + + @Test + public void testConcurrentOperations() throws Exception { + System.out.println("=== 测试并发操作 ==="); + + EventBus eventBus = new EventBus(); + SharedTaskList taskList = new SharedTaskList(eventBus); + + // 1. 并发添加任务 + java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(10); + List> futures = new java.util.ArrayList<>(); + + for (int i = 0; i < 100; i++) { + final int index = i; + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + TeamTask task = TeamTask.builder() + .title("并发任务-" + index) + .build(); + taskList.addTask(task); + } catch (Exception e) { + // 某些可能会失败(重复 ID 等) + } + }, executor); + futures.add(future); + } + + // 等待所有操作完成 + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); + + // 验证结果 + int taskCount = taskList.getAllTasks().size(); + assertTrue(taskCount > 0); + System.out.println("✓ 并发添加 " + taskCount + " 个任务成功"); + + // 2. 并发认领任务 + List claimableTasks = taskList.getClaimableTasks(); + if (!claimableTasks.isEmpty()) { + List> claimFutures = new java.util.ArrayList<>(); + + for (int i = 0; i < Math.min(10, claimableTasks.size()); i++) { + final String taskId = claimableTasks.get(i).getId(); + CompletableFuture claimFuture = taskList.claimTask(taskId, "agent-" + Thread.currentThread().getId()); + claimFutures.add(claimFuture); + } + + // 等待认领完成 + CompletableFuture.allOf(claimFutures.toArray(new CompletableFuture[0])).get(); + + long claimedCount = claimFutures.stream() + .map(f -> { + try { + return f.get(); + } catch (Exception e) { + return false; + } + }) + .filter(result -> result) + .count(); + System.out.println("✓ 并发认领 " + claimedCount + " 个任务"); + } + + executor.shutdown(); + System.out.println("✓ 并发操作测试通过\n"); + } + + @Test + public void testMemoryTTL() throws Exception { + System.out.println("=== 测试记忆 TTL ==="); + + // 创建短期 TTL 的管理器(1 秒) + SharedMemoryManager manager = new SharedMemoryManager( + Paths.get(TEST_WORK_DIR, AgentKernel.SOLONCODE_MEMORY), + 1000L, // 1 秒 TTL + 7000L, // 7 秒 TTL + 100L, // 快速清理 + true, + 100, + 50 + ); + + // 存储短期记忆(使用工厂方法,自动应用配置的 TTL) + ShortTermMemory stm = manager.createShortTermMemory("agent1", "测试内容", "task-1"); + long createTime = stm.getTimestamp(); + System.out.println("记忆创建时间: " + createTime); + System.out.println("记忆 TTL: " + stm.getTtl() + "ms"); + + manager.store(stm); + System.out.println("✓ 存储短期记忆 (TTL=1000ms)"); + + // 立即检索(应该能找到) + List memories1 = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + System.out.println("立即检索到 " + memories1.size() + " 条记忆"); + assertFalse(memories1.isEmpty(), "应该能检索到刚创建的记忆"); + + // 等待 TTL 过期(多等一些时间确保过期) + Thread.sleep(2000); // 改为 2 秒,确保足够超过 1 秒 TTL + + // 检查记忆是否已过期 + boolean isExpired = stm.isExpired(); + long elapsedTime = System.currentTimeMillis() - createTime; + System.out.println("经过时间: " + elapsedTime + "ms"); + System.out.println("记忆是否过期: " + isExpired); + + // 再次检索(应该找不到) + List memories2 = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + System.out.println("过期后检索到 " + memories2.size() + " 条记忆"); + assertTrue(memories2.isEmpty(), "过期后应该检索不到记忆"); + System.out.println("✓ TTL 过期后无法检索到记忆"); + + manager.shutdown(); + System.out.println("✓ 记忆 TTL 测试通过\n"); + } +} diff --git a/soloncode-cli/src/test/java/agentTems/EventBusDebugTest.java b/soloncode-cli/src/test/java/agentTems/EventBusDebugTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5037e8e2a7474b9ed1f33ae21d858f535b1b912b --- /dev/null +++ b/soloncode-cli/src/test/java/agentTems/EventBusDebugTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2017-2026 noear.org and 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 agentTems; + +import org.junit.jupiter.api.Test; +import org.noear.solon.bot.core.event.AgentEvent; +import org.noear.solon.bot.core.event.AgentEventType; +import org.noear.solon.bot.core.event.EventHandler; +import org.noear.solon.bot.core.event.EventBus; +import org.noear.solon.bot.core.event.EventMetadata; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * EventBus 调试测试 + * + * @author bai + * @since 3.9.5 + */ +public class EventBusDebugTest { + + @Test + public void testBasicEventPublish() throws Exception { + System.out.println("=== 基础事件发布测试 ==="); + + EventBus eventBus = new EventBus(); + + AtomicInteger handlerCallCount = new AtomicInteger(0); + CompletableFuture eventReceived = new CompletableFuture<>(); + + // 订阅事件 + String subscriptionId = eventBus.subscribe(AgentEventType.TASK_CREATED, event -> { + int count = handlerCallCount.incrementAndGet(); + System.out.println("Handler 被调用 #" + count); + System.out.println(" Event Type: " + event.getEventType()); + System.out.println(" Payload: " + event.getPayload()); + System.out.println(" Source Agent: " + event.getMetadata().getSourceAgent()); + + eventReceived.complete((String) event.getPayload()); + + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + System.out.println("订阅ID: " + subscriptionId); + System.out.println("订阅者数量: " + eventBus.getSubscriberCount(AgentEventType.TASK_CREATED)); + + // 等待订阅生效 + Thread.sleep(200); + + // 发布事件 + System.out.println("发布事件..."); + AgentEvent event = new AgentEvent( + AgentEventType.TASK_CREATED, + "测试任务", + EventMetadata.builder() + .sourceAgent("test-agent") + .taskId("task-1") + .build() + ); + + eventBus.publish(event); + System.out.println("事件已发布"); + + // 等待处理器接收 + String result = eventReceived.get(3, TimeUnit.SECONDS); + System.out.println("收到结果: " + result); + + assertNotNull(result); + assertEquals("测试任务", result); + assertEquals(1, handlerCallCount.get()); + + eventBus.shutdown(); + System.out.println("✓ 测试通过"); + } + + @Test + public void testMultipleSubscribers() throws Exception { + System.out.println("\n=== 多订阅者测试 ==="); + + EventBus eventBus = new EventBus(); + + AtomicInteger handler1Count = new AtomicInteger(0); + AtomicInteger handler2Count = new AtomicInteger(0); + + // 订阅者1 + eventBus.subscribe(AgentEventType.TASK_PROGRESS, event -> { + handler1Count.incrementAndGet(); + System.out.println("订阅者1 收到事件"); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + // 订阅者2 + eventBus.subscribe(AgentEventType.TASK_PROGRESS, event -> { + handler2Count.incrementAndGet(); + System.out.println("订阅者2 收到事件"); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + System.out.println("订阅者数量: " + eventBus.getSubscriberCount(AgentEventType.TASK_PROGRESS)); + Thread.sleep(200); + + // 发布事件 + AgentEvent event = new AgentEvent( + AgentEventType.TASK_PROGRESS, + "任务进度更新", + EventMetadata.builder() + .sourceAgent("main-agent") + .taskId("task-2") + .build() + ); + + eventBus.publish(event); + System.out.println("事件已发布"); + + // 等待处理器执行 + Thread.sleep(500); + + assertEquals(1, handler1Count.get(), "订阅者1应该被调用1次"); + assertEquals(1, handler2Count.get(), "订阅者2应该被调用1次"); + + eventBus.shutdown(); + System.out.println("✓ 测试通过"); + } + + @Test + public void testAsyncPublish() throws Exception { + System.out.println("\n=== 异步发布测试 ==="); + + EventBus eventBus = new EventBus(); + + CompletableFuture eventReceived = new CompletableFuture<>(); + + eventBus.subscribe(AgentEventType.TASK_COMPLETED, event -> { + System.out.println("收到完成事件: " + event.getPayload()); + eventReceived.complete((String) event.getPayload()); + return CompletableFuture.completedFuture(EventHandler.Result.success()); + }); + + Thread.sleep(200); + + // 异步发布 + AgentEvent event = new AgentEvent( + AgentEventType.TASK_COMPLETED, + "任务已完成", + EventMetadata.builder() + .sourceAgent("worker-agent") + .taskId("task-3") + .build() + ); + + CompletableFuture publishFuture = eventBus.publishAsync(event); + System.out.println("异步发布已启动"); + + // 等待发布完成 + publishFuture.get(2, TimeUnit.SECONDS); + System.out.println("发布完成"); + + // 等待接收 + String result = eventReceived.get(2, TimeUnit.SECONDS); + assertEquals("任务已完成", result); + + eventBus.shutdown(); + System.out.println("✓ 测试通过"); + } +} diff --git a/soloncode-cli/src/test/java/agentTems/MemoryStoreExample.java b/soloncode-cli/src/test/java/agentTems/MemoryStoreExample.java new file mode 100644 index 0000000000000000000000000000000000000000..dea566e82a6ae9574c89942ad2f3df525d99ed4a --- /dev/null +++ b/soloncode-cli/src/test/java/agentTems/MemoryStoreExample.java @@ -0,0 +1,312 @@ +/* + * Copyright 2017-2026 noear.org and 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 agentTems; + +import org.noear.solon.bot.core.memory.*; +import org.noear.solon.bot.core.memory.bank.Observation; +import org.noear.solon.bot.core.memory.bank.store.FileMemoryStore; +import org.noear.solon.bot.core.memory.bank.store.MemoryStore; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * MemoryStore 使用示例 + * + * 演示如何使用不同的记忆存储方式 + * + * @author bai + * @since 3.9.5 + */ +public class MemoryStoreExample { + + /** + * 示例 1:直接使用 FileMemoryStore(底层存储) + */ + public static void example1_FileMemoryStore() throws Exception { + System.out.println("=== 示例 1:直接使用 FileMemoryStore ==="); + + // 创建临时目录 + Path tempDir = Files.createTempDirectory("memory_example_"); + String storePath = tempDir.toAbsolutePath().toString(); + + // 创建 FileMemoryStore + MemoryStore store = new FileMemoryStore(storePath); + + // 创建 Observation(底层存储单位) + Observation obs = new Observation(); + obs.setId("obs-" + UUID.randomUUID()); + obs.setContent("测试 Observation 内容"); + obs.setImportance(8.0); + obs.setTimestamp(System.currentTimeMillis()); + + // 存储 + store.store(obs); + System.out.println("Observation 已存储: " + obs.getId()); + + // 加载 + Observation loaded = store.load(obs.getId()); + System.out.println("Observation 加载: " + (loaded != null ? "成功" : "失败")); + + // 获取统计信息 + System.out.println("统计信息: " + store.getStats()); + + // 清理 + store.clear(); + System.out.println("已清理临时目录: " + tempDir); + } + + /** + * 示例 2:使用 SharedMemoryManager(推荐方式) + */ + public static void example2_SharedMemoryManager() throws Exception { + System.out.println("\n=== 示例 2:使用 SharedMemoryManager(推荐)==="); + + // 创建临时目录 + Path tempDir = Files.createTempDirectory("shared_memory_"); + + // 创建 SharedMemoryManager(高层 API) + SharedMemoryManager manager = new SharedMemoryManager(tempDir); + + // 创建短期记忆 + ShortTermMemory shortMemory = manager.createShortTermMemory( + "agent1", + "这是一个短期记忆,会自动过期", + "task-1" + ); + shortMemory.putMetadata("测试", "示例"); + shortMemory.putMetadata("标签", "测试标签"); + manager.store(shortMemory); + System.out.println("短期记忆已存储: " + shortMemory.getId()); + + // 创建长期记忆 + LongTermMemory longMemory = manager.createLongTermMemory( + "这是一个长期记忆", + "agent1", + Arrays.asList("重要", "长期") + ); + longMemory.setImportance(9.0); // 设置重要性 + longMemory.putMetadata("taskId", "task-1"); + manager.store(longMemory); + System.out.println("长期记忆已存储: " + longMemory.getId()); + + // 创建工作记忆(结构化数据) + WorkingMemory working = new WorkingMemory("goal-1"); + working.setTaskId("task-1"); + working.setCurrentAgent("main"); + working.setStatus("进行中"); + working.setTaskDescription("完成记忆系统测试"); + manager.storeWorking(working); + System.out.println("工作记忆已存储: " + working.getId()); + + // 等待异步存储完成 + Thread.sleep(200); + + // 检索记忆 + List shortMemories = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + System.out.println("检索到短期记忆: " + shortMemories.size() + " 条"); + + List longMemories = manager.retrieve(Memory.MemoryType.LONG_TERM, 10); + System.out.println("检索到长期记忆: " + longMemories.size() + " 条"); + + // 按标签检索 + List tagged = manager.retrieveByTag("重要", 10); + System.out.println("按标签[重要]检索: " + tagged.size() + " 条"); + + // 获取工作记忆 + WorkingMemory loadedWorking = manager.getWorking("goal-1"); + System.out.println("工作记忆状态: " + (loadedWorking != null ? loadedWorking.getStatus() : "未找到")); + + // 关闭管理器 + manager.shutdown(); + + // 清理 + System.out.println("已清理临时目录"); + } + + /** + * 示例 3:记忆的生命周期管理 + */ + public static void example3_MemoryLifecycle() throws Exception { + System.out.println("\n=== 示例 3:记忆的生命周期管理 ==="); + + // 创建临时目录 + Path tempDir = Files.createTempDirectory("memory_lifecycle_"); + + // 创建 SharedMemoryManager(设置较短的 TTL) + SharedMemoryManager manager = new SharedMemoryManager( + tempDir, + 2000L, // 短期记忆 2 秒过期 + 5000L, // 长期记忆 5 秒过期 + 1000L, // 每秒清理一次 + true, // 立即持久化 + 100, // 最多 100 条短期记忆 + 50 // 最多 50 条长期记忆 + ); + + // 创建记忆 + ShortTermMemory memory1 = manager.createShortTermMemory( + "agent1", + "这个记忆会在 2 秒后过期", + "task-1" + ); + manager.store(memory1); + System.out.println("记忆已创建: " + memory1.getId()); + + // 立即检索(应该能找到) + List immediate = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + System.out.println("立即检索: " + immediate.size() + " 条"); + + // 等待 3 秒(应该已经过期) + System.out.println("等待 3 秒..."); + Thread.sleep(3000); + + // 再次检索(应该为空) + List afterExpiry = manager.retrieve(Memory.MemoryType.SHORT_TERM, 10); + System.out.println("过期后检索: " + afterExpiry.size() + " 条"); + + // 关闭管理器 + manager.shutdown(); + System.out.println("生命周期示例完成"); + } + + /** + * 示例 4:永久记忆(KnowledgeMemory) + */ + public static void example4_KnowledgeMemory() throws Exception { + System.out.println("\n=== 示例 4:永久记忆 ==="); + + // 创建临时目录 + Path tempDir = Files.createTempDirectory("knowledge_memory_"); + + // 创建 SharedMemoryManager + SharedMemoryManager manager = new SharedMemoryManager(tempDir); + + // 创建永久记忆(不会过期) + KnowledgeMemory knowledge = new KnowledgeMemory( + "系统架构说明", + "这是一个重要的架构文档,需要永久保存", + "架构文档" + ); + knowledge.putMetadata("agentId", "agent1"); + knowledge.putMetadata("来源", "用户输入"); + knowledge.setKeywords(Arrays.asList("架构", "文档", "重要")); + manager.store(knowledge); + System.out.println("永久记忆已创建: " + knowledge.getId()); + + // 等待存储完成 + Thread.sleep(200); + + // 检索永久记忆 + List knowledges = manager.retrieve(Memory.MemoryType.KNOWLEDGE, 10); + System.out.println("检索到永久记忆: " + knowledges.size() + " 条"); + + if (!knowledges.isEmpty()) { + KnowledgeMemory loaded = (KnowledgeMemory) knowledges.get(0); + System.out.println("内容: " + loaded.getContent()); + System.out.println("分类: " + loaded.getCategory()); + System.out.println("关键词: " + loaded.getKeywords()); + System.out.println("元数据-来源: " + loaded.getMetadata("来源")); + } + + // 关闭管理器 + manager.shutdown(); + System.out.println("永久记忆示例完成"); + } + + /** + * 示例 5:跨代理共享记忆 + */ + public static void example5_SharedMemory() throws Exception { + System.out.println("\n=== 示例 5:跨代理共享记忆 ==="); + + // 创建临时目录 + Path tempDir = Files.createTempDirectory("shared_memory_agents_"); + + // 创建 SharedMemoryManager(多个代理共享同一个实例) + SharedMemoryManager sharedManager = new SharedMemoryManager(tempDir); + + // 代理 1 存储信息 + ShortTermMemory mem1 = sharedManager.createShortTermMemory( + "coder", + "用户要求实现用户登录功能", + "task-login" + ); + mem1.putMetadata("标签", "需求"); + mem1.putMetadata("类型", "登录"); + sharedManager.store(mem1); + System.out.println("[代理 coder] 存储了需求"); + + // 代理 2 查看信息 + List allMemories = sharedManager.retrieve(Memory.MemoryType.SHORT_TERM, 100); + int coderCount = 0; + for (Memory m : allMemories) { + if (m instanceof ShortTermMemory) { + ShortTermMemory stm = (ShortTermMemory) m; + if ("coder".equals(stm.getAgentId())) { + coderCount++; + } + } + } + System.out.println("[代理 tester] 发现 coder 有 " + coderCount + " 条记忆"); + + // 代理 2 添加测试信息 + ShortTermMemory mem2 = sharedManager.createShortTermMemory( + "tester", + "需要测试登录功能的边界条件", + "task-login" + ); + mem2.putMetadata("标签", "测试"); + mem2.putMetadata("类型", "边界条件"); + sharedManager.store(mem2); + System.out.println("[代理 tester] 存储了测试计划"); + + // 等待存储完成 + Thread.sleep(100); + + // 查看所有与该任务相关的记忆 + List allAfter = sharedManager.retrieve(Memory.MemoryType.SHORT_TERM, 100); + System.out.println("\n任务 [task-login] 的所有记忆 (" + allAfter.size() + " 条):"); + for (Memory mem : allAfter) { + if (mem instanceof ShortTermMemory) { + ShortTermMemory stm = (ShortTermMemory) mem; + if ("task-login".equals(stm.getTaskId())) { + System.out.println(" - [" + stm.getAgentId() + "] " + stm.getContext()); + } + } + } + + // 关闭管理器 + sharedManager.shutdown(); + System.out.println("跨代理共享记忆示例完成"); + } + + /** + * 主函数:运行所有示例 + */ + public static void main(String[] args) throws Exception { + example1_FileMemoryStore(); + example2_SharedMemoryManager(); + example3_MemoryLifecycle(); + example4_KnowledgeMemory(); + example5_SharedMemory(); + + System.out.println("\n=== 所有示例运行完成 ==="); + } +} diff --git a/soloncode-cli/src/test/java/agentTems/MessagePassingDebugTest.java b/soloncode-cli/src/test/java/agentTems/MessagePassingDebugTest.java new file mode 100644 index 0000000000000000000000000000000000000000..09ce0f493245b7aa8f4b3b27976bd0303e992716 --- /dev/null +++ b/soloncode-cli/src/test/java/agentTems/MessagePassingDebugTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2026 noear.org and 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 agentTems; + +import org.junit.jupiter.api.Test; +import org.noear.solon.bot.core.message.AgentMessage; +import org.noear.solon.bot.core.message.MessageChannel; +import org.noear.solon.bot.core.message.MessageHandler; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 消息传递调试测试 + * + * @author bai + * @since 3.9.5 + */ +public class MessagePassingDebugTest { + + @Test + public void testBasicMessageSending() throws Exception { + System.out.println("=== 基础消息发送测试 ==="); + + MessageChannel messageChannel = new MessageChannel("./work/test-message"); + + // 注册处理器 + AtomicInteger receiveCount = new AtomicInteger(0); + messageChannel.registerHandler("receiver", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + int count = receiveCount.incrementAndGet(); + System.out.println("Handler 被调用 #" + count); + System.out.println(" From: " + message.getFrom()); + System.out.println(" To: " + message.getTo()); + System.out.println(" Type: " + message.getType()); + System.out.println(" Content: " + message.getContent()); + + return CompletableFuture.completedFuture("OK: " + message.getContent()); + } + }); + System.out.println("✓ 处理器已注册"); + + // 等待处理器就绪 + Thread.sleep(100); + + // 发送消息 + System.out.println("发送消息..."); + CompletableFuture response = messageChannel.request( + "sender", + "receiver", + "test", + "Hello" + ); + + // 等待响应 + System.out.println("等待响应..."); + Object result = response.get(5, TimeUnit.SECONDS); + System.out.println("收到响应: " + result); + + assertNotNull(result, "响应不应该为 null"); + assertEquals(1, receiveCount.get(), "处理器应该被调用一次"); + + messageChannel.shutdown(); + System.out.println("✓ 测试通过"); + } + + @Test + public void testMultipleMessages() throws Exception { + System.out.println("\n=== 多条消息测试 ==="); + + MessageChannel messageChannel = new MessageChannel("./work/test-message"); + + AtomicInteger receiveCount = new AtomicInteger(0); + + messageChannel.registerHandler("receiver", new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + int count = receiveCount.incrementAndGet(); + System.out.println("收到消息 #" + count + ": " + message.getContent()); + + // 模拟处理时间 + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return CompletableFuture.completedFuture("响应 #" + count); + } + }); + + Thread.sleep(100); + + // 发送3条消息 + for (int i = 1; i <= 3; i++) { + System.out.println("发送消息 " + i); + CompletableFuture response = messageChannel.request( + "sender", + "receiver", + "query", + "消息 " + i + ); + + Object result = response.get(5, TimeUnit.SECONDS); + System.out.println("收到响应: " + result); + assertNotNull(result); + } + + assertEquals(3, receiveCount.get(), "应该收到3条消息"); + + messageChannel.shutdown(); + System.out.println("✓ 测试通过"); + } +} diff --git a/soloncode-cli/src/test/java/features/bot/codecli/SkillTest.java b/soloncode-cli/src/test/java/features/bot/codecli/SkillTest.java deleted file mode 100644 index aed4f32ab31c599f83a6d8d4b2565b5630643018..0000000000000000000000000000000000000000 --- a/soloncode-cli/src/test/java/features/bot/codecli/SkillTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package features.bot.codecli; - -import org.junit.jupiter.api.Test; -import org.noear.solon.Solon; -import org.noear.solon.bot.core.CliSkillProvider; -import org.noear.solon.bot.core.AgentProperties; -import org.noear.solon.core.util.Assert; -import org.noear.solon.test.SolonTest; - -import java.util.Map; - -/** - * - * @author noear 2026/2/28 created - * - */ -@SolonTest -public class SkillTest { - @Test - public void case1() { - AgentProperties config = Solon.cfg().toBean("solon.code.cli", AgentProperties.class); - - CliSkillProvider cliSkillProvider = new CliSkillProvider(); - - if(Assert.isNotEmpty(config.getSkillPools())) { - for (Map.Entry entry : config.getSkillPools().entrySet()) { - cliSkillProvider.skillPool(entry.getKey(), entry.getValue()); - } - } - - //video generation animation - //AI image video media generation - - //discoverySkill.searchSkills() - - String desc = cliSkillProvider.getPoolManager().getSkillMap().get("@shared/docusign-automation").getDescription(); - System.out.println(desc); - assert desc.length() > 30; - - String list = cliSkillProvider.getExpertSkill().skillsearch("video generation animation"); - System.out.println(list); - assert list.length() > 100; - } -} diff --git a/soloncode-cli/src/test/java/features/bot/codecli/SubagentManagerTest.java b/soloncode-cli/src/test/java/features/bot/codecli/SubagentManagerTest.java deleted file mode 100644 index 27bd6db6384fc1eda31cf1a0f4fa13318e1e96e8..0000000000000000000000000000000000000000 --- a/soloncode-cli/src/test/java/features/bot/codecli/SubagentManagerTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package features.bot.codecli; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.noear.solon.bot.core.subagent.SubagentManager; - -import java.util.Arrays; -import java.util.List; - -/** - * - * @author noear 2026/3/7 created - * - */ -public class SubagentManagerTest { - private final SubagentManager manager = new SubagentManager(null); // Kernel 传空即可 - - @Test - public void testStandardFormat() { - List lines = Arrays.asList( - "---", - "name: reviewer", - "tools: Read, Write", - "model: sonnet", - "---", - "# Instruction", - "Review the code." - ); - - SubagentManager.SubagentFile result = manager.parseSubagentFile(lines); - - Assertions.assertEquals("reviewer", result.name); - Assertions.assertTrue(result.tools.contains("Read")); - Assertions.assertTrue(result.tools.contains("Write")); - Assertions.assertEquals("sonnet", result.model); - Assertions.assertTrue(result.systemPrompt.contains("Review the code.")); - } - - @Test - public void testFirstLineEmpty_ShouldBePlainBody() { - // 规范:第一行是空行,不触发 Frontmatter 解析 - List lines = Arrays.asList( - "", - "---", - "name: reviewer", - "---", - "Just content" - ); - - SubagentManager.SubagentFile result = manager.parseSubagentFile(lines); - - // 解析器不应识别出 name,整个内容应作为 body - Assertions.assertNull(result.name); - Assertions.assertTrue(result.systemPrompt.contains("name: reviewer")); - } - - @Test - public void testToolsAsArray() { - List lines = Arrays.asList( - "---", - "tools:", - " - Read", - " - Write", - "---", - "Body" - ); - - SubagentManager.SubagentFile result = manager.parseSubagentFile(lines); - Assertions.assertEquals(2, result.tools.size()); - Assertions.assertTrue(result.tools.contains("Read")); - } - - @Test - public void testNoClosingSeparator() { - // 只有开头没有结尾,应全量退回为 body - List lines = Arrays.asList( - "---", - "name: oops", - "This is just text now" - ); - - SubagentManager.SubagentFile result = manager.parseSubagentFile(lines); - Assertions.assertNull(result.name); - Assertions.assertTrue(result.systemPrompt.startsWith("---")); - } - - @Test - public void testYamlSyntaxError() { - // YAML 缩进错误或其他语法错误 - List lines = Arrays.asList( - "---", - "name: [invalid yaml", - "---", - "Body" - ); - - SubagentManager.SubagentFile result = manager.parseSubagentFile(lines); - // 应该捕获异常并退回到普通文本模式 - Assertions.assertNull(result.name); - Assertions.assertTrue(result.systemPrompt.contains("name: [invalid yaml")); - } -} diff --git a/soloncode-cli/src/test/java/org/noear/solon/ai/codecli/core/message/MessageApiTest.java b/soloncode-cli/src/test/java/org/noear/solon/ai/codecli/core/message/MessageApiTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5d8aad2a64c28cc0c0fee16e0551d87b71823646 --- /dev/null +++ b/soloncode-cli/src/test/java/org/noear/solon/ai/codecli/core/message/MessageApiTest.java @@ -0,0 +1,266 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.ai.codecli.core.message; + +import org.junit.jupiter.api.Test; +import org.noear.solon.bot.core.message.AgentMessage; +import org.noear.solon.bot.core.message.MessageHandler; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 新消息 API 测试示例 + * + * 演示如何使用新的 AgentMessage API + * + * @author bai + * @since 3.9.5 + */ +class MessageApiTest { + + /** + * 示例 1: 创建简单文本消息 + */ + @Test + void testSimpleMessage() { + // 使用 Builder 创建消息 + AgentMessage message = AgentMessage.of("Hello World") + .from("agent") + .to("user") + .type("notification") + .build(); + + // 类型安全的访问 + assertEquals("Hello World", message.getContent()); + assertEquals("agent", message.getFrom()); + assertEquals("user", message.getTo()); + assertEquals("notification", message.getType()); + } + + /** + * 示例 2: 创建列表消息 + */ + @Test + void testListMessage() { + List tags = Arrays.asList("controller", "rest", "api"); + + // 创建泛型消息 + AgentMessage> message = AgentMessage.>empty() + .content(tags) + .from("explore") + .to("plan") + .type("query.result") + .build(); + + // 类型安全的访问 - 无需强制转换 + List content = message.getContent(); + assertEquals(3, content.size()); + assertEquals("controller", content.get(0)); + } + + /** + * 示例 3: 带元数据的消息 + */ + @Test + void testMessageWithMetadata() { + AgentMessage message = AgentMessage.of("操作完成") + .from("bash") + .to("*") + .type("task_completed") + .metadata("taskId", "task-001") + .metadata("status", "success") + .metadata("duration", "1500") + .metadata("persistent", "true") + .build(); + + // 获取元数据 + assertEquals("task-001", message.getMetadata("taskId")); + assertEquals("success", message.getMetadata("status", "unknown")); + + // 类型安全的元数据获取 + assertEquals(1500, message.getIntMetadata("duration", 0)); + assertTrue(message.getBooleanMetadata("persistent", false)); + } + + /** + * 示例 4: 使用枚举类型 + */ + @Test + void testMessageWithEnum() { + // 使用枚举作为 from/to(手动转换为小写字符串) + AgentMessage message = AgentMessage.of("查询结果") + .from("explore") + .to("plan") + .type("query") + .build(); + + assertEquals("explore", message.getFrom()); + assertEquals("plan", message.getTo()); + } + + /** + * 示例 5: 转换为 Builder + */ + @Test + void testToBuilder() { + AgentMessage original = AgentMessage.of("原始消息") + .from("agent1") + .to("agent2") + .type("request") + .build(); + + // 使用 toBuilder 创建修改后的副本 + AgentMessage modified = original.toBuilder() + .to("agent3") + .metadata("modified", "true") + .build(); + + assertEquals("agent1", modified.getFrom()); + assertEquals("agent3", modified.getTo()); + assertEquals("true", modified.getMetadata("modified")); + } + + /** + * 示例 6: 消息处理器使用新 API + */ + @Test + void testMessageHandler() throws ExecutionException, InterruptedException { + // 创建消息处理器(使用匿名内部类,因为 handle 方法是泛型的) + MessageHandler handler = new MessageHandler() { + @Override + public CompletableFuture handle(AgentMessage message) { + // 类型安全的处理 + String content = (String) message.getContent(); + String taskId = message.getMetadata("taskId"); + + return CompletableFuture.completedFuture( + "处理完成: " + content + ", taskId=" + taskId + ); + } + }; + + // 测试处理器 + AgentMessage message = AgentMessage.of("测试消息") + .from("agent") + .to("user") + .metadata("taskId", "task-123") + .build(); + + CompletableFuture result = handler.handle(message); + + assertEquals("处理完成: 测试消息, taskId=task-123", result.get()); + } + + /** + * 示例 7: 空消息 + */ + @Test + void testEmptyMessage() { + AgentMessage message = AgentMessage.empty() + .from("system") + .to("*") + .type("system") + .build(); + + assertEquals("system", message.getFrom()); + assertEquals("*", message.getTo()); + assertNull(message.getContent()); + } + + /** + * 示例 8: 复杂对象消息 + */ + @Test + void testComplexObjectMessage() { + class QueryResult { + private final String query; + private final List results; + + QueryResult(String query, List results) { + this.query = query; + this.results = results; + } + + public String getQuery() { + return query; + } + + public List getResults() { + return results; + } + } + + QueryResult result = new QueryResult( + "Controller", + Arrays.asList("UserController.java", "ProductController.java") + ); + + AgentMessage message = AgentMessage.of(result) + .from("explore") + .to("plan") + .type("query.result") + .build(); + + // 类型安全的访问 - 无需转换 + QueryResult content = message.getContent(); + assertEquals("Controller", content.getQuery()); + assertEquals(2, content.getResults().size()); + } + + /** + * 示例 9: 默认值 + */ + @Test + void testDefaultValues() { + // 使用默认值创建消息 + AgentMessage message = AgentMessage.of("test") + .build(); + + assertEquals("system", message.getFrom()); // 默认 from + assertEquals("*", message.getTo()); // 默认 to + assertEquals("notification", message.getType()); // 默认 type + assertNotNull(message.getId()); // 自动生成 ID + assertTrue(message.getTimestamp() > 0); // 自动生成时间戳 + } + + /** + * 示例 10: 元数据操作 + */ + @Test + void testMetadataOperations() { + AgentMessage message = AgentMessage.of("test") + .metadata("key1", "value1") + .metadata("key2", "value2") + .build(); + + // 获取所有元数据 + assertEquals(2, message.getMetadata().size()); + assertEquals("value1", message.getMetadata("key1")); + assertEquals("value2", message.getMetadata("key2")); + + // 获取不存在的键(带默认值) + assertEquals("default", message.getMetadata("key3", "default")); + + // 类型安全的元数据 + assertEquals(100, message.getIntMetadata("key3", 100)); + assertTrue(message.getBooleanMetadata("key3", true)); + } +} diff --git a/soloncode-cli/src/test/java/org/noear/solon/ai/codecli/core/subagent/SubAgentMetadataTest.java b/soloncode-cli/src/test/java/org/noear/solon/ai/codecli/core/subagent/SubAgentMetadataTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8744a453d7c02049862343f169a063233b79f2ba --- /dev/null +++ b/soloncode-cli/src/test/java/org/noear/solon/ai/codecli/core/subagent/SubAgentMetadataTest.java @@ -0,0 +1,424 @@ +/* + * Copyright 2017-2026 noear.org and 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 org.noear.solon.ai.codecli.core.subagent; + +import org.junit.jupiter.api.Test; +import org.noear.solon.bot.core.subagent.SubAgentMetadata; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * SubAgent 元数据测试 + * + * @author bai + * @since 3.9.5 + */ +public class SubAgentMetadataTest { + + @Test + public void testParseMetadataWithAllFields() { + String prompt = "---\n" + + "name: explore\n" + + "description: Fast codebase exploration expert\n" + + "tools: Glob, Grep, Read\n" + + "model: glm-4-flash\n" + + "---\n\n" + + "## 探索代理\n\n" + + "你是一个快速的代码库探索专家。"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("explore", metadata.getName()); + assertEquals("Fast codebase exploration expert", metadata.getDescription()); + assertEquals("glm-4-flash", metadata.getModel()); + + List tools = metadata.getTools(); + assertEquals(3, tools.size()); + assertTrue(tools.contains("Glob")); + assertTrue(tools.contains("Grep")); + assertTrue(tools.contains("Read")); + } + + @Test + public void testParseMetadataWithPartialFields() { + String prompt = "---\n" + + "name: plan\n" + + "model: glm-4.7\n" + + "---\n\n" + + "## 计划代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("plan", metadata.getName()); + assertEquals("glm-4.7", metadata.getModel()); + assertNull(metadata.getDescription()); + assertTrue(metadata.getTools().isEmpty()); + } + + @Test + public void testParseMetadataWithoutYaml() { + String prompt = "## 计划代理\n\n" + + "你是一个软件架构师。"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertNull(metadata.getName()); + assertNull(metadata.getDescription()); + assertNull(metadata.getModel()); + assertTrue(metadata.getTools().isEmpty()); + } + + @Test + public void testParseAndClean() { + String prompt = "---\n" + + "name: bash\n" + + "model: glm-4-flash\n" + + "---\n\n" + + "## Bash 代理\n\n" + + "你是一个命令行执行专家。"; + + SubAgentMetadata.PromptWithMetadata result = SubAgentMetadata.parseAndClean(prompt); + + // 验证元数据 + assertEquals("bash", result.getMetadata().getName()); + assertEquals("glm-4-flash", result.getMetadata().getModel()); + + // 验证清理后的提示词(不包含 YAML 头部) + String cleaned = result.getPrompt(); + assertFalse(cleaned.contains("---")); + assertFalse(cleaned.contains("name: bash")); + assertTrue(cleaned.contains("## Bash 代理")); + assertTrue(cleaned.contains("你是一个命令行执行专家")); + } + + @Test + public void testParseAndCleanWithoutYaml() { + String prompt = "## 计划代理\n\n" + + "你是一个软件架构师。"; + + SubAgentMetadata.PromptWithMetadata result = SubAgentMetadata.parseAndClean(prompt); + + // 元数据应该为空 + assertNull(result.getMetadata().getName()); + assertNull(result.getMetadata().getModel()); + + // 提示词应该保持不变 + assertEquals(prompt, result.getPrompt()); + } + + @Test + public void testHasModel() { + SubAgentMetadata metadata1 = new SubAgentMetadata(); + assertFalse(metadata1.hasModel()); + + SubAgentMetadata metadata2 = SubAgentMetadata.fromPrompt( + "---\nmodel: glm-4.7\n---\n\n" + ); + assertTrue(metadata2.hasModel()); + } + + @Test + public void testParseWithComments() { + String prompt = "---\n" + + "# 这是一个测试代理\n" + + "name: test\n" + + "# model: old-model\n" + + "model: glm-4.7\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertEquals("glm-4.7", metadata.getModel()); + } + + @Test + public void testParseWithEmptyLines() { + String prompt = "---\n" + + "\n" + + "name: test\n" + + "\n" + + "model: glm-4.7\n" + + "\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertEquals("glm-4.7", metadata.getModel()); + } + + @Test + public void testRealExploreSubAgentPrompt() { + String prompt = "---\n" + + "name: explore\n" + + "description: Fast codebase exploration expert for finding files and analyzing structure\n" + + "tools: Glob, Grep, Read\n" + + "model: glm-4-flash\n" + + "---\n\n" + + "## 探索代理\n\n" + + "你是一个快速的代码库探索专家。你的任务是:\n" + + "\n" + + "### 核心能力\n" + + "- 使用 Glob 工具按模式查找文件(最高效)\n" + + "- 使用 Grep 工具搜索代码内容\n" + + "- 使用 Read 工具读取文件内容\n" + + "- 分析代码结构和架构"; + + SubAgentMetadata.PromptWithMetadata result = SubAgentMetadata.parseAndClean(prompt); + + // 验证元数据 + SubAgentMetadata metadata = result.getMetadata(); + assertEquals("explore", metadata.getName()); + assertEquals("glm-4-flash", metadata.getModel()); + assertTrue(metadata.hasModel()); + + // 验证工具列表 + List tools = metadata.getTools(); + assertEquals(3, tools.size()); + assertTrue(tools.contains("Glob")); + assertTrue(tools.contains("Grep")); + assertTrue(tools.contains("Read")); + + // 验证清理后的提示词 + String cleaned = result.getPrompt(); + assertFalse(cleaned.contains("---")); + assertTrue(cleaned.startsWith("## 探索代理")); + } + + @Test + public void testRealPlanSubAgentPrompt() { + String prompt = "---\n" + + "name: plan\n" + + "description: Software architect for designing implementation plans and technical choices\n" + + "tools: Read\n" + + "model: glm-4.7\n" + + "---\n\n" + + "## 计划代理(软件架构师)\n\n" + + "你是一个经验丰富的软件架构师。"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("plan", metadata.getName()); + assertEquals("glm-4.7", metadata.getModel()); + assertTrue(metadata.hasModel()); + } + + @Test + public void testParseDisallowedTools() { + String prompt = "---\n" + + "name: test\n" + + "disallowedTools: Bash, Write, Edit\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertTrue(metadata.hasDisallowedTools()); + assertEquals(3, metadata.getDisallowedTools().size()); + assertTrue(metadata.getDisallowedTools().contains("Bash")); + assertTrue(metadata.getDisallowedTools().contains("Write")); + assertTrue(metadata.getDisallowedTools().contains("Edit")); + } + + @Test + public void testParsePermissionMode() { + String prompt = "---\n" + + "name: test\n" + + "permissionMode: plan\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertEquals("plan", metadata.getPermissionMode()); + assertTrue(metadata.hasPermissionMode()); + } + + @Test + public void testParseMaxTurns() { + String prompt = "---\n" + + "name: test\n" + + "maxTurns: 50\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertEquals(Integer.valueOf(50), metadata.getMaxTurns()); + assertTrue(metadata.hasMaxTurns()); + } + + @Test + public void testParseSkills() { + String prompt = "---\n" + + "name: test\n" + + "skills: commit, review-pr, pdf\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertTrue(metadata.hasSkills()); + assertEquals(3, metadata.getSkills().size()); + assertTrue(metadata.getSkills().contains("commit")); + assertTrue(metadata.getSkills().contains("review-pr")); + assertTrue(metadata.getSkills().contains("pdf")); + } + + @Test + public void testParseMcpServers() { + String prompt = "---\n" + + "name: test\n" + + "mcpServers: slack, github\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertTrue(metadata.hasMcpServers()); + assertEquals(2, metadata.getMcpServers().size()); + assertTrue(metadata.getMcpServers().contains("slack")); + assertTrue(metadata.getMcpServers().contains("github")); + } + + @Test + public void testParseMemory() { + String prompt = "---\n" + + "name: test\n" + + "memory: project\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertEquals("project", metadata.getMemory()); + assertTrue(metadata.hasMemory()); + } + + @Test + public void testParseBackground() { + String prompt = "---\n" + + "name: test\n" + + "background: true\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertTrue(metadata.isBackground()); + } + + @Test + public void testParseIsolation() { + String prompt = "---\n" + + "name: test\n" + + "isolation: worktree\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertEquals("worktree", metadata.getIsolation()); + assertTrue(metadata.hasIsolation()); + } + + @Test + public void testParseCompleteMetadata() { + String prompt = "---\n" + + "name: comprehensive-test\n" + + "description: A comprehensive test with all metadata fields\n" + + "tools: Read, Glob, Grep\n" + + "disallowedTools: Bash, Write\n" + + "model: glm-4.7\n" + + "permissionMode: plan\n" + + "maxTurns: 100\n" + + "skills: commit, review\n" + + "mcpServers: slack\n" + + "memory: user\n" + + "background: false\n" + + "isolation: worktree\n" + + "---\n\n" + + "## 综合测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("comprehensive-test", metadata.getName()); + assertEquals("A comprehensive test with all metadata fields", metadata.getDescription()); + assertEquals("glm-4.7", metadata.getModel()); + assertEquals("plan", metadata.getPermissionMode()); + assertEquals(Integer.valueOf(100), metadata.getMaxTurns()); + assertEquals("user", metadata.getMemory()); + assertEquals("worktree", metadata.getIsolation()); + + assertTrue(metadata.hasModel()); + assertTrue(metadata.hasPermissionMode()); + assertTrue(metadata.hasMaxTurns()); + assertTrue(metadata.hasSkills()); + assertTrue(metadata.hasMcpServers()); + assertTrue(metadata.hasDisallowedTools()); + assertTrue(metadata.hasMemory()); + assertTrue(metadata.hasIsolation()); + assertFalse(metadata.isBackground()); + + assertEquals(3, metadata.getTools().size()); + assertEquals(2, metadata.getDisallowedTools().size()); + assertEquals(2, metadata.getSkills().size()); + assertEquals(1, metadata.getMcpServers().size()); + } + + @Test + public void testInvalidMaxTurnsIgnored() { + String prompt = "---\n" + + "name: test\n" + + "maxTurns: invalid\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertNull(metadata.getMaxTurns()); + assertFalse(metadata.hasMaxTurns()); + } + + @Test + public void testInvalidBooleanBackgroundIgnored() { + String prompt = "---\n" + + "name: test\n" + + "background: not-a-boolean\n" + + "---\n\n" + + "## 测试代理"; + + SubAgentMetadata metadata = SubAgentMetadata.fromPrompt(prompt); + + assertEquals("test", metadata.getName()); + assertFalse(metadata.isBackground()); + // Boolean.parseBoolean 返回 false for invalid values + } +}