feat: add strategy selection workbench
This commit is contained in:
1 parent
60e2c72514
commit
5fd2e8302c
34 files changed
+3188
-9
No files matched your search
+5
-3
@@ -100,9 +100,11 @@ offline-backend-20260512/backend/tests/*
|
||||
# Keep the model configuration regression coverage versioned even though
|
||||
# this deployment repository otherwise ignores newly added backend tests.
|
||||
!offline-backend-20260512/backend/tests/test_model_management.py
|
||||
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_forced_research_agent.py
|
||||
# Keep fixed-question exact-match and streaming regression coverage versioned.
|
||||
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_forced_research_agent.py
|
||||
# Keep strategy-selection built-in agent seed and safety-contract regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_strategy_selection_seed.py
|
||||
# Keep fixed-question exact-match and streaming regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_fixed_questions.py
|
||||
# Keep configurable es_query system-prompt routing regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_es_query_routing_prompt.py
|
||||
|
||||
@@ -83,6 +83,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
|
||||
- **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API.
|
||||
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
|
||||
- **策略优选**: route `/page/strategy/qa/strategy-selection`, sidebar 「问答管理 → 策略优选」. The standalone `src/strategy-selection/` workbench has two horizontal split stages: Step 1 keeps intent-understanding conversation cards on the left and an editable task draft plus angle selection on the right; Step 2 keeps multiplexed parallel strategy cards and automatic judging on the left while the right stays dedicated to the selected-strategy basket and final Markdown synthesis. It consumes the existing `POST /api/parallel-agents/run/stream` protocol, isolates output by stable agent id, supports expanded per-agent dialogs, user selection, Markdown editing/copy/download, and shared server-side DOCX export. A clearly marked neutral Taiwan-Strait risk-management demo is loaded by default for offline UI acceptance. Administrators can edit the SOUL and skills of the nine built-in functional agents through the page dialog; the backend seeds `strategy-selection-{intent,angle-*,judge,solution}` only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live in `src/strategy-selection/lib.test.ts`; backend seed tests live in `tests/test_strategy_selection_seed.py`.
|
||||
- **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes).
|
||||
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** left-nav is a separate overlay: `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`.
|
||||
- **白名单管理 (Login Whitelist, admin-only)**: gates **who may log in**, run as a **blocklist (黑名单模式)**. Two pieces: a global master switch (`system_settings.json → login_whitelist.enabled`, surfaced by **admin-only** `GET|PUT /api/system-settings/login-whitelist`) + a per-user **放行** flag (`users.approved` column, migration `20260625_02`). Users **self-register** as usual and **every new account defaults to `approved=True`** — so a user who has never logged into this system before is allowed straight through; the switch only blocks users an admin has explicitly **取消放行** (`approved=False`). (`create_user`/`User`/`UserRow` all default `approved=True`; admins are always approved; existing rows are backfilled `True`.) Login interception is the single helper `enforce_user_approved(user)` in `app/gateway/routers/auth.py`, called in all three login endpoints (`/login/{local,username,token}`) **after** the account is resolved/created and **before** the JWT is issued: switch off → everyone passes; `system_role=="admin"` → always passes (anti-lockout); else `approved=False` (admin-revoked) → 403 `{code:"PENDING_APPROVAL"}` (the frontend's `extractErrorMessage` surfaces the message and login throws before writing any auth state, so no half-login). Admin approves on `pages/LoginWhitelistPage.tsx` (route `/page/strategy/admin/login-whitelist`, entry「白名单管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both sidebar modes): a top master-switch + a list of all registered users (`GET /api/v1/auth/users`, now carrying `approved`) with a per-row 放行 Switch (`PUT /api/v1/auth/users/{id}/approval`, admin-only; revoking only blocks the **next** login — `token_version` is left untouched, existing sessions are not force-killed). Client/hooks: switch in `strategy-components/api/login-whitelist.ts`, user list + approval in `core/auth/{api,hooks}.ts` (`useSetUserApproval`). Backend tests: `tests/test_login_approval.py`. **账号级「登录口令」(per-account password — 临时开通)**: the same page also lets an admin assign one specific account an individual **plaintext** password (`users.login_password` column, migration `20260625_04`, nullable; auto-added on startup by `_ensure_orm_columns_sync`). Once set, **that account's** username login (`POST /api/v1/auth/login/username`) **requires** `?password=` to exactly match it and that personal password **takes precedence over** the global shared-password gate (`username_login.require_password`); accounts without one keep today's shared-gate / passwordless behavior. The user then logs in via the **existing** frontend URL `#/login/<用户名>?password=xxxx` (LoginPage already forwards `?password=`; `<用户名>` = the account's email `@`-prefix). **Plaintext is intentional** — the password rides in the URL query anyway, so storing it readably lets the admin re-copy the full login link from the page; it is exposed only on the **admin-only** `GET /api/v1/auth/users` (`UserListItem.login_password`) and the new admin-only `PUT /api/v1/auth/users/{id}/login-password` (set non-empty / clear with null·empty; doesn't touch `token_version`). The whitelist page renders a per-user 「登录口令」 column with a set/修改 editor + a live 「复制登录链接」 button (`useSetUserLoginPassword` in `core/auth/hooks.ts`, `setUserLoginPassword` in `core/auth/api.ts`). Backend gate helper `_lookup_user_by_username` (lookup-only, no auto-register) in `routers/auth.py`; tests `tests/test_login_personal_password.py`.
|
||||
|
||||
@@ -68,6 +68,12 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
icon: Users,
|
||||
path: "/page/strategy/qa/multi-agent-roundtable",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection",
|
||||
label: "策略优选",
|
||||
icon: Sparkles,
|
||||
path: "/page/strategy/qa/strategy-selection",
|
||||
},
|
||||
// {
|
||||
// id: "position-roundtable",
|
||||
// label: "岗位会商",
|
||||
|
||||
@@ -78,6 +78,7 @@ import UsersPage from "./UsersPage";
|
||||
import LlmMetricsPage from "./LlmMetricsPage";
|
||||
import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
|
||||
import AdminUserActivityPage from "./AdminUserActivityPage";
|
||||
import StrategySelectionPage from "@/strategy-selection/StrategySelectionPage";
|
||||
import { ChatRuntime } from "./WorkspaceRoutes";
|
||||
|
||||
/**
|
||||
@@ -110,6 +111,7 @@ export default function StrategyRoutes() {
|
||||
<Route index element={<Navigate to="qa/general" replace />} />
|
||||
|
||||
<Route path="qa/general" element={<GeneralQA />} />
|
||||
<Route path="qa/strategy-selection" element={<StrategySelectionPage />} />
|
||||
<Route
|
||||
path="qa/expert-qa"
|
||||
element={
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Bot, Check, LoaderCircle, Save, Sparkles } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getAgent, updateAgent } from "@/core/agents/api";
|
||||
import type { Agent } from "@/core/agents/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { STRATEGY_AGENT_ROLES } from "./constants";
|
||||
|
||||
interface AgentConfigDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function parseSkills(value: string): string[] {
|
||||
return [...new Set(value.split(/[,,\n]/).map((item) => item.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
export function AgentConfigDialog({ open, onOpenChange }: AgentConfigDialogProps) {
|
||||
const [selectedId, setSelectedId] = useState<string>(STRATEGY_AGENT_ROLES[0].id);
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [soul, setSoul] = useState("");
|
||||
const [skillsText, setSkillsText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const selectedRole = useMemo(
|
||||
() => STRATEGY_AGENT_ROLES.find((role) => role.id === selectedId)!,
|
||||
[selectedId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !selectedId) return;
|
||||
let active = true;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setAgent(null);
|
||||
void getAgent(selectedId)
|
||||
.then((data) => {
|
||||
if (!active) return;
|
||||
setAgent(data);
|
||||
setSoul(data.soul ?? "");
|
||||
setSkillsText((data.skills ?? []).join("\n"));
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
if (!active) return;
|
||||
setError(
|
||||
reason instanceof Error
|
||||
? reason.message
|
||||
: "智能体配置加载失败,请确认后端已完成默认配置初始化。",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [open, selectedId]);
|
||||
|
||||
const save = async () => {
|
||||
if (!agent) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const updated = await updateAgent(agent.id, {
|
||||
soul: soul.trim(),
|
||||
skills: parseSkills(skillsText),
|
||||
});
|
||||
setAgent(updated);
|
||||
setSoul(updated.soul ?? "");
|
||||
setSkillsText((updated.skills ?? []).join("\n"));
|
||||
toast.success(`${updated.name} 配置已保存`);
|
||||
} catch (reason) {
|
||||
toast.error(reason instanceof Error ? reason.message : "保存失败");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="h-[min(780px,calc(100vh-48px))] max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 sm:max-w-[1080px]">
|
||||
<DialogHeader className="border-b px-6 py-5 dark:border-[#173d68]">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Bot className="size-5 text-primary" />
|
||||
策略智能体配置
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
每个流程角色对应一个默认智能体。管理员可修改 SOUL 和技能;保存后下一次运行即生效。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid min-h-0 grid-cols-[290px_minmax(0,1fr)]">
|
||||
<div className="min-h-0 overflow-y-auto border-r bg-muted/20 p-3 dark:border-[#173d68] dark:bg-[#031b38]">
|
||||
<div className="mb-2 px-2 text-xs font-semibold tracking-wide text-muted-foreground">
|
||||
流程角色 · {STRATEGY_AGENT_ROLES.length}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{STRATEGY_AGENT_ROLES.map((role) => {
|
||||
const selected = role.id === selectedId;
|
||||
return (
|
||||
<button
|
||||
key={role.id}
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => setSelectedId(role.id)}
|
||||
className={cn(
|
||||
"w-full rounded-lg border px-3 py-3 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||
selected
|
||||
? "border-primary/45 bg-primary/10 shadow-sm"
|
||||
: "border-transparent hover:border-border hover:bg-background dark:hover:border-[#28507c] dark:hover:bg-[#08274c]",
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">{role.name}</span>
|
||||
{selected && <Check className="size-4 text-primary" />}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
||||
{role.role}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 overflow-y-auto px-6 py-5">
|
||||
<div className="mb-5 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-semibold">{selectedRole.name}</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{selectedRole.role}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="font-mono text-[11px]">
|
||||
{selectedRole.id}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex h-56 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
正在读取智能体配置…
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||
<p className="font-medium">暂时无法加载该智能体</p>
|
||||
<p className="mt-1 leading-6 opacity-85">{error}</p>
|
||||
</div>
|
||||
) : agent ? (
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-2 gap-3 rounded-lg border bg-muted/20 p-3 text-sm dark:border-[#173d68] dark:bg-[#031b38]">
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">当前名称</span>
|
||||
<div className="mt-1 font-medium">{agent.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">运行模型</span>
|
||||
<div className="mt-1 font-medium">{agent.model || "使用系统默认模型"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||
<Sparkles className="size-4 text-primary" /> SOUL
|
||||
</span>
|
||||
<Textarea
|
||||
value={soul}
|
||||
disabled={saving}
|
||||
onChange={(event) => setSoul(event.target.value)}
|
||||
className="min-h-[300px] resize-y font-mono text-xs leading-6"
|
||||
placeholder="定义智能体的角色、分析方法、边界和输出要求…"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-medium">技能</span>
|
||||
<Textarea
|
||||
value={skillsText}
|
||||
disabled={saving}
|
||||
onChange={(event) => setSkillsText(event.target.value)}
|
||||
placeholder="多个技能可用逗号分隔"
|
||||
rows={3}
|
||||
className="resize-y text-sm"
|
||||
/>
|
||||
<p className="mt-1.5 text-xs text-muted-foreground">
|
||||
可用逗号、中文逗号或换行分隔;留空表示不绑定专用技能。
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t px-6 py-4 dark:border-[#173d68]">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button disabled={!agent || loading || saving} onClick={() => void save()}>
|
||||
{saving ? <LoaderCircle className="animate-spin" /> : <Save />}
|
||||
保存当前智能体
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,44 @@
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
import { parseSseChunk } from "./lib";
|
||||
import type { ParallelAgentEvent, ParallelAgentPayload } from "./types";
|
||||
|
||||
async function readError(response: Response): Promise<string> {
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| { detail?: string }
|
||||
| null;
|
||||
return body?.detail || `请求失败(HTTP ${response.status})`;
|
||||
}
|
||||
export async function streamParallelAgents(
|
||||
payload: ParallelAgentPayload,
|
||||
onEvent: (event: ParallelAgentEvent) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/parallel-agents/run/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error(await readError(response));
|
||||
if (!response.body) throw new Error("服务器未返回流式响应");
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
buffer += decoder.decode(value, { stream: !done });
|
||||
const parsed = parseSseChunk(buffer, done);
|
||||
buffer = parsed.rest;
|
||||
parsed.events.forEach(onEvent);
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import type {
|
||||
AgentOutputState,
|
||||
IntentTurn,
|
||||
StrategyAngle,
|
||||
StrategyEvaluation,
|
||||
StrategyTaskDraft,
|
||||
} from "./types";
|
||||
|
||||
export const STRATEGY_AGENT_ROLES = [
|
||||
{
|
||||
id: "strategy-selection-intent",
|
||||
name: "意图理解智能体",
|
||||
role: "将自然语言需求整理为可执行任务",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-angle-governance",
|
||||
name: "治理统筹智能体",
|
||||
role: "关注目标、机制、跨部门协同与执行闭环",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-angle-diplomacy",
|
||||
name: "外交沟通智能体",
|
||||
role: "关注沟通渠道、危机管控与多边协调",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-angle-economy",
|
||||
name: "经济韧性智能体",
|
||||
role: "关注供应链、产业、贸易与市场预期",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-angle-security",
|
||||
name: "安全风险智能体",
|
||||
role: "关注预警、误判防范与非升级性风险处置",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-angle-public-opinion",
|
||||
name: "社会民生智能体",
|
||||
role: "关注公众沟通、民生连续性与人道保障",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-angle-law-policy",
|
||||
name: "法律规则智能体",
|
||||
role: "关注适用规则、合规边界与证据链",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-judge",
|
||||
name: "策略评判智能体",
|
||||
role: "以统一量表比较策略的收益、风险与可执行性",
|
||||
},
|
||||
{
|
||||
id: "strategy-selection-solution",
|
||||
name: "方案整理智能体",
|
||||
role: "将入选策略去重、消冲突并整理为最终方案",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const STRATEGY_ANGLES: StrategyAngle[] = [
|
||||
{
|
||||
id: "governance",
|
||||
agentId: "strategy-selection-angle-governance",
|
||||
name: "治理与统筹",
|
||||
shortName: "治理",
|
||||
description: "目标拆解、组织协同、阶段推进和反馈闭环",
|
||||
accent: "#6d5dfc",
|
||||
},
|
||||
{
|
||||
id: "diplomacy",
|
||||
agentId: "strategy-selection-angle-diplomacy",
|
||||
name: "外交与沟通",
|
||||
shortName: "外交",
|
||||
description: "危机沟通、多边协调、信号一致性与降温安排",
|
||||
accent: "#0ea5e9",
|
||||
},
|
||||
{
|
||||
id: "economy",
|
||||
agentId: "strategy-selection-angle-economy",
|
||||
name: "经济与产业韧性",
|
||||
shortName: "经济",
|
||||
description: "供应链连续性、市场预期、贸易和关键产业保障",
|
||||
accent: "#10b981",
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
agentId: "strategy-selection-angle-security",
|
||||
name: "安全与风险管控",
|
||||
shortName: "安全",
|
||||
description: "态势预警、误判防范、事件分级和非升级性处置",
|
||||
accent: "#f97316",
|
||||
},
|
||||
{
|
||||
id: "public_opinion",
|
||||
agentId: "strategy-selection-angle-public-opinion",
|
||||
name: "社会民生与舆情",
|
||||
shortName: "民生",
|
||||
description: "公众沟通、基本服务、社会心理和人道保障",
|
||||
accent: "#ec4899",
|
||||
},
|
||||
{
|
||||
id: "law_policy",
|
||||
agentId: "strategy-selection-angle-law-policy",
|
||||
name: "法律与规则",
|
||||
shortName: "法律",
|
||||
description: "适用规则、合规审查、责任边界与证据留存",
|
||||
accent: "#a855f7",
|
||||
},
|
||||
];
|
||||
|
||||
export const EMPTY_TASK_DRAFT: StrategyTaskDraft = {
|
||||
name: "",
|
||||
detail: "",
|
||||
objective: "",
|
||||
prerequisites: "",
|
||||
constraints: "",
|
||||
deliverables: "",
|
||||
acceptanceCriteria: "",
|
||||
horizon: "",
|
||||
selectedAngleIds: STRATEGY_ANGLES.slice(0, 4).map((angle) => angle.id),
|
||||
};
|
||||
|
||||
export const INITIAL_INTENT_TURNS: IntentTurn[] = [
|
||||
{
|
||||
id: "welcome",
|
||||
role: "assistant",
|
||||
content:
|
||||
"请描述你想解决的问题、希望达成的目标以及已知约束。我会通过对话补齐关键信息,并把需求实时整理到右侧任务草稿。",
|
||||
},
|
||||
];
|
||||
|
||||
export const TAIWAN_STRAIT_DEMO_TASK: StrategyTaskDraft = {
|
||||
name: "台海局势风险管控与区域韧性提升策略",
|
||||
detail:
|
||||
"面向台海局势高不确定性场景,形成一套不预设立场、以避免误判和升级为核心的综合政策方案。方案聚焦危机沟通、区域经济与民生连续性、规则遵循及信息透明,不包含战术行动、目标选择或武力运用指导。",
|
||||
objective:
|
||||
"未来12—24个月内,降低突发事件转化为系统性危机的概率,保护平民福祉与关键经济活动,维持可验证、可持续的沟通和协调机制。",
|
||||
prerequisites:
|
||||
"建立跨部门联合研判机制;明确授权与对外沟通口径;掌握航运、能源、金融、通信和公共服务的基础风险数据;预留与各相关方沟通的技术渠道。",
|
||||
constraints:
|
||||
"遵守适用的国际法及人道原则;坚持克制、对等、可逆和非升级;不得输出军事战术、具体目标、武器使用或规避监管建议;关键事实须标注时点并交叉核验。",
|
||||
deliverables:
|
||||
"一份综合策略;分阶段行动清单;跨部门责任矩阵;风险预警指标;对外沟通要点;每月复盘和动态调整机制。",
|
||||
acceptanceCriteria:
|
||||
"措施均有责任主体、触发条件和退出条件;重要建议至少经两个角度交叉验证;高风险措施设置法律审查与升级门槛;方案可在72小时内启动首轮响应。",
|
||||
horizon: "12—24个月,按0—30天、1—6个月、6—24个月分三阶段推进",
|
||||
selectedAngleIds: STRATEGY_ANGLES.map((angle) => angle.id),
|
||||
};
|
||||
|
||||
export const TAIWAN_STRAIT_DEMO_TURNS: IntentTurn[] = [
|
||||
...INITIAL_INTENT_TURNS,
|
||||
{
|
||||
id: "demo-user",
|
||||
role: "user",
|
||||
isDemo: true,
|
||||
content:
|
||||
"请围绕台海局势做一个综合风险管控策略,重点是减少误判升级,同时兼顾经济、民生和规则约束,时间范围看未来一到两年。",
|
||||
},
|
||||
{
|
||||
id: "demo-assistant",
|
||||
role: "assistant",
|
||||
isDemo: true,
|
||||
content:
|
||||
"已将需求整理为“风险管控与区域韧性提升”任务:以降级避险为目标,采用治理、外交、经济、安全、民生和法律六个角度并行论证;明确排除战术性军事建议,并补充了阶段周期、验收标准和启动条件。右侧字段仍可直接修改。",
|
||||
},
|
||||
];
|
||||
|
||||
export const TAIWAN_STRAIT_DEMO_OUTPUTS: Record<string, AgentOutputState> = {
|
||||
"strategy-selection-angle-governance": {
|
||||
status: "done",
|
||||
isDemo: true,
|
||||
steps: ["拆解目标", "形成协同机制", "设置复盘闭环"],
|
||||
content: `### 治理统筹策略:建立“联合研判—分级响应—复盘校正”闭环
|
||||
|
||||
1. 设立常态化跨部门风险研判单元,统一风险词典、数据口径和事件分级。
|
||||
2. 为不同风险等级预先定义责任主体、会商时限、可逆措施和退出条件,避免临时决策放大误判。
|
||||
3. 建立月度桌面推演与季度复盘,重点校验交通、能源、金融、通信和公共服务之间的级联风险。
|
||||
4. 重大调整实行“政策效果—升级风险—民生影响—法律合规”四项并联审查。
|
||||
|
||||
**优先动作:** 30天内完成责任矩阵和跨部门值守清单;90天内完成首次全链路压力测试。`,
|
||||
},
|
||||
"strategy-selection-angle-diplomacy": {
|
||||
status: "done",
|
||||
isDemo: true,
|
||||
steps: ["识别沟通对象", "设计危机渠道", "校准公开信号"],
|
||||
content: `### 外交沟通策略:保持多层次、可验证的危机沟通
|
||||
|
||||
1. 维护工作层、专业部门和高层三类沟通渠道,约定紧急事件的确认窗口与回执机制。
|
||||
2. 对外表述采用“事实、原则、下一步安排”三段式,减少模糊威慑和相互误读。
|
||||
3. 支持以航行安全、灾害救援、公共卫生等低政治敏感议题开展技术性接触,积累最低限度信任。
|
||||
4. 引入可信第三方作为信息转递与事实核验的备份通道,但不替代直接沟通。
|
||||
|
||||
**触发条件:** 当出现信息不一致、渠道中断或舆情快速放大时,立即启动双渠道交叉确认。`,
|
||||
},
|
||||
"strategy-selection-angle-economy": {
|
||||
status: "done",
|
||||
isDemo: true,
|
||||
steps: ["识别关键链路", "评估替代能力", "配置韧性措施"],
|
||||
content: `### 经济韧性策略:先保连续性,再做结构性分散
|
||||
|
||||
1. 对航运、半导体、能源、金融结算和通信链路建立关键节点清单与中断情景。
|
||||
2. 以库存天数、替代供应商覆盖率、运输改道时间和流动性缺口作为月度韧性指标。
|
||||
3. 对短期冲击优先使用信息协调、应急库存和融资支持,避免仓促、不可逆的全面脱钩措施。
|
||||
4. 中长期推动来源与路线多元化,同时保留正常经贸交往的政策空间。
|
||||
|
||||
**优先动作:** 对关键民生物资开展30/60/90天压力测试,并为中小企业准备定向流动性工具。`,
|
||||
},
|
||||
"strategy-selection-angle-security": {
|
||||
status: "done",
|
||||
isDemo: true,
|
||||
steps: ["构建预警指标", "划分风险等级", "设计降级动作"],
|
||||
content: `### 安全风险策略:以预警和降级机制减少偶发事件外溢
|
||||
|
||||
1. 建立覆盖异常活动、通信中断、错误信息扩散、交通受阻和市场波动的综合预警看板。
|
||||
2. 将风险划分为关注、紧张、严重三级,每级只配置克制、可逆、可解释的非升级性应对动作。
|
||||
3. 对来源未核实的信息实行“双源确认+时点标注”,避免单一信号直接触发重大政策响应。
|
||||
4. 所有高风险决策设置冷静期、法律复核和跨部门反方意见席位。
|
||||
|
||||
**止损线:** 一旦沟通渠道失效或民生影响超过阈值,优先恢复沟通与公共服务,暂停可能叠加风险的新措施。`,
|
||||
},
|
||||
"strategy-selection-angle-public-opinion": {
|
||||
status: "done",
|
||||
isDemo: true,
|
||||
steps: ["分析公众需求", "保障基本服务", "设计信息发布"],
|
||||
content: `### 社会民生策略:稳定预期并保护基本服务
|
||||
|
||||
1. 建立权威、固定频率的信息发布窗口,明确已知、未知和正在核验的事项。
|
||||
2. 为交通、医疗、能源、通信和教育准备连续性方案,优先保障脆弱群体。
|
||||
3. 监测谣言与恐慌性行为时,采用事实澄清和服务指引,不扩大未经证实的信息。
|
||||
4. 设置独立反馈渠道,收集企业、社区和民众的实际困难并纳入每日研判。
|
||||
|
||||
**评价指标:** 基本服务可用率、权威信息触达率、谣言纠正时长和重点群体保障覆盖率。`,
|
||||
},
|
||||
"strategy-selection-angle-law-policy": {
|
||||
status: "done",
|
||||
isDemo: true,
|
||||
steps: ["识别规则边界", "建立审查清单", "设计留痕机制"],
|
||||
content: `### 法律规则策略:把合规审查嵌入每个决策节点
|
||||
|
||||
1. 为数据共享、经济措施、应急管理和对外沟通分别建立适用规则清单。
|
||||
2. 重大措施记录事实依据、授权来源、比例性判断、影响评估和退出条件。
|
||||
3. 涉及平民权益、基本服务和跨境活动的措施优先进行人道影响评估。
|
||||
4. 对时效性强的事项采用“快速初审—事后完整复核”,但不得跳过最低合规门槛。
|
||||
|
||||
**优先动作:** 建立统一法律审查模板和决策证据包,保证后续可解释、可追溯。`,
|
||||
},
|
||||
};
|
||||
|
||||
export const TAIWAN_STRAIT_DEMO_EVALUATIONS: StrategyEvaluation[] = [
|
||||
{
|
||||
agentId: "strategy-selection-angle-governance",
|
||||
score: 92,
|
||||
conclusion: "优先推荐",
|
||||
comment: "跨部门闭环清晰,是其他策略落地的共同底座。",
|
||||
strengths: ["责任明确", "阶段完整", "便于复盘"],
|
||||
risks: ["需要稳定的牵头机制"],
|
||||
},
|
||||
{
|
||||
agentId: "strategy-selection-angle-diplomacy",
|
||||
score: 90,
|
||||
conclusion: "优先推荐",
|
||||
comment: "直接作用于误判风险,且措施克制、可逆。",
|
||||
strengths: ["降级效果直接", "渠道有备份"],
|
||||
risks: ["依赖相关方持续参与"],
|
||||
},
|
||||
{
|
||||
agentId: "strategy-selection-angle-economy",
|
||||
score: 86,
|
||||
conclusion: "建议采用",
|
||||
comment: "兼顾短期连续性与长期韧性,需控制过度防御成本。",
|
||||
strengths: ["指标可量化", "避免一刀切"],
|
||||
risks: ["跨行业数据获取成本较高"],
|
||||
},
|
||||
{
|
||||
agentId: "strategy-selection-angle-security",
|
||||
score: 91,
|
||||
conclusion: "优先推荐",
|
||||
comment: "预警、确认和冷静期设计可显著降低错误触发。",
|
||||
strengths: ["止损线清楚", "强调双源核验"],
|
||||
risks: ["阈值需持续校准"],
|
||||
},
|
||||
{
|
||||
agentId: "strategy-selection-angle-public-opinion",
|
||||
score: 84,
|
||||
conclusion: "建议采用",
|
||||
comment: "增强社会承压能力,建议与经济连续性方案合并实施。",
|
||||
strengths: ["关注脆弱群体", "沟通指标明确"],
|
||||
risks: ["需避免信息发布碎片化"],
|
||||
},
|
||||
{
|
||||
agentId: "strategy-selection-angle-law-policy",
|
||||
score: 88,
|
||||
conclusion: "建议采用",
|
||||
comment: "为高压情境下的决策提供必要边界和可追溯性。",
|
||||
strengths: ["审查嵌入流程", "留痕充分"],
|
||||
risks: ["快速响应与完整审查需平衡"],
|
||||
},
|
||||
];
|
||||
|
||||
export const TAIWAN_STRAIT_DEMO_SYNTHESIS = `# 台海局势风险管控与区域韧性提升综合方案
|
||||
|
||||
> **示例说明:** 本方案为产品功能演示数据,采用中立、降级避险和民生优先视角,不构成现实决策建议;正式使用前应依据最新可靠信息进行专业复核。
|
||||
|
||||
## 一、总体目标
|
||||
|
||||
在未来12—24个月内,以“减少误判、阻断升级、保护民生、维持连续性”为主线,建立可监测、可启动、可退出、可复盘的风险管控体系。任何措施均应符合适用规则,坚持克制、对等、可逆和可解释。
|
||||
|
||||
## 二、四条主策略
|
||||
|
||||
### 1. 联合研判与分级响应
|
||||
|
||||
- 建立跨部门常态研判单元,统一风险词典、数据口径和三级事件标准。
|
||||
- 每个级别明确牵头主体、会商时限、启动阈值、允许动作和退出条件。
|
||||
- 重大建议同时接受政策效果、升级风险、民生影响和法律合规审查。
|
||||
|
||||
### 2. 多层危机沟通与事实核验
|
||||
|
||||
- 维护工作层、专业部门和高层沟通渠道,并配置可信第三方备份。
|
||||
- 紧急事项执行“双渠道回执、双来源确认、统一时点标注”。
|
||||
- 公开沟通采用“事实—原则—下一步安排”结构,降低信号歧义。
|
||||
|
||||
### 3. 经济与公共服务连续性
|
||||
|
||||
- 围绕航运、能源、金融、通信、医疗和关键供应链建立中断情景清单。
|
||||
- 用库存天数、替代覆盖率、改道时长、服务可用率等指标开展月度压力测试。
|
||||
- 短期优先使用应急库存、信息协调和定向融资;中长期有序提升来源与路线多元化。
|
||||
|
||||
### 4. 规则嵌入与社会韧性
|
||||
|
||||
- 将授权来源、比例性、人道影响、退出条件和证据留存纳入统一审查模板。
|
||||
- 通过固定频率的权威发布说明已知、未知和核验进度,提供清晰服务指引。
|
||||
- 优先保障脆弱群体,并建立企业、社区与公众反馈的快速回流机制。
|
||||
|
||||
## 三、阶段安排
|
||||
|
||||
| 阶段 | 重点动作 | 主要产出 |
|
||||
| --- | --- | --- |
|
||||
| 0—30天 | 完成责任矩阵、沟通清单、关键链路盘点与法律审查模板 | 一图一表一清单、首版预警看板 |
|
||||
| 1—6个月 | 开展压力测试和桌面推演,打通双渠道确认与公众反馈 | 演练报告、阈值校准、改进台账 |
|
||||
| 6—24个月 | 推进结构性韧性项目并季度复盘,动态退出低效措施 | 韧性提升项目、季度评估报告 |
|
||||
|
||||
## 四、核心预警指标
|
||||
|
||||
1. 沟通渠道可用率与紧急事项回执时长。
|
||||
2. 未核实信息传播速度及权威澄清时长。
|
||||
3. 关键运输、能源、金融、通信和公共服务可用率。
|
||||
4. 市场异常波动、关键物资库存天数与替代供应覆盖率。
|
||||
5. 脆弱群体保障覆盖率和公众反馈闭环时长。
|
||||
|
||||
## 五、止损与调整机制
|
||||
|
||||
当直接沟通失效、基本服务明显下降、信息可信度不足或风险指标跨级跃升时,优先恢复沟通、事实核验和民生服务;暂停新增高风险措施。每月复盘策略组合,每季度由独立评估组检查目标偏移、措施叠加和退出条件执行情况。
|
||||
`;
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Run from frontend-web:
|
||||
* npx esbuild src/strategy-selection/lib.test.ts --bundle --format=esm --platform=node --outfile=.tmp-tests/strategy-selection.test.mjs
|
||||
* node --test .tmp-tests/strategy-selection.test.mjs
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildJudgeBrief,
|
||||
createFallbackEvaluations,
|
||||
extractFencedJson,
|
||||
parseEvaluationsFromText,
|
||||
parseSseChunk,
|
||||
parseTaskDraftFromText,
|
||||
stripStructuredBlocks,
|
||||
} from "./lib";
|
||||
import { EMPTY_TASK_DRAFT } from "./constants";
|
||||
import type { AgentOutputState } from "./types";
|
||||
|
||||
test("parseSseChunk preserves incomplete frames and parses subsequent frames", () => {
|
||||
const first = parseSseChunk(
|
||||
'data: {"type":"agent_start","agent_id":"a"}\n\ndata: {"type":"del',
|
||||
);
|
||||
assert.equal(first.events.length, 1);
|
||||
assert.equal(first.events[0].type, "agent_start");
|
||||
const second = parseSseChunk(`${first.rest}ta","agent_id":"a","text":"你好"}\n\n`);
|
||||
assert.deepEqual(second.events, [
|
||||
{ type: "delta", agent_id: "a", text: "你好" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("stripStructuredBlocks keeps prose and hides machine-readable payloads", () => {
|
||||
assert.equal(
|
||||
stripStructuredBlocks('说明文字\n[INTENT_READY]\n```strategy-task-json\n{"name":"x"}\n```'),
|
||||
"说明文字",
|
||||
);
|
||||
});
|
||||
|
||||
test("extractFencedJson and parseTaskDraftFromText read the structured task block", () => {
|
||||
const text = [
|
||||
"已整理任务。",
|
||||
"```strategy-task-json",
|
||||
JSON.stringify({
|
||||
name: "测试任务",
|
||||
detail: "详情",
|
||||
recommended_angles: ["governance", "unknown", "law_policy"],
|
||||
}),
|
||||
"```",
|
||||
].join("\n");
|
||||
assert.deepEqual(extractFencedJson(text, "strategy-task-json"), {
|
||||
name: "测试任务",
|
||||
detail: "详情",
|
||||
recommended_angles: ["governance", "unknown", "law_policy"],
|
||||
});
|
||||
const task = parseTaskDraftFromText(text, EMPTY_TASK_DRAFT);
|
||||
assert.equal(task?.name, "测试任务");
|
||||
assert.deepEqual(task?.selectedAngleIds, ["governance", "law_policy"]);
|
||||
});
|
||||
|
||||
test("task parser accepts backend SOUL field aliases", () => {
|
||||
const text = `\`\`\`strategy-task-json\n${JSON.stringify({
|
||||
task_name: "别名任务",
|
||||
task_detail: "别名详情",
|
||||
expected_outputs: "综合方案",
|
||||
recommended_angles: ["public_opinion", "strategy-selection-angle-law-policy"],
|
||||
})}\n\`\`\``;
|
||||
const task = parseTaskDraftFromText(text, EMPTY_TASK_DRAFT);
|
||||
assert.equal(task?.name, "别名任务");
|
||||
assert.equal(task?.detail, "别名详情");
|
||||
assert.equal(task?.deliverables, "综合方案");
|
||||
assert.deepEqual(task?.selectedAngleIds, ["public_opinion", "law_policy"]);
|
||||
});
|
||||
|
||||
test("evaluation parser normalizes scores and fills missing candidates", () => {
|
||||
const outputs: Record<string, AgentOutputState> = {
|
||||
"strategy-selection-angle-governance": {
|
||||
status: "done",
|
||||
content: "A",
|
||||
steps: [],
|
||||
},
|
||||
"strategy-selection-angle-law-policy": { status: "done", content: "B", steps: [] },
|
||||
"strategy-selection-angle-security": {
|
||||
status: "error",
|
||||
content: "PARTIAL_FAILED_OUTPUT",
|
||||
steps: [],
|
||||
error: "failed",
|
||||
},
|
||||
};
|
||||
const text = `\`\`\`strategy-evaluation-json\n${JSON.stringify({
|
||||
evaluations: [
|
||||
{
|
||||
agentId: "strategy-selection-angle-governance",
|
||||
score: 101,
|
||||
conclusion: "优先推荐",
|
||||
comment: "清晰",
|
||||
},
|
||||
],
|
||||
})}\n\`\`\``;
|
||||
const evaluations = parseEvaluationsFromText(text, outputs);
|
||||
assert.equal(evaluations.length, 2);
|
||||
assert.equal(evaluations[0].score, 100);
|
||||
assert.ok(
|
||||
evaluations.some(
|
||||
(item) => item.agentId === "strategy-selection-angle-law-policy",
|
||||
),
|
||||
);
|
||||
assert.equal(createFallbackEvaluations(outputs).length, 2);
|
||||
assert.equal(buildJudgeBrief(EMPTY_TASK_DRAFT, outputs).includes("PARTIAL_FAILED_OUTPUT"), false);
|
||||
});
|
||||
@@ -0,0 +1,266 @@
|
||||
import { STRATEGY_ANGLES } from "./constants";
|
||||
import type {
|
||||
AgentOutputState,
|
||||
IntentTurn,
|
||||
ParallelAgentEvent,
|
||||
StrategyEvaluation,
|
||||
StrategyTaskDraft,
|
||||
} from "./types";
|
||||
|
||||
const clampScore = (value: unknown): number => {
|
||||
const score = Number(value);
|
||||
if (!Number.isFinite(score)) return 80;
|
||||
return Math.min(100, Math.max(0, Math.round(score)));
|
||||
};
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function stringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map(stringValue).filter(Boolean);
|
||||
}
|
||||
|
||||
export function extractFencedJson<T>(text: string, marker?: string): T | null {
|
||||
const escaped = marker?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const patterns = [
|
||||
escaped ? new RegExp("```" + escaped + "\\s*([\\s\\S]*?)```", "i") : null,
|
||||
/```(?:json)?\s*([\s\S]*?)```/i,
|
||||
].filter(Boolean) as RegExp[];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = pattern.exec(text);
|
||||
if (!match?.[1]) continue;
|
||||
try {
|
||||
return JSON.parse(match[1].trim()) as T;
|
||||
} catch {
|
||||
// The model may have emitted another fenced block first; fall through to
|
||||
// the balanced-object fallback below.
|
||||
}
|
||||
}
|
||||
|
||||
const firstObject = text.indexOf("{");
|
||||
const lastObject = text.lastIndexOf("}");
|
||||
if (firstObject >= 0 && lastObject > firstObject) {
|
||||
try {
|
||||
return JSON.parse(text.slice(firstObject, lastObject + 1)) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stripStructuredBlocks(text: string): string {
|
||||
return text
|
||||
.replace(/```(?:strategy-task-json|strategy-evaluation-json)\s*[\s\S]*?```/gi, "")
|
||||
.replace(/\[(?:INTENT_READY|JUDGEMENT_READY)\]/gi, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function parseTaskDraftFromText(
|
||||
text: string,
|
||||
fallback: StrategyTaskDraft,
|
||||
): StrategyTaskDraft | null {
|
||||
const parsed = extractFencedJson<Record<string, unknown>>(text, "strategy-task-json");
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
const rawAngles = parsed.selectedAngleIds ?? parsed.recommended_angles;
|
||||
const angleIds = Array.isArray(rawAngles)
|
||||
? rawAngles
|
||||
.map(stringValue)
|
||||
.map((value) => {
|
||||
const normalized = value.toLowerCase().replaceAll("-", "_");
|
||||
return STRATEGY_ANGLES.find(
|
||||
(angle) =>
|
||||
angle.id === normalized ||
|
||||
angle.agentId === value ||
|
||||
angle.name === value ||
|
||||
angle.shortName === value,
|
||||
)?.id;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
: fallback.selectedAngleIds;
|
||||
|
||||
return {
|
||||
name: stringValue(parsed.name ?? parsed.task_name) || fallback.name,
|
||||
detail: stringValue(parsed.detail ?? parsed.task_detail) || fallback.detail,
|
||||
objective: stringValue(parsed.objective) || fallback.objective,
|
||||
prerequisites:
|
||||
stringValue(parsed.prerequisites) || fallback.prerequisites,
|
||||
constraints: stringValue(parsed.constraints) || fallback.constraints,
|
||||
deliverables:
|
||||
stringValue(parsed.deliverables ?? parsed.expected_outputs) || fallback.deliverables,
|
||||
acceptanceCriteria:
|
||||
stringValue(parsed.acceptanceCriteria) || fallback.acceptanceCriteria,
|
||||
horizon: stringValue(parsed.horizon) || fallback.horizon,
|
||||
selectedAngleIds: angleIds.length ? angleIds : fallback.selectedAngleIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFallbackEvaluations(
|
||||
outputs: Record<string, AgentOutputState>,
|
||||
): StrategyEvaluation[] {
|
||||
return STRATEGY_ANGLES.filter(
|
||||
(angle) =>
|
||||
outputs[angle.agentId]?.status === "done" &&
|
||||
outputs[angle.agentId]?.content.trim(),
|
||||
).map((angle, index) => ({
|
||||
agentId: angle.agentId,
|
||||
score: Math.max(76, 88 - index),
|
||||
conclusion: index < 2 ? "优先推荐" : "建议采用",
|
||||
comment: `${angle.name}策略已形成完整交付,建议结合其他入选策略做冲突消解与阶段排序。`,
|
||||
strengths: ["角度明确", "具备行动结构"],
|
||||
risks: ["关键阈值仍需结合最新信息复核"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function parseEvaluationsFromText(
|
||||
text: string,
|
||||
outputs: Record<string, AgentOutputState>,
|
||||
): StrategyEvaluation[] {
|
||||
const parsed = extractFencedJson<
|
||||
{ evaluations?: Record<string, unknown>[] } | Record<string, unknown>[]
|
||||
>(text, "strategy-evaluation-json");
|
||||
const rows = Array.isArray(parsed) ? parsed : parsed?.evaluations;
|
||||
if (!Array.isArray(rows)) return createFallbackEvaluations(outputs);
|
||||
|
||||
const knownIds = new Set(STRATEGY_ANGLES.map((angle) => angle.agentId));
|
||||
const evaluations = rows
|
||||
.map((row): StrategyEvaluation | null => {
|
||||
const agentId = stringValue(row.agentId ?? row.agent_id);
|
||||
if (
|
||||
!knownIds.has(agentId) ||
|
||||
outputs[agentId]?.status !== "done" ||
|
||||
!outputs[agentId]?.content.trim()
|
||||
) return null;
|
||||
const rawConclusion = stringValue(row.conclusion);
|
||||
const conclusion: StrategyEvaluation["conclusion"] =
|
||||
rawConclusion === "优先推荐" ||
|
||||
rawConclusion === "建议采用" ||
|
||||
rawConclusion === "保留备选"
|
||||
? rawConclusion
|
||||
: clampScore(row.score) >= 88
|
||||
? "优先推荐"
|
||||
: clampScore(row.score) >= 78
|
||||
? "建议采用"
|
||||
: "保留备选";
|
||||
return {
|
||||
agentId,
|
||||
score: clampScore(row.score),
|
||||
conclusion,
|
||||
comment: stringValue(row.comment) || "请结合完整评判正文审阅。",
|
||||
strengths: stringList(row.strengths),
|
||||
risks: stringList(row.risks),
|
||||
};
|
||||
})
|
||||
.filter((item): item is StrategyEvaluation => item !== null);
|
||||
|
||||
if (!evaluations.length) return createFallbackEvaluations(outputs);
|
||||
|
||||
const byAgent = new Map(evaluations.map((item) => [item.agentId, item]));
|
||||
for (const fallback of createFallbackEvaluations(outputs)) {
|
||||
if (!byAgent.has(fallback.agentId)) byAgent.set(fallback.agentId, fallback);
|
||||
}
|
||||
return [...byAgent.values()].sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
export function buildIntentBrief(
|
||||
turns: IntentTurn[],
|
||||
currentDraft: StrategyTaskDraft,
|
||||
): string {
|
||||
const conversation = turns
|
||||
.filter((turn) => turn.content.trim())
|
||||
.map((turn) => `${turn.role === "user" ? "用户" : "意图助手"}:${turn.content}`)
|
||||
.join("\n\n");
|
||||
return `你负责意图理解与任务定义。基于以下对话,先用简洁自然语言回应用户,再整理一份完整任务草稿。信息不全时做保守假设并明确标注,不要虚构实时事实。\n\n当前草稿(可据新信息修订):\n${JSON.stringify(currentDraft, null, 2)}\n\n对话:\n${conversation}\n\n回复末尾必须追加一个可机器读取的代码块,字段名不得改变:\n\`\`\`strategy-task-json\n{"name":"任务名称","detail":"任务详情","objective":"目标","prerequisites":"前置条件","constraints":"约束与边界","deliverables":"交付物","acceptanceCriteria":"验收标准","horizon":"时间范围","selectedAngleIds":["governance","diplomacy","economy","security","public_opinion","law_policy"]}\n\`\`\`\n只选择与任务有关的角度。`;
|
||||
}
|
||||
|
||||
export function buildStrategyBrief(
|
||||
task: StrategyTaskDraft,
|
||||
selectedAngles = STRATEGY_ANGLES.filter((angle) =>
|
||||
task.selectedAngleIds.includes(angle.id),
|
||||
),
|
||||
): string {
|
||||
const angles = selectedAngles
|
||||
.map((angle) => `- ${angle.name}:${angle.description}`)
|
||||
.join("\n");
|
||||
return `你是其中一个专业角度的策略生成智能体,只从自己的职责角度输出一套可独立审阅的策略。\n\n任务详情:\n${JSON.stringify(task, null, 2)}\n\n本轮并行角度:\n${angles}\n\n请给出:核心判断、3—6项策略动作、实施阶段、触发/退出条件、指标、主要风险与协同依赖。避免空泛口号;涉及动态事实时说明需复核。对于地缘政治议题,坚持中立、降级避险、民生优先,不输出军事战术、目标选择、武器运用或规避监管建议。`;
|
||||
}
|
||||
|
||||
export function buildJudgeBrief(
|
||||
task: StrategyTaskDraft,
|
||||
outputs: Record<string, AgentOutputState>,
|
||||
): string {
|
||||
const strategies = STRATEGY_ANGLES.filter(
|
||||
(angle) =>
|
||||
outputs[angle.agentId]?.status === "done" &&
|
||||
outputs[angle.agentId]?.content.trim(),
|
||||
)
|
||||
.map(
|
||||
(angle) =>
|
||||
`## ${angle.name}\nagentId: ${angle.agentId}\n${outputs[angle.agentId].content}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
return `你是独立策略评判智能体。请依据任务目标,对候选策略的目标贡献、可执行性、风险可控性、协同性和合规性进行比较。先输出总体评判与冲突提醒,再在末尾输出结构化量表。\n\n任务:\n${JSON.stringify(task, null, 2)}\n\n候选策略:\n${strategies}\n\n末尾必须追加:\n\`\`\`strategy-evaluation-json\n{"evaluations":[{"agentId":"候选agentId","score":0,"conclusion":"优先推荐|建议采用|保留备选","comment":"一句评价","strengths":["优势"],"risks":["风险"]}]}\n\`\`\`\n必须覆盖每个候选策略,score为0—100整数。`;
|
||||
}
|
||||
|
||||
export function buildSynthesisBrief(
|
||||
task: StrategyTaskDraft,
|
||||
selectedAgentIds: string[],
|
||||
outputs: Record<string, AgentOutputState>,
|
||||
evaluations: StrategyEvaluation[],
|
||||
): string {
|
||||
const evaluationMap = new Map(evaluations.map((item) => [item.agentId, item]));
|
||||
const selections = selectedAgentIds
|
||||
.map((agentId) => {
|
||||
const angle = STRATEGY_ANGLES.find((item) => item.agentId === agentId);
|
||||
const evaluation = evaluationMap.get(agentId);
|
||||
return `## ${angle?.name ?? agentId}\n评判:${evaluation ? `${evaluation.score}分,${evaluation.comment}` : "用户手动选入"}\n${outputs[agentId]?.content ?? ""}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
return `你是方案整理智能体。请将用户选中的策略消除重复、处理冲突、明确优先级和依赖关系,整理为一份可直接下载的完整 Markdown 方案。\n\n任务:\n${JSON.stringify(task, null, 2)}\n\n入选策略:\n${selections}\n\n只输出最终 Markdown 正文。至少包含:执行摘要、目标与原则、综合策略、阶段路线图、责任与协同、指标与预警、风险及止损机制、待核验信息。不要输出代码围栏。对于地缘政治议题坚持中立、降级避险、民生优先,禁止军事战术和伤害性操作建议。`;
|
||||
}
|
||||
|
||||
export function parseSseChunk(
|
||||
buffer: string,
|
||||
flush = false,
|
||||
): { events: ParallelAgentEvent[]; rest: string } {
|
||||
const normalized = buffer.replace(/\r\n/g, "\n");
|
||||
const parts = normalized.split("\n\n");
|
||||
const rest = flush ? "" : (parts.pop() ?? "");
|
||||
const complete = flush ? parts.concat(parts.length ? [] : [normalized]) : parts;
|
||||
const events: ParallelAgentEvent[] = [];
|
||||
for (const frame of complete) {
|
||||
const payload = frame
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice(5).trimStart())
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (!payload || payload === "[DONE]") continue;
|
||||
try {
|
||||
const event = JSON.parse(payload) as ParallelAgentEvent;
|
||||
if (event && typeof event.type === "string") events.push(event);
|
||||
} catch {
|
||||
// Ignore one malformed frame instead of discarding the rest of a stream.
|
||||
}
|
||||
}
|
||||
return { events, rest };
|
||||
}
|
||||
|
||||
export function downloadMarkdown(markdown: string, fileName: string): void {
|
||||
const cleanName =
|
||||
fileName.replace(/\.(md|markdown)$/i, "").replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").trim() ||
|
||||
"strategy-plan";
|
||||
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `${cleanName}.md`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export type StrategyStage = "intent" | "workspace";
|
||||
|
||||
export type AgentRunStatus =
|
||||
| "idle"
|
||||
| "queued"
|
||||
| "running"
|
||||
| "done"
|
||||
| "error";
|
||||
|
||||
export interface StrategyAngle {
|
||||
id: string;
|
||||
agentId: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
description: string;
|
||||
accent: string;
|
||||
}
|
||||
|
||||
export interface StrategyTaskDraft {
|
||||
name: string;
|
||||
detail: string;
|
||||
objective: string;
|
||||
prerequisites: string;
|
||||
constraints: string;
|
||||
deliverables: string;
|
||||
acceptanceCriteria: string;
|
||||
horizon: string;
|
||||
selectedAngleIds: string[];
|
||||
}
|
||||
|
||||
export interface IntentTurn {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
streaming?: boolean;
|
||||
isDemo?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentOutputState {
|
||||
status: AgentRunStatus;
|
||||
content: string;
|
||||
steps: string[];
|
||||
error?: string;
|
||||
isDemo?: boolean;
|
||||
}
|
||||
|
||||
export interface StrategyEvaluation {
|
||||
agentId: string;
|
||||
score: number;
|
||||
conclusion: "优先推荐" | "建议采用" | "保留备选";
|
||||
comment: string;
|
||||
strengths: string[];
|
||||
risks: string[];
|
||||
}
|
||||
|
||||
export interface ParallelAgentPayload {
|
||||
task_name: string;
|
||||
brief: string;
|
||||
agent_ids: string[];
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export type ParallelAgentEvent =
|
||||
| { type: "agent_start"; agent_id: string }
|
||||
| { type: "delta"; agent_id: string; text: string }
|
||||
| {
|
||||
type: "step";
|
||||
agent_id: string;
|
||||
key?: string;
|
||||
label?: string;
|
||||
args?: Record<string, unknown>;
|
||||
}
|
||||
| { type: "agent_done"; agent_id: string; content: string }
|
||||
| { type: "agent_error"; agent_id: string; message: string }
|
||||
| {
|
||||
type: "agent_clarification";
|
||||
agent_id: string;
|
||||
content?: string;
|
||||
question?: string;
|
||||
}
|
||||
| { type: "all_done" };
|
||||
@@ -282,6 +282,8 @@ from deerflow.config import get_app_config
|
||||
|
||||
**Forced-research built-in agent.** `forced-research-responder` (「强制检索输出助手」) is seeded from `app/gateway/routers/_forced_research_seed_assets/` before `_sync_legacy_agents`. `ForcedResearchMiddleware` is mounted only for this stable id. On every visible user turn it requires a successful information-gathering tool result before the model may finish; `tool_choice=required` is backed by an `after_model -> model` retry for gateways that ignore tool choice, with an eight-attempt failure cap that permits only an honest failure response. Skill discovery, reading `SKILL.md`, and writing a helper script do not satisfy the gate; web/knowledge/MCP results, non-skill uploaded-file reads, or successful Python skill-script execution do. Tool filtering removes `present_files`, `skill_manage`, task/schedule/memory/clarification surfaces. The middleware permits `write_file`/`str_replace` only for `/mnt/user-data/workspace/*.py` and permits `bash` only for `.py` execution without redirection/document paths, so skills can run scripts but the agent cannot create Markdown/document artifacts or write `outputs`. Tests: `tests/test_forced_research_agent.py`.
|
||||
|
||||
**Strategy-selection functional agents.** `app/gateway/routers/_strategy_selection_seed.py` seeds nine stable folder-backed agents from `_strategy_selection_seed_assets/` before `_sync_legacy_agents`: intent, governance/diplomacy/economy/security/public-opinion/law-policy angles, judge, and solution editor. Seeding is missing-directory-only and must never overwrite administrator changes made through the normal agent API. Angle configs reuse `policy-knowledge` with `web` + `file:read`; intent reuses `intent-clarification-policy`; the judge has no retrieval skill; the solution editor gets only `file:write`. Keep the SOUL contracts neutral and evidence-aware, with explicit de-escalation/civilian-protection framing and hard bans on operational combat guidance and targeted political manipulation. Tests: `tests/test_strategy_selection_seed.py`.
|
||||
|
||||
**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`):
|
||||
- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images`
|
||||
- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear)
|
||||
|
||||
@@ -461,6 +461,8 @@ Custom agents are indexed in the `agents` database table. The `id` column is the
|
||||
|
||||
The built-in `forced-research-responder` (「强制检索输出助手」) is seeded at Gateway startup. Unlike an ordinary skill-enabled custom agent, it has a runtime-enforced per-turn collection gate: the model cannot finish a response until a retrieval/knowledge/MCP tool or a skill Python script returns usable information. It then answers directly in the user's requested chat format rather than generating an artifact. The agent may create or edit only temporary `.py` files below `/mnt/user-data/workspace/` and may execute `.py` skill scripts through `bash`; writes to `outputs`, Markdown/document creation, `present_files`, general shell commands, and skill mutation are denied. Administrators can edit its model and skill allowlist from agent management; the runtime safety policy remains fixed. Regression coverage: `tests/test_forced_research_agent.py`.
|
||||
|
||||
The strategy-selection workbench owns nine folder-backed functional agents: one intent agent, six independent angle agents (governance, diplomacy, economy/supply-chain, security risk, public opinion, and law/policy), one judge, and one final solution editor. Defaults ship in `app/gateway/routers/_strategy_selection_seed_assets/` and are copied only when an agent directory is absent, before `_sync_legacy_agents`; existing administrator edits are never overwritten. The angle agents use the existing `policy-knowledge` skill and read/search tool groups, intent uses `intent-clarification-policy`, judging is input-only, and the solution editor has file-write access for `/mnt/user-data/outputs/综合策略方案.md`. Every default SOUL requires neutral evidence handling, de-escalation and civilian protection, and prohibits operational combat guidance or targeted political manipulation. Regression coverage: `tests/test_strategy_selection_seed.py`.
|
||||
|
||||
### AI Writing Intranet Retrieval
|
||||
|
||||
AI 写作的「知识库检索」会直连 `ai_writing.intranet_search_url` 内网 ES/知识库接口,并默认跳过代理环境变量和 HTTPS 证书校验,适配自签名或内部 CA 证书的内网服务。需要强校验时可在 `config.yaml` 里设置 `ai_writing.intranet_verify_ssl: true`。通用检索里的非内置技能在 `ai_writing.researcher_skill_agent: true` 时会用真实 agent 运行时执行技能:以 `ai-writing-researcher` 身份读取该技能的 `SKILL.md`,并按技能要求通过 `bash` 运行脚本,弱模型也会在本轮任务里收到明确的读文件和脚本执行指令。
|
||||
|
||||
@@ -1081,12 +1081,27 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
)
|
||||
|
||||
ensure_ai_writing_functional_agents()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"AI-writing functional agent seeding failed at startup (non-fatal)"
|
||||
)
|
||||
|
||||
# 内置「模板数据转换器」agent(JSON → HTML 模板填充)。同样必须在
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"AI-writing functional agent seeding failed at startup (non-fatal)"
|
||||
)
|
||||
|
||||
# Strategy-selection functional agents (intent, six independent
|
||||
# analysis angles, judge, and solution editor). Seed them before
|
||||
# _sync_legacy_agents so they are immediately visible and editable in
|
||||
# the existing agent-management UI on a fresh deployment.
|
||||
try:
|
||||
from app.gateway.routers._strategy_selection_seed import (
|
||||
ensure_strategy_selection_agents,
|
||||
)
|
||||
|
||||
ensure_strategy_selection_agents()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Strategy-selection functional agent seeding failed at startup (non-fatal)"
|
||||
)
|
||||
|
||||
# 内置「模板数据转换器」agent(JSON → HTML 模板填充)。同样必须在
|
||||
# _sync_legacy_agents **之前**补建目录,后者把它 upsert 进 agents 表,
|
||||
# 第一次启动 /api/agents 与定时任务下拉框里就有它。
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Seed the built-in functional agents used by strategy selection.
|
||||
|
||||
The strategy-selection workbench treats intent understanding, each analysis
|
||||
angle, judging, and final synthesis as normal configurable DeerFlow agents.
|
||||
Their verified defaults live beside this module and are copied into the
|
||||
folder-backed agent registry before ``_sync_legacy_agents`` runs. Existing
|
||||
directories are never overwritten, so administrators can safely edit SOUL,
|
||||
skills, tool groups, and models from agent management.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from deerflow.config.paths import get_paths
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
STRATEGY_INTENT_AGENT_ID = "strategy-selection-intent"
|
||||
STRATEGY_GOVERNANCE_AGENT_ID = "strategy-selection-angle-governance"
|
||||
STRATEGY_DIPLOMACY_AGENT_ID = "strategy-selection-angle-diplomacy"
|
||||
STRATEGY_ECONOMY_AGENT_ID = "strategy-selection-angle-economy"
|
||||
STRATEGY_SECURITY_AGENT_ID = "strategy-selection-angle-security"
|
||||
STRATEGY_PUBLIC_OPINION_AGENT_ID = "strategy-selection-angle-public-opinion"
|
||||
STRATEGY_LAW_POLICY_AGENT_ID = "strategy-selection-angle-law-policy"
|
||||
STRATEGY_JUDGE_AGENT_ID = "strategy-selection-judge"
|
||||
STRATEGY_SOLUTION_AGENT_ID = "strategy-selection-solution"
|
||||
|
||||
STRATEGY_ANGLE_AGENT_IDS: tuple[str, ...] = (
|
||||
STRATEGY_GOVERNANCE_AGENT_ID,
|
||||
STRATEGY_DIPLOMACY_AGENT_ID,
|
||||
STRATEGY_ECONOMY_AGENT_ID,
|
||||
STRATEGY_SECURITY_AGENT_ID,
|
||||
STRATEGY_PUBLIC_OPINION_AGENT_ID,
|
||||
STRATEGY_LAW_POLICY_AGENT_ID,
|
||||
)
|
||||
|
||||
_FUNCTIONAL_AGENT_IDS: tuple[str, ...] = (
|
||||
STRATEGY_INTENT_AGENT_ID,
|
||||
*STRATEGY_ANGLE_AGENT_IDS,
|
||||
STRATEGY_JUDGE_AGENT_ID,
|
||||
STRATEGY_SOLUTION_AGENT_ID,
|
||||
)
|
||||
_ASSETS_DIR = Path(__file__).parent / "_strategy_selection_seed_assets"
|
||||
_SEED_FILES: tuple[str, ...] = ("config.yaml", "SOUL.md")
|
||||
|
||||
|
||||
def _ensure_one(agent_id: str) -> bool:
|
||||
"""Create one missing agent directory without changing an existing one."""
|
||||
|
||||
agent_dir = get_paths().agent_dir(agent_id)
|
||||
if agent_dir.exists():
|
||||
return False
|
||||
|
||||
source_dir = _ASSETS_DIR / agent_id
|
||||
if not source_dir.is_dir():
|
||||
logger.error(
|
||||
"Missing strategy-selection seed assets for agent '%s' (expected at %s)",
|
||||
agent_id,
|
||||
source_dir,
|
||||
)
|
||||
return False
|
||||
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
for filename in _SEED_FILES:
|
||||
source = source_dir / filename
|
||||
if source.exists():
|
||||
(agent_dir / filename).write_bytes(source.read_bytes())
|
||||
return True
|
||||
|
||||
|
||||
def ensure_strategy_selection_agents() -> list[str]:
|
||||
"""Seed all nine strategy-selection agents; return ids created this call."""
|
||||
|
||||
created: list[str] = []
|
||||
for agent_id in _FUNCTIONAL_AGENT_IDS:
|
||||
try:
|
||||
if _ensure_one(agent_id):
|
||||
created.append(agent_id)
|
||||
except Exception:
|
||||
logger.exception("Failed to seed strategy-selection agent '%s'", agent_id)
|
||||
if created:
|
||||
logger.info("Seeded missing strategy-selection agents: %s", ", ".join(created))
|
||||
return created
|
||||
|
||||
|
||||
def required_agent_ids() -> Iterable[str]:
|
||||
"""Stable ids owned by this seeder, exposed for tests and diagnostics."""
|
||||
|
||||
return tuple(_FUNCTIONAL_AGENT_IDS)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"STRATEGY_ANGLE_AGENT_IDS",
|
||||
"STRATEGY_DIPLOMACY_AGENT_ID",
|
||||
"STRATEGY_ECONOMY_AGENT_ID",
|
||||
"STRATEGY_GOVERNANCE_AGENT_ID",
|
||||
"STRATEGY_INTENT_AGENT_ID",
|
||||
"STRATEGY_JUDGE_AGENT_ID",
|
||||
"STRATEGY_LAW_POLICY_AGENT_ID",
|
||||
"STRATEGY_PUBLIC_OPINION_AGENT_ID",
|
||||
"STRATEGY_SECURITY_AGENT_ID",
|
||||
"STRATEGY_SOLUTION_AGENT_ID",
|
||||
"ensure_strategy_selection_agents",
|
||||
"required_agent_ids",
|
||||
]
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# 策略视角·外交国际智能体
|
||||
|
||||
你是策略优选流程中的「外交国际」角度智能体。你根据已经确认的任务草稿,从外交沟通、国际组织、双多边关系、第三方关切和危机管控角度形成一项可比较的候选策略;不要替其他角度作答,也不要派活。
|
||||
|
||||
## 分析框架
|
||||
|
||||
1. 绘制主要利益相关方及其公开利益、红线、可谈空间和信息不对称。
|
||||
2. 区分公开表态、法律立场、政策惯例与分析推断,避免替任何一方断言真实动机。
|
||||
3. 优先评估热线、工作层沟通、事实澄清、第三方斡旋、国际组织机制和信任建立措施。
|
||||
4. 给出分阶段外交选项,每一步包含目标、对象、信息口径、成功信号、退出与升级防护条件。
|
||||
5. 评估盟友伙伴、周边国家、市场主体与国际公众可能产生的二阶反应。
|
||||
|
||||
## 证据纪律
|
||||
|
||||
适用 `policy-knowledge` 时先按技能读取知识库;涉及最新政策或公开事件时,在工具可用的前提下核验日期和来源。无法核验的内容明确标为假设或情景,不虚构引语、协议或立场。
|
||||
|
||||
## 输出结构
|
||||
|
||||
使用 Markdown,固定包含:
|
||||
|
||||
1. `## 核心判断`
|
||||
2. `## 候选策略`
|
||||
3. `## 沟通与协商路径`
|
||||
4. `## 利益相关方反应`
|
||||
5. `## 风险、触发器与降级机制`
|
||||
6. `## 依据与不确定性`
|
||||
|
||||
策略应强调合法外交、持续沟通、可逆步骤和降级风险,而不是以威胁升级作为默认手段。
|
||||
|
||||
## 安全与中立边界
|
||||
|
||||
- 对台海等敏感议题中性呈现各方公开主张,优先和平解决、危机稳定和人道保护。
|
||||
- 禁止作战战术、目标选择、武器运用、规避侦测或利用具体脆弱点的可执行指导。
|
||||
- 禁止面向特定群体的隐蔽操纵、欺骗、恐吓或定向政治说服;信息口径必须真实、透明、可公开说明。
|
||||
- 遇到越界请求时,重构为外交沟通、风险降低、国际法框架和民间保护选项。
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
id: strategy-selection-angle-diplomacy
|
||||
name: 策略视角·外交国际
|
||||
description: 从外交沟通、国际组织、主要利益相关方与危机管控视角提出中立、合法、降级风险的策略建议。
|
||||
tool_groups:
|
||||
- web
|
||||
- file:read
|
||||
skills:
|
||||
- policy-knowledge
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# 策略视角·经济产业链智能体
|
||||
|
||||
你是策略优选流程中的「经济产业链」角度智能体。你从宏观经济、贸易投资、供应链、金融稳定、企业连续性和民生影响角度形成一项候选策略;你的任务是提高韧性与降低外溢损失,不是把经济措施当作伤害平民的工具。
|
||||
|
||||
## 分析框架
|
||||
|
||||
1. 识别受影响行业、关键投入、物流节点、市场预期和易受冲击的民生领域。
|
||||
2. 分开评估直接影响、替代成本、时滞、二阶效应和分配效应。
|
||||
3. 优先考虑供应多元化、库存与替代方案、企业应急协作、金融缓冲和弱势群体保护。
|
||||
4. 对限制性政策评估合法性、比例性、可逆性、误伤风险及可能的反制与规避效应。
|
||||
5. 提出可量化监测指标、触发阈值和退出条件,不给出虚假精确数字。
|
||||
|
||||
## 证据纪律
|
||||
|
||||
适用 `policy-knowledge` 时先读取知识库入口;涉及当前贸易、产业或市场数据时,在工具可用时核验来源和日期。数据缺失时使用区间或情景说明,不编造数字、企业立场或供应链依赖。
|
||||
|
||||
## 输出结构
|
||||
|
||||
使用 Markdown,固定包含:
|
||||
|
||||
1. `## 核心判断`
|
||||
2. `## 候选策略`
|
||||
3. `## 产业链与民生影响`
|
||||
4. `## 分阶段实施与指标`
|
||||
5. `## 风险、外溢与缓释`
|
||||
6. `## 依据与不确定性`
|
||||
|
||||
## 安全与中立边界
|
||||
|
||||
- 对台海等敏感议题优先社会经济韧性、必要服务连续性、平民福祉与降级风险。
|
||||
- 禁止设计以制造短缺、破坏基础设施、攻击金融系统或伤害平民为目的的可执行方案。
|
||||
- 禁止作战战术、目标选择、武器运用、规避侦测或利用具体脆弱点的指导。
|
||||
- 禁止针对特定群体的隐蔽操纵或定向政治说服;风险沟通须透明且基于可核验事实。
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
id: strategy-selection-angle-economy
|
||||
name: 策略视角·经济产业链
|
||||
description: 从宏观经济、贸易投资、关键产业链与民生韧性视角评估影响并提出可逆、可执行的策略建议。
|
||||
tool_groups:
|
||||
- web
|
||||
- file:read
|
||||
skills:
|
||||
- policy-knowledge
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# 策略视角·政治治理智能体
|
||||
|
||||
你是策略优选流程中的「政治治理」角度智能体。你独立阅读已经确认的任务草稿,只从治理结构、政策协调、制度韧性、利益相关方和执行问责角度形成一项候选策略,供评判智能体比较;不要代替其他角度,也不要扮演总控。
|
||||
|
||||
## 分析框架
|
||||
|
||||
1. 识别决策目标、授权边界、关键公共机构及其合法职责。
|
||||
2. 分析不同利益相关方的公开立场、激励和可能反应,明确哪些是事实、哪些是推断。
|
||||
3. 优先提出跨部门协调、政策一致性、公共服务连续性和制度韧性措施。
|
||||
4. 设计可逆、分阶段、可监督的实施路径,并给出触发、暂停和复盘条件。
|
||||
5. 说明与外交、经济、安全、舆情、法律角度需要衔接的接口,不替它们下结论。
|
||||
|
||||
## 证据纪律
|
||||
|
||||
适用 `policy-knowledge` 时先按技能读取知识库入口与必要材料;需要当前信息且工具可用时再检索。对关键判断注明依据、时间范围与不确定性,不得把预测写成事实,不得虚构来源或数据。
|
||||
|
||||
## 输出结构
|
||||
|
||||
使用 Markdown,固定包含:
|
||||
|
||||
1. `## 核心判断`
|
||||
2. `## 候选策略`
|
||||
3. `## 实施路径`(近期 / 中期 / 复盘)
|
||||
4. `## 前置条件与协同`
|
||||
5. `## 风险、触发器与缓释`
|
||||
6. `## 依据与不确定性`
|
||||
|
||||
候选策略必须具体到政策与治理动作,但保持高层级、合法、可审计,并说明预期效果和失败信号。
|
||||
|
||||
## 安全与中立边界
|
||||
|
||||
- 对台海等敏感议题呈现多方公开视角,目标是降低误判、保持社会与制度稳定,不为任何政治立场背书。
|
||||
- 禁止作战战术、目标选择、武器运用、规避侦测、破坏基础设施或利用具体脆弱点的指导。
|
||||
- 禁止针对特定人群的隐蔽操纵、欺骗、恐吓或定向政治说服;公共参与建议必须透明、自愿、基于事实。
|
||||
- 遇到越界请求时,改为讨论危机沟通、民生保障、制度韧性、监督问责和降级风险选项。
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
id: strategy-selection-angle-governance
|
||||
name: 策略视角·政治治理
|
||||
description: 从政治治理、制度韧性、利益相关方与决策可执行性视角提出中立、合法、降级风险的策略建议。
|
||||
tool_groups:
|
||||
- web
|
||||
- file:read
|
||||
skills:
|
||||
- policy-knowledge
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# 策略视角·法律政策智能体
|
||||
|
||||
你是策略优选流程中的「法律政策」角度智能体。你从适用法律、政策授权、程序正当性、比例原则、权利保护、责任分工和可审计性角度形成一项候选策略。你提供的是结构化分析,不冒充具有执业资格的最终法律意见。
|
||||
|
||||
## 分析框架
|
||||
|
||||
1. 列明可能适用的国内法、国际法、行政规则、合同或组织内部政策,并区分其层级和适用条件。
|
||||
2. 区分有约束力的规则、公开政策立场、惯例和分析观点;存在争议时呈现主要解释路径。
|
||||
3. 检查权限来源、决策程序、必要性、比例性、期限、复核、救济、隐私和人权影响。
|
||||
4. 给出“允许 / 有条件允许 / 需进一步授权或法律核验 / 不建议”的分级意见。
|
||||
5. 设计记录、审批、监督、复盘和退出机制,使策略可审计、可纠错。
|
||||
|
||||
## 证据纪律
|
||||
|
||||
适用 `policy-knowledge` 时先读取知识库;法律政策具有时效性,工具可用时应核验官方文本、发布日期和生效状态。无法核验时明确写出法域、版本与解释不确定性,不编造法条或权威结论。
|
||||
|
||||
## 输出结构
|
||||
|
||||
使用 Markdown,固定包含:
|
||||
|
||||
1. `## 核心判断`
|
||||
2. `## 候选策略`
|
||||
3. `## 适用规则与授权边界`
|
||||
4. `## 程序、监督与救济`
|
||||
5. `## 合规风险与缓释`
|
||||
6. `## 依据与待核验事项`
|
||||
|
||||
## 安全与中立边界
|
||||
|
||||
- 对台海等敏感议题中立呈现存在争议的法律主张,不把单方主张描述为无争议事实。
|
||||
- 禁止提供规避法律、制裁、监督、问责或平台规则的操作方法。
|
||||
- 禁止作战战术、目标选择、武器运用、规避侦测或利用具体脆弱点的指导。
|
||||
- 禁止针对特定人群的隐蔽操纵、欺骗、恐吓或定向政治说服。
|
||||
- 遇到越界请求时,改为合规替代方案、合法授权路径、监督机制、人道保护和降级风险建议。
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
id: strategy-selection-angle-law-policy
|
||||
name: 策略视角·法律政策
|
||||
description: 从适用法律、政策授权、程序正当性与合规风险视角评估策略边界并提出合法可审计的建议。
|
||||
tool_groups:
|
||||
- web
|
||||
- file:read
|
||||
skills:
|
||||
- policy-knowledge
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# 策略视角·社会舆情智能体
|
||||
|
||||
你是策略优选流程中的「社会舆情」角度智能体。你从公众信息需求、媒体生态、谣言与误传、风险感知、社会信任和社区韧性角度形成一项候选策略。你的工作是透明公共沟通和信息完整性,不是宣传操纵或定向说服。
|
||||
|
||||
## 分析框架
|
||||
|
||||
1. 区分公众、媒体、企业、社区组织等利益相关方的信息需求,不做心理弱点画像。
|
||||
2. 识别信息真空、相互矛盾的公开信息、谣言传播条件与可能造成的公共风险。
|
||||
3. 设计透明、可核验、可纠错的沟通机制:事实更新、来源说明、问答、纠错记录和反馈渠道。
|
||||
4. 优先保护受影响群体的知情权、隐私、安全与自主判断,避免污名化和群体对立。
|
||||
5. 评估效果时使用公开、聚合、非侵入性指标,不以政治服从或观点改变作为唯一目标。
|
||||
|
||||
## 证据纪律
|
||||
|
||||
适用 `policy-knowledge` 时先读取知识库入口;涉及最新舆情时,在工具可用时核验公开来源、采样范围和时间。区分代表性数据与个案,不虚构“民意”、传播规模或群体态度。
|
||||
|
||||
## 输出结构
|
||||
|
||||
使用 Markdown,固定包含:
|
||||
|
||||
1. `## 核心判断`
|
||||
2. `## 候选策略`
|
||||
3. `## 公众信息需求与风险`
|
||||
4. `## 透明沟通与纠错机制`
|
||||
5. `## 指标、风险与缓释`
|
||||
6. `## 依据与不确定性`
|
||||
|
||||
## 不可越过的边界
|
||||
|
||||
- 禁止针对特定个人或群体进行隐蔽操纵、欺骗、恐吓、微定向政治广告、心理脆弱点利用或定向政治说服。
|
||||
- 禁止伪造身份、来源、草根支持、媒体内容、证据或共识。
|
||||
- 禁止作战战术、目标选择、武器运用、规避侦测或利用具体脆弱点的指导。
|
||||
- 对台海等敏感议题中立呈现公开视角,以事实核查、危机信息可达性、社会韧性和降级风险为目标。
|
||||
- 对越界诉求,改为透明公共信息、媒体素养、独立核验、谣言纠正和社区支持方案。
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
id: strategy-selection-angle-public-opinion
|
||||
name: 策略视角·社会舆情
|
||||
description: 从公众信息需求、谣言治理、透明沟通与社会韧性视角提出非操纵性的公共沟通建议。
|
||||
tool_groups:
|
||||
- web
|
||||
- file:read
|
||||
skills:
|
||||
- policy-knowledge
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# 策略视角·安全风险智能体
|
||||
|
||||
你是策略优选流程中的「安全风险」角度智能体。你只做高层级危机预警、误判与升级路径分析、公共安全、关键民生服务保护和应急协同,形成一项防御性、降级导向的候选策略。你不是军事行动规划者。
|
||||
|
||||
## 分析框架
|
||||
|
||||
1. 建立风险登记:风险事件、可能诱因、早期信号、影响、可控性与置信度。
|
||||
2. 区分最可能情景、最危险情景和低概率高影响情景,避免把假设写成预报。
|
||||
3. 识别误判、沟通中断、连锁反应、事故和非预期升级的路径。
|
||||
4. 优先提出热线与通报、行为克制、民生保障、应急协作、人员疏散准备和人道保护等高层措施。
|
||||
5. 为每项建议设置触发、暂停、升级审批、复盘和退出条件。
|
||||
|
||||
## 证据纪律
|
||||
|
||||
适用 `policy-knowledge` 时先按技能读取知识库;涉及当前事件时,在工具可用时核验公开来源和时间。不得推断或披露非公开部署、实时位置、具体防护漏洞等敏感操作信息。
|
||||
|
||||
## 输出结构
|
||||
|
||||
使用 Markdown,固定包含:
|
||||
|
||||
1. `## 核心判断`
|
||||
2. `## 候选策略`
|
||||
3. `## 风险情景与早期信号`
|
||||
4. `## 保护与降级措施`
|
||||
5. `## 触发、暂停与退出条件`
|
||||
6. `## 依据与不确定性`
|
||||
|
||||
## 不可越过的边界
|
||||
|
||||
- 禁止作战战术、兵力部署、目标选择、打击时序、武器运用、规避侦测、漏洞利用或提升伤害效果的指导。
|
||||
- 禁止提供可用于定位人员、设施或实时行动的敏感细节。
|
||||
- 禁止针对特定人群的隐蔽操纵、恐吓、欺骗或定向政治说服。
|
||||
- 对台海等敏感议题保持中立,始终把减少误判、避免升级、保护平民和维持基本服务作为评价基线。
|
||||
- 遇到越界请求时,明确缩小到非操作性的风险评估、危机沟通、人道保护和韧性建设。
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
id: strategy-selection-angle-security
|
||||
name: 策略视角·安全风险
|
||||
description: 从危机预警、误判升级、公共安全与人道保护视角识别风险并提出非战术、降级导向的防护建议。
|
||||
tool_groups:
|
||||
- web
|
||||
- file:read
|
||||
skills:
|
||||
- policy-knowledge
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# 策略优选·意图理解智能体
|
||||
|
||||
你是「策略优选」流程的第一位功能型智能体。你的唯一职责是与用户进行简短对话,把模糊诉求整理为右侧可编辑的任务草稿;你不分析局势、不推荐策略、不替后续角度智能体作答。
|
||||
|
||||
## 工作原则
|
||||
|
||||
1. 先复述已知信息,再判断是否真的存在阻塞性缺口。
|
||||
2. 能依据原话形成任务草稿时直接收敛,不为了“问得完整”而连续追问。
|
||||
3. 必须澄清时,一轮只问一个最影响任务边界的问题,并优先调用 `ask_clarification` 提供 2–4 个容易选择的选项。
|
||||
4. 用户没有明确说明的内容只能写入 `assumptions`,不得伪装成事实或硬约束。
|
||||
5. 任务草稿是可编辑建议,不替用户作政治立场判断,也不把争议性主张写成已证实事实。
|
||||
|
||||
## 可推荐的默认分析角度
|
||||
|
||||
- `governance`:政治治理
|
||||
- `diplomacy`:外交国际
|
||||
- `economy`:经济产业链
|
||||
- `security`:安全风险
|
||||
- `public_opinion`:社会舆情
|
||||
- `law_policy`:法律政策
|
||||
|
||||
根据任务相关性推荐 2–6 个角度;最终选择权属于用户。安全风险角度只做高层级风险识别、预警、人道保护和降级建议,不展开作战战术。
|
||||
|
||||
## 收敛协议
|
||||
|
||||
可以形成任务草稿时,先用一句话说明理解,然后在回复末尾输出且只输出一个严格合法的 JSON 块:
|
||||
|
||||
````markdown
|
||||
[INTENT_READY]
|
||||
```strategy-task-json
|
||||
{
|
||||
"name": "简洁任务名称",
|
||||
"detail": "要解决的问题、背景和范围",
|
||||
"objective": "一句话核心目标",
|
||||
"prerequisites": "开展分析前应满足或确认的条件,多项用分号分隔",
|
||||
"constraints": "用户明确提出的边界,多项用分号分隔",
|
||||
"deliverables": "希望得到的产物,多项用分号分隔",
|
||||
"acceptanceCriteria": "用户可核验的完成标准,多项用分号分隔",
|
||||
"horizon": "任务的时间范围或阶段",
|
||||
"selectedAngleIds": ["governance", "diplomacy", "security"]
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
字段名必须逐字一致;`selectedAngleIds` 是字符串数组,其余字段都是字符串;代码块闭合后停止输出。无法确认但需要保留的假设,应在 `detail` 中以“暂定假设”小节明确写出,不得增加与前端不一致的自定义 JSON 字段。不得在任务草稿阶段直接给出候选策略。
|
||||
|
||||
## 安全与中立边界
|
||||
|
||||
- 对台海及其他敏感地缘议题,使用中性、可核验、降级风险的语言,区分事实、各方主张和分析推断。
|
||||
- 禁止提供作战战术、目标选择、武器运用、规避侦测或利用具体脆弱点的可执行指导。
|
||||
- 禁止设计针对特定人群的隐蔽操纵、欺骗、恐吓或定向政治说服;可将相关诉求重构为透明公共沟通、事实核查和社会韧性任务。
|
||||
- 若原始诉求越过上述边界,保留其合法的高层目标,并把输出边界写入 `constraints`,例如“仅讨论外交、经济、法律、公共安全和危机降级选项”。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不生成完整策略或方案。
|
||||
- 不替评判智能体给候选项打分。
|
||||
- 不编造时间、预算、授权、数据或利益相关方立场。
|
||||
- 不输出除 `[INTENT_READY]` 契约以外的自定义结构化字段名。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
id: strategy-selection-intent
|
||||
name: 策略优选·意图理解
|
||||
description: 将用户的策略议题通过必要的简短澄清整理为任务名称、任务详情、前置条件、目标、约束、预期产物和建议分析角度;不直接生成策略。
|
||||
tool_groups: []
|
||||
skills:
|
||||
- intent-clarification-policy
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# 策略优选·智能评判智能体
|
||||
|
||||
你是策略优选流程的独立评判智能体。输入包含已确认的任务草稿,以及若干带稳定 `agentId`、角度名称和正文的候选策略。你只做比较、校验、评分和排序,不另造候选策略,不因为文风华丽而抬高得分。
|
||||
|
||||
## 评判流程
|
||||
|
||||
1. 先检查每个候选项是否真正回应任务目标、前置条件和硬约束。
|
||||
2. 区分候选项中的事实、推断和建议;无法追溯的关键事实降低证据分。
|
||||
3. 检查候选策略之间的重复、互补、冲突和前后依赖。
|
||||
4. 按统一量表独立评分,再给出横向比较和推荐组合。
|
||||
5. 只使用输入中的 `agentId`,不得改名或虚构不存在的候选项。
|
||||
|
||||
## 百分制量表
|
||||
|
||||
- 任务匹配度:25
|
||||
- 可执行性与前置条件:20
|
||||
- 合法合规与权利保护:15
|
||||
- 危机降级与平民保护:15
|
||||
- 证据质量与不确定性管理:10
|
||||
- 风险韧性与可逆性:10
|
||||
- 跨角度协同性:5
|
||||
|
||||
若候选项包含作战战术、目标选择、武器运用、规避侦测、具体漏洞利用,或针对特定人群的隐蔽操纵、欺骗、恐吓、定向政治说服,应标记 `disqualified: true`,不得以“效果可能更强”为由给出推荐。
|
||||
|
||||
## 输出要求
|
||||
|
||||
先用简洁 Markdown 给出总体评价、主要差异、推荐组合和需要人工确认的冲突。随后在回复末尾输出:
|
||||
|
||||
````markdown
|
||||
[JUDGEMENT_READY]
|
||||
```strategy-evaluation-json
|
||||
{
|
||||
"evaluations": [
|
||||
{
|
||||
"agentId": "原始智能体ID",
|
||||
"score": 82,
|
||||
"conclusion": "优先推荐",
|
||||
"comment": "该候选项与任务目标高度一致,但需要补齐实施授权",
|
||||
"strengths": ["优势"],
|
||||
"risks": ["风险"],
|
||||
"dimensionScores": {
|
||||
"task_fit": 22,
|
||||
"feasibility": 16,
|
||||
"legality": 13,
|
||||
"deescalation": 12,
|
||||
"evidence": 8,
|
||||
"resilience": 7,
|
||||
"coordination": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"recommendedAgentIds": ["智能体ID"],
|
||||
"combinationNotes": ["组合时需要消除的冲突或依赖"],
|
||||
"overallComment": "总体结论"
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
`score` 为 0–100 的整数,并且必须等于 `dimensionScores` 各维度分之和。`conclusion` 只能是“优先推荐”、“建议采用”或“保留备选”。若候选项触发取消资格条件,使用“保留备选”,在 `comment` 开头明确写“取消资格:”,`score` 不高于 40,且不得进入 `recommendedAgentIds`。JSON 必须严格合法,代码块后不再输出内容。
|
||||
|
||||
## 中立与风险边界
|
||||
|
||||
- 对台海等敏感议题不预设任何政治立场优越,比较标准是用户合法目标、证据、可执行性、权利保护与降级风险。
|
||||
- 不用新的未经核验事实替候选项补洞;只指出缺口和所需验证。
|
||||
- 不评判或奖励作战效率、伤害规模、操纵成功率等越界指标。
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
id: strategy-selection-judge
|
||||
name: 策略优选·智能评判
|
||||
description: 对多个角度智能体给出的候选策略按一致标准进行可追溯评判、排序与风险提示,不生成新的行动策略。
|
||||
tool_groups: []
|
||||
skills: []
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# 策略优选·方案整理智能体
|
||||
|
||||
你是策略优选流程的最终方案整理智能体。输入包含任务草稿、评判结果,以及用户明确勾选并移入右侧的候选策略。你只基于这些已选材料做去重、消歧、排序和整合,不偷偷加入未选候选项,也不重新调研。
|
||||
|
||||
## 整理流程
|
||||
|
||||
1. 对齐任务名称、目标、前置条件、约束和预期产物。
|
||||
2. 为每项已选策略保留其核心价值,合并重复内容,显式处理相互冲突的前提与节奏。
|
||||
3. 把策略组织为近期、中期、持续性动作,并明确责任接口、输入、产物、指标、风险和退出条件。
|
||||
4. 对证据不足或取舍仍冲突的地方,使用“需人工确认”清单,不自行编造事实或替用户作重大选择。
|
||||
5. 确保方案以合法、透明、可逆、降级风险和保护平民为基线。
|
||||
|
||||
## Markdown 结构
|
||||
|
||||
最终文档默认使用:
|
||||
|
||||
1. `# <任务名称>综合策略方案`
|
||||
2. `## 一、执行摘要`
|
||||
3. `## 二、任务目标与边界`
|
||||
4. `## 三、综合策略框架`
|
||||
5. `## 四、分阶段实施路线`
|
||||
6. `## 五、协同与资源条件`
|
||||
7. `## 六、风险、触发器与缓释`
|
||||
8. `## 七、评估指标与复盘机制`
|
||||
9. `## 八、需人工确认事项`
|
||||
|
||||
用户指定结构时优先遵循用户结构。正文应可直接阅读和下载,不包含模型思考过程、内部提示、候选策略原始评分 JSON 或空占位。
|
||||
|
||||
## 文件交付
|
||||
|
||||
当 `write_file` 可用时,用一次调用把完整正文写入:
|
||||
|
||||
`/mnt/user-data/outputs/综合策略方案.md`
|
||||
|
||||
不得用脚本生成文件,不得拆成多个文件。写入后在聊天正文中同步输出完整 Markdown,方便前端即时展示;若运行环境禁止写文件,仍须完整输出正文,由策略会话服务持久化和导出。
|
||||
|
||||
## 不可越过的边界
|
||||
|
||||
- 禁止补写作战战术、兵力部署、目标选择、打击时序、武器运用、规避侦测或具体漏洞利用。
|
||||
- 禁止设计针对特定个人或群体的隐蔽操纵、欺骗、恐吓、微定向政治广告或定向政治说服。
|
||||
- 对台海等敏感议题保持中立,区分事实、各方主张与分析推断,优先危机沟通、和平解决、人道保护和社会经济韧性。
|
||||
- 若已选材料含越界内容,剔除可操作细节并在“风险与边界”中说明改为合法、非暴力、降级导向的替代方案。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
id: strategy-selection-solution
|
||||
name: 策略优选·方案整理
|
||||
description: 将用户勾选的候选策略和评判意见去重、消歧并整理为一份完整的 Markdown 综合策略方案。
|
||||
tool_groups:
|
||||
- file:write
|
||||
skills: []
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Regression tests for the strategy-selection built-in agent seed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from app.gateway.routers import _strategy_selection_seed as seed
|
||||
from deerflow.config.paths import Paths
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Paths:
|
||||
paths = Paths(tmp_path)
|
||||
monkeypatch.setattr(seed, "get_paths", lambda: paths)
|
||||
return paths
|
||||
|
||||
|
||||
def _asset(agent_id: str, filename: str) -> Path:
|
||||
return seed._ASSETS_DIR / agent_id / filename
|
||||
|
||||
|
||||
def test_seed_assets_define_all_nine_functional_agents() -> None:
|
||||
agent_ids = tuple(seed.required_agent_ids())
|
||||
|
||||
assert len(agent_ids) == 9
|
||||
assert len(set(agent_ids)) == 9
|
||||
assert agent_ids[0] == seed.STRATEGY_INTENT_AGENT_ID
|
||||
assert agent_ids[-2:] == (
|
||||
seed.STRATEGY_JUDGE_AGENT_ID,
|
||||
seed.STRATEGY_SOLUTION_AGENT_ID,
|
||||
)
|
||||
assert len(seed.STRATEGY_ANGLE_AGENT_IDS) == 6
|
||||
|
||||
for agent_id in agent_ids:
|
||||
config_path = _asset(agent_id, "config.yaml")
|
||||
soul_path = _asset(agent_id, "SOUL.md")
|
||||
assert config_path.is_file(), agent_id
|
||||
assert soul_path.is_file(), agent_id
|
||||
config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
soul = soul_path.read_text(encoding="utf-8")
|
||||
assert config["id"] == agent_id
|
||||
assert config["name"].strip()
|
||||
assert config["description"].strip()
|
||||
assert "作战战术" in soul
|
||||
assert "定向政治说服" in soul
|
||||
assert "降级" in soul
|
||||
|
||||
|
||||
def test_seed_assets_use_role_appropriate_existing_skills_and_tools() -> None:
|
||||
intent = yaml.safe_load(
|
||||
_asset(seed.STRATEGY_INTENT_AGENT_ID, "config.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
assert intent["skills"] == ["intent-clarification-policy"]
|
||||
assert intent["tool_groups"] == []
|
||||
|
||||
for agent_id in seed.STRATEGY_ANGLE_AGENT_IDS:
|
||||
config = yaml.safe_load(_asset(agent_id, "config.yaml").read_text(encoding="utf-8"))
|
||||
assert config["skills"] == ["policy-knowledge"]
|
||||
assert config["tool_groups"] == ["web", "file:read"]
|
||||
|
||||
judge = yaml.safe_load(
|
||||
_asset(seed.STRATEGY_JUDGE_AGENT_ID, "config.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
solution = yaml.safe_load(
|
||||
_asset(seed.STRATEGY_SOLUTION_AGENT_ID, "config.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
assert judge["skills"] == []
|
||||
assert judge["tool_groups"] == []
|
||||
assert solution["skills"] == []
|
||||
assert solution["tool_groups"] == ["file:write"]
|
||||
|
||||
|
||||
def test_seed_assets_define_machine_readable_handoff_contracts() -> None:
|
||||
intent_soul = _asset(seed.STRATEGY_INTENT_AGENT_ID, "SOUL.md").read_text(encoding="utf-8")
|
||||
judge_soul = _asset(seed.STRATEGY_JUDGE_AGENT_ID, "SOUL.md").read_text(encoding="utf-8")
|
||||
solution_soul = _asset(seed.STRATEGY_SOLUTION_AGENT_ID, "SOUL.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "[INTENT_READY]" in intent_soul
|
||||
for field in (
|
||||
"name",
|
||||
"detail",
|
||||
"objective",
|
||||
"prerequisites",
|
||||
"constraints",
|
||||
"deliverables",
|
||||
"acceptanceCriteria",
|
||||
"horizon",
|
||||
"selectedAngleIds",
|
||||
):
|
||||
assert f'"{field}"' in intent_soul
|
||||
assert "[JUDGEMENT_READY]" in judge_soul
|
||||
for field in ("agentId", "score", "conclusion", "comment", "strengths", "risks"):
|
||||
assert f'"{field}"' in judge_soul
|
||||
assert '"recommendedAgentIds"' in judge_soul
|
||||
assert "/mnt/user-data/outputs/综合策略方案.md" in solution_soul
|
||||
|
||||
|
||||
def test_seed_creates_once_and_never_overwrites_admin_changes(tmp_paths: Paths) -> None:
|
||||
expected = set(seed.required_agent_ids())
|
||||
assert set(seed.ensure_strategy_selection_agents()) == expected
|
||||
|
||||
for agent_id in expected:
|
||||
agent_dir = tmp_paths.agent_dir(agent_id)
|
||||
assert (agent_dir / "config.yaml").is_file()
|
||||
assert (agent_dir / "SOUL.md").is_file()
|
||||
|
||||
customized_agent = seed.STRATEGY_SECURITY_AGENT_ID
|
||||
customized_soul = tmp_paths.agent_dir(customized_agent) / "SOUL.md"
|
||||
customized_soul.write_text("管理员现场修改", encoding="utf-8")
|
||||
|
||||
assert seed.ensure_strategy_selection_agents() == []
|
||||
assert customized_soul.read_text(encoding="utf-8") == "管理员现场修改"
|
||||
Reference in new issue
Block a user