create-context-template
Advanced tools
| /** | ||
| * 环境变量配置管理 | ||
| * | ||
| * 自动加载 .env 文件: | ||
| * - 使用 dotenv 库自动加载环境变量 | ||
| * - 支持 Node.js 所有版本 | ||
| * | ||
| * 优先级(从高到低): | ||
| * 1. .env.local (本地覆盖,不提交到 git) | ||
| * 2. .env.{NODE_ENV} (环境特定配置,如 .env.production) | ||
| * 3. .env (默认配置) | ||
| * | ||
| * 使用方式: | ||
| * ```typescript | ||
| * import { config } from './config/env.js'; | ||
| * console.log(config.llm.deepseek.apiKey); | ||
| * ``` | ||
| */ | ||
| import { config as dotenvConfig } from "dotenv"; | ||
| import { fileURLToPath } from "url"; | ||
| import { dirname, resolve } from "path"; | ||
| //加载环境变量文件 | ||
| loadEnv(); | ||
| /** 应用配置接口 */ | ||
| export interface AppConfig { | ||
| /** Node 环境 */ | ||
| nodeEnv: "development" | "production" | "test"; | ||
| /** 日志配置 */ | ||
| logging: { | ||
| level: "debug" | "info" | "warn" | "error"; | ||
| enableConsole: boolean; | ||
| }; | ||
| /** LLM 提供商配置 */ | ||
| llm: { | ||
| /** DeepSeek 配置 */ | ||
| deepseek: { | ||
| apiKey: string; | ||
| baseURL?: string; | ||
| }; | ||
| }; | ||
| /** 默认 LLM 配置 */ | ||
| defaultProvider: string; | ||
| } | ||
| /** 获取环境变量,支持默认值 */ | ||
| function getEnvVar(name: string, defaultValue?: string): string { | ||
| const value = process.env[name]; | ||
| if (!value && defaultValue === undefined) { | ||
| throw new Error(`Environment variable ${name} is required but not set`); | ||
| } | ||
| return value || defaultValue!; | ||
| } | ||
| /** 获取环境变量布尔值 */ | ||
| function getEnvBoolean(name: string, defaultValue: boolean = false): boolean { | ||
| const value = process.env[name]; | ||
| if (!value) return defaultValue; | ||
| return value.toLowerCase() === "true" || value === "1"; | ||
| } | ||
| /** 验证 NODE_ENV 值 */ | ||
| function validateNodeEnv(env: string): "development" | "production" | "test" { | ||
| if (["development", "production", "test"].includes(env)) { | ||
| return env as "development" | "production" | "test"; | ||
| } | ||
| console.warn(`Invalid NODE_ENV: ${env}, falling back to 'development'`); | ||
| return "development"; | ||
| } | ||
| /** 验证日志级别 */ | ||
| function validateLogLevel(level: string): "debug" | "info" | "warn" | "error" { | ||
| if (["debug", "info", "warn", "error"].includes(level)) { | ||
| return level as "debug" | "info" | "warn" | "error"; | ||
| } | ||
| console.warn(`Invalid LOG_LEVEL: ${level}, falling back to 'info'`); | ||
| return "info"; | ||
| } | ||
| /** 导出类型安全的配置对象 */ | ||
| export const config: AppConfig = { | ||
| nodeEnv: validateNodeEnv(getEnvVar("NODE_ENV", "development")), | ||
| logging: { | ||
| level: validateLogLevel(getEnvVar("LOG_LEVEL", "info")), | ||
| enableConsole: getEnvBoolean("ENABLE_CONSOLE_LOG", true), | ||
| }, | ||
| llm: { | ||
| deepseek: { | ||
| apiKey: getEnvVar("DEEPSEEK_API_KEY", ""), | ||
| baseURL: getEnvVar("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), | ||
| }, | ||
| }, | ||
| defaultProvider: getEnvVar("DEFAULT_LLM_PROVIDER", "deepseek"), | ||
| }; | ||
| /** | ||
| * 获取指定提供商的配置 | ||
| * @param provider - LLM 提供商名称 | ||
| * @returns 提供商配置对象 | ||
| */ | ||
| export function getLLMKeyByProvider(provider: string) { | ||
| const providerKey = provider.toLowerCase(); | ||
| let apiKey = config.llm[providerKey].apiKey; | ||
| if (!apiKey) { | ||
| throw new Error(`API key for provider "${provider}" not found. `); | ||
| } | ||
| return apiKey; | ||
| } | ||
| export function loadEnv() { | ||
| // 获取当前文件所在目录 | ||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
| const projectRoot = resolve(__dirname, "../.."); | ||
| // 加载环境变量文件(按优先级) | ||
| const nodeEnv = process.env.NODE_ENV || "development"; | ||
| // 1. 加载 .env.local(最高优先级) | ||
| dotenvConfig({ path: resolve(projectRoot, ".env.local"), override: false }); | ||
| // 2. 加载 .env.{NODE_ENV} | ||
| if (nodeEnv !== "development") { | ||
| dotenvConfig({ | ||
| path: resolve(projectRoot, `.env.${nodeEnv}`), | ||
| override: false, | ||
| }); | ||
| } | ||
| // 3. 加载 .env(默认配置) | ||
| dotenvConfig({ path: resolve(projectRoot, ".env"), override: false }); | ||
| } | ||
| /** | ||
| * Agent 模块统一导出 | ||
| */ | ||
| export { SimpleAgent } from './SimpleAgent.js'; | ||
| export type { AgentResult, AgentConfig } from './SimpleAgent.js'; | ||
| export { MainAgent, SubAgent, createMultiAgentSystem } from './MultiAgent.js'; | ||
| export type { MainAgentResult, SubAgentResult } from './MultiAgent.js'; |
| /** | ||
| * 多智能体系统实现 | ||
| * 演示主Agent协调多个子Agent完成任务的架构 | ||
| */ | ||
| import { ContextManager, ContextType, Message } from '../context/index.js'; | ||
| import { ToolManager } from '../tool/ToolManager.js'; | ||
| import { ILLMService } from '../llm/types/index.js'; | ||
| import { executeToolLoop } from '../llm/utils/executeToolLoop.js'; | ||
| import { ExecutionHistoryContext } from '../context/modules/ExecutionHistoryContext.js'; | ||
| import { eventBus } from '../../evaluation/EventBus.js'; | ||
| import { | ||
| MAIN_AGENT_PROMPT, | ||
| SUB_AGENT_A_PROMPT, | ||
| SUB_AGENT_B_PROMPT, | ||
| } from '../promptManager/index.js'; | ||
| /** | ||
| * 子Agent执行结果 | ||
| */ | ||
| export interface SubAgentResult { | ||
| /** 子Agent名称 */ | ||
| agentName: string; | ||
| /** 执行结果 */ | ||
| result: string; | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * 主Agent执行结果 | ||
| */ | ||
| export interface MainAgentResult { | ||
| /** 收集到的 Agent 名称列表 */ | ||
| agents: string[]; | ||
| /** 每个 Agent 调用的工具记录 */ | ||
| tools: Record<string, string[]>; | ||
| /** 最终响应 */ | ||
| finalResponse: string; | ||
| /** 子Agent执行记录 */ | ||
| subAgentResults: SubAgentResult[]; | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * 子Agent类 | ||
| * 使用 executeToolLoop 执行任务(工具可为空) | ||
| */ | ||
| export class SubAgent { | ||
| private name: string; | ||
| private llmService: ILLMService; | ||
| private systemPrompt: string; | ||
| private maxLoops: number; | ||
| constructor( | ||
| name: string, | ||
| llmService: ILLMService, | ||
| systemPrompt: string, | ||
| maxLoops: number = 5 | ||
| ) { | ||
| this.name = name; | ||
| this.llmService = llmService; | ||
| this.systemPrompt = systemPrompt; | ||
| this.maxLoops = maxLoops; | ||
| } | ||
| /** | ||
| * 执行子Agent任务 | ||
| * @param instruction - 主Agent下发的指令 | ||
| */ | ||
| async run(instruction: string): Promise<SubAgentResult> { | ||
| console.log(`\n🤖 子Agent [${this.name}] 开始执行...`); | ||
| console.log(`📋 指令: ${instruction}`); | ||
| // 发射子Agent调用事件 | ||
| eventBus.emit('agent:call', { agentName: this.name }); | ||
| try { | ||
| // 初始化上下文管理器 | ||
| const contextManager = new ContextManager(); | ||
| await contextManager.init(); | ||
| // 设置系统提示词 | ||
| contextManager.add( | ||
| this.systemPrompt, | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| // 设置用户输入(来自主Agent的指令) | ||
| contextManager.setUserInput(instruction); | ||
| // 初始化工具管理器(工具为空,但仍使用 executeToolLoop) | ||
| const toolManager = new ToolManager(); | ||
| // 清空默认工具,使子Agent无工具可用 | ||
| toolManager.clear(); | ||
| // 执行工具循环 | ||
| const loopResult = await executeToolLoop( | ||
| this.llmService, | ||
| contextManager, | ||
| toolManager, | ||
| { | ||
| maxLoops: this.maxLoops, | ||
| agentName: this.name, | ||
| } | ||
| ); | ||
| console.log(`✅ 子Agent [${this.name}] 执行完成`); | ||
| return { | ||
| agentName: this.name, | ||
| result: loopResult.result || '', | ||
| success: loopResult.success, | ||
| error: loopResult.error, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| console.error(`❌ 子Agent [${this.name}] 执行失败: ${errorMessage}`); | ||
| return { | ||
| agentName: this.name, | ||
| result: '', | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| getName(): string { | ||
| return this.name; | ||
| } | ||
| } | ||
| /** | ||
| * 主Agent类 | ||
| * 协调者角色,负责任务分配和结果汇总 | ||
| * 不直接使用工具,而是通过调用子Agent完成任务 | ||
| */ | ||
| export class MainAgent { | ||
| private name: string; | ||
| private llmService: ILLMService; | ||
| private systemPrompt: string; | ||
| private subAgentA: SubAgent; | ||
| private subAgentB: SubAgent; | ||
| private contextManager: ContextManager; | ||
| constructor(llmService: ILLMService, name: string = 'main_agent') { | ||
| this.name = name; | ||
| this.llmService = llmService; | ||
| this.systemPrompt = MAIN_AGENT_PROMPT; | ||
| this.contextManager = new ContextManager(); | ||
| // 初始化两个子Agent | ||
| this.subAgentA = new SubAgent( | ||
| 'researcher', | ||
| llmService, | ||
| SUB_AGENT_A_PROMPT | ||
| ); | ||
| this.subAgentB = new SubAgent( | ||
| 'executor', | ||
| llmService, | ||
| SUB_AGENT_B_PROMPT | ||
| ); | ||
| } | ||
| /** | ||
| * 执行主Agent | ||
| * @param userInput - 用户输入 | ||
| */ | ||
| async run(userInput: string): Promise<MainAgentResult> { | ||
| // 初始化上下文管理器 | ||
| this.contextManager.init(); | ||
| // 重置事件收集器 | ||
| eventBus.reset(); | ||
| // 发射主Agent调用事件 | ||
| eventBus.emit('agent:call', { agentName: this.name }); | ||
| console.log(`\n🎯 主Agent [${this.name}] 开始处理任务...`); | ||
| console.log(`📝 用户输入: ${userInput}`); | ||
| const subAgentResults: SubAgentResult[] = []; | ||
| try { | ||
| // 1. 调用子Agent A(研究者) | ||
| const instructionA = `请针对以下用户需求进行研究分析:\n${userInput}`; | ||
| const resultA = await this.subAgentA.run(instructionA); | ||
| subAgentResults.push(resultA); | ||
| let executionHistoryA = ` | ||
| 子Agent执行完成-${this.subAgentA.getName()}: | ||
| 主Agent指令: ${instructionA} | ||
| 输出: ${resultA.result} | ||
| `; | ||
| this.contextManager.add(executionHistoryA, ContextType.EXECUTION_HISTORY); | ||
| // 2. 调用子Agent B(执行者) | ||
| const instructionB = `基于以下用户需求,请提供具体的执行方案:\n${userInput}`; | ||
| const resultB = await this.subAgentB.run(instructionB); | ||
| subAgentResults.push(resultB); | ||
| // 记录子Agent B的执行结果 | ||
| let executionHistoryB = ` | ||
| 子Agent执行完成-${this.subAgentB.getName()}: | ||
| 主Agent指令: ${instructionB} | ||
| 输出: ${resultB.result} | ||
| `; | ||
| this.contextManager.add(executionHistoryB, ContextType.EXECUTION_HISTORY); | ||
| // 3. 主Agent汇总结果 | ||
| const finalResponse = await this.summarizeResults(userInput); | ||
| console.log(`\n✅ 主Agent [${this.name}] 任务完成`); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse, | ||
| subAgentResults, | ||
| success: true, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| console.error(`❌ 主Agent [${this.name}] 执行失败: ${errorMessage}`); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse: '', | ||
| subAgentResults, | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * 汇总子Agent结果 | ||
| * 主Agent不使用工具,直接调用LLM进行汇总 | ||
| */ | ||
| private async summarizeResults(userInput: string): Promise<string> { | ||
| console.log(`\n📊 主Agent 正在汇总子Agent结果...`); | ||
| // 构建汇总上下文 | ||
| const contextManager = new ContextManager(); | ||
| await contextManager.init(); | ||
| // 设置系统提示词 | ||
| contextManager.add( | ||
| this.systemPrompt, | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| // 设置汇总指令 | ||
| const summarizeInstruction = `基于上述子Agent的研究分析和执行方案,请为用户提供一个综合性的最终答复。 | ||
| 用户原始需求:${userInput} | ||
| 请整合各子Agent的输出,给出完整、清晰的最终响应。`; | ||
| contextManager.setUserInput(summarizeInstruction); | ||
| // 获取上下文并调用LLM(不使用工具) | ||
| const messages = contextManager.getContext(); | ||
| const response = await this.llmService.complete(messages, []); | ||
| return response.content || ''; | ||
| } | ||
| /** | ||
| * 获取执行历史 | ||
| */ | ||
| getExecutionHistory(): Message[] { | ||
| return this.contextManager.get(ContextType.EXECUTION_HISTORY); | ||
| } | ||
| getName(): string { | ||
| return this.name; | ||
| } | ||
| } | ||
| /** | ||
| * 创建多智能体系统的便捷函数 | ||
| */ | ||
| export function createMultiAgentSystem(llmService: ILLMService): MainAgent { | ||
| return new MainAgent(llmService); | ||
| } |
| /** | ||
| * 简单 Agent 实现 | ||
| * 使用 ContextManager + ToolManager + LLMService 实现基本的工具调用循环 | ||
| */ | ||
| import { ContextManager, ContextType } from '../context/index.js'; | ||
| import { ToolManager } from '../tool/ToolManager.js'; | ||
| import { ILLMService, ToolLoopResult } from '../llm/types/index.js'; | ||
| import { executeToolLoop } from '../llm/utils/executeToolLoop.js'; | ||
| import { eventBus } from '../../evaluation/EventBus.js'; | ||
| import { SIMPLE_AGENT_PROMPT } from '../promptManager/index.js'; | ||
| /** | ||
| * Agent 执行结果 | ||
| */ | ||
| export interface AgentResult { | ||
| /** 收集到的 Agent 名称列表 */ | ||
| agents: string[]; | ||
| /** 每个 Agent 调用的工具记录 */ | ||
| tools: Record<string, string[]>; | ||
| /** 最终响应内容 */ | ||
| finalResponse: string; | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * Agent 配置 | ||
| */ | ||
| export interface AgentConfig { | ||
| /** Agent 名称 */ | ||
| name?: string; | ||
| /** 最大工具调用循环次数 */ | ||
| maxLoops?: number; | ||
| /** 系统提示词 */ | ||
| systemPrompt?: string; | ||
| } | ||
| /** | ||
| * 简单 Agent 类 | ||
| * | ||
| * 使用方式: | ||
| * ```typescript | ||
| * const agent = new SimpleAgent(llmService, { name: 'my_agent' }); | ||
| * const result = await agent.run('列出当前目录的文件'); | ||
| * ``` | ||
| */ | ||
| export class SimpleAgent { | ||
| private llmService: ILLMService; | ||
| private contextManager: ContextManager; | ||
| private toolManager: ToolManager; | ||
| private config: Required<AgentConfig>; | ||
| constructor(llmService: ILLMService, config?: AgentConfig) { | ||
| this.llmService = llmService; | ||
| this.contextManager = new ContextManager(); | ||
| this.toolManager = new ToolManager(); | ||
| this.config = { | ||
| name: config?.name ?? 'simple_agent', | ||
| maxLoops: config?.maxLoops ?? 10, | ||
| systemPrompt: config?.systemPrompt ?? SIMPLE_AGENT_PROMPT, | ||
| }; | ||
| } | ||
| /** | ||
| * 执行 Agent | ||
| * @param userInput - 用户输入 | ||
| * @returns Agent 执行结果 | ||
| */ | ||
| async run(userInput: string): Promise<AgentResult> { | ||
| // 重置事件收集器 | ||
| eventBus.reset(); | ||
| // 发射 Agent 调用事件 | ||
| eventBus.emit('agent:call', { agentName: this.config.name }); | ||
| try { | ||
| // 初始化上下文管理器 | ||
| await this.contextManager.init(); | ||
| // 设置系统提示词 | ||
| this.contextManager.add( | ||
| this.config.systemPrompt, | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| // 设置用户输入 | ||
| this.contextManager.setUserInput(userInput); | ||
| // 执行工具循环 | ||
| const loopResult = await executeToolLoop( | ||
| this.llmService, | ||
| this.contextManager, | ||
| this.toolManager, | ||
| { | ||
| maxLoops: this.config.maxLoops, | ||
| agentName: this.config.name, | ||
| } | ||
| ); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse: loopResult.result || '', | ||
| success: loopResult.success, | ||
| error: loopResult.error, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| console.error(`Agent 执行失败: ${errorMessage}`); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse: '', | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * 获取 Agent 名称 | ||
| */ | ||
| getName(): string { | ||
| return this.config.name; | ||
| } | ||
| /** | ||
| * 获取工具管理器 | ||
| */ | ||
| getToolManager(): ToolManager { | ||
| return this.toolManager; | ||
| } | ||
| /** | ||
| * 获取上下文管理器 | ||
| */ | ||
| getContextManager(): ContextManager { | ||
| return this.contextManager; | ||
| } | ||
| } |
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { ContextManager } from '../ContextManager.js'; | ||
| import { ContextType } from '../types.js'; | ||
| describe('ContextManager 测试', () => { | ||
| let contextManager: ContextManager; | ||
| beforeEach(async () => { | ||
| contextManager = new ContextManager(); | ||
| await contextManager.init(); | ||
| }); | ||
| describe('初始化', () => { | ||
| it('应该正确初始化', async () => { | ||
| const manager = new ContextManager(); | ||
| expect(manager.isInitialized()).toBe(false); | ||
| await manager.init(); | ||
| expect(manager.isInitialized()).toBe(true); | ||
| }); | ||
| it('不应该重复初始化', async () => { | ||
| await contextManager.init(); // 第二次调用 | ||
| expect(contextManager.isInitialized()).toBe(true); | ||
| }); | ||
| it('未初始化时应该抛出错误', () => { | ||
| const manager = new ContextManager(); | ||
| expect(() => manager.add('test', ContextType.CONVERSATION)).toThrow( | ||
| 'ContextManager 未初始化' | ||
| ); | ||
| }); | ||
| }); | ||
| describe('添加上下文', () => { | ||
| it('应该能添加会话上下文', () => { | ||
| const message = { role: 'user', content: '你好' }; | ||
| contextManager.add(message, ContextType.CONVERSATION); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(1); | ||
| }); | ||
| it('应该能添加工具上下文', () => { | ||
| const toolCall = { | ||
| id: '1', | ||
| name: 'test_tool', | ||
| arguments: {}, | ||
| result: 'success', | ||
| }; | ||
| contextManager.add(toolCall, ContextType.TOOL); | ||
| expect(contextManager.getCount(ContextType.TOOL)).toBe(1); | ||
| }); | ||
| it('应该能添加记忆上下文', () => { | ||
| const memory = { key: 'user_name', value: '张三' }; | ||
| contextManager.add(memory, ContextType.MEMORY); | ||
| expect(contextManager.getCount(ContextType.MEMORY)).toBe(1); | ||
| }); | ||
| }); | ||
| describe('获取上下文', () => { | ||
| it('应该能获取指定类型的上下文', () => { | ||
| const message = { role: 'user', content: '测试消息' }; | ||
| contextManager.add(message, ContextType.CONVERSATION); | ||
| const contexts = contextManager.get(ContextType.CONVERSATION); | ||
| expect(contexts.length).toBe(1); | ||
| }); | ||
| it('应该能获取所有上下文', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '你好' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { key: 'preference', value: 'dark_mode' }, | ||
| ContextType.MEMORY | ||
| ); | ||
| const allContexts = contextManager.getAll(); | ||
| expect(allContexts.length).toBeGreaterThan(0); | ||
| }); | ||
| it('空上下文应该返回空数组', () => { | ||
| const contexts = contextManager.get(ContextType.CONVERSATION); | ||
| expect(contexts).toEqual([]); | ||
| }); | ||
| }); | ||
| describe('统计信息', () => { | ||
| it('应该正确统计上下文数量', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息1' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { role: 'user', content: '消息2' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { key: 'test', value: 'value' }, | ||
| ContextType.MEMORY | ||
| ); | ||
| const stats = contextManager.getStats(); | ||
| expect(stats.total).toBe(3); | ||
| expect(stats.byType[ContextType.CONVERSATION]).toBe(2); | ||
| expect(stats.byType[ContextType.MEMORY]).toBe(1); | ||
| }); | ||
| it('应该正确检查上下文是否存在', () => { | ||
| expect(contextManager.hasContext(ContextType.CONVERSATION)).toBe(false); | ||
| contextManager.add( | ||
| { role: 'user', content: '测试' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| expect(contextManager.hasContext(ContextType.CONVERSATION)).toBe(true); | ||
| }); | ||
| it('应该正确检查是否为空', () => { | ||
| expect(contextManager.isEmpty()).toBe(true); | ||
| contextManager.add( | ||
| { role: 'user', content: '测试' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| expect(contextManager.isEmpty()).toBe(false); | ||
| }); | ||
| }); | ||
| describe('更新和删除', () => { | ||
| it('应该能更新指定上下文项', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '原始消息' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.update(ContextType.CONVERSATION, 0, { | ||
| role: 'user', | ||
| content: '更新后的消息', | ||
| }); | ||
| const contexts = contextManager.get(ContextType.CONVERSATION); | ||
| expect(contexts[0].content).toContain('更新后的消息'); | ||
| }); | ||
| it('应该能删除最后一项', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息1' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { role: 'user', content: '消息2' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(2); | ||
| contextManager.removeLast(ContextType.CONVERSATION); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(1); | ||
| }); | ||
| it('应该能清空指定类型的上下文', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息1' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { role: 'user', content: '消息2' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.clear(ContextType.CONVERSATION); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(0); | ||
| }); | ||
| it('应该能重置所有上下文', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { key: 'test', value: 'value' }, | ||
| ContextType.MEMORY | ||
| ); | ||
| contextManager.reset(); | ||
| expect(contextManager.isEmpty()).toBe(true); | ||
| }); | ||
| }); | ||
| describe('模块访问', () => { | ||
| it('应该能获取指定类型的模块实例', () => { | ||
| const module = contextManager.getModule(ContextType.CONVERSATION); | ||
| expect(module).toBeDefined(); | ||
| expect(module.type).toBe(ContextType.CONVERSATION); | ||
| }); | ||
| it('应该能获取所有模块', () => { | ||
| const modules = contextManager.getAllModules(); | ||
| expect(modules.size).toBe(5); // 5 种上下文类型 | ||
| }); | ||
| }); | ||
| describe('验证', () => { | ||
| it('应该通过验证', () => { | ||
| expect(contextManager.validate()).toBe(true); | ||
| }); | ||
| it('未初始化的管理器不应通过验证', () => { | ||
| const manager = new ContextManager(); | ||
| expect(manager.validate()).toBe(false); | ||
| }); | ||
| }); | ||
| describe('预留接口', () => { | ||
| it('压缩检查应该返回 false(未实现)', () => { | ||
| expect(contextManager.needsCompression()).toBe(false); | ||
| }); | ||
| it('token 计数应该返回 0(未实现)', async () => { | ||
| const count = await contextManager.getTokenCount(); | ||
| expect(count).toBe(0); | ||
| }); | ||
| it('应该能导出为 JSON', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '测试' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| const json = contextManager.toJSON(); | ||
| expect(json).toBeDefined(); | ||
| expect(typeof json).toBe('string'); | ||
| }); | ||
| }); | ||
| }); | ||
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { ConversationContext } from '../../modules/ConversationContext.js'; | ||
| import { ContextType } from '../../types.js'; | ||
| describe('ConversationContext 测试', () => { | ||
| let context: ConversationContext; | ||
| beforeEach(() => { | ||
| context = new ConversationContext(); | ||
| }); | ||
| it('应该正确初始化', () => { | ||
| expect(context.type).toBe(ContextType.CONVERSATION); | ||
| expect(context.isEmpty()).toBe(true); | ||
| expect(context.getCount()).toBe(0); | ||
| }); | ||
| describe('添加消息', () => { | ||
| it('应该能添加用户消息', () => { | ||
| context.addUserMessage('你好'); | ||
| expect(context.getCount()).toBe(1); | ||
| const messages = context.format(); | ||
| expect(messages[0].role).toBe('user'); | ||
| expect(messages[0].content).toBe('你好'); | ||
| }); | ||
| it('应该能添加助手消息', () => { | ||
| context.addAssistantMessage('你好!有什么可以帮助你的吗?'); | ||
| expect(context.getCount()).toBe(1); | ||
| const messages = context.format(); | ||
| expect(messages[0].role).toBe('assistant'); | ||
| }); | ||
| it('应该能添加系统消息', () => { | ||
| context.addSystemMessage('你是一个有帮助的助手'); | ||
| expect(context.getCount()).toBe(1); | ||
| const messages = context.format(); | ||
| expect(messages[0].role).toBe('system'); | ||
| }); | ||
| it('应该能添加带图片的消息', () => { | ||
| context.addUserMessage('这是什么?', { | ||
| url: 'https://example.com/image.jpg', | ||
| }); | ||
| const messages = context.format(); | ||
| expect(messages[0].content).toBeDefined(); | ||
| expect(Array.isArray(messages[0].content)).toBe(true); | ||
| }); | ||
| it('应该能添加带工具调用的助手消息', () => { | ||
| const toolCalls = [ | ||
| { | ||
| id: 'call_1', | ||
| type: 'function', | ||
| function: { name: 'get_weather', arguments: '{"city": "北京"}' }, | ||
| }, | ||
| ]; | ||
| context.addAssistantMessage('让我查一下天气', toolCalls); | ||
| const messages = context.format(); | ||
| expect(messages[0].tool_calls).toBeDefined(); | ||
| expect(messages[0].tool_calls?.length).toBe(1); | ||
| }); | ||
| }); | ||
| describe('格式化', () => { | ||
| it('应该正确格式化为 LLM 消息格式', () => { | ||
| context.addUserMessage('你好'); | ||
| context.addAssistantMessage('你好!'); | ||
| const formatted = context.format(); | ||
| expect(formatted).toHaveLength(2); | ||
| expect(formatted[0].role).toBe('user'); | ||
| expect(formatted[1].role).toBe('assistant'); | ||
| }); | ||
| it('应该正确格式化带 base64 图片的消息', () => { | ||
| context.addUserMessage('分析这张图片', { | ||
| base64: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', | ||
| mimeType: 'image/png', | ||
| }); | ||
| const formatted = context.format(); | ||
| expect(Array.isArray(formatted[0].content)).toBe(true); | ||
| }); | ||
| }); | ||
| describe('查询方法', () => { | ||
| it('应该能获取最后一条消息', () => { | ||
| context.addUserMessage('第一条消息'); | ||
| context.addUserMessage('第二条消息'); | ||
| const lastMessage = context.getLastMessage(); | ||
| expect(lastMessage?.content).toBe('第二条消息'); | ||
| }); | ||
| it('应该能按角色统计消息数量', () => { | ||
| context.addUserMessage('用户消息1'); | ||
| context.addUserMessage('用户消息2'); | ||
| context.addAssistantMessage('助手消息'); | ||
| expect(context.getCountByRole('user')).toBe(2); | ||
| expect(context.getCountByRole('assistant')).toBe(1); | ||
| }); | ||
| it('应该能获取所有用户消息', () => { | ||
| context.addUserMessage('用户消息1'); | ||
| context.addAssistantMessage('助手消息'); | ||
| context.addUserMessage('用户消息2'); | ||
| const userMessages = context.getUserMessages(); | ||
| expect(userMessages.length).toBe(2); | ||
| expect(userMessages[0].content).toBe('用户消息1'); | ||
| expect(userMessages[1].content).toBe('用户消息2'); | ||
| }); | ||
| it('应该能获取所有助手消息', () => { | ||
| context.addUserMessage('用户消息'); | ||
| context.addAssistantMessage('助手消息1'); | ||
| context.addAssistantMessage('助手消息2'); | ||
| const assistantMessages = context.getAssistantMessages(); | ||
| expect(assistantMessages.length).toBe(2); | ||
| }); | ||
| }); | ||
| describe('基础操作', () => { | ||
| it('应该能清空所有消息', () => { | ||
| context.addUserMessage('测试消息'); | ||
| expect(context.getCount()).toBe(1); | ||
| context.clear(); | ||
| expect(context.getCount()).toBe(0); | ||
| expect(context.isEmpty()).toBe(true); | ||
| }); | ||
| it('应该能删除最后一条消息', () => { | ||
| context.addUserMessage('消息1'); | ||
| context.addUserMessage('消息2'); | ||
| expect(context.getCount()).toBe(2); | ||
| context.removeLast(); | ||
| expect(context.getCount()).toBe(1); | ||
| }); | ||
| it('应该能更新指定消息', () => { | ||
| context.addUserMessage('原始消息'); | ||
| context.update(0, { | ||
| role: 'user', | ||
| content: '更新后的消息', | ||
| }); | ||
| const messages = context.format(); | ||
| expect(messages[0].content).toBe('更新后的消息'); | ||
| }); | ||
| }); | ||
| }); | ||
| import { ContextType, ContextItem, IContext } from '../types.js'; | ||
| import { logger } from '../../../utils/logger.js'; | ||
| /** | ||
| * 基础上下文抽象类 | ||
| * 提供所有上下文模块的通用功能 | ||
| */ | ||
| export abstract class BaseContext<T = any> implements IContext<T> { | ||
| /** 上下文类型 */ | ||
| public readonly type: ContextType; | ||
| /** 存储上下文项的数组 */ | ||
| protected items: ContextItem<T>[] = []; | ||
| constructor(type: ContextType) { | ||
| this.type = type; | ||
| logger.debug(`初始化上下文模块: ${type}`); | ||
| } | ||
| /** | ||
| * 添加上下文项 | ||
| */ | ||
| add(content: T, metadata?: Record<string, any>): void { | ||
| const item: ContextItem<T> = { | ||
| content, | ||
| type: this.type, | ||
| metadata, | ||
| timestamp: Date.now(), | ||
| id: this.generateId(), | ||
| }; | ||
| this.items.push(item); | ||
| logger.debug(`添加上下文项到 ${this.type}: ${this.items.length} 项`); | ||
| } | ||
| /** | ||
| * 获取所有上下文项 | ||
| */ | ||
| getAll(): ContextItem<T>[] { | ||
| return [...this.items]; | ||
| } | ||
| /** | ||
| * 获取指定索引的上下文项 | ||
| */ | ||
| get(index: number): ContextItem<T> | undefined { | ||
| if (index < 0 || index >= this.items.length) { | ||
| logger.warn(`索引 ${index} 超出范围 (0-${this.items.length - 1})`); | ||
| return undefined; | ||
| } | ||
| return this.items[index]; | ||
| } | ||
| /** | ||
| * 清空所有上下文 | ||
| */ | ||
| clear(): void { | ||
| const count = this.items.length; | ||
| this.items = []; | ||
| logger.debug(`清空 ${this.type} 上下文: 移除了 ${count} 项`); | ||
| } | ||
| /** | ||
| * 获取上下文数量 | ||
| */ | ||
| getCount(): number { | ||
| return this.items.length; | ||
| } | ||
| /** | ||
| * 检查是否为空 | ||
| */ | ||
| isEmpty(): boolean { | ||
| return this.items.length === 0; | ||
| } | ||
| /** | ||
| * 移除最后一项 | ||
| */ | ||
| removeLast(): void { | ||
| if (this.items.length > 0) { | ||
| this.items.pop(); | ||
| logger.debug(`从 ${this.type} 移除最后一项`); | ||
| } else { | ||
| logger.warn(`尝试从空的 ${this.type} 上下文移除项`); | ||
| } | ||
| } | ||
| /** | ||
| * 更新指定索引的上下文项 | ||
| */ | ||
| update(index: number, content: T, metadata?: Record<string, any>): void { | ||
| if (index < 0 || index >= this.items.length) { | ||
| logger.error(`无法更新: 索引 ${index} 超出范围`); | ||
| throw new Error(`索引 ${index} 超出范围 (0-${this.items.length - 1})`); | ||
| } | ||
| const oldItem = this.items[index]; | ||
| this.items[index] = { | ||
| ...oldItem, | ||
| content, | ||
| metadata: metadata || oldItem.metadata, | ||
| timestamp: Date.now(), | ||
| }; | ||
| logger.debug(`更新 ${this.type} 上下文项 [${index}]`); | ||
| } | ||
| /** | ||
| * 格式化为特定格式(由子类实现) | ||
| */ | ||
| abstract format(): any[]; | ||
| /** | ||
| * TODO: 后续实现序列化 | ||
| * 转换为 JSON | ||
| */ | ||
| toJSON(): string { | ||
| // TODO: 实现序列化逻辑 | ||
| return JSON.stringify({ | ||
| type: this.type, | ||
| items: this.items, | ||
| }); | ||
| } | ||
| /** | ||
| * TODO: 后续实现序列化 | ||
| * 从 JSON 恢复 | ||
| */ | ||
| fromJSON(json: string): void { | ||
| // TODO: 实现反序列化逻辑 | ||
| try { | ||
| const data = JSON.parse(json); | ||
| if (data.type === this.type && Array.isArray(data.items)) { | ||
| this.items = data.items; | ||
| logger.debug(`从 JSON 恢复 ${this.type} 上下文: ${this.items.length} 项`); | ||
| } else { | ||
| throw new Error('无效的 JSON 数据格式'); | ||
| } | ||
| } catch (error: any) { | ||
| logger.error(`从 JSON 恢复失败: ${error.message}`); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * 生成唯一 ID | ||
| */ | ||
| protected generateId(): string { | ||
| return `${this.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | ||
| } | ||
| } | ||
| import { ContextType, ContextStats, IContext, Message } from "./types.js"; | ||
| import { ConversationContext } from "./modules/ConversationContext.js"; | ||
| import { ToolMessageSequenceContext } from "./modules/ToolMessageSequenceContext.js"; | ||
| import { MemoryContext } from "./modules/MemoryContext.js"; | ||
| import { SystemPromptContext } from "./modules/SystemPromptContext.js"; | ||
| import { StructuredOutputContext } from "./modules/StructuredOutputContext.js"; | ||
| import { RelevantContext } from "./modules/RelevantContext.js"; | ||
| import { ExecutionHistoryContext } from "./modules/ExecutionHistoryContext.js"; | ||
| /** | ||
| * 上下文管理器 | ||
| * 负责统一管理所有类型的上下文模块 | ||
| */ | ||
| export class ContextManager { | ||
| /** 存储所有上下文模块的映射 */ | ||
| private contexts: Map<ContextType, IContext> = new Map(); | ||
| /** 用户输入 */ | ||
| private userInput: string = ""; | ||
| /** 是否已初始化 */ | ||
| private initialized: boolean = false; | ||
| constructor() {} | ||
| /** | ||
| * 初始化所有上下文模块 | ||
| */ | ||
| async init(): Promise<void> { | ||
| if (this.initialized) { | ||
| return; | ||
| } | ||
| // 初始化所有上下文模块 | ||
| this.contexts.set( | ||
| ContextType.CONVERSATION_HISTORY, | ||
| new ConversationContext() | ||
| ); | ||
| this.contexts.set( | ||
| ContextType.TOOL_MESSAGE_SEQUENCE, | ||
| new ToolMessageSequenceContext() | ||
| ); | ||
| this.contexts.set(ContextType.MEMORY, new MemoryContext()); | ||
| this.contexts.set(ContextType.SYSTEM_PROMPT, new SystemPromptContext()); | ||
| this.contexts.set( | ||
| ContextType.STRUCTURED_OUTPUT, | ||
| new StructuredOutputContext() | ||
| ); | ||
| this.contexts.set(ContextType.RELEVANT_CONTEXT, new RelevantContext()); | ||
| this.contexts.set(ContextType.EXECUTION_HISTORY, new ExecutionHistoryContext()); | ||
| this.initialized = true; | ||
| } | ||
| /** | ||
| * 检查是否已初始化 | ||
| */ | ||
| isInitialized(): boolean { | ||
| return this.initialized; | ||
| } | ||
| /** 设置用户输入 */ | ||
| setUserInput(userInput: string): void { | ||
| this.userInput = userInput; | ||
| } | ||
| /** | ||
| * 统一的添加上下文方法 | ||
| * @param content - 上下文内容 | ||
| * @param type - 上下文类型 | ||
| * @param metadata - 可选的元数据 | ||
| * | ||
| * @example | ||
| * // SYSTEM_PROMPT - 字符串 | ||
| * contextManager.add('你是一个助手', ContextType.SYSTEM_PROMPT); | ||
| * | ||
| * // MEMORY - { key, value } 格式 | ||
| * contextManager.add({ key: 'user_name', value: '张三' }, ContextType.MEMORY); | ||
| * | ||
| * // CONVERSATION_HISTORY - { role, content } 格式 | ||
| * contextManager.add({ role: 'user', content: '你好' }, ContextType.CONVERSATION_HISTORY); | ||
| * | ||
| * // TOOL_MESSAGE_SEQUENCE - Message 对象 | ||
| * contextManager.add({ role: 'assistant', content: '...', tool_calls: [...] }, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| * | ||
| * // STRUCTURED_OUTPUT - 字符串 | ||
| * contextManager.add('json', ContextType.STRUCTURED_OUTPUT); | ||
| * | ||
| * // RELEVANT_CONTEXT - { key, value } 格式 | ||
| * contextManager.add({ key: 'scene', value: '客服场景' }, ContextType.RELEVANT_CONTEXT); | ||
| */ | ||
| add(content: any, type: ContextType, metadata?: Record<string, any>): void { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| this.validateContent(content, type); | ||
| context.add(content, metadata); | ||
| } | ||
| /** | ||
| * 校验 content 格式是否匹配 type | ||
| */ | ||
| private validateContent(content: any, type: ContextType): void { | ||
| switch (type) { | ||
| case ContextType.SYSTEM_PROMPT: | ||
| case ContextType.STRUCTURED_OUTPUT: | ||
| case ContextType.EXECUTION_HISTORY: | ||
| if (typeof content !== "string") { | ||
| throw new Error(`${type} 需要字符串类型`); | ||
| } | ||
| break; | ||
| case ContextType.TOOL_MESSAGE_SEQUENCE: | ||
| case ContextType.CONVERSATION_HISTORY: | ||
| if (!content?.role || content?.content === undefined) { | ||
| throw new Error(`${type} 需要 { role, content } 格式的 Message 对象`); | ||
| } | ||
| break; | ||
| case ContextType.MEMORY: | ||
| case ContextType.RELEVANT_CONTEXT: | ||
| if (!content?.key || content?.value === undefined) { | ||
| throw new Error(`${type} 需要 { key, value } 格式`); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * 获取指定类型的上下文 | ||
| * @param type - 上下文类型 | ||
| * @returns 格式化的上下文数据 | ||
| */ | ||
| get(type: ContextType): any[] { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| return context.format(); | ||
| } | ||
| /** | ||
| * 获取完整上下文(供 LLM 调用使用) | ||
| * | ||
| * 自动检测并组装所有可用的上下文类型: | ||
| * - system消息(systemPrompt + structuredOutput + relevantContext + memory + 会话历史摘要) | ||
| * - 用户输入 | ||
| * - 工具消息序列 | ||
| * | ||
| * @returns Message[] 格式的上下文数组 | ||
| */ | ||
| getContext(): Message[] { | ||
| this.ensureInitialized(); | ||
| const messages: Message[] = []; | ||
| // 1. system消息(包含历史摘要) | ||
| const systemMessage = this.buildSystemMessage(); | ||
| if (systemMessage) { | ||
| messages.push(systemMessage); | ||
| } | ||
| // 2. 用户输入(当前请求) | ||
| if (this.userInput) { | ||
| messages.push({ role: "user", content: this.userInput }); | ||
| } | ||
| // 3. 工具消息序列 | ||
| const toolContext = this.contexts.get(ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| if (toolContext && !toolContext.isEmpty()) { | ||
| const toolMessages = toolContext.format(); | ||
| if (toolMessages && toolMessages.length > 0) { | ||
| messages.push(...toolMessages); | ||
| } | ||
| } | ||
| // 4. 执行历史 | ||
| const executionHistory = this.getModule<ExecutionHistoryContext>( | ||
| ContextType.EXECUTION_HISTORY | ||
| ); | ||
| const executionHistoryMessages = executionHistory.format(); | ||
| if (executionHistoryMessages.length > 0) { | ||
| messages.push(...executionHistoryMessages); | ||
| } | ||
| return messages; | ||
| } | ||
| /** | ||
| * 构建system消息(私有方法) | ||
| * 自动拼接: systemPrompt + structuredOutput + relevantContext + memory + 会话历史摘要 | ||
| * | ||
| * @returns Message 对象或 null | ||
| */ | ||
| private buildSystemMessage(): Message | null { | ||
| const parts: string[] = []; | ||
| // 1. 系统提示词(必需) | ||
| const systemPromptContext = this.getModule<SystemPromptContext>( | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| const systemPrompts = systemPromptContext.formatNormal(); | ||
| if (systemPrompts) { | ||
| parts.push(systemPrompts); | ||
| } | ||
| // 2. 结构化输出要求 | ||
| const structuredOutputContext = this.getModule<StructuredOutputContext>( | ||
| ContextType.STRUCTURED_OUTPUT | ||
| ); | ||
| const structuredOutput = structuredOutputContext.format(); | ||
| if (structuredOutput.length > 0) { | ||
| parts.push("\n【结构化输出要求】"); | ||
| parts.push(structuredOutput[0]); | ||
| } | ||
| // 3. 相关上下文 | ||
| const relevantContext = this.getModule<RelevantContext>( | ||
| ContextType.RELEVANT_CONTEXT | ||
| ); | ||
| const relevantInfo = relevantContext.format(); | ||
| if (relevantInfo.length > 0) { | ||
| parts.push("\n【相关上下文】"); | ||
| parts.push(relevantInfo.join("\n")); | ||
| } | ||
| // 4. 用户记忆 | ||
| const memoryContext = this.getModule<MemoryContext>(ContextType.MEMORY); | ||
| const memories = memoryContext.format(); | ||
| if (memories.length > 0) { | ||
| parts.push("\n【用户记忆】"); | ||
| parts.push(memories.join("\n")); | ||
| } | ||
| // 5. 会话历史摘要 | ||
| const historySummary = this.formatConversationHistoryForPrompt(); | ||
| if (historySummary) { | ||
| parts.push(historySummary); | ||
| } | ||
| // 如果没有任何内容,返回null | ||
| if (parts.length === 0) { | ||
| return null; | ||
| } | ||
| return { | ||
| role: "system", | ||
| content: parts.join("\n"), | ||
| }; | ||
| } | ||
| /** | ||
| * 将会话历史格式化为简洁列表(用于系统提示词) | ||
| * 只取最近15条,以简洁形式呈现 | ||
| * | ||
| * @returns 格式化的历史摘要字符串,或 null | ||
| */ | ||
| private formatConversationHistoryForPrompt(): string | null { | ||
| const conversationContext = this.contexts.get( | ||
| ContextType.CONVERSATION_HISTORY | ||
| ); | ||
| if (!conversationContext || conversationContext.isEmpty()) { | ||
| return null; | ||
| } | ||
| const history = conversationContext.format(); | ||
| if (!history || history.length === 0) { | ||
| return null; | ||
| } | ||
| // 取最近15条 | ||
| const recentHistory = history.slice(-15); | ||
| const lines: string[] = []; | ||
| lines.push("\n## 【历史对话参考】"); | ||
| lines.push(""); | ||
| lines.push("最近对话记录:"); | ||
| recentHistory.forEach((msg, idx) => { | ||
| const role = msg.role === "user" ? "[用户]" : "[助手]"; | ||
| let summary = ""; | ||
| try { | ||
| const content = JSON.parse(msg.content as string); | ||
| if (msg.role === "user") { | ||
| summary = content.text || msg.content; | ||
| } else { | ||
| summary = | ||
| content.action === "finish" | ||
| ? `已完成: ${(content.result || "").slice(0, 50)}...` | ||
| : content.action || (msg.content as string).slice(0, 50); | ||
| } | ||
| } catch { | ||
| summary = | ||
| typeof msg.content === "string" | ||
| ? msg.content.slice(0, 50) | ||
| : String(msg.content); | ||
| } | ||
| lines.push(`${idx + 1}. ${role} ${summary}`); | ||
| }); | ||
| return lines.join("\n"); | ||
| } | ||
| /** | ||
| * 添加相关上下文 | ||
| */ | ||
| addRelevantContext(key: string, value: any, description?: string): void { | ||
| this.ensureInitialized(); | ||
| this.add({ key, value, description }, ContextType.RELEVANT_CONTEXT); | ||
| } | ||
| /** | ||
| * 获取相关上下文值 | ||
| */ | ||
| getRelevantContextValue(key: string): any { | ||
| this.ensureInitialized(); | ||
| const relevantContext = this.getModule<RelevantContext>( | ||
| ContextType.RELEVANT_CONTEXT | ||
| ); | ||
| return relevantContext.getValue(key); | ||
| } | ||
| /** | ||
| * 更新相关上下文 | ||
| */ | ||
| updateRelevantContext(key: string, value: any): void { | ||
| this.ensureInitialized(); | ||
| const relevantContext = this.getModule<RelevantContext>( | ||
| ContextType.RELEVANT_CONTEXT | ||
| ); | ||
| relevantContext.updateValue(key, value); | ||
| } | ||
| /** | ||
| * 获取指定类型上下文的数量 | ||
| */ | ||
| getCount(type: ContextType): number { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| return context.getCount(); | ||
| } | ||
| /** | ||
| * 获取所有上下文的统计信息 | ||
| */ | ||
| getStats(): ContextStats { | ||
| this.ensureInitialized(); | ||
| let total = 0; | ||
| const byType: Record<string, number> = {}; | ||
| this.contexts.forEach((context, type) => { | ||
| const count = context.getCount(); | ||
| total += count; | ||
| byType[type] = count; | ||
| }); | ||
| return { | ||
| total, | ||
| byType, | ||
| tokenCount: undefined, | ||
| }; | ||
| } | ||
| /** | ||
| * 检查指定类型上下文是否存在 | ||
| */ | ||
| hasContext(type: ContextType): boolean { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| return context ? !context.isEmpty() : false; | ||
| } | ||
| /** | ||
| * 检查上下文是否为空 | ||
| */ | ||
| isEmpty(): boolean { | ||
| this.ensureInitialized(); | ||
| return Array.from(this.contexts.values()).every((context) => | ||
| context.isEmpty() | ||
| ); | ||
| } | ||
| /** | ||
| * 清空指定类型的上下文 | ||
| */ | ||
| clear(type: ContextType): void { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| context.clear(); | ||
| } | ||
| /** | ||
| * 重置所有上下文(清空状态) | ||
| */ | ||
| reset(): void { | ||
| this.ensureInitialized(); | ||
| this.contexts.forEach((context) => { | ||
| context.clear(); | ||
| }); | ||
| this.userInput = ""; | ||
| } | ||
| /** | ||
| * 获取指定类型的上下文模块实例 | ||
| */ | ||
| getModule<T extends IContext>(type: ContextType): T { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| return context as T; | ||
| } | ||
| /** | ||
| * 打印当前上下文状态(调试用) | ||
| */ | ||
| debug(): void { | ||
| this.ensureInitialized(); | ||
| console.log("=== ContextManager 状态 ==="); | ||
| console.log(`已初始化: ${this.initialized}`); | ||
| console.log(`总计: ${this.getStats().total} 项`); | ||
| console.log("\n各类型统计:"); | ||
| this.contexts.forEach((context, type) => { | ||
| console.log(` ${type}: ${context.getCount()} 项`); | ||
| }); | ||
| console.log("========================\n"); | ||
| } | ||
| /** | ||
| * 确保已初始化 | ||
| */ | ||
| private ensureInitialized(): void { | ||
| if (!this.initialized) { | ||
| throw new Error("ContextManager 未初始化,请先调用 init() 方法"); | ||
| } | ||
| } | ||
| } |
| /** | ||
| * 上下文管理模块统一导出 | ||
| */ | ||
| // 类型定义 | ||
| export * from './types.js'; | ||
| // 基础类 | ||
| export { BaseContext } from './base/BaseContext.js'; | ||
| // 核心管理器 | ||
| export { ContextManager } from './ContextManager.js'; | ||
| // 具体上下文模块 | ||
| export { ConversationContext } from './modules/ConversationContext.js'; | ||
| export { ToolMessageSequenceContext } from './modules/ToolMessageSequenceContext.js'; | ||
| export { MemoryContext } from './modules/MemoryContext.js'; | ||
| export { SystemPromptContext } from './modules/SystemPromptContext.js'; | ||
| export { StructuredOutputContext } from './modules/StructuredOutputContext.js'; | ||
| export { RelevantContext } from './modules/RelevantContext.js'; |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, ConversationMessage } from '../types.js'; | ||
| /** | ||
| * 会话上下文管理类 | ||
| * 负责管理用户和助手之间的对话历史 | ||
| */ | ||
| export class ConversationContext extends BaseContext<ConversationMessage> { | ||
| constructor() { | ||
| super(ContextType.CONVERSATION_HISTORY); | ||
| } | ||
| /** | ||
| * 格式化为 LLM 消息格式 | ||
| * 转换为标准的 OpenAI 消息格式 | ||
| */ | ||
| format(): any[] { | ||
| return this.items.map((item) => { | ||
| const message = item.content; | ||
| const formatted: any = { | ||
| role: message.role, | ||
| content: message.content, | ||
| }; | ||
| // 添加图片数据(如果存在) | ||
| if (message.imageData) { | ||
| if (message.imageData.url) { | ||
| formatted.content = [ | ||
| { type: 'text', text: message.content }, | ||
| { | ||
| type: 'image_url', | ||
| image_url: { url: message.imageData.url }, | ||
| }, | ||
| ]; | ||
| } else if (message.imageData.base64) { | ||
| formatted.content = [ | ||
| { type: 'text', text: message.content }, | ||
| { | ||
| type: 'image_url', | ||
| image_url: { | ||
| url: `data:${message.imageData.mimeType || 'image/png'};base64,${message.imageData.base64}`, | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
| } | ||
| // 添加工具调用(如果存在) | ||
| if (message.toolCalls && message.toolCalls.length > 0) { | ||
| formatted.tool_calls = message.toolCalls; | ||
| } | ||
| // 添加工具调用 ID(如果是工具消息) | ||
| if (message.role === 'tool' && item.metadata?.toolCallId) { | ||
| formatted.tool_call_id = item.metadata.toolCallId; | ||
| } | ||
| return formatted; | ||
| }); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, Message } from '../types.js'; | ||
| /** | ||
| * 执行历史上下文管理类 | ||
| * 专门用于主Agent记录子Agent的执行结果 | ||
| * 存储字符串,格式化时转换为 user 消息 | ||
| */ | ||
| export class ExecutionHistoryContext extends BaseContext<string> { | ||
| constructor() { | ||
| super(ContextType.EXECUTION_HISTORY); | ||
| } | ||
| /** | ||
| * 格式化为 Message 数组 | ||
| * 将存储的字符串转换为 user 角色的消息 | ||
| */ | ||
| format(): Message[] { | ||
| return this.items.map(item => ({ | ||
| role: 'user' as const, | ||
| content: item.content, | ||
| })); | ||
| } | ||
| /** | ||
| * 获取最后一次执行记录(字符串形式) | ||
| */ | ||
| getLastExecutionRecord(): string | undefined { | ||
| if (this.items.length === 0) return undefined; | ||
| return this.items[this.items.length - 1].content; | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, MemoryItem } from '../types.js'; | ||
| /** | ||
| * 用户记忆上下文管理类 | ||
| * 负责管理用户偏好、历史信息等长期记忆 | ||
| */ | ||
| export class MemoryContext extends BaseContext<MemoryItem> { | ||
| /** 用于快速查找的键值映射 */ | ||
| private keyMap: Map<string, number> = new Map(); | ||
| constructor() { | ||
| super(ContextType.MEMORY); | ||
| } | ||
| /** | ||
| * 重写 add 方法以维护 keyMap | ||
| */ | ||
| add(content: MemoryItem, metadata?: Record<string, any>): void { | ||
| // 检查是否已存在 | ||
| const existingIndex = this.keyMap.get(content.key); | ||
| if (existingIndex !== undefined) { | ||
| // 更新已存在的记忆 | ||
| this.update(existingIndex, content); | ||
| } else { | ||
| // 添加新记忆 | ||
| super.add(content, metadata); | ||
| this.keyMap.set(content.key, this.items.length - 1); | ||
| } | ||
| } | ||
| /** | ||
| * 获取指定键的记忆值 | ||
| */ | ||
| getMemory(key: string): any | undefined { | ||
| const index = this.keyMap.get(key); | ||
| if (index !== undefined) { | ||
| return this.items[index]?.content.value; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * 检查是否存在指定键的记忆 | ||
| */ | ||
| hasMemory(key: string): boolean { | ||
| return this.keyMap.has(key); | ||
| } | ||
| /** | ||
| * 格式化为自然语言描述 | ||
| */ | ||
| format(): string[] { | ||
| // 按优先级排序(优先级越小越靠前) | ||
| const sortedItems = [...this.items].sort((a, b) => { | ||
| const priorityA = a.content.priority ?? 999; | ||
| const priorityB = b.content.priority ?? 999; | ||
| return priorityA - priorityB; | ||
| }); | ||
| return sortedItems.map((item) => { | ||
| const memory = item.content; | ||
| let text = `${memory.key}: ${JSON.stringify(memory.value)}`; | ||
| if (memory.description) { | ||
| text += ` (${memory.description})`; | ||
| } | ||
| return text; | ||
| }); | ||
| } | ||
| /** | ||
| * 清空所有记忆 | ||
| */ | ||
| clear(): void { | ||
| super.clear(); | ||
| this.keyMap.clear(); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType } from '../types.js'; | ||
| /** | ||
| * 相关上下文项 | ||
| */ | ||
| export interface RelevantContextItem { | ||
| /** 上下文键(如 "known_roles", "scene_info") */ | ||
| key: string; | ||
| /** 上下文值(灵活类型) */ | ||
| value: any; | ||
| /** 描述 */ | ||
| description?: string; | ||
| } | ||
| /** | ||
| * 相关上下文管理类 | ||
| * | ||
| * 用于存储问题相关的背景知识,这些知识: | ||
| * - 不属于用户记忆(非用户个人信息) | ||
| * - 不属于会话历史(非对话记录) | ||
| * - 不属于工具输出(非工具执行结果) | ||
| * - 是为了解决当前问题而需要的临时背景信息 | ||
| */ | ||
| export class RelevantContext extends BaseContext<RelevantContextItem> { | ||
| constructor() { | ||
| super(ContextType.RELEVANT_CONTEXT); | ||
| } | ||
| /** | ||
| * 获取指定键的值 | ||
| */ | ||
| getValue(key: string): any | undefined { | ||
| const item = this.items.find((item) => item.content.key === key); | ||
| return item?.content.value; | ||
| } | ||
| /** | ||
| * 更新指定键的值 | ||
| */ | ||
| updateValue(key: string, value: any): void { | ||
| const index = this.items.findIndex((item) => item.content.key === key); | ||
| if (index !== -1) { | ||
| const existingItem = this.items[index].content; | ||
| this.update(index, { ...existingItem, value }); | ||
| } | ||
| } | ||
| /** | ||
| * 检查是否存在指定键 | ||
| */ | ||
| hasKey(key: string): boolean { | ||
| return this.items.some((item) => item.content.key === key); | ||
| } | ||
| /** | ||
| * 格式化为文本数组(用于拼接到 prompt) | ||
| */ | ||
| format(): string[] { | ||
| return this.items.map((item) => { | ||
| const { key, value, description } = item.content; | ||
| const desc = description ? ` (${description})` : ''; | ||
| // 根据值类型格式化 | ||
| if (Array.isArray(value)) { | ||
| return `${key}${desc}: ${value.join('、')}`; | ||
| } else if (typeof value === 'object' && value !== null) { | ||
| return `${key}${desc}: ${JSON.stringify(value)}`; | ||
| } else { | ||
| return `${key}${desc}: ${value}`; | ||
| } | ||
| }); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, StructuredOutputSchema } from '../types.js'; | ||
| /** | ||
| * 结构化输出上下文管理类 | ||
| * 负责管理结构化输出的 Schema 定义 | ||
| */ | ||
| export class StructuredOutputContext extends BaseContext<StructuredOutputSchema> { | ||
| constructor() { | ||
| super(ContextType.STRUCTURED_OUTPUT); | ||
| } | ||
| /** | ||
| * 格式化为 LLM 理解的格式 | ||
| * 转换为 OpenAI 的 response_format 格式 | ||
| */ | ||
| format(): any[] { | ||
| if (this.isEmpty()) { | ||
| return []; | ||
| } | ||
| // item.content 就是字符串(如 'json') | ||
| const latestItem = this.items[this.items.length - 1]; | ||
| const type = latestItem.content; | ||
| return [ | ||
| { | ||
| type: type, | ||
| }, | ||
| ]; | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, SystemPromptItem } from '../types.js'; | ||
| /** | ||
| * 系统提示词上下文管理类 | ||
| * 负责管理系统级提示词,支持多段提示词和优先级 | ||
| */ | ||
| export class SystemPromptContext extends BaseContext<SystemPromptItem> { | ||
| constructor() { | ||
| super(ContextType.SYSTEM_PROMPT); | ||
| } | ||
| /** | ||
| * 格式化为单一系统消息 | ||
| * 合并所有启用的提示词,按优先级排序 | ||
| */ | ||
| format(): any[] { | ||
| if (this.items.length === 0) { | ||
| return []; | ||
| } | ||
| // item.content 就是字符串 | ||
| const combinedContent = this.items.map((item) => item.content).join('\n\n'); | ||
| return [ | ||
| { | ||
| role: 'system', | ||
| content: combinedContent, | ||
| }, | ||
| ]; | ||
| } | ||
| /** | ||
| * 格式化为普通字符串(用于合并到系统消息中) | ||
| * @returns 合并后的提示词文本,或 null | ||
| */ | ||
| formatNormal(): string | null { | ||
| if (this.items.length === 0) { | ||
| return null; | ||
| } | ||
| return this.items.map((item) => item.content).join('\n\n'); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, Message } from '../types.js'; | ||
| /** | ||
| * 工具消息序列上下文管理类 | ||
| * 用于存储工具调用循环中的消息序列 | ||
| * 顺序: assistant (含tool_calls) → tool → assistant → tool → ... | ||
| */ | ||
| export class ToolMessageSequenceContext extends BaseContext<Message> { | ||
| constructor() { | ||
| super(ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| } | ||
| /** | ||
| * 格式化为 Message 数组 (API 格式) | ||
| */ | ||
| format(): Message[] { | ||
| return this.items.map((item) => item.content); | ||
| } | ||
| } |
| /** | ||
| * 上下文管理系统的核心类型定义 | ||
| */ | ||
| /** | ||
| * 上下文类型枚举 | ||
| */ | ||
| export enum ContextType { | ||
| /** 会话历史上下文 */ | ||
| CONVERSATION_HISTORY = 'conversation_history', | ||
| /** 工具消息序列上下文 */ | ||
| TOOL_MESSAGE_SEQUENCE = 'tool_message_sequence', | ||
| /** 用户记忆上下文 */ | ||
| MEMORY = 'memory', | ||
| /** 系统提示词上下文 */ | ||
| SYSTEM_PROMPT = 'system_prompt', | ||
| /** 结构化输出上下文 */ | ||
| STRUCTURED_OUTPUT = 'structured_output', | ||
| /** 相关上下文 */ | ||
| RELEVANT_CONTEXT = 'relevant_context', | ||
| /** 执行历史上下文 */ | ||
| EXECUTION_HISTORY = 'execution_history', | ||
| } | ||
| /** | ||
| * 单个上下文项的结构 | ||
| */ | ||
| export interface ContextItem<T = any> { | ||
| /** 上下文内容 */ | ||
| content: T; | ||
| /** 上下文类型 */ | ||
| type: ContextType; | ||
| /** 元数据 */ | ||
| metadata?: Record<string, any>; | ||
| /** 时间戳 */ | ||
| timestamp: number; | ||
| /** 唯一标识(可选) */ | ||
| id?: string; | ||
| } | ||
| /** | ||
| * 统计信息结构 | ||
| */ | ||
| export interface ContextStats { | ||
| /** 总计数量 */ | ||
| total: number; | ||
| /** 按类型分组的数量 */ | ||
| byType: Record<string, number>; | ||
| /** 总 token 数(预留) */ | ||
| tokenCount?: number; | ||
| } | ||
| /** | ||
| * 图片数据 | ||
| */ | ||
| export interface ImageData { | ||
| url?: string; | ||
| base64?: string; | ||
| mimeType?: string; | ||
| } | ||
| /** | ||
| * 消息角色 | ||
| */ | ||
| export type MessageRole = 'user' | 'assistant' | 'system' | 'tool'; | ||
| /** | ||
| * 标准消息结构(用于 LLM API 调用) | ||
| */ | ||
| export interface Message { | ||
| role: MessageRole; | ||
| content: string; | ||
| tool_calls?: any[]; | ||
| tool_call_id?: string; | ||
| name?: string; | ||
| } | ||
| /** | ||
| * 会话消息结构 | ||
| */ | ||
| export interface ConversationMessage { | ||
| role: MessageRole; | ||
| content: string; | ||
| imageData?: ImageData; | ||
| toolCalls?: any[]; | ||
| } | ||
| /** | ||
| * 用户记忆项结构 | ||
| */ | ||
| export interface MemoryItem { | ||
| /** 记忆键 */ | ||
| key: string; | ||
| /** 记忆值 */ | ||
| value: any; | ||
| /** 描述 */ | ||
| description?: string; | ||
| /** 优先级 */ | ||
| priority?: number; | ||
| } | ||
| /** | ||
| * 系统提示词项结构 | ||
| */ | ||
| export interface SystemPromptItem { | ||
| /** 提示词内容 */ | ||
| content: string; | ||
| /** 优先级(数字越小优先级越高) */ | ||
| priority?: number; | ||
| /** 是否启用 */ | ||
| enabled?: boolean; | ||
| } | ||
| /** | ||
| * 结构化输出 Schema | ||
| */ | ||
| export interface StructuredOutputSchema { | ||
| /** Schema 类型(如 json_schema) */ | ||
| type: string; | ||
| /** Schema 定义 */ | ||
| schema: Record<string, any>; | ||
| /** 输出格式(json、yaml 等) */ | ||
| format?: string; | ||
| /** 是否严格模式 */ | ||
| strict?: boolean; | ||
| } | ||
| /** | ||
| * 基础上下文接口 | ||
| * 所有具体上下文模块必须实现的方法 | ||
| */ | ||
| export interface IContext<T = any> { | ||
| /** 上下文类型 */ | ||
| readonly type: ContextType; | ||
| /** | ||
| * 添加上下文项 | ||
| * @param content - 内容 | ||
| * @param metadata - 元数据 | ||
| */ | ||
| add(content: T, metadata?: Record<string, any>): void; | ||
| /** | ||
| * 获取所有上下文项 | ||
| */ | ||
| getAll(): ContextItem<T>[]; | ||
| /** | ||
| * 获取指定索引的上下文项 | ||
| * @param index - 索引 | ||
| */ | ||
| get(index: number): ContextItem<T> | undefined; | ||
| /** | ||
| * 清空所有上下文 | ||
| */ | ||
| clear(): void; | ||
| /** | ||
| * 获取上下文数量 | ||
| */ | ||
| getCount(): number; | ||
| /** | ||
| * 检查是否为空 | ||
| */ | ||
| isEmpty(): boolean; | ||
| /** | ||
| * 格式化为特定格式(由子类实现) | ||
| */ | ||
| format(): any[]; | ||
| /** | ||
| * 移除最后一项 | ||
| */ | ||
| removeLast(): void; | ||
| /** | ||
| * 更新指定索引的上下文项 | ||
| * @param index - 索引 | ||
| * @param content - 新内容 | ||
| * @param metadata - 新元数据 | ||
| */ | ||
| update(index: number, content: T, metadata?: Record<string, any>): void; | ||
| /** | ||
| * 转换为 JSON | ||
| */ | ||
| toJSON(): string; | ||
| /** | ||
| * 从 JSON 恢复 | ||
| */ | ||
| fromJSON(json: string): void; | ||
| } |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||
| import { | ||
| extractApiKey, | ||
| getBaseURL, | ||
| getDefaultContextWindow, | ||
| } from '../utils/helpers.js'; | ||
| import { LLMConfig } from '../types/index.js'; | ||
| describe('辅助函数测试', () => { | ||
| describe('extractApiKey', () => { | ||
| it('应该优先使用用户传递的 API Key', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openai', | ||
| model: 'gpt-4', | ||
| apiKey: 'user-provided-key', | ||
| }; | ||
| expect(extractApiKey(config)).toBe('user-provided-key'); | ||
| }); | ||
| it('应该从环境变量获取 API Key(当用户未传递时)', () => { | ||
| // 注意:这个测试依赖于实际的环境变量加载 | ||
| // 如果 .env 中有 DEEPSEEK_API_KEY,则会返回该值 | ||
| const config: LLMConfig = { | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| }; | ||
| // 由于依赖环境变量,我们只测试不抛出错误即可 | ||
| // 或者可以 mock getLLMConfig 函数 | ||
| const result = extractApiKey(config); | ||
| expect(typeof result).toBe('string'); | ||
| }); | ||
| it('无需 API Key 的提供商应该返回 not-required', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'ollama', | ||
| model: 'llama2', | ||
| }; | ||
| expect(extractApiKey(config)).toBe('not-required'); | ||
| }); | ||
| it('缺少 API Key 时应该抛出清晰的错误信息', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'unknown-provider', | ||
| model: 'some-model', | ||
| }; | ||
| expect(() => extractApiKey(config)).toThrow( | ||
| /API key for provider "unknown-provider" not found/ | ||
| ); | ||
| }); | ||
| }); | ||
| describe('getBaseURL', () => { | ||
| it('应该优先使用用户传递的 baseURL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openai', | ||
| model: 'gpt-4', | ||
| baseURL: 'https://custom.api.com', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('https://custom.api.com'); | ||
| }); | ||
| it('应该从环境变量获取 baseURL(当用户未传递时)', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| }; | ||
| // 会返回环境变量或默认值 | ||
| const result = getBaseURL(config); | ||
| expect(result).toBeTruthy(); | ||
| expect(typeof result).toBe('string'); | ||
| }); | ||
| it('应该返回默认 Base URL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openai', | ||
| model: 'gpt-4', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('https://api.openai.com/v1'); | ||
| }); | ||
| it('应该返回 OpenRouter 的默认 URL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openrouter', | ||
| model: 'gpt-4', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('https://openrouter.ai/api/v1'); | ||
| }); | ||
| it('应该返回 Ollama 的默认 URL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'ollama', | ||
| model: 'llama2', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('http://localhost:11434/v1'); | ||
| }); | ||
| it('未知提供商应该抛出错误', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'unknown-provider', | ||
| model: 'some-model', | ||
| }; | ||
| expect(() => getBaseURL(config)).toThrow( | ||
| /No base URL found for provider "unknown-provider"/ | ||
| ); | ||
| }); | ||
| }); | ||
| describe('getDefaultContextWindow', () => { | ||
| it('应该返回 GPT-4o 的上下文窗口', () => { | ||
| expect(getDefaultContextWindow('openai', 'gpt-4o')).toBe(128000); | ||
| }); | ||
| it('应该返回默认上下文窗口', () => { | ||
| expect(getDefaultContextWindow('openai', 'unknown-model')).toBe(8192); | ||
| }); | ||
| it('应该返回提供商的默认值', () => { | ||
| expect(getDefaultContextWindow('anthropic')).toBe(200000); | ||
| }); | ||
| it('未知提供商应该返回默认值 8192', () => { | ||
| expect(getDefaultContextWindow('unknown-provider')).toBe(8192); | ||
| }); | ||
| }); | ||
| }); |
| import { ILLMService, LLMConfig, UnifiedToolManager } from './types/index.js'; | ||
| import { DeepSeekService } from './services/DeepSeekService.js'; | ||
| import { extractApiKey, getBaseURL } from './utils/helpers.js'; | ||
| /** | ||
| * 公共工厂函数:创建 LLM 服务实例(支持可选的工具管理器) | ||
| * | ||
| * @param config - LLM 配置(包括 provider、model、apiKey 等) | ||
| * @param toolManager - 可选的工具管理器实例 | ||
| * @param eventManager - 可选的事件管理器实例 | ||
| * @returns Promise<ILLMService> 实例 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { ToolManager } from '../tool/ToolManager.js'; | ||
| * | ||
| * const toolManager = new ToolManager(); | ||
| * const service = await createLLMService( | ||
| * { | ||
| * provider: 'deepseek', | ||
| * model: 'deepseek-chat', | ||
| * apiKey: 'your-api-key', | ||
| * maxIterations: 10 | ||
| * }, | ||
| * toolManager | ||
| * ); | ||
| * | ||
| * // 使用服务 | ||
| * const response = await service.complete(messages, tools); | ||
| * ``` | ||
| */ | ||
| export async function createLLMService( | ||
| config: LLMConfig, | ||
| toolManager?: UnifiedToolManager, | ||
| eventManager?: any | ||
| ): Promise<ILLMService> { | ||
| // 1. 创建服务实例 | ||
| const service = await _createLLMService(config, toolManager); | ||
| // 2. 设置事件管理器(如果提供且服务支持) | ||
| if (eventManager && typeof (service as any).setEventManager === 'function') { | ||
| (service as any).setEventManager(eventManager); | ||
| } | ||
| return service; | ||
| } | ||
| /** | ||
| * 内部函数:创建 LLM 服务实例 | ||
| */ | ||
| async function _createLLMService( | ||
| config: LLMConfig, | ||
| toolManager?: UnifiedToolManager | ||
| ): Promise<ILLMService> { | ||
| // 1. 提取和验证 API Key | ||
| const apiKey = extractApiKey(config); | ||
| // 2. 获取 Base URL | ||
| const baseURL = getBaseURL(config); | ||
| // 3. 根据 provider 创建服务 | ||
| switch (config.provider.toLowerCase()) { | ||
| case 'deepseek': { | ||
| // 使用 ES 动态导入 OpenAI SDK | ||
| const { default: OpenAI } = await import('openai'); | ||
| const openai = new OpenAI({ apiKey, baseURL }); | ||
| return new DeepSeekService( | ||
| openai, | ||
| config.model || 'deepseek-chat', | ||
| { | ||
| baseURL, | ||
| maxRetries: 3, | ||
| toolManager, | ||
| maxIterations: config.maxIterations || 5, | ||
| } | ||
| ); | ||
| } | ||
| // 🟡 可扩展:其他提供商 | ||
| // case 'openai': | ||
| // case 'anthropic': | ||
| // case 'qwen': | ||
| // case 'siliconflow': | ||
| // case 'openrouter': | ||
| default: | ||
| throw new Error(`Unsupported LLM provider: ${config.provider}`); | ||
| } | ||
| } |
| // 导出核心类型 | ||
| export * from './types/index.js'; | ||
| // 导出服务实现 | ||
| export { DeepSeekService } from './services/DeepSeekService.js'; | ||
| // 导出工厂方法 | ||
| export { createLLMService } from './factory.js'; | ||
| // 导出辅助函数 | ||
| export { | ||
| extractApiKey, | ||
| getBaseURL, | ||
| getDefaultContextWindow, | ||
| sleep, | ||
| } from './utils/helpers.js'; |
| import OpenAI from 'openai'; | ||
| import { | ||
| ILLMService, | ||
| LLMResponse, | ||
| LLMChatOptions, | ||
| ImageData, | ||
| ToolSet, | ||
| UnifiedToolManager, | ||
| } from '../types/index.js'; | ||
| import { logger } from '../../../utils/logger.js'; | ||
| import { sleep } from '../utils/helpers.js'; | ||
| /** | ||
| * DeepSeek LLM 服务 | ||
| * 使用 OpenAI SDK(DeepSeek 兼容 OpenAI API) | ||
| * | ||
| * 提供两种使用方式: | ||
| * 1. complete() - 上下文补全(推荐) | ||
| * 2. generate() - 内置工具调用循环(可选) | ||
| */ | ||
| export class DeepSeekService implements ILLMService { | ||
| private client: OpenAI; | ||
| private model: string; | ||
| private maxRetries: number; | ||
| private toolManager?: UnifiedToolManager; | ||
| private maxIterations: number; | ||
| constructor( | ||
| openai: OpenAI, | ||
| model: string, | ||
| options?: { | ||
| baseURL?: string; | ||
| maxRetries?: number; | ||
| toolManager?: UnifiedToolManager; | ||
| maxIterations?: number; | ||
| } | ||
| ) { | ||
| this.client = openai; | ||
| this.model = model; | ||
| this.maxRetries = options?.maxRetries || 3; | ||
| this.toolManager = options?.toolManager; | ||
| this.maxIterations = options?.maxIterations || 5; | ||
| logger.debug(`初始化 DeepSeekService: model=${model}`); | ||
| } | ||
| /** | ||
| * 核心方法:上下文补全 | ||
| * 接收格式化的上下文(消息历史),返回模型响应 | ||
| * @param messages - 上下文消息列表 | ||
| * @param tools - 可用的工具定义列表 | ||
| * @param options - 生成参数(temperature, maxTokens 等) | ||
| * @returns 模型响应(包含内容、工具调用、使用统计) | ||
| */ | ||
| async complete( | ||
| messages: any[], | ||
| tools?: any[], | ||
| options?: LLMChatOptions | ||
| ): Promise<LLMResponse> { | ||
| let attempt = 0; | ||
| while (attempt < this.maxRetries) { | ||
| attempt++; | ||
| try { | ||
| logger.debug( | ||
| `调用 DeepSeek API (尝试 ${attempt}/${this.maxRetries}): ${messages.length} 条消息, ${tools?.length || 0} 个工具` | ||
| ); | ||
| const response = await this.client.chat.completions.create({ | ||
| model: this.model, | ||
| messages, | ||
| tools: tools && tools.length > 0 ? tools : undefined, | ||
| tool_choice: options?.toolChoice, | ||
| temperature: options?.temperature, | ||
| max_tokens: options?.maxTokens, | ||
| top_p: options?.topP, | ||
| frequency_penalty: options?.frequencyPenalty, | ||
| presence_penalty: options?.presencePenalty, | ||
| stop: options?.stop, | ||
| response_format: options?.responseFormat, | ||
| }); | ||
| const message = response.choices[0]?.message; | ||
| if (!message) { | ||
| throw new Error('DeepSeek API 返回空响应'); | ||
| } | ||
| const result: LLMResponse = { | ||
| content: message.content || '', | ||
| toolCalls: message.tool_calls as any, | ||
| finishReason: response.choices[0]?.finish_reason, | ||
| usage: response.usage | ||
| ? { | ||
| promptTokens: response.usage.prompt_tokens, | ||
| completionTokens: response.usage.completion_tokens, | ||
| totalTokens: response.usage.total_tokens, | ||
| } | ||
| : undefined, | ||
| }; | ||
| logger.debug( | ||
| `DeepSeek 响应: ${result.content.slice(0, 100)}${result.content.length > 100 ? '...' : ''}, 工具调用: ${result.toolCalls?.length || 0}` | ||
| ); | ||
| return result; | ||
| } catch (error: any) { | ||
| logger.error( | ||
| `DeepSeek API 调用失败 (${attempt}/${this.maxRetries}): ${error.message}` | ||
| ); | ||
| if (attempt >= this.maxRetries) { | ||
| throw new Error(`DeepSeek API 调用失败: ${error.message}`); | ||
| } | ||
| // 指数退避 | ||
| const delay = 500 * attempt; | ||
| logger.debug(`等待 ${delay}ms 后重试...`); | ||
| await sleep(delay); | ||
| } | ||
| } | ||
| throw new Error('Unreachable'); | ||
| } | ||
| /** | ||
| * 简单对话:无工具,单次调用 | ||
| * @param userInput - 用户输入 | ||
| * @param systemPrompt - 可选的系统提示词 | ||
| */ | ||
| async simpleChat(userInput: string, systemPrompt?: string): Promise<string> { | ||
| const messages: any[] = []; | ||
| if (systemPrompt) { | ||
| messages.push({ role: 'system', content: systemPrompt }); | ||
| } | ||
| messages.push({ role: 'user', content: userInput }); | ||
| const response = await this.complete(messages); | ||
| return response.content; | ||
| } | ||
| /** | ||
| * 获取配置信息 | ||
| */ | ||
| getConfig(): { provider: string; model: string } { | ||
| return { | ||
| provider: 'deepseek', | ||
| model: this.model, | ||
| }; | ||
| } | ||
| /** | ||
| * 完整方法:支持工具调用循环 | ||
| * 需要在构造时传入 toolManager | ||
| * @param userInput - 用户输入 | ||
| * @param imageData - 可选的图片数据 | ||
| * @param _stream - 是否流式输出(暂未实现) | ||
| */ | ||
| async generate( | ||
| userInput: string, | ||
| imageData?: ImageData, | ||
| _stream?: boolean | ||
| ): Promise<string> { | ||
| if (!this.toolManager) { | ||
| throw new Error( | ||
| 'generate() 方法需要 toolManager,请在构造时传入或使用 chat() 方法' | ||
| ); | ||
| } | ||
| // 初始化消息列表 | ||
| const messages: any[] = [ | ||
| { | ||
| role: 'system', | ||
| content: '你是一个有帮助的 AI 助手,可以使用工具来完成任务。', | ||
| }, | ||
| ]; | ||
| // 添加用户消息 | ||
| const userMessage: any = { role: 'user', content: userInput }; | ||
| if (imageData) { | ||
| userMessage.content = [ | ||
| { type: 'text', text: userInput }, | ||
| imageData.url | ||
| ? { type: 'image_url', image_url: { url: imageData.url } } | ||
| : { | ||
| type: 'image_url', | ||
| image_url: { | ||
| url: `data:${imageData.mimeType || 'image/png'};base64,${imageData.base64}`, | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
| messages.push(userMessage); | ||
| // 获取可用工具 | ||
| const availableTools = await this.toolManager.getToolsForProvider( | ||
| 'deepseek' | ||
| ); | ||
| // 工具调用循环 | ||
| let iteration = 0; | ||
| while (iteration < this.maxIterations) { | ||
| iteration++; | ||
| logger.debug(`工具调用迭代: ${iteration}/${this.maxIterations}`); | ||
| // 调用 LLM | ||
| const response = await this.complete(messages, availableTools, { | ||
| toolChoice: iteration === 1 ? 'auto' : undefined, | ||
| }); | ||
| // 无工具调用,返回结果 | ||
| if (!response.toolCalls || response.toolCalls.length === 0) { | ||
| logger.debug('无工具调用,返回最终结果'); | ||
| return response.content; | ||
| } | ||
| // 记录思考内容 | ||
| if (response.content) { | ||
| logger.info(`💭 助手思考: ${response.content}`); | ||
| } | ||
| // 添加助手消息(包含工具调用) | ||
| messages.push({ | ||
| role: 'assistant', | ||
| content: response.content || null, | ||
| tool_calls: response.toolCalls, | ||
| }); | ||
| // 执行所有工具调用 | ||
| for (const toolCall of response.toolCalls) { | ||
| if (toolCall.type !== 'function' || !toolCall.function) { | ||
| continue; | ||
| } | ||
| logger.info(`🔧 使用工具: ${toolCall.function.name}`); | ||
| try { | ||
| const args = JSON.parse(toolCall.function.arguments); | ||
| const result = await this.toolManager.executeTool( | ||
| toolCall.function.name, | ||
| args | ||
| ); | ||
| logger.info(`✅ 工具执行成功: ${JSON.stringify(result).slice(0, 200)}`); | ||
| // 添加工具执行结果 | ||
| messages.push({ | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| content: JSON.stringify(result), | ||
| }); | ||
| } catch (error: any) { | ||
| logger.error(`❌ 工具执行失败: ${error.message}`); | ||
| // 添加错误结果 | ||
| messages.push({ | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| content: JSON.stringify({ error: error.message }), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| throw new Error(`达到最大迭代次数 (${this.maxIterations})`); | ||
| } | ||
| /** | ||
| * 获取所有可用工具 | ||
| */ | ||
| async getAllTools(): Promise<ToolSet> { | ||
| if (!this.toolManager) { | ||
| return {}; | ||
| } | ||
| return this.toolManager.getAllTools(); | ||
| } | ||
| /** | ||
| * 流式对话(预留接口) | ||
| * TODO: 实现流式响应 | ||
| */ | ||
| async chatStream( | ||
| messages: any[], | ||
| tools?: any[], | ||
| options?: LLMChatOptions | ||
| ): Promise<AsyncIterable<LLMResponse>> { | ||
| throw new Error('流式响应暂未实现'); | ||
| } | ||
| } | ||
| /** 图片数据 */ | ||
| export interface ImageData { | ||
| url?: string; | ||
| base64?: string; | ||
| mimeType?: string; | ||
| } | ||
| /** 工具参数定义 */ | ||
| export interface ToolParameters { | ||
| type: 'object'; | ||
| properties: Record<string, any>; | ||
| required?: string[]; | ||
| } | ||
| /** 工具定义 */ | ||
| export interface Tool { | ||
| name: string; | ||
| description: string; | ||
| parameters: ToolParameters; | ||
| } | ||
| /** 工具集合 */ | ||
| export type ToolSet = Record<string, Tool>; | ||
| /** 工具调用 */ | ||
| export interface ToolCall { | ||
| id: string; | ||
| type: 'function'; | ||
| function: { | ||
| name: string; | ||
| arguments: string; | ||
| }; | ||
| } | ||
| /** LLM 配置 */ | ||
| export interface LLMConfig { | ||
| provider: string; | ||
| model: string; | ||
| apiKey?: string; | ||
| maxIterations?: number; | ||
| baseURL?: string; | ||
| qwenOptions?: Record<string, any>; | ||
| aws?: Record<string, any>; | ||
| azure?: Record<string, any>; | ||
| } | ||
| /** MCP Manager 接口 (占位符) */ | ||
| export interface MCPManager { | ||
| getAllTools(): Promise<ToolSet>; | ||
| executeTool(name: string, args: any): Promise<any>; | ||
| } | ||
| /** Context Manager - 导入真实实现 */ | ||
| export { ContextManager } from '../../context/index.js'; | ||
| /** Unified Tool Manager 接口 (占位符) */ | ||
| export interface UnifiedToolManager { | ||
| getToolsForProvider(provider: string): Promise<any[]>; | ||
| getAllTools(): Promise<ToolSet>; | ||
| executeTool(name: string, args: any): Promise<any>; | ||
| } | ||
| /** Event Manager 接口 (占位符) */ | ||
| export interface EventManager { | ||
| emit(event: string, data: any): void; | ||
| } | ||
| /** | ||
| * LLM 响应结构 | ||
| */ | ||
| export interface LLMResponse { | ||
| /** 响应内容 */ | ||
| content: string; | ||
| /** 工具调用列表 */ | ||
| toolCalls?: ToolCall[]; | ||
| /** 结束原因 */ | ||
| finishReason?: string; | ||
| /** Token 使用统计 */ | ||
| usage?: { | ||
| promptTokens: number; | ||
| completionTokens: number; | ||
| totalTokens: number; | ||
| }; | ||
| } | ||
| /** | ||
| * LLM 调用选项 | ||
| */ | ||
| export interface LLMChatOptions { | ||
| /** 温度参数 (0-2) */ | ||
| temperature?: number; | ||
| /** 最大 token 数 */ | ||
| maxTokens?: number; | ||
| /** 工具选择策略 */ | ||
| toolChoice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } }; | ||
| /** Top P 采样 */ | ||
| topP?: number; | ||
| /** 频率惩罚 */ | ||
| frequencyPenalty?: number; | ||
| /** 存在惩罚 */ | ||
| presencePenalty?: number; | ||
| /** 停止词 */ | ||
| stop?: string | string[]; | ||
| /** 结构化输出格式 */ | ||
| responseFormat?: any; | ||
| } | ||
| /** | ||
| * 工具循环执行结果 | ||
| */ | ||
| export interface ToolLoopResult { | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 最终结果 */ | ||
| result?: string; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| /** 循环次数 */ | ||
| loopCount: number; | ||
| } | ||
| /** | ||
| * 工具循环配置 | ||
| */ | ||
| export interface ToolLoopConfig { | ||
| /** 最大循环次数 */ | ||
| maxLoops?: number; | ||
| /** Agent 名称 */ | ||
| agentName?: string; | ||
| } | ||
| /** LLM Service 核心接口 */ | ||
| export interface ILLMService { | ||
| /** | ||
| * 核心方法:上下文补全 | ||
| * 接收格式化的上下文(消息历史),返回模型响应 | ||
| * @param messages - 上下文消息列表 | ||
| * @param tools - 可用的工具定义列表 | ||
| * @param options - 生成参数(temperature, maxTokens 等) | ||
| * @returns 模型响应(包含内容、工具调用、使用统计) | ||
| */ | ||
| complete( | ||
| messages: any[], | ||
| tools?: any[], | ||
| options?: LLMChatOptions | ||
| ): Promise<LLMResponse>; | ||
| /** | ||
| * 简单对话:无工具,单次调用 | ||
| * @param userInput - 用户输入 | ||
| * @param systemPrompt - 可选的系统提示词 | ||
| */ | ||
| simpleChat(userInput: string, systemPrompt?: string): Promise<string>; | ||
| /** | ||
| * 完整方法:支持工具调用循环(可选实现) | ||
| * 内置工具调用、上下文管理、迭代执行 | ||
| * @param userInput - 用户输入 | ||
| * @param imageData - 可选的图片数据 | ||
| * @param stream - 是否流式输出 | ||
| */ | ||
| generate?( | ||
| userInput: string, | ||
| imageData?: ImageData, | ||
| stream?: boolean | ||
| ): Promise<string>; | ||
| /** 获取配置 */ | ||
| getConfig(): { provider: string; model: string }; | ||
| /** 可选:事件管理器 */ | ||
| setEventManager?(eventManager: EventManager): void; | ||
| /** @deprecated 由 Agent 管理工具 */ | ||
| getAllTools?(): Promise<ToolSet>; | ||
| } |
| /** | ||
| * 工具循环执行器 | ||
| * 简化版本的工具调用循环逻辑,供 Agent 使用 | ||
| */ | ||
| import { ILLMService, ToolLoopResult, ToolLoopConfig } from '../types/index.js'; | ||
| import { ContextManager } from '../../context/index.js'; | ||
| import { ToolManager } from '../../tool/ToolManager.js'; | ||
| import { ContextType, Message } from '../../context/types.js'; | ||
| import { eventBus } from '../../../evaluation/EventBus.js'; | ||
| import { deepParseArgs, sleep } from './helpers.js'; | ||
| /** | ||
| * 执行工具循环 | ||
| * | ||
| * 循环逻辑: | ||
| * 1. 从 ContextManager 获取当前上下文 | ||
| * 2. 调用 LLM 完成 | ||
| * 3. 如果返回工具调用,执行工具并更新上下文 | ||
| * 4. 重复直到 LLM 返回最终结果或达到最大循环次数 | ||
| * | ||
| * @param llmService - LLM 服务实例 | ||
| * @param contextManager - 上下文管理器 | ||
| * @param toolManager - 工具管理器 | ||
| * @param config - 可选配置 | ||
| * @returns 工具循环执行结果 | ||
| */ | ||
| export async function executeToolLoop( | ||
| llmService: ILLMService, | ||
| contextManager: ContextManager, | ||
| toolManager: ToolManager, | ||
| config?: ToolLoopConfig | ||
| ): Promise<ToolLoopResult> { | ||
| const maxLoops = config?.maxLoops ?? 10; | ||
| const agentName = config?.agentName ?? 'simple_agent'; | ||
| let loopCount = 0; | ||
| console.log(`开始工具循环,最大循环次数: ${maxLoops}`); | ||
| while (loopCount < maxLoops) { | ||
| loopCount++; | ||
| console.log(`🔄 工具循环 ${loopCount}/${maxLoops}`); | ||
| try { | ||
| // 1. 获取当前上下文 | ||
| const messages = contextManager.getContext(); | ||
| // 2. 获取格式化的工具定义 | ||
| const tools = toolManager.getFormattedTools(); | ||
| console.log(`调用 LLM: ${messages.length} 条消息, ${tools.length} 个工具`); | ||
| // 3. 调用 LLM | ||
| const response = await llmService.complete(messages, tools); | ||
| // 4. 判断是否有工具调用 | ||
| if ( | ||
| response.finishReason === 'tool_calls' && | ||
| response.toolCalls && | ||
| response.toolCalls.length > 0 | ||
| ) { | ||
| console.log(`检测到 ${response.toolCalls.length} 个工具调用`); | ||
| // 记录 LLM 的思考内容(如果有) | ||
| if (response.content) { | ||
| console.log(`💭 LLM 思考: ${response.content.slice(0, 100)}...`); | ||
| } | ||
| // 5. 构建 assistant 消息(包含工具调用) | ||
| const assistantMessage: Message = { | ||
| role: 'assistant', | ||
| content: response.content || '', | ||
| tool_calls: response.toolCalls, | ||
| }; | ||
| // 6. 添加到 TOOL_MESSAGE_SEQUENCE | ||
| contextManager.add(assistantMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| // 7. 执行所有工具调用 | ||
| for (const toolCall of response.toolCalls) { | ||
| const toolName = toolCall.function.name; | ||
| try { | ||
| // 解析参数 | ||
| const rawArgs = toolCall.function.arguments | ||
| ? JSON.parse(toolCall.function.arguments) | ||
| : {}; | ||
| const args = deepParseArgs(rawArgs); | ||
| console.log(`🔧 执行工具: ${toolName}`); | ||
| // 触发工具调用事件(用于评估系统) | ||
| eventBus.emit('tool:call', { | ||
| agentName, | ||
| toolName, | ||
| }); | ||
| // 执行工具 | ||
| const result = await toolManager.execute(toolName, args); | ||
| const resultString = JSON.stringify(result); | ||
| console.log(`✅ 工具结果: ${resultString.slice(0, 200)}...`); | ||
| // 8. 构建 tool 消息 | ||
| const toolMessage: Message = { | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| name: toolName, | ||
| content: resultString, | ||
| }; | ||
| // 9. 添加到 TOOL_MESSAGE_SEQUENCE | ||
| contextManager.add(toolMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| // 等待一下避免请求过快 | ||
| await sleep(500); | ||
| } catch (error) { | ||
| console.error(`❌ 工具执行失败: ${toolName}`, error); | ||
| // 将错误信息作为工具结果返回 | ||
| const errorMessage: Message = { | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| name: toolName, | ||
| content: JSON.stringify({ | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }), | ||
| }; | ||
| contextManager.add(errorMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| } | ||
| } | ||
| // 继续循环 | ||
| continue; | ||
| } | ||
| // 10. 没有工具调用,返回最终结果 | ||
| console.log(`✅ 工具循环完成,循环次数: ${loopCount}`); | ||
| console.log(`最终结果: ${response.content?.slice(0, 200)}...`); | ||
| // 添加最终的 assistant 消息 | ||
| const finalMessage: Message = { | ||
| role: 'assistant', | ||
| content: response.content, | ||
| }; | ||
| contextManager.add(finalMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| return { | ||
| success: true, | ||
| result: response.content, | ||
| loopCount, | ||
| }; | ||
| } catch (error) { | ||
| console.error(`❌ LLM 调用失败 (循环 ${loopCount}):`, error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| loopCount, | ||
| }; | ||
| } | ||
| } | ||
| // 超过最大循环次数 | ||
| console.warn(`⚠️ 超过最大循环次数 (${maxLoops})`); | ||
| return { | ||
| success: false, | ||
| error: `超过最大循环次数 (${maxLoops})`, | ||
| loopCount, | ||
| }; | ||
| } |
| import { LLMConfig } from "../types/index.js"; | ||
| import { | ||
| config as envConfig, | ||
| getLLMKeyByProvider, | ||
| } from "../../../config/env.js"; | ||
| /** | ||
| * 提取 API Key | ||
| * | ||
| * 优先级(从高到低): | ||
| * 1. 用户传递的 config.apiKey(显式配置) | ||
| * 2. 环境变量中的配置(通过 provider 自动查找) | ||
| * 3. 如果都没有且该 provider 需要 API Key,则抛出错误 | ||
| * | ||
| * @param config - LLM 配置 | ||
| * @returns API Key 字符串 | ||
| */ | ||
| export function extractApiKey(config: LLMConfig): string { | ||
| const provider = config.provider.toLowerCase(); | ||
| // 无需 API Key 的提供商 | ||
| if (["ollama", "lmstudio", "aws"].includes(provider)) { | ||
| return "not-required"; | ||
| } | ||
| // 1. 优先使用用户传递的 API Key | ||
| if (config.apiKey) { | ||
| return config.apiKey; | ||
| } | ||
| // 2. 尝试从环境变量配置中获取 | ||
| const providerConfigKey = getLLMKeyByProvider(provider); | ||
| return providerConfigKey; | ||
| } | ||
| /** | ||
| * 获取提供商的 Base URL | ||
| * | ||
| * 优先级(从高到低): | ||
| * 1. 用户传递的 config.baseURL(显式配置) | ||
| * 2. 环境变量中的配置(通过 provider 自动查找) | ||
| * 3. 硬编码的默认 Base URL | ||
| * | ||
| * @param config - LLM 配置 | ||
| * @returns Base URL 字符串 | ||
| * | ||
| */ | ||
| export function getBaseURL(config: LLMConfig): string { | ||
| // 1. 优先使用用户传递的 baseURL | ||
| if (config.baseURL) { | ||
| return config.baseURL; | ||
| } | ||
| const provider = config.provider.toLowerCase(); | ||
| // 3. 硬编码的默认 Base URL(兜底) | ||
| const defaultBaseURLs: Record<string, string> = { | ||
| deepseek: "https://api.deepseek.com", | ||
| openai: "https://api.openai.com/v1", | ||
| anthropic: "https://api.anthropic.com", | ||
| siliconflow: "https://api.siliconflow.cn/v1", | ||
| qwen: "https://dashscope.aliyuncs.com/compatible-mode/v1", | ||
| openrouter: "https://openrouter.ai/api/v1", | ||
| ollama: "http://localhost:11434/v1", | ||
| lmstudio: "http://localhost:1234/v1", | ||
| }; | ||
| const baseURL = defaultBaseURLs[provider]; | ||
| if (!baseURL) { | ||
| throw new Error( | ||
| `No base URL found for provider "${provider}". ` + | ||
| `Please pass baseURL in config: { provider: '${provider}', model: '...', baseURL: 'https://...' }` | ||
| ); | ||
| } | ||
| return baseURL; | ||
| } | ||
| /** 获取默认上下文窗口大小 */ | ||
| export function getDefaultContextWindow( | ||
| provider: string, | ||
| model?: string | ||
| ): number { | ||
| const defaults: Record<string, Record<string, number>> = { | ||
| openai: { | ||
| "gpt-4o": 128000, | ||
| "gpt-4": 8192, | ||
| "gpt-3.5-turbo": 16385, | ||
| default: 8192, | ||
| }, | ||
| anthropic: { default: 200000 }, | ||
| gemini: { default: 1000000 }, | ||
| deepseek: { default: 128000 }, | ||
| ollama: { default: 8192 }, | ||
| }; | ||
| const providerDefaults = defaults[provider.toLowerCase()] || { | ||
| default: 8192, | ||
| }; | ||
| return providerDefaults[model || "default"] || providerDefaults.default; | ||
| } | ||
| /** 睡眠函数 */ | ||
| export function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| /** | ||
| * 深度解析参数:递归检查字符串类型的参数值,尝试 JSON 解析 | ||
| * 用于处理 Claude 等模型返回嵌套 JSON 字符串的情况 | ||
| * | ||
| * 例如:{ bgmRequests: "[{...}]" } → { bgmRequests: [{...}] } | ||
| */ | ||
| export function deepParseArgs(args: any): any { | ||
| if (typeof args === "string") { | ||
| // 尝试解析看起来像 JSON 数组或对象的字符串 | ||
| const trimmed = args.trim(); | ||
| if (trimmed.startsWith("[") || trimmed.startsWith("{")) { | ||
| // 第一次尝试:直接解析 | ||
| try { | ||
| const parsed = JSON.parse(trimmed); | ||
| console.log("[deepParseArgs] ✅ 第一次尝试解析成功"); | ||
| return deepParseArgs(parsed); | ||
| } catch (e) { | ||
| console.log( | ||
| "[deepParseArgs] ❌ 第一次尝试解析失败:", | ||
| (e as Error).message | ||
| ); | ||
| console.log( | ||
| "[deepParseArgs] 字符串前100字符:", | ||
| trimmed.substring(0, 100) | ||
| ); | ||
| // 第二次尝试:处理 Claude 模型返回的双重转义问题 | ||
| // 将 \\n 转换为 \n,\\\" 转换为 \" | ||
| try { | ||
| const unescaped = trimmed | ||
| .replace(/\\\\n/g, "\\n") | ||
| .replace(/\\\\"/g, '\\"'); | ||
| console.log("[deepParseArgs] 尝试处理双重转义后解析..."); | ||
| const parsed = JSON.parse(unescaped); | ||
| console.log("[deepParseArgs] ✅ 第二次尝试(处理转义后)解析成功"); | ||
| return deepParseArgs(parsed); | ||
| } catch (e2) { | ||
| console.log( | ||
| "[deepParseArgs] ❌ 第二次尝试也失败:", | ||
| (e2 as Error).message | ||
| ); | ||
| // 都失败了,返回原始字符串 | ||
| return args; | ||
| } | ||
| } | ||
| } | ||
| return args; | ||
| } | ||
| if (Array.isArray(args)) { | ||
| return args.map(deepParseArgs); | ||
| } | ||
| if (args && typeof args === "object") { | ||
| const result: any = {}; | ||
| for (const [key, value] of Object.entries(args)) { | ||
| result[key] = deepParseArgs(value); | ||
| } | ||
| return result; | ||
| } | ||
| return args; | ||
| } |
| /** | ||
| * 提示词管理模块 | ||
| */ | ||
| export { | ||
| SIMPLE_AGENT_PROMPT, | ||
| MAIN_AGENT_PROMPT, | ||
| SUB_AGENT_A_PROMPT, | ||
| SUB_AGENT_B_PROMPT, | ||
| } from './prompts.js'; |
| /** | ||
| * 提示词常量定义 | ||
| * 集中管理各类 Agent 的系统提示词 | ||
| */ | ||
| /** | ||
| * SimpleAgent 默认提示词 | ||
| */ | ||
| export const SIMPLE_AGENT_PROMPT = `你是一个有用的 AI 助手,可以使用工具来帮助用户完成任务。 | ||
| 你可以使用以下工具: | ||
| - list_files: 列出指定目录下的文件和文件夹 | ||
| - read_file: 读取指定文件的内容 | ||
| 请根据用户的需求,选择合适的工具来完成任务。`; | ||
| /** | ||
| * 主 Agent 提示词(协调者) | ||
| * 用于多智能体系统中的主控 Agent | ||
| */ | ||
| export const MAIN_AGENT_PROMPT = `你是一个任务协调者,负责分析用户需求并协调子Agent完成任务。 | ||
| 你的职责: | ||
| 1. 分析用户的任务需求 | ||
| 2. 将任务分配给合适的子Agent | ||
| 3. 汇总子Agent的执行结果 | ||
| 4. 向用户提供最终的综合答复 | ||
| 你不直接执行具体任务,而是通过协调子Agent来完成工作。`; | ||
| /** | ||
| * 子 Agent A 提示词(研究者) | ||
| */ | ||
| export const SUB_AGENT_A_PROMPT = `你是一个研究分析专家,擅长: | ||
| - 收集和整理信息 | ||
| - 分析问题的各个方面 | ||
| - 提供详细的背景知识 | ||
| 请根据主Agent的指令,提供专业的研究分析结果。`; | ||
| /** | ||
| * 子 Agent B 提示词(执行者) | ||
| */ | ||
| export const SUB_AGENT_B_PROMPT = `你是一个执行专家,擅长: | ||
| - 制定具体的执行方案 | ||
| - 提供实用的建议和步骤 | ||
| - 给出可操作的解决方案 | ||
| 请根据主Agent的指令,提供具体的执行方案。`; |
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { ToolManager } from '../ToolManager.js'; | ||
| describe('ToolManager 测试', () => { | ||
| let toolManager: ToolManager; | ||
| beforeEach(() => { | ||
| toolManager = new ToolManager(); | ||
| }); | ||
| describe('基础功能', () => { | ||
| it('应该成功创建 ToolManager 实例', () => { | ||
| expect(toolManager).toBeDefined(); | ||
| expect(toolManager).toBeInstanceOf(ToolManager); | ||
| }); | ||
| it('应该自动注册所有工具', () => { | ||
| const toolNames = toolManager.getToolNames(); | ||
| expect(toolNames).toContain('read_file'); | ||
| expect(toolNames).toContain('grep_search'); | ||
| }); | ||
| it('应该返回正确的工具数量', () => { | ||
| const tools = toolManager.getTools(); | ||
| expect(tools.length).toBeGreaterThan(0); | ||
| }); | ||
| }); | ||
| describe('工具查询', () => { | ||
| it('应该能够通过名称获取工具', () => { | ||
| const tool = toolManager.getTool('read_file'); | ||
| expect(tool).toBeDefined(); | ||
| expect(tool?.name).toBe('read_file'); | ||
| expect(tool?.category).toBe('filesystem'); | ||
| }); | ||
| it('应该在工具不存在时返回 undefined', () => { | ||
| const tool = toolManager.getTool('non_existent_tool'); | ||
| expect(tool).toBeUndefined(); | ||
| }); | ||
| it('应该正确检查工具是否存在', () => { | ||
| expect(toolManager.hasTool('read_file')).toBe(true); | ||
| expect(toolManager.hasTool('non_existent_tool')).toBe(false); | ||
| }); | ||
| it('应该返回所有工具名称', () => { | ||
| const toolNames = toolManager.getToolNames(); | ||
| expect(toolNames).toBeInstanceOf(Array); | ||
| expect(toolNames.length).toBeGreaterThan(0); | ||
| expect(toolNames).toContain('read_file'); | ||
| }); | ||
| }); | ||
| describe('工具统计', () => { | ||
| it('应该返回正确的统计信息', () => { | ||
| const stats = toolManager.getStats(); | ||
| expect(stats).toHaveProperty('totalTools'); | ||
| expect(stats).toHaveProperty('categories'); | ||
| expect(stats).toHaveProperty('toolNames'); | ||
| expect(stats.totalTools).toBeGreaterThan(0); | ||
| }); | ||
| it('应该按分类统计工具数量', () => { | ||
| const stats = toolManager.getStats(); | ||
| expect(stats.categories).toHaveProperty('filesystem'); | ||
| expect(stats.categories).toHaveProperty('search'); | ||
| }); | ||
| }); | ||
| describe('工具执行', () => { | ||
| it('应该在工具不存在时抛出错误', async () => { | ||
| await expect( | ||
| toolManager.execute('non_existent_tool', {}) | ||
| ).rejects.toThrow(/not found/); | ||
| }); | ||
| it('应该在错误信息中列出可用工具', async () => { | ||
| try { | ||
| await toolManager.execute('non_existent_tool', {}); | ||
| } catch (error: any) { | ||
| expect(error.message).toContain('Available:'); | ||
| expect(error.message).toContain('read_file'); | ||
| } | ||
| }); | ||
| }); | ||
| describe('工具结构验证', () => { | ||
| it('每个工具应该有必需的属性', () => { | ||
| const tools = toolManager.getTools(); | ||
| tools.forEach((tool) => { | ||
| expect(tool).toHaveProperty('name'); | ||
| expect(tool).toHaveProperty('category'); | ||
| expect(tool).toHaveProperty('internal'); | ||
| expect(tool).toHaveProperty('description'); | ||
| expect(tool).toHaveProperty('version'); | ||
| expect(tool).toHaveProperty('parameters'); | ||
| expect(tool).toHaveProperty('handler'); | ||
| expect(typeof tool.handler).toBe('function'); | ||
| }); | ||
| }); | ||
| it('每个工具的参数定义应该是有效的 JSON Schema', () => { | ||
| const tools = toolManager.getTools(); | ||
| tools.forEach((tool) => { | ||
| expect(tool.parameters).toHaveProperty('type'); | ||
| expect(tool.parameters.type).toBe('object'); | ||
| expect(tool.parameters).toHaveProperty('properties'); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { formatToolForLLM, InternalTool } from '../types.js'; | ||
| describe('types 测试', () => { | ||
| describe('formatToolForLLM', () => { | ||
| it('应该正确格式化工具定义', () => { | ||
| const mockTool: InternalTool = { | ||
| name: 'test_tool', | ||
| category: 'test', | ||
| internal: true, | ||
| description: 'Test tool description', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| arg1: { | ||
| type: 'string', | ||
| description: 'Test argument', | ||
| }, | ||
| }, | ||
| required: ['arg1'], | ||
| }, | ||
| handler: async () => ({ success: true }), | ||
| }; | ||
| const formatted = formatToolForLLM(mockTool); | ||
| expect(formatted).toEqual({ | ||
| name: 'test_tool', | ||
| category: 'test', | ||
| description: 'Test tool description', | ||
| version: '1.0.0', | ||
| parameters: mockTool.parameters, | ||
| }); | ||
| }); | ||
| it('应该不包含 handler 和其他内部属性', () => { | ||
| const mockTool: InternalTool = { | ||
| name: 'test_tool', | ||
| category: 'test', | ||
| internal: true, | ||
| description: 'Test tool', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: {}, | ||
| }, | ||
| handler: async () => ({ success: true }), | ||
| renderResultForAssistant: (result) => JSON.stringify(result), | ||
| needsPermissions: () => false, | ||
| }; | ||
| const formatted = formatToolForLLM(mockTool); | ||
| expect(formatted).not.toHaveProperty('handler'); | ||
| expect(formatted).not.toHaveProperty('internal'); | ||
| expect(formatted).not.toHaveProperty('renderResultForAssistant'); | ||
| expect(formatted).not.toHaveProperty('needsPermissions'); | ||
| }); | ||
| it('应该保留完整的参数 Schema 结构', () => { | ||
| const complexParameters = { | ||
| type: 'object' as const, | ||
| properties: { | ||
| nested: { | ||
| type: 'object' as const, | ||
| properties: { | ||
| deep: { | ||
| type: 'string' as const, | ||
| }, | ||
| }, | ||
| }, | ||
| array: { | ||
| type: 'array' as const, | ||
| items: { | ||
| type: 'number' as const, | ||
| }, | ||
| }, | ||
| }, | ||
| required: ['nested'], | ||
| }; | ||
| const mockTool: InternalTool = { | ||
| name: 'complex_tool', | ||
| category: 'test', | ||
| internal: true, | ||
| description: 'Complex tool', | ||
| version: '1.0.0', | ||
| parameters: complexParameters, | ||
| handler: async () => ({}), | ||
| }; | ||
| const formatted = formatToolForLLM(mockTool); | ||
| expect(formatted.parameters).toEqual(complexParameters); | ||
| }); | ||
| }); | ||
| }); | ||
| /** | ||
| * 工具模块统一导出 | ||
| */ | ||
| // 类型定义 | ||
| export * from './types.js'; | ||
| // 工具管理器 | ||
| export { ToolManager } from './ToolManager.js'; | ||
| // 具体工具 | ||
| export { ListFilesTool } from './ListFiles/definitions.js'; | ||
| export type { ListFilesArgs, ListFilesResult } from './ListFiles/executors.js'; | ||
| export { ReadFileTool } from './ReadFile/definitions.js'; | ||
| export type { ReadFileArgs, ReadFileResult } from './ReadFile/executors.js'; |
| import { InternalTool } from "../types"; | ||
| import { listFilesExecutor } from "./executors"; | ||
| import { renderResultForAssistant } from "./executors"; | ||
| import { ListFilesArgs, ListFilesResult } from "./executors"; | ||
| export const ListFilesTool: InternalTool<ListFilesArgs, ListFilesResult> = { | ||
| name: 'list_files', | ||
| category: 'filesystem', | ||
| internal: true, | ||
| description: '列出指定目录下的文件和文件夹。如果不指定目录,则列出当前工作目录。', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| directory: { | ||
| type: 'string', | ||
| description: '要列出内容的目录路径,默认为当前工作目录', | ||
| }, | ||
| }, | ||
| required: [], | ||
| }, | ||
| handler: listFilesExecutor, | ||
| renderResultForAssistant: renderResultForAssistant, | ||
| }; |
| /** | ||
| * 列出目录文件工具 | ||
| * 列出指定目录下的文件和文件夹 | ||
| */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { InternalTool } from '../types.js'; | ||
| export interface ListFilesArgs { | ||
| /** 目录路径,默认为当前工作目录 */ | ||
| directory?: string; | ||
| } | ||
| export interface ListFilesResult { | ||
| /** 目录路径 */ | ||
| directory: string; | ||
| /** 文件列表 */ | ||
| files: Array<{ | ||
| name: string; | ||
| type: 'file' | 'directory'; | ||
| size?: number; | ||
| }>; | ||
| /** 文件总数 */ | ||
| totalCount: number; | ||
| } | ||
| /** | ||
| * 列出文件执行器 | ||
| * @param args - 列出文件参数 | ||
| * @param config - 配置 | ||
| * @returns - 列出文件结果 | ||
| */ | ||
| export async function listFilesExecutor(args: ListFilesArgs, config: any): Promise<ListFilesResult> { | ||
| const cwd = config?.cwd || process.cwd(); | ||
| const targetDir = args.directory ? path.resolve(cwd, args.directory) : cwd; | ||
| // 检查目录是否存在 | ||
| if (!fs.existsSync(targetDir)) { | ||
| throw new Error(`目录不存在: ${targetDir}`); | ||
| } | ||
| // 检查是否是目录 | ||
| const stats = fs.statSync(targetDir); | ||
| if (!stats.isDirectory()) { | ||
| throw new Error(`路径不是目录: ${targetDir}`); | ||
| } | ||
| // 读取目录内容 | ||
| const entries = fs.readdirSync(targetDir, { withFileTypes: true }); | ||
| const files = entries | ||
| .filter((entry) => !entry.name.startsWith('.')) // 过滤隐藏文件 | ||
| .map((entry) => { | ||
| const fullPath = path.join(targetDir, entry.name); | ||
| const result: { | ||
| name: string; | ||
| type: 'file' | 'directory'; | ||
| size?: number; | ||
| } = { | ||
| name: entry.name, | ||
| type: entry.isDirectory() ? 'directory' : 'file', | ||
| }; | ||
| // 对于文件,获取大小 | ||
| if (entry.isFile()) { | ||
| try { | ||
| const fileStats = fs.statSync(fullPath); | ||
| result.size = fileStats.size; | ||
| } catch { | ||
| // 忽略权限错误 | ||
| } | ||
| } | ||
| return result; | ||
| }) | ||
| .sort((a, b) => { | ||
| // 目录在前,文件在后 | ||
| if (a.type !== b.type) { | ||
| return a.type === 'directory' ? -1 : 1; | ||
| } | ||
| return a.name.localeCompare(b.name); | ||
| }); | ||
| return { | ||
| directory: targetDir, | ||
| files, | ||
| totalCount: files.length, | ||
| }; | ||
| } | ||
| /** | ||
| * 格式化工具结果 | ||
| * @param result - 列表文件结果 | ||
| * @returns | ||
| */ | ||
| export function renderResultForAssistant(result: ListFilesResult): string { | ||
| const lines = [`目录: ${result.directory}`, `共 ${result.totalCount} 个项目:`, '']; | ||
| for (const file of result.files) { | ||
| const icon = file.type === 'directory' ? '📁' : '📄'; | ||
| const size = file.size !== undefined ? ` (${formatSize(file.size)})` : ''; | ||
| lines.push(`${icon} ${file.name}${size}`); | ||
| } | ||
| return lines.join('\n'); | ||
| } | ||
| /** | ||
| * 格式化文件大小 | ||
| */ | ||
| function formatSize(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } |
| import { InternalTool } from "../types"; | ||
| import { readFileExecutor } from "./executors"; | ||
| import { renderResultForAssistant } from "./executors"; | ||
| import { ReadFileArgs, ReadFileResult } from "./executors"; | ||
| export const ReadFileTool: InternalTool<ReadFileArgs, ReadFileResult> = { | ||
| name: 'read_file', | ||
| category: 'filesystem', | ||
| internal: true, | ||
| description: '读取指定文件的内容。支持文本文件,返回文件内容、大小和行数。', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| filePath: { | ||
| type: 'string', | ||
| description: '要读取的文件路径', | ||
| }, | ||
| encoding: { | ||
| type: 'string', | ||
| description: '文件编码,默认为 utf-8', | ||
| }, | ||
| }, | ||
| required: ['filePath'], | ||
| }, | ||
| handler: readFileExecutor, | ||
| renderResultForAssistant: renderResultForAssistant, | ||
| }; | ||
| /** | ||
| * 读取文件内容工具 | ||
| * 读取指定文件的内容 | ||
| */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { InternalTool } from '../types.js'; | ||
| export interface ReadFileArgs { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 编码,默认 utf-8 */ | ||
| encoding?: BufferEncoding; | ||
| } | ||
| export interface ReadFileResult { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 文件内容 */ | ||
| content: string; | ||
| /** 文件大小(字节) */ | ||
| size: number; | ||
| /** 行数 */ | ||
| lineCount: number; | ||
| } | ||
| /** | ||
| * 读取文件执行器 | ||
| * @param args - 读取文件参数 | ||
| * @param config - 配置 | ||
| * @returns - 读取文件结果 | ||
| */ | ||
| export async function readFileExecutor(args: ReadFileArgs, config: any): Promise<ReadFileResult> { | ||
| const cwd = config?.cwd || process.cwd(); | ||
| const targetPath = path.resolve(cwd, args.filePath); | ||
| const encoding = args.encoding || 'utf-8'; | ||
| // 检查文件是否存在 | ||
| if (!fs.existsSync(targetPath)) { | ||
| throw new Error(`文件不存在: ${targetPath}`); | ||
| } | ||
| // 检查是否是文件 | ||
| const stats = fs.statSync(targetPath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`路径不是文件: ${targetPath}`); | ||
| } | ||
| // 检查文件大小,避免读取过大的文件 | ||
| const maxSize = 1024 * 1024; // 1MB | ||
| if (stats.size > maxSize) { | ||
| throw new Error(`文件太大 (${formatSize(stats.size)}),最大支持 1MB`); | ||
| } | ||
| // 读取文件内容 | ||
| const content = fs.readFileSync(targetPath, encoding); | ||
| const lineCount = content.split('\n').length; | ||
| return { | ||
| filePath: targetPath, | ||
| content, | ||
| size: stats.size, | ||
| lineCount, | ||
| }; | ||
| } | ||
| /** | ||
| * 格式化工具结果 | ||
| * @param result - 读取文件结果 | ||
| * @returns - 读取文件结果 | ||
| */ | ||
| export function renderResultForAssistant(result: ReadFileResult): string { | ||
| return [ | ||
| `文件: ${result.filePath}`, | ||
| `大小: ${formatSize(result.size)}`, | ||
| `行数: ${result.lineCount}`, | ||
| '', | ||
| '--- 内容 ---', | ||
| result.content, | ||
| ].join('\n'); | ||
| } | ||
| /** | ||
| * 格式化文件大小 | ||
| */ | ||
| function formatSize(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } |
| /** | ||
| * 读取文件内容工具 | ||
| * 读取指定文件的内容 | ||
| */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { InternalTool } from '../types.js'; | ||
| export interface ReadFileArgs { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 编码,默认 utf-8 */ | ||
| encoding?: BufferEncoding; | ||
| } | ||
| export interface ReadFileResult { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 文件内容 */ | ||
| content: string; | ||
| /** 文件大小(字节) */ | ||
| size: number; | ||
| /** 行数 */ | ||
| lineCount: number; | ||
| } | ||
| export const ReadFileTool: InternalTool<ReadFileArgs, ReadFileResult> = { | ||
| name: 'read_file', | ||
| category: 'filesystem', | ||
| internal: true, | ||
| description: '读取指定文件的内容。支持文本文件,返回文件内容、大小和行数。', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| filePath: { | ||
| type: 'string', | ||
| description: '要读取的文件路径', | ||
| }, | ||
| encoding: { | ||
| type: 'string', | ||
| description: '文件编码,默认为 utf-8', | ||
| }, | ||
| }, | ||
| required: ['filePath'], | ||
| }, | ||
| async handler(args, context) { | ||
| const cwd = context?.cwd || process.cwd(); | ||
| const targetPath = path.resolve(cwd, args.filePath); | ||
| const encoding = args.encoding || 'utf-8'; | ||
| // 检查文件是否存在 | ||
| if (!fs.existsSync(targetPath)) { | ||
| throw new Error(`文件不存在: ${targetPath}`); | ||
| } | ||
| // 检查是否是文件 | ||
| const stats = fs.statSync(targetPath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`路径不是文件: ${targetPath}`); | ||
| } | ||
| // 检查文件大小,避免读取过大的文件 | ||
| const maxSize = 1024 * 1024; // 1MB | ||
| if (stats.size > maxSize) { | ||
| throw new Error(`文件太大 (${formatSize(stats.size)}),最大支持 1MB`); | ||
| } | ||
| // 读取文件内容 | ||
| const content = fs.readFileSync(targetPath, encoding); | ||
| const lineCount = content.split('\n').length; | ||
| return { | ||
| filePath: targetPath, | ||
| content, | ||
| size: stats.size, | ||
| lineCount, | ||
| }; | ||
| }, | ||
| renderResultForAssistant(result) { | ||
| return [ | ||
| `文件: ${result.filePath}`, | ||
| `大小: ${formatSize(result.size)}`, | ||
| `行数: ${result.lineCount}`, | ||
| '', | ||
| '--- 内容 ---', | ||
| result.content, | ||
| ].join('\n'); | ||
| }, | ||
| }; | ||
| /** | ||
| * 格式化文件大小 | ||
| */ | ||
| function formatSize(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } |
| import { InternalTool, InternalToolContext } from './types.js'; | ||
| import { ListFilesTool } from './ListFiles/definitions.js'; | ||
| import { ReadFileTool } from './ReadFile/definitions.js'; | ||
| // 注册的工具列表 | ||
| const toolsList: InternalTool[] = [ListFilesTool, ReadFileTool]; | ||
| /** | ||
| * OpenAI 格式的工具定义 | ||
| */ | ||
| interface OpenAITool { | ||
| type: 'function'; | ||
| function: { | ||
| name: string; | ||
| description: string; | ||
| parameters: any; | ||
| }; | ||
| } | ||
| /** | ||
| * 工具管理类 | ||
| * 负责工具的注册、查询和执行 | ||
| */ | ||
| export class ToolManager { | ||
| private tools: Map<string, InternalTool> = new Map(); | ||
| constructor() { | ||
| this.registerAllTools(); | ||
| } | ||
| /** | ||
| * 注册所有工具 | ||
| */ | ||
| private registerAllTools() { | ||
| toolsList.forEach((tool) => { | ||
| this.tools.set(tool.name, tool); | ||
| }); | ||
| } | ||
| /** | ||
| * 执行指定工具 | ||
| */ | ||
| async execute<TArgs = any, TResult = any>( | ||
| name: string, | ||
| args: TArgs, | ||
| context?: InternalToolContext | ||
| ): Promise<TResult> { | ||
| const tool = this.tools.get(name); | ||
| if (!tool) { | ||
| throw new Error(`Tool '${name}' not found. Available: ${this.getToolNames().join(', ')}`); | ||
| } | ||
| try { | ||
| return await tool.handler(args, context); | ||
| } catch (error: any) { | ||
| console.error(`Tool '${name}' failed:`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * 获取格式化的工具定义(OpenAI 格式) | ||
| * 供 LLM API 调用使用 | ||
| */ | ||
| getFormattedTools(): OpenAITool[] { | ||
| return Array.from(this.tools.values()).map((tool) => ({ | ||
| type: 'function' as const, | ||
| function: { | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: tool.parameters, | ||
| }, | ||
| })); | ||
| } | ||
| /** | ||
| * 获取所有工具 | ||
| */ | ||
| getTools(): InternalTool[] { | ||
| return Array.from(this.tools.values()); | ||
| } | ||
| /** | ||
| * 获取指定工具 | ||
| */ | ||
| getTool(name: string): InternalTool | undefined { | ||
| return this.tools.get(name); | ||
| } | ||
| /** | ||
| * 获取所有工具名称 | ||
| */ | ||
| getToolNames(): string[] { | ||
| return Array.from(this.tools.keys()); | ||
| } | ||
| /** | ||
| * 检查工具是否存在 | ||
| */ | ||
| hasTool(name: string): boolean { | ||
| return this.tools.has(name); | ||
| } | ||
| /** | ||
| * 清空所有工具 | ||
| * 用于创建无工具的Agent场景 | ||
| */ | ||
| clear(): void { | ||
| this.tools.clear(); | ||
| } | ||
| /** | ||
| * 获取工具统计信息 | ||
| */ | ||
| getStats() { | ||
| const tools = this.getTools(); | ||
| const categories = new Map<string, number>(); | ||
| tools.forEach((tool) => { | ||
| const count = categories.get(tool.category) || 0; | ||
| categories.set(tool.category, count + 1); | ||
| }); | ||
| return { | ||
| totalTools: tools.length, | ||
| categories: Object.fromEntries(categories), | ||
| toolNames: this.getToolNames(), | ||
| }; | ||
| } | ||
| } |
| /** | ||
| * 工具定义类型约束 | ||
| * 用于定义所有工具的统一结构,便于格式化并作为提示词传给大模型 | ||
| */ | ||
| /** | ||
| * JSON Schema 参数定义 | ||
| */ | ||
| export interface ToolParameterSchema { | ||
| type: 'object' | 'array' | 'string' | 'number' | 'boolean'; | ||
| description?: string; | ||
| properties?: Record<string, ToolParameterSchema>; | ||
| items?: ToolParameterSchema; | ||
| required?: string[]; | ||
| enum?: string[]; | ||
| default?: any; | ||
| } | ||
| /** | ||
| * 工具上下文 | ||
| */ | ||
| export interface InternalToolContext { | ||
| abortSignal?: AbortSignal; | ||
| cwd?: string; | ||
| [key: string]: any; // 扩展字段 | ||
| } | ||
| /** | ||
| * 工具定义基础接口 | ||
| */ | ||
| export interface InternalTool<TArgs = any, TResult = any> { | ||
| /** 工具名称(唯一标识) */ | ||
| name: string; | ||
| /** 工具分类(如 filesystem、search、network) */ | ||
| category: string; | ||
| /** 是否为内部工具 */ | ||
| internal: boolean; | ||
| /** 工具描述(简短,详细描述在 prompt 中) */ | ||
| description: string; | ||
| /** 版本号 */ | ||
| version: string; | ||
| /** 参数定义(JSON Schema 格式) */ | ||
| parameters: ToolParameterSchema; | ||
| /** 工具处理函数 */ | ||
| handler: (args: TArgs, context?: InternalToolContext) => Promise<TResult>; | ||
| /** 可选:格式化结果给 AI */ | ||
| renderResultForAssistant?: (result: TResult) => string; | ||
| /** 可选:权限控制 */ | ||
| needsPermissions?: (input?: TArgs) => boolean; // 是否需要权限 | ||
| isEnabled?: () => Promise<boolean>; // 是否启用 | ||
| isReadOnly?: () => boolean; // 是否只读 | ||
| isConcurrencySafe?: () => boolean; // 是否并发安全 | ||
| } | ||
| /** | ||
| * 格式化工具定义为大模型可读格式 | ||
| */ | ||
| export interface FormattedToolDefinition { | ||
| name: string; | ||
| category: string; | ||
| description: string; | ||
| version: string; | ||
| parameters: ToolParameterSchema; | ||
| } | ||
| /** | ||
| * 将工具定义格式化为大模型输入格式 | ||
| */ | ||
| export function formatToolForLLM(tool: InternalTool): FormattedToolDefinition { | ||
| return { | ||
| name: tool.name, | ||
| category: tool.category, | ||
| description: tool.description, | ||
| version: tool.version, | ||
| parameters: tool.parameters, | ||
| }; | ||
| } | ||
| /** | ||
| * 通用测试数据集 | ||
| * 包含单 Agent 和多 Agent 的测试用例 | ||
| */ | ||
| import { TestCase } from './types.js'; | ||
| /** | ||
| * 单 Agent 测试用例 (SimpleAgent) | ||
| */ | ||
| export const SIMPLE_AGENT_TESTS: TestCase[] = [ | ||
| // S1. 单工具测试 - 列出文件 | ||
| { | ||
| id: 'S1', | ||
| description: '单工具调用 - 列出当前目录文件', | ||
| input: '列出当前目录下的所有文件和文件夹', | ||
| expected: { | ||
| agents: ['simple_agent'], | ||
| tools: { simple_agent: ['list_files'] }, | ||
| }, | ||
| }, | ||
| // S2. 多工具测试 - 先列出再读取 | ||
| { | ||
| id: 'S2', | ||
| description: '多工具调用 - 列出文件后读取', | ||
| input: '先列出当前目录的文件,然后读取 README.md 的内容', | ||
| expected: { | ||
| agents: ['simple_agent'], | ||
| tools: { simple_agent: ['list_files', 'read_file'] }, | ||
| }, | ||
| }, | ||
| ]; | ||
| /** | ||
| * 多 Agent 测试用例 (MultiAgent: MainAgent + SubAgents) | ||
| */ | ||
| export const MULTI_AGENT_TESTS: TestCase[] = [ | ||
| // M1. 简单任务分发 - 主Agent协调子Agent | ||
| { | ||
| id: 'M1', | ||
| description: '多Agent协调 - 简单任务分发', | ||
| input: '帮我分析一下如何提升代码质量', | ||
| expected: { | ||
| agents: ['main_agent', 'researcher', 'executor'], | ||
| tools: { | ||
| main_agent: [], | ||
| researcher: [], | ||
| executor: [], | ||
| }, | ||
| }, | ||
| }, | ||
| // M2. 研究+执行 - 调用 researcher 和 executor | ||
| { | ||
| id: 'M2', | ||
| description: '多Agent协调 - 研究与执行', | ||
| input: '请研究并提供一个实现用户认证系统的方案', | ||
| expected: { | ||
| agents: ['main_agent', 'researcher', 'executor'], | ||
| tools: { | ||
| main_agent: [], | ||
| researcher: [], | ||
| executor: [], | ||
| }, | ||
| }, | ||
| }, | ||
| // M3. 复杂任务 - 多轮子Agent协作 | ||
| { | ||
| id: 'M3', | ||
| description: '多Agent协调 - 复杂任务处理', | ||
| input: '帮我设计一个完整的电商系统架构,包括技术选型和实施计划', | ||
| expected: { | ||
| agents: ['main_agent', 'researcher', 'executor'], | ||
| tools: { | ||
| main_agent: [], | ||
| researcher: [], | ||
| executor: [], | ||
| }, | ||
| }, | ||
| }, | ||
| ]; | ||
| /** | ||
| * 所有测试用例 | ||
| */ | ||
| export const TEST_CASES: TestCase[] = [...SIMPLE_AGENT_TESTS, ...MULTI_AGENT_TESTS]; | ||
| /** | ||
| * 根据ID获取测试用例 | ||
| */ | ||
| export function getTestById(id: string): TestCase | undefined { | ||
| return TEST_CASES.find((test) => test.id === id); | ||
| } | ||
| /** | ||
| * 获取单Agent测试用例 | ||
| */ | ||
| export function getSimpleAgentTests(): TestCase[] { | ||
| return SIMPLE_AGENT_TESTS; | ||
| } | ||
| /** | ||
| * 获取多Agent测试用例 | ||
| */ | ||
| export function getMultiAgentTests(): TestCase[] { | ||
| return MULTI_AGENT_TESTS; | ||
| } |
| /** | ||
| * 评估函数 - 简化版本 | ||
| * 比较期望行为和实际执行数据 | ||
| */ | ||
| import { TestCase, CollectedData, EvaluateResult } from './types.js'; | ||
| /** | ||
| * 评估函数 | ||
| * @param testCase 测试用例 | ||
| * @param actual 实际收集到的数据 | ||
| * @returns 评估结果 | ||
| */ | ||
| export function evaluate(testCase: TestCase, actual: CollectedData): EvaluateResult { | ||
| const expected = testCase.expected; | ||
| // 1. 评估Agent调用 | ||
| const missedAgents = expected.agents.filter((a) => !actual.agents.includes(a)); | ||
| const extraAgents = actual.agents.filter((a) => !expected.agents.includes(a)); | ||
| const agentMatch = missedAgents.length === 0 && extraAgents.length === 0; | ||
| // 2. 评估工具调用 | ||
| const missedTools: { agent: string; tool: string }[] = []; | ||
| const extraTools: { agent: string; tool: string }[] = []; | ||
| // 检查遗漏的工具 | ||
| for (const [agent, tools] of Object.entries(expected.tools)) { | ||
| const actualTools = actual.tools[agent] || []; | ||
| for (const tool of tools) { | ||
| if (!actualTools.includes(tool)) { | ||
| missedTools.push({ agent, tool }); | ||
| } | ||
| } | ||
| } | ||
| // 检查多余的工具 | ||
| for (const [agent, tools] of Object.entries(actual.tools)) { | ||
| const expectedTools = expected.tools[agent] || []; | ||
| for (const tool of tools) { | ||
| if (!expectedTools.includes(tool)) { | ||
| extraTools.push({ agent, tool }); | ||
| } | ||
| } | ||
| } | ||
| const toolMatch = missedTools.length === 0 && extraTools.length === 0; | ||
| // 3. 综合判断 | ||
| const passed = agentMatch && toolMatch; | ||
| return { | ||
| passed, | ||
| agentMatch, | ||
| toolMatch, | ||
| details: { | ||
| agents: { | ||
| expected: expected.agents, | ||
| actual: actual.agents, | ||
| missed: missedAgents, | ||
| extra: extraAgents, | ||
| }, | ||
| tools: { | ||
| expected: expected.tools, | ||
| actual: actual.tools, | ||
| missed: missedTools, | ||
| extra: extraTools, | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * 格式化评估结果为可读字符串 | ||
| */ | ||
| export function formatResult(result: EvaluateResult): string { | ||
| let output = ''; | ||
| // Agent评估 | ||
| if (result.agentMatch) { | ||
| output += `✅ Agent调用正确\n`; | ||
| } else { | ||
| output += `❌ Agent调用错误\n`; | ||
| output += ` 期望: ${result.details.agents.expected.join(', ') || '无'}\n`; | ||
| output += ` 实际: ${result.details.agents.actual.join(', ') || '无'}\n`; | ||
| if (result.details.agents.missed.length > 0) { | ||
| output += ` 遗漏: ${result.details.agents.missed.join(', ')}\n`; | ||
| } | ||
| if (result.details.agents.extra.length > 0) { | ||
| output += ` 多余: ${result.details.agents.extra.join(', ')}\n`; | ||
| } | ||
| } | ||
| // 工具评估 | ||
| if (result.toolMatch) { | ||
| output += `✅ 工具调用正确\n`; | ||
| } else { | ||
| output += `❌ 工具调用错误\n`; | ||
| if (result.details.tools.missed.length > 0) { | ||
| const missed = result.details.tools.missed.map((t) => `${t.agent}.${t.tool}`).join(', '); | ||
| output += ` 遗漏: ${missed}\n`; | ||
| } | ||
| if (result.details.tools.extra.length > 0) { | ||
| const extra = result.details.tools.extra.map((t) => `${t.agent}.${t.tool}`).join(', '); | ||
| output += ` 多余: ${extra}\n`; | ||
| } | ||
| } | ||
| return output; | ||
| } | ||
| /** | ||
| * 事件总线 + 数据收集器(合并版本) | ||
| * 负责事件的发射、监听,以及收集执行过程中的数据 | ||
| */ | ||
| import { EventEmitter } from 'events'; | ||
| import { CollectedData } from './types.js'; | ||
| /** | ||
| * 事件类型 | ||
| */ | ||
| export type EventType = | ||
| | 'agent:call' // 子Agent被调用 | ||
| | 'tool:call' // 工具被调用 | ||
| | 'edit:complete'; // 编辑节点完成 | ||
| /** | ||
| * 事件数据类型 | ||
| */ | ||
| export interface AgentCallEvent { | ||
| agentName: string; | ||
| } | ||
| export interface ToolCallEvent { | ||
| agentName: string; | ||
| toolName: string; | ||
| } | ||
| export interface EditCompleteEvent { | ||
| successCount: number; | ||
| failCount: number; | ||
| } | ||
| /** | ||
| * 事件总线 - 单例模式 | ||
| * 合并了事件发射和数据收集功能 | ||
| */ | ||
| class EventBus { | ||
| private emitter = new EventEmitter(); | ||
| private static instance: EventBus; | ||
| // 数据收集(原 Collector 功能) | ||
| private agents: string[] = []; | ||
| private tools: Map<string, string[]> = new Map(); | ||
| private editResult: { success: number; fail: number } | null = null; | ||
| constructor() { | ||
| this.setupListeners(); | ||
| } | ||
| /** | ||
| * 获取单例实例 | ||
| */ | ||
| static getInstance(): EventBus { | ||
| if (!this.instance) { | ||
| this.instance = new EventBus(); | ||
| } | ||
| return this.instance; | ||
| } | ||
| /** | ||
| * 设置内部事件监听器(用于数据收集) | ||
| */ | ||
| private setupListeners() { | ||
| // 监听子Agent调用事件 | ||
| this.emitter.on('agent:call', (data: AgentCallEvent) => { | ||
| this.agents.push(data.agentName); | ||
| // 为该Agent初始化工具列表 | ||
| if (!this.tools.has(data.agentName)) { | ||
| this.tools.set(data.agentName, []); | ||
| } | ||
| }); | ||
| // 监听工具调用事件 | ||
| this.emitter.on('tool:call', (data: ToolCallEvent) => { | ||
| const agentTools = this.tools.get(data.agentName); | ||
| if (agentTools) { | ||
| // 去重添加 | ||
| if (!agentTools.includes(data.toolName)) { | ||
| agentTools.push(data.toolName); | ||
| } | ||
| } else { | ||
| this.tools.set(data.agentName, [data.toolName]); | ||
| } | ||
| }); | ||
| // 监听编辑完成事件 | ||
| this.emitter.on('edit:complete', (data: EditCompleteEvent) => { | ||
| this.editResult = { | ||
| success: data.successCount, | ||
| fail: data.failCount, | ||
| }; | ||
| }); | ||
| } | ||
| /** | ||
| * 发射事件 | ||
| */ | ||
| emit(event: EventType, data: AgentCallEvent | ToolCallEvent | EditCompleteEvent) { | ||
| this.emitter.emit(event, data); | ||
| } | ||
| /** | ||
| * 监听事件(供外部使用) | ||
| */ | ||
| on(event: EventType, handler: (data: any) => void) { | ||
| this.emitter.on(event, handler); | ||
| } | ||
| /** | ||
| * 获取收集到的数据 | ||
| */ | ||
| getData(): CollectedData { | ||
| return { | ||
| agents: [...new Set(this.agents)], // 去重 | ||
| tools: Object.fromEntries(this.tools), | ||
| editResult: this.editResult, | ||
| }; | ||
| } | ||
| /** | ||
| * 重置收集器(每次测试前调用) | ||
| */ | ||
| reset() { | ||
| this.agents = []; | ||
| this.tools = new Map(); | ||
| this.editResult = null; | ||
| } | ||
| /** | ||
| * 重置实例(用于测试) | ||
| */ | ||
| static resetInstance() { | ||
| if (this.instance) { | ||
| this.instance.emitter.removeAllListeners(); | ||
| this.instance = new EventBus(); | ||
| } | ||
| } | ||
| } | ||
| // 导出单例 | ||
| export const eventBus = EventBus.getInstance(); | ||
| /** | ||
| * 评估模块使用示例 | ||
| * 展示如何使用 SimpleAgent 和 MultiAgent 进行测试 | ||
| */ | ||
| import { eventBus } from './EventBus.js'; | ||
| import { evaluate, formatResult } from './evaluate.js'; | ||
| import { | ||
| TEST_CASES, | ||
| getTestById, | ||
| getSimpleAgentTests, | ||
| getMultiAgentTests, | ||
| } from './dataset.js'; | ||
| import { TestCase, EvaluateResult } from './types.js'; | ||
| import { SimpleAgent, MainAgent } from '../core/agent/index.js'; | ||
| import { createLLMService } from '../core/llm/index.js'; | ||
| import { ILLMService } from '../core/llm/types/index.js'; | ||
| // 缓存 LLM 服务和 Agent 实例 | ||
| let llmService: ILLMService | null = null; | ||
| let simpleAgent: SimpleAgent | null = null; | ||
| let multiAgent: MainAgent | null = null; | ||
| /** | ||
| * 获取 LLM 服务(延迟初始化) | ||
| */ | ||
| async function getLLMService(): Promise<ILLMService> { | ||
| if (!llmService) { | ||
| llmService = await createLLMService({ | ||
| provider: 'deepseek', | ||
| model: "deepseek-chat", | ||
| }); | ||
| } | ||
| return llmService; | ||
| } | ||
| /** | ||
| * 获取 SimpleAgent 实例 | ||
| */ | ||
| async function getSimpleAgent(): Promise<SimpleAgent> { | ||
| if (!simpleAgent) { | ||
| const service = await getLLMService(); | ||
| simpleAgent = new SimpleAgent(service, { name: 'simple_agent' }); | ||
| } | ||
| return simpleAgent; | ||
| } | ||
| /** | ||
| * 获取 MultiAgent (MainAgent) 实例 | ||
| */ | ||
| async function getMultiAgent(): Promise<MainAgent> { | ||
| if (!multiAgent) { | ||
| const service = await getLLMService(); | ||
| multiAgent = new MainAgent(service, 'main_agent'); | ||
| } | ||
| return multiAgent; | ||
| } | ||
| /** | ||
| * 判断测试用例是否为多 Agent 测试 | ||
| */ | ||
| function isMultiAgentTest(testCase: TestCase): boolean { | ||
| return testCase.id.startsWith('M'); | ||
| } | ||
| /** | ||
| * 运行单个测试用例 | ||
| */ | ||
| async function runTest(testCase: TestCase): Promise<EvaluateResult | null> { | ||
| console.log(`\n${'='.repeat(50)}`); | ||
| console.log(`🧪 测试: ${testCase.id} - ${testCase.description}`); | ||
| console.log(`${'='.repeat(50)}`); | ||
| const startTime = Date.now(); | ||
| try { | ||
| let agents: string[]; | ||
| let tools: Record<string, string[]>; | ||
| let finalResponse: string; | ||
| let success: boolean; | ||
| let error: string | undefined; | ||
| if (isMultiAgentTest(testCase)) { | ||
| // 多 Agent 测试 | ||
| const agent = await getMultiAgent(); | ||
| const result = await agent.run(testCase.input); | ||
| agents = result.agents; | ||
| tools = result.tools; | ||
| finalResponse = result.finalResponse; | ||
| success = result.success; | ||
| error = result.error; | ||
| } else { | ||
| // 单 Agent 测试 | ||
| const agent = await getSimpleAgent(); | ||
| const result = await agent.run(testCase.input); | ||
| agents = result.agents; | ||
| tools = result.tools; | ||
| finalResponse = result.finalResponse; | ||
| success = result.success; | ||
| error = result.error; | ||
| } | ||
| // 评估 | ||
| const evalResult = evaluate(testCase, { | ||
| agents, | ||
| tools, | ||
| editResult: null, | ||
| }); | ||
| // 输出结果 | ||
| const status = evalResult.passed ? '✅ PASS' : '❌ FAIL'; | ||
| const time = Date.now() - startTime; | ||
| console.log(`\n${status} (${time}ms)`); | ||
| console.log(formatResult(evalResult)); | ||
| const responsePreview = finalResponse.slice(0, 200); | ||
| console.log(`\n📝 Agent回复: ${responsePreview}${finalResponse.length > 200 ? '...' : ''}`); | ||
| if (!success) { | ||
| console.log(`\n⚠️ Agent 执行失败: ${error}`); | ||
| } | ||
| return evalResult; | ||
| } catch (err) { | ||
| console.error(`❌ 执行失败:`, err); | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * 运行测试集 | ||
| */ | ||
| async function runTests(testCases: TestCase[], title: string) { | ||
| console.log(`\n${'#'.repeat(60)}`); | ||
| console.log(`# ${title}: ${testCases.length} 个用例`); | ||
| console.log(`${'#'.repeat(60)}`); | ||
| const results: Array<{ testCase: TestCase; result: EvaluateResult }> = []; | ||
| const startTime = Date.now(); | ||
| for (const testCase of testCases) { | ||
| const result = await runTest(testCase); | ||
| if (result) { | ||
| results.push({ testCase, result }); | ||
| } | ||
| } | ||
| // 汇总 | ||
| const passed = results.filter((r) => r.result.passed).length; | ||
| const failed = results.length - passed; | ||
| const totalTime = Date.now() - startTime; | ||
| console.log(`\n${'='.repeat(60)}`); | ||
| console.log(`${title} 完成`); | ||
| console.log(`${'='.repeat(60)}`); | ||
| console.log(`总数: ${results.length}`); | ||
| console.log(`✅ 通过: ${passed}`); | ||
| console.log(`❌ 失败: ${failed}`); | ||
| console.log(`⏱️ 耗时: ${(totalTime / 1000).toFixed(2)}s`); | ||
| return results; | ||
| } | ||
| /** | ||
| * 运行所有测试用例 | ||
| */ | ||
| async function runAllTests() { | ||
| console.log(`\n${'#'.repeat(60)}`); | ||
| console.log(`# 开始全量测试: ${TEST_CASES.length} 个用例`); | ||
| console.log(`${'#'.repeat(60)}`); | ||
| const startTime = Date.now(); | ||
| // 运行单 Agent 测试 | ||
| const simpleResults = await runTests(getSimpleAgentTests(), '单 Agent 测试'); | ||
| // 运行多 Agent 测试 | ||
| const multiResults = await runTests(getMultiAgentTests(), '多 Agent 测试'); | ||
| // 总汇总 | ||
| const allResults = [...simpleResults, ...multiResults]; | ||
| const passed = allResults.filter((r) => r.result.passed).length; | ||
| const failed = allResults.length - passed; | ||
| const totalTime = Date.now() - startTime; | ||
| console.log(`\n${'#'.repeat(60)}`); | ||
| console.log(`# 全部测试完成`); | ||
| console.log(`${'#'.repeat(60)}`); | ||
| console.log(`总数: ${allResults.length}`); | ||
| console.log(`✅ 通过: ${passed}`); | ||
| console.log(`❌ 失败: ${failed}`); | ||
| console.log(`⏱️ 总耗时: ${(totalTime / 1000).toFixed(2)}s`); | ||
| } | ||
| /** | ||
| * 打印帮助信息 | ||
| */ | ||
| function printHelp() { | ||
| console.log(` | ||
| 评估模块使用说明: | ||
| npx ts-node example.ts [选项] | ||
| 选项: | ||
| --help 显示帮助信息 | ||
| --test <id> 运行指定测试用例 (如: S1, M1) | ||
| --simple 运行单 Agent 测试集 (2 个用例) | ||
| --multi 运行多 Agent 测试集 (3 个用例) | ||
| (无参数) 运行所有测试用例 | ||
| 测试用例 ID: | ||
| S1, S2 单 Agent 测试 (SimpleAgent) | ||
| M1, M2, M3 多 Agent 测试 (MultiAgent) | ||
| `); | ||
| } | ||
| /** | ||
| * 主函数 | ||
| */ | ||
| async function main() { | ||
| const args = process.argv.slice(2); | ||
| if (args.includes('--help')) { | ||
| printHelp(); | ||
| } else if (args.includes('--simple')) { | ||
| // 运行单 Agent 测试集 | ||
| await runTests(getSimpleAgentTests(), '单 Agent 测试'); | ||
| } else if (args.includes('--multi')) { | ||
| // 运行多 Agent 测试集 | ||
| await runTests(getMultiAgentTests(), '多 Agent 测试'); | ||
| } else if (args.includes('--test') && args[args.indexOf('--test') + 1]) { | ||
| // 运行指定测试用例 | ||
| const testId = args[args.indexOf('--test') + 1]; | ||
| const testCase = getTestById(testId); | ||
| if (testCase) { | ||
| await runTest(testCase); | ||
| } else { | ||
| console.error(`未找到测试用例: ${testId}`); | ||
| console.log('可用的测试用例 ID: S1, S2, M1, M2, M3'); | ||
| } | ||
| } else { | ||
| // 运行所有测试 | ||
| await runAllTests(); | ||
| } | ||
| } | ||
| // 运行 | ||
| main().catch(console.error); |
| /** | ||
| * 评估模块模板 - 类型定义 | ||
| * 简化版本,专注核心功能 | ||
| */ | ||
| /** | ||
| * 测试用例定义 | ||
| */ | ||
| export interface TestCase { | ||
| id: string; // 测试用例ID | ||
| description: string; // 用例描述 | ||
| input: string; // 用户输入 | ||
| expected: ExpectedBehavior; // 期望结果 | ||
| } | ||
| /** | ||
| * 期望行为定义 | ||
| */ | ||
| export interface ExpectedBehavior { | ||
| // 期望调用的子Agent列表 | ||
| agents: string[]; | ||
| // 每个子Agent期望调用的工具 | ||
| tools: { | ||
| [agentName: string]: string[]; | ||
| }; | ||
| } | ||
| /** | ||
| * 事件收集器收集到的实际执行数据 | ||
| */ | ||
| export interface CollectedData { | ||
| // 实际调用的子Agent列表 | ||
| agents: string[]; | ||
| // 每个Agent实际调用的工具 | ||
| tools: { | ||
| [agentName: string]: string[]; | ||
| }; | ||
| // 编辑节点执行结果 | ||
| editResult: { | ||
| success: number; | ||
| fail: number; | ||
| } | null; | ||
| } | ||
| /** | ||
| * 评估结果 | ||
| */ | ||
| export interface EvaluateResult { | ||
| passed: boolean; // 是否通过 | ||
| agentMatch: boolean; // Agent调用是否匹配 | ||
| toolMatch: boolean; // 工具调用是否匹配 | ||
| // 详细信息 | ||
| details: { | ||
| agents: { | ||
| expected: string[]; | ||
| actual: string[]; | ||
| missed: string[]; // 遗漏的Agent | ||
| extra: string[]; // 多余的Agent | ||
| }; | ||
| tools: { | ||
| expected: { [agentName: string]: string[] }; | ||
| actual: { [agentName: string]: string[] }; | ||
| missed: { agent: string; tool: string }[]; | ||
| extra: { agent: string; tool: string }[]; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * 多智能体对话示例 | ||
| * 演示 MainAgent 协调多个 SubAgent 完成任务 | ||
| */ | ||
| import { createLLMService } from '../core/llm/index.js'; | ||
| import { MainAgent } from '../core/agent/index.js'; | ||
| async function main() { | ||
| console.log('🚀 Multi-Agent Chat Example\n'); | ||
| // 创建 LLM 服务 | ||
| const service = await createLLMService({ | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| }); | ||
| // 创建多智能体系统 (MainAgent + SubAgents) | ||
| const multiAgent = new MainAgent(service, 'main_agent'); | ||
| console.log('📋 任务: 分析如何提升代码质量\n'); | ||
| // 执行多智能体协作 | ||
| const result = await multiAgent.run('请帮我分析如何提升代码质量,并给出具体的改进建议'); | ||
| // 输出结果 | ||
| console.log('\n' + '='.repeat(60)); | ||
| console.log('📊 执行结果汇总'); | ||
| console.log('='.repeat(60)); | ||
| console.log(`\n✅ 执行状态: ${result.success ? '成功' : '失败'}`); | ||
| console.log(`🤖 参与的 Agent: ${result.agents.join(' → ')}`); | ||
| console.log('\n📝 子 Agent 执行记录:'); | ||
| for (const subResult of result.subAgentResults) { | ||
| console.log(`\n [${subResult.agentName}]`); | ||
| console.log(` 状态: ${subResult.success ? '✅ 成功' : '❌ 失败'}`); | ||
| console.log(` 输出: ${subResult.result.slice(0, 200)}${subResult.result.length > 200 ? '...' : ''}`); | ||
| } | ||
| console.log('\n' + '='.repeat(60)); | ||
| console.log('🎯 最终响应'); | ||
| console.log('='.repeat(60)); | ||
| console.log(result.finalResponse); | ||
| } | ||
| main().catch(console.error); |
| /** | ||
| * 简单对话示例 | ||
| */ | ||
| import { createLLMService } from "../core/llm/index.js"; | ||
| async function main() { | ||
| // 创建 LLM 服务 | ||
| const service = await createLLMService({ | ||
| provider: "deepseek", | ||
| model: "deepseek-chat", | ||
| }); | ||
| console.log("🚀 Simple Chat Example\n"); | ||
| // 简单对话 | ||
| const response = await service.simpleChat( | ||
| "你好!你能用一句话介绍一下自己吗?", | ||
| "你是一个有用的AI助手。" | ||
| ); | ||
| console.log("Assistant:", response); | ||
| } | ||
| main().catch(console.error); |
| /** | ||
| * 简单的日志工具 | ||
| */ | ||
| export enum LogLevel { | ||
| DEBUG = 'debug', | ||
| INFO = 'info', | ||
| WARN = 'warn', | ||
| ERROR = 'error', | ||
| } | ||
| class Logger { | ||
| private level: LogLevel = LogLevel.INFO; | ||
| setLevel(level: LogLevel) { | ||
| this.level = level; | ||
| } | ||
| debug(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.DEBUG)) { | ||
| console.log('[DEBUG]', ...args); | ||
| } | ||
| } | ||
| info(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.INFO)) { | ||
| console.log('[INFO]', ...args); | ||
| } | ||
| } | ||
| warn(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.WARN)) { | ||
| console.warn('[WARN]', ...args); | ||
| } | ||
| } | ||
| error(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.ERROR)) { | ||
| console.error('[ERROR]', ...args); | ||
| } | ||
| } | ||
| private shouldLog(level: LogLevel): boolean { | ||
| const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; | ||
| return levels.indexOf(level) >= levels.indexOf(this.level); | ||
| } | ||
| } | ||
| export const logger = new Logger(); | ||
| // 根据配置设置日志级别 | ||
| import { config } from '../config/env.js'; | ||
| logger.setLevel(config.logging.level as LogLevel); |
+11
-10
@@ -75,12 +75,13 @@ #!/usr/bin/env node | ||
| . | ||
| ├── core/ # 核心系统模块 | ||
| │ ├── llm/ # LLM 服务层(多模型支持) | ||
| │ ├── context/ # 上下文管理系统 | ||
| │ ├── tool/ # 工具管理系统 | ||
| │ ├── agent/ # Agent 编排(预留) | ||
| │ └── promptManager/ # 提示词管理 | ||
| ├── evaluation/ # 测试与评估 | ||
| ├── utils/ # 工具函数(日志等) | ||
| ├── config/ # 配置(环境变量加载) | ||
| ├── examples/ # 使用示例 | ||
| ├── src/ # 源代码目录 | ||
| │ ├── config/ # 配置(环境变量加载) | ||
| │ ├── utils/ # 工具函数(日志等) | ||
| │ ├── core/ # 核心系统模块 | ||
| │ │ ├── llm/ # LLM 服务层(多模型支持) | ||
| │ │ ├── context/ # 上下文管理系统 | ||
| │ │ ├── tool/ # 工具管理系统 | ||
| │ │ ├── agent/ # Agent 编排 | ||
| │ │ └── promptManager/ # 提示词管理 | ||
| │ ├── evaluation/ # 测试与评估 | ||
| │ └── examples/ # 使用示例 | ||
| └── docs/ # 项目文档 | ||
@@ -87,0 +88,0 @@ └── ARCHITECTURE.md # 架构设计文档(必读!) |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} |
@@ -10,4 +10,4 @@ export function generatePackageJson(config) { | ||
| // 开发脚本 | ||
| dev: 'tsx watch examples/simple-chat.ts', | ||
| 'dev:multi-chat': 'tsx watch examples/multi-chat.ts', | ||
| dev: 'tsx watch src/examples/simple-chat.ts', | ||
| 'dev:multi-chat': 'tsx watch src/examples/multi-chat.ts', | ||
| // 构建脚本 | ||
@@ -21,3 +21,3 @@ build: 'tsc', | ||
| // 评估脚本 | ||
| eval: 'tsx evaluationTemplate/example.ts', | ||
| eval: 'tsx src/evaluation/example.ts', | ||
| // 清理脚本 | ||
@@ -24,0 +24,0 @@ clean: 'rm -rf dist', |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"packageJson.js","sourceRoot":"","sources":["../../src/templates/packageJson.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,mBAAmB,CAAC,MAAqB;IACvD,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,WAAW;QACxB,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,iDAAiD;QAC9D,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE;YACP,OAAO;YACP,GAAG,EAAE,mCAAmC;YACxC,gBAAgB,EAAE,kCAAkC;YAEpD,OAAO;YACP,KAAK,EAAE,KAAK;YACZ,YAAY,EAAE,cAAc;YAE5B,OAAO;YACP,IAAI,EAAE,YAAY;YAClB,YAAY,EAAE,QAAQ;YACtB,SAAS,EAAE,aAAa;YAExB,OAAO;YACP,IAAI,EAAE,mCAAmC;YAEzC,OAAO;YACP,KAAK,EAAE,aAAa;SACrB;QACD,YAAY,EAAE;YACZ,MAAM,EAAE,SAAS,EAAE,2BAA2B;YAC9C,MAAM,EAAE,SAAS,EAAE,SAAS;SAC7B;QACD,eAAe,EAAE;YACf,aAAa,EAAE,UAAU,EAAE,YAAY;YACvC,GAAG,EAAE,SAAS,EAAE,iBAAiB;YACjC,UAAU,EAAE,QAAQ,EAAE,iBAAiB;YACvC,MAAM,EAAE,QAAQ,EAAE,OAAO;YACzB,YAAY,EAAE,QAAQ,EAAE,QAAQ;SACjC;QACD,OAAO,EAAE;YACP,IAAI,EAAE,UAAU;SACjB;QACD,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,CAAC;KACxE,CAAC;AACJ,CAAC"} | ||
| {"version":3,"file":"packageJson.js","sourceRoot":"","sources":["../../src/templates/packageJson.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,mBAAmB,CAAC,MAAqB;IACvD,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,WAAW;QACxB,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,iDAAiD;QAC9D,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE;YACP,OAAO;YACP,GAAG,EAAE,uCAAuC;YAC5C,gBAAgB,EAAE,sCAAsC;YAExD,OAAO;YACP,KAAK,EAAE,KAAK;YACZ,YAAY,EAAE,cAAc;YAE5B,OAAO;YACP,IAAI,EAAE,YAAY;YAClB,YAAY,EAAE,QAAQ;YACtB,SAAS,EAAE,aAAa;YAExB,OAAO;YACP,IAAI,EAAE,+BAA+B;YAErC,OAAO;YACP,KAAK,EAAE,aAAa;SACrB;QACD,YAAY,EAAE;YACZ,MAAM,EAAE,SAAS,EAAE,2BAA2B;YAC9C,MAAM,EAAE,SAAS,EAAE,SAAS;SAC7B;QACD,eAAe,EAAE;YACf,aAAa,EAAE,UAAU,EAAE,YAAY;YACvC,GAAG,EAAE,SAAS,EAAE,iBAAiB;YACjC,UAAU,EAAE,QAAQ,EAAE,iBAAiB;YACvC,MAAM,EAAE,QAAQ,EAAE,OAAO;YACzB,YAAY,EAAE,QAAQ,EAAE,QAAQ;SACjC;QACD,OAAO,EAAE;YACP,IAAI,EAAE,UAAU;SACjB;QACD,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,CAAC;KACxE,CAAC;AACJ,CAAC"} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"tsconfig.d.ts","sourceRoot":"","sources":["../../src/templates/tsconfig.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,gBAAgB,IAAI,MAAM,CAkDzC"} | ||
| {"version":3,"file":"tsconfig.d.ts","sourceRoot":"","sources":["../../src/templates/tsconfig.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,gBAAgB,IAAI,MAAM,CAyCzC"} |
@@ -13,3 +13,3 @@ /** | ||
| outDir: './dist', | ||
| rootDir: './', | ||
| rootDir: './src', | ||
| // 严格模式 | ||
@@ -34,15 +34,6 @@ strict: true, | ||
| paths: { | ||
| '@/*': ['./*'], | ||
| '@/*': ['./src/*'], | ||
| }, | ||
| }, | ||
| include: [ | ||
| 'llm/**/*', | ||
| 'context/**/*', | ||
| 'tool/**/*', | ||
| 'agent/**/*', | ||
| 'evaluationTemplate/**/*', | ||
| 'utils/**/*', | ||
| 'config/**/*', | ||
| 'examples/**/*', | ||
| ], | ||
| include: ['src/**/*'], | ||
| exclude: ['node_modules', 'dist', '**/*.test.ts'], | ||
@@ -49,0 +40,0 @@ }; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"tsconfig.js","sourceRoot":"","sources":["../../src/templates/tsconfig.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,eAAe,EAAE;YACf,OAAO;YACP,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,QAAQ;YAChB,gBAAgB,EAAE,MAAM;YAExB,OAAO;YACP,MAAM,EAAE,QAAQ;YAChB,OAAO,EAAE,IAAI;YAEb,OAAO;YACP,MAAM,EAAE,IAAI;YACZ,gBAAgB,EAAE,IAAI;YACtB,mBAAmB,EAAE,IAAI;YACzB,cAAc,EAAE,IAAI;YACpB,kBAAkB,EAAE,IAAI;YACxB,iBAAiB,EAAE,IAAI;YAEvB,gBAAgB;YAChB,eAAe,EAAE,IAAI;YACrB,4BAA4B,EAAE,IAAI;YAElC,OAAO;YACP,YAAY,EAAE,IAAI;YAClB,iBAAiB,EAAE,IAAI;YAEvB,MAAM;YACN,sBAAsB,EAAE,IAAI;YAC5B,qBAAqB,EAAE,IAAI;YAE3B,YAAY;YACZ,OAAO,EAAE,GAAG;YACZ,KAAK,EAAE;gBACL,KAAK,EAAE,CAAC,KAAK,CAAC;aACf;SACF;QACD,OAAO,EAAE;YACP,UAAU;YACV,cAAc;YACd,WAAW;YACX,YAAY;YACZ,yBAAyB;YACzB,YAAY;YACZ,aAAa;YACb,eAAe;SAChB;QACD,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,cAAc,CAAC;KAClD,CAAC;AACJ,CAAC"} | ||
| {"version":3,"file":"tsconfig.js","sourceRoot":"","sources":["../../src/templates/tsconfig.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,eAAe,EAAE;YACf,OAAO;YACP,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,QAAQ;YAChB,gBAAgB,EAAE,MAAM;YAExB,OAAO;YACP,MAAM,EAAE,QAAQ;YAChB,OAAO,EAAE,OAAO;YAEhB,OAAO;YACP,MAAM,EAAE,IAAI;YACZ,gBAAgB,EAAE,IAAI;YACtB,mBAAmB,EAAE,IAAI;YACzB,cAAc,EAAE,IAAI;YACpB,kBAAkB,EAAE,IAAI;YACxB,iBAAiB,EAAE,IAAI;YAEvB,gBAAgB;YAChB,eAAe,EAAE,IAAI;YACrB,4BAA4B,EAAE,IAAI;YAElC,OAAO;YACP,YAAY,EAAE,IAAI;YAClB,iBAAiB,EAAE,IAAI;YAEvB,MAAM;YACN,sBAAsB,EAAE,IAAI;YAC5B,qBAAqB,EAAE,IAAI;YAE3B,YAAY;YACZ,OAAO,EAAE,GAAG;YACZ,KAAK,EAAE;gBACL,KAAK,EAAE,CAAC,SAAS,CAAC;aACnB;SACF;QACD,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,cAAc,CAAC;KAClD,CAAC;AACJ,CAAC"} |
@@ -11,3 +11,3 @@ /** | ||
| environment: 'node', | ||
| include: ['**/__tests__/**/*.test.ts'], | ||
| include: ['src/**/__tests__/**/*.test.ts'], | ||
| coverage: { | ||
@@ -14,0 +14,0 @@ provider: 'v8', |
+1
-0
| /** | ||
| * 类型定义 | ||
| * | ||
| */ | ||
@@ -4,0 +5,0 @@ export interface ProjectConfig { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IAGvB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;IAChD,WAAW,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC"} | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IAGvB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;IAChD,WAAW,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC"} |
+1
-0
| /** | ||
| * 类型定义 | ||
| * | ||
| */ | ||
| export {}; | ||
| //# sourceMappingURL=types.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"} | ||
| {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"fileSystem.d.ts","sourceRoot":"","sources":["../../src/utils/fileSystem.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAK5C;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjF;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAOtF;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CAGf"} | ||
| {"version":3,"file":"fileSystem.d.ts","sourceRoot":"","sources":["../../src/utils/fileSystem.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAK5C;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjF;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAOtF;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CAGf"} |
+22
-14
@@ -25,17 +25,25 @@ /** | ||
| const targetDir = path.join(process.cwd(), config.projectName); | ||
| // 要复制的目录列表 | ||
| const dirs = ['core', 'docs', 'evaluation', 'utils', 'config', 'examples']; | ||
| for (const dir of dirs) { | ||
| const srcPath = path.join(templateDir, dir); | ||
| const destPath = path.join(targetDir, dir); | ||
| if (await fs.pathExists(srcPath)) { | ||
| await fs.copy(srcPath, destPath, { | ||
| filter: (src) => { | ||
| const basename = path.basename(src); | ||
| // 过滤掉 .DS_Store 等系统文件 | ||
| return !basename.startsWith('.') || basename === '.gitignore'; | ||
| }, | ||
| }); | ||
| } | ||
| // 复制 src 目录(包含所有源代码) | ||
| const srcPath = path.join(templateDir, 'src'); | ||
| const srcDest = path.join(targetDir, 'src'); | ||
| if (await fs.pathExists(srcPath)) { | ||
| await fs.copy(srcPath, srcDest, { | ||
| filter: (src) => { | ||
| const basename = path.basename(src); | ||
| // 过滤掉 .DS_Store 等系统文件 | ||
| return !basename.startsWith('.') || basename === '.gitignore'; | ||
| }, | ||
| }); | ||
| } | ||
| // 单独复制 docs 目录(保留在根目录) | ||
| const docsPath = path.join(templateDir, 'docs'); | ||
| const docsDest = path.join(targetDir, 'docs'); | ||
| if (await fs.pathExists(docsPath)) { | ||
| await fs.copy(docsPath, docsDest, { | ||
| filter: (src) => { | ||
| const basename = path.basename(src); | ||
| return !basename.startsWith('.') || basename === '.gitignore'; | ||
| }, | ||
| }); | ||
| } | ||
| // 复制 .env.example | ||
@@ -42,0 +50,0 @@ const envSrc = path.join(templateDir, '.env.example'); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"fileSystem.js","sourceRoot":"","sources":["../../src/utils/fileSystem.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"fileSystem.js","sourceRoot":"","sources":["../../src/utils/fileSystem.ts"],"names":[],"mappings":"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"} |
+1
-1
| { | ||
| "name": "create-context-template", | ||
| "version": "1.0.1", | ||
| "version": "1.0.2", | ||
| "description": "CLI tool to scaffold LLM projects with context engineering architecture", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+35
-23
@@ -9,2 +9,4 @@ # create-context-template | ||
| > 📚 **相关资源**:[上下文工程实践指南](https://github.com/WakeUp-Jin/Practical-Guide-to-Context-Engineering) -深入学习上下文工程的设计理念和最佳实践 | ||
| ## ✨ 特性 | ||
@@ -144,2 +146,3 @@ | ||
| **核心方法:** | ||
| - `complete(messages, tools)` - 完整的 LLM 调用,支持工具 | ||
@@ -153,10 +156,10 @@ - `simpleChat(userMessage, systemPrompt)` - 简单对话 | ||
| | 上下文类型 | 说明 | 用途 | | ||
| |----------|------|------| | ||
| | **ConversationContext** | 会话历史记录 | 维护对话连续性 | | ||
| | 上下文类型 | 说明 | 用途 | | ||
| | ------------------------------ | ------------ | ---------------- | | ||
| | **ConversationContext** | 会话历史记录 | 维护对话连续性 | | ||
| | **ToolMessageSequenceContext** | 工具调用序列 | 追踪工具使用历史 | | ||
| | **MemoryContext** | 用户记忆 | 长期记忆存储 | | ||
| | **SystemPromptContext** | 系统提示词 | 定义 AI 行为 | | ||
| | **StructuredOutputContext** | 结构化输出 | JSON 格式化输出 | | ||
| | **RelevantContext** | 相关上下文 | 动态相关信息 | | ||
| | **MemoryContext** | 用户记忆 | 长期记忆存储 | | ||
| | **SystemPromptContext** | 系统提示词 | 定义 AI 行为 | | ||
| | **StructuredOutputContext** | 结构化输出 | JSON 格式化输出 | | ||
| | **RelevantContext** | 相关上下文 | 动态相关信息 | | ||
@@ -168,2 +171,3 @@ ### 3. 工具系统 | ||
| **内置工具:** | ||
| - **ReadFileTool** - 读取文件内容 | ||
@@ -173,2 +177,3 @@ - **ListFilesTool** - 列出目录文件 | ||
| **工具定义规范:** | ||
| - 标准化的工具接口 | ||
@@ -204,2 +209,3 @@ - JSON Schema 参数定义 | ||
| **核心思想:** | ||
| 1. **LLM 是核心** - 保证核心是 LLM,随着模型能力提升,Agent 效果自动变好 | ||
@@ -216,4 +222,4 @@ 2. **开发重心是上下文** - 极大发挥应用开发者的能力和创造力 | ||
| ```typescript | ||
| import { createLLMService } from './core/llm/index.js'; | ||
| import { loadEnv } from './config/env.js'; | ||
| import { createLLMService } from "./core/llm/index.js"; | ||
| import { loadEnv } from "./config/env.js"; | ||
@@ -223,4 +229,4 @@ loadEnv(); | ||
| const service = await createLLMService({ | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| provider: "deepseek", | ||
| model: "deepseek-chat", | ||
| apiKey: process.env.DEEPSEEK_API_KEY, | ||
@@ -230,7 +236,7 @@ }); | ||
| const response = await service.simpleChat( | ||
| 'Hello! Can you introduce yourself?', | ||
| 'You are a helpful AI assistant.' | ||
| "Hello! Can you introduce yourself?", | ||
| "You are a helpful AI assistant." | ||
| ); | ||
| console.log('Assistant:', response); | ||
| console.log("Assistant:", response); | ||
| ``` | ||
@@ -241,5 +247,5 @@ | ||
| ```typescript | ||
| import { createLLMService } from './core/llm/index.js'; | ||
| import { ContextManager } from './core/context/index.js'; | ||
| import { ToolManager } from './core/tool/index.js'; | ||
| import { createLLMService } from "./core/llm/index.js"; | ||
| import { ContextManager } from "./core/context/index.js"; | ||
| import { ToolManager } from "./core/tool/index.js"; | ||
@@ -253,11 +259,14 @@ // 初始化上下文和工具 | ||
| // 创建 LLM 服务 | ||
| const service = await createLLMService({ | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| apiKey: process.env.DEEPSEEK_API_KEY, | ||
| }, toolManager); | ||
| const service = await createLLMService( | ||
| { | ||
| provider: "deepseek", | ||
| model: "deepseek-chat", | ||
| apiKey: process.env.DEEPSEEK_API_KEY, | ||
| }, | ||
| toolManager | ||
| ); | ||
| // 使用 generate 方法自动处理工具调用 | ||
| const answer = await service.generate( | ||
| '请帮我读取 package.json 文件,并告诉我项目名称是什么' | ||
| "请帮我读取 package.json 文件,并告诉我项目名称是什么" | ||
| ); | ||
@@ -321,2 +330,3 @@ | ||
| ### v1.0 - 核心功能 ✅ | ||
| - [x] CLI 交互界面 | ||
@@ -330,2 +340,3 @@ - [x] 多包管理器支持 | ||
| ### v1.1 - 增强功能 🚧 | ||
| - [ ] 模板类型选择(full/minimal) | ||
@@ -337,2 +348,3 @@ - [ ] 更多 LLM 提供商模板 | ||
| ### v2.0 - 扩展功能 🔮 | ||
| - [ ] Web 服务器集成(Hono/Koa/Express) | ||
@@ -339,0 +351,0 @@ - [ ] 插件系统 |
| /** | ||
| * 环境变量配置管理 | ||
| * | ||
| * 自动加载 .env 文件: | ||
| * - 使用 dotenv 库自动加载环境变量 | ||
| * - 支持 Node.js 所有版本 | ||
| * | ||
| * 优先级(从高到低): | ||
| * 1. .env.local (本地覆盖,不提交到 git) | ||
| * 2. .env.{NODE_ENV} (环境特定配置,如 .env.production) | ||
| * 3. .env (默认配置) | ||
| * | ||
| * 使用方式: | ||
| * ```typescript | ||
| * import { config } from './config/env.js'; | ||
| * console.log(config.llm.deepseek.apiKey); | ||
| * ``` | ||
| */ | ||
| import { config as dotenvConfig } from "dotenv"; | ||
| import { fileURLToPath } from "url"; | ||
| import { dirname, resolve } from "path"; | ||
| //加载环境变量文件 | ||
| loadEnv(); | ||
| /** 应用配置接口 */ | ||
| export interface AppConfig { | ||
| /** Node 环境 */ | ||
| nodeEnv: "development" | "production" | "test"; | ||
| /** 日志配置 */ | ||
| logging: { | ||
| level: "debug" | "info" | "warn" | "error"; | ||
| enableConsole: boolean; | ||
| }; | ||
| /** LLM 提供商配置 */ | ||
| llm: { | ||
| /** DeepSeek 配置 */ | ||
| deepseek: { | ||
| apiKey: string; | ||
| baseURL?: string; | ||
| }; | ||
| }; | ||
| /** 默认 LLM 配置 */ | ||
| defaultProvider: string; | ||
| } | ||
| /** 获取环境变量,支持默认值 */ | ||
| function getEnvVar(name: string, defaultValue?: string): string { | ||
| const value = process.env[name]; | ||
| if (!value && defaultValue === undefined) { | ||
| throw new Error(`Environment variable ${name} is required but not set`); | ||
| } | ||
| return value || defaultValue!; | ||
| } | ||
| /** 获取环境变量布尔值 */ | ||
| function getEnvBoolean(name: string, defaultValue: boolean = false): boolean { | ||
| const value = process.env[name]; | ||
| if (!value) return defaultValue; | ||
| return value.toLowerCase() === "true" || value === "1"; | ||
| } | ||
| /** 验证 NODE_ENV 值 */ | ||
| function validateNodeEnv(env: string): "development" | "production" | "test" { | ||
| if (["development", "production", "test"].includes(env)) { | ||
| return env as "development" | "production" | "test"; | ||
| } | ||
| console.warn(`Invalid NODE_ENV: ${env}, falling back to 'development'`); | ||
| return "development"; | ||
| } | ||
| /** 验证日志级别 */ | ||
| function validateLogLevel(level: string): "debug" | "info" | "warn" | "error" { | ||
| if (["debug", "info", "warn", "error"].includes(level)) { | ||
| return level as "debug" | "info" | "warn" | "error"; | ||
| } | ||
| console.warn(`Invalid LOG_LEVEL: ${level}, falling back to 'info'`); | ||
| return "info"; | ||
| } | ||
| /** 导出类型安全的配置对象 */ | ||
| export const config: AppConfig = { | ||
| nodeEnv: validateNodeEnv(getEnvVar("NODE_ENV", "development")), | ||
| logging: { | ||
| level: validateLogLevel(getEnvVar("LOG_LEVEL", "info")), | ||
| enableConsole: getEnvBoolean("ENABLE_CONSOLE_LOG", true), | ||
| }, | ||
| llm: { | ||
| deepseek: { | ||
| apiKey: getEnvVar("DEEPSEEK_API_KEY", ""), | ||
| baseURL: getEnvVar("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), | ||
| }, | ||
| }, | ||
| defaultProvider: getEnvVar("DEFAULT_LLM_PROVIDER", "deepseek"), | ||
| }; | ||
| /** | ||
| * 获取指定提供商的配置 | ||
| * @param provider - LLM 提供商名称 | ||
| * @returns 提供商配置对象 | ||
| */ | ||
| export function getLLMKeyByProvider(provider: string) { | ||
| const providerKey = provider.toLowerCase(); | ||
| let apiKey = config.llm[providerKey].apiKey; | ||
| if (!apiKey) { | ||
| throw new Error(`API key for provider "${provider}" not found. `); | ||
| } | ||
| return apiKey; | ||
| } | ||
| export function loadEnv() { | ||
| // 获取当前文件所在目录 | ||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
| const projectRoot = resolve(__dirname, ".."); | ||
| // 加载环境变量文件(按优先级) | ||
| const nodeEnv = process.env.NODE_ENV || "development"; | ||
| // 1. 加载 .env.local(最高优先级) | ||
| dotenvConfig({ path: resolve(projectRoot, ".env.local"), override: false }); | ||
| // 2. 加载 .env.{NODE_ENV} | ||
| if (nodeEnv !== "development") { | ||
| dotenvConfig({ | ||
| path: resolve(projectRoot, `.env.${nodeEnv}`), | ||
| override: false, | ||
| }); | ||
| } | ||
| // 3. 加载 .env(默认配置) | ||
| dotenvConfig({ path: resolve(projectRoot, ".env"), override: false }); | ||
| } | ||
| /** | ||
| * Agent 模块统一导出 | ||
| */ | ||
| export { SimpleAgent } from './SimpleAgent.js'; | ||
| export type { AgentResult, AgentConfig } from './SimpleAgent.js'; | ||
| export { MainAgent, SubAgent, createMultiAgentSystem } from './MultiAgent.js'; | ||
| export type { MainAgentResult, SubAgentResult } from './MultiAgent.js'; |
| /** | ||
| * 多智能体系统实现 | ||
| * 演示主Agent协调多个子Agent完成任务的架构 | ||
| */ | ||
| import { ContextManager, ContextType, Message } from '../context/index.js'; | ||
| import { ToolManager } from '../tool/ToolManager.js'; | ||
| import { ILLMService } from '../llm/types/index.js'; | ||
| import { executeToolLoop } from '../llm/utils/executeToolLoop.js'; | ||
| import { ExecutionHistoryContext } from '../context/modules/ExecutionHistoryContext.js'; | ||
| import { eventBus } from '../../evaluation/EventBus.js'; | ||
| import { | ||
| MAIN_AGENT_PROMPT, | ||
| SUB_AGENT_A_PROMPT, | ||
| SUB_AGENT_B_PROMPT, | ||
| } from '../promptManager/index.js'; | ||
| /** | ||
| * 子Agent执行结果 | ||
| */ | ||
| export interface SubAgentResult { | ||
| /** 子Agent名称 */ | ||
| agentName: string; | ||
| /** 执行结果 */ | ||
| result: string; | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * 主Agent执行结果 | ||
| */ | ||
| export interface MainAgentResult { | ||
| /** 收集到的 Agent 名称列表 */ | ||
| agents: string[]; | ||
| /** 每个 Agent 调用的工具记录 */ | ||
| tools: Record<string, string[]>; | ||
| /** 最终响应 */ | ||
| finalResponse: string; | ||
| /** 子Agent执行记录 */ | ||
| subAgentResults: SubAgentResult[]; | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * 子Agent类 | ||
| * 使用 executeToolLoop 执行任务(工具可为空) | ||
| */ | ||
| export class SubAgent { | ||
| private name: string; | ||
| private llmService: ILLMService; | ||
| private systemPrompt: string; | ||
| private maxLoops: number; | ||
| constructor( | ||
| name: string, | ||
| llmService: ILLMService, | ||
| systemPrompt: string, | ||
| maxLoops: number = 5 | ||
| ) { | ||
| this.name = name; | ||
| this.llmService = llmService; | ||
| this.systemPrompt = systemPrompt; | ||
| this.maxLoops = maxLoops; | ||
| } | ||
| /** | ||
| * 执行子Agent任务 | ||
| * @param instruction - 主Agent下发的指令 | ||
| */ | ||
| async run(instruction: string): Promise<SubAgentResult> { | ||
| console.log(`\n🤖 子Agent [${this.name}] 开始执行...`); | ||
| console.log(`📋 指令: ${instruction}`); | ||
| // 发射子Agent调用事件 | ||
| eventBus.emit('agent:call', { agentName: this.name }); | ||
| try { | ||
| // 初始化上下文管理器 | ||
| const contextManager = new ContextManager(); | ||
| await contextManager.init(); | ||
| // 设置系统提示词 | ||
| contextManager.add( | ||
| this.systemPrompt, | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| // 设置用户输入(来自主Agent的指令) | ||
| contextManager.setUserInput(instruction); | ||
| // 初始化工具管理器(工具为空,但仍使用 executeToolLoop) | ||
| const toolManager = new ToolManager(); | ||
| // 清空默认工具,使子Agent无工具可用 | ||
| toolManager.clear(); | ||
| // 执行工具循环 | ||
| const loopResult = await executeToolLoop( | ||
| this.llmService, | ||
| contextManager, | ||
| toolManager, | ||
| { | ||
| maxLoops: this.maxLoops, | ||
| agentName: this.name, | ||
| } | ||
| ); | ||
| console.log(`✅ 子Agent [${this.name}] 执行完成`); | ||
| return { | ||
| agentName: this.name, | ||
| result: loopResult.result || '', | ||
| success: loopResult.success, | ||
| error: loopResult.error, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| console.error(`❌ 子Agent [${this.name}] 执行失败: ${errorMessage}`); | ||
| return { | ||
| agentName: this.name, | ||
| result: '', | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| getName(): string { | ||
| return this.name; | ||
| } | ||
| } | ||
| /** | ||
| * 主Agent类 | ||
| * 协调者角色,负责任务分配和结果汇总 | ||
| * 不直接使用工具,而是通过调用子Agent完成任务 | ||
| */ | ||
| export class MainAgent { | ||
| private name: string; | ||
| private llmService: ILLMService; | ||
| private systemPrompt: string; | ||
| private subAgentA: SubAgent; | ||
| private subAgentB: SubAgent; | ||
| private contextManager: ContextManager; | ||
| constructor(llmService: ILLMService, name: string = 'main_agent') { | ||
| this.name = name; | ||
| this.llmService = llmService; | ||
| this.systemPrompt = MAIN_AGENT_PROMPT; | ||
| this.contextManager = new ContextManager(); | ||
| // 初始化两个子Agent | ||
| this.subAgentA = new SubAgent( | ||
| 'researcher', | ||
| llmService, | ||
| SUB_AGENT_A_PROMPT | ||
| ); | ||
| this.subAgentB = new SubAgent( | ||
| 'executor', | ||
| llmService, | ||
| SUB_AGENT_B_PROMPT | ||
| ); | ||
| } | ||
| /** | ||
| * 执行主Agent | ||
| * @param userInput - 用户输入 | ||
| */ | ||
| async run(userInput: string): Promise<MainAgentResult> { | ||
| // 初始化上下文管理器 | ||
| this.contextManager.init(); | ||
| // 重置事件收集器 | ||
| eventBus.reset(); | ||
| // 发射主Agent调用事件 | ||
| eventBus.emit('agent:call', { agentName: this.name }); | ||
| console.log(`\n🎯 主Agent [${this.name}] 开始处理任务...`); | ||
| console.log(`📝 用户输入: ${userInput}`); | ||
| const subAgentResults: SubAgentResult[] = []; | ||
| try { | ||
| // 1. 调用子Agent A(研究者) | ||
| const instructionA = `请针对以下用户需求进行研究分析:\n${userInput}`; | ||
| const resultA = await this.subAgentA.run(instructionA); | ||
| subAgentResults.push(resultA); | ||
| let executionHistoryA = ` | ||
| 子Agent执行完成-${this.subAgentA.getName()}: | ||
| 主Agent指令: ${instructionA} | ||
| 输出: ${resultA.result} | ||
| `; | ||
| this.contextManager.add(executionHistoryA, ContextType.EXECUTION_HISTORY); | ||
| // 2. 调用子Agent B(执行者) | ||
| const instructionB = `基于以下用户需求,请提供具体的执行方案:\n${userInput}`; | ||
| const resultB = await this.subAgentB.run(instructionB); | ||
| subAgentResults.push(resultB); | ||
| // 记录子Agent B的执行结果 | ||
| let executionHistoryB = ` | ||
| 子Agent执行完成-${this.subAgentB.getName()}: | ||
| 主Agent指令: ${instructionB} | ||
| 输出: ${resultB.result} | ||
| `; | ||
| this.contextManager.add(executionHistoryB, ContextType.EXECUTION_HISTORY); | ||
| // 3. 主Agent汇总结果 | ||
| const finalResponse = await this.summarizeResults(userInput); | ||
| console.log(`\n✅ 主Agent [${this.name}] 任务完成`); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse, | ||
| subAgentResults, | ||
| success: true, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| console.error(`❌ 主Agent [${this.name}] 执行失败: ${errorMessage}`); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse: '', | ||
| subAgentResults, | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * 汇总子Agent结果 | ||
| * 主Agent不使用工具,直接调用LLM进行汇总 | ||
| */ | ||
| private async summarizeResults(userInput: string): Promise<string> { | ||
| console.log(`\n📊 主Agent 正在汇总子Agent结果...`); | ||
| // 构建汇总上下文 | ||
| const contextManager = new ContextManager(); | ||
| await contextManager.init(); | ||
| // 设置系统提示词 | ||
| contextManager.add( | ||
| this.systemPrompt, | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| // 设置汇总指令 | ||
| const summarizeInstruction = `基于上述子Agent的研究分析和执行方案,请为用户提供一个综合性的最终答复。 | ||
| 用户原始需求:${userInput} | ||
| 请整合各子Agent的输出,给出完整、清晰的最终响应。`; | ||
| contextManager.setUserInput(summarizeInstruction); | ||
| // 获取上下文并调用LLM(不使用工具) | ||
| const messages = contextManager.getContext(); | ||
| const response = await this.llmService.complete(messages, []); | ||
| return response.content || ''; | ||
| } | ||
| /** | ||
| * 获取执行历史 | ||
| */ | ||
| getExecutionHistory(): Message[] { | ||
| return this.contextManager.get(ContextType.EXECUTION_HISTORY); | ||
| } | ||
| getName(): string { | ||
| return this.name; | ||
| } | ||
| } | ||
| /** | ||
| * 创建多智能体系统的便捷函数 | ||
| */ | ||
| export function createMultiAgentSystem(llmService: ILLMService): MainAgent { | ||
| return new MainAgent(llmService); | ||
| } |
| /** | ||
| * 简单 Agent 实现 | ||
| * 使用 ContextManager + ToolManager + LLMService 实现基本的工具调用循环 | ||
| */ | ||
| import { ContextManager, ContextType } from '../context/index.js'; | ||
| import { ToolManager } from '../tool/ToolManager.js'; | ||
| import { ILLMService, ToolLoopResult } from '../llm/types/index.js'; | ||
| import { executeToolLoop } from '../llm/utils/executeToolLoop.js'; | ||
| import { eventBus } from '../../evaluation/EventBus.js'; | ||
| import { SIMPLE_AGENT_PROMPT } from '../promptManager/index.js'; | ||
| /** | ||
| * Agent 执行结果 | ||
| */ | ||
| export interface AgentResult { | ||
| /** 收集到的 Agent 名称列表 */ | ||
| agents: string[]; | ||
| /** 每个 Agent 调用的工具记录 */ | ||
| tools: Record<string, string[]>; | ||
| /** 最终响应内容 */ | ||
| finalResponse: string; | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * Agent 配置 | ||
| */ | ||
| export interface AgentConfig { | ||
| /** Agent 名称 */ | ||
| name?: string; | ||
| /** 最大工具调用循环次数 */ | ||
| maxLoops?: number; | ||
| /** 系统提示词 */ | ||
| systemPrompt?: string; | ||
| } | ||
| /** | ||
| * 简单 Agent 类 | ||
| * | ||
| * 使用方式: | ||
| * ```typescript | ||
| * const agent = new SimpleAgent(llmService, { name: 'my_agent' }); | ||
| * const result = await agent.run('列出当前目录的文件'); | ||
| * ``` | ||
| */ | ||
| export class SimpleAgent { | ||
| private llmService: ILLMService; | ||
| private contextManager: ContextManager; | ||
| private toolManager: ToolManager; | ||
| private config: Required<AgentConfig>; | ||
| constructor(llmService: ILLMService, config?: AgentConfig) { | ||
| this.llmService = llmService; | ||
| this.contextManager = new ContextManager(); | ||
| this.toolManager = new ToolManager(); | ||
| this.config = { | ||
| name: config?.name ?? 'simple_agent', | ||
| maxLoops: config?.maxLoops ?? 10, | ||
| systemPrompt: config?.systemPrompt ?? SIMPLE_AGENT_PROMPT, | ||
| }; | ||
| } | ||
| /** | ||
| * 执行 Agent | ||
| * @param userInput - 用户输入 | ||
| * @returns Agent 执行结果 | ||
| */ | ||
| async run(userInput: string): Promise<AgentResult> { | ||
| // 重置事件收集器 | ||
| eventBus.reset(); | ||
| // 发射 Agent 调用事件 | ||
| eventBus.emit('agent:call', { agentName: this.config.name }); | ||
| try { | ||
| // 初始化上下文管理器 | ||
| await this.contextManager.init(); | ||
| // 设置系统提示词 | ||
| this.contextManager.add( | ||
| this.config.systemPrompt, | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| // 设置用户输入 | ||
| this.contextManager.setUserInput(userInput); | ||
| // 执行工具循环 | ||
| const loopResult = await executeToolLoop( | ||
| this.llmService, | ||
| this.contextManager, | ||
| this.toolManager, | ||
| { | ||
| maxLoops: this.config.maxLoops, | ||
| agentName: this.config.name, | ||
| } | ||
| ); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse: loopResult.result || '', | ||
| success: loopResult.success, | ||
| error: loopResult.error, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| console.error(`Agent 执行失败: ${errorMessage}`); | ||
| // 从事件系统获取收集的数据 | ||
| const collected = eventBus.getData(); | ||
| return { | ||
| agents: collected.agents, | ||
| tools: collected.tools, | ||
| finalResponse: '', | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * 获取 Agent 名称 | ||
| */ | ||
| getName(): string { | ||
| return this.config.name; | ||
| } | ||
| /** | ||
| * 获取工具管理器 | ||
| */ | ||
| getToolManager(): ToolManager { | ||
| return this.toolManager; | ||
| } | ||
| /** | ||
| * 获取上下文管理器 | ||
| */ | ||
| getContextManager(): ContextManager { | ||
| return this.contextManager; | ||
| } | ||
| } |
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { ContextManager } from '../ContextManager.js'; | ||
| import { ContextType } from '../types.js'; | ||
| describe('ContextManager 测试', () => { | ||
| let contextManager: ContextManager; | ||
| beforeEach(async () => { | ||
| contextManager = new ContextManager(); | ||
| await contextManager.init(); | ||
| }); | ||
| describe('初始化', () => { | ||
| it('应该正确初始化', async () => { | ||
| const manager = new ContextManager(); | ||
| expect(manager.isInitialized()).toBe(false); | ||
| await manager.init(); | ||
| expect(manager.isInitialized()).toBe(true); | ||
| }); | ||
| it('不应该重复初始化', async () => { | ||
| await contextManager.init(); // 第二次调用 | ||
| expect(contextManager.isInitialized()).toBe(true); | ||
| }); | ||
| it('未初始化时应该抛出错误', () => { | ||
| const manager = new ContextManager(); | ||
| expect(() => manager.add('test', ContextType.CONVERSATION)).toThrow( | ||
| 'ContextManager 未初始化' | ||
| ); | ||
| }); | ||
| }); | ||
| describe('添加上下文', () => { | ||
| it('应该能添加会话上下文', () => { | ||
| const message = { role: 'user', content: '你好' }; | ||
| contextManager.add(message, ContextType.CONVERSATION); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(1); | ||
| }); | ||
| it('应该能添加工具上下文', () => { | ||
| const toolCall = { | ||
| id: '1', | ||
| name: 'test_tool', | ||
| arguments: {}, | ||
| result: 'success', | ||
| }; | ||
| contextManager.add(toolCall, ContextType.TOOL); | ||
| expect(contextManager.getCount(ContextType.TOOL)).toBe(1); | ||
| }); | ||
| it('应该能添加记忆上下文', () => { | ||
| const memory = { key: 'user_name', value: '张三' }; | ||
| contextManager.add(memory, ContextType.MEMORY); | ||
| expect(contextManager.getCount(ContextType.MEMORY)).toBe(1); | ||
| }); | ||
| }); | ||
| describe('获取上下文', () => { | ||
| it('应该能获取指定类型的上下文', () => { | ||
| const message = { role: 'user', content: '测试消息' }; | ||
| contextManager.add(message, ContextType.CONVERSATION); | ||
| const contexts = contextManager.get(ContextType.CONVERSATION); | ||
| expect(contexts.length).toBe(1); | ||
| }); | ||
| it('应该能获取所有上下文', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '你好' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { key: 'preference', value: 'dark_mode' }, | ||
| ContextType.MEMORY | ||
| ); | ||
| const allContexts = contextManager.getAll(); | ||
| expect(allContexts.length).toBeGreaterThan(0); | ||
| }); | ||
| it('空上下文应该返回空数组', () => { | ||
| const contexts = contextManager.get(ContextType.CONVERSATION); | ||
| expect(contexts).toEqual([]); | ||
| }); | ||
| }); | ||
| describe('统计信息', () => { | ||
| it('应该正确统计上下文数量', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息1' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { role: 'user', content: '消息2' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { key: 'test', value: 'value' }, | ||
| ContextType.MEMORY | ||
| ); | ||
| const stats = contextManager.getStats(); | ||
| expect(stats.total).toBe(3); | ||
| expect(stats.byType[ContextType.CONVERSATION]).toBe(2); | ||
| expect(stats.byType[ContextType.MEMORY]).toBe(1); | ||
| }); | ||
| it('应该正确检查上下文是否存在', () => { | ||
| expect(contextManager.hasContext(ContextType.CONVERSATION)).toBe(false); | ||
| contextManager.add( | ||
| { role: 'user', content: '测试' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| expect(contextManager.hasContext(ContextType.CONVERSATION)).toBe(true); | ||
| }); | ||
| it('应该正确检查是否为空', () => { | ||
| expect(contextManager.isEmpty()).toBe(true); | ||
| contextManager.add( | ||
| { role: 'user', content: '测试' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| expect(contextManager.isEmpty()).toBe(false); | ||
| }); | ||
| }); | ||
| describe('更新和删除', () => { | ||
| it('应该能更新指定上下文项', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '原始消息' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.update(ContextType.CONVERSATION, 0, { | ||
| role: 'user', | ||
| content: '更新后的消息', | ||
| }); | ||
| const contexts = contextManager.get(ContextType.CONVERSATION); | ||
| expect(contexts[0].content).toContain('更新后的消息'); | ||
| }); | ||
| it('应该能删除最后一项', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息1' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { role: 'user', content: '消息2' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(2); | ||
| contextManager.removeLast(ContextType.CONVERSATION); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(1); | ||
| }); | ||
| it('应该能清空指定类型的上下文', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息1' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { role: 'user', content: '消息2' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.clear(ContextType.CONVERSATION); | ||
| expect(contextManager.getCount(ContextType.CONVERSATION)).toBe(0); | ||
| }); | ||
| it('应该能重置所有上下文', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '消息' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| contextManager.add( | ||
| { key: 'test', value: 'value' }, | ||
| ContextType.MEMORY | ||
| ); | ||
| contextManager.reset(); | ||
| expect(contextManager.isEmpty()).toBe(true); | ||
| }); | ||
| }); | ||
| describe('模块访问', () => { | ||
| it('应该能获取指定类型的模块实例', () => { | ||
| const module = contextManager.getModule(ContextType.CONVERSATION); | ||
| expect(module).toBeDefined(); | ||
| expect(module.type).toBe(ContextType.CONVERSATION); | ||
| }); | ||
| it('应该能获取所有模块', () => { | ||
| const modules = contextManager.getAllModules(); | ||
| expect(modules.size).toBe(5); // 5 种上下文类型 | ||
| }); | ||
| }); | ||
| describe('验证', () => { | ||
| it('应该通过验证', () => { | ||
| expect(contextManager.validate()).toBe(true); | ||
| }); | ||
| it('未初始化的管理器不应通过验证', () => { | ||
| const manager = new ContextManager(); | ||
| expect(manager.validate()).toBe(false); | ||
| }); | ||
| }); | ||
| describe('预留接口', () => { | ||
| it('压缩检查应该返回 false(未实现)', () => { | ||
| expect(contextManager.needsCompression()).toBe(false); | ||
| }); | ||
| it('token 计数应该返回 0(未实现)', async () => { | ||
| const count = await contextManager.getTokenCount(); | ||
| expect(count).toBe(0); | ||
| }); | ||
| it('应该能导出为 JSON', () => { | ||
| contextManager.add( | ||
| { role: 'user', content: '测试' }, | ||
| ContextType.CONVERSATION | ||
| ); | ||
| const json = contextManager.toJSON(); | ||
| expect(json).toBeDefined(); | ||
| expect(typeof json).toBe('string'); | ||
| }); | ||
| }); | ||
| }); | ||
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { ConversationContext } from '../../modules/ConversationContext.js'; | ||
| import { ContextType } from '../../types.js'; | ||
| describe('ConversationContext 测试', () => { | ||
| let context: ConversationContext; | ||
| beforeEach(() => { | ||
| context = new ConversationContext(); | ||
| }); | ||
| it('应该正确初始化', () => { | ||
| expect(context.type).toBe(ContextType.CONVERSATION); | ||
| expect(context.isEmpty()).toBe(true); | ||
| expect(context.getCount()).toBe(0); | ||
| }); | ||
| describe('添加消息', () => { | ||
| it('应该能添加用户消息', () => { | ||
| context.addUserMessage('你好'); | ||
| expect(context.getCount()).toBe(1); | ||
| const messages = context.format(); | ||
| expect(messages[0].role).toBe('user'); | ||
| expect(messages[0].content).toBe('你好'); | ||
| }); | ||
| it('应该能添加助手消息', () => { | ||
| context.addAssistantMessage('你好!有什么可以帮助你的吗?'); | ||
| expect(context.getCount()).toBe(1); | ||
| const messages = context.format(); | ||
| expect(messages[0].role).toBe('assistant'); | ||
| }); | ||
| it('应该能添加系统消息', () => { | ||
| context.addSystemMessage('你是一个有帮助的助手'); | ||
| expect(context.getCount()).toBe(1); | ||
| const messages = context.format(); | ||
| expect(messages[0].role).toBe('system'); | ||
| }); | ||
| it('应该能添加带图片的消息', () => { | ||
| context.addUserMessage('这是什么?', { | ||
| url: 'https://example.com/image.jpg', | ||
| }); | ||
| const messages = context.format(); | ||
| expect(messages[0].content).toBeDefined(); | ||
| expect(Array.isArray(messages[0].content)).toBe(true); | ||
| }); | ||
| it('应该能添加带工具调用的助手消息', () => { | ||
| const toolCalls = [ | ||
| { | ||
| id: 'call_1', | ||
| type: 'function', | ||
| function: { name: 'get_weather', arguments: '{"city": "北京"}' }, | ||
| }, | ||
| ]; | ||
| context.addAssistantMessage('让我查一下天气', toolCalls); | ||
| const messages = context.format(); | ||
| expect(messages[0].tool_calls).toBeDefined(); | ||
| expect(messages[0].tool_calls?.length).toBe(1); | ||
| }); | ||
| }); | ||
| describe('格式化', () => { | ||
| it('应该正确格式化为 LLM 消息格式', () => { | ||
| context.addUserMessage('你好'); | ||
| context.addAssistantMessage('你好!'); | ||
| const formatted = context.format(); | ||
| expect(formatted).toHaveLength(2); | ||
| expect(formatted[0].role).toBe('user'); | ||
| expect(formatted[1].role).toBe('assistant'); | ||
| }); | ||
| it('应该正确格式化带 base64 图片的消息', () => { | ||
| context.addUserMessage('分析这张图片', { | ||
| base64: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', | ||
| mimeType: 'image/png', | ||
| }); | ||
| const formatted = context.format(); | ||
| expect(Array.isArray(formatted[0].content)).toBe(true); | ||
| }); | ||
| }); | ||
| describe('查询方法', () => { | ||
| it('应该能获取最后一条消息', () => { | ||
| context.addUserMessage('第一条消息'); | ||
| context.addUserMessage('第二条消息'); | ||
| const lastMessage = context.getLastMessage(); | ||
| expect(lastMessage?.content).toBe('第二条消息'); | ||
| }); | ||
| it('应该能按角色统计消息数量', () => { | ||
| context.addUserMessage('用户消息1'); | ||
| context.addUserMessage('用户消息2'); | ||
| context.addAssistantMessage('助手消息'); | ||
| expect(context.getCountByRole('user')).toBe(2); | ||
| expect(context.getCountByRole('assistant')).toBe(1); | ||
| }); | ||
| it('应该能获取所有用户消息', () => { | ||
| context.addUserMessage('用户消息1'); | ||
| context.addAssistantMessage('助手消息'); | ||
| context.addUserMessage('用户消息2'); | ||
| const userMessages = context.getUserMessages(); | ||
| expect(userMessages.length).toBe(2); | ||
| expect(userMessages[0].content).toBe('用户消息1'); | ||
| expect(userMessages[1].content).toBe('用户消息2'); | ||
| }); | ||
| it('应该能获取所有助手消息', () => { | ||
| context.addUserMessage('用户消息'); | ||
| context.addAssistantMessage('助手消息1'); | ||
| context.addAssistantMessage('助手消息2'); | ||
| const assistantMessages = context.getAssistantMessages(); | ||
| expect(assistantMessages.length).toBe(2); | ||
| }); | ||
| }); | ||
| describe('基础操作', () => { | ||
| it('应该能清空所有消息', () => { | ||
| context.addUserMessage('测试消息'); | ||
| expect(context.getCount()).toBe(1); | ||
| context.clear(); | ||
| expect(context.getCount()).toBe(0); | ||
| expect(context.isEmpty()).toBe(true); | ||
| }); | ||
| it('应该能删除最后一条消息', () => { | ||
| context.addUserMessage('消息1'); | ||
| context.addUserMessage('消息2'); | ||
| expect(context.getCount()).toBe(2); | ||
| context.removeLast(); | ||
| expect(context.getCount()).toBe(1); | ||
| }); | ||
| it('应该能更新指定消息', () => { | ||
| context.addUserMessage('原始消息'); | ||
| context.update(0, { | ||
| role: 'user', | ||
| content: '更新后的消息', | ||
| }); | ||
| const messages = context.format(); | ||
| expect(messages[0].content).toBe('更新后的消息'); | ||
| }); | ||
| }); | ||
| }); | ||
| import { ContextType, ContextItem, IContext } from '../types.js'; | ||
| import { logger } from '../../../utils/logger.js'; | ||
| /** | ||
| * 基础上下文抽象类 | ||
| * 提供所有上下文模块的通用功能 | ||
| */ | ||
| export abstract class BaseContext<T = any> implements IContext<T> { | ||
| /** 上下文类型 */ | ||
| public readonly type: ContextType; | ||
| /** 存储上下文项的数组 */ | ||
| protected items: ContextItem<T>[] = []; | ||
| constructor(type: ContextType) { | ||
| this.type = type; | ||
| logger.debug(`初始化上下文模块: ${type}`); | ||
| } | ||
| /** | ||
| * 添加上下文项 | ||
| */ | ||
| add(content: T, metadata?: Record<string, any>): void { | ||
| const item: ContextItem<T> = { | ||
| content, | ||
| type: this.type, | ||
| metadata, | ||
| timestamp: Date.now(), | ||
| id: this.generateId(), | ||
| }; | ||
| this.items.push(item); | ||
| logger.debug(`添加上下文项到 ${this.type}: ${this.items.length} 项`); | ||
| } | ||
| /** | ||
| * 获取所有上下文项 | ||
| */ | ||
| getAll(): ContextItem<T>[] { | ||
| return [...this.items]; | ||
| } | ||
| /** | ||
| * 获取指定索引的上下文项 | ||
| */ | ||
| get(index: number): ContextItem<T> | undefined { | ||
| if (index < 0 || index >= this.items.length) { | ||
| logger.warn(`索引 ${index} 超出范围 (0-${this.items.length - 1})`); | ||
| return undefined; | ||
| } | ||
| return this.items[index]; | ||
| } | ||
| /** | ||
| * 清空所有上下文 | ||
| */ | ||
| clear(): void { | ||
| const count = this.items.length; | ||
| this.items = []; | ||
| logger.debug(`清空 ${this.type} 上下文: 移除了 ${count} 项`); | ||
| } | ||
| /** | ||
| * 获取上下文数量 | ||
| */ | ||
| getCount(): number { | ||
| return this.items.length; | ||
| } | ||
| /** | ||
| * 检查是否为空 | ||
| */ | ||
| isEmpty(): boolean { | ||
| return this.items.length === 0; | ||
| } | ||
| /** | ||
| * 移除最后一项 | ||
| */ | ||
| removeLast(): void { | ||
| if (this.items.length > 0) { | ||
| this.items.pop(); | ||
| logger.debug(`从 ${this.type} 移除最后一项`); | ||
| } else { | ||
| logger.warn(`尝试从空的 ${this.type} 上下文移除项`); | ||
| } | ||
| } | ||
| /** | ||
| * 更新指定索引的上下文项 | ||
| */ | ||
| update(index: number, content: T, metadata?: Record<string, any>): void { | ||
| if (index < 0 || index >= this.items.length) { | ||
| logger.error(`无法更新: 索引 ${index} 超出范围`); | ||
| throw new Error(`索引 ${index} 超出范围 (0-${this.items.length - 1})`); | ||
| } | ||
| const oldItem = this.items[index]; | ||
| this.items[index] = { | ||
| ...oldItem, | ||
| content, | ||
| metadata: metadata || oldItem.metadata, | ||
| timestamp: Date.now(), | ||
| }; | ||
| logger.debug(`更新 ${this.type} 上下文项 [${index}]`); | ||
| } | ||
| /** | ||
| * 格式化为特定格式(由子类实现) | ||
| */ | ||
| abstract format(): any[]; | ||
| /** | ||
| * TODO: 后续实现序列化 | ||
| * 转换为 JSON | ||
| */ | ||
| toJSON(): string { | ||
| // TODO: 实现序列化逻辑 | ||
| return JSON.stringify({ | ||
| type: this.type, | ||
| items: this.items, | ||
| }); | ||
| } | ||
| /** | ||
| * TODO: 后续实现序列化 | ||
| * 从 JSON 恢复 | ||
| */ | ||
| fromJSON(json: string): void { | ||
| // TODO: 实现反序列化逻辑 | ||
| try { | ||
| const data = JSON.parse(json); | ||
| if (data.type === this.type && Array.isArray(data.items)) { | ||
| this.items = data.items; | ||
| logger.debug(`从 JSON 恢复 ${this.type} 上下文: ${this.items.length} 项`); | ||
| } else { | ||
| throw new Error('无效的 JSON 数据格式'); | ||
| } | ||
| } catch (error: any) { | ||
| logger.error(`从 JSON 恢复失败: ${error.message}`); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * 生成唯一 ID | ||
| */ | ||
| protected generateId(): string { | ||
| return `${this.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | ||
| } | ||
| } | ||
| import { ContextType, ContextStats, IContext, Message } from "./types.js"; | ||
| import { ConversationContext } from "./modules/ConversationContext.js"; | ||
| import { ToolMessageSequenceContext } from "./modules/ToolMessageSequenceContext.js"; | ||
| import { MemoryContext } from "./modules/MemoryContext.js"; | ||
| import { SystemPromptContext } from "./modules/SystemPromptContext.js"; | ||
| import { StructuredOutputContext } from "./modules/StructuredOutputContext.js"; | ||
| import { RelevantContext } from "./modules/RelevantContext.js"; | ||
| import { ExecutionHistoryContext } from "./modules/ExecutionHistoryContext.js"; | ||
| /** | ||
| * 上下文管理器 | ||
| * 负责统一管理所有类型的上下文模块 | ||
| */ | ||
| export class ContextManager { | ||
| /** 存储所有上下文模块的映射 */ | ||
| private contexts: Map<ContextType, IContext> = new Map(); | ||
| /** 用户输入 */ | ||
| private userInput: string = ""; | ||
| /** 是否已初始化 */ | ||
| private initialized: boolean = false; | ||
| constructor() {} | ||
| /** | ||
| * 初始化所有上下文模块 | ||
| */ | ||
| async init(): Promise<void> { | ||
| if (this.initialized) { | ||
| return; | ||
| } | ||
| // 初始化所有上下文模块 | ||
| this.contexts.set( | ||
| ContextType.CONVERSATION_HISTORY, | ||
| new ConversationContext() | ||
| ); | ||
| this.contexts.set( | ||
| ContextType.TOOL_MESSAGE_SEQUENCE, | ||
| new ToolMessageSequenceContext() | ||
| ); | ||
| this.contexts.set(ContextType.MEMORY, new MemoryContext()); | ||
| this.contexts.set(ContextType.SYSTEM_PROMPT, new SystemPromptContext()); | ||
| this.contexts.set( | ||
| ContextType.STRUCTURED_OUTPUT, | ||
| new StructuredOutputContext() | ||
| ); | ||
| this.contexts.set(ContextType.RELEVANT_CONTEXT, new RelevantContext()); | ||
| this.contexts.set(ContextType.EXECUTION_HISTORY, new ExecutionHistoryContext()); | ||
| this.initialized = true; | ||
| } | ||
| /** | ||
| * 检查是否已初始化 | ||
| */ | ||
| isInitialized(): boolean { | ||
| return this.initialized; | ||
| } | ||
| /** 设置用户输入 */ | ||
| setUserInput(userInput: string): void { | ||
| this.userInput = userInput; | ||
| } | ||
| /** | ||
| * 统一的添加上下文方法 | ||
| * @param content - 上下文内容 | ||
| * @param type - 上下文类型 | ||
| * @param metadata - 可选的元数据 | ||
| * | ||
| * @example | ||
| * // SYSTEM_PROMPT - 字符串 | ||
| * contextManager.add('你是一个助手', ContextType.SYSTEM_PROMPT); | ||
| * | ||
| * // MEMORY - { key, value } 格式 | ||
| * contextManager.add({ key: 'user_name', value: '张三' }, ContextType.MEMORY); | ||
| * | ||
| * // CONVERSATION_HISTORY - { role, content } 格式 | ||
| * contextManager.add({ role: 'user', content: '你好' }, ContextType.CONVERSATION_HISTORY); | ||
| * | ||
| * // TOOL_MESSAGE_SEQUENCE - Message 对象 | ||
| * contextManager.add({ role: 'assistant', content: '...', tool_calls: [...] }, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| * | ||
| * // STRUCTURED_OUTPUT - 字符串 | ||
| * contextManager.add('json', ContextType.STRUCTURED_OUTPUT); | ||
| * | ||
| * // RELEVANT_CONTEXT - { key, value } 格式 | ||
| * contextManager.add({ key: 'scene', value: '客服场景' }, ContextType.RELEVANT_CONTEXT); | ||
| */ | ||
| add(content: any, type: ContextType, metadata?: Record<string, any>): void { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| this.validateContent(content, type); | ||
| context.add(content, metadata); | ||
| } | ||
| /** | ||
| * 校验 content 格式是否匹配 type | ||
| */ | ||
| private validateContent(content: any, type: ContextType): void { | ||
| switch (type) { | ||
| case ContextType.SYSTEM_PROMPT: | ||
| case ContextType.STRUCTURED_OUTPUT: | ||
| case ContextType.EXECUTION_HISTORY: | ||
| if (typeof content !== "string") { | ||
| throw new Error(`${type} 需要字符串类型`); | ||
| } | ||
| break; | ||
| case ContextType.TOOL_MESSAGE_SEQUENCE: | ||
| case ContextType.CONVERSATION_HISTORY: | ||
| if (!content?.role || content?.content === undefined) { | ||
| throw new Error(`${type} 需要 { role, content } 格式的 Message 对象`); | ||
| } | ||
| break; | ||
| case ContextType.MEMORY: | ||
| case ContextType.RELEVANT_CONTEXT: | ||
| if (!content?.key || content?.value === undefined) { | ||
| throw new Error(`${type} 需要 { key, value } 格式`); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * 获取指定类型的上下文 | ||
| * @param type - 上下文类型 | ||
| * @returns 格式化的上下文数据 | ||
| */ | ||
| get(type: ContextType): any[] { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| return context.format(); | ||
| } | ||
| /** | ||
| * 获取完整上下文(供 LLM 调用使用) | ||
| * | ||
| * 自动检测并组装所有可用的上下文类型: | ||
| * - system消息(systemPrompt + structuredOutput + relevantContext + memory + 会话历史摘要) | ||
| * - 用户输入 | ||
| * - 工具消息序列 | ||
| * | ||
| * @returns Message[] 格式的上下文数组 | ||
| */ | ||
| getContext(): Message[] { | ||
| this.ensureInitialized(); | ||
| const messages: Message[] = []; | ||
| // 1. system消息(包含历史摘要) | ||
| const systemMessage = this.buildSystemMessage(); | ||
| if (systemMessage) { | ||
| messages.push(systemMessage); | ||
| } | ||
| // 2. 用户输入(当前请求) | ||
| if (this.userInput) { | ||
| messages.push({ role: "user", content: this.userInput }); | ||
| } | ||
| // 3. 工具消息序列 | ||
| const toolContext = this.contexts.get(ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| if (toolContext && !toolContext.isEmpty()) { | ||
| const toolMessages = toolContext.format(); | ||
| if (toolMessages && toolMessages.length > 0) { | ||
| messages.push(...toolMessages); | ||
| } | ||
| } | ||
| // 4. 执行历史 | ||
| const executionHistory = this.getModule<ExecutionHistoryContext>( | ||
| ContextType.EXECUTION_HISTORY | ||
| ); | ||
| const executionHistoryMessages = executionHistory.format(); | ||
| if (executionHistoryMessages.length > 0) { | ||
| messages.push(...executionHistoryMessages); | ||
| } | ||
| return messages; | ||
| } | ||
| /** | ||
| * 构建system消息(私有方法) | ||
| * 自动拼接: systemPrompt + structuredOutput + relevantContext + memory + 会话历史摘要 | ||
| * | ||
| * @returns Message 对象或 null | ||
| */ | ||
| private buildSystemMessage(): Message | null { | ||
| const parts: string[] = []; | ||
| // 1. 系统提示词(必需) | ||
| const systemPromptContext = this.getModule<SystemPromptContext>( | ||
| ContextType.SYSTEM_PROMPT | ||
| ); | ||
| const systemPrompts = systemPromptContext.formatNormal(); | ||
| if (systemPrompts) { | ||
| parts.push(systemPrompts); | ||
| } | ||
| // 2. 结构化输出要求 | ||
| const structuredOutputContext = this.getModule<StructuredOutputContext>( | ||
| ContextType.STRUCTURED_OUTPUT | ||
| ); | ||
| const structuredOutput = structuredOutputContext.format(); | ||
| if (structuredOutput.length > 0) { | ||
| parts.push("\n【结构化输出要求】"); | ||
| parts.push(structuredOutput[0]); | ||
| } | ||
| // 3. 相关上下文 | ||
| const relevantContext = this.getModule<RelevantContext>( | ||
| ContextType.RELEVANT_CONTEXT | ||
| ); | ||
| const relevantInfo = relevantContext.format(); | ||
| if (relevantInfo.length > 0) { | ||
| parts.push("\n【相关上下文】"); | ||
| parts.push(relevantInfo.join("\n")); | ||
| } | ||
| // 4. 用户记忆 | ||
| const memoryContext = this.getModule<MemoryContext>(ContextType.MEMORY); | ||
| const memories = memoryContext.format(); | ||
| if (memories.length > 0) { | ||
| parts.push("\n【用户记忆】"); | ||
| parts.push(memories.join("\n")); | ||
| } | ||
| // 5. 会话历史摘要 | ||
| const historySummary = this.formatConversationHistoryForPrompt(); | ||
| if (historySummary) { | ||
| parts.push(historySummary); | ||
| } | ||
| // 如果没有任何内容,返回null | ||
| if (parts.length === 0) { | ||
| return null; | ||
| } | ||
| return { | ||
| role: "system", | ||
| content: parts.join("\n"), | ||
| }; | ||
| } | ||
| /** | ||
| * 将会话历史格式化为简洁列表(用于系统提示词) | ||
| * 只取最近15条,以简洁形式呈现 | ||
| * | ||
| * @returns 格式化的历史摘要字符串,或 null | ||
| */ | ||
| private formatConversationHistoryForPrompt(): string | null { | ||
| const conversationContext = this.contexts.get( | ||
| ContextType.CONVERSATION_HISTORY | ||
| ); | ||
| if (!conversationContext || conversationContext.isEmpty()) { | ||
| return null; | ||
| } | ||
| const history = conversationContext.format(); | ||
| if (!history || history.length === 0) { | ||
| return null; | ||
| } | ||
| // 取最近15条 | ||
| const recentHistory = history.slice(-15); | ||
| const lines: string[] = []; | ||
| lines.push("\n## 【历史对话参考】"); | ||
| lines.push(""); | ||
| lines.push("最近对话记录:"); | ||
| recentHistory.forEach((msg, idx) => { | ||
| const role = msg.role === "user" ? "[用户]" : "[助手]"; | ||
| let summary = ""; | ||
| try { | ||
| const content = JSON.parse(msg.content as string); | ||
| if (msg.role === "user") { | ||
| summary = content.text || msg.content; | ||
| } else { | ||
| summary = | ||
| content.action === "finish" | ||
| ? `已完成: ${(content.result || "").slice(0, 50)}...` | ||
| : content.action || (msg.content as string).slice(0, 50); | ||
| } | ||
| } catch { | ||
| summary = | ||
| typeof msg.content === "string" | ||
| ? msg.content.slice(0, 50) | ||
| : String(msg.content); | ||
| } | ||
| lines.push(`${idx + 1}. ${role} ${summary}`); | ||
| }); | ||
| return lines.join("\n"); | ||
| } | ||
| /** | ||
| * 添加相关上下文 | ||
| */ | ||
| addRelevantContext(key: string, value: any, description?: string): void { | ||
| this.ensureInitialized(); | ||
| this.add({ key, value, description }, ContextType.RELEVANT_CONTEXT); | ||
| } | ||
| /** | ||
| * 获取相关上下文值 | ||
| */ | ||
| getRelevantContextValue(key: string): any { | ||
| this.ensureInitialized(); | ||
| const relevantContext = this.getModule<RelevantContext>( | ||
| ContextType.RELEVANT_CONTEXT | ||
| ); | ||
| return relevantContext.getValue(key); | ||
| } | ||
| /** | ||
| * 更新相关上下文 | ||
| */ | ||
| updateRelevantContext(key: string, value: any): void { | ||
| this.ensureInitialized(); | ||
| const relevantContext = this.getModule<RelevantContext>( | ||
| ContextType.RELEVANT_CONTEXT | ||
| ); | ||
| relevantContext.updateValue(key, value); | ||
| } | ||
| /** | ||
| * 获取指定类型上下文的数量 | ||
| */ | ||
| getCount(type: ContextType): number { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| return context.getCount(); | ||
| } | ||
| /** | ||
| * 获取所有上下文的统计信息 | ||
| */ | ||
| getStats(): ContextStats { | ||
| this.ensureInitialized(); | ||
| let total = 0; | ||
| const byType: Record<string, number> = {}; | ||
| this.contexts.forEach((context, type) => { | ||
| const count = context.getCount(); | ||
| total += count; | ||
| byType[type] = count; | ||
| }); | ||
| return { | ||
| total, | ||
| byType, | ||
| tokenCount: undefined, | ||
| }; | ||
| } | ||
| /** | ||
| * 检查指定类型上下文是否存在 | ||
| */ | ||
| hasContext(type: ContextType): boolean { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| return context ? !context.isEmpty() : false; | ||
| } | ||
| /** | ||
| * 检查上下文是否为空 | ||
| */ | ||
| isEmpty(): boolean { | ||
| this.ensureInitialized(); | ||
| return Array.from(this.contexts.values()).every((context) => | ||
| context.isEmpty() | ||
| ); | ||
| } | ||
| /** | ||
| * 清空指定类型的上下文 | ||
| */ | ||
| clear(type: ContextType): void { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| context.clear(); | ||
| } | ||
| /** | ||
| * 重置所有上下文(清空状态) | ||
| */ | ||
| reset(): void { | ||
| this.ensureInitialized(); | ||
| this.contexts.forEach((context) => { | ||
| context.clear(); | ||
| }); | ||
| this.userInput = ""; | ||
| } | ||
| /** | ||
| * 获取指定类型的上下文模块实例 | ||
| */ | ||
| getModule<T extends IContext>(type: ContextType): T { | ||
| this.ensureInitialized(); | ||
| const context = this.contexts.get(type); | ||
| if (!context) { | ||
| throw new Error(`未知的上下文类型: ${type}`); | ||
| } | ||
| return context as T; | ||
| } | ||
| /** | ||
| * 打印当前上下文状态(调试用) | ||
| */ | ||
| debug(): void { | ||
| this.ensureInitialized(); | ||
| console.log("=== ContextManager 状态 ==="); | ||
| console.log(`已初始化: ${this.initialized}`); | ||
| console.log(`总计: ${this.getStats().total} 项`); | ||
| console.log("\n各类型统计:"); | ||
| this.contexts.forEach((context, type) => { | ||
| console.log(` ${type}: ${context.getCount()} 项`); | ||
| }); | ||
| console.log("========================\n"); | ||
| } | ||
| /** | ||
| * 确保已初始化 | ||
| */ | ||
| private ensureInitialized(): void { | ||
| if (!this.initialized) { | ||
| throw new Error("ContextManager 未初始化,请先调用 init() 方法"); | ||
| } | ||
| } | ||
| } |
| /** | ||
| * 上下文管理模块统一导出 | ||
| */ | ||
| // 类型定义 | ||
| export * from './types.js'; | ||
| // 基础类 | ||
| export { BaseContext } from './base/BaseContext.js'; | ||
| // 核心管理器 | ||
| export { ContextManager } from './ContextManager.js'; | ||
| // 具体上下文模块 | ||
| export { ConversationContext } from './modules/ConversationContext.js'; | ||
| export { ToolMessageSequenceContext } from './modules/ToolMessageSequenceContext.js'; | ||
| export { MemoryContext } from './modules/MemoryContext.js'; | ||
| export { SystemPromptContext } from './modules/SystemPromptContext.js'; | ||
| export { StructuredOutputContext } from './modules/StructuredOutputContext.js'; | ||
| export { RelevantContext } from './modules/RelevantContext.js'; |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, ConversationMessage } from '../types.js'; | ||
| /** | ||
| * 会话上下文管理类 | ||
| * 负责管理用户和助手之间的对话历史 | ||
| */ | ||
| export class ConversationContext extends BaseContext<ConversationMessage> { | ||
| constructor() { | ||
| super(ContextType.CONVERSATION_HISTORY); | ||
| } | ||
| /** | ||
| * 格式化为 LLM 消息格式 | ||
| * 转换为标准的 OpenAI 消息格式 | ||
| */ | ||
| format(): any[] { | ||
| return this.items.map((item) => { | ||
| const message = item.content; | ||
| const formatted: any = { | ||
| role: message.role, | ||
| content: message.content, | ||
| }; | ||
| // 添加图片数据(如果存在) | ||
| if (message.imageData) { | ||
| if (message.imageData.url) { | ||
| formatted.content = [ | ||
| { type: 'text', text: message.content }, | ||
| { | ||
| type: 'image_url', | ||
| image_url: { url: message.imageData.url }, | ||
| }, | ||
| ]; | ||
| } else if (message.imageData.base64) { | ||
| formatted.content = [ | ||
| { type: 'text', text: message.content }, | ||
| { | ||
| type: 'image_url', | ||
| image_url: { | ||
| url: `data:${message.imageData.mimeType || 'image/png'};base64,${message.imageData.base64}`, | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
| } | ||
| // 添加工具调用(如果存在) | ||
| if (message.toolCalls && message.toolCalls.length > 0) { | ||
| formatted.tool_calls = message.toolCalls; | ||
| } | ||
| // 添加工具调用 ID(如果是工具消息) | ||
| if (message.role === 'tool' && item.metadata?.toolCallId) { | ||
| formatted.tool_call_id = item.metadata.toolCallId; | ||
| } | ||
| return formatted; | ||
| }); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, Message } from '../types.js'; | ||
| /** | ||
| * 执行历史上下文管理类 | ||
| * 专门用于主Agent记录子Agent的执行结果 | ||
| * 存储字符串,格式化时转换为 user 消息 | ||
| */ | ||
| export class ExecutionHistoryContext extends BaseContext<string> { | ||
| constructor() { | ||
| super(ContextType.EXECUTION_HISTORY); | ||
| } | ||
| /** | ||
| * 格式化为 Message 数组 | ||
| * 将存储的字符串转换为 user 角色的消息 | ||
| */ | ||
| format(): Message[] { | ||
| return this.items.map(item => ({ | ||
| role: 'user' as const, | ||
| content: item.content, | ||
| })); | ||
| } | ||
| /** | ||
| * 获取最后一次执行记录(字符串形式) | ||
| */ | ||
| getLastExecutionRecord(): string | undefined { | ||
| if (this.items.length === 0) return undefined; | ||
| return this.items[this.items.length - 1].content; | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, MemoryItem } from '../types.js'; | ||
| /** | ||
| * 用户记忆上下文管理类 | ||
| * 负责管理用户偏好、历史信息等长期记忆 | ||
| */ | ||
| export class MemoryContext extends BaseContext<MemoryItem> { | ||
| /** 用于快速查找的键值映射 */ | ||
| private keyMap: Map<string, number> = new Map(); | ||
| constructor() { | ||
| super(ContextType.MEMORY); | ||
| } | ||
| /** | ||
| * 重写 add 方法以维护 keyMap | ||
| */ | ||
| add(content: MemoryItem, metadata?: Record<string, any>): void { | ||
| // 检查是否已存在 | ||
| const existingIndex = this.keyMap.get(content.key); | ||
| if (existingIndex !== undefined) { | ||
| // 更新已存在的记忆 | ||
| this.update(existingIndex, content); | ||
| } else { | ||
| // 添加新记忆 | ||
| super.add(content, metadata); | ||
| this.keyMap.set(content.key, this.items.length - 1); | ||
| } | ||
| } | ||
| /** | ||
| * 获取指定键的记忆值 | ||
| */ | ||
| getMemory(key: string): any | undefined { | ||
| const index = this.keyMap.get(key); | ||
| if (index !== undefined) { | ||
| return this.items[index]?.content.value; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * 检查是否存在指定键的记忆 | ||
| */ | ||
| hasMemory(key: string): boolean { | ||
| return this.keyMap.has(key); | ||
| } | ||
| /** | ||
| * 格式化为自然语言描述 | ||
| */ | ||
| format(): string[] { | ||
| // 按优先级排序(优先级越小越靠前) | ||
| const sortedItems = [...this.items].sort((a, b) => { | ||
| const priorityA = a.content.priority ?? 999; | ||
| const priorityB = b.content.priority ?? 999; | ||
| return priorityA - priorityB; | ||
| }); | ||
| return sortedItems.map((item) => { | ||
| const memory = item.content; | ||
| let text = `${memory.key}: ${JSON.stringify(memory.value)}`; | ||
| if (memory.description) { | ||
| text += ` (${memory.description})`; | ||
| } | ||
| return text; | ||
| }); | ||
| } | ||
| /** | ||
| * 清空所有记忆 | ||
| */ | ||
| clear(): void { | ||
| super.clear(); | ||
| this.keyMap.clear(); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType } from '../types.js'; | ||
| /** | ||
| * 相关上下文项 | ||
| */ | ||
| export interface RelevantContextItem { | ||
| /** 上下文键(如 "known_roles", "scene_info") */ | ||
| key: string; | ||
| /** 上下文值(灵活类型) */ | ||
| value: any; | ||
| /** 描述 */ | ||
| description?: string; | ||
| } | ||
| /** | ||
| * 相关上下文管理类 | ||
| * | ||
| * 用于存储问题相关的背景知识,这些知识: | ||
| * - 不属于用户记忆(非用户个人信息) | ||
| * - 不属于会话历史(非对话记录) | ||
| * - 不属于工具输出(非工具执行结果) | ||
| * - 是为了解决当前问题而需要的临时背景信息 | ||
| */ | ||
| export class RelevantContext extends BaseContext<RelevantContextItem> { | ||
| constructor() { | ||
| super(ContextType.RELEVANT_CONTEXT); | ||
| } | ||
| /** | ||
| * 获取指定键的值 | ||
| */ | ||
| getValue(key: string): any | undefined { | ||
| const item = this.items.find((item) => item.content.key === key); | ||
| return item?.content.value; | ||
| } | ||
| /** | ||
| * 更新指定键的值 | ||
| */ | ||
| updateValue(key: string, value: any): void { | ||
| const index = this.items.findIndex((item) => item.content.key === key); | ||
| if (index !== -1) { | ||
| const existingItem = this.items[index].content; | ||
| this.update(index, { ...existingItem, value }); | ||
| } | ||
| } | ||
| /** | ||
| * 检查是否存在指定键 | ||
| */ | ||
| hasKey(key: string): boolean { | ||
| return this.items.some((item) => item.content.key === key); | ||
| } | ||
| /** | ||
| * 格式化为文本数组(用于拼接到 prompt) | ||
| */ | ||
| format(): string[] { | ||
| return this.items.map((item) => { | ||
| const { key, value, description } = item.content; | ||
| const desc = description ? ` (${description})` : ''; | ||
| // 根据值类型格式化 | ||
| if (Array.isArray(value)) { | ||
| return `${key}${desc}: ${value.join('、')}`; | ||
| } else if (typeof value === 'object' && value !== null) { | ||
| return `${key}${desc}: ${JSON.stringify(value)}`; | ||
| } else { | ||
| return `${key}${desc}: ${value}`; | ||
| } | ||
| }); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, StructuredOutputSchema } from '../types.js'; | ||
| /** | ||
| * 结构化输出上下文管理类 | ||
| * 负责管理结构化输出的 Schema 定义 | ||
| */ | ||
| export class StructuredOutputContext extends BaseContext<StructuredOutputSchema> { | ||
| constructor() { | ||
| super(ContextType.STRUCTURED_OUTPUT); | ||
| } | ||
| /** | ||
| * 格式化为 LLM 理解的格式 | ||
| * 转换为 OpenAI 的 response_format 格式 | ||
| */ | ||
| format(): any[] { | ||
| if (this.isEmpty()) { | ||
| return []; | ||
| } | ||
| // item.content 就是字符串(如 'json') | ||
| const latestItem = this.items[this.items.length - 1]; | ||
| const type = latestItem.content; | ||
| return [ | ||
| { | ||
| type: type, | ||
| }, | ||
| ]; | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, SystemPromptItem } from '../types.js'; | ||
| /** | ||
| * 系统提示词上下文管理类 | ||
| * 负责管理系统级提示词,支持多段提示词和优先级 | ||
| */ | ||
| export class SystemPromptContext extends BaseContext<SystemPromptItem> { | ||
| constructor() { | ||
| super(ContextType.SYSTEM_PROMPT); | ||
| } | ||
| /** | ||
| * 格式化为单一系统消息 | ||
| * 合并所有启用的提示词,按优先级排序 | ||
| */ | ||
| format(): any[] { | ||
| if (this.items.length === 0) { | ||
| return []; | ||
| } | ||
| // item.content 就是字符串 | ||
| const combinedContent = this.items.map((item) => item.content).join('\n\n'); | ||
| return [ | ||
| { | ||
| role: 'system', | ||
| content: combinedContent, | ||
| }, | ||
| ]; | ||
| } | ||
| /** | ||
| * 格式化为普通字符串(用于合并到系统消息中) | ||
| * @returns 合并后的提示词文本,或 null | ||
| */ | ||
| formatNormal(): string | null { | ||
| if (this.items.length === 0) { | ||
| return null; | ||
| } | ||
| return this.items.map((item) => item.content).join('\n\n'); | ||
| } | ||
| } |
| import { BaseContext } from '../base/BaseContext.js'; | ||
| import { ContextType, Message } from '../types.js'; | ||
| /** | ||
| * 工具消息序列上下文管理类 | ||
| * 用于存储工具调用循环中的消息序列 | ||
| * 顺序: assistant (含tool_calls) → tool → assistant → tool → ... | ||
| */ | ||
| export class ToolMessageSequenceContext extends BaseContext<Message> { | ||
| constructor() { | ||
| super(ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| } | ||
| /** | ||
| * 格式化为 Message 数组 (API 格式) | ||
| */ | ||
| format(): Message[] { | ||
| return this.items.map((item) => item.content); | ||
| } | ||
| } |
| /** | ||
| * 上下文管理系统的核心类型定义 | ||
| */ | ||
| /** | ||
| * 上下文类型枚举 | ||
| */ | ||
| export enum ContextType { | ||
| /** 会话历史上下文 */ | ||
| CONVERSATION_HISTORY = 'conversation_history', | ||
| /** 工具消息序列上下文 */ | ||
| TOOL_MESSAGE_SEQUENCE = 'tool_message_sequence', | ||
| /** 用户记忆上下文 */ | ||
| MEMORY = 'memory', | ||
| /** 系统提示词上下文 */ | ||
| SYSTEM_PROMPT = 'system_prompt', | ||
| /** 结构化输出上下文 */ | ||
| STRUCTURED_OUTPUT = 'structured_output', | ||
| /** 相关上下文 */ | ||
| RELEVANT_CONTEXT = 'relevant_context', | ||
| /** 执行历史上下文 */ | ||
| EXECUTION_HISTORY = 'execution_history', | ||
| } | ||
| /** | ||
| * 单个上下文项的结构 | ||
| */ | ||
| export interface ContextItem<T = any> { | ||
| /** 上下文内容 */ | ||
| content: T; | ||
| /** 上下文类型 */ | ||
| type: ContextType; | ||
| /** 元数据 */ | ||
| metadata?: Record<string, any>; | ||
| /** 时间戳 */ | ||
| timestamp: number; | ||
| /** 唯一标识(可选) */ | ||
| id?: string; | ||
| } | ||
| /** | ||
| * 统计信息结构 | ||
| */ | ||
| export interface ContextStats { | ||
| /** 总计数量 */ | ||
| total: number; | ||
| /** 按类型分组的数量 */ | ||
| byType: Record<string, number>; | ||
| /** 总 token 数(预留) */ | ||
| tokenCount?: number; | ||
| } | ||
| /** | ||
| * 图片数据 | ||
| */ | ||
| export interface ImageData { | ||
| url?: string; | ||
| base64?: string; | ||
| mimeType?: string; | ||
| } | ||
| /** | ||
| * 消息角色 | ||
| */ | ||
| export type MessageRole = 'user' | 'assistant' | 'system' | 'tool'; | ||
| /** | ||
| * 标准消息结构(用于 LLM API 调用) | ||
| */ | ||
| export interface Message { | ||
| role: MessageRole; | ||
| content: string; | ||
| tool_calls?: any[]; | ||
| tool_call_id?: string; | ||
| name?: string; | ||
| } | ||
| /** | ||
| * 会话消息结构 | ||
| */ | ||
| export interface ConversationMessage { | ||
| role: MessageRole; | ||
| content: string; | ||
| imageData?: ImageData; | ||
| toolCalls?: any[]; | ||
| } | ||
| /** | ||
| * 用户记忆项结构 | ||
| */ | ||
| export interface MemoryItem { | ||
| /** 记忆键 */ | ||
| key: string; | ||
| /** 记忆值 */ | ||
| value: any; | ||
| /** 描述 */ | ||
| description?: string; | ||
| /** 优先级 */ | ||
| priority?: number; | ||
| } | ||
| /** | ||
| * 系统提示词项结构 | ||
| */ | ||
| export interface SystemPromptItem { | ||
| /** 提示词内容 */ | ||
| content: string; | ||
| /** 优先级(数字越小优先级越高) */ | ||
| priority?: number; | ||
| /** 是否启用 */ | ||
| enabled?: boolean; | ||
| } | ||
| /** | ||
| * 结构化输出 Schema | ||
| */ | ||
| export interface StructuredOutputSchema { | ||
| /** Schema 类型(如 json_schema) */ | ||
| type: string; | ||
| /** Schema 定义 */ | ||
| schema: Record<string, any>; | ||
| /** 输出格式(json、yaml 等) */ | ||
| format?: string; | ||
| /** 是否严格模式 */ | ||
| strict?: boolean; | ||
| } | ||
| /** | ||
| * 基础上下文接口 | ||
| * 所有具体上下文模块必须实现的方法 | ||
| */ | ||
| export interface IContext<T = any> { | ||
| /** 上下文类型 */ | ||
| readonly type: ContextType; | ||
| /** | ||
| * 添加上下文项 | ||
| * @param content - 内容 | ||
| * @param metadata - 元数据 | ||
| */ | ||
| add(content: T, metadata?: Record<string, any>): void; | ||
| /** | ||
| * 获取所有上下文项 | ||
| */ | ||
| getAll(): ContextItem<T>[]; | ||
| /** | ||
| * 获取指定索引的上下文项 | ||
| * @param index - 索引 | ||
| */ | ||
| get(index: number): ContextItem<T> | undefined; | ||
| /** | ||
| * 清空所有上下文 | ||
| */ | ||
| clear(): void; | ||
| /** | ||
| * 获取上下文数量 | ||
| */ | ||
| getCount(): number; | ||
| /** | ||
| * 检查是否为空 | ||
| */ | ||
| isEmpty(): boolean; | ||
| /** | ||
| * 格式化为特定格式(由子类实现) | ||
| */ | ||
| format(): any[]; | ||
| /** | ||
| * 移除最后一项 | ||
| */ | ||
| removeLast(): void; | ||
| /** | ||
| * 更新指定索引的上下文项 | ||
| * @param index - 索引 | ||
| * @param content - 新内容 | ||
| * @param metadata - 新元数据 | ||
| */ | ||
| update(index: number, content: T, metadata?: Record<string, any>): void; | ||
| /** | ||
| * 转换为 JSON | ||
| */ | ||
| toJSON(): string; | ||
| /** | ||
| * 从 JSON 恢复 | ||
| */ | ||
| fromJSON(json: string): void; | ||
| } |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||
| import { | ||
| extractApiKey, | ||
| getBaseURL, | ||
| getDefaultContextWindow, | ||
| } from '../utils/helpers.js'; | ||
| import { LLMConfig } from '../types/index.js'; | ||
| describe('辅助函数测试', () => { | ||
| describe('extractApiKey', () => { | ||
| it('应该优先使用用户传递的 API Key', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openai', | ||
| model: 'gpt-4', | ||
| apiKey: 'user-provided-key', | ||
| }; | ||
| expect(extractApiKey(config)).toBe('user-provided-key'); | ||
| }); | ||
| it('应该从环境变量获取 API Key(当用户未传递时)', () => { | ||
| // 注意:这个测试依赖于实际的环境变量加载 | ||
| // 如果 .env 中有 DEEPSEEK_API_KEY,则会返回该值 | ||
| const config: LLMConfig = { | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| }; | ||
| // 由于依赖环境变量,我们只测试不抛出错误即可 | ||
| // 或者可以 mock getLLMConfig 函数 | ||
| const result = extractApiKey(config); | ||
| expect(typeof result).toBe('string'); | ||
| }); | ||
| it('无需 API Key 的提供商应该返回 not-required', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'ollama', | ||
| model: 'llama2', | ||
| }; | ||
| expect(extractApiKey(config)).toBe('not-required'); | ||
| }); | ||
| it('缺少 API Key 时应该抛出清晰的错误信息', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'unknown-provider', | ||
| model: 'some-model', | ||
| }; | ||
| expect(() => extractApiKey(config)).toThrow( | ||
| /API key for provider "unknown-provider" not found/ | ||
| ); | ||
| }); | ||
| }); | ||
| describe('getBaseURL', () => { | ||
| it('应该优先使用用户传递的 baseURL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openai', | ||
| model: 'gpt-4', | ||
| baseURL: 'https://custom.api.com', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('https://custom.api.com'); | ||
| }); | ||
| it('应该从环境变量获取 baseURL(当用户未传递时)', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| }; | ||
| // 会返回环境变量或默认值 | ||
| const result = getBaseURL(config); | ||
| expect(result).toBeTruthy(); | ||
| expect(typeof result).toBe('string'); | ||
| }); | ||
| it('应该返回默认 Base URL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openai', | ||
| model: 'gpt-4', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('https://api.openai.com/v1'); | ||
| }); | ||
| it('应该返回 OpenRouter 的默认 URL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'openrouter', | ||
| model: 'gpt-4', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('https://openrouter.ai/api/v1'); | ||
| }); | ||
| it('应该返回 Ollama 的默认 URL', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'ollama', | ||
| model: 'llama2', | ||
| }; | ||
| expect(getBaseURL(config)).toBe('http://localhost:11434/v1'); | ||
| }); | ||
| it('未知提供商应该抛出错误', () => { | ||
| const config: LLMConfig = { | ||
| provider: 'unknown-provider', | ||
| model: 'some-model', | ||
| }; | ||
| expect(() => getBaseURL(config)).toThrow( | ||
| /No base URL found for provider "unknown-provider"/ | ||
| ); | ||
| }); | ||
| }); | ||
| describe('getDefaultContextWindow', () => { | ||
| it('应该返回 GPT-4o 的上下文窗口', () => { | ||
| expect(getDefaultContextWindow('openai', 'gpt-4o')).toBe(128000); | ||
| }); | ||
| it('应该返回默认上下文窗口', () => { | ||
| expect(getDefaultContextWindow('openai', 'unknown-model')).toBe(8192); | ||
| }); | ||
| it('应该返回提供商的默认值', () => { | ||
| expect(getDefaultContextWindow('anthropic')).toBe(200000); | ||
| }); | ||
| it('未知提供商应该返回默认值 8192', () => { | ||
| expect(getDefaultContextWindow('unknown-provider')).toBe(8192); | ||
| }); | ||
| }); | ||
| }); |
| import { ILLMService, LLMConfig, UnifiedToolManager } from './types/index.js'; | ||
| import { DeepSeekService } from './services/DeepSeekService.js'; | ||
| import { extractApiKey, getBaseURL } from './utils/helpers.js'; | ||
| /** | ||
| * 公共工厂函数:创建 LLM 服务实例(支持可选的工具管理器) | ||
| * | ||
| * @param config - LLM 配置(包括 provider、model、apiKey 等) | ||
| * @param toolManager - 可选的工具管理器实例 | ||
| * @param eventManager - 可选的事件管理器实例 | ||
| * @returns Promise<ILLMService> 实例 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { ToolManager } from '../tool/ToolManager.js'; | ||
| * | ||
| * const toolManager = new ToolManager(); | ||
| * const service = await createLLMService( | ||
| * { | ||
| * provider: 'deepseek', | ||
| * model: 'deepseek-chat', | ||
| * apiKey: 'your-api-key', | ||
| * maxIterations: 10 | ||
| * }, | ||
| * toolManager | ||
| * ); | ||
| * | ||
| * // 使用服务 | ||
| * const response = await service.complete(messages, tools); | ||
| * ``` | ||
| */ | ||
| export async function createLLMService( | ||
| config: LLMConfig, | ||
| toolManager?: UnifiedToolManager, | ||
| eventManager?: any | ||
| ): Promise<ILLMService> { | ||
| // 1. 创建服务实例 | ||
| const service = await _createLLMService(config, toolManager); | ||
| // 2. 设置事件管理器(如果提供且服务支持) | ||
| if (eventManager && typeof (service as any).setEventManager === 'function') { | ||
| (service as any).setEventManager(eventManager); | ||
| } | ||
| return service; | ||
| } | ||
| /** | ||
| * 内部函数:创建 LLM 服务实例 | ||
| */ | ||
| async function _createLLMService( | ||
| config: LLMConfig, | ||
| toolManager?: UnifiedToolManager | ||
| ): Promise<ILLMService> { | ||
| // 1. 提取和验证 API Key | ||
| const apiKey = extractApiKey(config); | ||
| // 2. 获取 Base URL | ||
| const baseURL = getBaseURL(config); | ||
| // 3. 根据 provider 创建服务 | ||
| switch (config.provider.toLowerCase()) { | ||
| case 'deepseek': { | ||
| // 使用 ES 动态导入 OpenAI SDK | ||
| const { default: OpenAI } = await import('openai'); | ||
| const openai = new OpenAI({ apiKey, baseURL }); | ||
| return new DeepSeekService( | ||
| openai, | ||
| config.model || 'deepseek-chat', | ||
| { | ||
| baseURL, | ||
| maxRetries: 3, | ||
| toolManager, | ||
| maxIterations: config.maxIterations || 5, | ||
| } | ||
| ); | ||
| } | ||
| // 🟡 可扩展:其他提供商 | ||
| // case 'openai': | ||
| // case 'anthropic': | ||
| // case 'qwen': | ||
| // case 'siliconflow': | ||
| // case 'openrouter': | ||
| default: | ||
| throw new Error(`Unsupported LLM provider: ${config.provider}`); | ||
| } | ||
| } |
| // 导出核心类型 | ||
| export * from './types/index.js'; | ||
| // 导出服务实现 | ||
| export { DeepSeekService } from './services/DeepSeekService.js'; | ||
| // 导出工厂方法 | ||
| export { createLLMService } from './factory.js'; | ||
| // 导出辅助函数 | ||
| export { | ||
| extractApiKey, | ||
| getBaseURL, | ||
| getDefaultContextWindow, | ||
| sleep, | ||
| } from './utils/helpers.js'; |
| import OpenAI from 'openai'; | ||
| import { | ||
| ILLMService, | ||
| LLMResponse, | ||
| LLMChatOptions, | ||
| ImageData, | ||
| ToolSet, | ||
| UnifiedToolManager, | ||
| } from '../types/index.js'; | ||
| import { logger } from '../../../utils/logger.js'; | ||
| import { sleep } from '../utils/helpers.js'; | ||
| /** | ||
| * DeepSeek LLM 服务 | ||
| * 使用 OpenAI SDK(DeepSeek 兼容 OpenAI API) | ||
| * | ||
| * 提供两种使用方式: | ||
| * 1. complete() - 上下文补全(推荐) | ||
| * 2. generate() - 内置工具调用循环(可选) | ||
| */ | ||
| export class DeepSeekService implements ILLMService { | ||
| private client: OpenAI; | ||
| private model: string; | ||
| private maxRetries: number; | ||
| private toolManager?: UnifiedToolManager; | ||
| private maxIterations: number; | ||
| constructor( | ||
| openai: OpenAI, | ||
| model: string, | ||
| options?: { | ||
| baseURL?: string; | ||
| maxRetries?: number; | ||
| toolManager?: UnifiedToolManager; | ||
| maxIterations?: number; | ||
| } | ||
| ) { | ||
| this.client = openai; | ||
| this.model = model; | ||
| this.maxRetries = options?.maxRetries || 3; | ||
| this.toolManager = options?.toolManager; | ||
| this.maxIterations = options?.maxIterations || 5; | ||
| logger.debug(`初始化 DeepSeekService: model=${model}`); | ||
| } | ||
| /** | ||
| * 核心方法:上下文补全 | ||
| * 接收格式化的上下文(消息历史),返回模型响应 | ||
| * @param messages - 上下文消息列表 | ||
| * @param tools - 可用的工具定义列表 | ||
| * @param options - 生成参数(temperature, maxTokens 等) | ||
| * @returns 模型响应(包含内容、工具调用、使用统计) | ||
| */ | ||
| async complete( | ||
| messages: any[], | ||
| tools?: any[], | ||
| options?: LLMChatOptions | ||
| ): Promise<LLMResponse> { | ||
| let attempt = 0; | ||
| while (attempt < this.maxRetries) { | ||
| attempt++; | ||
| try { | ||
| logger.debug( | ||
| `调用 DeepSeek API (尝试 ${attempt}/${this.maxRetries}): ${messages.length} 条消息, ${tools?.length || 0} 个工具` | ||
| ); | ||
| const response = await this.client.chat.completions.create({ | ||
| model: this.model, | ||
| messages, | ||
| tools: tools && tools.length > 0 ? tools : undefined, | ||
| tool_choice: options?.toolChoice, | ||
| temperature: options?.temperature, | ||
| max_tokens: options?.maxTokens, | ||
| top_p: options?.topP, | ||
| frequency_penalty: options?.frequencyPenalty, | ||
| presence_penalty: options?.presencePenalty, | ||
| stop: options?.stop, | ||
| response_format: options?.responseFormat, | ||
| }); | ||
| const message = response.choices[0]?.message; | ||
| if (!message) { | ||
| throw new Error('DeepSeek API 返回空响应'); | ||
| } | ||
| const result: LLMResponse = { | ||
| content: message.content || '', | ||
| toolCalls: message.tool_calls as any, | ||
| finishReason: response.choices[0]?.finish_reason, | ||
| usage: response.usage | ||
| ? { | ||
| promptTokens: response.usage.prompt_tokens, | ||
| completionTokens: response.usage.completion_tokens, | ||
| totalTokens: response.usage.total_tokens, | ||
| } | ||
| : undefined, | ||
| }; | ||
| logger.debug( | ||
| `DeepSeek 响应: ${result.content.slice(0, 100)}${result.content.length > 100 ? '...' : ''}, 工具调用: ${result.toolCalls?.length || 0}` | ||
| ); | ||
| return result; | ||
| } catch (error: any) { | ||
| logger.error( | ||
| `DeepSeek API 调用失败 (${attempt}/${this.maxRetries}): ${error.message}` | ||
| ); | ||
| if (attempt >= this.maxRetries) { | ||
| throw new Error(`DeepSeek API 调用失败: ${error.message}`); | ||
| } | ||
| // 指数退避 | ||
| const delay = 500 * attempt; | ||
| logger.debug(`等待 ${delay}ms 后重试...`); | ||
| await sleep(delay); | ||
| } | ||
| } | ||
| throw new Error('Unreachable'); | ||
| } | ||
| /** | ||
| * 简单对话:无工具,单次调用 | ||
| * @param userInput - 用户输入 | ||
| * @param systemPrompt - 可选的系统提示词 | ||
| */ | ||
| async simpleChat(userInput: string, systemPrompt?: string): Promise<string> { | ||
| const messages: any[] = []; | ||
| if (systemPrompt) { | ||
| messages.push({ role: 'system', content: systemPrompt }); | ||
| } | ||
| messages.push({ role: 'user', content: userInput }); | ||
| const response = await this.complete(messages); | ||
| return response.content; | ||
| } | ||
| /** | ||
| * 获取配置信息 | ||
| */ | ||
| getConfig(): { provider: string; model: string } { | ||
| return { | ||
| provider: 'deepseek', | ||
| model: this.model, | ||
| }; | ||
| } | ||
| /** | ||
| * 完整方法:支持工具调用循环 | ||
| * 需要在构造时传入 toolManager | ||
| * @param userInput - 用户输入 | ||
| * @param imageData - 可选的图片数据 | ||
| * @param _stream - 是否流式输出(暂未实现) | ||
| */ | ||
| async generate( | ||
| userInput: string, | ||
| imageData?: ImageData, | ||
| _stream?: boolean | ||
| ): Promise<string> { | ||
| if (!this.toolManager) { | ||
| throw new Error( | ||
| 'generate() 方法需要 toolManager,请在构造时传入或使用 chat() 方法' | ||
| ); | ||
| } | ||
| // 初始化消息列表 | ||
| const messages: any[] = [ | ||
| { | ||
| role: 'system', | ||
| content: '你是一个有帮助的 AI 助手,可以使用工具来完成任务。', | ||
| }, | ||
| ]; | ||
| // 添加用户消息 | ||
| const userMessage: any = { role: 'user', content: userInput }; | ||
| if (imageData) { | ||
| userMessage.content = [ | ||
| { type: 'text', text: userInput }, | ||
| imageData.url | ||
| ? { type: 'image_url', image_url: { url: imageData.url } } | ||
| : { | ||
| type: 'image_url', | ||
| image_url: { | ||
| url: `data:${imageData.mimeType || 'image/png'};base64,${imageData.base64}`, | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
| messages.push(userMessage); | ||
| // 获取可用工具 | ||
| const availableTools = await this.toolManager.getToolsForProvider( | ||
| 'deepseek' | ||
| ); | ||
| // 工具调用循环 | ||
| let iteration = 0; | ||
| while (iteration < this.maxIterations) { | ||
| iteration++; | ||
| logger.debug(`工具调用迭代: ${iteration}/${this.maxIterations}`); | ||
| // 调用 LLM | ||
| const response = await this.complete(messages, availableTools, { | ||
| toolChoice: iteration === 1 ? 'auto' : undefined, | ||
| }); | ||
| // 无工具调用,返回结果 | ||
| if (!response.toolCalls || response.toolCalls.length === 0) { | ||
| logger.debug('无工具调用,返回最终结果'); | ||
| return response.content; | ||
| } | ||
| // 记录思考内容 | ||
| if (response.content) { | ||
| logger.info(`💭 助手思考: ${response.content}`); | ||
| } | ||
| // 添加助手消息(包含工具调用) | ||
| messages.push({ | ||
| role: 'assistant', | ||
| content: response.content || null, | ||
| tool_calls: response.toolCalls, | ||
| }); | ||
| // 执行所有工具调用 | ||
| for (const toolCall of response.toolCalls) { | ||
| if (toolCall.type !== 'function' || !toolCall.function) { | ||
| continue; | ||
| } | ||
| logger.info(`🔧 使用工具: ${toolCall.function.name}`); | ||
| try { | ||
| const args = JSON.parse(toolCall.function.arguments); | ||
| const result = await this.toolManager.executeTool( | ||
| toolCall.function.name, | ||
| args | ||
| ); | ||
| logger.info(`✅ 工具执行成功: ${JSON.stringify(result).slice(0, 200)}`); | ||
| // 添加工具执行结果 | ||
| messages.push({ | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| content: JSON.stringify(result), | ||
| }); | ||
| } catch (error: any) { | ||
| logger.error(`❌ 工具执行失败: ${error.message}`); | ||
| // 添加错误结果 | ||
| messages.push({ | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| content: JSON.stringify({ error: error.message }), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| throw new Error(`达到最大迭代次数 (${this.maxIterations})`); | ||
| } | ||
| /** | ||
| * 获取所有可用工具 | ||
| */ | ||
| async getAllTools(): Promise<ToolSet> { | ||
| if (!this.toolManager) { | ||
| return {}; | ||
| } | ||
| return this.toolManager.getAllTools(); | ||
| } | ||
| /** | ||
| * 流式对话(预留接口) | ||
| * TODO: 实现流式响应 | ||
| */ | ||
| async chatStream( | ||
| messages: any[], | ||
| tools?: any[], | ||
| options?: LLMChatOptions | ||
| ): Promise<AsyncIterable<LLMResponse>> { | ||
| throw new Error('流式响应暂未实现'); | ||
| } | ||
| } | ||
| /** 图片数据 */ | ||
| export interface ImageData { | ||
| url?: string; | ||
| base64?: string; | ||
| mimeType?: string; | ||
| } | ||
| /** 工具参数定义 */ | ||
| export interface ToolParameters { | ||
| type: 'object'; | ||
| properties: Record<string, any>; | ||
| required?: string[]; | ||
| } | ||
| /** 工具定义 */ | ||
| export interface Tool { | ||
| name: string; | ||
| description: string; | ||
| parameters: ToolParameters; | ||
| } | ||
| /** 工具集合 */ | ||
| export type ToolSet = Record<string, Tool>; | ||
| /** 工具调用 */ | ||
| export interface ToolCall { | ||
| id: string; | ||
| type: 'function'; | ||
| function: { | ||
| name: string; | ||
| arguments: string; | ||
| }; | ||
| } | ||
| /** LLM 配置 */ | ||
| export interface LLMConfig { | ||
| provider: string; | ||
| model: string; | ||
| apiKey?: string; | ||
| maxIterations?: number; | ||
| baseURL?: string; | ||
| qwenOptions?: Record<string, any>; | ||
| aws?: Record<string, any>; | ||
| azure?: Record<string, any>; | ||
| } | ||
| /** MCP Manager 接口 (占位符) */ | ||
| export interface MCPManager { | ||
| getAllTools(): Promise<ToolSet>; | ||
| executeTool(name: string, args: any): Promise<any>; | ||
| } | ||
| /** Context Manager - 导入真实实现 */ | ||
| export { ContextManager } from '../../context/index.js'; | ||
| /** Unified Tool Manager 接口 (占位符) */ | ||
| export interface UnifiedToolManager { | ||
| getToolsForProvider(provider: string): Promise<any[]>; | ||
| getAllTools(): Promise<ToolSet>; | ||
| executeTool(name: string, args: any): Promise<any>; | ||
| } | ||
| /** Event Manager 接口 (占位符) */ | ||
| export interface EventManager { | ||
| emit(event: string, data: any): void; | ||
| } | ||
| /** | ||
| * LLM 响应结构 | ||
| */ | ||
| export interface LLMResponse { | ||
| /** 响应内容 */ | ||
| content: string; | ||
| /** 工具调用列表 */ | ||
| toolCalls?: ToolCall[]; | ||
| /** 结束原因 */ | ||
| finishReason?: string; | ||
| /** Token 使用统计 */ | ||
| usage?: { | ||
| promptTokens: number; | ||
| completionTokens: number; | ||
| totalTokens: number; | ||
| }; | ||
| } | ||
| /** | ||
| * LLM 调用选项 | ||
| */ | ||
| export interface LLMChatOptions { | ||
| /** 温度参数 (0-2) */ | ||
| temperature?: number; | ||
| /** 最大 token 数 */ | ||
| maxTokens?: number; | ||
| /** 工具选择策略 */ | ||
| toolChoice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } }; | ||
| /** Top P 采样 */ | ||
| topP?: number; | ||
| /** 频率惩罚 */ | ||
| frequencyPenalty?: number; | ||
| /** 存在惩罚 */ | ||
| presencePenalty?: number; | ||
| /** 停止词 */ | ||
| stop?: string | string[]; | ||
| /** 结构化输出格式 */ | ||
| responseFormat?: any; | ||
| } | ||
| /** | ||
| * 工具循环执行结果 | ||
| */ | ||
| export interface ToolLoopResult { | ||
| /** 是否成功 */ | ||
| success: boolean; | ||
| /** 最终结果 */ | ||
| result?: string; | ||
| /** 错误信息 */ | ||
| error?: string; | ||
| /** 循环次数 */ | ||
| loopCount: number; | ||
| } | ||
| /** | ||
| * 工具循环配置 | ||
| */ | ||
| export interface ToolLoopConfig { | ||
| /** 最大循环次数 */ | ||
| maxLoops?: number; | ||
| /** Agent 名称 */ | ||
| agentName?: string; | ||
| } | ||
| /** LLM Service 核心接口 */ | ||
| export interface ILLMService { | ||
| /** | ||
| * 核心方法:上下文补全 | ||
| * 接收格式化的上下文(消息历史),返回模型响应 | ||
| * @param messages - 上下文消息列表 | ||
| * @param tools - 可用的工具定义列表 | ||
| * @param options - 生成参数(temperature, maxTokens 等) | ||
| * @returns 模型响应(包含内容、工具调用、使用统计) | ||
| */ | ||
| complete( | ||
| messages: any[], | ||
| tools?: any[], | ||
| options?: LLMChatOptions | ||
| ): Promise<LLMResponse>; | ||
| /** | ||
| * 简单对话:无工具,单次调用 | ||
| * @param userInput - 用户输入 | ||
| * @param systemPrompt - 可选的系统提示词 | ||
| */ | ||
| simpleChat(userInput: string, systemPrompt?: string): Promise<string>; | ||
| /** | ||
| * 完整方法:支持工具调用循环(可选实现) | ||
| * 内置工具调用、上下文管理、迭代执行 | ||
| * @param userInput - 用户输入 | ||
| * @param imageData - 可选的图片数据 | ||
| * @param stream - 是否流式输出 | ||
| */ | ||
| generate?( | ||
| userInput: string, | ||
| imageData?: ImageData, | ||
| stream?: boolean | ||
| ): Promise<string>; | ||
| /** 获取配置 */ | ||
| getConfig(): { provider: string; model: string }; | ||
| /** 可选:事件管理器 */ | ||
| setEventManager?(eventManager: EventManager): void; | ||
| /** @deprecated 由 Agent 管理工具 */ | ||
| getAllTools?(): Promise<ToolSet>; | ||
| } |
| /** | ||
| * 工具循环执行器 | ||
| * 简化版本的工具调用循环逻辑,供 Agent 使用 | ||
| */ | ||
| import { ILLMService, ToolLoopResult, ToolLoopConfig } from '../types/index.js'; | ||
| import { ContextManager } from '../../context/index.js'; | ||
| import { ToolManager } from '../../tool/ToolManager.js'; | ||
| import { ContextType, Message } from '../../context/types.js'; | ||
| import { eventBus } from '../../../evaluation/EventBus.js'; | ||
| import { deepParseArgs, sleep } from './helpers.js'; | ||
| /** | ||
| * 执行工具循环 | ||
| * | ||
| * 循环逻辑: | ||
| * 1. 从 ContextManager 获取当前上下文 | ||
| * 2. 调用 LLM 完成 | ||
| * 3. 如果返回工具调用,执行工具并更新上下文 | ||
| * 4. 重复直到 LLM 返回最终结果或达到最大循环次数 | ||
| * | ||
| * @param llmService - LLM 服务实例 | ||
| * @param contextManager - 上下文管理器 | ||
| * @param toolManager - 工具管理器 | ||
| * @param config - 可选配置 | ||
| * @returns 工具循环执行结果 | ||
| */ | ||
| export async function executeToolLoop( | ||
| llmService: ILLMService, | ||
| contextManager: ContextManager, | ||
| toolManager: ToolManager, | ||
| config?: ToolLoopConfig | ||
| ): Promise<ToolLoopResult> { | ||
| const maxLoops = config?.maxLoops ?? 10; | ||
| const agentName = config?.agentName ?? 'simple_agent'; | ||
| let loopCount = 0; | ||
| console.log(`开始工具循环,最大循环次数: ${maxLoops}`); | ||
| while (loopCount < maxLoops) { | ||
| loopCount++; | ||
| console.log(`🔄 工具循环 ${loopCount}/${maxLoops}`); | ||
| try { | ||
| // 1. 获取当前上下文 | ||
| const messages = contextManager.getContext(); | ||
| // 2. 获取格式化的工具定义 | ||
| const tools = toolManager.getFormattedTools(); | ||
| console.log(`调用 LLM: ${messages.length} 条消息, ${tools.length} 个工具`); | ||
| // 3. 调用 LLM | ||
| const response = await llmService.complete(messages, tools); | ||
| // 4. 判断是否有工具调用 | ||
| if ( | ||
| response.finishReason === 'tool_calls' && | ||
| response.toolCalls && | ||
| response.toolCalls.length > 0 | ||
| ) { | ||
| console.log(`检测到 ${response.toolCalls.length} 个工具调用`); | ||
| // 记录 LLM 的思考内容(如果有) | ||
| if (response.content) { | ||
| console.log(`💭 LLM 思考: ${response.content.slice(0, 100)}...`); | ||
| } | ||
| // 5. 构建 assistant 消息(包含工具调用) | ||
| const assistantMessage: Message = { | ||
| role: 'assistant', | ||
| content: response.content || '', | ||
| tool_calls: response.toolCalls, | ||
| }; | ||
| // 6. 添加到 TOOL_MESSAGE_SEQUENCE | ||
| contextManager.add(assistantMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| // 7. 执行所有工具调用 | ||
| for (const toolCall of response.toolCalls) { | ||
| const toolName = toolCall.function.name; | ||
| try { | ||
| // 解析参数 | ||
| const rawArgs = toolCall.function.arguments | ||
| ? JSON.parse(toolCall.function.arguments) | ||
| : {}; | ||
| const args = deepParseArgs(rawArgs); | ||
| console.log(`🔧 执行工具: ${toolName}`); | ||
| // 触发工具调用事件(用于评估系统) | ||
| eventBus.emit('tool:call', { | ||
| agentName, | ||
| toolName, | ||
| }); | ||
| // 执行工具 | ||
| const result = await toolManager.execute(toolName, args); | ||
| const resultString = JSON.stringify(result); | ||
| console.log(`✅ 工具结果: ${resultString.slice(0, 200)}...`); | ||
| // 8. 构建 tool 消息 | ||
| const toolMessage: Message = { | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| name: toolName, | ||
| content: resultString, | ||
| }; | ||
| // 9. 添加到 TOOL_MESSAGE_SEQUENCE | ||
| contextManager.add(toolMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| // 等待一下避免请求过快 | ||
| await sleep(500); | ||
| } catch (error) { | ||
| console.error(`❌ 工具执行失败: ${toolName}`, error); | ||
| // 将错误信息作为工具结果返回 | ||
| const errorMessage: Message = { | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| name: toolName, | ||
| content: JSON.stringify({ | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }), | ||
| }; | ||
| contextManager.add(errorMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| } | ||
| } | ||
| // 继续循环 | ||
| continue; | ||
| } | ||
| // 10. 没有工具调用,返回最终结果 | ||
| console.log(`✅ 工具循环完成,循环次数: ${loopCount}`); | ||
| console.log(`最终结果: ${response.content?.slice(0, 200)}...`); | ||
| // 添加最终的 assistant 消息 | ||
| const finalMessage: Message = { | ||
| role: 'assistant', | ||
| content: response.content, | ||
| }; | ||
| contextManager.add(finalMessage, ContextType.TOOL_MESSAGE_SEQUENCE); | ||
| return { | ||
| success: true, | ||
| result: response.content, | ||
| loopCount, | ||
| }; | ||
| } catch (error) { | ||
| console.error(`❌ LLM 调用失败 (循环 ${loopCount}):`, error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| loopCount, | ||
| }; | ||
| } | ||
| } | ||
| // 超过最大循环次数 | ||
| console.warn(`⚠️ 超过最大循环次数 (${maxLoops})`); | ||
| return { | ||
| success: false, | ||
| error: `超过最大循环次数 (${maxLoops})`, | ||
| loopCount, | ||
| }; | ||
| } |
| import { LLMConfig } from "../types/index.js"; | ||
| import { | ||
| config as envConfig, | ||
| getLLMKeyByProvider, | ||
| } from "../../../config/env.js"; | ||
| /** | ||
| * 提取 API Key | ||
| * | ||
| * 优先级(从高到低): | ||
| * 1. 用户传递的 config.apiKey(显式配置) | ||
| * 2. 环境变量中的配置(通过 provider 自动查找) | ||
| * 3. 如果都没有且该 provider 需要 API Key,则抛出错误 | ||
| * | ||
| * @param config - LLM 配置 | ||
| * @returns API Key 字符串 | ||
| */ | ||
| export function extractApiKey(config: LLMConfig): string { | ||
| const provider = config.provider.toLowerCase(); | ||
| // 无需 API Key 的提供商 | ||
| if (["ollama", "lmstudio", "aws"].includes(provider)) { | ||
| return "not-required"; | ||
| } | ||
| // 1. 优先使用用户传递的 API Key | ||
| if (config.apiKey) { | ||
| return config.apiKey; | ||
| } | ||
| // 2. 尝试从环境变量配置中获取 | ||
| const providerConfigKey = getLLMKeyByProvider(provider); | ||
| return providerConfigKey; | ||
| } | ||
| /** | ||
| * 获取提供商的 Base URL | ||
| * | ||
| * 优先级(从高到低): | ||
| * 1. 用户传递的 config.baseURL(显式配置) | ||
| * 2. 环境变量中的配置(通过 provider 自动查找) | ||
| * 3. 硬编码的默认 Base URL | ||
| * | ||
| * @param config - LLM 配置 | ||
| * @returns Base URL 字符串 | ||
| * | ||
| */ | ||
| export function getBaseURL(config: LLMConfig): string { | ||
| // 1. 优先使用用户传递的 baseURL | ||
| if (config.baseURL) { | ||
| return config.baseURL; | ||
| } | ||
| const provider = config.provider.toLowerCase(); | ||
| // 3. 硬编码的默认 Base URL(兜底) | ||
| const defaultBaseURLs: Record<string, string> = { | ||
| deepseek: "https://api.deepseek.com", | ||
| openai: "https://api.openai.com/v1", | ||
| anthropic: "https://api.anthropic.com", | ||
| siliconflow: "https://api.siliconflow.cn/v1", | ||
| qwen: "https://dashscope.aliyuncs.com/compatible-mode/v1", | ||
| openrouter: "https://openrouter.ai/api/v1", | ||
| ollama: "http://localhost:11434/v1", | ||
| lmstudio: "http://localhost:1234/v1", | ||
| }; | ||
| const baseURL = defaultBaseURLs[provider]; | ||
| if (!baseURL) { | ||
| throw new Error( | ||
| `No base URL found for provider "${provider}". ` + | ||
| `Please pass baseURL in config: { provider: '${provider}', model: '...', baseURL: 'https://...' }` | ||
| ); | ||
| } | ||
| return baseURL; | ||
| } | ||
| /** 获取默认上下文窗口大小 */ | ||
| export function getDefaultContextWindow( | ||
| provider: string, | ||
| model?: string | ||
| ): number { | ||
| const defaults: Record<string, Record<string, number>> = { | ||
| openai: { | ||
| "gpt-4o": 128000, | ||
| "gpt-4": 8192, | ||
| "gpt-3.5-turbo": 16385, | ||
| default: 8192, | ||
| }, | ||
| anthropic: { default: 200000 }, | ||
| gemini: { default: 1000000 }, | ||
| deepseek: { default: 128000 }, | ||
| ollama: { default: 8192 }, | ||
| }; | ||
| const providerDefaults = defaults[provider.toLowerCase()] || { | ||
| default: 8192, | ||
| }; | ||
| return providerDefaults[model || "default"] || providerDefaults.default; | ||
| } | ||
| /** 睡眠函数 */ | ||
| export function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| /** | ||
| * 深度解析参数:递归检查字符串类型的参数值,尝试 JSON 解析 | ||
| * 用于处理 Claude 等模型返回嵌套 JSON 字符串的情况 | ||
| * | ||
| * 例如:{ bgmRequests: "[{...}]" } → { bgmRequests: [{...}] } | ||
| */ | ||
| export function deepParseArgs(args: any): any { | ||
| if (typeof args === "string") { | ||
| // 尝试解析看起来像 JSON 数组或对象的字符串 | ||
| const trimmed = args.trim(); | ||
| if (trimmed.startsWith("[") || trimmed.startsWith("{")) { | ||
| // 第一次尝试:直接解析 | ||
| try { | ||
| const parsed = JSON.parse(trimmed); | ||
| console.log("[deepParseArgs] ✅ 第一次尝试解析成功"); | ||
| return deepParseArgs(parsed); | ||
| } catch (e) { | ||
| console.log( | ||
| "[deepParseArgs] ❌ 第一次尝试解析失败:", | ||
| (e as Error).message | ||
| ); | ||
| console.log( | ||
| "[deepParseArgs] 字符串前100字符:", | ||
| trimmed.substring(0, 100) | ||
| ); | ||
| // 第二次尝试:处理 Claude 模型返回的双重转义问题 | ||
| // 将 \\n 转换为 \n,\\\" 转换为 \" | ||
| try { | ||
| const unescaped = trimmed | ||
| .replace(/\\\\n/g, "\\n") | ||
| .replace(/\\\\"/g, '\\"'); | ||
| console.log("[deepParseArgs] 尝试处理双重转义后解析..."); | ||
| const parsed = JSON.parse(unescaped); | ||
| console.log("[deepParseArgs] ✅ 第二次尝试(处理转义后)解析成功"); | ||
| return deepParseArgs(parsed); | ||
| } catch (e2) { | ||
| console.log( | ||
| "[deepParseArgs] ❌ 第二次尝试也失败:", | ||
| (e2 as Error).message | ||
| ); | ||
| // 都失败了,返回原始字符串 | ||
| return args; | ||
| } | ||
| } | ||
| } | ||
| return args; | ||
| } | ||
| if (Array.isArray(args)) { | ||
| return args.map(deepParseArgs); | ||
| } | ||
| if (args && typeof args === "object") { | ||
| const result: any = {}; | ||
| for (const [key, value] of Object.entries(args)) { | ||
| result[key] = deepParseArgs(value); | ||
| } | ||
| return result; | ||
| } | ||
| return args; | ||
| } |
| /** | ||
| * 提示词管理模块 | ||
| */ | ||
| export { | ||
| SIMPLE_AGENT_PROMPT, | ||
| MAIN_AGENT_PROMPT, | ||
| SUB_AGENT_A_PROMPT, | ||
| SUB_AGENT_B_PROMPT, | ||
| } from './prompts.js'; |
| /** | ||
| * 提示词常量定义 | ||
| * 集中管理各类 Agent 的系统提示词 | ||
| */ | ||
| /** | ||
| * SimpleAgent 默认提示词 | ||
| */ | ||
| export const SIMPLE_AGENT_PROMPT = `你是一个有用的 AI 助手,可以使用工具来帮助用户完成任务。 | ||
| 你可以使用以下工具: | ||
| - list_files: 列出指定目录下的文件和文件夹 | ||
| - read_file: 读取指定文件的内容 | ||
| 请根据用户的需求,选择合适的工具来完成任务。`; | ||
| /** | ||
| * 主 Agent 提示词(协调者) | ||
| * 用于多智能体系统中的主控 Agent | ||
| */ | ||
| export const MAIN_AGENT_PROMPT = `你是一个任务协调者,负责分析用户需求并协调子Agent完成任务。 | ||
| 你的职责: | ||
| 1. 分析用户的任务需求 | ||
| 2. 将任务分配给合适的子Agent | ||
| 3. 汇总子Agent的执行结果 | ||
| 4. 向用户提供最终的综合答复 | ||
| 你不直接执行具体任务,而是通过协调子Agent来完成工作。`; | ||
| /** | ||
| * 子 Agent A 提示词(研究者) | ||
| */ | ||
| export const SUB_AGENT_A_PROMPT = `你是一个研究分析专家,擅长: | ||
| - 收集和整理信息 | ||
| - 分析问题的各个方面 | ||
| - 提供详细的背景知识 | ||
| 请根据主Agent的指令,提供专业的研究分析结果。`; | ||
| /** | ||
| * 子 Agent B 提示词(执行者) | ||
| */ | ||
| export const SUB_AGENT_B_PROMPT = `你是一个执行专家,擅长: | ||
| - 制定具体的执行方案 | ||
| - 提供实用的建议和步骤 | ||
| - 给出可操作的解决方案 | ||
| 请根据主Agent的指令,提供具体的执行方案。`; |
| import { describe, it, expect, beforeEach } from 'vitest'; | ||
| import { ToolManager } from '../ToolManager.js'; | ||
| describe('ToolManager 测试', () => { | ||
| let toolManager: ToolManager; | ||
| beforeEach(() => { | ||
| toolManager = new ToolManager(); | ||
| }); | ||
| describe('基础功能', () => { | ||
| it('应该成功创建 ToolManager 实例', () => { | ||
| expect(toolManager).toBeDefined(); | ||
| expect(toolManager).toBeInstanceOf(ToolManager); | ||
| }); | ||
| it('应该自动注册所有工具', () => { | ||
| const toolNames = toolManager.getToolNames(); | ||
| expect(toolNames).toContain('read_file'); | ||
| expect(toolNames).toContain('grep_search'); | ||
| }); | ||
| it('应该返回正确的工具数量', () => { | ||
| const tools = toolManager.getTools(); | ||
| expect(tools.length).toBeGreaterThan(0); | ||
| }); | ||
| }); | ||
| describe('工具查询', () => { | ||
| it('应该能够通过名称获取工具', () => { | ||
| const tool = toolManager.getTool('read_file'); | ||
| expect(tool).toBeDefined(); | ||
| expect(tool?.name).toBe('read_file'); | ||
| expect(tool?.category).toBe('filesystem'); | ||
| }); | ||
| it('应该在工具不存在时返回 undefined', () => { | ||
| const tool = toolManager.getTool('non_existent_tool'); | ||
| expect(tool).toBeUndefined(); | ||
| }); | ||
| it('应该正确检查工具是否存在', () => { | ||
| expect(toolManager.hasTool('read_file')).toBe(true); | ||
| expect(toolManager.hasTool('non_existent_tool')).toBe(false); | ||
| }); | ||
| it('应该返回所有工具名称', () => { | ||
| const toolNames = toolManager.getToolNames(); | ||
| expect(toolNames).toBeInstanceOf(Array); | ||
| expect(toolNames.length).toBeGreaterThan(0); | ||
| expect(toolNames).toContain('read_file'); | ||
| }); | ||
| }); | ||
| describe('工具统计', () => { | ||
| it('应该返回正确的统计信息', () => { | ||
| const stats = toolManager.getStats(); | ||
| expect(stats).toHaveProperty('totalTools'); | ||
| expect(stats).toHaveProperty('categories'); | ||
| expect(stats).toHaveProperty('toolNames'); | ||
| expect(stats.totalTools).toBeGreaterThan(0); | ||
| }); | ||
| it('应该按分类统计工具数量', () => { | ||
| const stats = toolManager.getStats(); | ||
| expect(stats.categories).toHaveProperty('filesystem'); | ||
| expect(stats.categories).toHaveProperty('search'); | ||
| }); | ||
| }); | ||
| describe('工具执行', () => { | ||
| it('应该在工具不存在时抛出错误', async () => { | ||
| await expect( | ||
| toolManager.execute('non_existent_tool', {}) | ||
| ).rejects.toThrow(/not found/); | ||
| }); | ||
| it('应该在错误信息中列出可用工具', async () => { | ||
| try { | ||
| await toolManager.execute('non_existent_tool', {}); | ||
| } catch (error: any) { | ||
| expect(error.message).toContain('Available:'); | ||
| expect(error.message).toContain('read_file'); | ||
| } | ||
| }); | ||
| }); | ||
| describe('工具结构验证', () => { | ||
| it('每个工具应该有必需的属性', () => { | ||
| const tools = toolManager.getTools(); | ||
| tools.forEach((tool) => { | ||
| expect(tool).toHaveProperty('name'); | ||
| expect(tool).toHaveProperty('category'); | ||
| expect(tool).toHaveProperty('internal'); | ||
| expect(tool).toHaveProperty('description'); | ||
| expect(tool).toHaveProperty('version'); | ||
| expect(tool).toHaveProperty('parameters'); | ||
| expect(tool).toHaveProperty('handler'); | ||
| expect(typeof tool.handler).toBe('function'); | ||
| }); | ||
| }); | ||
| it('每个工具的参数定义应该是有效的 JSON Schema', () => { | ||
| const tools = toolManager.getTools(); | ||
| tools.forEach((tool) => { | ||
| expect(tool.parameters).toHaveProperty('type'); | ||
| expect(tool.parameters.type).toBe('object'); | ||
| expect(tool.parameters).toHaveProperty('properties'); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { formatToolForLLM, InternalTool } from '../types.js'; | ||
| describe('types 测试', () => { | ||
| describe('formatToolForLLM', () => { | ||
| it('应该正确格式化工具定义', () => { | ||
| const mockTool: InternalTool = { | ||
| name: 'test_tool', | ||
| category: 'test', | ||
| internal: true, | ||
| description: 'Test tool description', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| arg1: { | ||
| type: 'string', | ||
| description: 'Test argument', | ||
| }, | ||
| }, | ||
| required: ['arg1'], | ||
| }, | ||
| handler: async () => ({ success: true }), | ||
| }; | ||
| const formatted = formatToolForLLM(mockTool); | ||
| expect(formatted).toEqual({ | ||
| name: 'test_tool', | ||
| category: 'test', | ||
| description: 'Test tool description', | ||
| version: '1.0.0', | ||
| parameters: mockTool.parameters, | ||
| }); | ||
| }); | ||
| it('应该不包含 handler 和其他内部属性', () => { | ||
| const mockTool: InternalTool = { | ||
| name: 'test_tool', | ||
| category: 'test', | ||
| internal: true, | ||
| description: 'Test tool', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: {}, | ||
| }, | ||
| handler: async () => ({ success: true }), | ||
| renderResultForAssistant: (result) => JSON.stringify(result), | ||
| needsPermissions: () => false, | ||
| }; | ||
| const formatted = formatToolForLLM(mockTool); | ||
| expect(formatted).not.toHaveProperty('handler'); | ||
| expect(formatted).not.toHaveProperty('internal'); | ||
| expect(formatted).not.toHaveProperty('renderResultForAssistant'); | ||
| expect(formatted).not.toHaveProperty('needsPermissions'); | ||
| }); | ||
| it('应该保留完整的参数 Schema 结构', () => { | ||
| const complexParameters = { | ||
| type: 'object' as const, | ||
| properties: { | ||
| nested: { | ||
| type: 'object' as const, | ||
| properties: { | ||
| deep: { | ||
| type: 'string' as const, | ||
| }, | ||
| }, | ||
| }, | ||
| array: { | ||
| type: 'array' as const, | ||
| items: { | ||
| type: 'number' as const, | ||
| }, | ||
| }, | ||
| }, | ||
| required: ['nested'], | ||
| }; | ||
| const mockTool: InternalTool = { | ||
| name: 'complex_tool', | ||
| category: 'test', | ||
| internal: true, | ||
| description: 'Complex tool', | ||
| version: '1.0.0', | ||
| parameters: complexParameters, | ||
| handler: async () => ({}), | ||
| }; | ||
| const formatted = formatToolForLLM(mockTool); | ||
| expect(formatted.parameters).toEqual(complexParameters); | ||
| }); | ||
| }); | ||
| }); | ||
| /** | ||
| * 工具模块统一导出 | ||
| */ | ||
| // 类型定义 | ||
| export * from './types.js'; | ||
| // 工具管理器 | ||
| export { ToolManager } from './ToolManager.js'; | ||
| // 具体工具 | ||
| export { ListFilesTool } from './ListFiles/definitions.js'; | ||
| export type { ListFilesArgs, ListFilesResult } from './ListFiles/executors.js'; | ||
| export { ReadFileTool } from './ReadFile/definitions.js'; | ||
| export type { ReadFileArgs, ReadFileResult } from './ReadFile/executors.js'; |
| import { InternalTool } from "../types"; | ||
| import { listFilesExecutor } from "./executors"; | ||
| import { renderResultForAssistant } from "./executors"; | ||
| import { ListFilesArgs, ListFilesResult } from "./executors"; | ||
| export const ListFilesTool: InternalTool<ListFilesArgs, ListFilesResult> = { | ||
| name: 'list_files', | ||
| category: 'filesystem', | ||
| internal: true, | ||
| description: '列出指定目录下的文件和文件夹。如果不指定目录,则列出当前工作目录。', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| directory: { | ||
| type: 'string', | ||
| description: '要列出内容的目录路径,默认为当前工作目录', | ||
| }, | ||
| }, | ||
| required: [], | ||
| }, | ||
| handler: listFilesExecutor, | ||
| renderResultForAssistant: renderResultForAssistant, | ||
| }; |
| /** | ||
| * 列出目录文件工具 | ||
| * 列出指定目录下的文件和文件夹 | ||
| */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { InternalTool } from '../types.js'; | ||
| export interface ListFilesArgs { | ||
| /** 目录路径,默认为当前工作目录 */ | ||
| directory?: string; | ||
| } | ||
| export interface ListFilesResult { | ||
| /** 目录路径 */ | ||
| directory: string; | ||
| /** 文件列表 */ | ||
| files: Array<{ | ||
| name: string; | ||
| type: 'file' | 'directory'; | ||
| size?: number; | ||
| }>; | ||
| /** 文件总数 */ | ||
| totalCount: number; | ||
| } | ||
| /** | ||
| * 列出文件执行器 | ||
| * @param args - 列出文件参数 | ||
| * @param config - 配置 | ||
| * @returns - 列出文件结果 | ||
| */ | ||
| export async function listFilesExecutor(args: ListFilesArgs, config: any): Promise<ListFilesResult> { | ||
| const cwd = config?.cwd || process.cwd(); | ||
| const targetDir = args.directory ? path.resolve(cwd, args.directory) : cwd; | ||
| // 检查目录是否存在 | ||
| if (!fs.existsSync(targetDir)) { | ||
| throw new Error(`目录不存在: ${targetDir}`); | ||
| } | ||
| // 检查是否是目录 | ||
| const stats = fs.statSync(targetDir); | ||
| if (!stats.isDirectory()) { | ||
| throw new Error(`路径不是目录: ${targetDir}`); | ||
| } | ||
| // 读取目录内容 | ||
| const entries = fs.readdirSync(targetDir, { withFileTypes: true }); | ||
| const files = entries | ||
| .filter((entry) => !entry.name.startsWith('.')) // 过滤隐藏文件 | ||
| .map((entry) => { | ||
| const fullPath = path.join(targetDir, entry.name); | ||
| const result: { | ||
| name: string; | ||
| type: 'file' | 'directory'; | ||
| size?: number; | ||
| } = { | ||
| name: entry.name, | ||
| type: entry.isDirectory() ? 'directory' : 'file', | ||
| }; | ||
| // 对于文件,获取大小 | ||
| if (entry.isFile()) { | ||
| try { | ||
| const fileStats = fs.statSync(fullPath); | ||
| result.size = fileStats.size; | ||
| } catch { | ||
| // 忽略权限错误 | ||
| } | ||
| } | ||
| return result; | ||
| }) | ||
| .sort((a, b) => { | ||
| // 目录在前,文件在后 | ||
| if (a.type !== b.type) { | ||
| return a.type === 'directory' ? -1 : 1; | ||
| } | ||
| return a.name.localeCompare(b.name); | ||
| }); | ||
| return { | ||
| directory: targetDir, | ||
| files, | ||
| totalCount: files.length, | ||
| }; | ||
| } | ||
| /** | ||
| * 格式化工具结果 | ||
| * @param result - 列表文件结果 | ||
| * @returns | ||
| */ | ||
| export function renderResultForAssistant(result: ListFilesResult): string { | ||
| const lines = [`目录: ${result.directory}`, `共 ${result.totalCount} 个项目:`, '']; | ||
| for (const file of result.files) { | ||
| const icon = file.type === 'directory' ? '📁' : '📄'; | ||
| const size = file.size !== undefined ? ` (${formatSize(file.size)})` : ''; | ||
| lines.push(`${icon} ${file.name}${size}`); | ||
| } | ||
| return lines.join('\n'); | ||
| } | ||
| /** | ||
| * 格式化文件大小 | ||
| */ | ||
| function formatSize(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } |
| import { InternalTool } from "../types"; | ||
| import { readFileExecutor } from "./executors"; | ||
| import { renderResultForAssistant } from "./executors"; | ||
| import { ReadFileArgs, ReadFileResult } from "./executors"; | ||
| export const ReadFileTool: InternalTool<ReadFileArgs, ReadFileResult> = { | ||
| name: 'read_file', | ||
| category: 'filesystem', | ||
| internal: true, | ||
| description: '读取指定文件的内容。支持文本文件,返回文件内容、大小和行数。', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| filePath: { | ||
| type: 'string', | ||
| description: '要读取的文件路径', | ||
| }, | ||
| encoding: { | ||
| type: 'string', | ||
| description: '文件编码,默认为 utf-8', | ||
| }, | ||
| }, | ||
| required: ['filePath'], | ||
| }, | ||
| handler: readFileExecutor, | ||
| renderResultForAssistant: renderResultForAssistant, | ||
| }; | ||
| /** | ||
| * 读取文件内容工具 | ||
| * 读取指定文件的内容 | ||
| */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { InternalTool } from '../types.js'; | ||
| export interface ReadFileArgs { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 编码,默认 utf-8 */ | ||
| encoding?: BufferEncoding; | ||
| } | ||
| export interface ReadFileResult { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 文件内容 */ | ||
| content: string; | ||
| /** 文件大小(字节) */ | ||
| size: number; | ||
| /** 行数 */ | ||
| lineCount: number; | ||
| } | ||
| /** | ||
| * 读取文件执行器 | ||
| * @param args - 读取文件参数 | ||
| * @param config - 配置 | ||
| * @returns - 读取文件结果 | ||
| */ | ||
| export async function readFileExecutor(args: ReadFileArgs, config: any): Promise<ReadFileResult> { | ||
| const cwd = config?.cwd || process.cwd(); | ||
| const targetPath = path.resolve(cwd, args.filePath); | ||
| const encoding = args.encoding || 'utf-8'; | ||
| // 检查文件是否存在 | ||
| if (!fs.existsSync(targetPath)) { | ||
| throw new Error(`文件不存在: ${targetPath}`); | ||
| } | ||
| // 检查是否是文件 | ||
| const stats = fs.statSync(targetPath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`路径不是文件: ${targetPath}`); | ||
| } | ||
| // 检查文件大小,避免读取过大的文件 | ||
| const maxSize = 1024 * 1024; // 1MB | ||
| if (stats.size > maxSize) { | ||
| throw new Error(`文件太大 (${formatSize(stats.size)}),最大支持 1MB`); | ||
| } | ||
| // 读取文件内容 | ||
| const content = fs.readFileSync(targetPath, encoding); | ||
| const lineCount = content.split('\n').length; | ||
| return { | ||
| filePath: targetPath, | ||
| content, | ||
| size: stats.size, | ||
| lineCount, | ||
| }; | ||
| } | ||
| /** | ||
| * 格式化工具结果 | ||
| * @param result - 读取文件结果 | ||
| * @returns - 读取文件结果 | ||
| */ | ||
| export function renderResultForAssistant(result: ReadFileResult): string { | ||
| return [ | ||
| `文件: ${result.filePath}`, | ||
| `大小: ${formatSize(result.size)}`, | ||
| `行数: ${result.lineCount}`, | ||
| '', | ||
| '--- 内容 ---', | ||
| result.content, | ||
| ].join('\n'); | ||
| } | ||
| /** | ||
| * 格式化文件大小 | ||
| */ | ||
| function formatSize(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } |
| /** | ||
| * 读取文件内容工具 | ||
| * 读取指定文件的内容 | ||
| */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { InternalTool } from '../types.js'; | ||
| export interface ReadFileArgs { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 编码,默认 utf-8 */ | ||
| encoding?: BufferEncoding; | ||
| } | ||
| export interface ReadFileResult { | ||
| /** 文件路径 */ | ||
| filePath: string; | ||
| /** 文件内容 */ | ||
| content: string; | ||
| /** 文件大小(字节) */ | ||
| size: number; | ||
| /** 行数 */ | ||
| lineCount: number; | ||
| } | ||
| export const ReadFileTool: InternalTool<ReadFileArgs, ReadFileResult> = { | ||
| name: 'read_file', | ||
| category: 'filesystem', | ||
| internal: true, | ||
| description: '读取指定文件的内容。支持文本文件,返回文件内容、大小和行数。', | ||
| version: '1.0.0', | ||
| parameters: { | ||
| type: 'object', | ||
| properties: { | ||
| filePath: { | ||
| type: 'string', | ||
| description: '要读取的文件路径', | ||
| }, | ||
| encoding: { | ||
| type: 'string', | ||
| description: '文件编码,默认为 utf-8', | ||
| }, | ||
| }, | ||
| required: ['filePath'], | ||
| }, | ||
| async handler(args, context) { | ||
| const cwd = context?.cwd || process.cwd(); | ||
| const targetPath = path.resolve(cwd, args.filePath); | ||
| const encoding = args.encoding || 'utf-8'; | ||
| // 检查文件是否存在 | ||
| if (!fs.existsSync(targetPath)) { | ||
| throw new Error(`文件不存在: ${targetPath}`); | ||
| } | ||
| // 检查是否是文件 | ||
| const stats = fs.statSync(targetPath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`路径不是文件: ${targetPath}`); | ||
| } | ||
| // 检查文件大小,避免读取过大的文件 | ||
| const maxSize = 1024 * 1024; // 1MB | ||
| if (stats.size > maxSize) { | ||
| throw new Error(`文件太大 (${formatSize(stats.size)}),最大支持 1MB`); | ||
| } | ||
| // 读取文件内容 | ||
| const content = fs.readFileSync(targetPath, encoding); | ||
| const lineCount = content.split('\n').length; | ||
| return { | ||
| filePath: targetPath, | ||
| content, | ||
| size: stats.size, | ||
| lineCount, | ||
| }; | ||
| }, | ||
| renderResultForAssistant(result) { | ||
| return [ | ||
| `文件: ${result.filePath}`, | ||
| `大小: ${formatSize(result.size)}`, | ||
| `行数: ${result.lineCount}`, | ||
| '', | ||
| '--- 内容 ---', | ||
| result.content, | ||
| ].join('\n'); | ||
| }, | ||
| }; | ||
| /** | ||
| * 格式化文件大小 | ||
| */ | ||
| function formatSize(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } |
| import { InternalTool, InternalToolContext } from './types.js'; | ||
| import { ListFilesTool } from './ListFiles/definitions.js'; | ||
| import { ReadFileTool } from './ReadFile/definitions.js'; | ||
| // 注册的工具列表 | ||
| const toolsList: InternalTool[] = [ListFilesTool, ReadFileTool]; | ||
| /** | ||
| * OpenAI 格式的工具定义 | ||
| */ | ||
| interface OpenAITool { | ||
| type: 'function'; | ||
| function: { | ||
| name: string; | ||
| description: string; | ||
| parameters: any; | ||
| }; | ||
| } | ||
| /** | ||
| * 工具管理类 | ||
| * 负责工具的注册、查询和执行 | ||
| */ | ||
| export class ToolManager { | ||
| private tools: Map<string, InternalTool> = new Map(); | ||
| constructor() { | ||
| this.registerAllTools(); | ||
| } | ||
| /** | ||
| * 注册所有工具 | ||
| */ | ||
| private registerAllTools() { | ||
| toolsList.forEach((tool) => { | ||
| this.tools.set(tool.name, tool); | ||
| }); | ||
| } | ||
| /** | ||
| * 执行指定工具 | ||
| */ | ||
| async execute<TArgs = any, TResult = any>( | ||
| name: string, | ||
| args: TArgs, | ||
| context?: InternalToolContext | ||
| ): Promise<TResult> { | ||
| const tool = this.tools.get(name); | ||
| if (!tool) { | ||
| throw new Error(`Tool '${name}' not found. Available: ${this.getToolNames().join(', ')}`); | ||
| } | ||
| try { | ||
| return await tool.handler(args, context); | ||
| } catch (error: any) { | ||
| console.error(`Tool '${name}' failed:`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * 获取格式化的工具定义(OpenAI 格式) | ||
| * 供 LLM API 调用使用 | ||
| */ | ||
| getFormattedTools(): OpenAITool[] { | ||
| return Array.from(this.tools.values()).map((tool) => ({ | ||
| type: 'function' as const, | ||
| function: { | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: tool.parameters, | ||
| }, | ||
| })); | ||
| } | ||
| /** | ||
| * 获取所有工具 | ||
| */ | ||
| getTools(): InternalTool[] { | ||
| return Array.from(this.tools.values()); | ||
| } | ||
| /** | ||
| * 获取指定工具 | ||
| */ | ||
| getTool(name: string): InternalTool | undefined { | ||
| return this.tools.get(name); | ||
| } | ||
| /** | ||
| * 获取所有工具名称 | ||
| */ | ||
| getToolNames(): string[] { | ||
| return Array.from(this.tools.keys()); | ||
| } | ||
| /** | ||
| * 检查工具是否存在 | ||
| */ | ||
| hasTool(name: string): boolean { | ||
| return this.tools.has(name); | ||
| } | ||
| /** | ||
| * 清空所有工具 | ||
| * 用于创建无工具的Agent场景 | ||
| */ | ||
| clear(): void { | ||
| this.tools.clear(); | ||
| } | ||
| /** | ||
| * 获取工具统计信息 | ||
| */ | ||
| getStats() { | ||
| const tools = this.getTools(); | ||
| const categories = new Map<string, number>(); | ||
| tools.forEach((tool) => { | ||
| const count = categories.get(tool.category) || 0; | ||
| categories.set(tool.category, count + 1); | ||
| }); | ||
| return { | ||
| totalTools: tools.length, | ||
| categories: Object.fromEntries(categories), | ||
| toolNames: this.getToolNames(), | ||
| }; | ||
| } | ||
| } |
| /** | ||
| * 工具定义类型约束 | ||
| * 用于定义所有工具的统一结构,便于格式化并作为提示词传给大模型 | ||
| */ | ||
| /** | ||
| * JSON Schema 参数定义 | ||
| */ | ||
| export interface ToolParameterSchema { | ||
| type: 'object' | 'array' | 'string' | 'number' | 'boolean'; | ||
| description?: string; | ||
| properties?: Record<string, ToolParameterSchema>; | ||
| items?: ToolParameterSchema; | ||
| required?: string[]; | ||
| enum?: string[]; | ||
| default?: any; | ||
| } | ||
| /** | ||
| * 工具上下文 | ||
| */ | ||
| export interface InternalToolContext { | ||
| abortSignal?: AbortSignal; | ||
| cwd?: string; | ||
| [key: string]: any; // 扩展字段 | ||
| } | ||
| /** | ||
| * 工具定义基础接口 | ||
| */ | ||
| export interface InternalTool<TArgs = any, TResult = any> { | ||
| /** 工具名称(唯一标识) */ | ||
| name: string; | ||
| /** 工具分类(如 filesystem、search、network) */ | ||
| category: string; | ||
| /** 是否为内部工具 */ | ||
| internal: boolean; | ||
| /** 工具描述(简短,详细描述在 prompt 中) */ | ||
| description: string; | ||
| /** 版本号 */ | ||
| version: string; | ||
| /** 参数定义(JSON Schema 格式) */ | ||
| parameters: ToolParameterSchema; | ||
| /** 工具处理函数 */ | ||
| handler: (args: TArgs, context?: InternalToolContext) => Promise<TResult>; | ||
| /** 可选:格式化结果给 AI */ | ||
| renderResultForAssistant?: (result: TResult) => string; | ||
| /** 可选:权限控制 */ | ||
| needsPermissions?: (input?: TArgs) => boolean; // 是否需要权限 | ||
| isEnabled?: () => Promise<boolean>; // 是否启用 | ||
| isReadOnly?: () => boolean; // 是否只读 | ||
| isConcurrencySafe?: () => boolean; // 是否并发安全 | ||
| } | ||
| /** | ||
| * 格式化工具定义为大模型可读格式 | ||
| */ | ||
| export interface FormattedToolDefinition { | ||
| name: string; | ||
| category: string; | ||
| description: string; | ||
| version: string; | ||
| parameters: ToolParameterSchema; | ||
| } | ||
| /** | ||
| * 将工具定义格式化为大模型输入格式 | ||
| */ | ||
| export function formatToolForLLM(tool: InternalTool): FormattedToolDefinition { | ||
| return { | ||
| name: tool.name, | ||
| category: tool.category, | ||
| description: tool.description, | ||
| version: tool.version, | ||
| parameters: tool.parameters, | ||
| }; | ||
| } | ||
| /** | ||
| * 通用测试数据集 | ||
| * 包含单 Agent 和多 Agent 的测试用例 | ||
| */ | ||
| import { TestCase } from './types.js'; | ||
| /** | ||
| * 单 Agent 测试用例 (SimpleAgent) | ||
| */ | ||
| export const SIMPLE_AGENT_TESTS: TestCase[] = [ | ||
| // S1. 单工具测试 - 列出文件 | ||
| { | ||
| id: 'S1', | ||
| description: '单工具调用 - 列出当前目录文件', | ||
| input: '列出当前目录下的所有文件和文件夹', | ||
| expected: { | ||
| agents: ['simple_agent'], | ||
| tools: { simple_agent: ['list_files'] }, | ||
| }, | ||
| }, | ||
| // S2. 多工具测试 - 先列出再读取 | ||
| { | ||
| id: 'S2', | ||
| description: '多工具调用 - 列出文件后读取', | ||
| input: '先列出当前目录的文件,然后读取 README.md 的内容', | ||
| expected: { | ||
| agents: ['simple_agent'], | ||
| tools: { simple_agent: ['list_files', 'read_file'] }, | ||
| }, | ||
| }, | ||
| ]; | ||
| /** | ||
| * 多 Agent 测试用例 (MultiAgent: MainAgent + SubAgents) | ||
| */ | ||
| export const MULTI_AGENT_TESTS: TestCase[] = [ | ||
| // M1. 简单任务分发 - 主Agent协调子Agent | ||
| { | ||
| id: 'M1', | ||
| description: '多Agent协调 - 简单任务分发', | ||
| input: '帮我分析一下如何提升代码质量', | ||
| expected: { | ||
| agents: ['main_agent', 'researcher', 'executor'], | ||
| tools: { | ||
| main_agent: [], | ||
| researcher: [], | ||
| executor: [], | ||
| }, | ||
| }, | ||
| }, | ||
| // M2. 研究+执行 - 调用 researcher 和 executor | ||
| { | ||
| id: 'M2', | ||
| description: '多Agent协调 - 研究与执行', | ||
| input: '请研究并提供一个实现用户认证系统的方案', | ||
| expected: { | ||
| agents: ['main_agent', 'researcher', 'executor'], | ||
| tools: { | ||
| main_agent: [], | ||
| researcher: [], | ||
| executor: [], | ||
| }, | ||
| }, | ||
| }, | ||
| // M3. 复杂任务 - 多轮子Agent协作 | ||
| { | ||
| id: 'M3', | ||
| description: '多Agent协调 - 复杂任务处理', | ||
| input: '帮我设计一个完整的电商系统架构,包括技术选型和实施计划', | ||
| expected: { | ||
| agents: ['main_agent', 'researcher', 'executor'], | ||
| tools: { | ||
| main_agent: [], | ||
| researcher: [], | ||
| executor: [], | ||
| }, | ||
| }, | ||
| }, | ||
| ]; | ||
| /** | ||
| * 所有测试用例 | ||
| */ | ||
| export const TEST_CASES: TestCase[] = [...SIMPLE_AGENT_TESTS, ...MULTI_AGENT_TESTS]; | ||
| /** | ||
| * 根据ID获取测试用例 | ||
| */ | ||
| export function getTestById(id: string): TestCase | undefined { | ||
| return TEST_CASES.find((test) => test.id === id); | ||
| } | ||
| /** | ||
| * 获取单Agent测试用例 | ||
| */ | ||
| export function getSimpleAgentTests(): TestCase[] { | ||
| return SIMPLE_AGENT_TESTS; | ||
| } | ||
| /** | ||
| * 获取多Agent测试用例 | ||
| */ | ||
| export function getMultiAgentTests(): TestCase[] { | ||
| return MULTI_AGENT_TESTS; | ||
| } |
| /** | ||
| * 评估函数 - 简化版本 | ||
| * 比较期望行为和实际执行数据 | ||
| */ | ||
| import { TestCase, CollectedData, EvaluateResult } from './types.js'; | ||
| /** | ||
| * 评估函数 | ||
| * @param testCase 测试用例 | ||
| * @param actual 实际收集到的数据 | ||
| * @returns 评估结果 | ||
| */ | ||
| export function evaluate(testCase: TestCase, actual: CollectedData): EvaluateResult { | ||
| const expected = testCase.expected; | ||
| // 1. 评估Agent调用 | ||
| const missedAgents = expected.agents.filter((a) => !actual.agents.includes(a)); | ||
| const extraAgents = actual.agents.filter((a) => !expected.agents.includes(a)); | ||
| const agentMatch = missedAgents.length === 0 && extraAgents.length === 0; | ||
| // 2. 评估工具调用 | ||
| const missedTools: { agent: string; tool: string }[] = []; | ||
| const extraTools: { agent: string; tool: string }[] = []; | ||
| // 检查遗漏的工具 | ||
| for (const [agent, tools] of Object.entries(expected.tools)) { | ||
| const actualTools = actual.tools[agent] || []; | ||
| for (const tool of tools) { | ||
| if (!actualTools.includes(tool)) { | ||
| missedTools.push({ agent, tool }); | ||
| } | ||
| } | ||
| } | ||
| // 检查多余的工具 | ||
| for (const [agent, tools] of Object.entries(actual.tools)) { | ||
| const expectedTools = expected.tools[agent] || []; | ||
| for (const tool of tools) { | ||
| if (!expectedTools.includes(tool)) { | ||
| extraTools.push({ agent, tool }); | ||
| } | ||
| } | ||
| } | ||
| const toolMatch = missedTools.length === 0 && extraTools.length === 0; | ||
| // 3. 综合判断 | ||
| const passed = agentMatch && toolMatch; | ||
| return { | ||
| passed, | ||
| agentMatch, | ||
| toolMatch, | ||
| details: { | ||
| agents: { | ||
| expected: expected.agents, | ||
| actual: actual.agents, | ||
| missed: missedAgents, | ||
| extra: extraAgents, | ||
| }, | ||
| tools: { | ||
| expected: expected.tools, | ||
| actual: actual.tools, | ||
| missed: missedTools, | ||
| extra: extraTools, | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * 格式化评估结果为可读字符串 | ||
| */ | ||
| export function formatResult(result: EvaluateResult): string { | ||
| let output = ''; | ||
| // Agent评估 | ||
| if (result.agentMatch) { | ||
| output += `✅ Agent调用正确\n`; | ||
| } else { | ||
| output += `❌ Agent调用错误\n`; | ||
| output += ` 期望: ${result.details.agents.expected.join(', ') || '无'}\n`; | ||
| output += ` 实际: ${result.details.agents.actual.join(', ') || '无'}\n`; | ||
| if (result.details.agents.missed.length > 0) { | ||
| output += ` 遗漏: ${result.details.agents.missed.join(', ')}\n`; | ||
| } | ||
| if (result.details.agents.extra.length > 0) { | ||
| output += ` 多余: ${result.details.agents.extra.join(', ')}\n`; | ||
| } | ||
| } | ||
| // 工具评估 | ||
| if (result.toolMatch) { | ||
| output += `✅ 工具调用正确\n`; | ||
| } else { | ||
| output += `❌ 工具调用错误\n`; | ||
| if (result.details.tools.missed.length > 0) { | ||
| const missed = result.details.tools.missed.map((t) => `${t.agent}.${t.tool}`).join(', '); | ||
| output += ` 遗漏: ${missed}\n`; | ||
| } | ||
| if (result.details.tools.extra.length > 0) { | ||
| const extra = result.details.tools.extra.map((t) => `${t.agent}.${t.tool}`).join(', '); | ||
| output += ` 多余: ${extra}\n`; | ||
| } | ||
| } | ||
| return output; | ||
| } | ||
| /** | ||
| * 事件总线 + 数据收集器(合并版本) | ||
| * 负责事件的发射、监听,以及收集执行过程中的数据 | ||
| */ | ||
| import { EventEmitter } from 'events'; | ||
| import { CollectedData } from './types.js'; | ||
| /** | ||
| * 事件类型 | ||
| */ | ||
| export type EventType = | ||
| | 'agent:call' // 子Agent被调用 | ||
| | 'tool:call' // 工具被调用 | ||
| | 'edit:complete'; // 编辑节点完成 | ||
| /** | ||
| * 事件数据类型 | ||
| */ | ||
| export interface AgentCallEvent { | ||
| agentName: string; | ||
| } | ||
| export interface ToolCallEvent { | ||
| agentName: string; | ||
| toolName: string; | ||
| } | ||
| export interface EditCompleteEvent { | ||
| successCount: number; | ||
| failCount: number; | ||
| } | ||
| /** | ||
| * 事件总线 - 单例模式 | ||
| * 合并了事件发射和数据收集功能 | ||
| */ | ||
| class EventBus { | ||
| private emitter = new EventEmitter(); | ||
| private static instance: EventBus; | ||
| // 数据收集(原 Collector 功能) | ||
| private agents: string[] = []; | ||
| private tools: Map<string, string[]> = new Map(); | ||
| private editResult: { success: number; fail: number } | null = null; | ||
| constructor() { | ||
| this.setupListeners(); | ||
| } | ||
| /** | ||
| * 获取单例实例 | ||
| */ | ||
| static getInstance(): EventBus { | ||
| if (!this.instance) { | ||
| this.instance = new EventBus(); | ||
| } | ||
| return this.instance; | ||
| } | ||
| /** | ||
| * 设置内部事件监听器(用于数据收集) | ||
| */ | ||
| private setupListeners() { | ||
| // 监听子Agent调用事件 | ||
| this.emitter.on('agent:call', (data: AgentCallEvent) => { | ||
| this.agents.push(data.agentName); | ||
| // 为该Agent初始化工具列表 | ||
| if (!this.tools.has(data.agentName)) { | ||
| this.tools.set(data.agentName, []); | ||
| } | ||
| }); | ||
| // 监听工具调用事件 | ||
| this.emitter.on('tool:call', (data: ToolCallEvent) => { | ||
| const agentTools = this.tools.get(data.agentName); | ||
| if (agentTools) { | ||
| // 去重添加 | ||
| if (!agentTools.includes(data.toolName)) { | ||
| agentTools.push(data.toolName); | ||
| } | ||
| } else { | ||
| this.tools.set(data.agentName, [data.toolName]); | ||
| } | ||
| }); | ||
| // 监听编辑完成事件 | ||
| this.emitter.on('edit:complete', (data: EditCompleteEvent) => { | ||
| this.editResult = { | ||
| success: data.successCount, | ||
| fail: data.failCount, | ||
| }; | ||
| }); | ||
| } | ||
| /** | ||
| * 发射事件 | ||
| */ | ||
| emit(event: EventType, data: AgentCallEvent | ToolCallEvent | EditCompleteEvent) { | ||
| this.emitter.emit(event, data); | ||
| } | ||
| /** | ||
| * 监听事件(供外部使用) | ||
| */ | ||
| on(event: EventType, handler: (data: any) => void) { | ||
| this.emitter.on(event, handler); | ||
| } | ||
| /** | ||
| * 获取收集到的数据 | ||
| */ | ||
| getData(): CollectedData { | ||
| return { | ||
| agents: [...new Set(this.agents)], // 去重 | ||
| tools: Object.fromEntries(this.tools), | ||
| editResult: this.editResult, | ||
| }; | ||
| } | ||
| /** | ||
| * 重置收集器(每次测试前调用) | ||
| */ | ||
| reset() { | ||
| this.agents = []; | ||
| this.tools = new Map(); | ||
| this.editResult = null; | ||
| } | ||
| /** | ||
| * 重置实例(用于测试) | ||
| */ | ||
| static resetInstance() { | ||
| if (this.instance) { | ||
| this.instance.emitter.removeAllListeners(); | ||
| this.instance = new EventBus(); | ||
| } | ||
| } | ||
| } | ||
| // 导出单例 | ||
| export const eventBus = EventBus.getInstance(); | ||
| /** | ||
| * 评估模块使用示例 | ||
| * 展示如何使用 SimpleAgent 和 MultiAgent 进行测试 | ||
| */ | ||
| import { eventBus } from './EventBus.js'; | ||
| import { evaluate, formatResult } from './evaluate.js'; | ||
| import { | ||
| TEST_CASES, | ||
| getTestById, | ||
| getSimpleAgentTests, | ||
| getMultiAgentTests, | ||
| } from './dataset.js'; | ||
| import { TestCase, EvaluateResult } from './types.js'; | ||
| import { SimpleAgent, MainAgent } from '../core/agent/index.js'; | ||
| import { createLLMService } from '../core/llm/index.js'; | ||
| import { ILLMService } from '../core/llm/types/index.js'; | ||
| // 缓存 LLM 服务和 Agent 实例 | ||
| let llmService: ILLMService | null = null; | ||
| let simpleAgent: SimpleAgent | null = null; | ||
| let multiAgent: MainAgent | null = null; | ||
| /** | ||
| * 获取 LLM 服务(延迟初始化) | ||
| */ | ||
| async function getLLMService(): Promise<ILLMService> { | ||
| if (!llmService) { | ||
| llmService = await createLLMService({ | ||
| provider: 'deepseek', | ||
| model: "deepseek-chat", | ||
| }); | ||
| } | ||
| return llmService; | ||
| } | ||
| /** | ||
| * 获取 SimpleAgent 实例 | ||
| */ | ||
| async function getSimpleAgent(): Promise<SimpleAgent> { | ||
| if (!simpleAgent) { | ||
| const service = await getLLMService(); | ||
| simpleAgent = new SimpleAgent(service, { name: 'simple_agent' }); | ||
| } | ||
| return simpleAgent; | ||
| } | ||
| /** | ||
| * 获取 MultiAgent (MainAgent) 实例 | ||
| */ | ||
| async function getMultiAgent(): Promise<MainAgent> { | ||
| if (!multiAgent) { | ||
| const service = await getLLMService(); | ||
| multiAgent = new MainAgent(service, 'main_agent'); | ||
| } | ||
| return multiAgent; | ||
| } | ||
| /** | ||
| * 判断测试用例是否为多 Agent 测试 | ||
| */ | ||
| function isMultiAgentTest(testCase: TestCase): boolean { | ||
| return testCase.id.startsWith('M'); | ||
| } | ||
| /** | ||
| * 运行单个测试用例 | ||
| */ | ||
| async function runTest(testCase: TestCase): Promise<EvaluateResult | null> { | ||
| console.log(`\n${'='.repeat(50)}`); | ||
| console.log(`🧪 测试: ${testCase.id} - ${testCase.description}`); | ||
| console.log(`${'='.repeat(50)}`); | ||
| const startTime = Date.now(); | ||
| try { | ||
| let agents: string[]; | ||
| let tools: Record<string, string[]>; | ||
| let finalResponse: string; | ||
| let success: boolean; | ||
| let error: string | undefined; | ||
| if (isMultiAgentTest(testCase)) { | ||
| // 多 Agent 测试 | ||
| const agent = await getMultiAgent(); | ||
| const result = await agent.run(testCase.input); | ||
| agents = result.agents; | ||
| tools = result.tools; | ||
| finalResponse = result.finalResponse; | ||
| success = result.success; | ||
| error = result.error; | ||
| } else { | ||
| // 单 Agent 测试 | ||
| const agent = await getSimpleAgent(); | ||
| const result = await agent.run(testCase.input); | ||
| agents = result.agents; | ||
| tools = result.tools; | ||
| finalResponse = result.finalResponse; | ||
| success = result.success; | ||
| error = result.error; | ||
| } | ||
| // 评估 | ||
| const evalResult = evaluate(testCase, { | ||
| agents, | ||
| tools, | ||
| editResult: null, | ||
| }); | ||
| // 输出结果 | ||
| const status = evalResult.passed ? '✅ PASS' : '❌ FAIL'; | ||
| const time = Date.now() - startTime; | ||
| console.log(`\n${status} (${time}ms)`); | ||
| console.log(formatResult(evalResult)); | ||
| const responsePreview = finalResponse.slice(0, 200); | ||
| console.log(`\n📝 Agent回复: ${responsePreview}${finalResponse.length > 200 ? '...' : ''}`); | ||
| if (!success) { | ||
| console.log(`\n⚠️ Agent 执行失败: ${error}`); | ||
| } | ||
| return evalResult; | ||
| } catch (err) { | ||
| console.error(`❌ 执行失败:`, err); | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * 运行测试集 | ||
| */ | ||
| async function runTests(testCases: TestCase[], title: string) { | ||
| console.log(`\n${'#'.repeat(60)}`); | ||
| console.log(`# ${title}: ${testCases.length} 个用例`); | ||
| console.log(`${'#'.repeat(60)}`); | ||
| const results: Array<{ testCase: TestCase; result: EvaluateResult }> = []; | ||
| const startTime = Date.now(); | ||
| for (const testCase of testCases) { | ||
| const result = await runTest(testCase); | ||
| if (result) { | ||
| results.push({ testCase, result }); | ||
| } | ||
| } | ||
| // 汇总 | ||
| const passed = results.filter((r) => r.result.passed).length; | ||
| const failed = results.length - passed; | ||
| const totalTime = Date.now() - startTime; | ||
| console.log(`\n${'='.repeat(60)}`); | ||
| console.log(`${title} 完成`); | ||
| console.log(`${'='.repeat(60)}`); | ||
| console.log(`总数: ${results.length}`); | ||
| console.log(`✅ 通过: ${passed}`); | ||
| console.log(`❌ 失败: ${failed}`); | ||
| console.log(`⏱️ 耗时: ${(totalTime / 1000).toFixed(2)}s`); | ||
| return results; | ||
| } | ||
| /** | ||
| * 运行所有测试用例 | ||
| */ | ||
| async function runAllTests() { | ||
| console.log(`\n${'#'.repeat(60)}`); | ||
| console.log(`# 开始全量测试: ${TEST_CASES.length} 个用例`); | ||
| console.log(`${'#'.repeat(60)}`); | ||
| const startTime = Date.now(); | ||
| // 运行单 Agent 测试 | ||
| const simpleResults = await runTests(getSimpleAgentTests(), '单 Agent 测试'); | ||
| // 运行多 Agent 测试 | ||
| const multiResults = await runTests(getMultiAgentTests(), '多 Agent 测试'); | ||
| // 总汇总 | ||
| const allResults = [...simpleResults, ...multiResults]; | ||
| const passed = allResults.filter((r) => r.result.passed).length; | ||
| const failed = allResults.length - passed; | ||
| const totalTime = Date.now() - startTime; | ||
| console.log(`\n${'#'.repeat(60)}`); | ||
| console.log(`# 全部测试完成`); | ||
| console.log(`${'#'.repeat(60)}`); | ||
| console.log(`总数: ${allResults.length}`); | ||
| console.log(`✅ 通过: ${passed}`); | ||
| console.log(`❌ 失败: ${failed}`); | ||
| console.log(`⏱️ 总耗时: ${(totalTime / 1000).toFixed(2)}s`); | ||
| } | ||
| /** | ||
| * 打印帮助信息 | ||
| */ | ||
| function printHelp() { | ||
| console.log(` | ||
| 评估模块使用说明: | ||
| npx ts-node example.ts [选项] | ||
| 选项: | ||
| --help 显示帮助信息 | ||
| --test <id> 运行指定测试用例 (如: S1, M1) | ||
| --simple 运行单 Agent 测试集 (2 个用例) | ||
| --multi 运行多 Agent 测试集 (3 个用例) | ||
| (无参数) 运行所有测试用例 | ||
| 测试用例 ID: | ||
| S1, S2 单 Agent 测试 (SimpleAgent) | ||
| M1, M2, M3 多 Agent 测试 (MultiAgent) | ||
| `); | ||
| } | ||
| /** | ||
| * 主函数 | ||
| */ | ||
| async function main() { | ||
| const args = process.argv.slice(2); | ||
| if (args.includes('--help')) { | ||
| printHelp(); | ||
| } else if (args.includes('--simple')) { | ||
| // 运行单 Agent 测试集 | ||
| await runTests(getSimpleAgentTests(), '单 Agent 测试'); | ||
| } else if (args.includes('--multi')) { | ||
| // 运行多 Agent 测试集 | ||
| await runTests(getMultiAgentTests(), '多 Agent 测试'); | ||
| } else if (args.includes('--test') && args[args.indexOf('--test') + 1]) { | ||
| // 运行指定测试用例 | ||
| const testId = args[args.indexOf('--test') + 1]; | ||
| const testCase = getTestById(testId); | ||
| if (testCase) { | ||
| await runTest(testCase); | ||
| } else { | ||
| console.error(`未找到测试用例: ${testId}`); | ||
| console.log('可用的测试用例 ID: S1, S2, M1, M2, M3'); | ||
| } | ||
| } else { | ||
| // 运行所有测试 | ||
| await runAllTests(); | ||
| } | ||
| } | ||
| // 运行 | ||
| main().catch(console.error); |
| /** | ||
| * 评估模块模板 - 类型定义 | ||
| * 简化版本,专注核心功能 | ||
| */ | ||
| /** | ||
| * 测试用例定义 | ||
| */ | ||
| export interface TestCase { | ||
| id: string; // 测试用例ID | ||
| description: string; // 用例描述 | ||
| input: string; // 用户输入 | ||
| expected: ExpectedBehavior; // 期望结果 | ||
| } | ||
| /** | ||
| * 期望行为定义 | ||
| */ | ||
| export interface ExpectedBehavior { | ||
| // 期望调用的子Agent列表 | ||
| agents: string[]; | ||
| // 每个子Agent期望调用的工具 | ||
| tools: { | ||
| [agentName: string]: string[]; | ||
| }; | ||
| } | ||
| /** | ||
| * 事件收集器收集到的实际执行数据 | ||
| */ | ||
| export interface CollectedData { | ||
| // 实际调用的子Agent列表 | ||
| agents: string[]; | ||
| // 每个Agent实际调用的工具 | ||
| tools: { | ||
| [agentName: string]: string[]; | ||
| }; | ||
| // 编辑节点执行结果 | ||
| editResult: { | ||
| success: number; | ||
| fail: number; | ||
| } | null; | ||
| } | ||
| /** | ||
| * 评估结果 | ||
| */ | ||
| export interface EvaluateResult { | ||
| passed: boolean; // 是否通过 | ||
| agentMatch: boolean; // Agent调用是否匹配 | ||
| toolMatch: boolean; // 工具调用是否匹配 | ||
| // 详细信息 | ||
| details: { | ||
| agents: { | ||
| expected: string[]; | ||
| actual: string[]; | ||
| missed: string[]; // 遗漏的Agent | ||
| extra: string[]; // 多余的Agent | ||
| }; | ||
| tools: { | ||
| expected: { [agentName: string]: string[] }; | ||
| actual: { [agentName: string]: string[] }; | ||
| missed: { agent: string; tool: string }[]; | ||
| extra: { agent: string; tool: string }[]; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * 多智能体对话示例 | ||
| * 演示 MainAgent 协调多个 SubAgent 完成任务 | ||
| */ | ||
| import { createLLMService } from '../core/llm/index.js'; | ||
| import { MainAgent } from '../core/agent/index.js'; | ||
| async function main() { | ||
| console.log('🚀 Multi-Agent Chat Example\n'); | ||
| // 创建 LLM 服务 | ||
| const service = await createLLMService({ | ||
| provider: 'deepseek', | ||
| model: 'deepseek-chat', | ||
| }); | ||
| // 创建多智能体系统 (MainAgent + SubAgents) | ||
| const multiAgent = new MainAgent(service, 'main_agent'); | ||
| console.log('📋 任务: 分析如何提升代码质量\n'); | ||
| // 执行多智能体协作 | ||
| const result = await multiAgent.run('请帮我分析如何提升代码质量,并给出具体的改进建议'); | ||
| // 输出结果 | ||
| console.log('\n' + '='.repeat(60)); | ||
| console.log('📊 执行结果汇总'); | ||
| console.log('='.repeat(60)); | ||
| console.log(`\n✅ 执行状态: ${result.success ? '成功' : '失败'}`); | ||
| console.log(`🤖 参与的 Agent: ${result.agents.join(' → ')}`); | ||
| console.log('\n📝 子 Agent 执行记录:'); | ||
| for (const subResult of result.subAgentResults) { | ||
| console.log(`\n [${subResult.agentName}]`); | ||
| console.log(` 状态: ${subResult.success ? '✅ 成功' : '❌ 失败'}`); | ||
| console.log(` 输出: ${subResult.result.slice(0, 200)}${subResult.result.length > 200 ? '...' : ''}`); | ||
| } | ||
| console.log('\n' + '='.repeat(60)); | ||
| console.log('🎯 最终响应'); | ||
| console.log('='.repeat(60)); | ||
| console.log(result.finalResponse); | ||
| } | ||
| main().catch(console.error); |
| /** | ||
| * 简单对话示例 | ||
| */ | ||
| import { createLLMService } from "../core/llm/index.js"; | ||
| async function main() { | ||
| // 创建 LLM 服务 | ||
| const service = await createLLMService({ | ||
| provider: "deepseek", | ||
| model: "deepseek-chat", | ||
| }); | ||
| console.log("🚀 Simple Chat Example\n"); | ||
| // 简单对话 | ||
| const response = await service.simpleChat( | ||
| "你好!你能用一句话介绍一下自己吗?", | ||
| "你是一个有用的AI助手。" | ||
| ); | ||
| console.log("Assistant:", response); | ||
| } | ||
| main().catch(console.error); |
| /** | ||
| * 简单的日志工具 | ||
| */ | ||
| export enum LogLevel { | ||
| DEBUG = 'debug', | ||
| INFO = 'info', | ||
| WARN = 'warn', | ||
| ERROR = 'error', | ||
| } | ||
| class Logger { | ||
| private level: LogLevel = LogLevel.INFO; | ||
| setLevel(level: LogLevel) { | ||
| this.level = level; | ||
| } | ||
| debug(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.DEBUG)) { | ||
| console.log('[DEBUG]', ...args); | ||
| } | ||
| } | ||
| info(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.INFO)) { | ||
| console.log('[INFO]', ...args); | ||
| } | ||
| } | ||
| warn(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.WARN)) { | ||
| console.warn('[WARN]', ...args); | ||
| } | ||
| } | ||
| error(...args: any[]) { | ||
| if (this.shouldLog(LogLevel.ERROR)) { | ||
| console.error('[ERROR]', ...args); | ||
| } | ||
| } | ||
| private shouldLog(level: LogLevel): boolean { | ||
| const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; | ||
| return levels.indexOf(level) >= levels.indexOf(this.level); | ||
| } | ||
| } | ||
| export const logger = new Logger(); | ||
| // 根据配置设置日志级别 | ||
| import { config } from '../config/env.js'; | ||
| logger.setLevel(config.logging.level as LogLevel); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
193492
0.39%4785
0.04%364
3.41%1
Infinity%