9.5 KiB
9.5 KiB
智能体推荐弹窗功能 — 开发进度
分支:
dhk-1
最后更新: 2026-05-25
状态: 全部完成 ✅
功能概述
在圆桌规划流程的 Step 1(任务意图理解)和 Step 2(多智能体圆桌研讨)之间,插入一个推荐弹窗:
- 用户完成任务澄清后,点击"进入多智能体研讨"→ 弹出推荐弹窗
- 弹窗拉取
GET /api/agents候选池,调用roundtable-recommender智能体流式推荐 2-6 个最合适的智能体 - 推荐结果默认勾选,用户可取消勾选 / 从候选池增选(总选 2-8 个)
- 确认后 Step 2 使用用户最终选定的智能体列表,不再硬编码 6 个圆桌席位
已完成的所有步骤
Step 1 — 后端:新建 roundtable-recommender 智能体 ✅
文件:
offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/config.yamloffline-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 角色 + 弹窗接入 |
下次开始前检查项
- 后端是否已重启 —
recommend.router注册需重启 FastAPI(make dev或./scripts/restart-all.sh) roundtable-recommender智能体已注册 — 访问GET /api/agents确认列表里有roundtable-recommender- typecheck 干净 —
pnpm typecheck 2>&1 | grep roundtable-planning无输出 - 弹窗流程端到端测试:
- Step 1 完成意图澄清 →
intentReady不为 null - 点击"进入多智能体研讨"→ 弹窗出现,开始加载智能体列表
- 弹窗展示推荐结果(流式文字 + 卡片列表)
- 调整勾选 → 确认 → Step 2 用所选智能体初始化
- Step 1 完成意图澄清 →
潜在后续工作(未列入本次范围)
- 弹窗内显示每个候选智能体的完整 description(目前从
/api/agents取,已有字段) - 推荐失败时的 Toast 提示(目前静默兜底)
- 推荐理由(reason)在 Step 2 角色面板中展示
HighFidelityReport的avatarType字段按新选角色动态渲染