diff --git a/PLAN4.md b/PLAN4.md new file mode 100644 index 0000000..18158f7 --- /dev/null +++ b/PLAN4.md @@ -0,0 +1,277 @@ +# PLAN4 — 流编辑器深度重构:LLM→智能体→流节点→工作流 四层架构 + +## 一、问题诊断 + +### 当前架构痛点(逐一定位代码行级别) + +| # | 问题 | 根因位置 | 影响 | +|---|------|----------|------| +| 1 | **LLM/智能体与节点未打通** | [engine.py#L101-L118](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/modules/flow_engine/engine.py#L101-L118) `LLMNodeAgent` 每次硬编码新建 `OpenAIChatModel`,忽略节点配置中 `model`/`temperature`/`system_prompt` | 节点的LLM配置表单完全无效 | +| 2 | **节点只有壳,无法配置** | [FlowNode.vue](file:///c:/Users/刘泽明/Documents/Git/hg-agents/frontend/src/views/flow/FlowNode.vue) 固定 Top/Bottom Handle,条件节点只有一个出口 | 条件分支无法区分 true/false | +| 3 | **连线位置漂移** | [FlowEdge.vue](file:///c:/Users/刘泽明/Documents/Git/hg-agents/frontend/src/views/flow/FlowEdge.vue) 自定义 SVG path + 没有绑定 vue-flow edge marker | 线条起点/终点不精准 | +| 4 | **条件分支不可控** | [engine.py#L277-L293](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/modules/flow_engine/engine.py#L277-L293) `ConditionNodeAgent` 只返回固定字符串 + 拓扑排序不支持跳过 | 即使 LLM 判断了真假,引擎也不会分支 | +| 5 | **触发节点无实际参数** | FlowEditor 触发节点只有标签,无企微回调 URL / webhook 配置 | 无法真正接入企微触发 | +| 6 | **AgentFactory 未集成** | [engine.py](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/modules/flow_engine/engine.py) 从未调用 [factory.py](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/agentscope_integration/factory.py) | 智能体复用、缓存管理能力全部浪费 | + +--- + +## 二、目标架构:四层递进模型 + +``` +┌──────────────────────────────────────────────┐ +│ 第四层: 工作流 (Workflow) │ +│ 多个节点串联的执行拓扑图,含条件分支、循环 │ +│ → FlowEngine.execute() │ +├──────────────────────────────────────────────┤ +│ 第三层: 流节点 (Flow Node) │ +│ 每个节点是可配置的 Agent 实例 │ +│ → LLMNodeAgent / ToolNodeAgent / ... │ +├──────────────────────────────────────────────┤ +│ 第二层: 智能体 (Agent) │ +│ 封装了 LLM + 工具 + 系统提示词的 agent │ +│ → AgentFactory.get_agent(agent_type, config) │ +├──────────────────────────────────────────────┤ +│ 第一层: LLM 模型 (Model) │ +│ 底层大语言模型配置 (api_key, model, base_url) │ +│ → config.py Settings / 模型配置面板 │ +└──────────────────────────────────────────────┘ +``` + +用户操作流程:**新建 Agent(选定模型+提示词+工具)→ 拖入流编辑器作为节点 → 连线组装工作流 → 发布到企微** + +--- + +## 三、实施方案 + +### P0 — 核心打通(必须完成) + +#### P0-1: 节点 Handle 重构 — 按节点类型动态分配输入/输出端口 + +**文件**: [FlowNode.vue](file:///c:/Users/刘泽明/Documents/Git/hg-agents/frontend/src/views/flow/FlowNode.vue) + +**现状**: 所有节点固定 Top=target, Bottom=source,条件节点无法双出口。 + +**目标**: +``` +trigger: Top(target) ←可有可无 | Bottom(source) +llm: Left(target) | Right(source) +tool: Left(target) | Right(source) +mcp: Left(target) | Right(source) +wecom_notify: Left(target) | 无出口(终节点) OR Bottom(source) +condition: Left(target) | Right(source=true) + Bottom(source=false) +rag: Left(target) | Right(source) +output: Left(target) | 无出口 +``` + +**实施方案**: +1. 为条件节点增加两个 `Handle`:`Position.Right` (id="true") 和 `Position.Bottom` (id="false") +2. 为终节点(output/wecom_notify)移除 source Handle +3. FlowEditor.vue 中 `onConnect` 记录 `sourceHandle`,条件节点时记录含 `true`/`false` 分支信息 +4. 保存时 edges 格式增加 `condition` 字段:`{source, target, condition: "true"|"false"}` + +#### P0-2: LLM节点集成 AgentFactory — 使用真实 Agent 配置 + +**文件**: [engine.py#L101-L160](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/modules/flow_engine/engine.py#L101-L160) + +**现状**: `LLMNodeAgent.reply()` 每次新建模型,忽略节点 `config`。 + +**实施方案**: +```python +class LLMNodeAgent(AgentBase): + async def reply(self, msg: Msg, **kwargs) -> Msg: + # 从节点 config 读取参数,而非硬编码 + system_prompt = self.config.get("system_prompt", "") + model_name = self.config.get("model", settings.LLM_MODEL) + temperature = float(self.config.get("temperature", 0.7)) + agent_id = self.config.get("agent_id", "") # 可选:关联已有 Agent + + # 优先使用 AgentFactory 获取或创建 agent + if agent_id: + agent = await Agents.get_or_create(agent_id, model_name, system_prompt) + else: + agent = await self._create_agent(model_name, system_prompt, temperature) + + result = await agent(msg) + return self._to_msg(result) +``` + +#### P0-3: 条件分支引擎 — 支持 true/false 两条路径 + +**文件**: [engine.py#L277-L340](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/modules/flow_engine/engine.py#L277-L340) + [engine.py#L45-L75](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/modules/flow_engine/engine.py#L45-L75) + +**现状**: 拓扑排序线性执行,无法跳过分支。 + +**实施方案**: + +将 `FlowEngine.execute()` 从纯拓扑排序改为 **DAG 条件遍历**: + +```python +async def execute(self, input_msg, context): + nodes = self.definition["nodes"] + edges = self.definition["edges"] + + # 构建邻接表: node_id -> [(target_id, condition)] + graph = self._build_graph(nodes, edges) + + # 找到起始节点(入度为0的节点) + start_nodes = self._find_start_nodes(graph) + + # BFS/DFS 带条件遍历 + result = await self._traverse(start_nodes[0], input_msg, context, graph) + + return result + +async def _traverse(self, node_id, msg, context, graph): + agent = self._get_agent(node_id) + result = await agent.reply(msg) + context["_node_results"][node_id] = str(result) + + # 如果是条件节点,根据判断结果选择分支 + next_nodes = graph.get(node_id, []) + for target_id, cond in next_nodes: + if cond is None or cond == "true": + await self._traverse(target_id, result, context, graph) + + return result +``` + +**关键**: 条件节点执行后解析 `condition:true|xxx` 或 `condition:false|xxx`,只沿对应分支继续。 + +#### P0-4: 连线位置修复 — 使用 vue-flow 原生 Handle 机制 + +**文件**: [FlowEditor.vue](file:///c:/Users/刘泽明/Documents/Git/hg-agents/frontend/src/views/flow/FlowEditor.vue#L247-L268) + +**现状**: `elements` 用的是普通数组,节点自定义组件通过 `id` 查找,vue-flow 无法正确渲染 SVG 连线。 + +**实施方案**: +1. 使用 `useVueFlow` 的 `addNodes`/`addEdges` API 代替手动 `elements.value.push` +2. 注册 `@connect` 事件,让 vue-flow 原生处理连线渲染 +3. 删除 [FlowEdge.vue](file:///c:/Users/刘泽明/Documents/Git/hg-agents/frontend/src/views/flow/FlowEdge.vue) 自定义 edge(vue-flow 内置 bezier 曲线更精准) +4. 条件节点的两条边使用不同颜色:true=绿, false=红 + +```typescript +const { addNodes, addEdges, onConnect: onVueConnect } = useVueFlow() + +function onConnect(connection: Edge) { + const newEdge = { + ...connection, + id: `edge_${connection.source}_${connection.target}`, + type: connection.sourceHandle === 'false' ? 'smoothstep-red' : 'default', + animated: true, + style: connection.sourceHandle === 'false' + ? { stroke: '#F56C6C' } + : { stroke: '#409EFF' }, + } + addEdges([newEdge]) +} +``` + +--- + +### P1 — 节点配置增强(提升灵活性) + +#### P1-1: LLM节点 — 支持选择已有 Agent + +**文件**: FlowEditor.vue LLM 配置区域 + +- **新增** `agent_id` 下拉框:从后端 `GET /api/agents` 加载已有智能体列表 +- 选择 Agent 后自动填入 `system_prompt`、`model`、`temperature` +- 不选 Agent 则手动填写(当前行为保留) + +#### P1-2: 触发节点 — 企微 Webhook URL 配置 + +**文件**: FlowEditor.vue trigger 配置区域 + +- 触发节点增加 `wecom_webhook_url` 和 `event_type` 字段 +- `event_type` 支持:`text_message` / `button_click` / `enter_chat` +- 发布流时自动注册企微回调(如果配置了 webhook) + +#### P1-3: 节点配置持久化增强 — 支持 JSON Schema + +**后端 schema 变更**: [schemas/__init__.py](file:///c:/Users/刘泽明/Documents/Git/hg-agents/backend/schemas/__init__.py#L179-L181) + +`FlowNode.config` 目前是 `dict = {}`,改为结构化的各类型: + +```python +class LLMNodeConfig(BaseModel): + system_prompt: str = "" + model: str = "gpt-4o-mini" + temperature: float = 0.7 + agent_id: str = "" + +class ToolNodeConfig(BaseModel): + tool_name: str = "" + +class ConditionNodeConfig(BaseModel): + condition: str = "" + +class WeComNotifyNodeConfig(BaseModel): + message_template: str = "" + target: str = "@all" + +class FlowNode(BaseModel): + id: str | None = None + type: str # trigger/llm/tool/mcp/wecom_notify/condition/rag/output + label: str + config: LLMNodeConfig | ToolNodeConfig | ... = {} +``` + +#### P1-4: 新增 Agent 管理 API(后端) + +**新文件**: `backend/modules/agent/router.py` + +提供 `GET/POST/PUT/DELETE /api/agents`,管理可复用的 Agent: + +```python +# Agent 模型 +class Agent(Base): + __tablename__ = "agents" + name, system_prompt, model, temperature, tools: list[str], status +``` + +这样 FlowEditor 的 LLM 节点可以通过 API 获取已有 Agent 列表。 + +--- + +### P2 — 体验完善 + +#### P2-1: 撤销/重做 (Ctrl+Z / Ctrl+Y) + +使用 vue-flow 内置的 `useUndoRedo` composable。 + +#### P2-2: 节点缩略图 MiniMap 增强 + +当前 MiniMap 已导入但不可用,改为真正的 Pane 节点缩略图。 + +#### P2-3: 流模板市场 + +预置 3-5 个常用工作流模板(文档处理流、企微通知流、数据分析流)。 + +--- + +## 四、文件变更清单 + +| 文件 | 变更类型 | 说明 | +|------|----------|------| +| `frontend/src/views/flow/FlowNode.vue` | **重写** | 按节点类型动态 Handle | +| `frontend/src/views/flow/FlowEdge.vue` | **删除** | 用 vue-flow 内置 edge | +| `frontend/src/views/flow/FlowEditor.vue` | **重大重构** | useVueFlow API、Agent选择、条件边 | +| `backend/modules/flow_engine/engine.py` | **重大重构** | AgentFactory集成、条件DAG遍历 | +| `backend/modules/flow_engine/router.py` | 修改 | 新增agent列表查询端点 | +| `backend/modules/agent/router.py` | **新建** | Agent CRUD API | +| `backend/models/__init__.py` | 修改 | 新增Agent模型 | +| `backend/schemas/__init__.py` | 修改 | 新增Agent schema + FlowNode结构化config | +| `frontend/src/api/index.ts` | 修改 | 新增agentApi | +| `frontend/src/router/index.ts` | 修改 | 新增agent管理路由 | + +--- + +## 五、实施顺序 + +``` +Phase 1 (P0-1, P0-4) → Handle重构 + 连线修复 [前端为主] +Phase 2 (P0-3) → 条件分支引擎 [后端为主] +Phase 3 (P0-2, P1-1) → LLM节点集成AgentFactory [前后端联动] +Phase 4 (P1-2 ~ P1-4) → 节点配置增强 + Agent管理API [前后端联动] +Phase 5 (P2-1 ~ P2-3) → 体验完善 [前端为主] +``` \ No newline at end of file diff --git a/backend/models/__init__.py b/backend/models/__init__.py index 7af3900..20cf5e5 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -198,6 +198,22 @@ class SystemMetric(Base): collected_at = Column(DateTime, default=datetime.utcnow) +class AgentConfig(Base): + __tablename__ = "agent_configs" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String(100), nullable=False) + description = Column(String(500)) + system_prompt = Column(Text, default="") + model = Column(String(50), default="gpt-4o-mini") + temperature = Column(Integer, default=7) + tools = Column(JSON, default=list) + status = Column(String(20), default="active") + creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + class AuditLog(Base): __tablename__ = "audit_logs" diff --git a/backend/modules/agent_manager/router.py b/backend/modules/agent_manager/router.py index bf791bf..c047491 100644 --- a/backend/modules/agent_manager/router.py +++ b/backend/modules/agent_manager/router.py @@ -1,9 +1,10 @@ import uuid -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from database import get_db -from models import User, ChatSession, ChatMessage +from models import User, ChatSession, ChatMessage, AgentConfig +from schemas import AgentConfigCreate, AgentConfigUpdate, AgentConfigOut from agentscope_integration.factory import AgentFactory router = APIRouter(prefix="/api/agent", tags=["agent"]) @@ -16,10 +17,6 @@ async def agent_chat( payload: dict, db: AsyncSession = Depends(get_db), ): - """ - 与智能体对话。 - agent_type: employee | manager | task | document - """ user_ctx = request.state.user user_id = uuid.UUID(user_ctx["id"]) msg_content = payload.get("message", "") @@ -28,7 +25,6 @@ async def agent_chat( result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: - from fastapi import HTTPException raise HTTPException(404, "用户不存在") session_result = await db.execute( @@ -80,18 +76,107 @@ async def agent_chat( @router.get("/list") -async def get_agent_list(request: Request): +async def get_agent_list(request: Request, db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(AgentConfig).where(AgentConfig.status == "active").order_by(AgentConfig.updated_at.desc()) + ) + agents = result.scalars().all() return { "code": 200, - "data": [ - {"type": "employee", "name": "员工AI助手", "description": "日常问答、文档处理、知识查询"}, - {"type": "manager", "name": "管理分析助手", "description": "下属工作分析、效能评估"}, - {"type": "task", "name": "任务管理助手", "description": "任务创建、分派、追踪"}, - {"type": "document", "name": "文档处理助手", "description": "格式修正、内容提取、导入导出"}, - ], + "data": [{ + "id": str(a.id), + "name": a.name, + "description": a.description, + "system_prompt": a.system_prompt, + "model": a.model, + "temperature": float(a.temperature) / 10.0, + "tools": a.tools or [], + "status": a.status, + } for a in agents], } +@router.post("/", response_model=AgentConfigOut) +async def create_agent(req: AgentConfigCreate, request: Request, db: AsyncSession = Depends(get_db)): + user_ctx = request.state.user + agent = AgentConfig( + name=req.name, + description=req.description, + system_prompt=req.system_prompt, + model=req.model, + temperature=int(req.temperature * 10), + tools=req.tools, + creator_id=uuid.UUID(user_ctx["id"]), + ) + db.add(agent) + await db.flush() + return AgentConfigOut( + id=agent.id, name=agent.name, description=agent.description, + system_prompt=agent.system_prompt, model=agent.model, + temperature=float(agent.temperature) / 10.0, + tools=agent.tools or [], status=agent.status, + creator_id=agent.creator_id, + created_at=agent.created_at, updated_at=agent.updated_at, + ) + + +@router.get("/{agent_id}", response_model=AgentConfigOut) +async def get_agent(agent_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(AgentConfig).where(AgentConfig.id == agent_id)) + agent = result.scalar_one_or_none() + if not agent: + raise HTTPException(404, "Agent不存在") + return AgentConfigOut( + id=agent.id, name=agent.name, description=agent.description, + system_prompt=agent.system_prompt, model=agent.model, + temperature=float(agent.temperature) / 10.0, + tools=agent.tools or [], status=agent.status, + creator_id=agent.creator_id, + created_at=agent.created_at, updated_at=agent.updated_at, + ) + + +@router.put("/{agent_id}", response_model=AgentConfigOut) +async def update_agent(agent_id: uuid.UUID, req: AgentConfigUpdate, request: Request, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(AgentConfig).where(AgentConfig.id == agent_id)) + agent = result.scalar_one_or_none() + if not agent: + raise HTTPException(404, "Agent不存在") + if req.name is not None: + agent.name = req.name + if req.description is not None: + agent.description = req.description + if req.system_prompt is not None: + agent.system_prompt = req.system_prompt + if req.model is not None: + agent.model = req.model + if req.temperature is not None: + agent.temperature = int(req.temperature * 10) + if req.tools is not None: + agent.tools = req.tools + if req.status is not None: + agent.status = req.status + await db.flush() + return AgentConfigOut( + id=agent.id, name=agent.name, description=agent.description, + system_prompt=agent.system_prompt, model=agent.model, + temperature=float(agent.temperature) / 10.0, + tools=agent.tools or [], status=agent.status, + creator_id=agent.creator_id, + created_at=agent.created_at, updated_at=agent.updated_at, + ) + + +@router.delete("/{agent_id}") +async def delete_agent(agent_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(AgentConfig).where(AgentConfig.id == agent_id)) + agent = result.scalar_one_or_none() + if not agent: + raise HTTPException(404, "Agent不存在") + await db.delete(agent) + return {"code": 200, "message": "已删除"} + + @router.get("/history/{session_id}") async def get_chat_history( session_id: str, @@ -103,7 +188,6 @@ async def get_chat_history( ) session = session_result.scalar_one_or_none() if not session: - from fastapi import HTTPException raise HTTPException(404, "会话不存在") msg_result = await db.execute( diff --git a/backend/modules/flow_engine/engine.py b/backend/modules/flow_engine/engine.py index 5c02299..0f1bec9 100644 --- a/backend/modules/flow_engine/engine.py +++ b/backend/modules/flow_engine/engine.py @@ -1,7 +1,7 @@ import json import uuid import logging -from collections import deque +import re from agentscope.agent import AgentBase from agentscope.message import Msg from agentscope.tool import Toolkit @@ -21,20 +21,33 @@ class FlowEngine: self._agent_cache: dict[str, AgentBase] = {} async def execute(self, input_msg: Msg, context: dict) -> Msg: - execution_order = self._topological_sort() - current_msg = input_msg + graph = self._build_graph() + start_nodes = self._find_start_nodes(graph) + if not start_nodes: + start_nodes = list(self.nodes.keys())[:1] - for i, node_id in enumerate(execution_order): - agent = await self._get_or_create_agent(node_id, context) - node = self.nodes[node_id] + visited: set[str] = set() + last_result: Msg | None = None + + async def traverse(node_id: str, incoming_msg: Msg) -> None: + nonlocal last_result + if node_id in visited: + return + visited.add(node_id) + + node = self.nodes.get(node_id) + if not node: + return - enriched_content = self._resolve_input_mapping(node, current_msg, context) + agent = await self._get_or_create_agent(node_id, context) + enriched_content = self._resolve_input_mapping(node, incoming_msg, context) + current_msg = incoming_msg if enriched_content.strip(): user_text = current_msg.get_text_content() if hasattr(current_msg, 'get_text_content') else str(current_msg) current_msg = Msg(name="user", content=f"{enriched_content}\n\n---\n{user_text}", role="user") try: - result = await agent(current_msg) + result = await agent.reply(current_msg) exec_record = { "node_id": node_id, "node_type": node.get("type"), @@ -43,7 +56,17 @@ class FlowEngine: "output": result.get_text_content()[:500] if hasattr(result, 'get_text_content') else str(result)[:500], } context.setdefault("_node_results", {})[node_id] = exec_record - current_msg = result + last_result = result + + is_condition = node.get("type") == "condition" + cond_result = self._parse_condition_result(result) + + for target_id, edge_cond in graph.get(node_id, []): + if is_condition: + if edge_cond and edge_cond == cond_result: + await traverse(target_id, result) + else: + await traverse(target_id, result) except Exception as e: logger.error(f"节点 {node.get('label', node_id)} 执行失败: {e}") exec_record = { @@ -54,9 +77,39 @@ class FlowEngine: "error": str(e), } context.setdefault("_node_results", {})[node_id] = exec_record - current_msg = Msg(name="system", content=f"[节点 {node.get('label', node_id)} 执行失败: {e}]", role="system") + error_msg = Msg(name="system", content=f"[节点 {node.get('label', node_id)} 执行失败: {e}]", role="system") + last_result = error_msg - return current_msg + if start_nodes: + await traverse(start_nodes[0], input_msg) + + return last_result or input_msg + + def _build_graph(self) -> dict[str, list[tuple[str, str | None]]]: + graph: dict[str, list[tuple[str, str | None]]] = {nid: [] for nid in self.nodes} + for edge in self.edges: + source = edge.get("source") or edge.get("from") + target = edge.get("target") or edge.get("to") + cond = edge.get("condition") or edge.get("sourceHandle") + if cond == "source": + cond = None + if source and target and source in self.nodes and target in self.nodes: + graph[source].append((target, cond)) + return graph + + def _find_start_nodes(self, graph: dict) -> list[str]: + target_nodes: set[str] = set() + for targets in graph.values(): + for target_id, _ in targets: + target_nodes.add(target_id) + return [nid for nid in self.nodes if nid not in target_nodes] + + def _parse_condition_result(self, result: Msg) -> str | None: + content = result.get_text_content() if hasattr(result, 'get_text_content') else str(result) + m = re.search(r'condition:(true|false)', content) + if m: + return m.group(1) + return None async def _get_or_create_agent(self, node_id: str, context: dict) -> AgentBase: if node_id in self._agent_cache: @@ -67,32 +120,6 @@ class FlowEngine: self._agent_cache[node_id] = agent return agent - def _topological_sort(self) -> list[str]: - in_degree: dict[str, int] = {nid: 0 for nid in self.nodes} - adj: dict[str, list[str]] = {nid: [] for nid in self.nodes} - - for edge in self.edges: - source = edge.get("from") or edge.get("source") - target = edge.get("to") or edge.get("target") - if source and target and source in self.nodes and target in self.nodes: - adj[source].append(target) - in_degree[target] = in_degree.get(target, 0) + 1 - - queue = deque([nid for nid, deg in in_degree.items() if deg == 0]) - order = [] - - while queue: - node_id = queue.popleft() - order.append(node_id) - for neighbor in adj.get(node_id, []): - in_degree[neighbor] -= 1 - if in_degree[neighbor] == 0: - queue.append(neighbor) - - remaining = [nid for nid in self.nodes if nid not in order] - order.extend(remaining) - return order - def _resolve_input_mapping(self, node: dict, current_msg: Msg, context: dict) -> str: config = node.get("config", {}) input_mapping = config.get("input_mapping") @@ -176,16 +203,10 @@ class LLMNodeAgent(AgentBase): self.temperature = temperature async def reply(self, msg: Msg, **kwargs) -> Msg: - from agentscope.model import OpenAIChatModel + from agentscope_integration.factory import AgentFactory from agentscope.formatter import OpenAIChatFormatter - model = OpenAIChatModel( - config_name=f"flow_llm_{self.name}", - model_name=self.model_name, - api_key=settings.LLM_API_KEY, - api_base=settings.LLM_API_BASE, - ) - + model = AgentFactory._get_model() user_text = msg.get_text_content() if hasattr(msg, 'get_text_content') else str(msg) formatter = OpenAIChatFormatter() diff --git a/backend/modules/flow_engine/router.py b/backend/modules/flow_engine/router.py index 19dd07c..e009b58 100644 --- a/backend/modules/flow_engine/router.py +++ b/backend/modules/flow_engine/router.py @@ -233,6 +233,101 @@ async def test_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Dep return {"code": 200, "data": validation} +FLOW_TEMPLATES = [ + { + "id": "tpl_doc_process", + "name": "文档处理流", + "description": "自动解析文档内容,提取关键信息并生成摘要", + "icon": "Document", + "nodes": [ + {"id": "n1", "type": "trigger", "label": "文档上传", "config": {"event_type": "document_upload"}, "position": {"x": 100, "y": 100}}, + {"id": "n2", "type": "tool", "label": "解析文档", "config": {"tool_name": "parse_document"}, "position": {"x": 400, "y": 100}}, + {"id": "n3", "type": "llm", "label": "生成摘要", "config": {"system_prompt": "请为以下文档内容生成简洁摘要", "model": "gpt-4o-mini", "temperature": 0.5}, "position": {"x": 700, "y": 100}}, + {"id": "n4", "type": "output", "label": "输出结果", "config": {"format": "text"}, "position": {"x": 1000, "y": 100}}, + ], + "edges": [ + {"source": "n1", "target": "n2", "sourceHandle": "source"}, + {"source": "n2", "target": "n3", "sourceHandle": "source"}, + {"source": "n3", "target": "n4", "sourceHandle": "source"}, + ], + }, + { + "id": "tpl_wecom_notify", + "name": "企微通知流", + "description": "接收触发后查询数据并推送企微通知", + "icon": "Bell", + "nodes": [ + {"id": "n1", "type": "trigger", "label": "定时触发", "config": {"event_type": "scheduled"}, "position": {"x": 100, "y": 100}}, + {"id": "n2", "type": "tool", "label": "查询任务", "config": {"tool_name": "list_tasks"}, "position": {"x": 400, "y": 100}}, + {"id": "n3", "type": "condition", "label": "有待办任务?", "config": {"condition": "tasks.length > 0"}, "position": {"x": 700, "y": 100}}, + {"id": "n4", "type": "wecom_notify", "label": "推送通知", "config": {"message_template": "您有{{tasks.length}}条待办任务", "target": "@all"}, "position": {"x": 1000, "y": 50}}, + {"id": "n5", "type": "output", "label": "无任务", "config": {"format": "text"}, "position": {"x": 1000, "y": 200}}, + ], + "edges": [ + {"source": "n1", "target": "n2", "sourceHandle": "source"}, + {"source": "n2", "target": "n3", "sourceHandle": "source"}, + {"source": "n3", "target": "n4", "sourceHandle": "true"}, + {"source": "n3", "target": "n5", "sourceHandle": "false"}, + ], + }, + { + "id": "tpl_data_analysis", + "name": "数据分析流", + "description": "查询员工数据并生成效率分析报告", + "icon": "DataAnalysis", + "nodes": [ + {"id": "n1", "type": "trigger", "label": "分析请求", "config": {"event_type": "button_click"}, "position": {"x": 100, "y": 100}}, + {"id": "n2", "type": "tool", "label": "查询下属", "config": {"tool_name": "list_subordinates"}, "position": {"x": 400, "y": 100}}, + {"id": "n3", "type": "tool", "label": "统计数据", "config": {"tool_name": "get_task_statistics"}, "position": {"x": 700, "y": 100}}, + {"id": "n4", "type": "llm", "label": "生成报告", "config": {"system_prompt": "基于以下数据生成团队效率分析报告", "model": "gpt-4o", "temperature": 0.7}, "position": {"x": 1000, "y": 100}}, + {"id": "n5", "type": "output", "label": "报告输出", "config": {"format": "json"}, "position": {"x": 1300, "y": 100}}, + ], + "edges": [ + {"source": "n1", "target": "n2", "sourceHandle": "source"}, + {"source": "n2", "target": "n3", "sourceHandle": "source"}, + {"source": "n3", "target": "n4", "sourceHandle": "source"}, + {"source": "n4", "target": "n5", "sourceHandle": "source"}, + ], + }, + { + "id": "tpl_rag_qa", + "name": "知识库问答流", + "description": "从知识库检索信息后由LLM回答", + "icon": "Search", + "nodes": [ + {"id": "n1", "type": "trigger", "label": "问题触发", "config": {"event_type": "text_message"}, "position": {"x": 100, "y": 100}}, + {"id": "n2", "type": "rag", "label": "知识检索", "config": {"knowledge_base": "default", "top_k": 5}, "position": {"x": 400, "y": 100}}, + {"id": "n3", "type": "llm", "label": "生成回答", "config": {"system_prompt": "基于知识库检索结果回答用户问题", "model": "gpt-4o-mini", "temperature": 0.3}, "position": {"x": 700, "y": 100}}, + {"id": "n4", "type": "output", "label": "输出答案", "config": {"format": "text"}, "position": {"x": 1000, "y": 100}}, + ], + "edges": [ + {"source": "n1", "target": "n2", "sourceHandle": "source"}, + {"source": "n2", "target": "n3", "sourceHandle": "source"}, + {"source": "n3", "target": "n4", "sourceHandle": "source"}, + ], + }, + { + "id": "tpl_task_auto", + "name": "任务自动分配流", + "description": "根据描述自动创建任务并分派给合适人员", + "icon": "Tools", + "nodes": [ + {"id": "n1", "type": "trigger", "label": "任务描述", "config": {"event_type": "text_message"}, "position": {"x": 100, "y": 100}}, + {"id": "n2", "type": "llm", "label": "分析任务", "config": {"system_prompt": "分析以下任务描述,提取标题、优先级、负责人", "model": "gpt-4o-mini", "temperature": 0.5}, "position": {"x": 400, "y": 100}}, + {"id": "n3", "type": "tool", "label": "创建任务", "config": {"tool_name": "create_task"}, "position": {"x": 700, "y": 100}}, + {"id": "n4", "type": "wecom_notify", "label": "通知负责人", "config": {"message_template": "您有新任务: {{task_title}}", "target": "@all"}, "position": {"x": 1000, "y": 100}}, + {"id": "n5", "type": "output", "label": "完成", "config": {"format": "text"}, "position": {"x": 1300, "y": 100}}, + ], + "edges": [ + {"source": "n1", "target": "n2", "sourceHandle": "source"}, + {"source": "n2", "target": "n3", "sourceHandle": "source"}, + {"source": "n3", "target": "n4", "sourceHandle": "source"}, + {"source": "n4", "target": "n5", "sourceHandle": "source"}, + ], + }, +] + + @router.get("/market", response_model=list[FlowDefinitionOut]) async def flow_market(request: Request, db: AsyncSession = Depends(get_db)): result = await db.execute( @@ -250,6 +345,44 @@ async def flow_market(request: Request, db: AsyncSession = Depends(get_db)): ) for f in flows] +@router.get("/templates") +async def get_flow_templates(request: Request): + return {"code": 200, "data": FLOW_TEMPLATES} + + +@router.post("/templates/{template_id}/use") +async def use_flow_template( + template_id: str, + request: Request, + db: AsyncSession = Depends(get_db), +): + template = next((t for t in FLOW_TEMPLATES if t["id"] == template_id), None) + if not template: + raise HTTPException(404, "模板不存在") + + user_ctx = request.state.user + flow = FlowDefinition( + name=template["name"] + " (副本)", + description=template["description"], + definition_json={ + "nodes": template["nodes"], + "edges": template["edges"], + "trigger": {}, + }, + creator_id=uuid.UUID(user_ctx["id"]), + ) + db.add(flow) + await db.flush() + + return FlowDefinitionOut( + id=flow.id, name=flow.name, description=flow.description, + version=flow.version, status=flow.status, + definition_json=flow.definition_json, + published_to_wecom=flow.published_to_wecom, + created_at=flow.created_at, updated_at=flow.updated_at, + ) + + @router.get("/executions") async def list_executions(request: Request, db: AsyncSession = Depends(get_db)): result = await db.execute( diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py index 81b5beb..9a82c53 100644 --- a/backend/schemas/__init__.py +++ b/backend/schemas/__init__.py @@ -176,6 +176,36 @@ class EmployeeAnalysis(BaseModel): # --- Flow --- +class TriggerNodeConfig(BaseModel): + event_type: str = "text_message" + +class LLMNodeConfig(BaseModel): + system_prompt: str = "" + model: str = "gpt-4o-mini" + temperature: float = 0.7 + agent_id: str = "" + +class ToolNodeConfig(BaseModel): + tool_name: str = "" + +class MCPNodeConfig(BaseModel): + mcp_server: str = "" + tool_name: str = "" + +class WeComNotifyNodeConfig(BaseModel): + message_template: str = "" + target: str = "" + +class ConditionNodeConfig(BaseModel): + condition: str = "" + +class RAGNodeConfig(BaseModel): + knowledge_base: str = "" + top_k: int = 5 + +class OutputNodeConfig(BaseModel): + format: str = "text" + class FlowNode(BaseModel): id: str | None = None type: str @@ -257,6 +287,43 @@ class MCPServiceOut(BaseModel): from_attributes = True +# --- Agent Config --- +class AgentConfigCreate(BaseModel): + name: str + description: str | None = None + system_prompt: str = "" + model: str = "gpt-4o-mini" + temperature: float = 0.7 + tools: list[str] = [] + + +class AgentConfigUpdate(BaseModel): + name: str | None = None + description: str | None = None + system_prompt: str | None = None + model: str | None = None + temperature: float | None = None + tools: list[str] | None = None + status: str | None = None + + +class AgentConfigOut(BaseModel): + id: uuid.UUID + name: str + description: str | None = None + system_prompt: str + model: str + temperature: float + tools: list[str] = [] + status: str + creator_id: uuid.UUID | None = None + created_at: datetime | None = None + updated_at: datetime | None = None + + class Config: + from_attributes = True + + # --- Notification --- class NotificationTemplateCreate(BaseModel): name: str diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1b78f62..2f8754d 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -18,11 +18,11 @@ api.interceptors.request.use((config) => { api.interceptors.response.use( (response) => response.data, (error) => { - const msg = error.response?.data?.message || error.message || '请求失败' if (error.response?.status === 401) { localStorage.removeItem('token') - router.push('/login') + return Promise.reject(error) } + const msg = error.response?.data?.message || error.message || '请求失败' ElMessage.error(msg) return Promise.reject(error) } @@ -82,6 +82,8 @@ export const flowApi = { executeFlow: (id: string, data: any) => api.post(`/flow/definitions/${id}/execute`, data), testFlow: (id: string) => api.post(`/flow/definitions/${id}/test`), getMarket: () => api.get('/flow/market'), + getTemplates: () => api.get('/flow/templates'), + useTemplate: (id: string) => api.post(`/flow/templates/${id}/use`), } export const wecomApi = { @@ -94,6 +96,10 @@ export const agentApi = { chat: (type: string, data: any) => api.post(`/agent/chat/${type}`, data), getList: () => api.get('/agent/list'), getHistory: (sessionId: string) => api.get(`/agent/history/${sessionId}`), + getAgent: (id: string) => api.get(`/agent/${id}`), + createAgent: (data: any) => api.post('/agent/', data), + updateAgent: (id: string, data: any) => api.put(`/agent/${id}`, data), + deleteAgent: (id: string) => api.delete(`/agent/${id}`), } export const mcpApi = { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index f58582a..d912021 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -4,6 +4,14 @@ import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHashHistory(), routes: [ + { + path: '/', + redirect: '/login', + }, + { + path: '/:pathMatch(.*)*', + redirect: '/login', + }, { path: '/login', name: 'Login', @@ -200,31 +208,32 @@ const router = createRouter({ ], }) -router.beforeEach(async (to, _from, next) => { +router.beforeEach(async (to, _from) => { const userStore = useUserStore() if (!userStore.token) { - if (to.name === 'Login') { next(); return } - next({ name: 'Login', query: { redirect: to.fullPath } }) - return + if (to.name === 'Login') { return true } + return { name: 'Login', query: { redirect: to.fullPath } } } if (!userStore.user) { - await userStore.fetchUser() + try { + await userStore.fetchUser() + } catch { + return { name: 'Login', query: { redirect: to.fullPath } } + } if (!userStore.isLoggedIn) { - next({ name: 'Login', query: { redirect: to.fullPath } }) - return + return { name: 'Login', query: { redirect: to.fullPath } } } } if (to.meta.perms && Array.isArray(to.meta.perms) && to.meta.perms.length > 0) { if (!userStore.hasPermission(to.meta.perms[0])) { - next('/user/dashboard') - return + return '/user/dashboard' } } - next() + return true }) export default router \ No newline at end of file diff --git a/frontend/src/views/agent/AgentList.vue b/frontend/src/views/agent/AgentList.vue index e18c1c3..ff6926d 100644 --- a/frontend/src/views/agent/AgentList.vue +++ b/frontend/src/views/agent/AgentList.vue @@ -2,51 +2,164 @@
{{ agent.description }}
-拖拽节点到画布
-点击节点端口连线
-右键删除连线
+从绿色圆点拖线(true)
+从红色圆点拖线(false)
+右键点击边可删除
滚轮缩放画布
{{ tpl.description || '暂无描述' }}
+ +