Files
deerflow-code/frontend-web/docs/recommend-agents-progress.md
T
2026-05-25 16:08:18 +08:00

9.5 KiB
Raw Blame History

智能体推荐弹窗功能 — 开发进度

分支: dhk-1
最后更新: 2026-05-25
状态: 全部完成 ✅


功能概述

在圆桌规划流程的 Step 1(任务意图理解)和 Step 2(多智能体圆桌研讨)之间,插入一个推荐弹窗:

  1. 用户完成任务澄清后,点击"进入多智能体研讨"→ 弹出推荐弹窗
  2. 弹窗拉取 GET /api/agents 候选池,调用 roundtable-recommender 智能体流式推荐 2-6 个最合适的智能体
  3. 推荐结果默认勾选,用户可取消勾选 / 从候选池增选(总选 2-8 个)
  4. 确认后 Step 2 使用用户最终选定的智能体列表,不再硬编码 6 个圆桌席位

已完成的所有步骤

Step 1 — 后端:新建 roundtable-recommender 智能体 ✅

文件:

  • offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/config.yaml
  • offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/SOUL.md

要点:

  • id: roundtable-recommender,name: 智能体推荐
  • SOUL.md 规定输出协议:自然语言推理 + [RECOMMEND_READY] 标记 + ```json {"picks": [...]} ``` 块
  • 每个 pick: {"agent_id": "...", "reason": "..."},agent_id 必须来自候选池
  • 约束:2 ≤ picks ≤ 6,禁止使用任何工具

Step 2 — 后端:新建 /api/recommend/stream 接口 ✅

文件:

  • offline-backend-20260512/backend/app/gateway/routers/recommend.py(新建)
  • offline-backend-20260512/backend/app/gateway/app.py(注册路由)

接口规格:

POST /api/recommend/stream
Content-Type: application/json

Body:
{
  "intent": { "objective": "...", "constraints": [...], "assumptions": [...] },
  "candidates": [{ "agent_id": "...", "name": "...", "description": "..." }, ...],
  "model": "deepseek-chat"   // 可选
}

SSE 流:透传上游 LangGraph frames + 最终状态帧
最终帧: { "status": "done"|"asking"|"error", "content": "...", "picks": [...] | null }

关键实现:

  • 无 /init,每次请求内部创建新 thread(stateless)
  • _validate_picks():对比候选池 id、去重、夹断到 2-6,< 2 返回 []
  • _build_prompt():将 intent + 候选池渲染为单条 user message
  • _RECOMMEND_READY_PATTERN:正则提取 [RECOMMEND_READY] 后的 JSON 块
  • httpx.AsyncClient(trust_env=False):防止 loopback 被代理拦截
  • 复用 multi_agent.py 的 _auth_headers, _create_thread, _stream_upstream 等

app.py 注册:

from app.gateway.routers import recommend
app.include_router(recommend.router)

Step 3 — 前端:recommend.ts API 客户端 ✅

文件: src/roundtable-planning/api/recommend.ts

导出:

// 类型
export interface RecommendPick { agent_id: string; reason: string }
export interface StreamRecommendResult { status: "done"|"asking"|"error"; content: string; picks: RecommendPick[]|null }
export type RecommendStreamPhase = "connecting"|"connected"|"streaming"
export interface RecommendCandidate { agent_id: string; name: string; description: string }

// 候选池过滤(排除规则集中管理)
export function filterRecommendCandidates(agents: Agent[]): RecommendCandidate[]

// 流式推荐
export async function streamRecommend(req: StreamRecommendRequest): Promise<StreamRecommendResult>

排除规则(filterRecommendCandidates):

  • roundtable-intent — Step 1 澄清专员
  • roundtable-recommender — 推荐者自身
  • 前缀 roundtable-coordinator- — 每 session 生成的一次性协调器

Step 4 — 前端:RecommendAgentsDialog.tsx 弹窗组件 ✅

文件: src/roundtable-planning/components/RecommendAgentsDialog.tsx

Props:

export interface RecommendAgentsDialogProps {
  open: boolean;
  intent: IntentSummary | null;
  model?: string;
  onCancel: () => void;
  onConfirm: (agentIds: string[]) => void;
}

内部阶段机器:

"idle" → "loading_agents" → "recommending" → "ready"
                                           ↘ "fallback"  (推荐失败,兜底)
                                           ↘ "error"

UI 分区:

  • 推荐参会(默认全选):推荐智能体卡片,带 reason 说明
  • 其他候选(默认未选):可增选,补足 8 个上限

选择约束: min 2,max 8;确认按钮在范围外 disabled

安全机制:

  • generationRef 计数器防止 stale async 污染状态
  • AbortController 取消在途推荐请求
  • intersectInOrder():前端二次验证推荐 id 在候选池中存在
  • 兜底列表 FALLBACK_AGENT_IDS:6 个内置 roundtable-* 智能体

确认顺序: 推荐且已选 → 用户增选(按交互顺序)


Step 5 — 前端:RoundtablePlanningPage.tsx 动态 Agent 角色 ✅

