代码提交
This commit is contained in:
1 parent
357f245aec
commit
9a70329db2
2 files changed
+423
-60
No files matched your search
@@ -0,0 +1,268 @@
|
||||
# 智能体推荐弹窗功能 — 开发进度
|
||||
|
||||
> 分支: `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 注册:**
|
||||
```python
|
||||
from app.gateway.routers import recommend
|
||||
app.include_router(recommend.router)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — 前端:`recommend.ts` API 客户端 ✅
|
||||
|
||||
**文件:** `src/roundtable-planning/api/recommend.ts`
|
||||
|
||||
**导出:**
|
||||
```typescript
|
||||
// 类型
|
||||
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:**
|
||||
```typescript
|
||||
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`(全部硬编码常量)
|
||||
|
||||
**新增模块级:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
|
||||
```
|
||||
|
||||
**新增 state:**
|
||||
```typescript
|
||||
const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);
|
||||
```
|
||||
|
||||
**新增 callback `handleConfirmAgents`:**
|
||||
```typescript
|
||||
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("成功确认!已切换至第二阶段圆桌研讨系统");
|
||||
}, []);
|
||||
```
|
||||
|
||||
**两个"进入多智能体研讨"按钮改为:**
|
||||
```typescript
|
||||
onClick={() => setIsRecommendDialogOpen(true)}
|
||||
```
|
||||
|
||||
**JSX 渲染(在 showToast 之前):**
|
||||
```tsx
|
||||
<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` 字段按新选角色动态渲染
|
||||
Reference in new issue
Block a user