feat(roundtable): 会商席位「技能识别强化」改为按需开关(智能体级 + 链条每席位级)
弱模型补偿指令从全域生效改为默认关闭、两个 opt-in 开关取 OR: - 智能体级: AgentConfig.seat_skill_directive(config.yaml),在智能体编辑弹窗 (agent-card / agent-detail-panel)开关;agents 路由 create/update/读写全程穿。 - 业务链条每席位级: 存进链条 seats JSON(ChainSeatSchema.seat_skill_directive, 无需 DB 列/迁移),在链条编辑器每个席位卡片右侧 ✨ 图标→下拉框(开关+说明)配置; 按 agentId 查表穿到前台 useStep2Orchestration 各席位调用 + 后台作业 seat_skill_directives:{agentId:bool} → InProcessRoundtableGateway.run_seat。 - ROUNDTABLE_SEAT_SKILL_DIRECTIVE 降级为总熔断开关(默认放行,不再 force-on)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a8c06ca7dc
commit
9a1b674950
24 files changed
+298
-29
No files matched your search
@@ -229,6 +229,10 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
);
|
||||
const [editPublished, setEditPublished] = useState(agent.published);
|
||||
const [editIsPrivate, setEditIsPrivate] = useState(agent.is_private);
|
||||
// 会商席位「技能识别强化」开关(弱模型补偿)。默认关。
|
||||
const [editSeatSkillDirective, setEditSeatSkillDirective] = useState(
|
||||
agent.seat_skill_directive ?? false,
|
||||
);
|
||||
const [editSquareId, setEditSquareId] = useState(agent.square_id ?? "");
|
||||
const [publishPickerOpen, setPublishPickerOpen] = useState(false);
|
||||
const { data: publishSquares } = usePublishSquares();
|
||||
@@ -274,6 +278,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
setEditSquareId(agent.square_id ?? "");
|
||||
setSelectedSkills(agent.skills ?? []);
|
||||
setEditCommonQuestions(normalizeAgentQuestions(agent.common_questions));
|
||||
setEditSeatSkillDirective(agent.seat_skill_directive ?? false);
|
||||
}, [
|
||||
agent.common_questions,
|
||||
agent.description,
|
||||
@@ -282,6 +287,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
agent.published,
|
||||
agent.square_id,
|
||||
agent.skills,
|
||||
agent.seat_skill_directive,
|
||||
editOpen,
|
||||
]);
|
||||
|
||||
@@ -295,6 +301,8 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
if (agentDetail) {
|
||||
setEditSoul(agentDetail.soul ?? "");
|
||||
setEditCommonQuestions(normalizeAgentQuestions(agentDetail.common_questions));
|
||||
// 列表响应不带 config 额外字段(seat_skill_directive 为 null),详情才有真值。
|
||||
setEditSeatSkillDirective(agentDetail.seat_skill_directive ?? false);
|
||||
} else {
|
||||
setEditSoul(agent.soul ?? "");
|
||||
}
|
||||
@@ -386,6 +394,8 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
request.skills = selectedSkills;
|
||||
if (JSON.stringify(nextQuestions) !== JSON.stringify(baselineQuestions))
|
||||
request.common_questions = nextQuestions;
|
||||
if (editSeatSkillDirective !== ((agentDetail?.seat_skill_directive ?? agent.seat_skill_directive) ?? false))
|
||||
request.seat_skill_directive = editSeatSkillDirective;
|
||||
|
||||
if (Object.keys(request).length === 0) {
|
||||
setEditOpen(false);
|
||||
@@ -960,6 +970,19 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
onChange={setEditCommonQuestions}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">会商技能识别强化</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
作为会商席位时,把已配置技能(含 SKILL.md 路径与「先 read_file
|
||||
再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在业务链条配置里打开(任一即生效)。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={editSeatSkillDirective}
|
||||
onCheckedChange={setEditSeatSkillDirective}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">公开发布</p>
|
||||
|
||||
@@ -76,6 +76,8 @@ interface AgentDraft {
|
||||
published: boolean;
|
||||
featured: boolean;
|
||||
templateName: string;
|
||||
/** 会商席位「技能识别强化」开关。 */
|
||||
seatSkillDirective: boolean;
|
||||
}
|
||||
|
||||
function draftFromAgent(agent: Agent): AgentDraft {
|
||||
@@ -88,6 +90,7 @@ function draftFromAgent(agent: Agent): AgentDraft {
|
||||
published: agent.published,
|
||||
featured: agent.featured_order != null,
|
||||
templateName: agent.template_name ?? "",
|
||||
seatSkillDirective: agent.seat_skill_directive ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,6 +298,8 @@ export function AgentDetailPanel({
|
||||
if (draft.soul !== (agent.soul ?? "")) request.soul = draft.soul;
|
||||
if (draft.templateName.trim() !== (agent.template_name ?? ""))
|
||||
request.template_name = draft.templateName.trim();
|
||||
if (draft.seatSkillDirective !== (agent.seat_skill_directive ?? false))
|
||||
request.seat_skill_directive = draft.seatSkillDirective;
|
||||
if (nextPublished !== agent.published) request.published = nextPublished;
|
||||
if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? []))
|
||||
request.skills = draft.skills;
|
||||
@@ -561,6 +566,21 @@ export function AgentDetailPanel({
|
||||
配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
|
||||
<div className="pr-3">
|
||||
<p className="text-sm font-medium">会商技能识别强化</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
作为会商席位时,把已配置技能(含 SKILL.md 路径与「先 read_file
|
||||
再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在业务链条配置中打开(任一即生效)。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={draft.seatSkillDirective}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft({ ...draft, seatSkillDirective: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">公开发布</p>
|
||||
|
||||
@@ -58,6 +58,10 @@ export interface Agent {
|
||||
/** 入库模板名(外部知识入库接口的 templateName)。配置后该智能体会话中的
|
||||
* Markdown 产物右上角出现"发布入库"按钮。空/缺省 = 不支持入库。 */
|
||||
template_name?: string | null;
|
||||
/** 会商席位「技能识别强化」开关。作为会商席位运行时,是否把本智能体配置的技能
|
||||
* (名称 + SKILL.md 路径 + 必须先 read_file 的硬指令) 拼进本轮任务以补偿弱模型。
|
||||
* 与业务链条同名开关取 OR。null/false = 关 (默认)。 */
|
||||
seat_skill_directive?: boolean | null;
|
||||
tags?: Tag[];
|
||||
featured_order?: number | null;
|
||||
/** Whether the current user has favorited (收藏) this agent. */
|
||||
@@ -91,6 +95,8 @@ export interface CreateAgentRequest {
|
||||
tool_groups?: string[] | null;
|
||||
common_questions?: AgentQuestion[] | null;
|
||||
template_name?: string | null;
|
||||
/** 会商席位「技能识别强化」开关;缺省/false = 关。 */
|
||||
seat_skill_directive?: boolean | null;
|
||||
}
|
||||
|
||||
export interface UpdateAgentRequest {
|
||||
@@ -106,4 +112,6 @@ export interface UpdateAgentRequest {
|
||||
common_questions?: AgentQuestion[] | null;
|
||||
/** 入库模板名;传空字符串表示清除。 */
|
||||
template_name?: string | null;
|
||||
/** 会商席位「技能识别强化」开关;缺省/false = 关。 */
|
||||
seat_skill_directive?: boolean | null;
|
||||
}
|
||||
@@ -83,6 +83,8 @@ export default function NewAgentPage() {
|
||||
const [modelInput, setModelInput] = useState<string>("");
|
||||
const [chatModelInput, setChatModelInput] = useState<string>("");
|
||||
const [templateNameInput, setTemplateNameInput] = useState("");
|
||||
// 会商席位「技能识别强化」开关(弱模型补偿)。默认关。
|
||||
const [seatSkillDirectiveInput, setSeatSkillDirectiveInput] = useState(false);
|
||||
const [formError, setFormError] = useState("");
|
||||
const [agentId, setAgentId] = useState("");
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
@@ -230,6 +232,7 @@ export default function NewAgentPage() {
|
||||
skills: selectedSkills,
|
||||
model: modelOverride,
|
||||
template_name: templateNameInput.trim(),
|
||||
seat_skill_directive: seatSkillDirectiveInput,
|
||||
});
|
||||
setAgent(updated);
|
||||
return updated;
|
||||
@@ -251,6 +254,7 @@ export default function NewAgentPage() {
|
||||
skills: selectedSkills,
|
||||
model: modelOverride,
|
||||
template_name: templateNameInput.trim(),
|
||||
seat_skill_directive: seatSkillDirectiveInput,
|
||||
});
|
||||
setAgentId(created.id);
|
||||
setAgent(created);
|
||||
@@ -281,6 +285,7 @@ export default function NewAgentPage() {
|
||||
selectedSkills,
|
||||
soulInput,
|
||||
templateNameInput,
|
||||
seatSkillDirectiveInput,
|
||||
t.agents.nameStepCheckError,
|
||||
t.agents.nameStepNetworkError,
|
||||
validateForm,
|
||||
|
||||
@@ -234,6 +234,12 @@ export interface StreamMultiAgentRequest {
|
||||
reasoningEffort?: "minimal" | "low" | "medium" | "high";
|
||||
/** 仅 special 用;ultra 档置真,给席位解锁 `task` 子代理工具。 */
|
||||
subagentEnabled?: boolean;
|
||||
/**
|
||||
* 仅 special 用。会商席位「技能识别强化」开关(沿用所选业务链条配置)。置 true 时后端把该
|
||||
* 席位配置的技能(含 SKILL.md 路径 + 必须先 read_file 的硬指令)拼进本轮任务,补偿弱模型。
|
||||
* 与该席位 agent 自身 config.yaml 的同名开关取 OR。省略 = false(关)。
|
||||
*/
|
||||
seatSkillDirective?: boolean;
|
||||
skillStopNames?: string[];
|
||||
/**
|
||||
* 仅 leader 用。置 true 表示这是「派活后让总控判断继续/综合」的轮次:后端会把各已交付
|
||||
@@ -485,6 +491,8 @@ export async function streamMultiAgent(
|
||||
thinking_enabled: req.thinkingEnabled,
|
||||
reasoning_effort: req.reasoningEffort,
|
||||
subagent_enabled: req.subagentEnabled,
|
||||
// 仅 special 用;会商技能识别强化(沿用所选业务链条开关)。省略 → 后端按 false(关)处理。
|
||||
seat_skill_directive: req.seatSkillDirective ?? false,
|
||||
skill_stop_names: req.skillStopNames ?? [],
|
||||
// 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。
|
||||
synthesis_mode: req.synthesisMode ?? false,
|
||||
|
||||
@@ -98,6 +98,9 @@ function toStartBody(p: StartJobPayload) {
|
||||
seedMessage: p.seedMessage,
|
||||
threadIds: p.threadIds ?? null,
|
||||
coordinatorName: p.coordinatorName ?? "roundtable-coordinator",
|
||||
// 每席位技能识别强化映射 {agentId: bool}:沿用所选业务链条里各席位的开关;缺省 {}。
|
||||
// 后端 run_seat 按 agentId 查表,与各席位 agent 自身开关取 OR。
|
||||
seat_skill_directives: p.seatSkillDirectives ?? {},
|
||||
...(seat
|
||||
? {
|
||||
thinking_enabled: seat.thinking_enabled,
|
||||
|
||||
@@ -150,18 +150,21 @@ export function BusinessChainPicker({
|
||||
// 席位模型覆盖(链条配置):按 agent_id 取,空/缺省 = 跟随全局模型。
|
||||
const modelById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.model ?? null]));
|
||||
const modelOf = (id: string) => modelById.get(id) ?? null;
|
||||
// 席位「会商技能识别强化」开关(链条配置):按 agent_id 取,缺省 = 关。
|
||||
const skillById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.seat_skill_directive ?? false]));
|
||||
const skillOf = (id: string) => skillById.get(id) ?? false;
|
||||
|
||||
// 链条带分层、过滤存活后仍并行 → dag 编排(携带快速/沙箱子模式);否则退回线性 chain。
|
||||
if (dagPlan) {
|
||||
// dag:席位顺序按 stage 展平(与 plan 一致)。
|
||||
const agents = dagPlan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id) }));
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id) }));
|
||||
onConfirm({ agents, mode: "dag", chain: chainMeta, plan: dagPlan, dagExecMode });
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id) }));
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id) }));
|
||||
onConfirm({ agents, mode: "chain", chain: chainMeta });
|
||||
};
|
||||
|
||||
|
||||
@@ -279,6 +279,12 @@ export interface UseStep2OrchestrationOptions {
|
||||
* 智能体」弹窗确认时的 `SeatSelectionResult.seatMode` 决定。缺省 `pro`。
|
||||
*/
|
||||
selectedMode: SeatMode;
|
||||
/**
|
||||
* 每席位「技能识别强化」开关映射 {agentId: bool}(沿用所选业务链条里每个席位的配置)。
|
||||
* 按 agentId 查表后透传给该 special 席位 run,与该席位 agent 自身 config.yaml 的同名开关取
|
||||
* OR。recommend / 未选链条 → 空 {}(各席位仅靠其自身开关)。
|
||||
*/
|
||||
seatSkillDirectives?: Record<string, boolean>;
|
||||
/**
|
||||
* 编排模式:
|
||||
* - `recommend`(默认)—— 总控自由派活循环(`runOrchestration`);
|
||||
@@ -434,6 +440,7 @@ export function useStep2Orchestration(
|
||||
currentStep,
|
||||
selectedModel,
|
||||
selectedMode,
|
||||
seatSkillDirectives = {},
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
dagExecMode = "fast",
|
||||
@@ -567,6 +574,8 @@ export function useStep2Orchestration(
|
||||
const selectedModelRef = useRef<string>(selectedModel);
|
||||
// 席位执行模式同样打到 ref,让席位调用在编排循环里读到最新值而不重建回调。
|
||||
const selectedModeRef = useRef<SeatMode>(selectedMode);
|
||||
// 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。
|
||||
const seatSkillDirectivesRef = useRef<Record<string, boolean>>(seatSkillDirectives);
|
||||
const agentConfigsRef = useRef<Record<number, AgentConfig>>(agentConfigs);
|
||||
const agentNameToRoleRef = useRef(agentNameToRole);
|
||||
const threadIdsRef = useRef<ThreadIdMap | null>(threadIds);
|
||||
@@ -606,6 +615,9 @@ export function useStep2Orchestration(
|
||||
useEffect(() => {
|
||||
selectedModeRef.current = selectedMode;
|
||||
}, [selectedMode]);
|
||||
useEffect(() => {
|
||||
seatSkillDirectivesRef.current = seatSkillDirectives;
|
||||
}, [seatSkillDirectives]);
|
||||
useEffect(() => {
|
||||
agentConfigsRef.current = agentConfigs;
|
||||
}, [agentConfigs]);
|
||||
@@ -1602,6 +1614,9 @@ export function useStep2Orchestration(
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
// 席位执行模式(快速/思考/专业问答/多智能体问答)→ thinking/reasoning/subagent。
|
||||
...seatModeToRequestFields(selectedModeRef.current),
|
||||
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该席位
|
||||
// agent 自身开关取 OR。
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[a.agentId] ?? false,
|
||||
signal: subAborter.signal,
|
||||
onTextDelta: (text) =>
|
||||
a.isParallel ? ingestParallelDelta(a.bubbleId, text) : appendToDialogue(a.bubbleId, text),
|
||||
@@ -2031,6 +2046,9 @@ export function useStep2Orchestration(
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
// 席位执行模式 → thinking/reasoning/subagent(与推荐/DAG 路径一致)。
|
||||
...seatModeToRequestFields(selectedModeRef.current),
|
||||
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该席位
|
||||
// agent 自身开关取 OR。
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[seat.agent_id] ?? false,
|
||||
signal: subAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(subBubbleId, text),
|
||||
onPhaseChange: (phase, toolName, args) => {
|
||||
@@ -2371,6 +2389,9 @@ export function useStep2Orchestration(
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
// 席位执行模式 → thinking/reasoning/subagent(与推荐/链条路径一致)。
|
||||
...seatModeToRequestFields(selectedModeRef.current),
|
||||
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该
|
||||
// 席位 agent 自身开关取 OR。
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[agentId] ?? false,
|
||||
signal: subAborter.signal,
|
||||
// §6:并行席位走 ref 全量累积 + 节流 flush(只渲染选中的);单节点
|
||||
// stage 实际串行,1 路流直接 setState 不卡,沿用 appendToDialogue。
|
||||
|
||||
@@ -29,6 +29,13 @@ export interface ChainSeat {
|
||||
* 的 `handleConfirmAgents`)→ 走现有 per-seat 模型链路。
|
||||
*/
|
||||
model?: string | null;
|
||||
/**
|
||||
* 可选「会商技能识别强化」开关(弱模型补偿)。true = 该席位作为会商子智能体执行时,把其
|
||||
* 配置的技能(名称 + SKILL.md 路径 + 必须先 read_file 的硬指令)拼进本轮任务。与该席位
|
||||
* 智能体自身的同名开关(智能体编辑里的开关)取 OR。缺省 / false = 关。随 `seats` JSON 落库
|
||||
* (无需 DB 列),在链条编辑器里逐席位开关。
|
||||
*/
|
||||
seat_skill_directive?: boolean;
|
||||
}
|
||||
|
||||
/** 一条可复用的业务链条(与后端 `/api/roundtable-chains` 记录对应)。 */
|
||||
@@ -80,8 +87,9 @@ export interface SeatSelectionResult {
|
||||
/**
|
||||
* 有序席位 —— 与现有 `handleConfirmAgents` 入参兼容。dag 模式下按 stage 展平排序。
|
||||
* `model` 为该席位在来源链条里配置的模型覆盖(空 / 缺省 = 跟随全局模型)。
|
||||
* `seat_skill_directive` 为该席位在来源链条里配置的「技能识别强化」开关(缺省 = 关)。
|
||||
*/
|
||||
agents: Array<{ agent_id: string; name: string; model?: string | null }>;
|
||||
agents: Array<{ agent_id: string; name: string; model?: string | null; seat_skill_directive?: boolean }>;
|
||||
/**
|
||||
* 决定 Step 2 走哪种编排:
|
||||
* - `recommend` 总控自由派活;
|
||||
@@ -135,6 +143,9 @@ export function buildChainSeatSelection(
|
||||
// 席位模型覆盖(链条配置):按 agent_id 取,空/缺省 = 跟随全局。
|
||||
const modelById = new Map(chain.seats.map((s) => [s.agent_id, s.model ?? null]));
|
||||
const modelOf = (id: string) => modelById.get(id) ?? null;
|
||||
// 席位「技能识别强化」开关(链条配置):按 agent_id 取,缺省 = 关。
|
||||
const skillById = new Map(chain.seats.map((s) => [s.agent_id, s.seat_skill_directive ?? false]));
|
||||
const skillOf = (id: string) => skillById.get(id) ?? false;
|
||||
|
||||
const survivingSeats = chain.seats.filter((s) => existingIds.has(s.agent_id));
|
||||
if (survivingSeats.length < CHAIN_MIN_SEATS) return null;
|
||||
@@ -156,12 +167,12 @@ export function buildChainSeatSelection(
|
||||
if (hasParallel || hasGoal || hasCoord) {
|
||||
const agents = plan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id) }));
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id) }));
|
||||
return { agents, mode: "dag", chain: chainMeta, plan, dagExecMode: "fast", seatMode: chain.seatMode };
|
||||
}
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id) }));
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id) }));
|
||||
return { agents, mode: "chain", chain: chainMeta, seatMode: chain.seatMode };
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,20 @@ export function setSeatModel(groups: StageGroups, agentId: string, model: string
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个席位的「会商技能识别强化」开关(不可变,供编辑器席位下拉 onChange)。
|
||||
* 按 agent_id 定位,跨 stage 适用。随 `seats` JSON 落库。
|
||||
*/
|
||||
export function setSeatSkillDirective(groups: StageGroups, agentId: string, enabled: boolean): StageGroups {
|
||||
return groups.map((g) => {
|
||||
if (!g.seats.some((s) => s.agent_id === agentId)) return g;
|
||||
return {
|
||||
...g,
|
||||
seats: g.seats.map((s) => (s.agent_id === agentId ? { ...s, seat_skill_directive: enabled } : s)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ── 变换(均返回新结构,不可变)──────────────────────────────────────────────
|
||||
|
||||
/** 末尾追加一个**新单席位 stage**(线性追加,pool 点击加入默认行为)。 */
|
||||
|
||||
@@ -132,6 +132,11 @@ export interface StartJobPayload {
|
||||
* reasoning/subagent 三字段发给后端。
|
||||
*/
|
||||
seatMode?: SeatMode;
|
||||
/**
|
||||
* 每席位「技能识别强化」开关映射 {agentId: bool}(沿用所选业务链条里每个席位的配置)。
|
||||
* 后台席位 run 按 agentId 查表(与各席位智能体自身的同名开关取 OR)决定是否注入。缺省 {}。
|
||||
*/
|
||||
seatSkillDirectives?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/** 终态集合——SSE/轮询判定作业是否结束。 */
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
Plus,
|
||||
Save,
|
||||
Sliders,
|
||||
Sparkles,
|
||||
SplitSquareVertical,
|
||||
Trash2,
|
||||
UserPlus,
|
||||
@@ -63,6 +64,12 @@ import {
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||
import { ModelNameSelect } from "@/components/workspace/model-name-select";
|
||||
@@ -105,6 +112,7 @@ import {
|
||||
removeSeat as removeSeatFromGroups,
|
||||
seatExistsInGroups,
|
||||
setSeatModel,
|
||||
setSeatSkillDirective,
|
||||
setStageGoal,
|
||||
splitSeatToNewStage,
|
||||
stageGroupsToSeats,
|
||||
@@ -366,6 +374,11 @@ export default function BusinessChainEditorPage() {
|
||||
setStageGroups((prev) => setSeatModel(prev, agentId, model));
|
||||
}, []);
|
||||
|
||||
// 席位「会商技能识别强化」开关:编辑器席位下拉 onChange。
|
||||
const setSeatSkillDirectiveCb = useCallback((agentId: string, enabled: boolean) => {
|
||||
setStageGroups((prev) => setSeatSkillDirective(prev, agentId, enabled));
|
||||
}, []);
|
||||
|
||||
// ── 提示词 AI 优化(总控提示词 / 各 Stage 阶段目标)────────────────────────
|
||||
const [optimizingCoordinator, setOptimizingCoordinator] = useState(false);
|
||||
const [goalOptimizingIdx, setGoalOptimizingIdx] = useState<number | null>(null);
|
||||
@@ -564,6 +577,8 @@ export default function BusinessChainEditorPage() {
|
||||
description: live?.description ?? s.description ?? "",
|
||||
// 席位模型覆盖:随席位一起落库(空 → null = 跟随全局)。
|
||||
model: s.model ?? null,
|
||||
// 席位「会商技能识别强化」开关:随席位一起落库(缺省 = false 关)。
|
||||
seat_skill_directive: s.seat_skill_directive ?? false,
|
||||
};
|
||||
});
|
||||
// 总控编排提示:trim 后空串落 null(清空),与后端「空=不注入」一致。
|
||||
@@ -1131,6 +1146,7 @@ export default function BusinessChainEditorPage() {
|
||||
onMergeSeat={mergeSeatToStage}
|
||||
onSplitSeat={splitSeat}
|
||||
onSeatModelChange={setSeatModelCb}
|
||||
onSeatSkillDirectiveChange={setSeatSkillDirectiveCb}
|
||||
onStageGoalChange={setGoal}
|
||||
goalOptimizingIdx={goalOptimizingIdx}
|
||||
onOptimizeGoal={handleOptimizeStageGoal}
|
||||
@@ -1309,6 +1325,7 @@ function StagesColumn({
|
||||
onMergeSeat,
|
||||
onSplitSeat,
|
||||
onSeatModelChange,
|
||||
onSeatSkillDirectiveChange,
|
||||
onStageGoalChange,
|
||||
goalOptimizingIdx,
|
||||
onOptimizeGoal,
|
||||
@@ -1325,6 +1342,7 @@ function StagesColumn({
|
||||
onMergeSeat: (agentId: string, targetStageIdx: number) => void;
|
||||
onSplitSeat: (agentId: string) => void;
|
||||
onSeatModelChange: (agentId: string, model: string | null) => void;
|
||||
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
|
||||
onStageGoalChange: (stageIdx: number, goal: string) => void;
|
||||
/** 正在 AI 优化阶段目标的 stage 下标(null = 没有进行中的优化)。 */
|
||||
goalOptimizingIdx: number | null;
|
||||
@@ -1423,6 +1441,7 @@ function StagesColumn({
|
||||
onMergeSeat={onMergeSeat}
|
||||
onSplitSeat={onSplitSeat}
|
||||
onSeatModelChange={onSeatModelChange}
|
||||
onSeatSkillDirectiveChange={onSeatSkillDirectiveChange}
|
||||
onGoalChange={(goal) => onStageGoalChange(stageIdx, goal)}
|
||||
goalOptimizing={goalOptimizingIdx === stageIdx}
|
||||
anyGoalOptimizing={goalOptimizingIdx !== null}
|
||||
@@ -1452,6 +1471,7 @@ function StageSection({
|
||||
onMergeSeat,
|
||||
onSplitSeat,
|
||||
onSeatModelChange,
|
||||
onSeatSkillDirectiveChange,
|
||||
onGoalChange,
|
||||
goalOptimizing,
|
||||
anyGoalOptimizing,
|
||||
@@ -1470,6 +1490,7 @@ function StageSection({
|
||||
onMergeSeat: (agentId: string, targetStageIdx: number) => void;
|
||||
onSplitSeat: (agentId: string) => void;
|
||||
onSeatModelChange: (agentId: string, model: string | null) => void;
|
||||
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
|
||||
onGoalChange: (goal: string) => void;
|
||||
/** 本 Stage 的阶段目标正在 AI 优化中。 */
|
||||
goalOptimizing: boolean;
|
||||
@@ -1631,6 +1652,7 @@ function StageSection({
|
||||
onMergeDown={() => onMergeSeat(seat.agent_id, stageIdx + 1)}
|
||||
onSplit={() => onSplitSeat(seat.agent_id)}
|
||||
onModelChange={(model) => onSeatModelChange(seat.agent_id, model)}
|
||||
onSkillDirectiveChange={(enabled) => onSeatSkillDirectiveChange(seat.agent_id, enabled)}
|
||||
onDetail={() => onSeatDetail(seat.agent_id, seat.name)}
|
||||
/>
|
||||
))}
|
||||
@@ -1654,6 +1676,7 @@ function SeatStrip({
|
||||
onMergeDown,
|
||||
onSplit,
|
||||
onModelChange,
|
||||
onSkillDirectiveChange,
|
||||
onDetail,
|
||||
}: {
|
||||
seat: ChainSeat;
|
||||
@@ -1668,6 +1691,8 @@ function SeatStrip({
|
||||
onSplit: () => void;
|
||||
/** 席位模型覆盖变更(null = 清除,跟随全局)。 */
|
||||
onModelChange: (model: string | null) => void;
|
||||
/** 席位「会商技能识别强化」开关变更。 */
|
||||
onSkillDirectiveChange: (enabled: boolean) => void;
|
||||
onDetail: () => void;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
@@ -1739,8 +1764,41 @@ function SeatStrip({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 控制:查看详情 / 并入上一阶段 / 独立成阶段 / 并入下一阶段 / 移除 */}
|
||||
{/* 控制:技能识别强化 / 查看详情 / 并入上一阶段 / 独立成阶段 / 并入下一阶段 / 移除 */}
|
||||
<div className="flex shrink-0 items-center gap-0.5 border-l border-border px-2">
|
||||
{/* 会商技能识别强化:小图标 → 下拉框(开关 + 说明)。开启 = 高亮。 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
title="会商技能识别强化(弱模型补偿)"
|
||||
className={cn(
|
||||
"p-1 transition",
|
||||
R_CTRL,
|
||||
seat.seat_skill_directive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs font-semibold text-foreground">会商技能识别强化</p>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
开启后,该席位作为会商子智能体执行时,会把已配置的技能(含 SKILL.md
|
||||
路径与「先 read_file 再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在该智能体编辑里打开(任一即生效)。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={seat.seat_skill_directive ?? false}
|
||||
onCheckedChange={onSkillDirectiveChange}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDetail}
|
||||
|
||||
@@ -312,6 +312,9 @@ export default function RoundtablePlanningPage() {
|
||||
// Step 2 席位执行模式(快速/思考/专业问答/多智能体问答)——决定每个子智能体席位的
|
||||
// 推理深度。由「选择参会智能体」弹窗确认时的 SeatSelectionResult.seatMode 决定。
|
||||
const [seatMode, setSeatMode] = useState<SeatMode>(DEFAULT_SEAT_MODE);
|
||||
// Step 2 每席位「技能识别强化」开关映射 {agentId: bool}:由所选业务链条里**每个席位**的同名
|
||||
// 配置决定(recommend 无链条 → 空 {})。透传给每个席位 run,与各席位智能体自身的开关取 OR。
|
||||
const [seatSkillDirectives, setSeatSkillDirectives] = useState<Record<string, boolean>>({});
|
||||
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活;
|
||||
// dag = 分层 DAG(stage 内并行、stage 间串行,配合 orchestrationPlan)。
|
||||
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
|
||||
@@ -467,6 +470,7 @@ export default function RoundtablePlanningPage() {
|
||||
currentStep,
|
||||
selectedModel,
|
||||
selectedMode: seatMode,
|
||||
seatSkillDirectives,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
dagExecMode,
|
||||
@@ -944,6 +948,11 @@ export default function RoundtablePlanningPage() {
|
||||
setOrchestrationMode(result.mode);
|
||||
// 席位执行模式:弹窗未带(旧调用)时回退默认 pro,保证行为可预期。
|
||||
setSeatMode(result.seatMode ?? DEFAULT_SEAT_MODE);
|
||||
// 会商技能识别强化:取所选业务链条里**每席位**的开关 → {agentId: bool} 映射
|
||||
// (recommend / 旧调用未带 → 空 {},各席位仅靠其智能体自身开关)。
|
||||
setSeatSkillDirectives(
|
||||
Object.fromEntries(result.agents.map((a) => [a.agent_id, a.seat_skill_directive ?? false])),
|
||||
);
|
||||
setChainMeta(result.mode === "recommend" ? null : result.chain ?? null);
|
||||
setOrchestrationPlan(result.mode === "dag" ? result.plan ?? null : null);
|
||||
setDagExecMode(result.mode === "dag" ? result.dagExecMode ?? "fast" : "fast");
|
||||
@@ -1518,6 +1527,7 @@ export default function RoundtablePlanningPage() {
|
||||
agents,
|
||||
model: selectedModel || undefined, // 模型沿用第二步选择
|
||||
seatMode, // 席位执行模式沿用第二步选择(后台续跑同样按此模式跑席位)
|
||||
seatSkillDirectives, // 每席位技能识别强化沿用所选业务链条里各席位的开关
|
||||
// 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)——
|
||||
// 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。
|
||||
orchestrationMode,
|
||||
@@ -1534,6 +1544,7 @@ export default function RoundtablePlanningPage() {
|
||||
drafts,
|
||||
selectedModel,
|
||||
seatMode,
|
||||
seatSkillDirectives,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
chainMeta,
|
||||
@@ -1557,6 +1568,7 @@ export default function RoundtablePlanningPage() {
|
||||
const intent = buildInitialIntent();
|
||||
const model = selectedModel || undefined;
|
||||
const seatModeSnap = seatMode;
|
||||
const seatSkillDirectivesSnap = seatSkillDirectives;
|
||||
const mode = orchestrationMode;
|
||||
const chainSnap = chainMeta;
|
||||
const planSnap = mode === "dag" ? orchestrationPlan : null;
|
||||
@@ -1591,6 +1603,7 @@ export default function RoundtablePlanningPage() {
|
||||
agents,
|
||||
model,
|
||||
seatMode: seatModeSnap,
|
||||
seatSkillDirectives: seatSkillDirectivesSnap,
|
||||
orchestrationMode: mode,
|
||||
chain: chainSnap,
|
||||
orchestrationPlan: planSnap,
|
||||
@@ -1614,6 +1627,7 @@ export default function RoundtablePlanningPage() {
|
||||
buildInitialIntent,
|
||||
selectedModel,
|
||||
seatMode,
|
||||
seatSkillDirectives,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
chainMeta,
|
||||
@@ -1654,6 +1668,7 @@ export default function RoundtablePlanningPage() {
|
||||
agents,
|
||||
model: selectedModel || undefined,
|
||||
seatMode,
|
||||
seatSkillDirectives,
|
||||
orchestrationMode,
|
||||
chain: chainMeta,
|
||||
orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
|
||||
|
||||
@@ -247,7 +247,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA
|
||||
| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. |
|
||||
| **Roundtable Drafts** (`/api/roundtable-drafts`) | **Strictly per-user** persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete (all owner-scoped). A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). taskId 深链会商记录 are **not** here — they live in the separate `/api/roundtable-task-drafts` store (see below) so personal history never mixes with task records. Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. Tests: `tests/test_roundtable_task_drafts.py`. |
|
||||
| **Roundtable Task Drafts** (`/api/roundtable-task-drafts`) | **Task-scoped, unpartitioned** store for taskId-deeplink roundtable 聊天记录 (无界嵌入抽屉). Keyed **only by `task_id`** (one task → many records), **never partitioned by user** — any authenticated caller may read/write/delete a task's records (`created_by` is audit-only). Physically separate from `roundtable_drafts` so the two never mix. `GET /by-task/{task_id}` - all drafts for a task, newest-first (history dropdown), enriched with the latest background-job status per draft via `RoundtableJobRepository.status_map_by_drafts`; `GET /by-task/{task_id}/latest` - newest draft (回显最新一条); `POST /` - create (`task_id` required); `GET/PUT/DELETE /{id}` - full read / partial update / delete (all user-agnostic). Backed by `deerflow.persistence.roundtable_task_drafts` (table `roundtable_task_drafts`, auto-created by `create_all`; migration `20260618_01`); store wired in `deps.py` as `app.state.roundtable_task_draft_store`. Existing `task_id`-bearing `roundtable_drafts` rows are migrated over by `scripts/migrate_roundtable_task_drafts.py`. Tests: `tests/test_roundtable_task_drafts.py`, `tests/test_roundtable_drafts_by_task.py`. |
|
||||
| **Roundtable Diagnostics** (`/api/roundtable-diagnostics`) | 会商全流程诊断日志 (报错 + 关键事件). One row per event in `roundtable_diagnostics`: `{scope (background = 后台挂起 / foreground = 前台服务端观测 / client = 前端上报), stage (step1_intent/step2_init/step2_leader/step2_seat/step2_recommend/step2_orchestration/step3_summary/step3_report/step3_dashboard/step3_structure/upstream/job), level (info/warning/error), event, message, detail (JSON), job_id/draft_id/task_id/user_id/agent_id/agent_name/cycle, created_at}`. **Reads are admin-only**: `GET /` - filter (scope/stage/level/event/job_id/draft_id/task_id/user_id/q/since/until; `scope` accepts a comma-separated set so 前端「前台调用」= `foreground,client`) + paginate (newest-first), each item enriched with `userName` (user_id → 邮箱 `@`-prefix via `get_local_provider().get_user`, best-effort batched cache); `GET /facets` - distinct values for filter dropdowns; `POST /admin/cleanup` - retention purge. **`POST /report` is NOT admin-gated** — any authenticated user's frontend posts the errors *the user actually hit* (HTTP 502 before any SSE frame / network drop / SSE error frame) so they're collected too; stored as `scope="client"`. Backed by `deerflow.persistence.roundtable_diagnostics` (table `roundtable_diagnostics`, auto-created by `create_all`; migration `20260619_01`); store wired in `deps.py` as `app.state.roundtable_diagnostic_store` + a process-level default store (`register_default_store`) so the harness-side engine/gateway can write across the app/harness boundary. **Writers** (all best-effort — diagnostics never break a run): background path uses `DiagnosticsRecorder` (bound to job ctx) threaded through `RoundtableJobExecutor` → `JobProgressWriter.diag(...)` (engine transitions/step3 failures/empty-delivery) + `InProcessRoundtableGateway` (per-turn timeout/report·summary·dashboard truncation+missing); foreground path uses `app/gateway/roundtable_diag.py::record_foreground_diag` in `multi_agent.py`/`intent.py`/`recommend.py` error frames. **Bug-fix shipped alongside**: `InProcessRoundtableGateway._run_agent_turn` guards each agent run with an **inactivity (idle) timeout** (`_await_run_with_idle_guard`, `ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS` default 1200s = 20min, tuned high for unstable intranet models) — **not** a total-duration cap, so a slow-but-alive run that keeps streaming tokens runs as long as it needs; only a run that emits **zero StreamBridge events for `idle_timeout` seconds straight** (a true freeze: 内网模型连接池挂死 / sqlite 锁在挂载卷上挂死) is cancelled + raises (顶层把 job 标 error,而非永久冻结) + records a `seat_turn_timeout` diagnostic. Activity is observed via `app.state.stream_bridge` (the worker publishes every chunk there). Optional absolute cap `ROUNDTABLE_SEAT_MAX_TOTAL_SECONDS` (default 0 = off). Frontend: admin page `pages/RoundtableDiagnosticsPage.tsx` (route `/page/strategy/admin/roundtable-diagnostics`), opened from the **会商页右上角设置按钮** (admin only — `handleSettingsClick` in `roundtable-planning/pages/RoundtablePlanningPage.tsx`); admin read client `strategy-components/api/roundtable-diagnostics.ts`. **Client error reporter** `roundtable-planning/api/diagnostics-report.ts` (`reportRoundtableError` fire-and-forget + `keepalive`, `setRoundtableErrorContext` for draft/task attribution set by the page, `stageForRoundtableAgent`) is called at every client-observable failure: `multi-agent.ts` (`streamMultiAgent` — covers Step2 leader/seat AND Step3 report/summary/dashboard/structure — reports HTTP error + SSE `{status:"error"}` frame + worker `event:error` frame [模型调用没成功] + fetch-reject [网络不可达] + mid-stream drop [连接重置/超时], deduped via a `reported` flag; `initMultiAgent` HTTP/error-frame/incomplete/mid-stream), `intent.ts` (Step1 HTTP+error-frame), `recommend.ts` (Step2 recommend HTTP+error-frame), `roundtable-jobs.ts` (job api/stream). The admin page's first filter is a **后台挂起 / 前台调用** toggle (frontend = `foreground,client`); the stage dropdown options adapt to it (background → step2+step3+job; frontend → step1+step2+step3) and each row shows the reporting user's name + 👤. Tests: `tests/test_roundtable_diagnostics.py`, `tests/test_roundtable_inprocess_timeout.py`. **Seat skill-recognition (弱模型补偿)**: a seat = one custom agent run whose configured skills (`agent config.yaml → skills`) are injected **only** via the system prompt's `<skill_system>` block (model must `read_file` the SKILL.md itself) — weak models (deepseek-chat 等) ignore it and never use their skills. `app/gateway/roundtable_seat_skills.py::append_seat_skill_directive(agent_id, task)` appends an explicit Chinese directive (each configured-and-enabled skill's name + description + SKILL.md container path + "先 read_file 再按其流程取信息/执行") to the **seat's per-turn tLine truncated
|
||||
| **Roundtable Diagnostics** (`/api/roundtable-diagnostics`) | 会商全流程诊断日志 (报错 + 关键事件). One row per event in `roundtable_diagnostics`: `{scope (background = 后台挂起 / foreground = 前台服务端观测 / client = 前端上报), stage (step1_intent/step2_init/step2_leader/step2_seat/step2_recommend/step2_orchestration/step3_summary/step3_report/step3_dashboard/step3_structure/upstream/job), level (info/warning/error), event, message, detail (JSON), job_id/draft_id/task_id/user_id/agent_id/agent_name/cycle, created_at}`. **Reads are admin-only**: `GET /` - filter (scope/stage/level/event/job_id/draft_id/task_id/user_id/q/since/until; `scope` accepts a comma-separated set so 前端「前台调用」= `foreground,client`) + paginate (newest-first), each item enriched with `userName` (user_id → 邮箱 `@`-prefix via `get_local_provider().get_user`, best-effort batched cache); `GET /facets` - distinct values for filter dropdowns; `POST /admin/cleanup` - retention purge. **`POST /report` is NOT admin-gated** — any authenticated user's frontend posts the errors *the user actually hit* (HTTP 502 before any SSE frame / network drop / SSE error frame) so they're collected too; stored as `scope="client"`. Backed by `deerflow.persistence.roundtable_diagnostics` (table `roundtable_diagnostics`, auto-created by `create_all`; migration `20260619_01`); store wired in `deps.py` as `app.state.roundtable_diagnostic_store` + a process-level default store (`register_default_store`) so the harness-side engine/gateway can write across the app/harness boundary. **Writers** (all best-effort — diagnostics never break a run): background path uses `DiagnosticsRecorder` (bound to job ctx) threaded through `RoundtableJobExecutor` → `JobProgressWriter.diag(...)` (engine transitions/step3 failures/empty-delivery) + `InProcessRoundtableGateway` (per-turn timeout/report·summary·dashboard truncation+missing); foreground path uses `app/gateway/roundtable_diag.py::record_foreground_diag` in `multi_agent.py`/`intent.py`/`recommend.py` error frames. **Bug-fix shipped alongside**: `InProcessRoundtableGateway._run_agent_turn` guards each agent run with an **inactivity (idle) timeout** (`_await_run_with_idle_guard`, `ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS` default 1200s = 20min, tuned high for unstable intranet models) — **not** a total-duration cap, so a slow-but-alive run that keeps streaming tokens runs as long as it needs; only a run that emits **zero StreamBridge events for `idle_timeout` seconds straight** (a true freeze: 内网模型连接池挂死 / sqlite 锁在挂载卷上挂死) is cancelled + raises (顶层把 job 标 error,而非永久冻结) + records a `seat_turn_timeout` diagnostic. Activity is observed via `app.state.stream_bridge` (the worker publishes every chunk there). Optional absolute cap `ROUNDTABLE_SEAT_MAX_TOTAL_SECONDS` (default 0 = off). Frontend: admin page `pages/RoundtableDiagnosticsPage.tsx` (route `/page/strategy/admin/roundtable-diagnostics`), opened from the **会商页右上角设置按钮** (admin only — `handleSettingsClick` in `roundtable-planning/pages/RoundtablePlanningPage.tsx`); admin read client `strategy-components/api/roundtable-diagnostics.ts`. **Client error reporter** `roundtable-planning/api/diagnostics-report.ts` (`reportRoundtableError` fire-and-forget + `keepalive`, `setRoundtableErrorContext` for draft/task attribution set by the page, `stageForRoundtableAgent`) is called at every client-observable failure: `multi-agent.ts` (`streamMultiAgent` — covers Step2 leader/seat AND Step3 report/summary/dashboard/structure — reports HTTP error + SSE `{status:"error"}` frame + worker `event:error` frame [模型调用没成功] + fetch-reject [网络不可达] + mid-stream drop [连接重置/超时], deduped via a `reported` flag; `initMultiAgent` HTTP/error-frame/incomplete/mid-stream), `intent.ts` (Step1 HTTP+error-frame), `recommend.ts` (Step2 recommend HTTP+error-frame), `roundtable-jobs.ts` (job api/stream). The admin page's first filter is a **后台挂起 / 前台调用** toggle (frontend = `foreground,client`); the stage dropdown options adapt to it (background → step2+step3+job; frontend → step1+step2+step3) and each row shows the reporting user's name + 👤. Tests: `tests/test_roundtable_diagnostics.py`, `tests/test_roundtable_inprocess_timeout.py`. **Seat skill-recognition (弱模型补偿)**: a seat = one custom agent run whose configured skills (`agent config.yaml → skills`) are injected **only** via the system prompt's `<skill_system>` block (model must `read_file` the SKILL.md itself) — weak models (deepseek-chat 等) ignore it and never use their skills. `app/gateway/roundtable_seat_skills.py::append_seat_skill_directive(agent_id, task, *, chain_enabled=False)` appends an explicit Chinese directive (each configured-and-enabled skill's name + description + SKILL.md container path + "先 read_file 再按其流程取信息/执行") toLine truncated
|
||||
| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_<sha1>` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. |
|
||||
| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/<code>`. Missing/revoked/non-`view` codes all return 404. |
|
||||
| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. |
|
||||
|
||||
@@ -339,6 +339,7 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
seat_thinking_enabled=p.seat_thinking_enabled,
|
||||
seat_reasoning_effort=p.seat_reasoning_effort,
|
||||
seat_subagent_enabled=p.seat_subagent_enabled,
|
||||
seat_skill_directives=p.seat_skill_directives,
|
||||
# 诊断日志上下文:网关每轮 leader/seat/report/summary/dashboard 报错均带这些归属。
|
||||
job_id=p.job_id,
|
||||
draft_id=p.draft_id,
|
||||
|
||||
@@ -525,6 +525,7 @@ class InProcessRoundtableGateway:
|
||||
seat_thinking_enabled: bool | None = None,
|
||||
seat_reasoning_effort: str | None = None,
|
||||
seat_subagent_enabled: bool = False,
|
||||
seat_skill_directives: dict[str, bool] | None = None,
|
||||
job_id: str | None = None,
|
||||
draft_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
@@ -547,6 +548,10 @@ class InProcessRoundtableGateway:
|
||||
self._seat_thinking_enabled = seat_thinking_enabled
|
||||
self._seat_reasoning_effort = seat_reasoning_effort
|
||||
self._seat_subagent_enabled = seat_subagent_enabled
|
||||
# 每席位「技能识别强化」开关映射 {agent_id: bool}(沿用 Step2 所选业务链条里每个席位
|
||||
# 的配置)。run_seat 按 agent_id 查表,与各席位 agent 自身的 config.yaml 开关取 OR,
|
||||
# 传给 append_seat_skill_directive。默认空 dict(仅各 agent 自身开关生效)。
|
||||
self._seat_skill_directives = dict(seat_skill_directives or {})
|
||||
self._seat_intro = ""
|
||||
self._first_leader = True
|
||||
|
||||
@@ -615,8 +620,12 @@ class InProcessRoundtableGateway:
|
||||
policy = roundtable_run_policy("seat")
|
||||
# 弱模型补偿:把该席位配置的技能(名称 + SKILL.md 路径 + 必须先 read_file 再按其
|
||||
# 流程取信息的硬指令)显式拼进本轮任务,避免弱模型无视系统提示里的技能块(见
|
||||
# roundtable_seat_skills 模块说明)。无配置技能时原样返回 task。
|
||||
seat_task = append_seat_skill_directive(agent_id, task)
|
||||
# roundtable_seat_skills 模块说明)。默认关:是否注入 = 该席位 agent 的 config.yaml
|
||||
# 开关 OR 该席位在所选业务链条里的开关(按 agent_id 查表)。无配置技能 / 两开关都关时
|
||||
# 原样返回 task。
|
||||
seat_task = append_seat_skill_directive(
|
||||
agent_id, task, chain_enabled=bool(self._seat_skill_directives.get(agent_id, False))
|
||||
)
|
||||
# 席位模式覆盖 policy 的 thinking/reasoning(None → policy 默认),与前端 _special_run 一致。
|
||||
messages = await _run_agent_turn(
|
||||
self._app,
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -189,6 +189,10 @@ class StartParams:
|
||||
seat_thinking_enabled: bool | None = None
|
||||
seat_reasoning_effort: str | None = None
|
||||
seat_subagent_enabled: bool = False
|
||||
# 每席位「技能识别强化」开关映射 {agent_id: bool}(沿用 Step2 所选业务链条里每个席位的
|
||||
# 配置)。透传给进程内网关,run_seat 按 agent_id 查表,与各席位 agent 自身 config.yaml 的
|
||||
# 同名开关取 OR。默认空 dict。
|
||||
seat_skill_directives: dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
|
||||
# 网关工厂:给定一次启动参数,返回一个 RoundtableGateway 实例。
|
||||
|
||||
@@ -11,12 +11,20 @@
|
||||
强得多——这与本仓已有的弱模型补偿手法(``_SEAT_NO_ASK``、总控派活强化、AI 写作 SKILL.md
|
||||
注入,见 ``project-ai-writing-skill-agent``)一脉相承。
|
||||
|
||||
**默认关闭、按需开启(2026-06-20 改造)**:不再全域生效。是否对某席位注入由两个开关
|
||||
取 **OR** 决定——
|
||||
1. **智能体级**:该席位 agent 的 ``config.yaml → seat_skill_directive``(智能体编辑页
|
||||
可开关);
|
||||
2. **业务链条级**:本次会商所选业务链条的同名开关,由调用方读出后以 ``chain_enabled``
|
||||
传入(前端手动与后台挂起两条路径都按席位透传)。
|
||||
任一为真即注入;两者都假 → 返回空串、原样使用任务文本(零副作用)。
|
||||
|
||||
两条席位执行路径共用本 helper:
|
||||
- 前端手动:``multi_agent._special_run``(seat / seat_parallel 角色);
|
||||
- 后台挂起:``roundtable_inprocess_gateway.InProcessRoundtableGateway.run_seat``。
|
||||
|
||||
无配置技能的席位 → 返回空串(完全无副作用)。可用 ``ROUNDTABLE_SEAT_SKILL_DIRECTIVE=0``
|
||||
全局关闭(默认开)。
|
||||
``ROUNDTABLE_SEAT_SKILL_DIRECTIVE`` 仅作**总开关(kill-switch)**:默认放行,显式设为
|
||||
0/false 时彻底关闭本功能(不论两个开关如何),用于线上一键熔断。它**不再** force-on。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,25 +35,36 @@ import os
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TRUTHY = frozenset({"1", "true", "yes", "on", "y"})
|
||||
_FALSY = frozenset({"0", "false", "no", "off", "n"})
|
||||
|
||||
|
||||
def _directive_enabled() -> bool:
|
||||
"""读 ``ROUNDTABLE_SEAT_SKILL_DIRECTIVE``(默认开)。空 / 未设 = 开。"""
|
||||
return (os.getenv("ROUNDTABLE_SEAT_SKILL_DIRECTIVE") or "1").strip().lower() in _TRUTHY
|
||||
def _master_enabled() -> bool:
|
||||
"""``ROUNDTABLE_SEAT_SKILL_DIRECTIVE`` 总开关:默认放行(kill-switch)。
|
||||
|
||||
空 / 未设 / 非法 = 放行;仅显式 0/false/no/off/n 时整体关闭。注意它**只熔断不强开**,
|
||||
具体是否注入仍由 agent / chain 两个开关取 OR 决定。
|
||||
"""
|
||||
return (os.getenv("ROUNDTABLE_SEAT_SKILL_DIRECTIVE") or "").strip().lower() not in _FALSY
|
||||
|
||||
|
||||
def build_seat_skill_directive(agent_id: str | None) -> str:
|
||||
def build_seat_skill_directive(agent_id: str | None, *, chain_enabled: bool = False) -> str:
|
||||
"""为某席位 agent 构建「必须先读 SKILL.md 再用技能」的本轮任务追加指令。
|
||||
|
||||
返回 ``""`` 表示无需追加(开关关闭 / 无 agent_id / 该席位没有配置技能 / 解析失败),
|
||||
调用方原样使用任务文本,零副作用。
|
||||
是否注入由 **agent 开关(config.yaml)OR chain 开关(``chain_enabled``)** 决定,
|
||||
默认两者皆关 → 不注入。返回 ``""`` 表示无需追加(总开关熔断 / 两开关都关 / 无 agent_id /
|
||||
该席位没有配置技能 / 解析失败),调用方原样使用任务文本,零副作用。
|
||||
"""
|
||||
if not _directive_enabled() or not agent_id:
|
||||
if not _master_enabled() or not agent_id:
|
||||
return ""
|
||||
try:
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
cfg = load_agent_config(agent_id)
|
||||
# agent 级开关 OR chain 级开关(默认关)。任一打开才继续,否则零副作用返回。
|
||||
agent_enabled = bool(cfg and cfg.seat_skill_directive)
|
||||
if not (agent_enabled or chain_enabled):
|
||||
return ""
|
||||
|
||||
configured = [name for name in (cfg.skills or []) if name] if cfg else []
|
||||
if not configured:
|
||||
return ""
|
||||
@@ -87,9 +106,12 @@ def build_seat_skill_directive(agent_id: str | None) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def append_seat_skill_directive(agent_id: str | None, task: str) -> str:
|
||||
"""把技能识别指令追加到席位本轮任务文本后(无指令时原样返回)。"""
|
||||
directive = build_seat_skill_directive(agent_id)
|
||||
def append_seat_skill_directive(agent_id: str | None, task: str, *, chain_enabled: bool = False) -> str:
|
||||
"""把技能识别指令追加到席位本轮任务文本后(无指令时原样返回)。
|
||||
|
||||
``chain_enabled``:本次会商所选业务链条是否打开「技能识别强化」。与 agent 级开关取 OR。
|
||||
"""
|
||||
directive = build_seat_skill_directive(agent_id, chain_enabled=chain_enabled)
|
||||
if not directive:
|
||||
return task
|
||||
return f"{task}\n\n---\n\n{directive}"
|
||||
@@ -64,6 +64,11 @@ class AgentResponse(BaseModel):
|
||||
description="入库模板名 (external knowledge-ingest templateName). When set, markdown artifacts produced in this agent's chats can be published to the external knowledge-ingest API.",
|
||||
)
|
||||
|
||||
seat_skill_directive: bool | None = Field(
|
||||
default=None,
|
||||
description="会商席位「技能识别强化」开关 (config.yaml). True = 本智能体作为会商席位时强化技能识别 (与业务链条同名开关取 OR). None/False = 关 (默认).",
|
||||
)
|
||||
|
||||
tags: list[dict] = Field(default_factory=list, description="Tags assigned to this agent")
|
||||
|
||||
featured_order: int | None = Field(
|
||||
@@ -115,6 +120,7 @@ class AgentCreateRequest(BaseModel):
|
||||
tool_groups: list[str] | None = Field(default=None, description="Optional tool group whitelist persisted to config.yaml")
|
||||
common_questions: list[CommonQuestion] | None = Field(default=None, description="Recommended questions (常用问题) persisted to config.yaml. Each item is {title, prompt}; legacy strings are coerced.")
|
||||
template_name: str | None = Field(default=None, description="入库模板名 persisted to config.yaml. Enables publishing markdown artifacts to the external knowledge-ingest API.")
|
||||
seat_skill_directive: bool | None = Field(default=None, description="会商席位「技能识别强化」开关 persisted to config.yaml. None/False = off (默认).")
|
||||
|
||||
|
||||
class AgentUpdateRequest(BaseModel):
|
||||
@@ -131,6 +137,7 @@ class AgentUpdateRequest(BaseModel):
|
||||
tool_groups: list[str] | None = Field(default=None, description="Updated tool group whitelist (config.yaml)")
|
||||
common_questions: list[CommonQuestion] | None = Field(default=None, description="Updated recommended questions (常用问题, config.yaml). Each item is {title, prompt}; legacy strings are coerced.")
|
||||
template_name: str | None = Field(default=None, description="Updated 入库模板名 (config.yaml). Empty string clears it.")
|
||||
seat_skill_directive: bool | None = Field(default=None, description="Updated 会商席位「技能识别强化」开关 (config.yaml).")
|
||||
|
||||
|
||||
class AgentSelectorResponse(BaseModel):
|
||||
@@ -236,6 +243,7 @@ def _write_agent_config(
|
||||
tool_groups: list[str] | None = None,
|
||||
common_questions: list[CommonQuestion] | None = None,
|
||||
template_name: str | None = None,
|
||||
seat_skill_directive: bool | None = None,
|
||||
) -> None:
|
||||
agent_dir = get_paths().agent_dir(agent_id)
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -264,13 +272,16 @@ def _write_agent_config(
|
||||
]
|
||||
if template_name is not None and template_name.strip():
|
||||
config_data["template_name"] = template_name.strip()
|
||||
# 会商席位技能识别强化开关:仅当显式设置时写入(None = 不写, 由 AgentConfig 默认为关)。
|
||||
if seat_skill_directive is not None:
|
||||
config_data["seat_skill_directive"] = bool(seat_skill_directive)
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
|
||||
def _load_config_extras(agent_id: str) -> dict[str, Any]:
|
||||
"""Read skills/model/tool_groups/common_questions from the agent's config.yaml, tolerant of missing files."""
|
||||
empty = {"skills": None, "model": None, "tool_groups": None, "common_questions": None, "template_name": None}
|
||||
empty = {"skills": None, "model": None, "tool_groups": None, "common_questions": None, "template_name": None, "seat_skill_directive": None}
|
||||
try:
|
||||
cfg = load_agent_config(agent_id)
|
||||
except (FileNotFoundError, ValueError):
|
||||
@@ -283,6 +294,7 @@ def _load_config_extras(agent_id: str) -> dict[str, Any]:
|
||||
"tool_groups": cfg.tool_groups,
|
||||
"common_questions": cfg.common_questions,
|
||||
"template_name": cfg.template_name,
|
||||
"seat_skill_directive": cfg.seat_skill_directive,
|
||||
}
|
||||
|
||||
|
||||
@@ -443,7 +455,7 @@ def _record_to_response(
|
||||
can happen lazily when the agent is actually opened.
|
||||
"""
|
||||
soul: str | None = None
|
||||
extras: dict[str, Any] = {"skills": None, "model": None, "tool_groups": None, "common_questions": None, "template_name": None}
|
||||
extras: dict[str, Any] = {"skills": None, "model": None, "tool_groups": None, "common_questions": None, "template_name": None, "seat_skill_directive": None}
|
||||
if include_extras:
|
||||
_ensure_agent_dir_from_record(record)
|
||||
extras = _load_config_extras(record["id"])
|
||||
@@ -467,6 +479,7 @@ def _record_to_response(
|
||||
skills=extras["skills"],
|
||||
common_questions=extras.get("common_questions"),
|
||||
template_name=extras.get("template_name"),
|
||||
seat_skill_directive=extras.get("seat_skill_directive"),
|
||||
featured_order=record.get("featured_order"),
|
||||
)
|
||||
|
||||
@@ -793,6 +806,7 @@ async def create_agent_endpoint(request: AgentCreateRequest, http_request: Reque
|
||||
tool_groups=request.tool_groups,
|
||||
common_questions=request.common_questions,
|
||||
template_name=request.template_name,
|
||||
seat_skill_directive=request.seat_skill_directive,
|
||||
)
|
||||
_write_agent_soul(agent_id, request.soul)
|
||||
|
||||
@@ -873,8 +887,9 @@ async def update_agent(agent_id: str, request: AgentUpdateRequest, http_request:
|
||||
next_tool_groups = request.tool_groups if "tool_groups" in fields_set else existing_extras["tool_groups"]
|
||||
next_common_questions = request.common_questions if "common_questions" in fields_set else existing_extras["common_questions"]
|
||||
next_template_name = request.template_name if "template_name" in fields_set else existing_extras["template_name"]
|
||||
next_seat_skill_directive = request.seat_skill_directive if "seat_skill_directive" in fields_set else existing_extras["seat_skill_directive"]
|
||||
|
||||
config_dirty = bool(update_data) or any(key in fields_set for key in ("skills", "model", "tool_groups", "common_questions", "template_name"))
|
||||
config_dirty = bool(update_data) or any(key in fields_set for key in ("skills", "model", "tool_groups", "common_questions", "template_name", "seat_skill_directive"))
|
||||
if config_dirty:
|
||||
_write_agent_config(
|
||||
agent_id,
|
||||
@@ -885,6 +900,7 @@ async def update_agent(agent_id: str, request: AgentUpdateRequest, http_request:
|
||||
tool_groups=next_tool_groups,
|
||||
common_questions=next_common_questions,
|
||||
template_name=next_template_name,
|
||||
seat_skill_directive=next_seat_skill_directive,
|
||||
)
|
||||
# Keep the listing cache in lock-step with config.yaml, but only
|
||||
# when the cached fields were actually touched — a partial update
|
||||
|
||||
@@ -1737,6 +1737,7 @@ async def _special_run(
|
||||
thinking_enabled: bool | None = None,
|
||||
reasoning_effort: str | None = None,
|
||||
subagent_enabled: bool = False,
|
||||
seat_skill_directive: bool = False,
|
||||
) -> AsyncIterator[str | dict[str, Any]]:
|
||||
"""子智能体(special)单轮:执行总控下发的 task 文本,交付 content。
|
||||
|
||||
@@ -1789,10 +1790,12 @@ async def _special_run(
|
||||
# 弱模型补偿:仅对真正的研讨席位(seat / seat_parallel;report/summary/dashboard/ingest
|
||||
# 等 Step3 单例不在此列)把其配置技能(名称 + SKILL.md 路径 + 必须先 read_file 再按其
|
||||
# 流程取信息的硬指令)显式拼进本轮任务,避免弱模型无视系统提示里的技能块(见
|
||||
# roundtable_seat_skills 模块说明)。无配置技能时原样返回。
|
||||
# roundtable_seat_skills 模块说明)。默认关闭:是否注入 = 该席位 agent 的开关
|
||||
# (config.yaml)OR 本次会商所选业务链条的开关(seat_skill_directive 透传)。无配置
|
||||
# 技能 / 两开关都关时原样返回。
|
||||
seat_message = new_message
|
||||
if role in ("seat", "seat_parallel"):
|
||||
seat_message = append_seat_skill_directive(agent_name, new_message)
|
||||
seat_message = append_seat_skill_directive(agent_name, new_message, chain_enabled=seat_skill_directive)
|
||||
body = _run_payload(
|
||||
agent_name=agent_name,
|
||||
model_name=model_name,
|
||||
@@ -1957,6 +1960,9 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
|
||||
_re_raw = payload.get("reasoning_effort")
|
||||
seat_reasoning_effort: str | None = _re_raw if isinstance(_re_raw, str) and _re_raw else None
|
||||
seat_subagent_enabled: bool = bool(payload.get("subagent_enabled") or False)
|
||||
# 业务链条级「技能识别强化」开关(仅 special 席位消费):前端读所选业务链条的同名配置
|
||||
# 后逐席位透传。与该席位 agent 自身的 config.yaml 开关取 OR;缺省 False(默认关)。
|
||||
seat_skill_directive: bool = bool(payload.get("seat_skill_directive") or False)
|
||||
|
||||
if not agent_name or agent_name not in agent_threads:
|
||||
raise HTTPException(
|
||||
@@ -2007,6 +2013,7 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
|
||||
thinking_enabled=seat_thinking_enabled,
|
||||
reasoning_effort=seat_reasoning_effort,
|
||||
subagent_enabled=seat_subagent_enabled,
|
||||
seat_skill_directive=seat_skill_directive,
|
||||
)
|
||||
async for chunk in gen:
|
||||
if isinstance(chunk, dict):
|
||||
|
||||
@@ -48,6 +48,11 @@ class ChainSeatSchema(BaseModel):
|
||||
# 数组以 JSON 透传持久化(见 persistence/roundtable_chains),故新增此字段无需迁移;
|
||||
# 仅需在此 schema 显式声明,否则 model_dump() 会把未知字段剥掉。
|
||||
model: str | None = Field(default=None, max_length=128)
|
||||
# 该席位「会商技能识别强化」开关(弱模型补偿)。True = 该席位作为会商子智能体执行时,
|
||||
# 把其配置的技能(名称 + SKILL.md 路径 + 必须先 read_file 的硬指令)拼进本轮任务。
|
||||
# 与该席位 agent 自身 config.yaml 的同名开关取 OR。默认 False(关)。整条 seats 数组以
|
||||
# JSON 透传持久化,故新增此字段无需迁移;仅需在此 schema 显式声明,否则 model_dump() 会剥掉它。
|
||||
seat_skill_directive: bool = False
|
||||
|
||||
|
||||
class ChainResponse(BaseModel):
|
||||
|
||||
@@ -77,6 +77,10 @@ class StartJobRequest(BaseModel):
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
subagent_enabled: bool = False
|
||||
# 每席位「技能识别强化」开关映射 {agent_id: bool}(前端读所选业务链条里**每个席位**的
|
||||
# 同名配置后下发)。后台席位 run 据此(与各席位 agent 自身 config.yaml 的开关取 OR)决定
|
||||
# 是否注入。缺省空 dict(全部按 False/关,仅各 agent 自身开关生效)。
|
||||
seat_skill_directives: dict[str, bool] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ResumeJobRequest(BaseModel):
|
||||
@@ -235,6 +239,7 @@ async def start_job(request: Request, body: StartJobRequest) -> RoundtableJobRes
|
||||
seat_thinking_enabled=body.thinking_enabled,
|
||||
seat_reasoning_effort=body.reasoning_effort,
|
||||
seat_subagent_enabled=body.subagent_enabled,
|
||||
seat_skill_directives=dict(body.seat_skill_directives or {}),
|
||||
)
|
||||
)
|
||||
return _to_response(row)
|
||||
|
||||
@@ -103,6 +103,12 @@ class AgentConfig(BaseModel):
|
||||
# 入库模板名(外部知识入库接口的 templateName)。配置后该智能体会话中的
|
||||
# Markdown 产物可一键发布到外部入库接口;None/空 = 不支持入库。
|
||||
template_name: str | None = None
|
||||
# 会商席位「技能识别强化」开关(弱模型补偿)。作为会商席位运行时,是否把本智能体
|
||||
# 配置的技能(名称 + SKILL.md 路径 + 必须先 read_file 再按其流程取信息的硬指令)显式
|
||||
# 拼进本轮任务,避免弱模型无视系统提示里的技能块。None/False = 关(默认);True = 开。
|
||||
# 与「业务链条配置」里的同名开关取 OR:任一打开即对该席位生效。见
|
||||
# ``app/gateway/roundtable_seat_skills.py``。
|
||||
seat_skill_directive: bool | None = None
|
||||
|
||||
|
||||
def load_agent_config(agent_id: str | None) -> AgentConfig | None:
|
||||
|
||||
@@ -177,12 +177,12 @@ def test_load_config_extras_returns_values_written_to_yaml(isolated_paths) -> No
|
||||
)
|
||||
|
||||
extras = _load_config_extras(agent_id)
|
||||
assert extras == {"skills": ["s1"], "model": None, "tool_groups": None, "common_questions": None}
|
||||
assert extras == {"skills": ["s1"], "model": None, "tool_groups": None, "common_questions": None, "template_name": None, "seat_skill_directive": None}
|
||||
|
||||
|
||||
def test_load_config_extras_missing_directory_returns_none_triple(isolated_paths) -> None:
|
||||
extras = _load_config_extras("does-not-exist")
|
||||
assert extras == {"skills": None, "model": None, "tool_groups": None, "common_questions": None}
|
||||
assert extras == {"skills": None, "model": None, "tool_groups": None, "common_questions": None, "template_name": None, "seat_skill_directive": None}
|
||||
|
||||
|
||||
def test_write_agent_config_persists_common_questions(isolated_paths) -> None:
|
||||
|
||||
Reference in new issue
Block a user