删除: ROUNDTABLE_ROLE_TEMPLATES, makeRoles, ROUNDTABLE_AGENT_IDS, AGENT_NAME_TO_ROLE(全部硬编码常量)

新增模块级:

const COMMANDER_SEAT_ID = 1
type SelectedAgent = { agent_id: string; name: string }
const BUILTIN_AGENT_META: Record<string, { avatarType: string; displayName: string }>
const FALLBACK_AVATAR_TYPES = ["blue","purple","emerald","amber","pink"]
function getAvatarTypeFor(agentId, index): string
function getDisplayNameFor(agent: SelectedAgent): string
function buildRoleTemplates(agents): ReadonlyArray<Omit<RoleEntry,"status">>
function makeRolesFromTemplates(templates, defaultStatus, overrides?): RoleEntry[]
function buildAgentNameToRole(agents): Record<string, {speakerId,avatarType,displayName}>
const DEFAULT_SELECTED_AGENTS: SelectedAgent[]  // 原 6 个内置席位

新增 state + useMemo:

const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(DEFAULT_SELECTED_AGENTS);
const roleTemplates    = useMemo(() => buildRoleTemplates(selectedAgents),    [selectedAgents]);
const agentNameToRole  = useMemo(() => buildAgentNameToRole(selectedAgents),  [selectedAgents]);
const roundtableAgentIds = useMemo(() => selectedAgents.map(a => a.agent_id), [selectedAgents]);

替换调用点:

替换前 替换后
AGENT_NAME_TO_ROLE[name] agentNameToRole[name]
ROUNDTABLE_AGENT_IDS roundtableAgentIds
makeRoles(status, ...) makeRolesFromTemplates(roleTemplates, status, ...)
角色面板 role.id === 2 ? <RobotAvatar ...> 链 getAvatar(agentNameToRole[role.agentId]?.avatarType ?? "blue", "h-7 w-7")
HighFidelityReport seatNameMap 用 AGENT_NAME_TO_ROLE 用 agentNameToRole
useEffect 依赖 加入 roleTemplates
dispatch useCallback 依赖 加入 agentNameToRole

Step 6 — 前端:弹窗接入页面 ✅

新增 import:

import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";

新增 state:

const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);

新增 callback handleConfirmAgents:

const handleConfirmAgents = useCallback((agentIds: string[]) => {
  setSelectedAgents(
    agentIds.map(id => ({
      agent_id: id,
      name: BUILTIN_AGENT_META[id]?.displayName ?? id,
    })),
  );
  step2InitRef.current = false;   // 重置 Step 2,让新 coordinator 按新名单初始化
  setIsRecommendDialogOpen(false);
  setCurrentStep(2);
  setShowToast("成功确认!已切换至第二阶段圆桌研讨系统");
}, []);

两个"进入多智能体研讨"按钮改为:

onClick={() => setIsRecommendDialogOpen(true)}

JSX 渲染(在 showToast 之前):

<RecommendAgentsDialog
  open={isRecommendDialogOpen}
  intent={intentReady}
  model={DEFAULT_MODEL}
  onCancel={() => setIsRecommendDialogOpen(false)}
  onConfirm={handleConfirmAgents}
/>

文件清单(本次改动)

后端(新建)

文件 说明
.deer-flow/agents/roundtable-recommender/config.yaml 推荐智能体元数据
.deer-flow/agents/roundtable-recommender/SOUL.md 推荐智能体系统提示
app/gateway/routers/recommend.py /api/recommend/stream 接口

后端(修改)

文件 说明
app/gateway/app.py 注册 recommend.router

前端(新建)

文件 说明
src/roundtable-planning/api/recommend.ts 前端 SSE 客户端
src/roundtable-planning/components/RecommendAgentsDialog.tsx 推荐弹窗组件

前端(修改)

文件 说明
src/roundtable-planning/pages/RoundtablePlanningPage.tsx 动态 Agent 角色 + 弹窗接入

下次开始前检查项

  1. 后端是否已重启 — recommend.router 注册需重启 FastAPI(make dev 或 ./scripts/restart-all.sh)
  2. roundtable-recommender 智能体已注册 — 访问 GET /api/agents 确认列表里有 roundtable-recommender
  3. typecheck 干净 — pnpm typecheck 2>&1 | grep roundtable-planning 无输出
  4. 弹窗流程端到端测试:
    • Step 1 完成意图澄清 → intentReady 不为 null
    • 点击"进入多智能体研讨"→ 弹窗出现,开始加载智能体列表
    • 弹窗展示推荐结果(流式文字 + 卡片列表)
    • 调整勾选 → 确认 → Step 2 用所选智能体初始化

潜在后续工作(未列入本次范围)

  • 弹窗内显示每个候选智能体的完整 description(目前从 /api/agents 取,已有字段)
  • 推荐失败时的 Toast 提示(目前静默兜底)
  • 推荐理由(reason)在 Step 2 角色面板中展示
  • HighFidelityReport 的 avatarType 字段按新选角色动态渲染