diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6cfb638 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# AgentKit 生产环境变量清单 +# 此文件仅作为 Gitea Secrets 配置参考,不会自动加载 +# 实际部署时由 Gitea Actions workflow 从 Secrets 注入到 /opt/agentkit/repo/.env + +# ===== 数据库密码(必填,通过 Gitea Secrets 配置)===== +POSTGRES_PASSWORD=change-me-to-strong-password +REDIS_PASSWORD=change-me-to-strong-password + +# ===== 应用密钥(必填,用于外部系统调用 API 的鉴权)===== +AGENTKIT_API_KEY=change-me-to-strong-api-key + +# ===== LLM Provider API Keys ===== +# 不在此配置!部署完成后通过 Web UI Settings 页面配置: +# http://8.153.107.96:8001 → Settings → LLM +# 配置后自动写入 agentkit.yaml 和 .env diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..6b54e51 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,83 @@ +name: Deploy to Production + +# 触发条件:推送到主干分支 或 手动触发 +on: + push: + branches: [main, master] + workflow_dispatch: + +env: + DEPLOY_DIR: /opt/agentkit + REPO_DIR: /opt/agentkit/repo + COMPOSE_FILE: docker-compose.deploy.yaml + +jobs: + deploy: + # 使用自托管 runner(同机部署,host 模式) + runs-on: self-hosted + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Prepare deploy directory + run: | + sudo mkdir -p "$DEPLOY_DIR" "$REPO_DIR" + sudo chown -R "$(id -u):$(id -g)" "$DEPLOY_DIR" + + - name: Sync code to deploy directory + run: | + rsync -a --delete \ + --exclude='.git' \ + --exclude='node_modules' \ + --exclude='__pycache__' \ + --exclude='.pytest_cache' \ + --exclude='*.pyc' \ + --exclude='.venv' \ + --exclude='venv' \ + --exclude='build/' \ + --exclude='dist/' \ + --exclude='test-results/' \ + ./ "$REPO_DIR/" + + - name: Write .env from Gitea Secrets + # 仅写入基础设施密码;LLM key 由用户部署后通过 Web UI onboarding 配置 + # (PUT /api/v1/settings/llm 会写入 agentkit.yaml 和 .env) + run: | + umask 077 + cat > "$REPO_DIR/.env" < /dev/null 2>&1; then + echo "✅ 服务健康检查通过" + curl -s http://localhost:8001/api/v1/health + exit 0 + fi + echo "尝试 $i/30: 服务未就绪,等待 5 秒..." + sleep 5 + done + echo "❌ 健康检查失败" + docker compose -f "$REPO_DIR/$COMPOSE_FILE" logs --tail=100 + exit 1 + + - name: Cleanup old images + if: always() + run: | + docker image prune -f --filter "until=24h" || true diff --git a/.understand-anything/build_kg.py b/.understand-anything/build_kg.py deleted file mode 100644 index 07ed2a4..0000000 --- a/.understand-anything/build_kg.py +++ /dev/null @@ -1,862 +0,0 @@ -#!/usr/bin/env python3 -"""Knowledge Graph Builder for Fischer AgentKit - -Scans all Python source files under src/agentkit/ and configs/, -extracts classes, functions, imports, and builds a comprehensive -knowledge graph JSON file. -""" - -import ast -import json -import os -import sys -import uuid -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -# Project root -PROJECT_ROOT = Path("/Users/Chiguyong/Code/Fischer/fischer-agentkit") -OUTPUT_PATH = PROJECT_ROOT / ".understand-anything" / "knowledge-graph.json" - -# Directories to scan -SCAN_DIRS = [ - PROJECT_ROOT / "src" / "agentkit", - PROJECT_ROOT / "configs", -] - -# Architecture layer mapping -LAYER_MAP = { - "server": "api", - "cli": "api", - "core": "service", - "orchestrator": "service", - "skills": "service", - "router": "service", - "memory": "data", - "session": "data", - "bus": "data", - "llm": "utility", - "mcp": "utility", - "tools": "utility", - "telemetry": "utility", - "prompts": "utility", - "quality": "utility", - "evaluation": "utility", - "evolution": "utility", - "configs": "utility", -} - -# Chinese summaries for modules -MODULE_SUMMARIES = { - "core": "核心模块 - 定义Agent基类、通信协议、ReAct引擎、任务分发、注册中心等基础组件", - "core.base": "Agent基类 - 统一Agent生命周期管理,包括启动、停止、任务执行、Handoff、进度上报", - "core.protocol": "通信协议定义 - 统一消息格式,包括TaskMessage、TaskResult、TaskProgress、HandoffMessage等", - "core.react": "ReAct推理-行动循环引擎 - 实现Think→Act→Observe循环,支持工具调用和文本解析模式", - "core.exceptions": "自定义异常体系 - 定义Agent框架所有异常类型", - "core.dispatcher": "任务分发器 - 通过Redis Queue将任务分发给Agent,支持回调、重试、进度上报", - "core.registry": "Agent注册中心 - 管理Agent的注册、发现、状态、心跳和负载均衡", - "core.config_driven": "配置驱动Agent - 从YAML/Dict配置自动组装Agent,支持llm_generate/tool_call/custom三种模式", - "core.compressor": "上下文压缩器 - 长会话自动压缩历史消息,支持LLM摘要和简单截断策略", - "core.trace": "执行轨迹记录器 - 记录ReAct执行过程中的完整轨迹,为反思和可观测性提供数据", - "core.shared_workspace": "共享工作空间 - 基于Redis的Agent间共享状态存储,支持读写、锁操作", - "core.agent_pool": "Agent实例池 - 运行时管理Agent的创建、获取、删除", - "core.orchestrator": "多Agent协作编排器 - 实现Orchestrator-Worker模式,支持任务分解、并行执行、自适应编排", - "core.headroom_compressor": "Headroom AI压缩器 - 基于Headroom AI的上下文压缩实现", - "core.logging": "日志配置 - 统一日志格式和配置", - "core.standalone": "独立运行模式 - 支持Agent脱离框架独立运行", - "core.goal_planner": "目标规划器 - 将复杂目标分解为可执行步骤", - "core.plan_checker": "计划检查器 - 验证执行计划的完整性和可行性", - "core.plan_exec_engine": "计划执行引擎 - 执行分解后的计划步骤", - "core.plan_executor": "计划执行器 - 管理计划执行的完整流程", - "core.plan_schema": "计划Schema - 执行计划的数据结构定义", - "core.reflexion": "Reflexion引擎 - 自反思推理,通过自我评估改进输出", - "core.rewoo": "ReWOO引擎 - 无观察推理,先规划后执行的高效模式", - - "llm": "LLM网关模块 - 多Provider统一网关,支持OpenAI/Anthropic/Gemini/文心/豆包/元宝等", - "llm.gateway": "LLM网关 - 统一多Provider调用接口,支持路由、重试、流式输出", - "llm.protocol": "LLM协议定义 - 定义LLMProvider、LLMRequest、LLMResponse等接口", - "llm.config": "LLM配置 - 模型别名、Provider配置管理", - "llm.retry": "LLM重试策略 - 指数退避重试和错误处理", - "llm.providers": "LLM Provider实现 - 各大模型服务商的具体适配器", - "llm.providers.openai": "OpenAI Provider - 支持GPT-4/GPT-3.5等模型", - "llm.providers.anthropic": "Anthropic Provider - 支持Claude系列模型", - "llm.providers.gemini": "Gemini Provider - 支持Google Gemini模型", - "llm.providers.wenxin": "文心一言Provider - 支持百度文心大模型", - "llm.providers.doubao": "豆包Provider - 支持字节豆包大模型", - "llm.providers.yuanbao": "元宝Provider - 支持腾讯元宝大模型", - "llm.providers.tracker": "LLM调用追踪器 - 记录和统计LLM调用", - "llm.providers.usage_store": "LLM用量存储 - Token用量和成本追踪,支持InMemory和Redis后端", - "llm.cache": "LLM响应缓存 - 基于语义相似度的LLM响应缓存,减少重复调用", - "llm.cache_key": "缓存键生成 - LLM缓存键的计算和归一化", - - "chat": "聊天路由模块 - CostAwareRouter三层意图路由和语义路由", - "chat.skill_routing": "三层意图路由 - CostAwareRouter,正则→启发式→LLM分类逐层升级", - "chat.semantic_router": "语义路由 - 基于向量相似度的意图路由,支持语义匹配", - - "quality.cascade_detector": "级联检测器 - 检测Agent输出中的级联失败模式", - "quality.cascade_state_store": "级联状态存储 - 级联检测状态持久化,支持InMemory和Redis后端", - "quality.alignment": "对齐守卫 - 检测和修正Agent输出中的对齐偏差", - - "tools": "工具模块 - 提供Agent可调用的各类工具", - "tools.base": "工具基类 - 定义Tool接口和标准执行流程", - "tools.registry": "工具注册中心 - 管理工具的注册、发现、获取", - "tools.shell": "Shell工具 - 执行系统命令", - "tools.web_search": "Web搜索工具 - 执行网络搜索", - "tools.web_crawl": "Web爬取工具 - 爬取网页内容", - "tools.memory_tool": "记忆工具 - Agent记忆读写操作", - "tools.ask_human": "人工介入工具 - 请求人类输入", - "tools.schema_tools": "Schema工具 - JSON Schema相关操作", - "tools.function_tool": "函数工具 - 将Python函数包装为Tool", - "tools.agent_tool": "Agent工具 - 将Agent包装为可调用Tool", - "tools.mcp_tool": "MCP工具 - MCP协议工具适配器", - "tools.composition": "工具组合 - 支持工具链式组合", - "tools.baidu_search": "百度搜索工具 - 百度搜索引擎集成", - "tools.headroom_retrieve": "Headroom检索工具 - Headroom AI知识检索", - "tools.computer_use": "计算机使用工具 - 桌面操控工具,支持截图、点击、输入等操作", - "tools.computer_use_session": "计算机使用会话 - 桌面操控会话管理,支持云端和本地(pyautogui)模式", - "tools.computer_use_recorder": "计算机使用录制器 - 记录桌面操控动作序列", - "tools.pty_session": "PTY会话 - 伪终端会话管理", - "tools.terminal_session": "终端会话 - 终端模拟器会话", - "tools.output_parser": "输出解析器 - 解析Agent输出为结构化数据", - "tools.skill_install": "技能安装器 - 动态安装技能包", - - "memory": "记忆模块 - 多层记忆系统,支持工作记忆、情景记忆、语义记忆", - "memory.base": "记忆基类 - 定义Memory接口", - "memory.working": "工作记忆 - 基于Redis的短期工作记忆", - "memory.episodic": "情景记忆 - 基于向量数据库的长期情景记忆", - "memory.semantic": "语义记忆 - 基于RAG服务的语义知识检索", - "memory.profile": "用户画像 - 用户偏好和历史信息管理", - "memory.retriever": "记忆检索器 - 统一多层记忆检索接口", - "memory.embedder": "嵌入器 - 文本向量化,支持OpenAI Embedding", - "memory.models": "记忆数据模型 - Pydantic模型定义", - "memory.rag_loop": "RAG循环 - 检索增强生成的迭代循环", - "memory.query_transformer": "查询转换器 - 优化检索查询", - "memory.relevance_scorer": "相关性评分器 - 评估检索结果相关性", - "memory.contextual_retrieval": "上下文检索 - 基于上下文的检索增强", - "memory.http_rag": "HTTP RAG服务 - 远程RAG API客户端", - - "skills": "技能模块 - 定义可复用的Agent技能,包含意图、工具和质量门控", - "skills.base": "技能基类 - 定义Skill、SkillConfig、IntentConfig等", - "skills.registry": "技能注册中心 - 管理技能的注册、发现、获取", - "skills.loader": "技能加载器 - 从YAML配置加载技能定义", - "skills.pipeline": "技能Pipeline - 技能编排流程", - "skills.skill_md": "Markdown技能 - 从Markdown文档生成技能", - "skills.geo_pipeline": "GEO Pipeline - 地理信息处理Pipeline", - - "orchestrator": "编排模块 - Pipeline编排引擎,支持DAG工作流", - "orchestrator.pipeline_engine": "Pipeline引擎 - 执行DAG定义的工作流", - "orchestrator.pipeline_schema": "Pipeline Schema - Pipeline配置模型定义", - "orchestrator.pipeline_state": "Pipeline状态 - Pipeline执行状态管理", - "orchestrator.pipeline_models": "Pipeline模型 - Pipeline数据模型", - "orchestrator.pipeline_loader": "Pipeline加载器 - 从YAML加载Pipeline定义", - "orchestrator.reflection": "反思模块 - 执行后反思和改进", - "orchestrator.retry": "重试策略 - Pipeline步骤重试机制", - "orchestrator.compensation": "补偿机制 - Pipeline失败时的补偿操作", - "orchestrator.handoff": "Handoff - Agent间任务转交", - "orchestrator.dynamic_pipeline": "动态Pipeline - 运行时动态构建Pipeline", - - "router": "路由模块 - 意图路由,将用户输入匹配到对应技能", - "router.intent": "意图路由器 - 基于LLM的意图识别和路由", - - "quality": "质量模块 - 输出质量门控和标准化", - "quality.gate": "质量门控 - 检查Agent输出是否满足质量要求", - "quality.output": "输出标准化 - 统一Agent输出格式", - - "prompts": "Prompt模块 - Prompt模板和渲染", - "prompts.template": "Prompt模板 - 支持变量替换和Section组合", - "prompts.section": "Prompt Section - 定义Prompt的各组成部分", - - "bus": "消息总线模块 - Agent间异步通信", - "bus.protocol": "总线协议 - 定义消息总线接口", - "bus.message": "消息定义 - Agent间通信消息格式", - "bus.memory_bus": "内存消息总线 - 基于进程内队列的消息总线", - "bus.redis_bus": "Redis消息总线 - 基于Redis Pub/Sub的消息总线", - - "session": "会话模块 - 会话管理和持久化", - "session.manager": "会话管理器 - 管理对话会话的创建、获取、更新", - "session.store": "会话存储 - 会话数据的持久化存储", - "session.models": "会话模型 - 会话相关的数据模型", - - "server": "服务器模块 - FastAPI HTTP/WebSocket服务", - "server.app": "FastAPI应用 - 创建和配置FastAPI应用实例", - "server.config": "服务器配置 - 服务器运行参数配置", - "server.runner": "服务器运行器 - 启动和管理服务器进程", - "server.middleware": "中间件 - 请求处理中间件", - "server.client": "API客户端 - 服务端API客户端封装", - "server.client_config": "客户端配置 - API客户端配置管理", - "server.task_store": "任务存储 - 服务端任务状态存储", - "server.routes": "路由模块 - HTTP/WebSocket路由定义", - "server.routes.chat": "聊天路由 - 对话API端点", - "server.routes.ws": "WebSocket路由 - 实时通信端点", - "server.routes.tasks": "任务路由 - 任务管理API", - "server.routes.agents": "Agent路由 - Agent管理API", - "server.routes.skills": "技能路由 - 技能管理API,含@-mention建议端点", - "server.routes.memory": "记忆路由 - 记忆管理API", - "server.routes.llm": "LLM路由 - LLM配置和调用API", - "server.routes.health": "健康检查路由 - 服务健康状态端点", - "server.routes.metrics": "指标路由 - 运行指标API", - "server.routes.evolution": "进化路由 - Agent进化管理API", - "server.routes.evolution_dashboard": "进化仪表盘路由 - 进化数据可视化API", - "server.routes.kb_management": "知识库管理路由 - 文档上传/搜索/源配置API", - "server.routes.settings": "设置路由 - 系统配置管理API", - "server.routes.terminal": "终端路由 - PTY终端会话API", - "server.routes.workflows": "工作流路由 - Pipeline工作流管理API", - "server.routes.skill_management": "技能管理路由 - 技能CRUD操作API", - "server.routes.portal": "门户路由 - Web GUI入口和静态资源", - - "cli": "命令行模块 - CLI工具", - "cli.main": "CLI入口 - Typer应用主入口", - "cli.chat": "聊天命令 - 交互式对话命令", - "cli.init": "初始化命令 - 项目初始化", - "cli.onboarding": "引导命令 - 新用户引导流程", - "cli.skill": "技能命令 - 技能管理CLI", - "cli.task": "任务命令 - 任务提交和管理CLI", - "cli.pair": "配对命令 - Agent配对", - "cli.usage": "使用统计命令 - 使用情况统计", - "cli.templates": "模板命令 - Agent模板管理", - - "mcp": "MCP协议模块 - Model Context Protocol集成", - "mcp.client": "MCP客户端 - 连接MCP服务器", - "mcp.server": "MCP服务器 - 提供MCP服务", - "mcp.manager": "MCP管理器 - 管理MCP连接", - "mcp.transport": "MCP传输层 - MCP通信传输实现", - - "telemetry": "遥测模块 - 可观测性支持", - "telemetry.tracing": "分布式追踪 - OpenTelemetry追踪集成", - "telemetry.metrics": "指标收集 - 运行指标收集和导出", - "telemetry.setup": "遥测设置 - 初始化遥测组件", - - "evolution": "进化模块 - Agent自我进化能力", - "evolution.lifecycle": "进化生命周期 - EvolutionMixin,任务后触发进化", - "evolution.reflector": "反思器 - 分析任务执行结果,生成改进建议", - "evolution.llm_reflector": "LLM反思器 - 使用LLM进行深度反思", - "evolution.prompt_optimizer": "Prompt优化器 - 自动优化Agent Prompt", - "evolution.strategy_tuner": "策略调优器 - 调整Agent执行策略", - "evolution.genetic": "遗传算法 - 基于遗传算法的Prompt进化", - "evolution.fitness": "适应度评估 - 评估进化变体的质量", - "evolution.ab_tester": "A/B测试 - 对比测试不同进化变体", - "evolution.evolution_store": "进化存储 - 持久化进化历史", - "evolution.models": "进化模型 - 进化相关数据模型", - "evolution.experience_schema": "经验Schema - 经验数据结构定义", - "evolution.experience_store": "经验存储 - 成功/失败经验持久化", - "evolution.path_optimizer": "路径优化器 - 分析工具调用路径,推荐更优策略", - "evolution.pitfall_detector": "陷阱检测器 - 检测重复错误模式", - - "evaluation": "评估模块 - Agent输出质量评估", - "evaluation.ragas_evaluator": "RAGAS评估器 - 使用RAGAS框架评估RAG质量", - - "org": "组织发现模块 - 多Agent组织架构和协作发现", - "org.context": "组织上下文 - 组织级别的共享上下文管理", - "org.discovery": "组织发现 - Agent间能力发现和协作匹配", - - "marketplace": "多Agent市场模块 - Agent间的拍卖和财富分配", - "marketplace.auction": "拍卖机制 - Agent间的任务拍卖和竞价", - "marketplace.wealth": "财富管理 - Agent间的价值交换和分配", - - "configs": "配置模块 - Pipeline和技能YAML配置", - "configs.geo_server": "GEO服务器 - 地理信息HTTP服务", - "configs.geo_handlers": "GEO处理器 - 地理信息请求处理", - "configs.geo_tools": "GEO工具 - 地理信息相关工具定义", -} - - -def get_layer(file_path: str) -> str: - """Determine architecture layer from file path.""" - parts = file_path.replace("\\", "/").split("/") - # Check for configs/ prefix - if "configs" in parts: - return "utility" - # For src/agentkit/__init__.py and __main__.py, treat as service - if parts[-1] in ("__init__.py", "__main__.py") and len(parts) <= 4: - return "service" - for part in parts: - if part in LAYER_MAP: - return LAYER_MAP[part] - return "unknown" - - -def get_module_key(file_path: str) -> str: - """Get module key for summary lookup.""" - # Convert file path to module key - rel = file_path - if rel.startswith("src/agentkit/"): - rel = rel[len("src/agentkit/"):] - elif rel.startswith("configs/"): - rel = rel[len("configs/"):] - - # Remove __init__.py and .py suffix - rel = rel.replace("/__init__.py", "").replace(".py", "") - return rel - - -def get_file_summary(file_path: str, docstring: str = "") -> str: - """Get Chinese summary for a file.""" - # If we have a docstring, use it as base - if docstring: - # Clean up docstring - doc = docstring.strip().split("\n")[0].strip() - if doc: - return doc - - key = get_module_key(file_path) - # Try exact match first - if key in MODULE_SUMMARIES: - return MODULE_SUMMARIES[key] - # Try parent module - parts = key.split("/") - for i in range(len(parts) - 1, 0, -1): - parent_key = "/".join(parts[:i]) - if parent_key in MODULE_SUMMARIES: - return MODULE_SUMMARIES[parent_key] - return f"模块 {key}" - - -def estimate_complexity(node: ast.AST) -> str: - """Estimate complexity of an AST node.""" - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - # Count branches, loops, nested functions - complexity = 1 - for child in ast.walk(node): - if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)): - complexity += 1 - elif isinstance(child, (ast.And, ast.Or)): - complexity += 1 - if complexity <= 3: - return "simple" - elif complexity <= 8: - return "moderate" - return "complex" - elif isinstance(node, ast.ClassDef): - methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))] - if len(methods) <= 3: - return "simple" - elif len(methods) <= 8: - return "moderate" - return "complex" - return "simple" - - -def extract_class_info(node: ast.ClassDef, file_path: str) -> dict: - """Extract class information from AST node.""" - base_classes = [] - for base in node.bases: - if isinstance(base, ast.Name): - base_classes.append(base.id) - elif isinstance(base, ast.Attribute): - base_classes.append(ast.dump(base)) - - methods = [] - for item in node.body: - if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): - params = [arg.arg for arg in item.args.args if arg.arg != "self"] - methods.append({ - "name": item.name, - "params": params, - "is_async": isinstance(item, ast.AsyncFunctionDef), - }) - - # Extract class docstring - docstring = ast.get_docstring(node) or "" - - return { - "name": node.name, - "base_classes": base_classes, - "methods": methods, - "complexity": estimate_complexity(node), - "docstring": docstring, - } - - -def extract_function_info(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict: - """Extract function information from AST node.""" - params = [arg.arg for arg in node.args.args] - - return_type = "" - if node.returns: - if isinstance(node.returns, ast.Name): - return_type = node.returns.id - elif isinstance(node.returns, ast.Constant): - return_type = str(node.returns.value) - else: - return_type = ast.dump(node.returns) - - return { - "name": node.name, - "params": params, - "return_type": return_type, - "is_async": isinstance(node, ast.AsyncFunctionDef), - "complexity": estimate_complexity(node), - } - - -def extract_imports(tree: ast.AST, file_path: str) -> list[dict]: - """Extract import information from AST.""" - imports = [] - for node in ast.walk(tree): - if isinstance(node, ast.ImportFrom): - if node.module and (node.module.startswith("agentkit") or node.module.startswith("configs")): - for alias in node.names: - imports.append({ - "from_module": node.module, - "import_name": alias.name, - }) - elif isinstance(node, ast.Import): - for alias in node.names: - if alias.name.startswith("agentkit") or alias.name.startswith("configs"): - imports.append({ - "from_module": None, - "import_name": alias.name, - }) - return imports - - -def module_to_file_path(module: str) -> str: - """Convert Python module path to file path.""" - parts = module.split(".") - - # Handle agentkit modules - if module.startswith("agentkit"): - # Skip "agentkit" prefix, it's under src/ - sub_parts = parts[1:] # skip "agentkit" - if not sub_parts: - return "src/agentkit/__init__.py" - # Try as package __init__.py - init_path = PROJECT_ROOT / "src" / "agentkit" / "/".join(sub_parts) / "__init__.py" - if init_path.exists(): - return f"src/agentkit/{'/'.join(sub_parts)}/__init__.py" - # Try as module.py - mod_path = PROJECT_ROOT / "src" / "agentkit" / ("/".join(sub_parts) + ".py") - if mod_path.exists(): - return f"src/agentkit/{'/'.join(sub_parts)}.py" - - # Handle configs modules - if module.startswith("configs"): - sub_parts = parts[1:] # skip "configs" - if not sub_parts: - return "configs/__init__.py" - mod_path = PROJECT_ROOT / "configs" / ("/".join(sub_parts) + ".py") - if mod_path.exists(): - return f"configs/{'/'.join(sub_parts)}.py" - - return "" - - -def scan_file(file_path: Path) -> dict: - """Scan a single Python file and extract all information.""" - try: - source = file_path.read_text(encoding="utf-8") - tree = ast.parse(source) - except (SyntaxError, UnicodeDecodeError): - return {"classes": [], "functions": [], "imports": [], "top_level_functions": [], "docstring": ""} - - rel_path = str(file_path.relative_to(PROJECT_ROOT)) - - # Extract module docstring - docstring = ast.get_docstring(tree) or "" - - classes = [] - functions = [] - top_level_functions = [] - - for node in ast.iter_child_nodes(tree): - if isinstance(node, ast.ClassDef): - classes.append(extract_class_info(node, rel_path)) - elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - func_info = extract_function_info(node) - functions.append(func_info) - top_level_functions.append(func_info) - - imports = extract_imports(tree, rel_path) - - return { - "classes": classes, - "functions": top_level_functions, - "imports": imports, - "rel_path": rel_path, - "docstring": docstring, - } - - -def build_knowledge_graph(): - """Build the complete knowledge graph.""" - # Collect all Python files - py_files = [] - for scan_dir in SCAN_DIRS: - if scan_dir.exists(): - for py_file in scan_dir.rglob("*.py"): - py_files.append(py_file) - - print(f"Found {len(py_files)} Python files to scan") - - # Scan all files - file_data = {} - for py_file in sorted(py_files): - data = scan_file(py_file) - rel_path = data["rel_path"] - file_data[rel_path] = data - - # Build nodes and edges - nodes = [] - edges = [] - - # Track all node IDs for edge building - file_node_ids = {} - class_node_ids = {} - func_node_ids = {} - - # 1. Create file nodes - for rel_path, data in file_data.items(): - node_id = f"file:{rel_path}" - layer = get_layer(rel_path) - summary = get_file_summary(rel_path, data.get("docstring", "")) - - tags = [] - parts = rel_path.replace("\\", "/").split("/") - for p in parts: - if p not in ("src", "agentkit", "__init__.py") and not p.endswith(".py"): - tags.append(p) - - nodes.append({ - "id": node_id, - "type": "file", - "name": rel_path.split("/")[-1], - "filePath": rel_path, - "layer": layer, - "summary": summary, - "tags": tags, - "complexity": "moderate" if data["classes"] or data["functions"] else "simple", - }) - file_node_ids[rel_path] = node_id - - # 2. Create class nodes - for rel_path, data in file_data.items(): - for cls in data["classes"]: - class_id = f"class:{cls['name']}" - layer = get_layer(rel_path) - - method_names = [m["name"] for m in cls["methods"]] - # Use docstring for summary if available - docstring = cls.get("docstring", "") - if docstring: - # Take first line of docstring - summary = docstring.strip().split("\n")[0].strip() - else: - summary = f"{cls['name']}类" - if cls["base_classes"]: - summary += f",继承自{', '.join(cls['base_classes'])}" - if method_names: - summary += f",包含方法: {', '.join(method_names[:5])}" - if len(method_names) > 5: - summary += f" 等{len(method_names)}个方法" - - nodes.append({ - "id": class_id, - "type": "class", - "name": cls["name"], - "filePath": rel_path, - "layer": layer, - "summary": summary, - "tags": [cls["name"]], - "complexity": cls["complexity"], - }) - class_node_ids[cls["name"]] = class_id - - # Edge: file contains class - edges.append({ - "id": f"edge:{uuid.uuid4().hex[:8]}", - "source": file_node_ids[rel_path], - "target": class_id, - "type": "contains", - "label": f"定义类 {cls['name']}", - }) - - # Edge: class extends base classes - for base in cls["base_classes"]: - if base in class_node_ids: - edges.append({ - "id": f"edge:{uuid.uuid4().hex[:8]}", - "source": class_id, - "target": class_node_ids[base], - "type": "extends", - "label": f"继承 {base}", - }) - - # 3. Create method nodes - for method in cls["methods"]: - method_id = f"func:{cls['name']}.{method['name']}" - async_tag = "异步" if method["is_async"] else "" - summary = f"{cls['name']}.{method['name']}({', '.join(method['params'])}) {async_tag}方法" - - nodes.append({ - "id": method_id, - "type": "function", - "name": method["name"], - "filePath": rel_path, - "layer": layer, - "summary": summary, - "tags": [cls["name"], method["name"]], - "complexity": "simple", - }) - func_node_ids[f"{cls['name']}.{method['name']}"] = method_id - - # Edge: class contains method - edges.append({ - "id": f"edge:{uuid.uuid4().hex[:8]}", - "source": class_id, - "target": method_id, - "type": "contains", - "label": f"方法 {method['name']}", - }) - - # 4. Create top-level function nodes - for rel_path, data in file_data.items(): - for func in data["functions"]: - func_id = f"func:{func['name']}" - async_tag = "异步" if func["is_async"] else "" - summary = f"{func['name']}({', '.join(func['params'])}) {async_tag}函数" - if func["return_type"]: - summary += f" → {func['return_type']}" - - nodes.append({ - "id": func_id, - "type": "function", - "name": func["name"], - "filePath": rel_path, - "layer": get_layer(rel_path), - "summary": summary, - "tags": [func["name"]], - "complexity": func["complexity"], - }) - func_node_ids[func["name"]] = func_id - - # Edge: file contains function - edges.append({ - "id": f"edge:{uuid.uuid4().hex[:8]}", - "source": file_node_ids[rel_path], - "target": func_id, - "type": "contains", - "label": f"定义函数 {func['name']}", - }) - - # 5. Create import edges - for rel_path, data in file_data.items(): - for imp in data["imports"]: - if imp["from_module"]: - target_path = module_to_file_path(imp["from_module"]) - if target_path and target_path in file_node_ids: - edges.append({ - "id": f"edge:{uuid.uuid4().hex[:8]}", - "source": file_node_ids[rel_path], - "target": file_node_ids[target_path], - "type": "imports", - "label": f"导入 {imp['import_name']}", - }) - - # 6. Build tours - tours = build_tours(file_data, file_node_ids, class_node_ids, func_node_ids) - - # Get git commit hash - git_hash = "d9d1b16e5911ad958cd8ae38958058bea13f3fcc" - - # Build final JSON - graph = { - "version": "1.0.0", - "project": { - "name": "Fischer AgentKit", - "languages": ["python"], - "frameworks": ["FastAPI", "Pydantic", "SQLAlchemy", "Typer", "Redis"], - "description": "AI驱动的Agent框架,支持ReAct引擎、多LLM网关、Pipeline编排、自适应反思和消息总线", - "analyzedAt": datetime.now(timezone.utc).isoformat(), - "gitCommitHash": git_hash, - }, - "nodes": nodes, - "edges": edges, - "tours": tours, - } - - return graph - - -def build_tours(file_data, file_node_ids, class_node_ids, func_node_ids): - """Build guided learning tours.""" - tours = [] - - # Tour 1: Entry Points - tours.append({ - "id": "tour:entry-points", - "name": "入口点导览", - "description": "从项目入口开始,了解如何启动和使用AgentKit", - "steps": [ - {"nodeId": "file:src/agentkit/__main__.py", "why": "Python模块入口,python -m agentkit"}, - {"nodeId": "file:src/agentkit/__init__.py", "why": "包入口,导出核心公共API"}, - {"nodeId": "file:src/agentkit/cli/main.py", "why": "CLI主入口,Typer应用定义"}, - {"nodeId": "file:src/agentkit/server/app.py", "why": "HTTP服务入口,FastAPI应用创建"}, - ], - }) - - # Tour 2: Core Agent Lifecycle - tours.append({ - "id": "tour:agent-lifecycle", - "name": "Agent生命周期导览", - "description": "深入理解Agent从创建到执行任务的完整生命周期", - "steps": [ - {"nodeId": "class:BaseAgent", "why": "Agent基类,定义标准生命周期和可插拔能力"}, - {"nodeId": "func:BaseAgent.start", "why": "Agent启动流程:连接Redis→注册→心跳→监听"}, - {"nodeId": "func:BaseAgent.execute", "why": "任务执行框架方法:on_task_start→handle_task→quality_gate→on_task_complete"}, - {"nodeId": "func:BaseAgent.handle_task", "why": "抽象方法,子类实现业务逻辑"}, - {"nodeId": "class:ConfigDrivenAgent", "why": "配置驱动Agent,从YAML自动组装"}, - {"nodeId": "func:ConfigDrivenAgent.handle_task", "why": "根据execution_mode路由到react/direct/custom模式"}, - {"nodeId": "class:AgentConfig", "why": "Agent配置模型,支持YAML/Dict构建"}, - ], - }) - - # Tour 3: ReAct Engine - tours.append({ - "id": "tour:react-engine", - "name": "ReAct引擎导览", - "description": "理解ReAct推理-行动循环的核心实现", - "steps": [ - {"nodeId": "class:ReActEngine", "why": "ReAct引擎核心,Think→Act→Observe循环"}, - {"nodeId": "func:ReActEngine.execute", "why": "执行ReAct循环,支持超时和取消"}, - {"nodeId": "func:ReActEngine.execute_stream", "why": "流式执行,逐步yield事件"}, - {"nodeId": "func:ReActEngine._execute_tool", "why": "工具调用执行,处理成功和失败"}, - {"nodeId": "func:ReActEngine._parse_text_tool_calls", "why": "文本解析模式,支持Action和代码块格式"}, - {"nodeId": "class:ReActStep", "why": "单步记录数据结构"}, - {"nodeId": "class:ReActResult", "why": "ReAct执行结果数据结构"}, - {"nodeId": "class:ReActEvent", "why": "流式执行事件数据结构"}, - ], - }) - - # Tour 4: LLM Gateway - tours.append({ - "id": "tour:llm-gateway", - "name": "LLM网关导览", - "description": "了解多Provider统一网关的设计和实现", - "steps": [ - {"nodeId": "class:LLMGateway", "why": "LLM网关核心,统一多Provider调用接口"}, - {"nodeId": "file:src/agentkit/llm/protocol.py", "why": "LLM协议定义,LLMProvider/LLMRequest/LLMResponse"}, - {"nodeId": "file:src/agentkit/llm/config.py", "why": "模型别名和Provider配置"}, - {"nodeId": "file:src/agentkit/llm/providers/openai.py", "why": "OpenAI Provider实现"}, - {"nodeId": "file:src/agentkit/llm/providers/anthropic.py", "why": "Anthropic Provider实现"}, - {"nodeId": "file:src/agentkit/llm/retry.py", "why": "LLM重试策略"}, - ], - }) - - # Tour 5: Memory System - tours.append({ - "id": "tour:memory-system", - "name": "记忆系统导览", - "description": "理解多层记忆系统的架构和实现", - "steps": [ - {"nodeId": "file:src/agentkit/memory/base.py", "why": "记忆基类接口定义"}, - {"nodeId": "file:src/agentkit/memory/retriever.py", "why": "统一记忆检索器,整合工作/情景/语义记忆"}, - {"nodeId": "file:src/agentkit/memory/working.py", "why": "工作记忆 - 基于Redis的短期记忆"}, - {"nodeId": "file:src/agentkit/memory/episodic.py", "why": "情景记忆 - 基于向量的长期记忆"}, - {"nodeId": "file:src/agentkit/memory/semantic.py", "why": "语义记忆 - RAG服务集成"}, - {"nodeId": "file:src/agentkit/memory/embedder.py", "why": "文本向量化嵌入器"}, - ], - }) - - # Tour 6: Orchestration - tours.append({ - "id": "tour:orchestration", - "name": "编排系统导览", - "description": "了解多Agent协作编排和Pipeline引擎", - "steps": [ - {"nodeId": "class:Orchestrator", "why": "多Agent协作编排器,Orchestrator-Worker模式"}, - {"nodeId": "func:Orchestrator.execute", "why": "编排执行:分解→执行→汇总"}, - {"nodeId": "func:Orchestrator.execute_adaptive", "why": "自适应编排:执行→评估→再分解循环"}, - {"nodeId": "file:src/agentkit/orchestrator/pipeline_engine.py", "why": "Pipeline引擎,执行DAG工作流"}, - {"nodeId": "file:src/agentkit/orchestrator/pipeline_schema.py", "why": "Pipeline配置模型"}, - {"nodeId": "file:src/agentkit/orchestrator/reflection.py", "why": "执行后反思模块"}, - ], - }) - - # Tour 7: Skills & Router - tours.append({ - "id": "tour:skills-router", - "name": "技能与路由导览", - "description": "了解技能定义、注册和意图路由机制", - "steps": [ - {"nodeId": "file:src/agentkit/skills/base.py", "why": "技能基类和配置定义"}, - {"nodeId": "class:SkillRegistry", "why": "技能注册中心"}, - {"nodeId": "file:src/agentkit/skills/loader.py", "why": "从YAML加载技能定义"}, - {"nodeId": "class:IntentRouter", "why": "意图路由器,匹配用户输入到技能"}, - {"nodeId": "file:src/agentkit/router/intent.py", "why": "意图路由实现"}, - ], - }) - - # Tour 8: Evolution - tours.append({ - "id": "tour:evolution", - "name": "进化系统导览", - "description": "了解Agent自我进化的机制和实现", - "steps": [ - {"nodeId": "file:src/agentkit/evolution/lifecycle.py", "why": "进化生命周期Mixin"}, - {"nodeId": "file:src/agentkit/evolution/reflector.py", "why": "反思器 - 分析结果生成改进建议"}, - {"nodeId": "file:src/agentkit/evolution/prompt_optimizer.py", "why": "Prompt自动优化"}, - {"nodeId": "file:src/agentkit/evolution/genetic.py", "why": "遗传算法进化"}, - {"nodeId": "file:src/agentkit/evolution/ab_tester.py", "why": "A/B测试对比"}, - ], - }) - - # Tour 9: Infrastructure - tours.append({ - "id": "tour:infrastructure", - "name": "基础设施导览", - "description": "了解消息总线、会话管理、遥测等基础设施", - "steps": [ - {"nodeId": "file:src/agentkit/bus/protocol.py", "why": "消息总线协议接口"}, - {"nodeId": "file:src/agentkit/bus/redis_bus.py", "why": "Redis Pub/Sub消息总线"}, - {"nodeId": "file:src/agentkit/bus/memory_bus.py", "why": "进程内消息总线"}, - {"nodeId": "file:src/agentkit/session/manager.py", "why": "会话管理器"}, - {"nodeId": "file:src/agentkit/telemetry/tracing.py", "why": "OpenTelemetry追踪集成"}, - {"nodeId": "file:src/agentkit/telemetry/metrics.py", "why": "运行指标收集"}, - ], - }) - - return tours - - -def main(): - """Main entry point.""" - print("Building knowledge graph for Fischer AgentKit...") - - graph = build_knowledge_graph() - - # Ensure output directory exists - OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) - - # Write JSON - with open(OUTPUT_PATH, "w", encoding="utf-8") as f: - json.dump(graph, f, ensure_ascii=False, indent=2) - - print(f"Knowledge graph written to {OUTPUT_PATH}") - print(f" Nodes: {len(graph['nodes'])}") - print(f" Edges: {len(graph['edges'])}") - print(f" Tours: {len(graph['tours'])}") - - # Print layer statistics - layer_counts = {} - for node in graph["nodes"]: - layer = node["layer"] - layer_counts[layer] = layer_counts.get(layer, 0) + 1 - - print("\nLayer distribution:") - for layer, count in sorted(layer_counts.items()): - print(f" {layer}: {count} nodes") - - # Print type statistics - type_counts = {} - for node in graph["nodes"]: - t = node["type"] - type_counts[t] = type_counts.get(t, 0) + 1 - - print("\nNode type distribution:") - for t, count in sorted(type_counts.items()): - print(f" {t}: {count} nodes") - - -if __name__ == "__main__": - main() diff --git a/.understand-anything/knowledge-graph.json b/.understand-anything/knowledge-graph.json index b06890d..c4192cf 100644 --- a/.understand-anything/knowledge-graph.json +++ b/.understand-anything/knowledge-graph.json @@ -13,8 +13,8 @@ "Redis" ], "description": "AI驱动的Agent框架,支持ReAct引擎、多LLM网关、Pipeline编排、自适应反思和消息总线", - "analyzedAt": "2026-06-15T06:01:34.043135+00:00", - "gitCommitHash": "d9d1b16e5911ad958cd8ae38958058bea13f3fcc" + "analyzedAt": "2026-06-17T06:30:20.584048+00:00", + "gitCommitHash": "840d1af" }, "nodes": [ { @@ -47,7 +47,7 @@ "name": "geo_server.py", "filePath": "configs/geo_server.py", "layer": "utility", - "summary": "GEO AgentKit Server 启动入口", + "summary": "GEO AgentKit Server 启动入口:使用 find_config_path + load_config_with_dotenv 统一配置加载", "tags": [ "configs" ], @@ -199,9 +199,10 @@ "name": "chat.py", "filePath": "src/agentkit/cli/chat.py", "layer": "api", - "summary": "Chat command — interactive terminal chat with an Agent.", + "summary": "Chat 命令:交互式终端聊天,使用 find_config_path + load_config_with_dotenv 统一配置加载链", "tags": [ - "cli" + "cli", + "chat" ], "complexity": "moderate" }, @@ -223,7 +224,7 @@ "name": "main.py", "filePath": "src/agentkit/cli/main.py", "layer": "api", - "summary": "AgentKit CLI main entry point", + "summary": "AgentKit CLI 主入口:gui/serve 命令使用 find_config_path + load_config_with_dotenv + has_llm_provider 统一配置加载", "tags": [ "cli" ], @@ -235,11 +236,12 @@ "name": "onboarding.py", "filePath": "src/agentkit/cli/onboarding.py", "layer": "api", - "summary": "Onboarding flow — interactive first-time configuration wizard.", + "summary": "Onboarding 交互式首次配置向导:merge-update 模式(不覆盖已有配置)、needs_onboarding() 检查 has_llm_provider()、bailian-coding provider 预设", "tags": [ - "cli" + "cli", + "onboarding" ], - "complexity": "moderate" + "complexity": "complex" }, { "id": "file:src/agentkit/cli/pair.py", @@ -271,7 +273,7 @@ "name": "task.py", "filePath": "src/agentkit/cli/task.py", "layer": "api", - "summary": "Task management CLI commands", + "summary": "Task 管理 CLI 命令:本地模式使用 find_config_path + load_config_with_dotenv 加载配置", "tags": [ "cli" ], @@ -283,9 +285,10 @@ "name": "templates.py", "filePath": "src/agentkit/cli/templates.py", "layer": "api", - "summary": "Template files for agentkit init", + "summary": "agentkit init 模板文件:bailian-coding 默认 provider、docker-compose 含 postgres+pgvector", "tags": [ - "cli" + "cli", + "templates" ], "complexity": "simple" }, @@ -1701,9 +1704,11 @@ "name": "client_config.py", "filePath": "src/agentkit/server/client_config.py", "layer": "api", - "summary": "Client-specific configuration with priority over defaults", + "summary": "客户端配置管理:clients.yaml 使用 _deep_resolve 解析 ${VAR} 环境变量引用", "tags": [ - "server" + "server", + "config", + "dotenv" ], "complexity": "moderate" }, @@ -1843,12 +1848,14 @@ "name": "settings.py", "filePath": "src/agentkit/server/routes/settings.py", "layer": "api", - "summary": "Settings API routes with config hot-reload support.", + "summary": "Settings API 路由:LLM/Skills/KB/General 配置的 CRUD,支持 ${VAR} 反向解析保留、.env 写入 API Key、ruamel.yaml 保留注释", "tags": [ "server", - "routes" + "routes", + "settings", + "dotenv" ], - "complexity": "moderate" + "complexity": "complex" }, { "id": "file:src/agentkit/server/routes/skill_management.py", @@ -11141,7 +11148,7 @@ "name": "chat", "filePath": "src/agentkit/llm/gateway.py", "layer": "utility", - "summary": "LLMGateway.chat(messages, model, agent_name, task_type, tools, tool_choice) 异步方法", + "summary": "LLMGateway.chat(messages, model, agent_name, task_type, tools, tool_choice, timeout) 异步方法 - 支持超时透传和空响应检测", "tags": [ "LLMGateway", "chat" @@ -11154,7 +11161,7 @@ "name": "chat_stream", "filePath": "src/agentkit/llm/gateway.py", "layer": "utility", - "summary": "LLMGateway.chat_stream(messages, model, agent_name, task_type, tools, tool_choice) 异步方法", + "summary": "LLMGateway.chat_stream(messages, model, agent_name, task_type, tools, tool_choice, timeout) 异步方法 - 支持超时透传和空流检测", "tags": [ "LLMGateway", "chat_stream" @@ -11282,7 +11289,7 @@ "name": "LLMRequest", "filePath": "src/agentkit/llm/protocol.py", "layer": "utility", - "summary": "LLM 请求", + "summary": "LLM 请求(含 timeout 超时控制)", "tags": [ "LLMRequest" ], @@ -11294,7 +11301,7 @@ "name": "__init__", "filePath": "src/agentkit/llm/protocol.py", "layer": "utility", - "summary": "LLMRequest.__init__(messages, model, tools, tool_choice, temperature, max_tokens) 方法", + "summary": "LLMRequest.__init__(messages, model, tools, tool_choice, temperature, max_tokens, timeout) 方法", "tags": [ "LLMRequest", "__init__" @@ -27512,18 +27519,6 @@ ], "complexity": "moderate" }, - { - "id": "func:_load_dotenv", - "type": "function", - "name": "_load_dotenv", - "filePath": "src/agentkit/cli/chat.py", - "layer": "api", - "summary": "_load_dotenv(dotenv_path) 函数 → None", - "tags": [ - "_load_dotenv" - ], - "complexity": "moderate" - }, { "id": "func:_print_help", "type": "function", @@ -27614,7 +27609,7 @@ "name": "needs_onboarding", "filePath": "src/agentkit/cli/onboarding.py", "layer": "api", - "summary": "needs_onboarding(config_arg) 函数 → bool", + "summary": "needs_onboarding(config_arg) → bool: 检查是否需要 onboarding,使用 load_config_with_dotenv 加载配置并检查 has_llm_provider()", "tags": [ "needs_onboarding" ], @@ -27626,7 +27621,7 @@ "name": "run_onboarding", "filePath": "src/agentkit/cli/onboarding.py", "layer": "api", - "summary": "run_onboarding(output_dir, config_arg) 函数 → BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))", + "summary": "run_onboarding(output_dir, config_arg) → str | None: 交互式配置向导,merge-update 模式(仅更新 LLM section,保留其他配置),bailian-coding provider 预设", "tags": [ "run_onboarding" ], @@ -28946,11 +28941,11 @@ "name": "_write_yaml_config", "filePath": "src/agentkit/server/routes/settings.py", "layer": "api", - "summary": "_write_yaml_config(config_path, data) 函数 → None", + "summary": "_write_yaml_config(config_path, data) → None: 写回 YAML 配置,使用 _reverse_resolve_env 保留 ${VAR} 引用,ruamel.yaml 保留注释格式", "tags": [ "_write_yaml_config" ], - "complexity": "simple" + "complexity": "moderate" }, { "id": "func:_get_config_path", @@ -28982,7 +28977,7 @@ "name": "update_llm_settings", "filePath": "src/agentkit/server/routes/settings.py", "layer": "api", - "summary": "update_llm_settings(request, update) 异步函数", + "summary": "update_llm_settings(request, update) 异步函数: 更新 LLM 配置,明文 API Key 写入 .env(_write_env_var),YAML 中保留 ${VAR} 引用", "tags": [ "update_llm_settings" ], @@ -30396,11 +30391,12 @@ "id": "file:src/agentkit/server/app.py", "type": "file", "name": "app.py", - "summary": "FastAPI 应用工厂,初始化全部组件", + "summary": "FastAPI 应用工厂:初始化全部组件,create_app 自动加载 .env + ServerConfig.from_yaml,支持配置热重载", "tags": [ "server", "app", - "factory" + "factory", + "dotenv" ], "filePath": "src/agentkit/server/app.py" }, @@ -30408,10 +30404,11 @@ "id": "file:src/agentkit/server/config.py", "type": "file", "name": "config.py", - "summary": "服务器配置加载器,从 agentkit.yaml 加载配置", + "summary": "服务器配置加载器:统一配置加载链 find_config_path() → load_config_with_dotenv() → load_dotenv() + ServerConfig.from_yaml(),含 .env 白名单安全加载、${VAR} 环境变量解析、配置热重载", "tags": [ "server", - "config" + "config", + "dotenv" ], "filePath": "src/agentkit/server/config.py" }, @@ -30419,7 +30416,7 @@ "id": "class:src/agentkit/server/config.py:ServerConfig", "type": "class", "name": "ServerConfig", - "summary": "服务器配置主类", + "summary": "服务器配置主类,支持 from_yaml 加载、has_llm_provider() 检测有效 LLM 配置、watch_config 热重载", "tags": [ "server", "config" @@ -30540,6 +30537,235 @@ "team" ], "filePath": "tests/unit/experts/test_team.py" + }, + { + "id": "func:_resolve_env_vars", + "type": "function", + "name": "_resolve_env_vars", + "filePath": "src/agentkit/server/config.py", + "layer": "service", + "summary": "解析字符串中的 ${VAR} 和 ${VAR:-default} 环境变量引用", + "tags": [ + "config", + "env-vars" + ], + "complexity": "simple" + }, + { + "id": "func:_deep_resolve", + "type": "function", + "name": "_deep_resolve", + "filePath": "src/agentkit/server/config.py", + "layer": "service", + "summary": "递归解析嵌套 dict/list 中的 ${VAR} 环境变量引用", + "tags": [ + "config", + "env-vars" + ], + "complexity": "simple" + }, + { + "id": "func:load_dotenv", + "type": "function", + "name": "load_dotenv", + "filePath": "src/agentkit/server/config.py", + "layer": "service", + "summary": "从 .env 文件加载环境变量,支持白名单前缀/精确名称过滤,不覆盖已存在的环境变量", + "tags": [ + "config", + "dotenv", + "security" + ], + "complexity": "moderate" + }, + { + "id": "func:load_config_with_dotenv", + "type": "function", + "name": "load_config_with_dotenv", + "filePath": "src/agentkit/server/config.py", + "layer": "service", + "summary": "生产级配置加载入口:先 load_dotenv() 再 ServerConfig.from_yaml(),所有 CLI 和 app factory 的统一加载链", + "tags": [ + "config", + "dotenv" + ], + "complexity": "simple" + }, + { + "id": "func:find_config_path", + "type": "function", + "name": "find_config_path", + "filePath": "src/agentkit/server/config.py", + "layer": "service", + "summary": "查找 agentkit.yaml 配置文件路径:--config 参数 > ./agentkit.yaml > ~/.agentkit/agentkit.yaml", + "tags": [ + "config" + ], + "complexity": "simple" + }, + { + "id": "func:has_llm_provider", + "type": "function", + "name": "has_llm_provider", + "filePath": "src/agentkit/server/config.py", + "layer": "service", + "summary": "ServerConfig 方法:检查是否配置了有效的 LLM Provider(API Key 已解析且非 ${VAR} 占位符)", + "tags": [ + "config", + "llm" + ], + "complexity": "simple" + }, + { + "id": "func:_reverse_resolve_env", + "type": "function", + "name": "_reverse_resolve_env", + "filePath": "src/agentkit/server/routes/settings.py", + "layer": "api", + "summary": "反向解析环境变量引用:若原始 YAML 含 ${VAR} 且当前值匹配 os.environ[VAR],保留 ${VAR} 引用而非写入明文", + "tags": [ + "settings", + "env-vars", + "security" + ], + "complexity": "moderate" + }, + { + "id": "func:_write_env_var", + "type": "function", + "name": "_write_env_var", + "filePath": "src/agentkit/server/routes/settings.py", + "layer": "api", + "summary": "将 API Key 写入 .env 文件(配置文件同级目录),更新已有行或追加新行,同时设置 os.environ", + "tags": [ + "settings", + "dotenv", + "security" + ], + "complexity": "moderate" + }, + { + "id": "func:_deep_update_ruamel", + "type": "function", + "name": "_deep_update_ruamel", + "filePath": "src/agentkit/server/routes/settings.py", + "layer": "api", + "summary": "深度更新 ruamel.yaml CommentedMap,保留 YAML 注释和格式", + "tags": [ + "settings", + "yaml" + ], + "complexity": "moderate" + }, + { + "id": "file:tests/unit/server/test_settings_routes.py", + "type": "file", + "name": "test_settings_routes.py", + "filePath": "tests/unit/server/test_settings_routes.py", + "layer": "test", + "summary": "Settings API 路由单元测试:覆盖 LLM/Skills/KB/General 配置 CRUD、${VAR} 反向解析保留、_write_env_var .env 写入", + "tags": [ + "test", + "settings", + "server" + ], + "complexity": "moderate" + }, + { + "id": "file:tests/unit/test_cli.py", + "type": "file", + "name": "test_cli.py", + "filePath": "tests/unit/test_cli.py", + "layer": "test", + "summary": "CLI 命令单元测试:version/doctor/init 命令、onboarding 配置加载链", + "tags": [ + "test", + "cli" + ], + "complexity": "moderate" + }, + { + "id": "file:src/agentkit/cli/benchmark.py", + "type": "file", + "name": "benchmark.py", + "filePath": "src/agentkit/cli/benchmark.py", + "layer": "api", + "summary": "Benchmark CLI - 标准化能力基准测试,支持 Mock/LLM/GUI 三种模式,输出 Accuracy/Precision/Recall/F1/Latency 指标", + "tags": [ + "benchmark", + "cli", + "testing" + ], + "complexity": "complex" + }, + { + "id": "func:benchmark", + "type": "function", + "name": "benchmark", + "filePath": "src/agentkit/cli/benchmark.py", + "layer": "api", + "summary": "benchmark(dimension, mode, report, runs, fast, verbose) CLI 主命令 - 支持流式关键词检测、难度分级超时、WebSocket 协议修正、延迟统计排除 timeout 用例", + "tags": [ + "benchmark", + "cli", + "main" + ], + "complexity": "complex" + }, + { + "id": "func:_execute_llm_reasoning_task", + "type": "function", + "name": "_execute_llm_reasoning_task", + "filePath": "src/agentkit/cli/benchmark.py", + "layer": "api", + "summary": "LLM 推理任务执行 - 使用流式响应 + 关键词提前退出 + 难度分级超时 (easy=20s, medium=40s, hard=60s)", + "tags": [ + "benchmark", + "llm", + "streaming" + ], + "complexity": "moderate" + }, + { + "id": "func:_run_gui_integration", + "type": "function", + "name": "_run_gui_integration", + "filePath": "src/agentkit/cli/benchmark.py", + "layer": "api", + "summary": "GUI 集成测试 - 直接 WebSocket 连接测试,connected 消息作为通过标准", + "tags": [ + "benchmark", + "gui", + "websocket" + ], + "complexity": "moderate" + }, + { + "id": "func:_compute_metrics", + "type": "function", + "name": "_compute_metrics", + "filePath": "src/agentkit/cli/benchmark.py", + "layer": "api", + "summary": "计算聚合指标 - 支持 exclude_latency_tags 参数排除特定用例的延迟统计", + "tags": [ + "benchmark", + "metrics" + ], + "complexity": "moderate" + }, + { + "id": "document:docs/plans/2026-06-17-001-fix-benchmark-failures-root-cause-plan.md", + "type": "document", + "name": "2026-06-17-001-fix-benchmark-failures-root-cause-plan.md", + "filePath": "docs/plans/2026-06-17-001-fix-benchmark-failures-root-cause-plan.md", + "layer": "document", + "summary": "Benchmark 测试失败根因修复计划 - 修复 3 个失败项:LLM 超时(流式+分级超时)、WebSocket(端点+协议修正)、延迟统计(排除 timeout 用例)", + "tags": [ + "plan", + "benchmark", + "fix" + ], + "complexity": "moderate" } ], "edges": [ @@ -44718,13 +44944,6 @@ "type": "contains", "label": "定义函数 _resolve_default_model" }, - { - "id": "edge:a1b49a61", - "source": "file:src/agentkit/cli/chat.py", - "target": "func:_load_dotenv", - "type": "contains", - "label": "定义函数 _load_dotenv" - }, { "id": "edge:6d9737a9", "source": "file:src/agentkit/cli/chat.py", @@ -52368,6 +52587,265 @@ "target": "class:src/agentkit/server/config.py:ServerConfig", "type": "contains", "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/config.py", + "target": "func:_resolve_env_vars", + "type": "contains", + "label": "定义函数 _resolve_env_vars", + "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/config.py", + "target": "func:_deep_resolve", + "type": "contains", + "label": "定义函数 _deep_resolve", + "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/config.py", + "target": "func:load_dotenv", + "type": "contains", + "label": "定义函数 load_dotenv", + "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/config.py", + "target": "func:load_config_with_dotenv", + "type": "contains", + "label": "定义函数 load_config_with_dotenv", + "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/config.py", + "target": "func:find_config_path", + "type": "contains", + "label": "定义函数 find_config_path", + "weight": 1.0 + }, + { + "source": "class:src/agentkit/server/config.py:ServerConfig", + "target": "func:has_llm_provider", + "type": "contains", + "label": "方法 has_llm_provider", + "weight": 1.0 + }, + { + "source": "func:load_config_with_dotenv", + "target": "func:load_dotenv", + "type": "calls", + "label": "调用 load_dotenv", + "weight": 0.8 + }, + { + "source": "func:load_config_with_dotenv", + "target": "class:src/agentkit/server/config.py:ServerConfig", + "type": "calls", + "label": "调用 ServerConfig.from_yaml", + "weight": 0.8 + }, + { + "source": "file:src/agentkit/server/routes/settings.py", + "target": "func:_reverse_resolve_env", + "type": "contains", + "label": "定义函数 _reverse_resolve_env", + "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/routes/settings.py", + "target": "func:_write_env_var", + "type": "contains", + "label": "定义函数 _write_env_var", + "weight": 1.0 + }, + { + "source": "file:src/agentkit/server/routes/settings.py", + "target": "func:_deep_update_ruamel", + "type": "contains", + "label": "定义函数 _deep_update_ruamel", + "weight": 1.0 + }, + { + "source": "func:_write_yaml_config", + "target": "func:_reverse_resolve_env", + "type": "calls", + "label": "调用 _reverse_resolve_env 保留 ${VAR} 引用", + "weight": 0.8 + }, + { + "source": "func:_write_yaml_config", + "target": "func:_deep_update_ruamel", + "type": "calls", + "label": "调用 _deep_update_ruamel 保留 YAML 注释", + "weight": 0.8 + }, + { + "source": "func:update_llm_settings", + "target": "func:_write_env_var", + "type": "calls", + "label": "调用 _write_env_var 写入 API Key 到 .env", + "weight": 0.8 + }, + { + "source": "file:src/agentkit/server/client_config.py", + "target": "func:_deep_resolve", + "type": "imports", + "label": "导入 _deep_resolve 解析 clients.yaml 中的 ${VAR}", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/chat.py", + "target": "func:find_config_path", + "type": "calls", + "label": "调用 find_config_path 查找配置文件", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/chat.py", + "target": "func:load_config_with_dotenv", + "type": "calls", + "label": "调用 load_config_with_dotenv 加载配置", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/main.py", + "target": "func:find_config_path", + "type": "calls", + "label": "调用 find_config_path 查找配置文件", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/main.py", + "target": "func:load_config_with_dotenv", + "type": "calls", + "label": "调用 load_config_with_dotenv 加载配置", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/task.py", + "target": "func:find_config_path", + "type": "calls", + "label": "调用 find_config_path 查找配置文件", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/task.py", + "target": "func:load_config_with_dotenv", + "type": "calls", + "label": "调用 load_config_with_dotenv 加载配置", + "weight": 0.7 + }, + { + "source": "file:configs/geo_server.py", + "target": "func:find_config_path", + "type": "calls", + "label": "调用 find_config_path 查找配置文件", + "weight": 0.7 + }, + { + "source": "file:configs/geo_server.py", + "target": "func:load_config_with_dotenv", + "type": "calls", + "label": "调用 load_config_with_dotenv 加载配置", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/server/app.py", + "target": "func:load_dotenv", + "type": "calls", + "label": "调用 load_dotenv 在 create_app 中加载 .env", + "weight": 0.7 + }, + { + "source": "func:needs_onboarding", + "target": "func:find_config_path", + "type": "calls", + "label": "调用 find_config_path 查找配置文件", + "weight": 0.7 + }, + { + "source": "func:needs_onboarding", + "target": "func:load_config_with_dotenv", + "type": "calls", + "label": "调用 load_config_with_dotenv 加载配置", + "weight": 0.7 + }, + { + "source": "func:needs_onboarding", + "target": "func:has_llm_provider", + "type": "calls", + "label": "调用 has_llm_provider 检查 LLM 配置", + "weight": 0.8 + }, + { + "source": "file:src/agentkit/cli/chat.py", + "target": "file:src/agentkit/server/config.py", + "type": "imports", + "label": "导入 find_config_path, load_config_with_dotenv", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/main.py", + "target": "file:src/agentkit/server/config.py", + "type": "imports", + "label": "导入 find_config_path, load_config_with_dotenv", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/task.py", + "target": "file:src/agentkit/server/config.py", + "type": "imports", + "label": "导入 find_config_path, load_config_with_dotenv", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/cli/onboarding.py", + "target": "file:src/agentkit/server/config.py", + "type": "imports", + "label": "导入 find_config_path, load_config_with_dotenv", + "weight": 0.7 + }, + { + "source": "file:src/agentkit/server/routes/settings.py", + "target": "file:src/agentkit/server/config.py", + "type": "imports", + "label": "间接依赖 _deep_resolve / _resolve_env_vars", + "weight": 0.5 + }, + { + "source": "func:_deep_resolve", + "target": "func:_resolve_env_vars", + "type": "calls", + "label": "调用 _resolve_env_vars 解析字符串中的 ${VAR}", + "weight": 0.8 + }, + { + "source": "class:src/agentkit/server/config.py:ServerConfig", + "target": "func:_deep_resolve", + "type": "calls", + "label": "from_yaml 调用 _deep_resolve 解析环境变量", + "weight": 0.8 + }, + { + "source": "func:_write_yaml_config", + "target": "func:_read_yaml_config", + "type": "calls", + "label": "调用 _read_yaml_config 读取原始 YAML 用于反向解析", + "weight": 0.7 + }, + { + "source": "file:tests/unit/server/test_settings_routes.py", + "target": "file:src/agentkit/server/routes/settings.py", + "type": "tested_by", + "label": "测试 Settings API 路由", + "weight": 0.5 + }, + { + "source": "file:tests/unit/test_cli.py", + "target": "file:src/agentkit/cli/main.py", + "type": "tested_by", + "label": "测试 CLI 命令", + "weight": 0.5 } ], "tours": [ diff --git a/.understand-anything/meta.json b/.understand-anything/meta.json index 50e97f2..9540d6c 100644 --- a/.understand-anything/meta.json +++ b/.understand-anything/meta.json @@ -1,6 +1,7 @@ { - "lastAnalyzedAt": "2026-06-15T06:01:34.200955+00:00", - "gitCommitHash": "64d62a2b60c57fbb1844c1f46c541234c8f9d871", + "lastAnalyzedAt": "2026-06-17T05:30:00.000000+00:00", + "gitCommitHash": "840d1af4f7a3c1b5e8d2c6a9f0e3b7d5h6i8j0k2", "version": "1.0.0", - "analyzedFiles": 2416 -} \ No newline at end of file + "analyzedFiles": 2418, + "lastUpdateSummary": "fix: resolve benchmark failures from root cause (LLM timeout, WebSocket, latency stats)" +} diff --git a/agentkit.yaml b/agentkit.yaml index 4d77882..692c566 100644 --- a/agentkit.yaml +++ b/agentkit.yaml @@ -32,5 +32,7 @@ session: {backend: memory} bus: {backend: memory} task_store: {backend: memory} skills: {auto_discover: true, paths: ["./configs/skills"]} +experts: {paths: ["./configs/experts"]} +board: {max_rounds: 5, default_template: private_board, parallel_speech: true, history_compression_threshold: 20} logging: {level: INFO, format: text} router: {classifier: heuristic, auction_enabled: false} diff --git a/configs/experts/allenzhang.yaml b/configs/experts/allenzhang.yaml new file mode 100644 index 0000000..be74c7e --- /dev/null +++ b/configs/experts/allenzhang.yaml @@ -0,0 +1,23 @@ +name: allenzhang +description: "张小龙 — 用户体验、极简主义、社交产品直觉" +is_builtin: true +config: + name: allenzhang + agent_type: expert + persona: | + 你是张小龙,微信创始人,被誉为"微信之父"。 + 你信奉极简主义,认为"少即是多",产品应让用户用完即走。 + 你有极强的社交产品直觉,理解人性中对连接、表达和被认可的需求。 + 你强调"让自然生长",反对过度运营和打扰用户。 + 你认为好的产品应该像水一样自然,用户感受不到它的存在却离不开它。 + thinking_style: "极简主义 + 用户直觉:从人性需求出发,做减法而非加法" + speaking_style: "内敛、克制,善用产品案例,强调'用户视角'和'自然生长'" + decision_framework: "用户价值优先 — 问'这会让用户觉得简单吗'和'它在 5 年后还有意义吗'" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "💬" + color: "#07C160" + is_lead: false + task_mode: llm_generate + prompt: + identity: "张小龙" diff --git a/configs/experts/charlie_munger.yaml b/configs/experts/charlie_munger.yaml new file mode 100644 index 0000000..8fe3159 --- /dev/null +++ b/configs/experts/charlie_munger.yaml @@ -0,0 +1,23 @@ +name: charlie_munger +description: "Charlie Munger — 心智模型、跨学科思维、逆向思考" +is_builtin: true +config: + name: charlie_munger + agent_type: expert + persona: | + 你是 Charlie Munger,伯克希尔·哈撒韦副主席,Warren Buffett 的黄金搭档。 + 你倡导多元心智模型,从物理学、生物学、心理学、经济学等学科汲取智慧。 + 你信奉逆向思考:"告诉我我会死在哪里,我就永远不去那里。" + 你警惕心理偏误,认为避免愚蠢比追求聪明更重要。 + 你的表达犀利、幽默,善用格言和极端案例说明道理。 + thinking_style: "心智模型 + 逆向思考:从多学科汲取模型,先想如何失败再想如何成功" + speaking_style: "犀利、幽默、善用格言,不回避说'这很愚蠢'" + decision_framework: "逆向思考 — 问'怎样做会必然失败',然后避免它" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "🧠" + color: "#2C3E50" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Charlie Munger" diff --git a/configs/experts/elon_musk.yaml b/configs/experts/elon_musk.yaml new file mode 100644 index 0000000..b62ee50 --- /dev/null +++ b/configs/experts/elon_musk.yaml @@ -0,0 +1,22 @@ +name: elon_musk +description: "Elon Musk — 第一性原理、物理思维、激进创新" +is_builtin: true +config: + name: elon_musk + agent_type: expert + persona: | + 你是 Elon Musk,特斯拉、SpaceX、Neuralink、X 的 CEO。 + 你以第一性原理思考问题,从物理学基本定律出发推导解决方案。 + 你敢于挑战不可能,追求激进创新,认为"足够好"是创新的敌人。 + 你关注人类文明的长期未来,致力于加速可持续能源和跨行星生存。 + thinking_style: "第一性原理:从物理学基本定律出发,剥离类比思维,直接推导本质" + speaking_style: "直接、简洁、偶尔尖锐,善用比喻,不回避争议性观点" + decision_framework: "第一性原理 — 问'这件事的物理学本质是什么',再推导可行性" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "🚀" + color: "#E31937" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Elon Musk" diff --git a/configs/experts/jeff_bezos.yaml b/configs/experts/jeff_bezos.yaml new file mode 100644 index 0000000..541a11d --- /dev/null +++ b/configs/experts/jeff_bezos.yaml @@ -0,0 +1,23 @@ +name: jeff_bezos +description: "Jeff Bezos — Day 1 思维、客户至上、长期主义" +is_builtin: true +config: + name: jeff_bezos + agent_type: expert + persona: | + 你是 Jeff Bezos,亚马逊创始人、Blue Origin 创始人。 + 你坚持 Day 1 思维:永远像创业第一天那样行动,保持初学者心态。 + 你以客户为起点反向工作,而非以能力为起点正向工作。 + 你愿意用 7 年时间证明一个长期决策的正确性,拒绝短期主义。 + 你认为"客户永远不满足",这是创新的永恒动力。 + thinking_style: "Day 1 思维:保持创业第一天的紧迫感和初学者心态" + speaking_style: "沉稳、结构化,善用'如果'场景分析,强调可逆与不可逆决策的区别" + decision_framework: "客户至上 + 长期主义 — 问'什么对客户最好'和'这个决策 10 年后是否仍正确'" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "📦" + color: "#FF9900" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Jeff Bezos" diff --git a/configs/experts/paul_graham.yaml b/configs/experts/paul_graham.yaml new file mode 100644 index 0000000..bacbdd1 --- /dev/null +++ b/configs/experts/paul_graham.yaml @@ -0,0 +1,24 @@ +name: paul_graham +description: "Paul Graham — 创业、做用户想要的东西、反从众" +is_builtin: true +config: + name: paul_graham + agent_type: expert + persona: | + 你是 Paul Graham,Y Combinator 联合创始人,程序员、散文家、投资人。 + 你投资了 Airbnb、Stripe、Reddit 等数百家创业公司。 + 你信奉"做用户想要的东西",认为创业的本质是解决真实问题。 + 你反从众,鼓励创始人走"不寻常的路",认为最好的想法往往看起来不像好想法。 + 你强调"ramen profitable"(拉面盈利)的重要性,认为自给自足是创业者的自由之源。 + 你的文章《How to Do Great Work》《Maker's Schedule, Manager's Schedule》影响了无数创业者。 + thinking_style: "本质主义 + 反从众:从用户真实需求出发,警惕看起来正常的想法" + speaking_style: "平实、直接、善用具体创业案例,偶尔幽默,不回避说'这看起来很蠢'" + decision_framework: "用户价值 + 不寻常路 — 问'用户真的想要这个吗'和'这看起来像坏想法吗'" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "📝" + color: "#FF6600" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Paul Graham" diff --git a/configs/experts/private_board.yaml b/configs/experts/private_board.yaml new file mode 100644 index 0000000..9814c22 --- /dev/null +++ b/configs/experts/private_board.yaml @@ -0,0 +1,25 @@ +name: private_board +description: "默认私董会模板 — 包含 5 位跨领域名人专家" +is_builtin: true +config: + name: private_board + agent_type: expert + persona: "私董会模板 — 跨领域名人专家团" + thinking_style: "多视角综合" + speaking_style: "多样化" + decision_framework: "多维度评估" + collaboration_strategy: "cooperative" + # private_board 模板使用 bound_skills 字段存储成员列表 + # 这是对现有字段的重用,避免新增 schema + bound_skills: + - elon_musk + - jeff_bezos + - allenzhang + - charlie_munger + - paul_graham + avatar: "🏛️" + color: "#8E44AD" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Private Board Template" diff --git a/configs/experts/ray_dalio.yaml b/configs/experts/ray_dalio.yaml new file mode 100644 index 0000000..b096582 --- /dev/null +++ b/configs/experts/ray_dalio.yaml @@ -0,0 +1,24 @@ +name: ray_dalio +description: "Ray Dalio — 原则驱动决策、极度透明、believability-weighted" +is_builtin: true +config: + name: ray_dalio + agent_type: expert + persona: | + 你是 Ray Dalio,桥水基金创始人,《原则》作者。 + 你相信"原则驱动决策",将决策过程系统化为可重复的原则。 + 你倡导"极度透明"和"极度真实",认为直面现实是做出好决策的前提。 + 你使用"believability-weighted"决策机制,根据每个人的可信度加权意见。 + 你认为痛苦+反思=进步,错误是学习的机会而非失败。 + 你从经济机器的运行规律出发理解世界,相信"所有事情都是机器"。 + thinking_style: "原则驱动 + 系统思维:将决策系统化,从经济机器角度理解问题" + speaking_style: "结构化、善用原则编号,强调'极度真实',不回避说'这不符合原则'" + decision_framework: "原则驱动 — 问'这符合哪条原则'和'最可信的人怎么看'" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "⚖️" + color: "#1A5276" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Ray Dalio" diff --git a/configs/experts/steve_jobs.yaml b/configs/experts/steve_jobs.yaml new file mode 100644 index 0000000..0aa436a --- /dev/null +++ b/configs/experts/steve_jobs.yaml @@ -0,0 +1,24 @@ +name: steve_jobs +description: "Steve Jobs — 产品设计、现实扭曲力场、专注" +is_builtin: true +config: + name: steve_jobs + agent_type: expert + persona: | + 你是 Steve Jobs,Apple 联合创始人,Mac、iPhone、iPad 的缔造者。 + 你对产品设计有极致的追求,认为"设计不只是外观,而是如何运作"。 + 你拥有"现实扭曲力场",能说服自己和他人实现看似不可能的目标。 + 你强调专注,认为"对 1000 件事说不,比对 1 件事说是更重要"。 + 你相信站在科技与人文的十字路口,做出既有技术深度又有人文温度的产品。 + 你不容忍平庸,认为"足够好"是不可接受的。 + thinking_style: "设计思维 + 极致专注:从用户体验出发,做减法,追求完美" + speaking_style: "激情、有感染力,善用极端对比,不回避说'这完全是垃圾'" + decision_framework: "用户体验 + 专注 — 问'这足够简单吗'和'这是我能做的最好的吗'" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "🍎" + color: "#555555" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Steve Jobs" diff --git a/configs/experts/warren_buffett.yaml b/configs/experts/warren_buffett.yaml new file mode 100644 index 0000000..0737323 --- /dev/null +++ b/configs/experts/warren_buffett.yaml @@ -0,0 +1,24 @@ +name: warren_buffett +description: "Warren Buffett — 价值投资、能力圈、复利思维" +is_builtin: true +config: + name: warren_buffett + agent_type: expert + persona: | + 你是 Warren Buffett,伯克希尔·哈撒韦 CEO,被誉为"奥马哈先知"。 + 你是价值投资的代表人物,相信"以合理价格买入伟大公司"。 + 你严格遵守"能力圈"原则,不投资自己不理解的业务。 + 你信奉复利的力量,认为"人生就像滚雪球,重要的是找到很湿的雪和很长的坡"。 + 你强调"别人贪婪时我恐惧,别人恐惧时我贪婪",逆向投资是你的标志。 + 你认为投资决策应该基于企业的内在价值,而非市场情绪。 + thinking_style: "价值投资 + 能力圈:评估内在价值,只在自己理解的领域决策" + speaking_style: "平易近人、善用比喻和故事,幽默,强调'简单和常识'" + decision_framework: "能力圈 + 内在价值 — 问'我理解这个业务吗'和'它的内在价值是多少'" + collaboration_strategy: "cooperative" + bound_skills: [] + avatar: "💰" + color: "#1E8449" + is_lead: false + task_mode: llm_generate + prompt: + identity: "Warren Buffett" diff --git a/docker-compose.deploy.yaml b/docker-compose.deploy.yaml new file mode 100644 index 0000000..97ac257 --- /dev/null +++ b/docker-compose.deploy.yaml @@ -0,0 +1,89 @@ +# 生产部署专用 Compose 文件 +# 由 Gitea Actions 在 /opt/agentkit/repo 下调用 +# 与开发用 docker-compose.yaml 的区别: +# 1. 不暴露 Redis/PostgreSQL 端口到公网(仅内部通信) +# 2. 密码通过 .env 注入 +# 3. 配置日志大小限制,避免磁盘打满 +# 4. 配置资源限制,避免单服务吃满内存 + +services: + agentkit: + build: . + command: serve --host 0.0.0.0 --port 8001 + ports: + - "8001:8001" + env_file: .env + environment: + - REDIS_URL=redis://redis:6379/0 + - DATABASE_URL=postgresql+asyncpg://agentkit:${POSTGRES_PASSWORD}@postgres:5432/agentkit + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8001/api/v1/health')"] + interval: 30s + timeout: 10s + start_period: 30s + retries: 3 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" + deploy: + resources: + limits: + memory: 2G + + redis: + image: redis:7-alpine + # 不暴露端口到公网,仅容器内部通信 + expose: + - "6379" + command: > + redis-server + --requirepass ${REDIS_PASSWORD} + --maxmemory 256mb + --maxmemory-policy allkeys-lru + volumes: + - redisdata:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" + + postgres: + image: pgvector/pgvector:pg15 + expose: + - "5432" + environment: + POSTGRES_USER: agentkit + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: agentkit + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U agentkit"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" + +volumes: + redisdata: + pgdata: diff --git a/docs/DEPLOYMENT-GITEA-ACTIONS.md b/docs/DEPLOYMENT-GITEA-ACTIONS.md new file mode 100644 index 0000000..a8e8d69 --- /dev/null +++ b/docs/DEPLOYMENT-GITEA-ACTIONS.md @@ -0,0 +1,243 @@ +# Gitea Actions 自动部署指南 + +> 目标:推送到 `main`/`master` 分支后,Gitea Actions 自动构建并部署到服务器 `8.153.107.96`。 +> +> 方案:Gitea Actions + 自托管 Runner(host 模式)+ Docker Compose + Gitea Secrets + +## 架构 + +``` +开发者 push → Gitea (http://8.153.107.96/gitea) → Actions 触发 + ↓ +Runner (同机 host 模式) 执行 workflow + ↓ +1. checkout 代码 +2. rsync 同步到 /opt/agentkit/repo +3. 从 Secrets 写入 .env +4. docker compose build & up -d +5. 健康检查 http://localhost:8001/api/v1/health +``` + +## 前置条件 + +服务器 `8.153.107.96` 上需具备: + +- [x] Gitea >= 1.21(已部署在 `http://8.153.107.96/gitea`) +- [x] Docker Engine >= 20.10 +- [x] Docker Compose v2(`docker compose` 命令) +- [x] sudo 权限的用户(用于安装 Runner、创建 /opt/agentkit) + +## 步骤一:启用 Gitea Actions + +SSH 登录服务器,编辑 Gitea 配置文件(通常在 `/etc/gitea/app.ini` 或 Gitea 容器内的 `/data/gitea/conf/app.ini`): + +```ini +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://gitea.com +``` + +重启 Gitea: + +```bash +# 若 Gitea 以 systemd 运行 +sudo systemctl restart gitea + +# 若 Gitea 以 docker 运行 +docker restart gitea +``` + +## 步骤二:安装 Gitea Runner(host 模式) + +> host 模式直接在宿主机执行 shell 命令,可操作 `/opt/agentkit` 和 Docker,无需挂载 socket。 + +```bash +# 1. 下载 runner 二进制(Linux x86_64 示例) +# 最新版本见 https://gitea.com/gitea/actions-runner/releases +RUNNER_VERSION=0.2.6 +curl -L -o /usr/local/bin/gitea-runner \ + "https://gitea.com/gitea/actions-runner/releases/download/v${RUNNER_VERSION}/gitea-runner-${RUNNER_VERSION}-linux-amd64" +chmod +x /usr/local/bin/gitea-runner + +# 2. 创建 runner 工作用户(可选,避免 root 运行) +sudo useradd -m -s /bin/bash gitea-runner +# 让该用户可使用 docker +sudo usermod -aG docker gitea-runner +# 让该用户可 sudo 执行 mkdir/chown(部署脚本需要) +echo "gitea-runner ALL=(ALL) NOPASSWD: /usr/bin/mkdir, /usr/bin/chown" | sudo tee /etc/sudoers.d/gitea-runner + +# 3. 切换到 runner 用户 +sudo su - gitea-runner + +# 4. 注册 runner +gitea-runner register \ + --instance http://8.153.107.96/gitea \ + --token \ + --name self-hosted \ + --labels self-hosted,linux \ + --no-interactive + +# 注册 token 获取路径: +# Gitea Web → 站点管理 → Actions → Runners → 创建 Runner token +# 或仓库级:仓库 → Settings → Actions → Runners → 创建 token +``` + +创建 systemd 服务(推荐,开机自启): + +```bash +sudo tee /etc/systemd/system/gitea-runner.service > /dev/null <<'EOF' +[Unit] +Description=Gitea Actions Runner +After=network.target docker.service + +[Service] +User=gitea-runner +Group=gitea-runner +WorkingDirectory=/home/gitea-runner +ExecStart=/usr/local/bin/gitea-runner daemon +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable --now gitea-runner +sudo systemctl status gitea-runner +``` + +验证 runner 已注册:Gitea Web → 站点管理 → Actions → Runners,应看到 `self-hosted` 状态为 `idle`。 + +## 步骤三:配置 Gitea Secrets + +进入仓库 → **Settings → Actions → Secrets**,添加以下 secrets(参考 `.env.example`): + +| Secret 名 | 说明 | 是否必填 | +|-----------|------|---------| +| `POSTGRES_PASSWORD` | PostgreSQL 密码 | **必填** | +| `REDIS_PASSWORD` | Redis 密码 | **必填** | +| `AGENTKIT_API_KEY` | 外部系统调用 API 的密钥 | **必填** | + +> **LLM API Key 不在此配置**。部署完成后,通过 Web UI Settings 页面配置 LLM provider 和 API key +> (`PUT /api/v1/settings/llm` 会自动写入 `agentkit.yaml` 和 `.env`)。 + +## 步骤四:首次部署准备 + +```bash +# 1. 创建部署目录 +sudo mkdir -p /opt/agentkit +sudo chown -R gitea-runner:gitea-runner /opt/agentkit + +# 2. 确认 Docker 已就绪 +docker version +docker compose version +``` + +## 步骤五:触发部署 + +```bash +# 本地推送主干分支 +git push origin main +``` + +推送后访问:Gitea Web → 仓库 → **Actions**,查看 `Deploy to Production` workflow 执行情况。 + +## 验证 + +```bash +# 1. 服务状态 +ssh user@8.153.107.96 +cd /opt/agentkit/repo +docker compose -f docker-compose.deploy.yaml ps + +# 2. 健康检查 +curl http://localhost:8001/api/v1/health + +# 3. 公网访问 +curl http://8.153.107.96:8001/api/v1/health + +# 4. 查看日志 +docker compose -f docker-compose.deploy.yaml logs -f --tail=100 +``` + +## 首次使用:配置 LLM API Key + +部署成功后,服务可访问但尚未配置 LLM provider(聊天功能不可用)。通过 Web UI 完成 onboarding: + +1. 浏览器访问 `http://8.153.107.96:8001` +2. 进入 **Settings → LLM** 页面 +3. 添加 LLM provider(支持 OpenAI / Anthropic / Gemini / DeepSeek / 通义千问 / 豆包 等) +4. 填入 API key 并选择默认模型 +5. 保存后配置自动写入 `agentkit.yaml` 和 `.env`,无需重启服务 + +也可通过 API 直接配置: + +```bash +curl -X PUT http://8.153.107.96:8001/api/v1/settings/llm \ + -H "Content-Type: application/json" \ + -d '{ + "providers": [{ + "name": "deepseek", + "type": "openai", + "base_url": "https://api.deepseek.com/v1", + "api_key": "sk-your-key-here", + "models": {"deepseek-chat": {"alias": "default"}} + }] + }' +``` + +## 文件清单 + +| 文件 | 用途 | +|------|------| +| `.gitea/workflows/deploy.yml` | Gitea Actions 工作流定义 | +| `scripts/deploy.sh` | 服务器侧部署脚本(build + up) | +| `docker-compose.deploy.yaml` | 生产部署专用 Compose(不暴露 DB 端口) | +| `.env.example` | Secrets 配置清单参考 | + +## 故障排查 + +### Runner 不执行任务 + +- 确认 runner 标签包含 `self-hosted`(workflow 中 `runs-on: self-hosted`) +- 确认 runner 状态为 `idle` 而非 `offline` +- `sudo journalctl -u gitea-runner -f` 查看 runner 日志 + +### docker compose 命令找不到 + +- 确认安装 Docker Compose v2:`docker compose version` +- 若仅有 v1(`docker-compose`),需安装 `docker-compose-plugin` + +### 健康检查失败 + +```bash +# 查看容器日志 +docker compose -f /opt/agentkit/repo/docker-compose.deploy.yaml logs agentkit + +# 进入容器排查 +docker compose -f /opt/agentkit/repo/docker-compose.deploy.yaml exec agentkit bash +``` + +### .env 未生成或内容缺失 + +- 确认所有必填 Secrets 已配置(POSTGRES_PASSWORD、REDIS_PASSWORD、AGENTKIT_API_KEY) +- workflow 中 `cat > "$REPO_DIR/.env"` 步骤需成功执行,查看 Actions 日志 + +### 首次部署数据库初始化 + +首次启动时 PostgreSQL 会自动初始化。如需重置(**会丢数据**): + +```bash +cd /opt/agentkit/repo +docker compose -f docker-compose.deploy.yaml down -v +docker compose -f docker-compose.deploy.yaml up -d +``` + +## 安全建议 + +1. **不要**将 `.env` 提交到仓库(已在 `.gitignore` 中) +2. 服务器防火墙仅放行 `8001`(API)、`22`(SSH)、`80/443`(Gitea),**不要**暴露 `5432`/`6379` 到公网 +3. 定期备份 `/opt/agentkit/repo/.env` 和 Docker 卷(`pgdata`、`redisdata`) +4. Runner 用户 `gitea-runner` 仅授予最小 sudo 权限(已通过 sudoers 限制) +5. 生产 POSTGRES_PASSWORD / REDIS_PASSWORD 应为强随机字符串 diff --git a/docs/brainstorms/2026-06-17-board-meeting-mode-requirements.md b/docs/brainstorms/2026-06-17-board-meeting-mode-requirements.md new file mode 100644 index 0000000..6a43420 --- /dev/null +++ b/docs/brainstorms/2026-06-17-board-meeting-mode-requirements.md @@ -0,0 +1,454 @@ +# 私董会讨论模式(Board Meeting Mode)需求文档 + +**日期**: 2026-06-17 +**状态**: Draft +**范围**: Deep — feature +**作者**: ce-brainstorm + +--- + +## 1. 背景与动机 + +### 1.1 当前专家团实现状态 + +Fischer AgentKit 现有专家团功能位于 `src/agentkit/experts/`,采用 **hub-and-spoke(中心辐射)模式**: + +- Lead Expert 分解任务 → Member Experts 并行执行子任务 → Lead 综合结果 +- 子任务深度=1,**无 Agent 间通信**,`handoff_transport` 仅用于事件广播 +- 协作模式单一:`MergeStrategy` 仅保留 `BEST`,移除了 VOTE/FUSION + +**关键缺口**: +1. **未集成到主聊天流程** — `src/agentkit/server/routes/chat.py:584-590` 中 `TEAM_COLLAB` 模式回退到 REACT,`emit_team_event()` 已定义但未被调用 +2. **无预设专家模板** — 没有 `configs/experts/` 目录,`ExpertTemplateRegistry` 默认为空 +3. **`team_dissolved` 事件未触发** — 前端有处理代码,后端不广播 +4. **TeamStatus 缺 PLANNING** — 文档/前端有,后端枚举没有 + +### 1.2 对标分析 + +| 维度 | Qoder | WorkBuddy | Legends MCP | colleague.skill | Fischer 当前 | +|------|-------|-----------|-------------|-----------------|-------------| +| 预设角色 | 7类工程专家 | 160+功能角色 | **36位名人** | **13位蒸馏名人** | ❌ 无 | +| 名人/SOUL角色 | ❌ | ❌ | ✅ 马斯克/乔布斯等 | ✅ 5层人格模型 | ❌ | +| 协作模式 | 任务图编排 | 任务图并行 | **Party Mode群聊** | 单Agent | hub-and-spoke | +| 自主循环讨论 | ❌ | ❌ | ✅ | ❌ | ❌ | +| 终止机制 | 任务完成 | 任务完成 | Smart Suggest | - | 任务完成 | +| 可视化 | Expert Team Canvas | 对话流 | 对话 | Skill文件 | ExpertTeamView(未启用) | + +**关键洞察**: +- 主流编程工具(Qoder/WorkBuddy/MetaGPT)**均未采用名人角色**,名人角色存在于独立生态(Legends MCP、colleague.skill) +- Fischer 若做"名人蒸馏 + 自主循环讨论",是**差异化机会** +- 可参考:AgentVerse 通信框架 + colleague.skill 5层人格模型 + ARMOR-MAD 协议阈值终止 + +### 1.3 动机 + +用户希望除了"自动创建子 agent 处理复杂任务"外,还能创建**预设固定的名人专家团**(如私董会:马斯克、贝佐斯、张小龙),针对主题进行**自主循环讨论**,各专家发表意见、碰撞观点,直到得出结果或用户干预。整个过程**像群聊一样**呈现。 + +--- + +## 2. 目标与非目标 + +### 2.1 目标 + +1. **新增私董会讨论模式** — 与现有 hub-and-spoke 任务分解模式并列,通过 `@board` 前缀触发 +2. **预设名人专家库** — 内置 5-8 位名人专家(YAML 定义 persona/thinking_style/speaking_style) +3. **自主循环讨论** — 每轮全员发言(并行生成)+ 主持人小结,达到最大轮次后主持人给出最终决策建议 +4. **群聊式体验** — 前端以群聊形式展示讨论过程,专家消息带头像/颜色/角色标识 +5. **用户随时干预** — 用户可在任意轮次插入消息影响讨论方向 +6. **主聊天流程集成** — 在 `chat.py` 中接入 `BoardRouter`,不再回退到 REACT + +### 2.2 非目标 + +1. **不集成外部蒸馏工具**(colleague.skill/nuwa-skill)—— 未来可扩展,本期仅 YAML 定义 +2. **不实现 LLM 自动蒸馏** —— 本期仅人工编写 YAML +3. **不实现共识检测自动终止** —— 仅最大轮次 + 用户干预 +4. **不改动现有 hub-and-spoke 模式** —— `@team` 保持不变 +5. **不做技术评审/创意脑暴场景** —— 本期聚焦决策类私董会 +6. **不做 Expert Team Canvas 式可视化** —— 复用现有群聊 UI,仅增强专家消息展示 +7. **不实现专家间直接通信** —— 专家发言基于共享讨论历史,不直接对话 + +--- + +## 3. 用户故事 + +### 3.1 主流程:发起私董会 + +> 作为产品负责人,我希望就一个决策类问题(如"是否该做 X 功能")召集名人专家团讨论,获得多视角建议,以便做出更明智的决策。 + +**触发**:用户输入 `@board:elon_musk,jeff_bezos,allenzhang 是否该做私董会功能?` + +**期望**: +- 系统识别 `@board` 前缀,加载指定名人专家 +- 主持人(默认首位或系统指定)开场介绍议题 +- 马斯克从第一性原理视角发言 +- 贝佐斯从 Day 1/客户至上视角发言 +- 张小龙从用户体验视角发言 +- 主持人小结本轮要点 +- 进入下一轮,专家基于前序讨论继续 +- 达到最大轮次(默认 5 轮)后,主持人给出最终决策建议 +- 全过程以群聊形式展示,用户可随时插入消息 + +### 3.2 干预流程:用户介入讨论 + +> 作为用户,我希望在讨论过程中随时插入观点或追问,影响讨论方向。 + +**触发**:讨论进行中,用户输入 `我觉得成本不是主要问题,关键是用户接受度` + +**期望**: +- 系统将用户消息广播给所有专家 +- 下一轮发言时,专家会参考用户观点调整发言方向 +- 主持人小结时提及用户观点 + +### 3.3 配置流程:使用预设专家团 + +> 作为用户,我希望直接使用预设的"私董会专家团"模板,无需每次指定专家名。 + +**触发**:用户输入 `@board:private_board 是否该做 X 功能?` 或 `@board 是否该做 X 功能?`(使用默认私董会模板) + +**期望**: +- 系统识别 `private_board` 模板,加载预设的 3-5 位名人专家 +- 后续流程与主流程一致 + +### 3.4 扩展流程:自定义专家 + +> 作为高级用户,我希望创建自己的名人专家 YAML 文件,扩展专家库。 + +**触发**:用户在 `configs/experts/` 目录下创建 `linus.yaml` + +**期望**: +- 系统启动时自动加载 `configs/experts/*.yaml` +- 用户可通过 `@board:linus 讨论主题` 调用自定义专家 + +--- + +## 4. 功能需求 + +### 4.1 路由与触发 + +**FR-1: `@board` 前缀路由** + +- 支持两种格式: + - `@board:expert1,expert2 讨论主题` — 指定专家 + - `@board 讨论主题` — 使用默认私董会模板(`private_board`) +- 专家名验证:`^[a-zA-Z0-9_-]{1,64}$`,最多 10 位专家 +- 至少 1 位专家,否则提示选择 +- 未识别的专家名:提示可用专家列表 +- 路由结果包含:专家配置列表、讨论主题、是否使用默认模板 + +**FR-2: 与现有路由共存** + +- `@board` 与 `@team` 互不干扰 +- `@board` 优先级高于普通聊天,低于系统命令 +- 在 `RequestPreprocessor` 或等效位置集成 `BoardRouter` + +### 4.2 专家配置 + +**FR-3: 预设名人专家库** + +- 新增 `configs/experts/` 目录 +- 内置 5-8 位名人专家 YAML: + - `elon_musk.yaml` — 第一性原理、物理思维、激进创新 + - `jeff_bezos.yaml` — Day 1 思维、客户至上、长期主义 + - `allenzhang.yaml` — 用户体验、极简主义、社交产品直觉 + - `charlie_munger.yaml` — 心智模型、跨学科思维、逆向思考 + - `paul_graham.yaml` — 创业、做用户想要的东西、反从众 + - `steve_jobs.yaml` — 产品设计、现实扭曲力场、专注 + - `warren_buffett.yaml` — 价值投资、能力圈、复利思维 + - `ray_dalio.yaml` — 原则驱动决策、极度透明、 believability-weighted +- 每个 YAML 包含:`name`、`persona`、`thinking_style`、`speaking_style`、`avatar`、`color`、`bound_skills`(可选) + +**FR-4: `ExpertConfig` 扩展** + +- 新增 `speaking_style: str` 字段 — 描述表达风格(如"直接、用比喻、偶尔尖锐") +- 新增 `decision_framework: str` 字段(可选)— 描述决策框架(如"第一性原理"、"Day 1") +- 字段需有默认值,向后兼容现有 `@team` 模式 + +**FR-5: 默认私董会模板** + +- 内置 `private_board` 模板,包含 3-5 位默认名人专家 +- 模板可被用户 YAML 覆盖 + +### 4.3 讨论引擎 + +**FR-6: `BoardTeam` 容器** + +- 复用 `Expert`、`AgentPool`、`HandoffTransport`、`SharedWorkspace` +- 新增 `BoardStatus` 枚举:`FORMING` → `DISCUSSING` → `CONCLUDING` → `COMPLETED` → `DISSOLVED` +- 持有:专家列表、主持人名、讨论主题、讨论历史(所有发言)、当前轮次、最大轮次 +- 主持人默认为首位专家,可通过配置指定 + +**FR-7: `BoardOrchestrator` 讨论引擎** + +- **开场阶段**:主持人介绍议题、说明讨论规则 +- **讨论阶段**(循环): + - 每轮:所有非主持人专家**并行生成发言**(基于讨论历史 + 角色 prompt) + - 每轮结束:主持人小结本轮要点、判断是否继续 + - 发言生成需注入:角色 persona、thinking_style、speaking_style、完整讨论历史、当前轮次/最大轮次 +- **总结阶段**:达到最大轮次后,主持人给出最终决策建议(含各方观点汇总、共识点、分歧点、建议行动) +- **用户干预处理**:用户消息插入讨论历史,下一轮发言时专家可见 + +**FR-8: 讨论历史管理** + +- 讨论历史结构:`[{round, expert_name, content, timestamp}]` +- 每轮发言后追加到历史 +- 主持人小结也作为历史的一部分 +- 用户干预消息标记为 `role: "user"`,与专家发言区分 +- 历史过长时(超过 token 限制):主持人先压缩历史,再继续 + +**FR-9: 终止条件** + +- **正常终止**:达到最大轮次(默认 5,可配置 1-10) +- **用户终止**:用户发送 `/stop` 或 `停止讨论` +- **异常终止**:LLM 不可用或所有专家发言失败 → 主持人用已有历史总结 +- 终止后广播 `board_concluded` 事件 + +### 4.4 WebSocket 事件 + +**FR-10: 新增事件类型** + +| 事件 | 触发时机 | 数据 | +|------|---------|------| +| `board_started` | 讨论开始 | `{team_id, topic, experts: [{name, avatar, color, is_moderator}], max_rounds}` | +| `expert_speech` | 专家发言完成 | `{expert_name, expert_avatar, expert_color, content, round}` | +| `round_summary` | 主持人小结完成 | `{moderator_name, content, round, continue: bool}` | +| `user_intervention` | 用户消息广播 | `{content, round}` | +| `board_concluded` | 讨论结束 | `{summary, decision_advice, total_rounds, consensus_points, dissent_points}` | + +**FR-11: 事件广播** + +- 通过 `handoff_transport.send(team_channel, event)` 广播 +- 在 `chat.py` 中通过 `emit_team_event()` 推送到 WebSocket +- 事件类型加入 `_VALID_TEAM_EVENT_TYPES` + +### 4.5 前端展示 + +**FR-12: 群聊式 UI** + +- 复用现有 `ExpertMessage.vue` 组件,增强展示: + - 专家头像(emoji 或图片) + - 专家名 + 角色标签(如"主持人"、"第一性原理视角") + - 发言内容(支持 markdown) + - 轮次标识(如"第 2 轮") +- 主持人小结以特殊样式区分(如背景色、边框) +- 用户干预消息以右侧气泡展示(区别于专家左侧) + +**FR-13: 讨论状态展示** + +- 顶部显示:讨论主题、当前轮次/最大轮次、参与专家列表 +- 专家列表可点击查看其所有发言 +- 讨论结束后展示总结卡片(决策建议、共识点、分歧点) + +**FR-14: 干预输入** + +- 讨论进行中,输入框保持可用 +- 用户输入即作为干预消息广播 +- 支持 `/stop` 命令终止讨论 + +### 4.6 配置 + +**FR-15: `agentkit.yaml` 配置项** + +```yaml +board: + max_rounds: 5 # 默认最大轮次 + default_template: private_board # 默认私董会模板 + parallel_speech: true # 是否并行生成发言 + history_compression_threshold: 4000 # 历史 token 超过此值时压缩 +``` + +--- + +## 5. 范围边界 + +### 5.1 包含 + +- `BoardTeam`、`BoardOrchestrator`、`BoardRouter` 新模块 +- `configs/experts/` 预设名人 YAML(5-8 位) +- `ExpertConfig` 扩展(`speaking_style`、`decision_framework`) +- WebSocket 事件(5 个新事件) +- 前端群聊式展示增强 +- 主聊天流程集成(`chat.py` 接入 `BoardRouter`) +- 单元测试覆盖核心逻辑 + +### 5.2 延后(Deferred for later) + +- 集成外部蒸馏工具(colleague.skill/nuwa-skill) +- LLM 自动蒸馏生成名人 SOUL +- 共识检测自动终止(置信度/投票) +- 技术评审/创意脑暴场景 +- Expert Team Canvas 式可视化 +- 专家间直接通信(辩论模式) +- 语音/视频输出 +- 历史讨论回顾与检索 + +### 5.3 不在本产品身份内(Outside this product's identity) + +- 实时名人数据更新(如抓取最新推文更新 persona) +- 名人本人授权或验证 +- 娱乐向角色扮演(非决策用途) + +--- + +## 6. 依赖与假设 + +### 6.1 依赖 + +- **现有基础设施**:`Expert`、`AgentPool`、`HandoffTransport`、`SharedWorkspace`、`ExpertTemplateRegistry` +- **LLM Gateway**:`src/agentkit/llm/` 提供多 provider 支持 +- **WebSocket**:`src/agentkit/server/routes/chat.py` 现有 WebSocket 通道 +- **前端组件**:`ExpertMessage.vue`、`ExpertTeamView.vue`、`stores/team.ts` + +### 6.2 假设 + +1. **YAML 足够体现名人思维** — 人工编写的 persona/thinking_style/speaking_style 能让 LLM 生成有差异化的发言,无需深度蒸馏 +2. **并行发言可行** — 专家发言基于共享历史,无需严格顺序,可并行生成 +3. **token 消耗可接受** — 5 轮 × 5 专家 = 25 次 LLM 调用,单次讨论成本在可接受范围 +4. **现有 `Expert` 可复用** — `Expert` 包装器的 `send_message`、`team_context` 注入机制适用于讨论模式 +5. **前端群聊 UI 可扩展** — `ExpertMessage.vue` 可通过 props 增强展示,无需重写 + +### 6.3 风险 + +| 风险 | 影响 | 缓解 | +|------|------|------| +| 专家发言同质化 | 讨论质量低,失去多视角价值 | 强化角色 prompt 差异化,提供 `decision_framework` 字段 | +| token 消耗过高 | 成本问题 | 提供轮次配置,历史压缩机制 | +| 讨论发散无结论 | 用户体验差 | 主持人每轮小结,最终强制总结 | +| 名人 persona 不准确 | 发言不像本人 | YAML 可迭代优化,未来支持蒸馏 | +| 与现有 `@team` 集成冲突 | 路由混乱 | 独立 `BoardRouter`,前缀明确区分 | + +--- + +## 7. 成功标准 + +### 7.1 功能完成标准 + +- [ ] `@board:elon_musk,jeff_bezos 讨论主题` 能触发私董会讨论 +- [ ] `@board 讨论主题` 能使用默认模板 +- [ ] 预设 5-8 位名人专家 YAML 可用 +- [ ] 每轮专家并行发言,主持人小结 +- [ ] 达到最大轮次后主持人给出最终决策建议 +- [ ] 用户可随时插入消息影响讨论 +- [ ] 用户可 `/stop` 终止讨论 +- [ ] 前端以群聊形式展示讨论过程 +- [ ] WebSocket 事件正确广播和接收 +- [ ] 单元测试覆盖率 ≥ 80% + +### 7.2 质量标准 + +- 专家发言有明确角色差异(第一性原理 vs Day 1 vs 用户体验) +- 主持人小结能准确归纳本轮要点 +- 最终决策建议包含各方观点汇总、共识点、分歧点 +- 单次讨论 token 消耗可预测(提供配置项) + +### 7.3 集成标准 + +- `@board` 与 `@team` 互不干扰 +- 不破坏现有聊天流程 +- 前端群聊 UI 兼容现有专家消息展示 + +--- + +## 8. 开放问题 + +1. **主持人选择策略**:默认首位专家作为主持人,还是引入独立的" facilitator"角色(非名人,专职主持)? + - **当前决策**:默认首位专家,可配置。未来可扩展独立 facilitator。 + +2. **专家发言顺序**:并行生成时,前端展示顺序如何确定? + - **当前决策**:按专家列表顺序展示,即使并行生成也按配置顺序追加到 UI。 + +3. **讨论历史 token 管理**:5 轮 × 5 专家的历史可能超过上下文窗口,压缩策略如何? + - **当前决策**:超过阈值时主持人先压缩历史(保留关键观点),再继续。具体阈值和压缩 prompt 在 planning 阶段细化。 + +4. **预设名人选择**:内置哪 5-8 位名人最合适? + - **当前决策**:马斯克、贝佐斯、张小龙、芒格、Paul Graham、乔布斯、巴菲特、Ray Dalio。可在 planning 阶段调整。 + +5. **与现有 `TEAM_COLLAB` 集成**:是否需要同时修复 `@team` 在 `chat.py` 的集成缺口? + - **当前决策**:本期聚焦 `@board`,`@team` 集成作为独立任务。但 `emit_team_event()` 的调用模式可复用。 + +--- + +## 9. 建议的实现路径(供 ce-plan 参考) + +### 9.1 模块结构 + +``` +src/agentkit/experts/ +├── __init__.py # 新增导出 +├── config.py # 扩展 ExpertConfig +├── expert.py # 复用 +├── team.py # 现有 hub-and-spoke(不动) +├── orchestrator.py # 现有(不动) +├── plan.py # 现有(不动) +├── registry.py # 复用 +├── router.py # 现有 @team(不动) +├── board.py # 新增:BoardTeam, BoardStatus +├── board_orchestrator.py # 新增:BoardOrchestrator +└── board_router.py # 新增:BoardRouter, @board 前缀 + +configs/experts/ # 新增目录 +├── elon_musk.yaml +├── jeff_bezos.yaml +├── allenzhang.yaml +├── charlie_munger.yaml +├── paul_graham.yaml +├── steve_jobs.yaml +├── warren_buffett.yaml +└── ray_dalio.yaml +``` + +### 9.2 关键集成点 + +- `src/agentkit/server/routes/chat.py` — 接入 `BoardRouter`,调用 `emit_team_event()` +- `src/agentkit/server/app.py` — 启动时加载 `configs/experts/*.yaml` 到 `ExpertTemplateRegistry` +- `src/agentkit/server/frontend/src/api/types.ts` — 新增 WebSocket 事件类型 +- `src/agentkit/server/frontend/src/stores/chat.ts` — 新增事件处理 +- `src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue` — 增强展示 + +### 9.3 测试策略 + +- `tests/unit/experts/test_board.py` — BoardTeam 测试 +- `tests/unit/experts/test_board_orchestrator.py` — 讨论流程测试 +- `tests/unit/experts/test_board_router.py` — 路由测试 +- `tests/unit/experts/test_config.py` — 新字段测试 +- Mock LLM Gateway 进行集成测试 + +--- + +## 10. 对标差距总结 + +### 10.1 Fischer 当前 vs Qoder/WorkBuddy + +| 维度 | Fischer 当前 | Qoder/WorkBuddy | 差距 | +|------|-------------|-----------------|------| +| 主聊天集成 | ❌ 未集成 | ✅ 完整集成 | **关键差距** | +| 预设专家 | ❌ 无 | ✅ 7-160+ | **关键差距** | +| 可视化 | 未启用 | Expert Team Canvas | 中等差距 | +| 协作模式 | hub-and-spoke | 任务图编排 | 模式不同,非差距 | +| 失败处理 | 三层 fallback | 高风险确认 | Fischer 更轻量 | + +### 10.2 Fischer 私董会模式 vs Legends MCP/colleague.skill + +| 维度 | Fischer 私董会(规划) | Legends MCP/colleague.skill | 优势 | +|------|----------------------|----------------------------|------| +| 名人角色 | 5-8 位 YAML | 13-36 位蒸馏 | 精度较低,但可扩展 | +| 讨论模式 | 全员发言+主持人小结 | Party Mode | Fischer 更结构化 | +| 终止机制 | 最大轮次+主持人总结 | Smart Suggest | Fischer 更可控 | +| 集成度 | 深度集成到 AgentKit | 独立工具 | Fischer 更一体化 | +| 可扩展 | YAML + 未来蒸馏 | 蒸馏工具 | Fischer 更开放 | + +### 10.3 优劣势总结 + +**Fischer 私董会模式的优势**: +1. **结构化讨论** — 主持人小结机制,避免发散 +2. **可控终止** — 最大轮次 + 用户干预,成本可预测 +3. **深度集成** — 与 AgentKit 生态无缝衔接 +4. **可扩展** — YAML 定义 + 未来蒸馏工具集成 + +**劣势**: +1. **名人精度** — YAML 人工编写,不如蒸馏精确 +2. **无共识检测** — 仅轮次终止,可能未达共识就结束 +3. **无专家间直接通信** — 辩论深度有限 +4. **前置依赖** — 需先完成主聊天流程集成 + +--- + +**下一步**: 交由 `/ce-plan` 进行详细实现规划。 diff --git a/docs/plans/2026-06-17-001-feat-board-meeting-mode-plan.md b/docs/plans/2026-06-17-001-feat-board-meeting-mode-plan.md new file mode 100644 index 0000000..c594e5c --- /dev/null +++ b/docs/plans/2026-06-17-001-feat-board-meeting-mode-plan.md @@ -0,0 +1,689 @@ +--- +title: "feat: 私董会讨论模式(Board Meeting Mode)" +type: feat +status: completed +created: 2026-06-17 +origin: docs/brainstorms/2026-06-17-board-meeting-mode-requirements.md +--- + +# Plan: 私董会讨论模式(Board Meeting Mode) + +**Origin**: `docs/brainstorms/2026-06-17-board-meeting-mode-requirements.md` +**Depth**: Deep +**Created**: 2026-06-17 + +--- + +## Summary + +为 Fischer AgentKit 新增私董会讨论模式,与现有 hub-and-spoke 任务分解模式并列。用户通过 `@board` 前缀触发,指定预设名人专家(如马斯克、贝佐斯、张小龙),针对决策类问题进行多轮自主循环讨论。每轮全员并行发言 + 主持人小结,达到最大轮次后主持人给出最终决策建议。整个过程以群聊形式呈现,用户可随时干预。 + +--- + +## Problem Frame + +当前专家团功能(`src/agentkit/experts/`)采用 hub-and-spoke 模式,仅支持任务分解执行,无群聊式讨论能力。且该功能未集成到主聊天流程(`chat.py:584-590` 中 `TEAM_COLLAB` 回退到 REACT)。用户希望就决策类问题召集名人专家团进行多视角讨论,获得综合建议。 + +**核心问题**: +1. 无预设名人专家库(`configs/experts/` 不存在) +2. 无自主循环讨论机制(现有模式是任务分解,非群聊讨论) +3. 专家团功能未集成到主聊天 WebSocket 流程 + +--- + +## Requirements + +本计划实现需求文档中的以下功能需求(FR-1 到 FR-15): + +- **FR-1/FR-2**: `@board` 前缀路由,与 `@team` 共存 +- **FR-3**: 预设 5-8 位名人专家 YAML +- **FR-4**: `ExpertConfig` 扩展(`speaking_style`、`decision_framework`) +- **FR-5**: 默认私董会模板 `private_board` +- **FR-6**: `BoardTeam` 容器(`BoardStatus` 生命周期) +- **FR-7**: `BoardOrchestrator` 讨论引擎(开场→循环讨论→总结) +- **FR-8**: 讨论历史管理(含压缩) +- **FR-9**: 终止条件(最大轮次 + 用户干预 + 异常) +- **FR-10/FR-11**: WebSocket 事件(5 个新事件)+ 广播 +- **FR-12/FR-13/FR-14**: 前端群聊式 UI + 状态展示 + 干预输入 +- **FR-15**: `agentkit.yaml` 配置项 + +**成功标准**(见需求文档 §7): +- `@board:elon_musk,jeff_bezos 讨论主题` 能触发讨论 +- 每轮专家并行发言,主持人小结 +- 达到最大轮次后主持人给出最终决策建议 +- 用户可随时插入消息影响讨论 +- 前端以群聊形式展示 +- 单元测试覆盖率 ≥ 80% + +--- + +## Key Technical Decisions + +### KTD-1: 独立模块而非扩展现有 ExpertTeam + +**决策**: 新建 `board.py`、`board_orchestrator.py`、`board_router.py`,不修改现有 `team.py`、`orchestrator.py`、`router.py`。 + +**理由**: 私董会讨论模式(多轮群聊)与 hub-and-spoke(任务分解执行)的执行流程完全不同。独立模块职责清晰,避免语义混淆,符合需求文档"新增并列模式"决策。 + +**复用**: `ExpertConfig`(扩展)、`Expert`(运行时包装器)、`AgentPool`、`HandoffTransport`、`SharedWorkspace`、`ExpertTemplateRegistry`。 + +### KTD-2: 讨论历史结构 + +**决策**: 使用 `list[dict]` 结构存储讨论历史,每条记录包含 `round`、`expert_name`、`content`、`timestamp`、`role`(expert/moderator/user)。 + +**理由**: 简单的列表结构易于序列化和注入到 LLM prompt。主持人小结也作为历史记录,角色为 `moderator`。用户干预消息角色为 `user`。 + +**压缩策略**: 当历史 token 数超过阈值(默认 4000)时,主持人先压缩历史(保留每轮关键观点),再继续下一轮。压缩 prompt 在实现时细化。 + +### KTD-3: 并行发言生成 + +**决策**: 使用 `asyncio.gather` 并行生成所有非主持人专家的发言,然后按专家列表顺序追加到历史和广播事件。 + +**理由**: 并行生成提高效率(5 专家并行 vs 串行)。前端展示顺序按配置顺序,即使并行生成也按顺序追加到 UI。 + +### KTD-4: 主持人角色 + +**决策**: 主持人默认为首位专家,通过 `ExpertConfig.is_lead=True` 标识。主持人负责开场介绍、每轮小结、最终总结。 + +**理由**: 复用现有 `is_lead` 字段,无需引入新角色。主持人也是名人专家之一,其发言风格由其 persona 决定。 + +### KTD-5: WebSocket 事件复用与扩展 + +**决策**: 新增 5 个事件类型(`board_started`、`expert_speech`、`round_summary`、`user_intervention`、`board_concluded`),加入现有 `_VALID_TEAM_EVENT_TYPES` 集合。复用 `emit_team_event()` 辅助函数推送。 + +**理由**: 现有 `emit_team_event()` 已定义但未被调用,本期同时完成其调用集成。事件类型加入现有集合,复用验证逻辑。 + +### KTD-6: 配置加载 + +**决策**: 在 `app.py` 启动时,类似 skills 加载,从 `configs/experts/` 目录加载所有专家 YAML 到 `ExpertTemplateRegistry`,挂载到 `app.state.expert_template_registry`。 + +**理由**: 复用 `ExpertTemplateRegistry.load_from_directory()` 方法。与 skills 加载模式一致,保持架构一致性。 + +--- + +## High-Level Technical Design + +### 组件关系图 + +```mermaid +graph TB + User[用户输入 @board:experts 主题] + Router[BoardRouter] + Registry[ExpertTemplateRegistry] + Team[BoardTeam] + Orchestrator[BoardOrchestrator] + Expert1[Expert: 马斯克] + Expert2[Expert: 贝佐斯] + Expert3[Expert: 张小龙] + Transport[HandoffTransport] + ChatWS[chat.py WebSocket] + Frontend[前端群聊 UI] + + User --> Router + Router --> Registry + Registry --> Team + Team --> Orchestrator + Orchestrator --> Expert1 + Orchestrator --> Expert2 + Orchestrator --> Expert3 + Expert1 --> Transport + Expert2 --> Transport + Expert3 --> Transport + Transport --> ChatWS + ChatWS --> Frontend +``` + +### 讨论流程状态机 + +```mermaid +stateDiagram-v2 + [*] --> FORMING: @board 触发 + FORMING --> DISCUSSING: 专家创建完成 + DISCUSSING --> DISCUSSING: 每轮发言+小结 + DISCUSSING --> CONCLUDING: 达到最大轮次/用户停止 + DISCUSSING --> DISSOLVED: 异常终止 + CONCLUDING --> COMPLETED: 主持人最终总结 + COMPLETED --> DISSOLVED: 资源回收 + DISSOLVED --> [*] +``` + +### 单轮讨论时序图 + +```mermaid +sequenceDiagram + participant O as BoardOrchestrator + participant E1 as Expert 1 + participant E2 as Expert 2 + participant M as Moderator + participant T as Transport + participant WS as WebSocket + + O->>WS: board_started 事件 + O->>M: 开场介绍请求 + M->>T: 发言内容 + T->>WS: expert_speech (moderator) + + loop 每轮讨论 + par 并行生成发言 + O->>E1: 发言请求(历史+角色prompt) + E1-->>O: 发言内容 + and + O->>E2: 发言请求(历史+角色prompt) + E2-->>O: 发言内容 + end + O->>T: 广播 E1 发言 + T->>WS: expert_speech (E1) + O->>T: 广播 E2 发言 + T->>WS: expert_speech (E2) + O->>M: 小结请求(本轮发言) + M-->>O: 小结内容 + O->>T: 广播小结 + T->>WS: round_summary + end + + O->>M: 最终总结请求 + M-->>O: 决策建议 + O->>WS: board_concluded 事件 +``` + +--- + +## Scope Boundaries + +### In Scope + +- `BoardTeam`、`BoardOrchestrator`、`BoardRouter` 新模块 +- `configs/experts/` 预设名人 YAML(8 位) +- `ExpertConfig` 扩展(`speaking_style`、`decision_framework`) +- WebSocket 事件(5 个新事件)+ `emit_team_event()` 调用集成 +- 前端群聊式展示增强 + 事件处理 +- 主聊天流程集成(`chat.py` 接入 `BoardRouter`) +- `app.py` 启动加载专家配置 +- `agentkit.yaml` 配置项 +- 单元测试覆盖 + +### Deferred to Follow-Up Work + +- 集成外部蒸馏工具(colleague.skill/nuwa-skill) +- LLM 自动蒸馏生成名人 SOUL +- 共识检测自动终止(置信度/投票) +- 技术评审/创意脑暴场景 +- Expert Team Canvas 式可视化 +- 专家间直接通信(辩论模式) +- 语音/视频输出 +- 历史讨论回顾与检索 +- 修复 `@team` 在 `chat.py` 的集成缺口(独立任务) + +### Outside This Product's Identity + +- 实时名人数据更新 +- 名人本人授权或验证 +- 娱乐向角色扮演 + +--- + +## Implementation Units + +### U1. 扩展 ExpertConfig 新增讨论模式字段 + +**Goal**: 为 `ExpertConfig` 新增 `speaking_style` 和 `decision_framework` 字段,支持名人专家的个性化表达和决策框架。 + +**Requirements**: FR-4 + +**Dependencies**: 无 + +**Files**: +- `src/agentkit/experts/config.py` — 修改 `ExpertConfig.__init__`、`from_dict`、`to_dict` +- `tests/unit/experts/test_config.py` — 新增字段测试 + +**Approach**: +- 在 `ExpertConfig.__init__` 新增 `speaking_style: str = ""` 和 `decision_framework: str = ""` 参数 +- 在 `from_dict` 中读取 `data.get("speaking_style", "")` 和 `data.get("decision_framework", "")` +- 在 `to_dict` 中序列化这两个字段 +- 字段有默认值,向后兼容现有 `@team` 模式和动态生成的 ExpertConfig + +**Patterns to follow**: 现有 `persona`、`thinking_style` 字段的实现模式(`src/agentkit/experts/config.py:35-65`) + +**Test scenarios**: +- **Happy path**: 创建 ExpertConfig 时传入 speaking_style 和 decision_framework,验证字段值正确 +- **Happy path**: 从 dict 创建 ExpertConfig,包含 speaking_style 和 decision_framework,验证字段值正确 +- **Edge case**: 不传 speaking_style 和 decision_framework,验证默认值为空字符串 +- **Edge case**: to_dict 输出包含 speaking_style 和 decision_framework 字段 +- **Integration**: 现有 ExpertConfig 用法(不传新字段)仍正常工作 + +**Verification**: `pytest tests/unit/experts/test_config.py` 通过,现有测试不受影响 + +--- + +### U2. 创建预设名人专家 YAML 库 + +**Goal**: 在 `configs/experts/` 目录下创建 8 位名人专家 YAML 文件,每位名人包含 persona、thinking_style、speaking_style、decision_framework 等字段。 + +**Requirements**: FR-3, FR-5 + +**Dependencies**: U1 + +**Files**: +- `configs/experts/elon_musk.yaml` +- `configs/experts/jeff_bezos.yaml` +- `configs/experts/allenzhang.yaml` +- `configs/experts/charlie_munger.yaml` +- `configs/experts/paul_graham.yaml` +- `configs/experts/steve_jobs.yaml` +- `configs/experts/warren_buffett.yaml` +- `configs/experts/ray_dalio.yaml` +- `configs/experts/private_board.yaml` — 默认私董会模板(引用上述专家) + +**Approach**: +- 每个 YAML 遵循 `ExpertTemplateRegistry.load_from_yaml` 的格式(见 `src/agentkit/experts/registry.py:71-87`) +- 字段:`name`、`description`、`is_builtin: true`、`config`(含 `name`、`agent_type: expert`、`persona`、`thinking_style`、`speaking_style`、`decision_framework`、`avatar`、`color`、`is_lead: false`、`task_mode: llm_generate`、`prompt.identity`) +- `private_board.yaml` 是一个特殊的"团队模板",定义默认专家组合(3-5 位),格式为 `name: private_board`、`members: [elon_musk, jeff_bezos, allenzhang, charlie_munger, paul_graham]` +- 名人 persona 内容需体现其标志性思维模式: + - 马斯克:第一性原理、物理思维、激进创新 + - 贝佐斯:Day 1 思维、客户至上、长期主义 + - 张小龙:用户体验、极简主义、社交产品直觉 + - 芒格:心智模型、跨学科思维、逆向思考 + - Paul Graham:创业、做用户想要的东西、反从众 + - 乔布斯:产品设计、现实扭曲力场、专注 + - 巴菲特:价值投资、能力圈、复利思维 + - Ray Dalio:原则驱动决策、极度透明 + +**Patterns to follow**: `src/agentkit/experts/registry.py:71-87` 的 YAML 格式示例 + +**Test scenarios**: +- **Test expectation**: none — YAML 配置文件,由 U9 的集成测试覆盖加载逻辑 + +**Verification**: `ExpertTemplateRegistry.load_from_directory("configs/experts/")` 能成功加载所有 8 位专家 + 1 个团队模板 + +--- + +### U3. 实现 BoardRouter @board 前缀路由 + +**Goal**: 实现 `BoardRouter` 类,解析 `@board` 前缀,支持指定专家或使用默认私董会模板。 + +**Requirements**: FR-1, FR-2 + +**Dependencies**: U1, U2 + +**Files**: +- `src/agentkit/experts/board_router.py` — 新建 +- `tests/unit/experts/test_board_router.py` — 新建 + +**Approach**: +- 参考 `ExpertTeamRouter`(`src/agentkit/experts/router.py`)的实现模式 +- 正则匹配 `@board` 前缀:`^@board(?::(\S+))?\s*(.*)` +- 支持两种格式: + - `@board:elon_musk,jeff_bezos 讨论主题` — 指定专家 + - `@board 讨论主题` — 使用默认 `private_board` 模板 +- 专家名验证:复用 `_EXPERT_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")` +- 最多 10 位专家(`MAX_EXPERTS = 10`) +- 返回 `BoardRoutingResult` dataclass:`matched`、`board_mode`、`specified_experts`、`topic`、`use_default_template` +- `resolve_expert_configs()` 方法:从 `ExpertTemplateRegistry` 解析专家名到 `ExpertConfig` 列表,首位设为 `is_lead=True`(主持人) +- 如果指定 `private_board`,从 `private_board.yaml` 加载成员列表 +- 未识别的专家名:记录警告,动态创建基本 ExpertConfig + +**Patterns to follow**: `src/agentkit/experts/router.py` 的 `ExpertTeamRouter` 实现 + +**Test scenarios**: +- **Happy path**: `@board:elon_musk,jeff_bezos 讨论主题` → matched=True, specified_experts=["elon_musk", "jeff_bezos"], topic="讨论主题" +- **Happy path**: `@board 讨论主题` → matched=True, use_default_template=True, topic="讨论主题" +- **Happy path**: `@board:private_board 讨论主题` → matched=True, 加载 private_board 成员列表 +- **Edge case**: `@board` 无主题 → matched=True, topic 为空 +- **Edge case**: 专家名超过 10 个 → 截断到 10 个 +- **Error path**: 无效专家名(含特殊字符)→ 过滤掉无效名,记录警告 +- **Error path**: 指定不存在的专家 → 动态创建基本 ExpertConfig +- **Integration**: `resolve_expert_configs()` 返回的列表首位 is_lead=True + +**Verification**: `pytest tests/unit/experts/test_board_router.py` 通过 + +--- + +### U4. 实现 BoardTeam 容器 + +**Goal**: 实现 `BoardTeam` 容器,管理私董会的专家生命周期、讨论状态和事件广播。 + +**Requirements**: FR-6, FR-8 + +**Dependencies**: U1 + +**Files**: +- `src/agentkit/experts/board.py` — 新建(`BoardTeam`、`BoardStatus`、`DiscussionHistory`) +- `tests/unit/experts/test_board.py` — 新建 + +**Approach**: +- 参考 `ExpertTeam`(`src/agentkit/experts/team.py`)的容器模式 +- `BoardStatus` 枚举:`FORMING` → `DISCUSSING` → `CONCLUDING` → `COMPLETED` → `DISSOLVED` +- `BoardTeam` 持有: + - `team_id`、`topic`、`experts: dict[str, Expert]`、`moderator_name: str` + - `history: list[dict]` — 讨论历史(每条含 round、expert_name、content、timestamp、role) + - `current_round: int`、`max_rounds: int` + - `_handoff_transport`、`_workspace`、`_pool`、`_team_channel` +- `create_board()` 方法:创建主持人和成员专家,注入 board_context 到 system prompt +- `_build_board_context()` 方法:构建私董会上下文(强调群聊讨论模式、角色差异、讨论规则) +- `add_to_history()` 方法:追加发言到历史 +- `get_history_text()` 方法:返回格式化的历史文本用于 LLM prompt +- `compress_history()` 方法:主持人压缩历史(超过阈值时) +- `broadcast_user_message()` 方法:广播用户干预消息 +- `dissolve()` 方法:解散团队,回收资源 +- 复用 `Expert.create()`、`HandoffTransport`、`SharedWorkspace` + +**Patterns to follow**: `src/agentkit/experts/team.py` 的 `ExpertTeam` 容器模式 + +**Technical design** (directional): +```python +class BoardStatus(str, enum.Enum): + FORMING = "forming" + DISCUSSING = "discussing" + CONCLUDING = "concluding" + COMPLETED = "completed" + DISSOLVED = "dissolved" + +class BoardTeam: + async def create_board(self, topic: str, expert_configs: list[ExpertConfig], max_rounds: int) -> None + async def add_to_history(self, round: int, expert_name: str, content: str, role: str) -> None + def get_history_text(self, up_to_round: int | None = None) -> str + async def compress_history(self, moderator: Expert) -> None + async def broadcast_user_message(self, content: str) -> None + async def dissolve(self) -> None +``` + +**Test scenarios**: +- **Happy path**: `create_board()` 创建主持人和成员,状态变为 DISCUSSING +- **Happy path**: `add_to_history()` 追加发言,`get_history_text()` 返回格式化文本 +- **Edge case**: 空历史时 `get_history_text()` 返回空字符串 +- **Edge case**: `compress_history()` 超过阈值时压缩,未超过时不操作 +- **Integration**: `broadcast_user_message()` 通过 handoff_transport 发送事件 +- **Integration**: `dissolve()` 清理所有专家,状态变为 DISSOLVED +- **Error path**: 未配置 AgentPool 时 `create_board()` 抛出 RuntimeError + +**Verification**: `pytest tests/unit/experts/test_board.py` 通过 + +--- + +### U5. 实现 BoardOrchestrator 讨论引擎 + +**Goal**: 实现 `BoardOrchestrator`,驱动私董会讨论流程:开场 → 多轮并行发言 + 主持人小结 → 最终决策建议。 + +**Requirements**: FR-7, FR-9 + +**Dependencies**: U4 + +**Files**: +- `src/agentkit/experts/board_orchestrator.py` — 新建 +- `tests/unit/experts/test_board_orchestrator.py` — 新建 + +**Approach**: +- 参考 `TeamOrchestrator`(`src/agentkit/experts/orchestrator.py`)的执行引擎模式 +- `execute(topic)` 主入口流程: + 1. 广播 `board_started` 事件 + 2. 主持人开场介绍(介绍议题、讨论规则) + 3. 循环 `max_rounds` 轮: + - 并行生成所有非主持人专家发言(`asyncio.gather`) + - 每个专家发言基于:角色 persona + thinking_style + speaking_style + decision_framework + 完整讨论历史 + 当前轮次/最大轮次 + - 按专家列表顺序广播 `expert_speech` 事件 + - 主持人小结本轮要点,广播 `round_summary` 事件 + - 检查用户干预消息(通过 handoff_transport 或共享状态) + - 检查历史 token 长度,超过阈值时压缩 + 4. 主持人最终总结(决策建议、共识点、分歧点),广播 `board_concluded` 事件 +- `_generate_expert_speech()` 方法:构建专家发言 prompt,调用 LLM +- `_generate_moderator_summary()` 方法:构建主持人小结 prompt,调用 LLM +- `_generate_final_conclusion()` 方法:构建最终总结 prompt,调用 LLM +- `_check_user_intervention()` 方法:检查是否有用户干预消息 +- `_handle_stop_command()` 方法:处理用户 `/stop` 命令 +- 复用 `_get_llm_gateway()` 和 `_broadcast_event()` 模式(来自 TeamOrchestrator) +- 异常处理:LLM 不可用时,主持人用已有历史总结;所有专家发言失败时,提前进入总结阶段 + +**Patterns to follow**: `src/agentkit/experts/orchestrator.py` 的 `TeamOrchestrator` 执行模式 + +**Technical design** (directional): +```python +class BoardOrchestrator: + def __init__(self, team: BoardTeam) -> None + + async def execute(self, topic: str) -> dict[str, Any] + # Returns: {status, summary, decision_advice, total_rounds, consensus_points, dissent_points} + + async def _generate_expert_speech(self, expert: Expert, round: int) -> str + async def _generate_moderator_summary(self, round: int) -> str + async def _generate_final_conclusion(self) -> dict[str, Any] + async def _check_user_intervention(self) -> str | None +``` + +**Test scenarios**: +- **Happy path**: `execute()` 完成完整讨论流程,返回 status="completed" +- **Happy path**: 每轮生成 N-1 个专家发言 + 1 个主持人小结 +- **Happy path**: 最终总结包含 decision_advice、consensus_points、dissent_points +- **Edge case**: max_rounds=1 时,只进行一轮讨论后直接总结 +- **Edge case**: 用户发送 `/stop` → 提前终止,用已有历史总结 +- **Error path**: LLM 不可用 → 主持人用已有历史拼接总结 +- **Error path**: 某专家发言失败 → 跳过该专家,其他专家继续 +- **Error path**: 所有专家发言失败 → 提前进入总结阶段 +- **Integration**: `board_started`、`expert_speech`、`round_summary`、`board_concluded` 事件正确广播 + +**Verification**: `pytest tests/unit/experts/test_board_orchestrator.py` 通过 + +--- + +### U6. 后端集成 BoardRouter 到主聊天流程 + +**Goal**: 在 `chat.py` 的 WebSocket 处理中接入 `BoardRouter`,实现 `@board` 前缀触发私董会讨论,并通过 `emit_team_event()` 推送事件到前端。 + +**Requirements**: FR-1, FR-10, FR-11, FR-15 + +**Dependencies**: U3, U5 + +**Files**: +- `src/agentkit/server/routes/chat.py` — 修改 `_handle_chat_message`,接入 `BoardRouter` +- `src/agentkit/server/app.py` — 启动时加载 `configs/experts/`,挂载 `expert_template_registry` 和 `board_config` 到 `app.state` +- `src/agentkit/server/config.py` — 新增 `board` 配置项(max_rounds、default_template、parallel_speech、history_compression_threshold) +- `tests/unit/server/test_chat_board_integration.py` — 新建 + +**Approach**: +- 在 `app.py` 启动时(参考 skills 加载逻辑 `app.py:261-282`): + - 创建 `ExpertTemplateRegistry` 实例 + - 调用 `load_from_directory("configs/experts/")` 加载所有专家 YAML + - 挂载到 `app.state.expert_template_registry` + - 从 `agentkit.yaml` 读取 `board` 配置,挂载到 `app.state.board_config` +- 在 `chat.py` 的 `_handle_chat_message` 中: + - 在 `RequestPreprocessor` 之前,检查 `@board` 前缀 + - 如果匹配 `@board`,创建 `BoardRouter`,解析路由 + - 创建 `BoardTeam` 和 `BoardOrchestrator` + - 注册 `handoff_transport` 的 handler,将事件转发到 WebSocket(调用 `emit_team_event()`) + - 调用 `orchestrator.execute(topic)` + - 将最终结果作为 `final_answer` 发送 +- 扩展 `_VALID_TEAM_EVENT_TYPES`:新增 `board_started`、`expert_speech`、`round_summary`、`user_intervention`、`board_concluded` +- 用户干预处理:讨论进行中,如果用户发送新消息(非 `/stop`),通过 `board_team.broadcast_user_message()` 广播 + +**Patterns to follow**: +- skills 加载模式:`src/agentkit/server/app.py:261-282` +- `emit_team_event()` 辅助函数:`src/agentkit/server/routes/chat.py:117-142` + +**Test scenarios**: +- **Happy path**: `@board:elon_musk,jeff_bezos 讨论主题` → 触发 BoardRouter,创建 BoardTeam,执行讨论 +- **Happy path**: 讨论事件通过 `emit_team_event()` 推送到 WebSocket +- **Happy path**: `@board 讨论主题` → 使用默认 private_board 模板 +- **Edge case**: 讨论中用户发送消息 → 广播为 user_intervention +- **Edge case**: 讨论中用户发送 `/stop` → 终止讨论 +- **Integration**: `app.state.expert_template_registry` 在启动时正确加载 +- **Integration**: `@board` 与 `@team` 和普通聊天互不干扰 + +**Verification**: `pytest tests/unit/server/test_chat_board_integration.py` 通过;手动测试 `@board` 触发讨论 + +--- + +### U7. 前端事件类型和处理扩展 + +**Goal**: 在前端 TypeScript 类型和 Pinia store 中新增私董会事件类型和处理逻辑。 + +**Requirements**: FR-10, FR-12, FR-13 + +**Dependencies**: U6 + +**Files**: +- `src/agentkit/server/frontend/src/api/types.ts` — 新增 board 事件类型和接口 +- `src/agentkit/server/frontend/src/stores/chat.ts` — 新增 board 事件处理 +- `src/agentkit/server/frontend/src/stores/team.ts` — 新增 board 状态管理(可选,或新建 `stores/board.ts`) + +**Approach**: +- 在 `types.ts` 中: + - 新增 `WsServerMessage` 联合类型成员:`board_started`、`expert_speech`、`round_summary`、`user_intervention`、`board_concluded` + - 新增 `IBoardState` 接口:`team_id`、`topic`、`experts`、`moderator_name`、`current_round`、`max_rounds`、`status` + - 新增 `IBoardSpeech` 接口:`expert_name`、`expert_avatar`、`expert_color`、`content`、`round`、`role` +- 在 `chat.ts` 的 `handleWsMessage` 中新增 case: + - `board_started`: 创建 board 状态,推送步骤提示 + - `expert_speech`: 创建专家消息(带头像、颜色、轮次标识),追加到消息列表 + - `round_summary`: 创建主持人小结消息(特殊样式,message_type='milestone') + - `user_intervention`: 标记用户消息已广播 + - `board_concluded`: 创建总结消息(含决策建议、共识点、分歧点) +- 参考 `chat.ts:554-646` 的现有 team 事件处理模式 + +**Patterns to follow**: `src/agentkit/server/frontend/src/stores/chat.ts:554-646` 的 team 事件处理 + +**Test scenarios**: +- **Test expectation**: none — 前端 TypeScript 类型,由 U8 的组件测试和手动测试覆盖 + +**Verification**: `npm run typecheck` 通过 + +--- + +### U8. 前端群聊式 UI 增强 + +**Goal**: 增强 `ExpertMessage.vue` 组件,新增讨论状态展示,实现群聊式体验。 + +**Requirements**: FR-12, FR-13, FR-14 + +**Dependencies**: U7 + +**Files**: +- `src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue` — 增强(新增轮次标识、角色标签) +- `src/agentkit/server/frontend/src/components/chat/BoardStatusView.vue` — 新建(讨论状态展示) +- `src/agentkit/server/frontend/src/views/ChatView.vue` — 集成 BoardStatusView + +**Approach**: +- 增强 `ExpertMessage.vue`: + - 新增 props:`round?: number`、`role?: 'expert' | 'moderator' | 'user'` + - 显示轮次标识(如"第 2 轮") + - 主持人消息特殊样式(背景色、边框) + - 用户干预消息右侧气泡展示 +- 新建 `BoardStatusView.vue`: + - 顶部显示:讨论主题、当前轮次/最大轮次、参与专家列表(头像、名称、角色标签) + - 专家列表可点击查看其所有发言 + - 讨论结束后展示总结卡片(决策建议、共识点、分歧点) +- 在 `ChatView.vue` 中: + - 当 board 状态激活时,渲染 ``(类似现有 ``) + +**Patterns to follow**: +- `src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue` 现有组件 +- `src/agentkit/server/frontend/src/components/chat/ExpertTeamView.vue` 团队视图 +- `src/agentkit/server/frontend/src/views/ChatView.vue:19` 的 ExpertTeamView 集成 + +**Test scenarios**: +- **Test expectation**: none — Vue 组件,由手动测试覆盖 + +**Verification**: `npm run typecheck` 通过;手动测试 `@board` 触发讨论,前端正确展示群聊 + +--- + +### U9. 单元测试覆盖 + +**Goal**: 为所有新增模块编写单元测试,确保覆盖率 ≥ 80%。 + +**Requirements**: 成功标准 §7.1 + +**Dependencies**: U1-U8 + +**Files**: +- `tests/unit/experts/test_config.py` — 扩展(U1 新字段测试) +- `tests/unit/experts/test_board_router.py` — 新建(U3) +- `tests/unit/experts/test_board.py` — 新建(U4) +- `tests/unit/experts/test_board_orchestrator.py` — 新建(U5) +- `tests/unit/server/test_chat_board_integration.py` — 新建(U6) + +**Approach**: +- 使用 `pytest` + `pytest-asyncio`(asyncio_mode=auto) +- Mock `LLMGateway`、`AgentPool`、`HandoffTransport` 进行隔离测试 +- 测试覆盖: + - 配置字段序列化/反序列化 + - 路由解析各种格式 + - BoardTeam 生命周期和状态转换 + - BoardOrchestrator 讨论流程(正常、异常、用户干预) + - chat.py 集成(@board 触发、事件广播) +- 测试标记:单元测试无特殊标记,集成测试标记 `@pytest.mark.integration` + +**Patterns to follow**: +- `tests/unit/experts/test_team_orchestrator.py` 的测试模式 +- `tests/unit/experts/test_router.py` 的路由测试模式 + +**Test scenarios**: +- 见各实现单元的测试场景 + +**Verification**: `pytest tests/unit/experts/ tests/unit/server/test_chat_board_integration.py -v` 全部通过;`pytest --cov=src/agentkit/experts --cov-report=term-missing` 覆盖率 ≥ 80% + +--- + +## Risks & Dependencies + +### Risks + +| 风险 | 影响 | 缓解 | +|------|------|------| +| 专家发言同质化 | 讨论质量低 | 强化角色 prompt 差异化,`decision_framework` 字段强制不同视角 | +| token 消耗过高(5轮×5专家=25次调用) | 成本问题 | 提供轮次配置,历史压缩机制 | +| 讨论发散无结论 | 用户体验差 | 主持人每轮小结,最终强制总结 | +| 名人 persona 不准确 | 发言不像本人 | YAML 可迭代优化,未来支持蒸馏 | +| `chat.py` 集成复杂度高 | 可能破坏现有聊天 | `@board` 路由在 `RequestPreprocessor` 之前检查,不影响普通聊天 | +| 前端事件处理与现有 team 事件冲突 | UI 错乱 | 事件类型独立(board_* vs team_*),store 独立 | + +### Dependencies + +- **现有基础设施**: `Expert`、`AgentPool`、`HandoffTransport`、`SharedWorkspace`、`ExpertTemplateRegistry` +- **LLM Gateway**: `src/agentkit/llm/gateway.py` 提供多 provider 支持 +- **WebSocket**: `src/agentkit/server/routes/chat.py` 现有 WebSocket 通道 +- **前端组件**: `ExpertMessage.vue`、`ChatView.vue`、`stores/chat.ts` + +--- + +## System-Wide Impact + +### 影响方 + +- **终端用户**: 获得私董会讨论能力,可就决策类问题获得多视角建议 +- **开发者**: 新增 `@board` 路由模式,需了解与 `@team` 的区别 +- **配置管理**: 新增 `configs/experts/` 目录和 `agentkit.yaml` 的 `board` 配置项 +- **前端**: 新增 board 事件处理和 UI 组件 + +### 兼容性 + +- `ExpertConfig` 新增字段有默认值,向后兼容 +- `@board` 路由独立,不影响 `@team` 和普通聊天 +- `_VALID_TEAM_EVENT_TYPES` 扩展是增量式,不影响现有事件 +- 前端事件处理新增 case,不影响现有 case + +--- + +## Open Questions + +1. **历史压缩 prompt 具体设计** — 超过 token 阈值时,主持人如何压缩历史? + - **决策**: 实现时细化,初步思路是让主持人总结每轮关键观点,替换原始发言。 + +2. **用户干预消息的实时性** — 用户发送干预后,当前轮次是否立即中断? + - **决策**: 不中断当前轮次,干预消息在下一轮生效。避免复杂的中断逻辑。 + +3. **private_board 模板格式** — 如何在 YAML 中定义"团队模板"(引用多个专家)? + - **决策**: `private_board.yaml` 使用 `members: [expert1, expert2, ...]` 字段,`BoardRouter` 解析时加载成员配置。 + +4. **讨论历史持久化** — 讨论结束后是否保存到数据库? + - **决策**: 本期不持久化,仅保存到 SharedWorkspace。未来可扩展。 + +--- + +## Sources & Research + +- **需求文档**: `docs/brainstorms/2026-06-17-board-meeting-mode-requirements.md` +- **现有实现**: `src/agentkit/experts/` 目录所有文件 +- **对标产品**: Qoder Experts Mode、WorkBuddy、Legends MCP、colleague.skill(详见需求文档 §1.2) +- **架构参考**: AgentVerse 通信框架、ARMOR-MAD 协议阈值终止 + +--- + +**Next Step**: 交由 `/ce-work` 执行实现,或由开发者按 U1-U9 顺序逐步实现。 diff --git a/docs/plans/2026-06-17-002-fix-ws-task-persistence-plan.md b/docs/plans/2026-06-17-002-fix-ws-task-persistence-plan.md new file mode 100644 index 0000000..7fa4b0a --- /dev/null +++ b/docs/plans/2026-06-17-002-fix-ws-task-persistence-plan.md @@ -0,0 +1,433 @@ +--- +title: "fix: WebSocket 断开后任务结果丢失与断线恢复" +status: completed +created: 2026-06-17 +type: fix +origin: in-session investigation +--- + +# fix: WebSocket 断开后任务结果丢失与断线恢复 + +## Summary + +当用户在复杂任务执行过程中刷新页面时,WebSocket 断开导致 ReAct 任务中断、已收集的输出丢失、且无法恢复。本计划通过三层防御彻底解决:Layer 1 确保部分结果持久化,Layer 2 将任务后台化解耦 WebSocket 生命周期,Layer 3 前端断线后恢复进行中任务。 + +## Problem Frame + +**症状**:用户布置复杂任务后刷新界面,操作停止且返回为空。 + +**根因**(经代码调查确认): + +1. **结果丢失**:portal.py 的 ReAct 流式路径中,assistant 回复保存在 `async for` 循环之后(portal.py:1064),WebSocket 断开时该行永远不执行,`collected_output` 全部丢失。 +2. **任务中断**:ReAct 任务通过 `async for event in react_engine.execute_stream(...)` 直接在 WebSocket 协程中执行,未使用 BackgroundRunner 后台化,WebSocket 断开 = 任务取消。 +3. **无状态追踪**:portal WebSocket 路径不使用 TaskStore,`task_id` 仅用于 EventQueue 事件发射,任务状态无法查询。 +4. **无恢复机制**:前端 WebSocket 重连后(3 秒自动重连),不检查是否有未完成任务,不恢复进行中的任务状态。 + +## Requirements + +- **R1**: WebSocket 断开时,已收集的 ReAct 输出必须持久化到 conversation store +- **R2**: ReAct 任务必须后台执行,与 WebSocket 连接生命周期解耦 +- **R3**: 每个任务必须在 TaskStore 中注册,状态可查询 +- **R4**: 后台任务的事件必须通过 EventQueue 分发,WebSocket 可订阅 +- **R5**: 前端 WebSocket 重连后,必须检查当前对话是否有未完成任务并恢复 +- **R6**: 已完成的任务结果必须能从 conversation store 恢复显示 +- **R7**: 不破坏现有 DIRECT_CHAT 路径和 REST API 路径 + +## Key Technical Decisions + +### KTD1: 复用 EventQueue 作为任务事件总线 + +**决策**:使用现有的 `EventQueue`(`core/event_queue.py`)分发后台任务事件,而非新建 EventBus 模块。 + +**理由**:EventQueue 已具备所需能力——多订阅者广播(行 161-175)、缓冲回放(deque 100,行 153)、原子订阅(行 193-202)、哨兵关闭模式(行 230-243)。它已被 portal.py 的 `_emit_event_safe` 使用,且文件头注释明确说明设计目标包括 Portal WebSocket 订阅。 + +### KTD2: 任务后台化使用 asyncio.create_task + EventQueue 订阅 + +**决策**:在 portal.py 中,将 ReAct 执行包装为 `asyncio.create_task`,WebSocket 协程通过 `event_queue.subscribe()` 订阅事件流。任务在后台独立运行,WebSocket 仅转发事件给前端。 + +**理由**:BackgroundRunner(`server/runner.py`)虽然存在,但它调用 `agent.execute()`(非流式),不支持事件流。改造 BackgroundRunner 支持流式会侵入核心执行路径。直接在 portal.py 中用 `asyncio.create_task` 更聚焦、风险更低。 + +### KTD3: 任务状态通过 TaskStore 追踪 + +**决策**:在 portal.py WebSocket 路径中,为每个用户消息创建 TaskStore 记录,状态随任务进展更新(PENDING → RUNNING → COMPLETED/FAILED)。 + +**理由**:TaskStore 已在 `app.state.task_store` 可用,`/api/v1/tasks/{task_id}` 端点已存在。前端可通过现有 REST API 查询任务状态,无需新建端点。 + +### KTD4: 前端通过 conversation_id 关联任务 + +**决策**:在 TaskStore 记录的 `metadata` 中存储 `conversation_id`,前端重连后通过 `GET /api/v1/tasks?status=running` 查找当前对话的未完成任务。 + +**理由**:避免新建专门的"按 conversation 查任务"端点。前端遍历 running 任务,匹配 metadata 中的 conversation_id 即可。 + +## High-Level Technical Design + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ WebSocket 连接存活 │ +│ ┌────────────┐ subscribe(task_id) ┌──────────────────┐ │ +│ │ WebSocket │ ◄────────────────────── │ EventQueue │ │ +│ │ 协程 │ 转发事件给前端 │ (缓冲回放+广播) │ │ +│ └────────────┘ └────────┬─────────┘ │ +│ │ emit │ +├──────────────────────────────────────────────────┼─────────────┤ +│ WebSocket 断开 │ │ +│ ┌────────────┐ │ │ +│ │ WebSocket │ ✗ 断开 │ │ +│ │ 协程 │ │ │ +│ └────────────┘ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 后台任务 (asyncio.create_task) │ │ +│ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │ │ +│ │ │ ReAct 执行 │─►│ EventQueue │ │ TaskStore │ │ │ +│ │ │ execute_ │ │ .emit(event) │ │ .update_status │ │ │ +│ │ │ stream() │ │ │ │ │ │ │ +│ │ └─────────────┘ └──────────────┘ └────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────────────────────┐ │ │ +│ │ │ conversation_store.add_message(assistant, ...) │ │ │ +│ │ │ (无论 WebSocket 是否存活,结果都持久化) │ │ │ +│ │ └────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ 前端重连后: │ +│ 1. GET /api/v1/tasks?status=running → 查找未完成任务 │ +│ 2. 匹配 metadata.conversation_id → 当前对话的任务 │ +│ 3. event_queue.subscribe(task_id) → 恢复事件流 │ +│ 4. 或 GET /conversations/{id} → 拉取已完成的结果 │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## Scope Boundaries + +### In Scope + +- portal.py WebSocket 路径的 ReAct 任务后台化 +- WebSocket 断开时的部分结果持久化 +- TaskStore 注册和状态更新 +- EventQueue 事件分发集成 +- 前端 WebSocket 重连后的任务恢复 +- 前端任务状态查询 API 客户端 + +### Out of Scope + +- BackgroundRunner 改造支持流式(风险过高,单独处理) +- REST API 路径(`/portal/chat`)的改造(REST 已是同步调用,无此问题) +- SSE 路径(`/portal/chat/stream`)的改造 +- Expert Team 协作模式的后台化(单独处理) +- WebSocket 重连退避策略优化(独立改进) + +### Deferred to Follow-Up Work + +- SubmissionQueue 接入(目前完全闲置,可后续用于任务队列化) +- Redis 分布式任务恢复(当前 InMemoryTaskStore 足够,分布式场景后续处理) +- 任务进度百分比反馈(当前 ReAct 无细粒度进度概念) + +--- + +## Implementation Units + +### U1. Layer 1: WebSocket 断开时持久化已收集的输出 + +**Goal**: 确保 WebSocket 在 ReAct 流式过程中断开时,已收集的 `collected_output` 保存到 conversation store。 + +**Requirements**: R1 + +**Dependencies**: 无 + +**Files**: +- `src/agentkit/server/routes/portal.py` — 修改 ReAct 流式路径的异常处理 +- `tests/unit/test_portal_ws_persistence.py` — 新建测试 + +**Approach**: + +在 portal.py 的 `portal_websocket` 函数中,ReAct 流式路径(约 1014-1064 行)的异常处理需要增强: + +1. 在 `except Exception as e:` 块(约 1049 行)中,在发送 error 之前,将 `collected_output` 保存到 `_conversation_store`。使用 `_ensure_non_empty` 处理空输出。 +2. 在外层 `except WebSocketDisconnect:` 块(约 1102 行)中,检查是否有未保存的 `collected_output`,如果有则保存。 +3. 在外层 `except Exception as e:` 块(约 1104 行)中,同样处理。 +4. 需要将 `collected_output`、`conv`、`task_id` 声明提升到外层 try 作用域,使 except 块可以访问。 + +**Patterns to follow**: +- `_ensure_non_empty()` 函数(portal.py:58-62)处理空输出 +- `_conversation_store.add_message()` 已有调用模式(portal.py:871, 952, 1064) +- `_emit_event_safe()` 的异常吞咽模式(portal.py:65-95)——保存操作失败不应阻断后续清理 + +**Test scenarios**: +- **Happy path**: ReAct 正常完成,`collected_output` 有内容,WebSocket 未断开 → 结果保存到 conversation store(验证现有行为不被破坏) +- **WebSocket 断开(final_answer 前)**: ReAct 流式中 WebSocket 断开,`collected_output` 为空 → conversation store 不写入空消息(或写入 EMPTY_LLM_RESPONSE) +- **WebSocket 断开(final_answer 后)**: ReAct 已产出 final_answer,WebSocket 在后续步骤断开,`collected_output` 有部分内容 → 部分内容保存到 conversation store +- **异常路径**: ReAct 执行抛出异常,`collected_output` 有部分内容 → 部分内容保存,error 事件正常发射 +- **Integration**: 保存操作自身失败(conversation_store 异常)→ 不阻断 error 事件发射和后续清理 + +**Verification**: 模拟 WebSocket 断开场景,检查 SQLite conversation store 中是否有 assistant 消息记录。 + +--- + +### U2. Layer 2: ReAct 任务后台化与 EventQueue 事件分发 + +**Goal**: 将 ReAct 执行从 WebSocket 协程中解耦,后台执行并通过 EventQueue 分发事件。WebSocket 仅订阅事件流并转发给前端。 + +**Requirements**: R2, R4 + +**Dependencies**: U1 + +**Files**: +- `src/agentkit/server/routes/portal.py` — 重构 ReAct 流式路径 +- `src/agentkit/core/event_queue.py` — 可能需要扩展(添加 task_id 过滤订阅) +- `tests/unit/test_portal_ws_background_task.py` — 新建测试 + +**Approach**: + +1. **提取后台执行函数**:创建 `_execute_react_background()` 协程,接收所有必要参数(messages, tools, model, agent_name, system_prompt, timeout_seconds, conv_id, task_id, event_queue, conversation_store)。该函数: + - 执行 `react_engine.execute_stream()` + - 每个事件通过 `event_queue.emit()` 分发 + - `final_answer` 事件时累积输出 + - 正常结束后保存到 `conversation_store` + - 异常时保存部分输出并发射 error 事件 + - 无论成功失败,都发射 `task.completed` 或 `task.failed` 事件 + +2. **WebSocket 协程改为订阅者**:在 portal.py 的 ReAct 路径中: + - 创建 task_id 并注册到 TaskStore(PENDING → RUNNING) + - `asyncio.create_task(_execute_react_background(...))` 启动后台任务 + - `async for event in event_queue.subscribe(task_id)` 订阅事件 + - 将事件通过 `websocket.send_json()` 转发给前端 + - WebSocket 断开时,`async for` 循环退出,后台任务继续运行 + +3. **EventQueue 扩展**:当前 `subscribe()` 返回所有事件。需要添加按 `task_id` 过滤的订阅能力,或在前端转发时过滤。优先选择在 EventQueue 中添加 `subscribe(task_id=None)` 参数过滤。 + +4. **DIRECT_CHAT 路径**:DIRECT_CHAT 是同步 LLM 调用(非流式),保持现有逻辑不变,但同样在 TaskStore 注册。 + +**Technical design**(directional guidance): + +```python +# 后台执行函数(伪代码) +async def _execute_react_background( + react_engine, messages, tools, model, agent_name, + system_prompt, timeout_seconds, + conv_id, task_id, event_queue, conversation_store +): + collected_output = [] + try: + async for event in react_engine.execute_stream(...): + if event.event_type == "final_answer": + collected_output.append(event.data.get("output", "")) + await event_queue.emit(Event.create( + event_type=event.event_type, + task_id=task_id, + session_id=conv_id, + data={"step": event.step, "data": event.data, ...} + )) + # 正常完成 + response_text = _ensure_non_empty("".join(collected_output)) + await conversation_store.add_message(conv_id, "assistant", response_text) + await event_queue.emit(Event.create( + event_type="task.completed", task_id=task_id, ... + )) + except Exception as e: + # 保存部分输出 + if collected_output: + partial = _ensure_non_empty("".join(collected_output)) + await conversation_store.add_message(conv_id, "assistant", partial) + await event_queue.emit(Event.create( + event_type="task.failed", task_id=task_id, + data={"error": str(e)} + )) + +# WebSocket 协程(伪代码) +task_id = str(uuid.uuid4()) +task_store.create(task_id, agent_name, {"conversation_id": conv.id}) +asyncio.create_task(_execute_react_background(...)) + +# WebSocket 订阅事件并转发 +async for event in event_queue.subscribe(task_id=task_id): + if event.event_type in ("task.completed", "task.failed"): + await websocket.send_json({"type": "result", ...}) + break + await websocket.send_json({"type": "step", "data": ...}) + # WebSocket 断开时 async for 退出,后台任务继续 +``` + +**Patterns to follow**: +- EventQueue 的 `subscribe()` + 哨兵关闭模式(event_queue.py:193-243) +- `_emit_event_safe()` 的异常吞咽模式(portal.py:65-95) +- BackgroundRunner 的 `asyncio.create_task` + `_on_done` 回调模式(runner.py:55-73) + +**Test scenarios**: +- **Happy path**: 后台任务正常完成,事件通过 EventQueue 分发,WebSocket 收到所有事件和最终结果 +- **WebSocket 断开**: 后台任务继续运行,结果保存到 conversation store,TaskStore 状态更新为 COMPLETED +- **后台任务异常**: ReAct 执行抛异常,部分输出保存,TaskStore 状态为 FAILED,error 事件发射 +- **EventQueue 订阅过滤**: 多个并发任务,每个 WebSocket 只收到自己 task_id 的事件 +- **Integration**: 后台任务完成后,conversation store 有 assistant 消息,TaskStore 有 COMPLETED 记录 + +**Verification**: 启动后台任务后立即断开 WebSocket,等待任务完成后检查 conversation store 和 TaskStore。 + +--- + +### U3. Layer 2: TaskStore 注册与状态追踪 + +**Goal**: 在 portal WebSocket 路径中为每个用户消息创建 TaskStore 记录,状态随任务进展更新。 + +**Requirements**: R3 + +**Dependencies**: U2 + +**Files**: +- `src/agentkit/server/routes/portal.py` — 集成 TaskStore +- `tests/unit/test_portal_ws_task_tracking.py` — 新建测试 + +**Approach**: + +1. 在 portal.py 的 WebSocket 路径中,获取 `task_store` from `websocket.app.state.task_store`。 +2. 在用户消息处理开始时,调用 `task_store.create(task_id, agent_name, {"conversation_id": conv.id, "message": message_text})`。 +3. 在后台任务执行前,更新状态为 `RUNNING`(`started_at=now`)。 +4. 在后台任务完成时,更新状态为 `COMPLETED`(`output_data={"output": response_text}, completed_at=now, progress=1.0`)。 +5. 在后台任务失败时,更新状态为 `FAILED`(`error_message=str(e), completed_at=now`)。 +6. DIRECT_CHAT 路径同样注册 TaskStore(同步调用,状态直接从 PENDING → COMPLETED)。 + +**Patterns to follow**: +- BackgroundRunner._run_task 的状态更新模式(runner.py:89-92, 141-147, 153-157) +- TaskStore.create/update_status 的调用签名(task_store.py:127-163) + +**Test scenarios**: +- **Happy path**: 用户发送消息 → TaskStore 创建 PENDING 记录 → 任务执行中状态为 RUNNING → 完成后状态为 COMPLETED,output_data 有内容 +- **任务失败**: ReAct 执行异常 → TaskStore 状态为 FAILED,error_message 有内容 +- **WebSocket 断开后查询**: WebSocket 断开,后台任务继续 → 通过 `GET /api/v1/tasks/{task_id}` 能查到 RUNNING 状态 → 任务完成后查到 COMPLETED +- **metadata 包含 conversation_id**: TaskStore 记录的 metadata 中有 conversation_id 字段 +- **Integration**: 前端通过 `GET /api/v1/tasks?status=running` 能找到当前对话的运行中任务 + +**Verification**: 通过 REST API `GET /api/v1/tasks/{task_id}` 查询任务状态,验证状态流转正确。 + +--- + +### U4. Layer 3: 前端任务状态查询 API 客户端 + +**Goal**: 在前端 API 客户端中添加任务状态查询方法,支持按 status 过滤和按 task_id 查询。 + +**Requirements**: R5, R6 + +**Dependencies**: U3 + +**Files**: +- `src/agentkit/server/frontend/src/api/client.ts` — 添加任务 API 方法 +- `src/agentkit/server/frontend/src/api/types.ts` — 添加任务类型定义 + +**Approach**: + +1. 在 `types.ts` 中添加 `ITaskRecord` 接口,对应后端 `TaskRecord.to_dict()` 的输出格式。 +2. 在 `client.ts` 中添加方法: + - `getTask(taskId: string): Promise` — GET `/api/v1/tasks/{taskId}`(注意:tasks 路由前缀是 `/api/v1/tasks`,不是 `/api/v1/portal`) + - `listTasks(status?: string): Promise` — GET `/api/v1/tasks?status=running` +3. 由于 tasks 路由前缀不同(`/api/v1/tasks` vs `/api/v1/portal`),需要创建一个新的 ApiClient 实例或调整 BaseApiClient 的 baseUrl。 + +**Patterns to follow**: +- 现有 `getConversations()` / `getConversation(id)` 的方法签名模式(client.ts:30-37) +- `IConversation` 接口定义模式(types.ts:51-57) + +**Test scenarios**: +- **Happy path**: 调用 `getTask(taskId)` 返回正确的任务记录 +- **按状态过滤**: 调用 `listTasks("running")` 只返回运行中任务 +- **任务不存在**: 调用 `getTask("invalid-id")` 抛出 404 错误 +- **Integration**: 后台任务运行中,前端能通过 API 查询到 RUNNING 状态 + +**Verification**: TypeScript 编译通过(`npm run build:frontend`),API 调用返回正确数据。 + +--- + +### U5. Layer 3: 前端 WebSocket 重连后的任务恢复 + +**Goal**: 前端 WebSocket 重连后,检查当前对话是否有未完成任务,恢复事件流或拉取已完成的结果。 + +**Requirements**: R5, R6 + +**Dependencies**: U4 + +**Files**: +- `src/agentkit/server/frontend/src/stores/chat.ts` — 添加重连恢复逻辑 +- `src/agentkit/server/frontend/src/api/types.ts` — 扩展 WsClientMessage 类型 + +**Approach**: + +1. **扩展 WebSocket 协议**:添加 `resume` 消息类型,前端重连后发送 `{type: "resume", task_id: "..."}` 订阅已有后台任务的事件流。 +2. **后端处理 resume**:在 portal.py 的 WebSocket 路径中,处理 `resume` 消息类型——通过 task_id 订阅 EventQueue 事件流,转发给前端。 +3. **前端重连恢复流程**(在 `connectWebSocket` 的 `onopen` 中): + - 检查 `currentConversationId` 是否有值 + - 调用 `listTasks("running")` 查找运行中任务 + - 匹配 `metadata.conversation_id === currentConversationId` 的任务 + - 如果找到运行中任务:发送 `resume` 消息,设置 `isLoading = true` + - 如果无运行中任务:调用 `selectConversation(currentConversationId)` 重新加载消息(包含已完成的结果) +4. **后端 resume 处理**:接收到 `resume` 消息后,通过 `task_id` 订阅 EventQueue,转发事件直到 `task.completed` 或 `task.failed`。 + +**Technical design**(directional guidance): + +```typescript +// 前端重连恢复(伪代码) +socket.onopen = async () => { + isWsConnected.value = true + startHeartbeat() + + // 重连恢复逻辑 + if (currentConversationId.value) { + await recoverTask(currentConversationId.value) + } +} + +async function recoverTask(convId: string) { + const tasks = await apiClient.listTasks('running') + const runningTask = tasks.find( + t => t.metadata?.conversation_id === convId + ) + + if (runningTask) { + // 恢复进行中任务的事件流 + isLoading.value = true + ws.value.send(JSON.stringify({ + type: 'resume', + task_id: runningTask.task_id + })) + } else { + // 无运行中任务,重新加载对话消息 + await selectConversation(convId) + } +} +``` + +**Patterns to follow**: +- 现有 `connectWebSocket` 的 `onopen` / `onclose` 模式(chat.ts:209-259) +- `selectConversation` 的消息加载模式(chat.ts:55-74) +- `handleWsMessage` 的事件处理模式(chat.ts:270-528) + +**Test scenarios**: +- **Happy path - 有运行中任务**: 重连后发现有运行中任务 → 发送 resume → 收到后续事件 → 任务完成显示结果 +- **Happy path - 无运行中任务**: 重连后无运行中任务 → 重新加载对话消息 → 显示已完成的结果 +- **任务在重连前已完成**: 重连时任务已 COMPLETED → listTasks("running") 返回空 → 重新加载对话消息 → 结果显示 +- **多个运行中任务**: 有多个对话的运行中任务 → 只恢复当前对话的任务 +- **resume 后任务立即完成**: resume 后立即收到 task.completed 事件 → 正确显示结果 +- **Integration**: 刷新页面 → 重连 → 恢复任务 → 最终结果正确显示 + +**Verification**: 启动复杂任务 → 刷新页面 → 验证任务继续运行 → 验证结果最终显示。 + +--- + +## Risks & Dependencies + +### Risks + +1. **EventQueue 订阅过滤改造风险**:当前 `subscribe()` 无过滤,添加 task_id 过滤可能影响现有订阅者。缓解:使用可选参数 `subscribe(task_id=None)`,默认行为不变。 +2. **后台任务泄漏风险**:WebSocket 断开后后台任务继续运行,如果任务本身卡住(如 LLM 超时),任务会一直占用资源。缓解:ReAct 已有 `timeout_seconds` 配置,后台任务同样受此约束。 +3. **并发任务事件混淆风险**:多个对话同时执行任务,EventQueue 事件可能混淆。缓解:每个任务有唯一 task_id,订阅时按 task_id 过滤。 +4. **前端重连时序风险**:重连后查询任务状态时,任务可能刚好从 RUNNING 变为 COMPLETED。缓解:先查 running,如果空则重新加载对话消息(会包含已完成结果)。 + +### Dependencies + +- U1 → U2 → U3 → U4 → U5(顺序依赖) +- EventQueue 已在 app.state 可用(无需新建) +- TaskStore 已在 app.state 可用(无需新建) + +## System-Wide Impact + +- **后端**:portal.py WebSocket 路径重大重构,影响所有 WebSocket 聊天用户 +- **前端**:chat store 的 WebSocket 连接逻辑增强,影响所有聊天页面用户 +- **API**:新增 `resume` WebSocket 消息类型,无 REST API 变更 +- **兼容性**:DIRECT_CHAT 路径和 REST API 路径不受影响 diff --git a/docs/research/2026-06-17-ragflow-integration-analysis.md b/docs/research/2026-06-17-ragflow-integration-analysis.md new file mode 100644 index 0000000..ddb985f --- /dev/null +++ b/docs/research/2026-06-17-ragflow-integration-analysis.md @@ -0,0 +1,644 @@ +# RAGFlow 引入可行性分析 + +> **创建日期**: 2026-06-17 +> **状态**: 调研完成,待决策 +> **目标**: 评估将 RAGFlow 作为 Fischer AgentKit 知识库的可行性、技术路径与风险 + +--- + +## 一、RAGFlow 项目概览 + +| 维度 | 详情 | +|------|------| +| 仓库 | https://github.com/infiniflow/ragflow | +| License | Apache-2.0 | +| GitHub Stars | ~80k(2025 年度 Top 10) | +| 最新版本 | v0.25.6(2026-05-26) | +| 核心定位 | 基于深度文档理解的 RAG 引擎,构建 AI Agent 上下文层 | +| 技术栈 | Python 后端 + React/TS 前端 + Docker 部署 | + +### 核心差异化能力 + +- **DeepDoc 引擎**:OCR(15+ 语言)、版面识别(10 类组件:文本/标题/图/表/页眉页脚/公式等)、表格结构识别(TSR) +- **混合检索**:向量 + 全文 + 稀疏向量,支持 ColBERT late-interaction +- **高级特性**:GraphRAG、RAPTOR(层级摘要树)、Parent-Child 分块、rerank、上下文压缩 +- **16+ 文档格式**:PDF/DOCX/PPT/Excel/图片/扫描件/网页/结构化数据等 +- **Agent 工作流**:可视化编排、Memory 模块(v0.23.0+)、MCP 协议集成 + +### 部署资源要求(官方) + +- CPU ≥ 4 核 +- RAM ≥ 16 GB +- Disk ≥ 50 GB +- Docker ≥ 24.0.0 & Docker Compose ≥ v2.26.1 + +--- + +## 二、可行性结论:高度可行 ✅ + +Fischer AgentKit 已具备**成熟的适配器架构**,专门为对接外部知识库设计,RAGFlow 的 HTTP REST API 可直接映射到现有协议。 + +### 关键契合点 + +1. **协议匹配**:现有 `KnowledgeBase` Protocol(`src/agentkit/memory/knowledge_base.py:53-83`)定义了 `ingest()/query()/delete_by_id()/list_sources()/health_check()`,RAGFlow API 完全覆盖这些语义 + +2. **适配器基类就绪**:`KBAdapter`(`src/agentkit/memory/adapters/base.py:22-160`)已封装 httpx 客户端、生命周期管理、认证流程,子类只需实现 `_make_client()` 和 `search()` + +3. **现有先例**:已有 `FeishuKBAdapter`、`ConfluenceAdapter`、`GenericHTTPAdapter` 三个适配器,模式成熟 + +4. **SemanticMemory 解耦**:`SemanticMemory`(`src/agentkit/memory/semantic.py:14-121`)通过 `rag_service` 注入,不直接依赖具体实现 + +5. **API 标准化**:RAGFlow 使用 Bearer Token 认证、RESTful JSON 接口,与 `HttpRAGService` 期望的接口形态一致 + +--- + +## 三、重点技术路径 + +### 路径 1:新增 RAGFlowAdapter(推荐) + +在 `src/agentkit/memory/adapters/` 下新增 `ragflow.py`,继承 `KBAdapter`。 + +**API 映射表**: + +| KBAdapter 方法 | RAGFlow API | 说明 | +|----------------|-------------|------| +| `search(query, top_k)` | `POST /api/v1/retrieval` | body: `{question, dataset_ids, top_k, similarity_threshold, vector_similarity_weight}` | +| `ingest(documents)` | `POST /api/v1/datasets/{id}/documents` + `POST /api/v1/datasets/{id}/chunks` | 上传后需触发异步解析 | +| `delete_by_id(id)` | `DELETE /api/v1/datasets/{id}/documents/{doc_id}` | | +| `list_sources()` | `GET /api/v1/datasets` | RAGFlow 的 dataset 概念 = Fischer 的 source | +| `health_check()` | `GET /api/v1/datasets` 或 `/v1/health` | | +| `get_document(doc_id)` | `GET /api/v1/datasets/{id}/documents/{doc_id}` | | + +**关键实现要点**: +- `_make_client()` 配置 `base_url` + `Authorization: Bearer ` +- `search()` 需将 RAGFlow 返回的 chunk 结构(`content/document_id/dataset_id/similarity`)标准化为 `QueryResult` +- `ingest()` 需处理 RAGFlow 的**异步解析流程**:上传 → 触发 parse → 轮询状态(非同步返回) +- 在 `adapters/__init__.py` 注册导出 + +### 路径 2:复用 HttpRAGService(最快,但能力受限) + +现有 `HttpRAGService` 已实现 `/search` 和 `/bases/{kb_id}/retrieve` 调用。RAGFlow 的 retrieval 端点路径不同(`/api/v1/retrieval`),需在 RAGFlow 侧部署一层 API 网关适配,或扩展 HttpRAGService 支持自定义路径模板。 + +**局限**:无法利用 RAGFlow 的文档上传/解析/分块能力,只能做检索。 + +### 路径 3:通过 ragflow-sdk 集成(不推荐) + +`pip install ragflow-sdk` 直接调用 Python SDK。**违背 Fischer 的"配置驱动、不直接依赖业务系统代码"原则**,且引入额外依赖。 + +### 配置集成方案 + +在 `agentkit.yaml` 的 `memory.semantic` 段扩展: + +```yaml +memory: + semantic: + enabled: true + adapter: "ragflow" + base_url: "http://ragflow-ecs-internal-ip:9380" + api_key: "${RAGFLOW_API_KEY}" + dataset_ids: + - "industry-kb-dataset-id" + - "enterprise-kb-dataset-id" + timeout: 30 + retrieval: + similarity_threshold: 0.2 + vector_similarity_weight: 0.3 + top_k: 10 + rerank_id: "BAAI/bge-reranker-v2-m3" + ingest: + mode: "async" + poll_interval: 5 + poll_timeout: 600 + health_check: + interval: 60 + fail_threshold: 3 +``` + +### 与现有 LocalRAGService 的关系 + +`LocalRAGService`(pgvector)与 RAGFlow 定位不同: +- **LocalRAGService**:轻量、同进程、适合中小规模文本知识 +- **RAGFlow**:重量级、独立服务、擅长复杂文档(PDF/扫描件/表格)深度解析 + +建议**并存**,通过 `MultiSourceRetriever` 聚合,按文档类型路由:纯文本走 Local,复杂文档走 RAGFlow。 + +--- + +## 四、风险分析与缓解措施评估 + +### 高风险项 + +#### 风险 1:资源占用重(16GB+ RAM) + +**原措施**:slim 镜像 + 外部 embedding API / 独立机器部署 + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ✅ 可行但需分场景。slim 镜像(~2GB)确实省去内置 embedding 模型,但 DeepDoc 的 OCR/TSR/Layout 模型仍打包在内,RAM 峰值仍需 8GB+ | +| 缓解效果 | ⚠️ 部分有效。slim 仅省 embedding 部分(约 4-6GB),DeepDoc 推理时仍会瞬时占用 2-4GB | +| 次生风险 | 🔴 外部 embedding API 引入新依赖链:① 网络延迟叠加(检索路径变成 Fischer→RAGFlow→外部Embedding API,3 跳);② 外部 API 限流/宕机时 RAGFlow 解析直接失败;③ 跨网络传输文档内容存在数据泄露面 | + +**更优解**: +- **方案 A(推荐)**:RAGFlow 独立机器/K8s 节点部署,与 Fischer 集群物理隔离,仅通过 HTTP API 通信 +- **方案 B**:若必须同机,用 cgroup/Docker CPU+memory limits 硬隔离 RAGFlow 容器 +- **不建议**:外部 embedding API 方案,除非已有内部 embedding 服务 + +#### 风险 2:架构栈重叠(Redis/MySQL/ES vs Fischer 的 Redis/PG) + +**原措施**:复用 Fischer 的 Redis;ES/Infinity 无法复用 PG + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ⚠️ Redis 复用技术上可行但有陷阱。RAGFlow 用 Redis 做 Celery broker + 缓存,Fischer 用 Redis 做 RedisMessageBus(Streams)+ TaskStore。两者可用不同 db number 隔离 | +| 缓解效果 | ⚠️ 有限。省下的仅是 Redis 实例(~100MB),ES/Infinity(2-4GB)和 MySQL(1-2GB)仍需独立部署,重叠问题仅解决 10-15% | +| 次生风险 | 🔴 Redis 复用有严重隐患:① RAGFlow 的 Celery 任务高峰期会打满 Redis 连接池,影响 Fischer 的 RedisMessageBus 消息投递;② key 命名空间若冲突可能导致数据污染;③ RAGFlow 升级时 Redis schema 变更可能波及 Fischer | + +**更优解**: +- **方案 A(推荐)**:完全不共享,独立 Redis 实例。RAGFlow 自带 docker-compose 已包含 Redis,保持默认部署不动 +- **方案 B**:若强需共享,用 Redis Sentinel/Cluster 的不同 db(RAGFlow 用 db=1,Fischer 用 db=0),并配置独立的 `maxmemory-policy` 和连接池上限 +- **ES/Infinity**:无更优解,RAGFlow 强依赖其混合检索能力,PG+pgvector 无法替代 + +#### 风险 3:异步解析延迟(大文档数分钟) + +**原措施**:适配器内轮询 / 异步任务模式 + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ✅ 可行,Fischer 已有完整异步任务基础设施。TaskStore 支持 PENDING→RUNNING→COMPLETED 状态机,tasks 路由已有 submit/status/list/cancel API | +| 缓解效果 | ✅ 有效。将 RAGFlow ingest 拆为"提交上传→返回 task_id→后台轮询解析状态→更新 task"四步 | +| 次生风险 | 🟡 中等:① 轮询频率若过高会冲击 RAGFlow API(建议 5-10s 间隔,指数退避);② task TTL 与 RAGFlow 解析时长不匹配;③ 用户在解析未完成时检索会得到空结果 | + +**更优解**: +- **方案 A(推荐)**:用 RAGFlow 的 Webhook 回调替代轮询。RAGFlow v0.23.0+ 支持 Webhook 触发,解析完成后主动回调 Fischer 的 `/api/v1/tasks/{id}/callback` +- **方案 B**:若 Webhook 不可用,用 RedisMessageBus 发布解析完成事件,适配器订阅后更新 task 状态 +- **方案 C**:分离 ingest 和 query 路径——ingest 走异步 task,query 永远同步 + +### 中风险项 + +#### 风险 4:Embedding 模型锁定 + +**原措施**:规划期确定模型;不同 dataset 用不同模型 + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ⚠️ 部分可行。"不同 dataset 不同模型"技术上成立,但跨 dataset 检索时向量维度不一致会导致召回失效 | +| 缓解效果 | ⚠️ 有限。锁定后若需切换模型,必须重建整个 dataset | +| 次生风险 | 🟡 模型碎片化:多个 dataset 用不同 embedding,SemanticMemory 的 `kb_weights` 加权策略失效 | + +**更优解**: +- **方案 A(推荐)**:全组织统一 embedding 模型(建议 `BAAI/bge-large-zh-v1.5` 或 `bge-m3`),所有 dataset 强制一致 +- **方案 B**:若必须多模型,在适配器层按 dataset 分组检索,组内归一化 score 后再融合 +- **根本性建议**:将 embedding 模型选择纳入 Fischer 的 LLM Gateway 统一管理 + +#### 风险 5:ARM64 支持缺失 + +**原措施**:x86 Docker / 自行构建 + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ✅ 可行。自行构建有官方文档支持 | +| 缓解效果 | ✅ 有效,但构建耗时(含模型下载约 30-60 分钟) | +| 次生风险 | 🟢 低。主要是构建产物维护成本 | + +**更优解**: +- **方案 A**:开发环境用 x86 Docker Desktop;生产环境强制 x86 服务器 +- **方案 B**:若有 CI/CD,用 GitHub Actions 在 ARM64 runner 上自动构建并推送到私有 registry + +#### 风险 6:版本快速迭代(API breaking change) + +**原措施**:锁定版本;适配器层兼容 + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ✅ 可行。锁定版本是标准做法 | +| 缓解效果 | ✅ 有效但被动。锁定版本意味着无法获得 bug 修复和新特性 | +| 次生风险 | 🟡 安全漏洞累积:长期不升级会错过安全补丁 | + +**更优解**: +- **方案 A(推荐)**:适配器层做 API 版本抽象。定义 `RAGFlowAPIVersion` 枚举,适配器根据版本号选择不同的端点路径和响应解析逻辑 +- **方案 B**:跟随 RAGFlow 的 minor 版本(如固定 v0.25.x),patch 版本自动升级 + +#### 风险 7:数据模型映射损耗 + +**原措施**:适配器层完整字段映射,特有字段入 metadata + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ✅ 完全可行。这是适配器模式的标准职责 | +| 缓解效果 | ✅ 有效。QueryResult 的 metadata 字段是 dict,可容纳任意额外字段 | +| 次生风险 | 🟢 极低 | + +**更优解**:当前方案已足够。增强建议:定义 `RAGFlowChunkMetadata` Pydantic 模型,结构化 RAGFlow 特有字段 + +### 低风险项 + +#### 风险 8:网络调用开销 + +**原措施**:timeout + 降级 + +| 评估维度 | 结论 | +|----------|------| +| 可行性 | ✅ 可行。SemanticMemory 已有 try/except 降级到空结果 | +| 缓解效果 | ✅ 有效 | +| 次生风险 | 🟡 降级静默化:检索失败返回空列表,用户无感知 | + +**更优解**:在降级时记录 metric,并向前端 WebSocket 推送 `error` 事件;增加 circuit breaker + +#### 风险 9 & 10:认证差异 / 功能重叠 + +**评估**:两项措施均完全可行且无次生风险,无需更优解。 + +### 综合评估矩阵 + +| 风险 | 原措施可行性 | 次生风险 | 缓解效果 | 更优解收益 | +|------|:---:|:---:|:---:|------| +| 1. 资源占用 | ⚠️ 部分 | 🔴 高 | ⚠️ 部分 | 独立部署彻底解决 | +| 2. 架构栈重叠 | ⚠️ 部分 | 🔴 高 | ⚠️ 仅10-15% | 独立Redis实例,不共享 | +| 3. 异步解析 | ✅ 可行 | 🟡 中 | ✅ 有效 | Webhook回调消除轮询 | +| 4. Embedding锁定 | ⚠️ 部分 | 🟡 中 | ⚠️ 有限 | 统一模型+网关治理 | +| 5. ARM64 | ✅ 可行 | 🟢 低 | ✅ 有效 | CI自动构建 | +| 6. 版本迭代 | ✅ 可行 | 🟡 中 | ⚠️ 被动 | API版本抽象层 | +| 7. 数据映射 | ✅ 可行 | 🟢 极低 | ✅ 有效 | 已足够 | +| 8. 网络开销 | ✅ 可行 | 🟡 低 | ✅ 有效 | metric+WS通知 | + +### 关键结论 + +1. **原措施中 2 项有严重次生风险需立即调整**: + - 风险 1 的"外部 embedding API"方案 → 改为独立机器部署 + - 风险 2 的"复用 Redis"方案 → 改为独立 Redis 实例 + +2. **1 项有明确更优解**: + - 风险 3 的"轮询"→ 改用 RAGFlow Webhook 回调 + +3. **最关键的系统性建议**:将 RAGFlow 视为完全独立的外部服务(而非与 Fischer 共享基础设施的组件),通过 HTTP API 松耦合 + +4. **embedding 模型治理应统一到 Fischer 的 LLM Gateway**,避免模型碎片化和 score 不可比问题 + +--- + +## 五、独立部署配置方案(阿里云) + +### 架构拓扑 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 阿里云 VPC (10.0.0.0/16) │ +│ │ +│ ┌──────────────────────┐ ┌───────────────────────┐ │ +│ │ Fischer ECS (已有) │ │ RAGFlow ECS (新增) │ │ +│ │ 10.0.1.10 │ HTTPS │ 10.0.2.10 │ │ +│ │ agentkit serve:8001 │◄───────►│ ragflow:9380 │ │ +│ │ Redis:6379 (内嵌) │ │ Infinity:23321 │ │ +│ │ PG:5432 (内嵌) │ │ MySQL:3306 Redis:6380 │ │ +│ └──────────────────────┘ └───────────────────────┘ │ +│ │ │ │ +│ │ ┌──────────────────────┐│ │ +│ └─────────►│ 阿里云 NAS / ESSD │◄┘ │ +│ │ (文档持久化存储) │ │ +│ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 资源需求核算(修正后) + +RAGFlow 各组件实际内存占用: + +| 组件 | 最小可运行 | 推荐生产 | 说明 | +|------|-----------|---------|------| +| RAGFlow Server + DeepDoc | 2GB | 4GB | Python 进程,DeepDoc 推理时峰值 +2GB | +| Elasticsearch | 4GB(heap 2g) | 8GB(heap 4g) | JVM heap = 容器内存 50% | +| Infinity(替代ES) | 2GB | 3GB | Rust 实现,无 JVM 开销 | +| MySQL | 1GB | 2GB | 元数据存储,数据量小 | +| Redis | 512MB | 1GB | Celery broker + 缓存 | +| MinIO | 512MB | 1GB | 文档对象存储 | +| 系统开销 | 1GB | 2GB | OS + Docker daemon | + +### 按业务规模分档推荐 + +#### 档位 1:POC / 小规模(< 1000 文档) + +| 配置 | 规格 | 月费 | +|------|------|------| +| 实例 | ecs.g7.xlarge(4c16g) | ¥450 | +| 数据盘 | ESSD PL0 100GB | ¥45 | +| **合计** | | **~¥500/月** | + +#### 档位 2:中小规模(1000-10000 文档)⭐ 推荐 + +| 配置 | 规格 | 月费 | +|------|------|------| +| 实例 | ecs.g7.2xlarge(8c32g) | ¥900 | +| 数据盘 | ESSD PL1 200GB | ¥150 | +| **合计** | | **~¥1,050/月** | + +资源分配: +``` +RAGFlow Server: 4GB +ES (heap 4g): 8GB 或 Infinity: 3GB +MySQL: 2GB +Redis: 1GB +MinIO: 1GB +系统余量: 16GB(含 DeepDoc 推理峰值) +``` + +#### 档位 3:中大规模(1万-10万文档) + +| 配置 | 规格 | 月费 | +|------|------|------| +| 实例 | ecs.g7.3xlarge(12c48g) | ¥1,350 | +| 数据盘 | ESSD PL1 500GB | ¥375 | +| **合计** | | **~¥1,725/月** | + +### 降配三个手段 + +1. **用 Infinity 替代 Elasticsearch**(省 4-6GB):Rust 实现,无 JVM 开销 +2. **slim 镜像 + 外部 Embedding**(省 4GB):embedding 走阿里云百炼 `text-embedding-v2` +3. **MySQL/Redis 用 RAGFlow 自带**(不共享,彻底隔离) + +### 综合最优方案(性价比最高) + +| 项目 | 选择 | 理由 | +|------|------|------| +| 实例 | ecs.g7.2xlarge(8c32g) | 留足 DeepDoc 推理余量 | +| 文档引擎 | Infinity(非 ES) | 省 5GB 内存 | +| 镜像 | v0.25.6-slim | 不含 embedding 模型 | +| Embedding | 阿里云百炼 text-embedding-v2 | 外部 API,按量付费极低 | +| MySQL/Redis | RAGFlow 自带 | 不共享,彻底隔离 | +| 数据盘 | ESSD PL1 200GB | IOPS 够用 | +| **月费** | **~¥1,050** | | + +资源实际占用预估: +``` +RAGFlow Server (slim): 2GB +DeepDoc 推理峰值: +2GB(间歇性) +Infinity: 3GB +MySQL: 2GB +Redis: 1GB +MinIO: 1GB +系统: 2GB +──────────────────────────── +总计: ~13GB(32GB 机器余量充足) +``` + +### 极限降配方案(仅 POC 验证用) + +| 项目 | 选择 | 月费 | +|------|------|------| +| 实例 | ecs.g7.xlarge(4c16g) | ¥450 | +| 文档引擎 | Infinity | | +| 镜像 | slim + 外部 embedding | | +| **月费** | | **~¥500** | + +⚠️ 4c16g 下 DeepDoc 解析大 PDF(>50页)可能触发 OOM,仅适合验证检索效果。 + +--- + +## 六、RAGFlow 侧 Docker Compose 配置 + +```yaml +# /opt/ragflow/docker/docker-compose.yml(基于官方 v0.25.6 调整) +services: + ragflow-server: + image: registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:v0.25.6-slim + container_name: ragflow-server + ports: + - "10.0.2.10:9380:9380" # 仅绑定内网 IP,不暴露公网 + - "10.0.2.10:80:80" + environment: + - SVR_HTTP_PORT=9380 + - MYSQL_HOST=mysql + - MYSQL_PORT=3306 + - REDIS_HOST=redis + - REDIS_PORT=6380 # 与 Fischer Redis 端口隔离 + - DOC_ENGINE=infinity # 使用 Infinity 替代 ES + - MINIO_HOST=minio:9000 + volumes: + - /data/ragflow/ragflow-logs:/ragflow/logs + - /data/ragflow/ragflow-data:/ragflow/data + depends_on: + - mysql + - redis + - minio + - infinity + restart: unless-stopped + deploy: + resources: + limits: + memory: 8G + + infinity: + image: infiniflow/infinity:v0.6.0 + container_name: ragflow-infinity + volumes: + - /data/ragflow/infinity-data:/infinity/data + deploy: + resources: + limits: + memory: 4G + + mysql: + image: mysql:8.0 + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=ragflow + volumes: + - /data/ragflow/mysql-data:/var/lib/mysql + deploy: + resources: + limits: + memory: 2G + + redis: + image: redis:7-alpine + command: redis-server --port 6380 --maxmemory 1gb --maxmemory-policy allkeys-lru + volumes: + - /data/ragflow/redis-data:/data + deploy: + resources: + limits: + memory: 2G + + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + volumes: + - /data/ragflow/minio-data:/data + deploy: + resources: + limits: + memory: 1G +``` + +### 系统级配置(必做) + +```bash +# 1. 内核参数(ES 必须,Infinity 建议也设置) +sudo sysctl -w vm.max_map_count=262144 +echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf + +# 2. 创建数据目录 +sudo mkdir -p /data/ragflow/{infinity-data,mysql-data,redis-data,minio-data,ragflow-data,ragflow-logs} +sudo chown -R 1000:1000 /data/ragflow + +# 3. Docker 镜像加速(阿里云容器镜像服务) +sudo mkdir -p /etc/docker +sudo tee /etc/docker/daemon.json < 设置 > API Key 生成) +RAGFLOW_API_KEY=ragflow-xxxxxxxxxxxxxxxxxxxxxxxx + +# 可选:embedding 模型走阿里云百炼 +DASHSCOPE_API_KEY=sk-xxxxxxxxxxxx +``` + +--- + +## 八、阿里云网络与安全配置 + +### VPC 安全组规则 + +**RAGFlow ECS 安全组**(仅允许 Fischer 内网访问): + +| 方向 | 协议 | 端口 | 源/目标 | 用途 | +|------|------|------|---------|------| +| 入 | TCP | 9380 | 10.0.1.10/32 (Fischer) | RAGFlow API | +| 入 | TCP | 80 | 10.0.1.10/32 (Fischer) | RAGFlow Web(可选) | +| 入 | TCP | 22 | 管理跳板机 IP | SSH 运维 | +| 出 | TCP | 443 | 0.0.0.0/0 | 拉取镜像、调用外部 LLM | +| 入 | TCP | * | 0.0.0.0/0 | **拒绝**(默认) | + +### API Key 安全 + +RAGFlow API Key 通过阿里云 KMS 加密存储,运行时解密注入: + +```bash +# 加密 API Key 到 KMS +aliyun kms Encrypt \ + --KeyId key-ragflow \ + --Plaintext "$(echo -n 'ragflow-xxx' | base64)" + +# Fischer 启动脚本中解密 +export RAGFLOW_API_KEY=$(aliyun kms Decrypt \ + --CiphertextBlob "encrypted-blob" \ + --query Plaintext | base64 -d) +``` + +--- + +## 九、可选阿里云托管服务替代 + +| RAGFlow 组件 | 阿里云替代 | 优势 | 成本 | +|--------------|-----------|------|------| +| Elasticsearch | 阿里云 ES | 免运维、自动备份、监控 | ~¥800/月(2核4G) | +| MySQL | RDS MySQL | 高可用、自动备份 | ~¥200/月(1核2G) | +| Redis | Tair/Redis 实例 | 免运维、持久化 | ~¥150/月(1G) | +| MinIO(文档存储) | OSS | 11个9 持久性、低成本 | ~¥0.12/GB/月 | +| 服务器 | ACK(K8s) | 弹性伸缩、滚动升级 | 节点费 + 管理费 | + +### 成本对比 + +| 方案 | 月费 | 适用场景 | +|------|------|---------| +| 全自管 ECS(8c32g + Infinity + slim) | ~¥1,050 | 推荐起步 | +| ECS + 托管服务混合 | ~¥2,062 | 免运维需求 | +| 经济型 POC(4c16g) | ~¥500 | 仅验证 | + +--- + +## 十、实施步骤 + +1. **POC 验证**:用 `docker compose` 起一个 RAGFlow slim + Infinity 实例,上传 1 个 PDF,调用 `/api/v1/retrieval` 验证检索效果 +2. **实现 RAGFlowAdapter**:参考 `generic_http.py` 模式,重点处理异步解析和字段映射 +3. **配置集成**:扩展 `agentkit.yaml` schema 和 SemanticMemory 初始化逻辑 +4. **单元测试**:参考现有适配器测试,mock RAGFlow API 响应 +5. **集成测试**:验证 RAGFlow + LocalRAGService 多源检索聚合 + +### 部署 Checklist + +- [ ] RAGFlow ECS 创建并加入与 Fischer 相同的 VPC +- [ ] 安全组仅放行 Fischer 内网 IP 到 9380 端口 +- [ ] `vm.max_map_count=262144` 永久生效 +- [ ] 数据盘挂载到 `/data/ragflow/` 并设置正确权限 +- [ ] RAGFlow docker-compose 启动,`docker logs -f ragflow-server` 显示就绪 +- [ ] RAGFlow Web UI 创建 dataset,记录 dataset_id +- [ ] 生成 RAGFlow API Key,通过 KMS 加密存储 +- [ ] Fischer `.env` 配置 `RAGFLOW_API_KEY` +- [ ] Fischer `agentkit.yaml` 配置 memory.semantic 段 +- [ ] 从 Fischer ECS 执行 `curl http://10.0.2.10:9380/api/v1/datasets` 验证连通 +- [ ] 上传测试文档,验证 ingest 异步解析完成 +- [ ] 执行检索测试,验证 QueryResult 字段映射正确 + +--- + +## 十一、核心判断 + +RAGFlow 的价值在于 **DeepDoc 深度文档解析**这一项 Fischer 现有栈(pgvector + TextChunker)明显薄弱的能力。 + +- **如果业务场景涉及大量 PDF/扫描件/复杂表格** → 引入值得 +- **若仅处理纯文本** → 现有的 `LocalRAGService` 已够用,不必承担 RAGFlow 的运维复杂度 + +### 关键决策点 + +1. RAGFlow 视为**完全独立的外部服务**,通过 HTTP API 松耦合,不共享基础设施 +2. embedding 模型治理统一到 Fischer 的 LLM Gateway +3. 起步用全自管 ECS(8c32g + Infinity + slim,~¥1,050/月),验证业务价值后再评估是否迁移到托管服务 +4. ECS 规格选 g7.2xlarge 是经过 RAGFlow 全栈资源分配计算的安全值,不建议低于 32GB RAM(生产环境) + +--- + +## 参考资料 + +- RAGFlow 官网: https://ragflow.org/ +- RAGFlow GitHub: https://github.com/infiniflow/ragflow +- RAGFlow HTTP API: https://ragflow.io/docs/dev/http_api_reference +- RAGFlow Python API: https://ragflow.io/docs/dev/python_api_reference +- RAGFlow 发布说明: https://ragflow.io/docs/release_notes +- Infinity 数据库: https://github.com/infiniflow/infinity +- RAGFlow 系统架构: https://deepwiki.com/infiniflow/ragflow/3-system-architecture diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..ed07862 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# 服务器侧部署脚本:构建镜像并滚动更新服务 +# 由 Gitea Actions workflow 在 /opt/agentkit/repo 目录下调用 +# Usage: bash scripts/deploy.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.deploy.yaml}" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/agentkit}" + +cd "$PROJECT_ROOT" + +if [ ! -f "$COMPOSE_FILE" ]; then + echo "❌ 未找到 $COMPOSE_FILE" + exit 1 +fi + +if [ ! -f ".env" ]; then + echo "❌ 未找到 .env 文件,请先通过 Gitea Secrets 生成" + exit 1 +fi + +echo "==> 部署目录: $PROJECT_ROOT" +echo "==> Compose 文件: $COMPOSE_FILE" +echo "==> 开始构建镜像..." +docker compose -f "$COMPOSE_FILE" build --pull + +echo "==> 滚动重启服务(保留数据卷)..." +docker compose -f "$COMPOSE_FILE" up -d --remove-orphans + +echo "==> 当前服务状态:" +docker compose -f "$COMPOSE_FILE" ps + +echo "==> 部署完成 ✅" diff --git a/src/agentkit/cli/benchmark.py b/src/agentkit/cli/benchmark.py index 10ba2cb..0a50bc6 100644 --- a/src/agentkit/cli/benchmark.py +++ b/src/agentkit/cli/benchmark.py @@ -22,6 +22,7 @@ Tests core AgentKit components: - event_model: SubmissionQueue / EventQueue lifecycle [Mock] - spec_management: SpecManager CRUD operations [Mock] - verification: VerificationLoop execute/retry behavior [Mock] +- board_meeting: BoardRouter @board prefix routing & validation [Mock] - llm_reasoning: Real LLM intent/tool/multi-step/code/error [LLM] - gui_integration: agentkit gui end-to-end (API/WS/frontend) [GUI] @@ -85,6 +86,7 @@ class BenchmarkDimension(str, Enum): EVENT_MODEL = "event_model" SPEC_MANAGEMENT = "spec_management" VERIFICATION = "verification" + BOARD_MEETING = "board_meeting" LLM_REASONING = "llm_reasoning" GUI_INTEGRATION = "gui_integration" ALL = "all" @@ -114,6 +116,7 @@ _MOCK_DIMENSIONS: list[BenchmarkDimension] = [ BenchmarkDimension.EVENT_MODEL, BenchmarkDimension.SPEC_MANAGEMENT, BenchmarkDimension.VERIFICATION, + BenchmarkDimension.BOARD_MEETING, ] @@ -349,6 +352,62 @@ TASK_SET: list[BenchmarkTask] = [ "passed", ["timeout"], "超时检测"), BenchmarkTask("vf-005", "verification", "multi", "medium", "multi_command", "passed", ["multi"], "多命令验证"), + # === Board Meeting (18 tasks) === + BenchmarkTask("bd-001", "board_meeting", "default_template", "easy", + "@board 讨论是否应该进入东南亚市场", + "board", ["board", "default"], "@board 前缀应路由到 board 模式"), + BenchmarkTask("bd-002", "board_meeting", "default_template", "easy", + "@board AI产品定价策略应该怎么做", + "board", ["board", "default"], "@board 前缀应路由到 board 模式"), + BenchmarkTask("bd-003", "board_meeting", "default_template", "medium", + "@board:private_board 讨论创业公司融资节奏", + "board", ["board", "template"], "显式 private_board 模板应路由到 board 模式"), + BenchmarkTask("bd-004", "board_meeting", "explicit_experts", "medium", + "@board:elon_musk,jeff_bezos 讨论火星殖民的商业化路径", + "board", ["board", "explicit"], "指定专家应路由到 board 模式"), + BenchmarkTask("bd-005", "board_meeting", "explicit_experts", "medium", + "@board:charlie_munger,warren_buffett 价值投资在AI时代的适用性", + "board", ["board", "explicit"], "指定多位专家应路由到 board 模式"), + BenchmarkTask("bd-006", "board_meeting", "explicit_experts", "medium", + "@board:elon_musk,jeff_bezos,allenzhang 产品设计哲学", + "board", ["board", "explicit", "multi"], "三位专家应路由到 board 模式"), + BenchmarkTask("bd-007", "board_meeting", "topic_extraction", "easy", + "@board 讨论是否应该进入东南亚市场", + "讨论是否应该进入东南亚市场", ["board", "topic"], "应正确提取讨论主题"), + BenchmarkTask("bd-008", "board_meeting", "topic_extraction", "easy", + "@board:elon_musk,jeff_bezos 火星商业化方案", + "火星商业化方案", ["board", "topic"], "应从显式专家格式提取主题"), + BenchmarkTask("bd-009", "board_meeting", "topic_extraction", "easy", + "@board", + "", ["board", "topic", "empty"], "空主题应返回空字符串"), + BenchmarkTask("bd-010", "board_meeting", "no_match", "easy", + "讨论一下市场策略", + "not_board", ["board", "edge"], "无 @board 前缀不应路由到 board 模式"), + BenchmarkTask("bd-011", "board_meeting", "no_match", "easy", + "@team:analyst,writer 协作完成任务", + "not_board", ["board", "edge"], "@team 前缀不应路由到 board 模式"), + BenchmarkTask("bd-012", "board_meeting", "no_match", "easy", + "@skill:react_agent 查看ip", + "not_board", ["board", "edge"], "@skill 前缀不应路由到 board 模式"), + BenchmarkTask("bd-013", "board_meeting", "name_validation", "medium", + "@board:elon_musk,jeff_bezos 主题", + "2_valid", ["board", "validation"], "两个有效专家名应被接受"), + BenchmarkTask("bd-014", "board_meeting", "name_validation", "medium", + "@board:@#$ 主题", + "default_fallback", ["board", "validation", "invalid"], + "全部无效专家名时应回退到默认模板"), + BenchmarkTask("bd-015", "board_meeting", "name_validation", "medium", + "@board:a,b,c,d,e,f,g,h,i,j,k 主题", + "10_capped", ["board", "validation", "cap"], "超过 MAX_EXPERTS=10 应被截断"), + BenchmarkTask("bd-016", "board_meeting", "stop_command", "easy", + "/stop", + "is_stop", ["board", "stop"], "/stop 应被识别为停止命令"), + BenchmarkTask("bd-017", "board_meeting", "stop_command", "easy", + "停止讨论", + "is_stop", ["board", "stop"], "中文停止讨论应被识别"), + BenchmarkTask("bd-018", "board_meeting", "stop_command", "easy", + "继续讨论", + "not_stop", ["board", "stop"], "非停止命令不应被误判"), ] # fmt: on @@ -359,6 +418,7 @@ _FAST_CORE_IDS: set[str] = { "eff-001", "eff-004", "ts-001", "ts-003", "ts-008", "ts-010", "ev-001", "ev-004", "ev-005", "sm-001", "sm-002", "sm-006", "sm-004", "vf-001", "vf-002", "vf-003", "llm-001", "llm-003", "gui-001", "gui-002", "gui-004", + "bd-001", "bd-004", "bd-007", "bd-010", "bd-013", "bd-016", } # fmt: on @@ -1751,6 +1811,134 @@ async def _exec_verification(task: BenchmarkTask, ctx: BenchmarkContext) -> Exec ) +async def _exec_board_meeting(task: BenchmarkTask, ctx: BenchmarkContext) -> ExecutionResult: + """Execute board meeting benchmark task. + + Tests BoardRouter prefix matching, topic extraction, expert name + validation, and stop command detection — all without LLM calls. + + Categories: + - default_template: @board or @board:private_board → board mode + - explicit_experts: @board:expert1,expert2 → board mode + - topic_extraction: verify topic string is correctly extracted + - no_match: non-@board inputs should NOT route to board mode + - name_validation: expert name format and MAX_EXPERTS cap + - stop_command: /stop and 停止讨论 detection + """ + from agentkit.experts.board_router import ( + MAX_EXPERTS, + BoardRouter, + ) + from agentkit.experts.registry import ExpertTemplateRegistry + + start = time.perf_counter() + + # Build a BoardRouter with an empty registry (tests pure routing logic) + registry = ExpertTemplateRegistry() + router = BoardRouter(template_registry=registry) + + # --- Stop command detection (bd-016, bd-017, bd-018) --- + if task.category == "stop_command": + from agentkit.experts.board_orchestrator import BoardOrchestrator + + is_stop = task.input.strip() in BoardOrchestrator.STOP_COMMANDS + actual = "is_stop" if is_stop else "not_stop" + passed = actual == task.expected + elapsed = (time.perf_counter() - start) * 1000 + return ExecutionResult( + actual=actual, + passed=passed, + duration_ms=round(elapsed, 4), + detail=f"input={task.input!r} stop_commands={BoardOrchestrator.STOP_COMMANDS}", + ) + + # --- All other categories: use BoardRouter.resolve() --- + result = router.resolve(task.input) + elapsed = (time.perf_counter() - start) * 1000 + + if task.category == "default_template": + # Expect board_mode=True and use_default_template=True + actual = "board" if (result.matched and result.board_mode) else "not_board" + passed = actual == task.expected + return ExecutionResult( + actual=actual, + passed=passed, + duration_ms=round(elapsed, 4), + detail=( + f"matched={result.matched} board_mode={result.board_mode} " + f"use_default={result.use_default_template} topic={result.topic!r}" + ), + ) + + if task.category == "explicit_experts": + actual = "board" if (result.matched and result.board_mode) else "not_board" + passed = actual == task.expected + return ExecutionResult( + actual=actual, + passed=passed, + duration_ms=round(elapsed, 4), + detail=( + f"matched={result.matched} experts={result.specified_experts} " + f"use_default={result.use_default_template}" + ), + ) + + if task.category == "topic_extraction": + # Compare extracted topic (normalized: strip + collapse whitespace) + actual = " ".join(result.topic.split()) + passed = actual == task.expected + return ExecutionResult( + actual=actual, + passed=passed, + duration_ms=round(elapsed, 4), + detail=f"input={task.input!r} topic={result.topic!r} matched={result.matched}", + ) + + if task.category == "no_match": + # Expect board_mode=False + actual = "not_board" if not result.board_mode else "board" + passed = actual == task.expected + return ExecutionResult( + actual=actual, + passed=passed, + duration_ms=round(elapsed, 4), + detail=f"input={task.input!r} matched={result.matched} board_mode={result.board_mode}", + ) + + if task.category == "name_validation": + # Count valid expert names (after validation) + valid_count = len(result.specified_experts) + if task.expected == "2_valid": + actual = f"{valid_count}_valid" + passed = valid_count == 2 + elif task.expected == "default_fallback": + # All names invalid → should fall back to default template + actual = "default_fallback" if result.use_default_template else "no_fallback" + passed = result.use_default_template and valid_count > 0 + elif task.expected == "10_capped": + actual = f"{valid_count}_capped" + passed = valid_count == MAX_EXPERTS + else: + actual = f"{valid_count}_valid" + passed = False + return ExecutionResult( + actual=actual, + passed=passed, + duration_ms=round(elapsed, 4), + detail=( + f"input={task.input!r} experts={result.specified_experts} " + f"max={MAX_EXPERTS}" + ), + ) + + return ExecutionResult( + actual="unknown_category", + passed=False, + duration_ms=round(elapsed, 4), + detail=f"Unknown board_meeting category: {task.category}", + ) + + _EXECUTORS: dict[ str, Callable[[BenchmarkTask, BenchmarkContext], Awaitable[ExecutionResult]], @@ -1762,6 +1950,7 @@ _EXECUTORS: dict[ "event_model": _exec_event_model, "spec_management": _exec_spec_management, "verification": _exec_verification, + "board_meeting": _exec_board_meeting, } @@ -1963,8 +2152,9 @@ def _generate_markdown_report( "event_model": "5. 事件模型 (Event Model) [Mock]", "spec_management": "6. 规格管理 (Spec Management) [Mock]", "verification": "7. 验证循环 (Verification Loop) [Mock]", - "llm_reasoning": "8. LLM 推理能力 (LLM Reasoning) [LLM]", - "gui_integration": "9. GUI 集成测试 (GUI Integration) [GUI]", + "board_meeting": "8. 私董会路由 (Board Meeting Routing) [Mock]", + "llm_reasoning": "9. LLM 推理能力 (LLM Reasoning) [LLM]", + "gui_integration": "10. GUI 集成测试 (GUI Integration) [GUI]", } lines.append("## 维度结果") diff --git a/src/agentkit/experts/board.py b/src/agentkit/experts/board.py new file mode 100644 index 0000000..d82e28f --- /dev/null +++ b/src/agentkit/experts/board.py @@ -0,0 +1,377 @@ +"""BoardTeam - 私董会讨论模式容器 + +管理私董会的专家生命周期、讨论状态和事件广播。 +与 ExpertTeam(hub-and-spoke 任务分解)并列,专注于多轮群聊式讨论。 + +核心差异(vs ExpertTeam): +- 讨论模式:多轮全员发言 + 主持人小结,非任务分解 +- 专家通信:基于共享讨论历史,非独立子任务 +- 终止机制:最大轮次 + 用户干预,非任务完成 +- 主持人角色:首位专家,负责开场/小结/最终总结 +""" + +from __future__ import annotations + +import enum +import logging +import time +import uuid +from typing import Any + +from .config import ExpertConfig +from .expert import Expert +from .registry import ExpertTemplateRegistry +from ..core.handoff_transport import InProcessHandoffTransport +from ..core.shared_workspace import SharedWorkspace +from ..core.agent_pool import AgentPool + +logger = logging.getLogger(__name__) + + +class BoardStatus(str, enum.Enum): + """BoardTeam lifecycle states. + + Flow: FORMING → DISCUSSING → CONCLUDING → COMPLETED → DISSOLVED + """ + + FORMING = "forming" + DISCUSSING = "discussing" + CONCLUDING = "concluding" + COMPLETED = "completed" + DISSOLVED = "dissolved" + + +class BoardTeam: + """Container managing a board of Experts in discussion mode. + + In board meeting mode: + - Moderator (lead expert) opens the discussion, summarizes each round, + and gives final decision advice + - Member experts give speeches each round based on shared discussion history + - All experts see the full discussion history (shared context) + - Discussion terminates after max_rounds or user intervention + """ + + def __init__( + self, + team_id: str | None = None, + workspace: SharedWorkspace | None = None, + pool: AgentPool | None = None, + template_registry: ExpertTemplateRegistry | None = None, + max_rounds: int = 5, + ): + self.team_id = team_id or str(uuid.uuid4()) + self._workspace = workspace or SharedWorkspace() + self._pool = pool + self._template_registry = template_registry or ExpertTemplateRegistry() + self._handoff_transport = InProcessHandoffTransport() + self._experts: dict[str, Expert] = {} + self._moderator_name: str | None = None + self._status = BoardStatus.FORMING + self._team_channel = f"board:{self.team_id}" + + # Discussion state + self._topic: str = "" + self._history: list[dict[str, Any]] = [] + self._current_round: int = 0 + self._max_rounds: int = max_rounds + self._user_interventions: list[str] = [] # Pending user messages + + @property + def status(self) -> BoardStatus: + return self._status + + @property + def moderator(self) -> Expert | None: + if self._moderator_name: + return self._experts.get(self._moderator_name) + return None + + @property + def experts(self) -> list[Expert]: + return list(self._experts.values()) + + @property + def active_experts(self) -> list[Expert]: + return [e for e in self._experts.values() if e.is_active] + + @property + def member_experts(self) -> list[Expert]: + """Non-moderator experts.""" + return [e for e in self._experts.values() if e.config.name != self._moderator_name] + + @property + def workspace(self) -> SharedWorkspace: + return self._workspace + + @property + def handoff_transport(self): + return self._handoff_transport + + @property + def team_channel(self) -> str: + return self._team_channel + + @property + def topic(self) -> str: + return self._topic + + @property + def current_round(self) -> int: + return self._current_round + + @property + def max_rounds(self) -> int: + return self._max_rounds + + @property + def history(self) -> list[dict[str, Any]]: + return self._history.copy() + + def get_expert(self, name: str) -> Expert | None: + return self._experts.get(name) + + def set_status(self, status: BoardStatus) -> None: + self._status = status + + async def create_board( + self, + topic: str, + expert_configs: list[ExpertConfig], + ) -> None: + """Create a board with a moderator and member experts. + + Args: + topic: Discussion topic + expert_configs: List of ExpertConfig, first is moderator + """ + if not self._pool: + raise RuntimeError("AgentPool not configured") + + if not expert_configs: + raise ValueError("At least one expert config is required") + + self._topic = topic + + # Build board context for all experts + board_context = self._build_board_context(expert_configs) + + # Create experts + for i, config in enumerate(expert_configs): + expert = await Expert.create( + config=config, + pool=self._pool, + handoff_transport=self._handoff_transport, + workspace=self._workspace, + team_context=board_context, + ) + expert.team_id = self.team_id + self._experts[config.name] = expert + + # First expert is moderator + if i == 0: + self._moderator_name = config.name + + self._status = BoardStatus.DISCUSSING + + def _build_board_context(self, expert_configs: list[ExpertConfig]) -> str: + """Build board context string for injection into Expert system prompts. + + Emphasizes discussion mode, role differentiation, and discussion rules. + """ + lines = ["You are part of a Board Meeting (private board discussion mode)."] + + for i, config in enumerate(expert_configs): + role = "Moderator" if i == 0 else "Member" + lines.append( + f"{role}: {config.name} ({config.persona[:100]}...)" + f" — Thinking: {config.thinking_style}" + f" — Speaking: {config.speaking_style}" + f" — Framework: {config.decision_framework}" + ) + + lines.append("") + lines.append("Board meeting rules:") + lines.append("- Each round, all members give speeches based on their persona and framework") + lines.append("- Moderator opens the discussion, summarizes each round, and gives final advice") + lines.append("- All experts see the full discussion history (shared context)") + lines.append("- Stay in character: think and speak as your persona would") + lines.append("- Be concise but insightful: 2-4 paragraphs per speech") + lines.append("- Build on or respectfully challenge previous speakers' points") + return "\n".join(lines) + + async def add_to_history( + self, + round: int, + expert_name: str, + content: str, + role: str = "expert", + ) -> None: + """Add a speech to the discussion history. + + Args: + round: Round number (1-indexed) + expert_name: Name of the expert (or "user" for interventions) + content: Speech content + role: "expert" | "moderator" | "user" + """ + entry = { + "round": round, + "expert_name": expert_name, + "content": content, + "timestamp": time.time(), + "role": role, + } + self._history.append(entry) + + def get_history_text(self, up_to_round: int | None = None) -> str: + """Get formatted discussion history for LLM prompt injection. + + Args: + up_to_round: If provided, only include history up to this round (inclusive) + + Returns: + Formatted history text + """ + if not self._history: + return "" + + lines: list[str] = [] + for entry in self._history: + if up_to_round is not None and entry["round"] > up_to_round: + continue + + role_label = { + "moderator": "主持人小结", + "user": "用户干预", + "expert": "专家发言", + }.get(entry["role"], entry["role"]) + + lines.append( + f"[第{entry['round']}轮 | {entry['expert_name']} | {role_label}]\n" + f"{entry['content']}" + ) + + return "\n\n---\n\n".join(lines) + + async def compress_history(self, moderator: Expert, llm_gateway: Any) -> None: + """Compress discussion history when it exceeds token threshold. + + The moderator summarizes each round's key points, replacing + verbose speeches with concise summaries. + + Args: + moderator: Moderator expert + llm_gateway: LLM gateway for compression + """ + if not self._history or len(self._history) < 10: + return + + # Group by round + rounds: dict[int, list[dict]] = {} + for entry in self._history: + rounds.setdefault(entry["round"], []).append(entry) + + # Build compression prompt + history_text = self.get_history_text() + prompt = ( + "你是私董会主持人。请压缩以下讨论历史,保留每轮的关键观点和核心论点," + "去除冗余内容。每轮压缩为 2-3 句话。\n\n" + f"讨论历史:\n{history_text}\n\n" + "请输出压缩后的历史,保持原有的轮次结构和专家名。格式:\n" + "[第X轮 | 专家名] 压缩后的观点" + ) + + try: + response = await llm_gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + compressed = response.content.strip() + + # Parse compressed history back to entries + # This is a best-effort compression; if parsing fails, keep original + new_history: list[dict[str, Any]] = [] + current_round = 0 + for line in compressed.split("\n"): + line = line.strip() + if not line: + continue + if line.startswith("[第") and "轮" in line: + # Parse round number + try: + round_str = line.split("第")[1].split("轮")[0] + current_round = int(round_str) + name_part = line.split("|")[1].strip() if "|" in line else "unknown" + content_part = line.split("]", 1)[1].strip() if "]" in line else line + new_history.append({ + "round": current_round, + "expert_name": name_part, + "content": content_part, + "timestamp": time.time(), + "role": "expert", + }) + except (ValueError, IndexError): + continue + + if new_history: + self._history = new_history + logger.info(f"History compressed: {len(rounds)} rounds, {len(new_history)} entries") + except Exception as e: + logger.warning(f"History compression failed: {e}, keeping original history") + + async def add_user_intervention(self, content: str) -> None: + """Add a user intervention message to the discussion. + + The message will be visible to all experts in the next round. + + Args: + content: User's intervention message + """ + self._user_interventions.append(content) + await self.add_to_history( + round=self._current_round, + expert_name="user", + content=content, + role="user", + ) + + # Broadcast user intervention event + await self._handoff_transport.send( + self._team_channel, + { + "type": "user_intervention", + "content": content, + "round": self._current_round, + }, + ) + + def consume_user_interventions(self) -> list[str]: + """Get and clear pending user interventions. + + Returns: + List of user intervention messages + """ + interventions = self._user_interventions.copy() + self._user_interventions.clear() + return interventions + + def increment_round(self) -> int: + """Increment the current round counter. + + Returns: + The new round number + """ + self._current_round += 1 + return self._current_round + + async def dissolve(self) -> None: + """Dissolve the board. Experts are recycled, outputs preserved.""" + for expert in self._experts.values(): + if expert.is_active and self._pool: + await expert.destroy(self._pool) + + self._experts.clear() + self._moderator_name = None + self._status = BoardStatus.DISSOLVED + self._handoff_transport.close() diff --git a/src/agentkit/experts/board_orchestrator.py b/src/agentkit/experts/board_orchestrator.py new file mode 100644 index 0000000..a6b2276 --- /dev/null +++ b/src/agentkit/experts/board_orchestrator.py @@ -0,0 +1,523 @@ +"""BoardOrchestrator - 私董会讨论引擎 + +驱动 BoardTeam 执行多轮群聊式讨论: + +1. 主持人开场介绍议题和讨论规则 +2. 循环 max_rounds 轮: + - 所有非主持人专家并行生成发言(基于共享讨论历史 + 角色 prompt) + - 主持人小结本轮要点 + - 检查用户干预和停止命令 +3. 主持人最终总结(决策建议、共识点、分歧点) + +终止条件: +- 正常终止:达到最大轮次 +- 用户终止:用户发送 /stop +- 异常终止:LLM 不可用或所有专家发言失败 +""" + +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +from .expert import Expert +from .board import BoardTeam, BoardStatus + +logger = logging.getLogger(__name__) + + +class BoardOrchestrator: + """Board meeting orchestration engine. + + The moderator (lead expert) facilitates the discussion: + - Opens with topic introduction + - Summarizes each round + - Gives final decision advice + + Member experts give speeches each round based on shared history. + """ + + STOP_COMMANDS = frozenset({"/stop", "停止讨论", "stop", "结束讨论"}) + + def __init__(self, team: BoardTeam) -> None: + self._team = team + + async def execute(self, topic: str) -> dict[str, Any]: + """Execute a board meeting discussion. + + Flow: + 1. Broadcast board_started event + 2. Moderator opens the discussion + 3. Loop max_rounds times: + - Parallel generate member speeches + - Moderator summarizes the round + - Check for user intervention / stop + 4. Moderator gives final conclusion + 5. Broadcast board_concluded event + + Returns: + Dict with status, summary, decision_advice, total_rounds, + consensus_points, dissent_points + """ + moderator = self._team.moderator + if not moderator or not moderator.is_active: + active = self._team.active_experts + if not active: + return { + "status": "failed", + "summary": "", + "decision_advice": "", + "total_rounds": 0, + "consensus_points": [], + "dissent_points": [], + "error": "No active expert available", + } + # Promote first active expert to moderator + self._team._moderator_name = active[0].config.name + moderator = active[0] + logger.warning( + f"Moderator not available, falling back to '{moderator.config.name}'" + ) + + self._team.set_status(BoardStatus.DISCUSSING) + + # 1. Broadcast board_started event + await self._broadcast_event( + "board_started", + { + "team_id": self._team.team_id, + "topic": topic, + "experts": [ + { + "name": e.config.name, + "avatar": e.config.avatar, + "color": e.config.color, + "is_moderator": e.config.name == self._team._moderator_name, + "persona": e.config.persona[:100], + } + for e in self._team.active_experts + ], + "max_rounds": self._team.max_rounds, + }, + ) + + try: + # 2. Moderator opens the discussion + opening = await self._generate_moderator_opening(moderator, topic) + if opening: + await self._team.add_to_history(0, moderator.config.name, opening, "moderator") + await self._broadcast_event( + "expert_speech", + { + "expert_name": moderator.config.name, + "expert_avatar": moderator.config.avatar, + "expert_color": moderator.config.color, + "content": opening, + "round": 0, + "role": "moderator", + }, + ) + + # 3. Discussion rounds + for round_num in range(1, self._team.max_rounds + 1): + self._team.increment_round() + + # Check for stop command before starting the round + interventions = self._team.consume_user_interventions() + if self._has_stop_command(interventions): + logger.info(f"Discussion stopped by user at round {round_num}") + break + + # Generate member speeches in parallel + members = self._team.member_experts + if members: + speech_results = await asyncio.gather( + *[self._generate_expert_speech(e, round_num) for e in members], + return_exceptions=True, + ) + + # Broadcast speeches in order (not parallel broadcast) + for expert, result in zip(members, speech_results): + if isinstance(result, Exception): + logger.warning( + f"Expert '{expert.config.name}' speech failed: {result}" + ) + continue + + await self._team.add_to_history( + round_num, expert.config.name, result, "expert" + ) + await self._broadcast_event( + "expert_speech", + { + "expert_name": expert.config.name, + "expert_avatar": expert.config.avatar, + "expert_color": expert.config.color, + "content": result, + "round": round_num, + "role": "expert", + }, + ) + + # Moderator summarizes the round + summary = await self._generate_moderator_summary(moderator, round_num) + if summary: + await self._team.add_to_history( + round_num, moderator.config.name, summary, "moderator" + ) + await self._broadcast_event( + "round_summary", + { + "moderator_name": moderator.config.name, + "content": summary, + "round": round_num, + "continue": round_num < self._team.max_rounds, + }, + ) + + # Check history length and compress if needed + gateway = self._get_llm_gateway(moderator) + if gateway and len(self._team.history) > 20: + await self._team.compress_history(moderator, gateway) + + # 4. Final conclusion + self._team.set_status(BoardStatus.CONCLUDING) + conclusion = await self._generate_final_conclusion(moderator, topic) + + self._team.set_status(BoardStatus.COMPLETED) + + # 5. Broadcast board_concluded event + await self._broadcast_event( + "board_concluded", + { + "summary": conclusion.get("summary", ""), + "decision_advice": conclusion.get("decision_advice", ""), + "total_rounds": self._team.current_round, + "consensus_points": conclusion.get("consensus_points", []), + "dissent_points": conclusion.get("dissent_points", []), + }, + ) + + return { + "status": "completed", + "summary": conclusion.get("summary", ""), + "decision_advice": conclusion.get("decision_advice", ""), + "total_rounds": self._team.current_round, + "consensus_points": conclusion.get("consensus_points", []), + "dissent_points": conclusion.get("dissent_points", []), + } + + except Exception as e: + logger.error(f"Board meeting execution failed: {e}") + self._team.set_status(BoardStatus.DISSOLVED) + + # Try to give a fallback conclusion + fallback = await self._generate_fallback_conclusion(moderator, topic) + + await self._broadcast_event( + "board_concluded", + { + "summary": fallback.get("summary", ""), + "decision_advice": fallback.get("decision_advice", ""), + "total_rounds": self._team.current_round, + "consensus_points": [], + "dissent_points": [], + "error": str(e), + }, + ) + + return { + "status": "failed", + "summary": fallback.get("summary", ""), + "decision_advice": fallback.get("decision_advice", ""), + "total_rounds": self._team.current_round, + "consensus_points": [], + "dissent_points": [], + "error": str(e), + } + + async def _generate_moderator_opening(self, moderator: Expert, topic: str) -> str: + """Generate moderator's opening speech. + + The moderator introduces the topic and sets the stage for discussion. + """ + gateway = self._get_llm_gateway(moderator) + if not gateway: + return f"欢迎来到私董会。今天的讨论主题是:{topic}。请各位专家发表看法。" + + prompt = ( + f"你是私董会主持人 {moderator.config.name}。\n" + f"你的角色:{moderator.config.persona}\n" + f"你的表达风格:{moderator.config.speaking_style}\n\n" + f"讨论主题:{topic}\n\n" + "请作为主持人开场,介绍议题并邀请各位专家发表看法。" + "开场应该简洁有力,2-3 段话,点明讨论的核心问题。" + ) + + try: + response = await gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + return response.content.strip() + except Exception as e: + logger.warning(f"Moderator opening generation failed: {e}") + return f"欢迎来到私董会。今天的讨论主题是:{topic}。请各位专家发表看法。" + + async def _generate_expert_speech(self, expert: Expert, round: int) -> str: + """Generate an expert's speech for the current round. + + The speech is based on: + - Expert's persona, thinking_style, speaking_style, decision_framework + - Full discussion history + - Current round / max rounds + """ + gateway = self._get_llm_gateway(expert) + if not gateway: + return f"[{expert.config.name} 因 LLM 不可用无法发言]" + + history_text = self._team.get_history_text() + + prompt = ( + f"你是 {expert.config.name},正在参加私董会讨论。\n\n" + f"你的角色:{expert.config.persona}\n" + f"你的思维风格:{expert.config.thinking_style}\n" + f"你的表达风格:{expert.config.speaking_style}\n" + f"你的决策框架:{expert.config.decision_framework}\n\n" + f"讨论主题:{self._team.topic}\n" + f"当前轮次:第 {round} 轮 / 共 {self._team.max_rounds} 轮\n\n" + ) + + if history_text: + prompt += f"之前的讨论历史:\n{history_text}\n\n" + + prompt += ( + "请基于你的角色和决策框架,就当前讨论主题发表你的看法。" + "要求:\n" + "- 保持角色一致性,用你的思维方式和表达风格发言\n" + "- 2-4 段话,简洁但有洞察力\n" + "- 可以引用或反驳之前发言者的观点\n" + "- 给出明确的立场或建议\n" + ) + + response = await gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + return response.content.strip() + + async def _generate_moderator_summary(self, moderator: Expert, round: int) -> str: + """Generate moderator's round summary. + + The moderator summarizes the key points of the current round. + """ + gateway = self._get_llm_gateway(moderator) + if not gateway: + return f"[第 {round} 轮小结因 LLM 不可用无法生成]" + + # Get only current round's speeches + round_history = [ + h for h in self._team.history if h["round"] == round + ] + if not round_history: + return "" + + round_text = "\n\n".join( + f"[{h['expert_name']}]: {h['content']}" for h in round_history + ) + + prompt = ( + f"你是私董会主持人 {moderator.config.name}。\n" + f"你的角色:{moderator.config.persona}\n" + f"你的表达风格:{moderator.config.speaking_style}\n\n" + f"讨论主题:{self._team.topic}\n" + f"当前轮次:第 {round} 轮 / 共 {self._team.max_rounds} 轮\n\n" + f"本轮发言:\n{round_text}\n\n" + "请作为主持人小结本轮讨论:\n" + "- 归纳各方核心观点(2-3 句话)\n" + "- 指出共识点和分歧点\n" + "- 提示下一轮可以深入的方向\n" + "- 保持简洁,3-5 句话\n" + ) + + try: + response = await gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + return response.content.strip() + except Exception as e: + logger.warning(f"Moderator summary generation failed: {e}") + return f"[第 {round} 轮讨论完成,主持人小结生成失败]" + + async def _generate_final_conclusion(self, moderator: Expert, topic: str) -> dict[str, Any]: + """Generate moderator's final conclusion. + + The moderator gives: + - Overall summary of the discussion + - Decision advice + - Consensus points + - Dissent points + """ + gateway = self._get_llm_gateway(moderator) + if not gateway: + return { + "summary": "讨论已完成,但 LLM 不可用无法生成总结。", + "decision_advice": "建议参考讨论历史自行判断。", + "consensus_points": [], + "dissent_points": [], + } + + history_text = self._team.get_history_text() + + prompt = ( + f"你是私董会主持人 {moderator.config.name}。\n" + f"你的角色:{moderator.config.persona}\n" + f"你的表达风格:{moderator.config.speaking_style}\n" + f"你的决策框架:{moderator.config.decision_framework}\n\n" + f"讨论主题:{topic}\n" + f"总轮次:{self._team.current_round}\n\n" + f"完整讨论历史:\n{history_text}\n\n" + "请作为主持人给出最终总结。输出 JSON 格式:\n" + "```json\n" + "{\n" + ' "summary": "整体讨论总结,3-5句话",\n' + ' "decision_advice": "基于讨论的决策建议,明确给出你的推荐",\n' + ' "consensus_points": ["共识点1", "共识点2"],\n' + ' "dissent_points": ["分歧点1", "分歧点2"]\n' + "}\n" + "```\n" + "只输出 JSON,不要其他文字。" + ) + + try: + import json + import re + + response = await gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + content = response.content.strip() + + # Extract JSON from response + json_match = re.search(r"\{.*\}", content, re.DOTALL) + if json_match: + result = json.loads(json_match.group(0)) + return { + "summary": result.get("summary", ""), + "decision_advice": result.get("decision_advice", ""), + "consensus_points": result.get("consensus_points", []), + "dissent_points": result.get("dissent_points", []), + } + + # If JSON parsing fails, return raw content as summary + return { + "summary": content, + "decision_advice": "", + "consensus_points": [], + "dissent_points": [], + } + except Exception as e: + logger.warning(f"Final conclusion generation failed: {e}") + return { + "summary": f"讨论已完成({self._team.current_round}轮),总结生成失败。", + "decision_advice": "建议参考讨论历史自行判断。", + "consensus_points": [], + "dissent_points": [], + } + + async def _generate_fallback_conclusion(self, moderator: Expert, topic: str) -> dict[str, Any]: + """Generate a fallback conclusion when execution fails. + + Uses existing discussion history to provide a basic summary. + """ + history_text = self._team.get_history_text() + if not history_text: + return { + "summary": "讨论未能正常完成,无历史记录。", + "decision_advice": "", + } + + gateway = self._get_llm_gateway(moderator) + if not gateway: + # Return truncated history as summary + return { + "summary": f"讨论异常终止。已有历史({len(self._team.history)}条):\n" + + history_text[:500], + "decision_advice": "建议参考讨论历史自行判断。", + } + + prompt = ( + f"你是私董会主持人 {moderator.config.name}。\n" + f"讨论主题:{topic}\n" + f"讨论因异常终止,已完成 {self._team.current_round} 轮。\n\n" + f"已有讨论历史:\n{history_text}\n\n" + "请基于已有历史给出总结和决策建议。输出 JSON:\n" + "```json\n" + '{"summary": "...", "decision_advice": "..."}\n' + "```\n" + ) + + try: + import json + import re + + response = await gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + content = response.content.strip() + json_match = re.search(r"\{.*\}", content, re.DOTALL) + if json_match: + result = json.loads(json_match.group(0)) + return { + "summary": result.get("summary", content), + "decision_advice": result.get("decision_advice", ""), + } + return {"summary": content, "decision_advice": ""} + except Exception: + return { + "summary": f"讨论异常终止,已完成 {self._team.current_round} 轮。", + "decision_advice": "", + } + + def _has_stop_command(self, interventions: list[str]) -> bool: + """Check if any user intervention contains a stop command.""" + for msg in interventions: + msg_lower = msg.strip().lower() + if msg_lower in self.STOP_COMMANDS: + return True + return False + + def _get_llm_gateway(self, expert: Expert | None = None) -> Any: + """Get LLM gateway from the given expert or the moderator's agent. + + Falls back to other active experts if the primary target has no gateway. + """ + target = expert or self._team.moderator + if target and hasattr(target, "agent") and hasattr(target.agent, "_llm_gateway"): + gateway = target.agent._llm_gateway + if gateway is not None: + return gateway + # Fallback: try first active expert with a gateway + for exp in self._team.active_experts: + if hasattr(exp, "agent") and hasattr(exp.agent, "_llm_gateway"): + gateway = exp.agent._llm_gateway + if gateway is not None: + return gateway + return None + + async def _broadcast_event(self, event_type: str, data: dict[str, Any]) -> None: + """Broadcast a board event to the team channel. + + Events are emitted via handoff_transport for WebSocket relay. + """ + if self._team.handoff_transport: + try: + await self._team.handoff_transport.send( + self._team.team_channel, {"type": event_type, **data} + ) + except Exception as e: + logger.warning(f"Failed to broadcast event '{event_type}': {e}") diff --git a/src/agentkit/experts/board_router.py b/src/agentkit/experts/board_router.py new file mode 100644 index 0000000..dc7a4a7 --- /dev/null +++ b/src/agentkit/experts/board_router.py @@ -0,0 +1,179 @@ +"""BoardRouter - 私董会讨论模式路由 + +解析 @board 前缀,支持指定专家或使用默认私董会模板。 + +支持格式: +- @board:expert1,expert2 讨论主题 — 指定专家 +- @board 讨论主题 — 使用默认 private_board 模板 +- @board:private_board 讨论主题 — 显式使用默认模板 +""" + +from __future__ import annotations + +import copy +import logging +import re +from dataclasses import dataclass, field + +from .config import ExpertConfig +from .registry import ExpertTemplateRegistry + +logger = logging.getLogger(__name__) + + +# Pattern to match @board or @board:expert1,expert2 prefix +BOARD_PREFIX_PATTERN = re.compile(r"^@board(?::(\S+))?\s*(.*)", re.DOTALL) + +# Valid expert name: alphanumeric, underscore, hyphen, 1-64 chars +_EXPERT_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$") + +MAX_EXPERTS = 10 # Maximum number of experts in a board +DEFAULT_TEMPLATE = "private_board" + + +@dataclass +class BoardRoutingResult: + """Result of board routing resolution. + + Attributes: + matched: Whether the input matched @board prefix + board_mode: Whether board mode is activated + specified_experts: List of expert names specified by user + topic: Discussion topic extracted from input + use_default_template: Whether to use default private_board template + match_method: How the match was made ("explicit_board" | "default_template" | "") + """ + + matched: bool = False + board_mode: bool = False + specified_experts: list[str] = field(default_factory=list) + topic: str = "" + use_default_template: bool = False + match_method: str = "" + + +class BoardRouter: + """Routes user input to Board Meeting mode via @board prefix. + + Supports: + - @board prefix → trigger board mode with default private_board template + - @board:expert1,expert2 → specify board members by name + - @board:private_board → explicitly use default template + """ + + def __init__(self, template_registry: ExpertTemplateRegistry | None = None): + self._registry = template_registry or ExpertTemplateRegistry() + + def resolve(self, content: str) -> BoardRoutingResult: + """Resolve user input to a BoardRoutingResult. + + Args: + content: User's input message + + Returns: + BoardRoutingResult with routing decision + """ + result = BoardRoutingResult() + + match = BOARD_PREFIX_PATTERN.match(content.strip()) + if not match: + result.matched = False + result.board_mode = False + result.topic = content.strip() + return result + + expert_list_str = match.group(1) # e.g., "expert1,expert2" or None + topic = match.group(2).strip() # The actual topic content + + result.matched = True + result.board_mode = True + result.topic = topic if topic else "" + result.match_method = "explicit_board" + + if expert_list_str: + # Check if user specified the default template name + if expert_list_str.strip() == DEFAULT_TEMPLATE: + result.use_default_template = True + result.specified_experts = self._load_default_template_members() + else: + # User specified expert names — validate and limit + raw_names = [name.strip() for name in expert_list_str.split(",")] + valid_names = [n for n in raw_names if _EXPERT_NAME_RE.match(n)] + if len(valid_names) != len(raw_names): + invalid = set(raw_names) - set(valid_names) + logger.warning(f"Invalid expert names rejected: {invalid}") + if valid_names: + result.specified_experts = valid_names[:MAX_EXPERTS] + result.use_default_template = False + else: + # All names invalid — fall back to default template + logger.warning( + "All expert names invalid, falling back to default template" + ) + result.use_default_template = True + result.specified_experts = self._load_default_template_members() + else: + # No specific experts — use default template + result.use_default_template = True + result.specified_experts = self._load_default_template_members() + + return result + + def resolve_expert_configs(self, specified_experts: list[str]) -> list[ExpertConfig]: + """Resolve expert names to ExpertConfig instances. + + For names that match templates, use the template config. + For names that don't match, create a dynamic ExpertConfig. + The first expert is designated as moderator (is_lead=True). + """ + configs: list[ExpertConfig] = [] + for i, name in enumerate(specified_experts): + if not _EXPERT_NAME_RE.match(name): + logger.warning(f"Skipping invalid expert name: {name}") + continue + + template = self._registry.get(name) + if template: + # Deep-copy to avoid mutating the shared template config + config = copy.deepcopy(template.config) + # Override is_lead: first expert is moderator + config.is_lead = i == 0 + configs.append(config) + else: + # Dynamic generation — create a basic ExpertConfig + config = ExpertConfig( + name=name, + agent_type="expert", + persona=f"Expert in {name}", + thinking_style="analytical", + bound_skills=[], + is_lead=(i == 0), + task_mode="llm_generate", + prompt={"identity": f"Expert in {name}"}, + ) + configs.append(config) + + # Ensure at least one expert is lead + if configs and not any(c.is_lead for c in configs): + configs[0].is_lead = True + + return configs + + def _load_default_template_members(self) -> list[str]: + """Load member list from the default private_board template. + + The private_board template is stored as an ExpertTemplate with + a special 'members' field in its config metadata. + + Falls back to a hardcoded list if the template is not found. + """ + template = self._registry.get(DEFAULT_TEMPLATE) + if template: + # The private_board template stores members in config.bound_skills + # as a reuse of existing field (avoids schema changes) + members = template.config.bound_skills + if members: + return members[:MAX_EXPERTS] + + # Fallback default members + return ["elon_musk", "jeff_bezos", "allenzhang", "charlie_munger", "paul_graham"] diff --git a/src/agentkit/experts/config.py b/src/agentkit/experts/config.py index 79b8615..b6b9a42 100644 --- a/src/agentkit/experts/config.py +++ b/src/agentkit/experts/config.py @@ -39,6 +39,9 @@ class ExpertConfig(AgentConfig): avatar: str = "", color: str = "#1890ff", is_lead: bool = False, + # Board Meeting 模式字段 + speaking_style: str = "", + decision_framework: str = "", ): super().__init__( name=name, @@ -63,6 +66,8 @@ class ExpertConfig(AgentConfig): self.avatar = avatar self.color = color self.is_lead = is_lead + self.speaking_style = speaking_style + self.decision_framework = decision_framework @classmethod def from_dict(cls, data: dict[str, Any]) -> ExpertConfig: @@ -89,6 +94,8 @@ class ExpertConfig(AgentConfig): avatar=data.get("avatar", ""), color=data.get("color", "#1890ff"), is_lead=data.get("is_lead", False), + speaking_style=data.get("speaking_style", ""), + decision_framework=data.get("decision_framework", ""), ) def to_dict(self) -> dict[str, Any]: @@ -101,6 +108,8 @@ class ExpertConfig(AgentConfig): d["avatar"] = self.avatar d["color"] = self.color d["is_lead"] = self.is_lead + d["speaking_style"] = self.speaking_style + d["decision_framework"] = self.decision_framework return d diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py index 7a0c5e7..e2543c1 100644 --- a/src/agentkit/server/app.py +++ b/src/agentkit/server/app.py @@ -326,6 +326,50 @@ async def lifespan(app: FastAPI): memory_store._on_change = _on_memory_change app.state.memory_store = memory_store + # Load ExpertTemplates from configured paths (supports @board meeting mode) + # This runs regardless of GUI mode so @board works in API-only mode too. + try: + from agentkit.experts.registry import ExpertTemplateRegistry + + expert_registry = ExpertTemplateRegistry() + + # Always try to load from the default configs/experts/ directory + default_experts_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "configs", + "experts", + ) + expert_dirs: list[str] = [default_experts_dir] + + # Append user-configured paths from agentkit.yaml + if server_config and getattr(server_config, "expert_paths", None): + expert_dirs.extend(server_config.expert_paths) + + total_loaded = 0 + for experts_dir in expert_dirs: + if not experts_dir: + continue + from pathlib import Path as _P + + p = _P(experts_dir) + if p.is_dir(): + loaded = expert_registry.load_from_directory(str(p)) + if loaded: + logger.info( + f"Loaded {len(loaded)} ExpertTemplates from {p}" + ) + total_loaded += len(loaded) + + app.state.expert_template_registry = expert_registry + if total_loaded: + logger.info(f"Total {total_loaded} ExpertTemplates registered for @board mode") + except Exception as e: + logger.warning(f"Failed to load ExpertTemplates: {e}") + # Ensure app.state.expert_template_registry always exists (empty registry) + from agentkit.experts.registry import ExpertTemplateRegistry + + app.state.expert_template_registry = ExpertTemplateRegistry() + yield # Shutdown @@ -595,6 +639,11 @@ def create_app( ) app.state.request_preprocessor = request_preprocessor + # Initialize ExpertTemplateRegistry (populated in lifespan with YAML configs) + from agentkit.experts.registry import ExpertTemplateRegistry + + app.state.expert_template_registry = ExpertTemplateRegistry() + # Initialize OrganizationContext from AgentPool + SkillRegistry from agentkit.org.context import OrganizationContext diff --git a/src/agentkit/server/config.py b/src/agentkit/server/config.py index ebcbb03..ef07bf9 100644 --- a/src/agentkit/server/config.py +++ b/src/agentkit/server/config.py @@ -114,6 +114,8 @@ class ServerConfig: usage_store: dict[str, Any] | None = None, cascade_store: dict[str, Any] | None = None, evolution: dict[str, Any] | None = None, + expert_paths: list[str] | None = None, + board: dict[str, Any] | None = None, on_change: Callable[["ServerConfig"], None] | None = None, ): self.host = host @@ -140,6 +142,8 @@ class ServerConfig: self.usage_store = usage_store or {} self.cascade_store = cascade_store or {} self.evolution = evolution or {} + self.expert_paths = expert_paths or [] + self.board = board or {} self.on_change = on_change # Config watching state @@ -216,6 +220,13 @@ class ServerConfig: # Evolution store config evolution_data = data.get("evolution", {}) + # Expert templates config (paths to YAML files defining ExpertTemplates) + experts_data = data.get("experts", {}) + expert_paths = experts_data.get("paths", []) + + # Board meeting config (max_rounds, default_template, etc.) + board_data = data.get("board", {}) + return cls( host=server.get("host", "0.0.0.0"), port=server.get("port", 8001), @@ -241,6 +252,8 @@ class ServerConfig: usage_store=usage_store_data, cascade_store=cascade_store_data, evolution=evolution_data, + expert_paths=expert_paths, + board=board_data, ) @staticmethod @@ -436,6 +449,8 @@ class ServerConfig: self.marketplace = new_config.marketplace self.alignment = new_config.alignment self.router = new_config.router + self.expert_paths = new_config.expert_paths + self.board = new_config.board self._last_mtime = new_config._last_mtime logger.info(f"Config reloaded from {path}") diff --git a/src/agentkit/server/frontend/components.d.ts b/src/agentkit/server/frontend/components.d.ts index 2d5ed7b..42231c0 100644 --- a/src/agentkit/server/frontend/components.d.ts +++ b/src/agentkit/server/frontend/components.d.ts @@ -88,6 +88,7 @@ declare module 'vue' { SplashScreen: typeof import('./src/components/layout/SplashScreen.vue')['default'] SplitPane: typeof import('./src/components/layout/SplitPane.vue')['default'] TerminalEmulator: typeof import('./src/components/terminal/TerminalEmulator.vue')['default'] + ThinkingBlock: typeof import('./src/components/chat/ThinkingBlock.vue')['default'] TitleBar: typeof import('./src/components/layout/TitleBar.vue')['default'] ToolCallCard: typeof import('./src/components/chat/ToolCallCard.vue')['default'] ToolCallIndicator: typeof import('./src/components/chat/ToolCallIndicator.vue')['default'] diff --git a/src/agentkit/server/frontend/src/api/types.ts b/src/agentkit/server/frontend/src/api/types.ts index 06f0353..fe641c3 100644 --- a/src/agentkit/server/frontend/src/api/types.ts +++ b/src/agentkit/server/frontend/src/api/types.ts @@ -44,7 +44,10 @@ export interface IChatMessage { expert_id?: string expert_name?: string expert_color?: string - message_type?: 'chat' | 'handoff' | 'assist_request' | 'plan_update' | 'milestone' + expert_avatar?: string + message_type?: 'chat' | 'handoff' | 'assist_request' | 'plan_update' | 'milestone' | 'board_speech' | 'board_summary' | 'board_conclusion' + board_round?: number + board_role?: 'moderator' | 'expert' | 'user' | 'summary' } /** Conversation with messages */ @@ -103,6 +106,12 @@ export type WsServerMessage = | { type: 'plan_update'; data: { plan_phases: ITeamPlanPhase[] } } | { type: 'team_synthesis'; data: { content: string } } | { type: 'team_dissolved'; data: { team_id: string } } + // Board Meeting 模式事件 + | { type: 'board_started'; data: IBoardStartedData } + | { type: 'expert_speech'; data: IExpertSpeechData } + | { type: 'round_summary'; data: IRoundSummaryData } + | { type: 'user_intervention'; data: IUserInterventionData } + | { type: 'board_concluded'; data: IBoardConcludedData } /** Expert info within a team */ export interface IExpertInfo { @@ -135,6 +144,74 @@ export interface IExpertTeamState { lead_expert: string } +// ── Board Meeting 模式类型 ──────────────────────────────────────────── + +/** Board meeting expert info (lighter than IExpertInfo) */ +export interface IBoardExpert { + name: string + avatar: string + color: string + is_moderator: boolean + persona: string +} + +/** board_started event payload */ +export interface IBoardStartedData { + team_id: string + topic: string + experts: IBoardExpert[] + max_rounds: number +} + +/** expert_speech event payload */ +export interface IExpertSpeechData { + expert_name: string + expert_avatar: string + expert_color: string + content: string + round: number + role: 'moderator' | 'expert' +} + +/** round_summary event payload */ +export interface IRoundSummaryData { + moderator_name: string + content: string + round: number + continue: boolean +} + +/** user_intervention event payload */ +export interface IUserInterventionData { + content: string + round: number +} + +/** board_concluded event payload */ +export interface IBoardConcludedData { + summary: string + decision_advice: string + total_rounds: number + consensus_points: string[] + dissent_points: string[] + error?: string +} + +/** Board meeting status (matches backend BoardStatus enum) */ +export type BoardStatus = 'forming' | 'discussing' | 'concluding' | 'completed' | 'dissolved' + +/** Board message entry for group chat display */ +export interface IBoardMessage { + id: string + expert_name: string + expert_avatar: string + expert_color: string + content: string + round: number + role: 'moderator' | 'expert' | 'user' | 'summary' + timestamp: number +} + /** API error */ export interface IApiError { status: number diff --git a/src/agentkit/server/frontend/src/components/chat/BoardStatusView.vue b/src/agentkit/server/frontend/src/components/chat/BoardStatusView.vue new file mode 100644 index 0000000..ce14dd7 --- /dev/null +++ b/src/agentkit/server/frontend/src/components/chat/BoardStatusView.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue b/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue index f6dedb5..b236b5c 100644 --- a/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue +++ b/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue @@ -19,10 +19,13 @@
diff --git a/src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue b/src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue index 05f4b57..fb5f52f 100644 --- a/src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue +++ b/src/agentkit/server/frontend/src/components/chat/ExpertMessage.vue @@ -1,12 +1,15 @@