mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 22:20:54 +08:00
Remove Manager/comprehensive mode; route everything expert-only.
Keep WhatsApp/Weixin channels intact—only drop manager dispatch UI and force expert normalization end-to-end.
This commit is contained in:
parent
cf31fd104d
commit
ff9ef7279d
11 changed files with 99 additions and 636 deletions
|
|
@ -1665,7 +1665,7 @@ async function renderStack() {
|
|||
]);
|
||||
const status = el("div", { class: "muted", text: `mode=${curMode} specialist=${curSpecialist} lang=${curLang}` });
|
||||
const saveExpertBtn = el("button", {
|
||||
class: "btn",
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "绑定专家" : "Bind specialist",
|
||||
onclick: async () => {
|
||||
const specialist = String(specialistSel.value || "generalist").trim() || "generalist";
|
||||
|
|
@ -1678,20 +1678,6 @@ async function renderStack() {
|
|||
status.textContent = `mode=${String(resp.interaction_mode || "expert")} specialist=${String(resp.specialist || specialist)} lang=${String(resp.lang || lang)}`;
|
||||
},
|
||||
});
|
||||
const saveComprehensiveBtn = el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "综合" : "Comprehensive",
|
||||
onclick: async () => {
|
||||
const specialist = String(specialistSel.value || "generalist").trim() || "generalist";
|
||||
const lang = String(langSel.value || "auto").trim().toLowerCase() || "auto";
|
||||
const resp = await apiPost(`/admin/api/chat/settings/channel-dispatch/${encodeURIComponent(channel)}`, {
|
||||
interaction_mode: "comprehensive",
|
||||
specialist,
|
||||
lang,
|
||||
});
|
||||
status.textContent = `mode=${String(resp.interaction_mode || "comprehensive")} specialist=${String(resp.specialist || specialist)} lang=${String(resp.lang || lang)}`;
|
||||
},
|
||||
});
|
||||
return el("div", { class: "card" }, [
|
||||
el("div", { class: "card__title", text: title }),
|
||||
el("div", { class: "row" }, [
|
||||
|
|
@ -1700,10 +1686,9 @@ async function renderStack() {
|
|||
el("label", { text: currentLang === "zh" ? "语言" : "Lang" }),
|
||||
langSel,
|
||||
saveExpertBtn,
|
||||
saveComprehensiveBtn,
|
||||
]),
|
||||
status,
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "默认绑定通用专家;综合模式下由全能者分派。" : "Defaults to generalist; comprehensive mode lets manager dispatch." }),
|
||||
el("div", { class: "muted", text: currentLang === "zh" ? "通道统一走专家模式;选择默认专家即可。" : "Channels are expert-only; pick the default specialist." }),
|
||||
]);
|
||||
};
|
||||
const weixinDispatchCard = createChannelDispatchCard("weixin", "Weixin dispatch", weixinDispatchResp || {});
|
||||
|
|
@ -2320,8 +2305,8 @@ async function renderStack() {
|
|||
class: "muted",
|
||||
text:
|
||||
currentLang === "zh"
|
||||
? "任何 skill/tool/角色/提示词变更后,请立即预热;复杂变更可直接重启。综合模式下:全能者会产出 dispatch.instruction_text,并只把该指令传给专家执行。系统每10分钟自动异步预热一次。"
|
||||
: "After any skill/tool/role/prompt change, run prewarm immediately; restart for complex changes. In comprehensive mode, manager produces dispatch.instruction_text and only this instruction is sent to specialists. System also auto-prewarms every 10 minutes.",
|
||||
? "任何 skill/tool/角色/提示词变更后,请立即预热;复杂变更可直接重启。系统每10分钟自动异步预热一次。"
|
||||
: "After any skill/tool/role/prompt change, run prewarm immediately; restart for complex changes. System also auto-prewarms every 10 minutes.",
|
||||
},
|
||||
),
|
||||
el("div", { class: "row" }, [btnPrewarm]),
|
||||
|
|
@ -2584,12 +2569,10 @@ async function renderUserManagement() {
|
|||
} else {
|
||||
accountActiveInput.checked = true;
|
||||
}
|
||||
const mode = String(((inst && inst.config) || {}).interaction_mode || "expert").trim().toLowerCase();
|
||||
const specialist = String(((inst && inst.config) || {}).specialist || "generalist").trim().toLowerCase() || "generalist";
|
||||
accountSpecialistInput.value = availableAccountSpecialists.includes(specialist) ? specialist : "generalist";
|
||||
if (mode === "comprehensive") {
|
||||
accountStatus.textContent = currentLang === "zh" ? "当前账号模式:综合" : "Current account mode: comprehensive";
|
||||
}
|
||||
accountStatus.textContent =
|
||||
currentLang === "zh" ? `当前账号模式:专家(${specialist})` : `Current account mode: expert (${specialist})`;
|
||||
botSecretInput.value = "";
|
||||
clearBotChk.checked = false;
|
||||
};
|
||||
|
|
@ -2636,7 +2619,7 @@ async function renderUserManagement() {
|
|||
const deleteCell = el("td", {});
|
||||
if (inst) {
|
||||
const btnBindExpert = el("button", {
|
||||
class: "btn",
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "绑定专家" : "Bind specialist",
|
||||
onclick: async (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -2653,24 +2636,6 @@ async function renderUserManagement() {
|
|||
await loadAccounts();
|
||||
},
|
||||
});
|
||||
const btnComprehensive = el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: currentLang === "zh" ? "综合" : "Comprehensive",
|
||||
onclick: async (e) => {
|
||||
e.stopPropagation();
|
||||
const specialist = String(accountSpecialistInput.value || "generalist").trim() || "generalist";
|
||||
await apiPost("/admin/api/user-channel-accounts/upsert", {
|
||||
tenant_id: tenantId,
|
||||
user_id: selectedUserId,
|
||||
channel: ch,
|
||||
account_id: accountId,
|
||||
name: displayName,
|
||||
is_active: !!inst.is_active,
|
||||
config: { interaction_mode: "comprehensive", specialist },
|
||||
});
|
||||
await loadAccounts();
|
||||
},
|
||||
});
|
||||
const btnDeleteAccount = el("button", { class: "btn btn--danger", text: t("users.accountDelete"), onclick: async (e) => {
|
||||
e.stopPropagation();
|
||||
await apiPost("/admin/api/user-channel-accounts/delete", {
|
||||
|
|
@ -2682,7 +2647,6 @@ async function renderUserManagement() {
|
|||
await loadAccounts();
|
||||
}});
|
||||
deleteCell.appendChild(btnBindExpert);
|
||||
deleteCell.appendChild(btnComprehensive);
|
||||
deleteCell.appendChild(btnDeleteAccount);
|
||||
} else {
|
||||
deleteCell.appendChild(document.createTextNode("—"));
|
||||
|
|
|
|||
|
|
@ -3361,8 +3361,8 @@ async function renderChatUi() {
|
|||
disabled: "disabled",
|
||||
});
|
||||
const composerShell = el("div", { class: "chat-composer-shell" });
|
||||
const MAIN_MODE_VALUE = "comprehensive";
|
||||
const EXCLUDED_SPECIALISTS = new Set(["main", "memory", "manager_self", "pycache", "__pycache__"]);
|
||||
const MAIN_MODE_VALUE = "generalist";
|
||||
const EXCLUDED_SPECIALISTS = new Set(["main", "memory", "manager_self", "pycache", "__pycache__", "comprehensive"]);
|
||||
let specialistCatalog = [];
|
||||
/** Hidden: mode is global-only (⋯ menu). Kept for specialist option list in `publishUserMenuPrefsBridge`. */
|
||||
const modeSelect = el("select", {
|
||||
|
|
@ -3372,6 +3372,7 @@ async function renderChatUi() {
|
|||
tabIndex: -1,
|
||||
});
|
||||
let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase();
|
||||
if (globalMenuModeValue === "comprehensive" || globalMenuModeValue === "main") globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
const modelSelect = el("select", {
|
||||
class: "input",
|
||||
style: "min-width:150px;max-width:240px;padding:6px 8px;",
|
||||
|
|
@ -3424,35 +3425,31 @@ async function renderChatUi() {
|
|||
const execSelectWrap = el("span", { class: "chat-exec-mode-wrap", style: "display:inline-flex;align-items:center;" }, [
|
||||
execSelect,
|
||||
]);
|
||||
const outboundPlanAgentVersion = () => {
|
||||
const modeVal = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
|
||||
if (modeVal === MAIN_MODE_VALUE) return PLAN_AGENT_V1;
|
||||
return normalizePlanAgentVersion(currentPlanAgentVersion);
|
||||
};
|
||||
const outboundPlanAgentVersion = () => PLAN_AGENT_V1;
|
||||
const refreshExecUi = () => {
|
||||
// Plan mode removed; keep agent execution only.
|
||||
execSelectWrap.style.display = "none";
|
||||
};
|
||||
const modeOptionLabel = (v) => {
|
||||
const key = String(v || "").trim().toLowerCase();
|
||||
if (key === MAIN_MODE_VALUE) return t("chat.modeComprehensive");
|
||||
if (key === "generalist") return t("chat.specialistGeneralist");
|
||||
const row = specialistCatalog.find((x) => String(x.id || "").toLowerCase() === key);
|
||||
if (!row) return key || t("chat.modeComprehensive");
|
||||
if (!row) return key || t("chat.specialistGeneralist");
|
||||
const zh = String(row.display_name_zh || "").trim();
|
||||
const en = String(row.display_name_en || "").trim();
|
||||
return currentLang === "zh" ? (zh || en || key) : (en || zh || key);
|
||||
};
|
||||
const isSelectableSpecialist = (v) => {
|
||||
const key = String(v || "").trim().toLowerCase();
|
||||
if (!key || key === MAIN_MODE_VALUE) return false;
|
||||
if (!key || key === "comprehensive" || key === "main") return false;
|
||||
if (key === "generalist") return true;
|
||||
return specialistCatalog.some((x) => String(x.id || "").toLowerCase() === key);
|
||||
};
|
||||
const persistModeSelection = () => {
|
||||
const v = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
|
||||
localStorage.setItem(CHAT_INTERACTION_MODE_KEY, v);
|
||||
if (v !== MAIN_MODE_VALUE) localStorage.setItem(CHAT_SPECIALIST_PREF_KEY, v);
|
||||
localStorage.setItem(CHAT_INTERACTION_MODE_KEY, "expert");
|
||||
localStorage.setItem(CHAT_USER_MENU_MODE_KEY, v);
|
||||
localStorage.setItem(CHAT_SPECIALIST_PREF_KEY, v);
|
||||
};
|
||||
const syncHiddenModeSelectFromGlobal = () => {
|
||||
const g = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
|
||||
|
|
@ -3460,9 +3457,9 @@ async function renderChatUi() {
|
|||
else modeSelect.value = MAIN_MODE_VALUE;
|
||||
};
|
||||
const applyModeOptions = () => {
|
||||
const prev = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
|
||||
let prev = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
|
||||
if (prev === "comprehensive" || prev === "main") prev = MAIN_MODE_VALUE;
|
||||
modeSelect.innerHTML = "";
|
||||
modeSelect.appendChild(el("option", { value: MAIN_MODE_VALUE, text: modeOptionLabel(MAIN_MODE_VALUE) }));
|
||||
modeSelect.appendChild(el("option", { value: "generalist", text: modeOptionLabel("generalist") }));
|
||||
specialistCatalog.forEach((x) => {
|
||||
const sid = String(x.id || "").toLowerCase();
|
||||
|
|
@ -3471,6 +3468,7 @@ async function renderChatUi() {
|
|||
});
|
||||
if (Array.from(modeSelect.options).some((o) => String(o.value || "") === prev)) {
|
||||
modeSelect.value = prev;
|
||||
globalMenuModeValue = prev;
|
||||
} else {
|
||||
modeSelect.value = MAIN_MODE_VALUE;
|
||||
globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
|
|
@ -3584,11 +3582,9 @@ async function renderChatUi() {
|
|||
if (gm) {
|
||||
const gIm = String(gm.interaction_mode || "").toLowerCase();
|
||||
const gSp = String(gm.specialist || "").toLowerCase();
|
||||
if (gIm === "comprehensive") globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
else if (gIm === "expert") {
|
||||
if (isSelectableSpecialist(gSp)) globalMenuModeValue = gSp;
|
||||
else globalMenuModeValue = "generalist";
|
||||
} else globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
if (isSelectableSpecialist(gSp)) globalMenuModeValue = gSp;
|
||||
else if (isSelectableSpecialist(gIm)) globalMenuModeValue = gIm;
|
||||
else globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
} else {
|
||||
try {
|
||||
const ur = await apiGet("/admin/api/chat/user-mode");
|
||||
|
|
@ -3602,15 +3598,13 @@ async function renderChatUi() {
|
|||
ur.plan_agent_version || localStorage.getItem(CHAT_PLAN_AGENT_VERSION_KEY),
|
||||
);
|
||||
localStorage.setItem(CHAT_PLAN_AGENT_VERSION_KEY, currentPlanAgentVersion);
|
||||
if (gum === "comprehensive") globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
else if (gum === "expert") {
|
||||
if (isSelectableSpecialist(gus)) globalMenuModeValue = gus;
|
||||
else globalMenuModeValue = "generalist";
|
||||
} else globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
if (isSelectableSpecialist(gus)) globalMenuModeValue = gus;
|
||||
else if (isSelectableSpecialist(gum)) globalMenuModeValue = gum;
|
||||
else globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
}
|
||||
} catch (_) {
|
||||
if (m === "comprehensive") globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
else if (m === "expert" && isSelectableSpecialist(s)) globalMenuModeValue = s;
|
||||
if (m === "expert" && isSelectableSpecialist(s)) globalMenuModeValue = s;
|
||||
else if (isSelectableSpecialist(s)) globalMenuModeValue = s;
|
||||
else globalMenuModeValue = MAIN_MODE_VALUE;
|
||||
}
|
||||
}
|
||||
|
|
@ -3633,17 +3627,18 @@ async function renderChatUi() {
|
|||
const saveUserGlobalModePreference = async () => {
|
||||
try {
|
||||
const modeVal = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
|
||||
const isMain = modeVal === MAIN_MODE_VALUE;
|
||||
const specialist = isSelectableSpecialist(modeVal) ? modeVal : MAIN_MODE_VALUE;
|
||||
const resp = await apiPost("/admin/api/chat/user-mode", {
|
||||
interaction_mode: isMain ? "comprehensive" : "expert",
|
||||
specialist: isMain ? "generalist" : modeVal,
|
||||
interaction_mode: "expert",
|
||||
specialist,
|
||||
confirm_strategy: String(currentConfirmStrategy || CONFIRM_STRATEGY_STRICT),
|
||||
plan_agent_version: String(currentPlanAgentVersion || PLAN_AGENT_V1),
|
||||
});
|
||||
if (resp && typeof resp.plan_agent_v2_globally_enabled === "boolean") {
|
||||
planAgentV2GloballyEnabled = !!resp.plan_agent_v2_globally_enabled;
|
||||
}
|
||||
localStorage.setItem(CHAT_USER_MENU_MODE_KEY, modeVal);
|
||||
localStorage.setItem(CHAT_USER_MENU_MODE_KEY, specialist);
|
||||
globalMenuModeValue = specialist;
|
||||
if (!resp || resp.ok === false) {
|
||||
const detail = String((resp && (resp.error || resp.detail)) || "unknown_error");
|
||||
throw new Error(detail);
|
||||
|
|
@ -3655,13 +3650,11 @@ async function renderChatUi() {
|
|||
if (ur && ur.ok) {
|
||||
const gum = String((ur.interaction_mode || "").toLowerCase());
|
||||
const gus = String((ur.specialist || "").toLowerCase());
|
||||
const expectedIm = isMain ? "comprehensive" : "expert";
|
||||
const expectedSp = isMain ? "generalist" : modeVal;
|
||||
if (gum !== expectedIm || gus !== expectedSp) {
|
||||
if (gum !== "expert" || gus !== specialist) {
|
||||
const msg =
|
||||
currentLang === "zh"
|
||||
? `模式未固化:后端返回 interaction_mode=${gum || "-"} specialist=${gus || "-"}(期望 ${expectedIm}/${expectedSp})`
|
||||
: `Mode not persisted: server returned interaction_mode=${gum || "-"} specialist=${gus || "-"} (expected ${expectedIm}/${expectedSp})`;
|
||||
? `模式未固化:后端返回 interaction_mode=${gum || "-"} specialist=${gus || "-"}(期望 expert/${specialist})`
|
||||
: `Mode not persisted: server returned interaction_mode=${gum || "-"} specialist=${gus || "-"} (expected expert/${specialist})`;
|
||||
showToast(msg, { kind: "error", ttlMs: 8000 });
|
||||
}
|
||||
}
|
||||
|
|
@ -5247,15 +5240,11 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
|
|||
sessionId: activeId,
|
||||
text: userText,
|
||||
attachments: attachmentPayload,
|
||||
interactionMode:
|
||||
String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase() === MAIN_MODE_VALUE ? "comprehensive" : "expert",
|
||||
interactionMode: "expert",
|
||||
idempotencyKey: String(turnId || ""),
|
||||
specialist:
|
||||
String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase() === MAIN_MODE_VALUE
|
||||
? "generalist"
|
||||
: isSelectableSpecialist(String(globalMenuModeValue || "").toLowerCase())
|
||||
? String(globalMenuModeValue || "generalist").toLowerCase()
|
||||
: "generalist",
|
||||
specialist: isSelectableSpecialist(String(globalMenuModeValue || "").toLowerCase())
|
||||
? String(globalMenuModeValue || "generalist").toLowerCase()
|
||||
: "generalist",
|
||||
memoryMode: String(localStorage.getItem(CHAT_MEMORY_MODE_KEY) || "default"),
|
||||
executionMode: String(currentExecutionMode || EXECUTION_MODE_AGENT),
|
||||
signal: abortController.signal,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def build_gateway_context(
|
|||
with abort_lock:
|
||||
active_run_session[rid] = sid
|
||||
p = dict(params or {})
|
||||
interaction_mode = str(p.get("interaction_mode") or "comprehensive").strip().lower() or "comprehensive"
|
||||
interaction_mode = str(p.get("interaction_mode") or "expert").strip().lower() or "expert"
|
||||
specialist = str(p.get("specialist") or "generalist").strip().lower() or "generalist"
|
||||
execution_mode = str(p.get("execution_mode") or "agent").strip().lower() or "agent"
|
||||
if execution_mode not in {"agent", "plan"}:
|
||||
|
|
@ -71,7 +71,7 @@ def build_gateway_context(
|
|||
if plan_agent_version not in {"v1", "v2"}:
|
||||
plan_agent_version = "v1"
|
||||
if interaction_mode != "expert":
|
||||
specialist = "generalist"
|
||||
interaction_mode = "expert"
|
||||
raw_env = dict(p.get("relay_share_envelope") or {}) if isinstance(p.get("relay_share_envelope"), dict) else {}
|
||||
ok_env, _err_env, norm_env = validate_relay_share_envelope(raw_env) if raw_env else (False, "", {})
|
||||
agent_params = {
|
||||
|
|
@ -101,7 +101,7 @@ def build_gateway_context(
|
|||
session_id = str(p.get("sessionId") or p.get("sessionKey") or "").strip()
|
||||
run_id = str(p.get("idempotencyKey") or "").strip() or uuid.uuid4().hex
|
||||
message = str(p.get("message") or "").strip()
|
||||
interaction_mode = str(p.get("interaction_mode") or "comprehensive").strip().lower() or "comprehensive"
|
||||
interaction_mode = str(p.get("interaction_mode") or "expert").strip().lower() or "expert"
|
||||
specialist = str(p.get("specialist") or "generalist").strip().lower() or "generalist"
|
||||
execution_mode = str(p.get("execution_mode") or "agent").strip().lower() or "agent"
|
||||
if execution_mode not in {"agent", "plan"}:
|
||||
|
|
@ -110,7 +110,7 @@ def build_gateway_context(
|
|||
if plan_agent_version not in {"v1", "v2"}:
|
||||
plan_agent_version = "v1"
|
||||
if interaction_mode != "expert":
|
||||
specialist = "generalist"
|
||||
interaction_mode = "expert"
|
||||
raw_env = dict(p.get("relay_share_envelope") or {}) if isinstance(p.get("relay_share_envelope"), dict) else {}
|
||||
ok_env, _err_env, norm_env = validate_relay_share_envelope(raw_env) if raw_env else (False, "", {})
|
||||
agent_params = {
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ async def run_agent_turn_via_bridge(
|
|||
text=msg_text,
|
||||
attachments=attachments,
|
||||
metadata={
|
||||
"interaction_mode": str(p.get("interaction_mode") or "comprehensive"),
|
||||
"interaction_mode": str(p.get("interaction_mode") or "expert"),
|
||||
"selected_specialist": str(p.get("specialist") or "generalist"),
|
||||
"execution_mode": execution_mode,
|
||||
"plan_agent_version": plan_agent_version,
|
||||
|
|
@ -426,7 +426,7 @@ async def run_agent_turn_via_bridge(
|
|||
"traceId": "",
|
||||
"reply": stopped,
|
||||
"selectedSpecialist": str(p.get("specialist") or "generalist"),
|
||||
"interactionMode": str(p.get("interaction_mode") or "comprehensive"),
|
||||
"interactionMode": str(p.get("interaction_mode") or "expert"),
|
||||
"dispatchReason": "aborted",
|
||||
"executionMode": execution_mode,
|
||||
"managerSelectedSpecialist": str(p.get("specialist") or "generalist"),
|
||||
|
|
@ -545,7 +545,7 @@ async def run_agent_turn_via_bridge(
|
|||
"traceId": "",
|
||||
"reply": user_facing_error,
|
||||
"selectedSpecialist": str(p.get("specialist") or "generalist"),
|
||||
"interactionMode": str(p.get("interaction_mode") or "comprehensive"),
|
||||
"interactionMode": str(p.get("interaction_mode") or "expert"),
|
||||
"dispatchReason": "execution_failed",
|
||||
"executionMode": execution_mode,
|
||||
"managerSelectedSpecialist": str(p.get("specialist") or "generalist"),
|
||||
|
|
@ -657,7 +657,7 @@ async def run_agent_turn_via_bridge(
|
|||
"traceId": str(getattr(result, "trace_id", "") or ""),
|
||||
"reply": str(getattr(result, "reply_text", "") or ""),
|
||||
"selectedSpecialist": str(getattr(result, "selected_specialist", "generalist") or "generalist"),
|
||||
"interactionMode": str(getattr(result, "interaction_mode", "comprehensive") or "comprehensive"),
|
||||
"interactionMode": str(getattr(result, "interaction_mode", "expert") or "expert"),
|
||||
"dispatchReason": str(getattr(result, "dispatch_reason", "") or ""),
|
||||
"executionMode": execution_mode,
|
||||
"managerSelectedSpecialist": str(getattr(result, "manager_selected_specialist", "generalist") or "generalist"),
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class OclawGatewayResult:
|
|||
mode: str = "sync_direct"
|
||||
task_id: str | None = None
|
||||
selected_specialist: str = "generalist"
|
||||
interaction_mode: str = "comprehensive"
|
||||
interaction_mode: str = "expert"
|
||||
dispatch_reason: str = ""
|
||||
manager_selected_specialist: str = "generalist"
|
||||
requested_specialist: str = "generalist"
|
||||
|
|
@ -89,7 +89,8 @@ class OclawGatewayResult:
|
|||
relay_ttl_turn_count: int = 0
|
||||
relay_ttl_session_count: int = 0
|
||||
relay_ttl_keep_count: int = 0
|
||||
# agent-core 本轮 ``chat_message.turn_uuid``;供 WS 收尾与è<C5BD>½åº“兜底对é½? turn_uuid: str = ""
|
||||
# agent-core 本轮 ``chat_message.turn_uuid``;供 WS 收尾与落库兜底对齐
|
||||
turn_uuid: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -207,7 +208,7 @@ class OclawGateway:
|
|||
# - stage "3": renamed on third user message (final)
|
||||
if stage_raw == "3":
|
||||
return
|
||||
if (cur_title not in ("新会�, "New Chat")) and (stage_raw != "1"):
|
||||
if (cur_title not in ("新会话", "New Chat")) and (stage_raw != "1"):
|
||||
return
|
||||
try:
|
||||
rows = self.store.get_messages(session_id=sid, limit=200)
|
||||
|
|
@ -269,7 +270,7 @@ class OclawGateway:
|
|||
if not sess:
|
||||
return
|
||||
cur_title = str(getattr(sess, "title", "") or "").strip()
|
||||
if cur_title not in ("新会�, "New Chat"):
|
||||
if cur_title not in ("新会话", "New Chat"):
|
||||
return
|
||||
try:
|
||||
rows = self.store.get_messages(session_id=sid, limit=20)
|
||||
|
|
@ -403,9 +404,9 @@ class OclawGateway:
|
|||
user_text = (
|
||||
"请基于以下信息输出最终答复。\n\n"
|
||||
f"原始用户问题:\n{str(msg.text or '').strip()}\n\n"
|
||||
f"已调用专� {str(specialist or '').strip()}\n\n"
|
||||
f"已调用专家: {str(specialist or '').strip()}\n\n"
|
||||
f"专家结果:\n{str(specialist_reply or '').strip()}\n\n"
|
||||
"è¦<EFBFBD>求:ä¿<EFBFBD>æŒ<EFBFBD>简æ´<EFBFBD>ã€<EFBFBD>准确,ä¸<EFBFBD>è¦<EFBFBD>暴露内部æµ<EFBFBD>程ã€?
|
||||
"要求:保持简洁、准确,不要暴露内部流程。"
|
||||
)
|
||||
messages = [{"role": "system", "content": manager_context}, {"role": "user", "content": user_text}]
|
||||
ensure_no_tool_or_embedded_image_payload(messages=messages, path="gateway.manager_finalize")
|
||||
|
|
@ -535,9 +536,9 @@ class OclawGateway:
|
|||
"If you need more rows/details, use database tools (`query_tabular_attachment` / `run_tabular_sql`) with table_id."
|
||||
)
|
||||
return (
|
||||
f"对于大表附件:当å‰<EFBFBD>上下文å<EFBFBD>ªæ<EFBFBD><EFBFBD>ä¾›å‰<EFBFBD>{preview_rows}行预览ã€?
|
||||
f"å<EFBFBD>•次读å<EFBFBD>–上é™<EFBFBD>为{max_rows_read}行ã€?
|
||||
"如果需è¦<EFBFBD>更多行或更细节,请通过数æ<EFBFBD>®åº“工具(`query_tabular_attachment` / `run_tabular_sql`)结å<E2809C>?table_id 查询ã€?
|
||||
f"对于大表附件:当前上下文只提供前{preview_rows}行预览。"
|
||||
f"单次读取上限为{max_rows_read}行。"
|
||||
"如果需要更多行或更细节,请通过数据库工具(`query_tabular_attachment` / `run_tabular_sql`)结合 table_id 查询。"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -548,8 +549,8 @@ class OclawGateway:
|
|||
"For detailed evidence, use `query_text_attachment` with `text_id` from `text_ref` attachment."
|
||||
)
|
||||
return (
|
||||
"对于长文本附件:上下文å<EFBFBD>¯èƒ½å<EFBFBD>ªåŒ…å<EFBFBD>«æ‘˜è¦<EFBFBD>/预览ã€?
|
||||
"如需细节è¯<EFBFBD>æ<EFBFBD>®ï¼Œè¯·ä½¿ç”¨ `text_ref` æ<><C3A6>ä¾›çš?text_id 调用 `query_text_attachment`ã€?
|
||||
"对于长文本附件:上下文可能只包含摘要/预览。"
|
||||
"如需细节证据,请使用 `text_ref` 提供的 text_id 调用 `query_text_attachment`。"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -560,7 +561,7 @@ class OclawGateway:
|
|||
"for OCR/description when visual evidence is required."
|
||||
)
|
||||
return (
|
||||
"对于图片附件:如需 OCR 或图åƒ<C3A5>细节,请使ç”?attachment_id 调用 `query_image_attachment`ã€?
|
||||
"对于图片附件:如需 OCR 或图像细节,请使用 attachment_id 调用 `query_image_attachment`。"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -570,7 +571,7 @@ class OclawGateway:
|
|||
"For video attachments: use `query_video_attachment` with attachment_id from `video_ref` "
|
||||
"to get metadata or transcript (if enabled)."
|
||||
)
|
||||
return "对于视频附件:请使用 `video_ref` æ<><C3A6>ä¾›çš?attachment_id 调用 `query_video_attachment` 获å<C2B7>–元信æ<C2A1>?转写ã€?
|
||||
return "对于视频附件:请使用 `video_ref` 提供的 attachment_id 调用 `query_video_attachment` 获取元信息/转写。"
|
||||
|
||||
@staticmethod
|
||||
def _tabular_limits_from_config() -> dict[str, int]:
|
||||
|
|
@ -734,6 +735,9 @@ class OclawGateway:
|
|||
trace_local: Callable[..., None],
|
||||
started_at: float,
|
||||
) -> GatewayDispatchPlan:
|
||||
del msg, lang, memory_enabled, trace_local, started_at
|
||||
# Expert-only product surface: never run Manager/comprehensive routing.
|
||||
interaction_mode = "expert"
|
||||
manager_specialist = requested_specialist
|
||||
dispatch_reason = "expert_direct"
|
||||
selected_executor = executor
|
||||
|
|
@ -741,32 +745,12 @@ class OclawGateway:
|
|||
specialist_input_msg: StandardMessage | None = None
|
||||
manager_exec_msg: StandardMessage | None = None
|
||||
manager_instruction_text = ""
|
||||
if interaction_mode == "expert" and callable(specialist_executor_factory):
|
||||
if callable(specialist_executor_factory):
|
||||
try:
|
||||
selected_executor = specialist_executor_factory(requested_specialist)
|
||||
except Exception:
|
||||
selected_executor = executor
|
||||
dispatch_reason = "expert_factory_failed"
|
||||
if interaction_mode == "comprehensive":
|
||||
(
|
||||
manager_specialist,
|
||||
dispatch_reason,
|
||||
dynamic_agent,
|
||||
instruction_text,
|
||||
) = self._manager_select_specialist(msg=msg, lang=lang, executor=executor, memory_enabled=memory_enabled)
|
||||
manager_instruction_text = str(instruction_text or "").strip()
|
||||
trace_local(
|
||||
event_type="manager_decision",
|
||||
payload={
|
||||
"interaction_mode": interaction_mode,
|
||||
"manager_selected_specialist": str(manager_specialist or ""),
|
||||
"dispatch_reason": str(dispatch_reason or ""),
|
||||
"instruction_chars": int(len(manager_instruction_text or "")),
|
||||
"dynamic_agent_used": bool(dynamic_agent is not None),
|
||||
"dynamic_agent_name": str((dynamic_agent or {}).get("name") or "") if isinstance(dynamic_agent, dict) else "",
|
||||
},
|
||||
started_at=started_at,
|
||||
)
|
||||
return GatewayDispatchPlan(
|
||||
interaction_mode=interaction_mode,
|
||||
requested_specialist=requested_specialist,
|
||||
|
|
@ -915,6 +899,8 @@ class OclawGateway:
|
|||
base_metadata = dict(msg.metadata or {})
|
||||
memory_enabled = self._memory_enabled()
|
||||
interaction_mode = normalize_interaction_mode(base_metadata.get("interaction_mode"))
|
||||
# Defensive: product surface is expert-only regardless of inbound metadata.
|
||||
interaction_mode = "expert"
|
||||
requested_specialist = normalize_requested_specialist(base_metadata.get("selected_specialist"))
|
||||
if requested_specialist == "memory" and not memory_enabled:
|
||||
requested_specialist = "generalist"
|
||||
|
|
@ -936,66 +922,6 @@ class OclawGateway:
|
|||
specialist_input_msg = plan.specialist_input_msg
|
||||
manager_exec_msg = plan.manager_exec_msg
|
||||
manager_instruction_text = plan.manager_instruction_text
|
||||
if interaction_mode == "comprehensive":
|
||||
if str(manager_instruction_text or "").strip():
|
||||
try:
|
||||
assignment_title = "Task assignment" if str(lang or "").startswith("en") else "任务分é…<EFBFBD>"
|
||||
assignment_text = (
|
||||
f"{assignment_title}\n"
|
||||
f"specialist={str(manager_specialist or '')}\n"
|
||||
f"instruction:\n{str(manager_instruction_text or '').strip()}"
|
||||
)
|
||||
self.store.add_message(
|
||||
session_id=msg.session_id,
|
||||
role="assistant",
|
||||
content=assignment_text,
|
||||
tool_calls=None,
|
||||
event_type="reasoning",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"comprehensive_mode_assignment_message_persist_failed session_id=%s",
|
||||
str(getattr(msg, "session_id", "") or ""),
|
||||
)
|
||||
# Build specialist/dynamic executor and dispatch only manager instruction to it.
|
||||
specialist_input_msg = StandardMessage(
|
||||
session_id=msg.session_id,
|
||||
tenant_id=msg.tenant_id,
|
||||
user_id=msg.user_id,
|
||||
role=msg.role,
|
||||
channel=msg.channel,
|
||||
text=str(manager_instruction_text or "").strip(),
|
||||
attachments=list(msg.attachments or []),
|
||||
metadata=dict(base_metadata),
|
||||
)
|
||||
manager_exec_msg = StandardMessage(
|
||||
session_id=msg.session_id,
|
||||
tenant_id=msg.tenant_id,
|
||||
user_id=msg.user_id,
|
||||
role=msg.role,
|
||||
channel=msg.channel,
|
||||
text=msg.text,
|
||||
attachments=list(msg.attachments or []),
|
||||
metadata=dict(base_metadata),
|
||||
)
|
||||
if manager_specialist in {"ops", "generalist", "memory"}:
|
||||
if callable(specialist_executor_factory):
|
||||
try:
|
||||
selected_executor = specialist_executor_factory(manager_specialist)
|
||||
except Exception:
|
||||
selected_executor = executor
|
||||
dispatch_reason = "manager_factory_failed"
|
||||
else:
|
||||
# Dynamic ephemeral agents removed from product surface; fall back to generalist.
|
||||
manager_specialist = "generalist"
|
||||
dispatch_reason = "dynamic_agent_disabled_fallback"
|
||||
if callable(specialist_executor_factory):
|
||||
try:
|
||||
selected_executor = specialist_executor_factory("generalist")
|
||||
except Exception:
|
||||
selected_executor = executor
|
||||
dynamic_agent = None
|
||||
|
||||
system_prompt_override = ""
|
||||
tools_override = None
|
||||
|
||||
|
|
@ -1032,7 +958,7 @@ class OclawGateway:
|
|||
started_at=t0,
|
||||
)
|
||||
if on_progress:
|
||||
on_progress("oclaw: running�)
|
||||
on_progress("oclaw: running…")
|
||||
if route_mode == "async_task":
|
||||
worker_id = ensure_worker_started(store=self.store)
|
||||
task = self.store.oclaw_task_create(
|
||||
|
|
@ -1181,7 +1107,7 @@ class OclawGateway:
|
|||
started_at=t0,
|
||||
)
|
||||
exec_msg = (
|
||||
specialist_input_msg if (interaction_mode == "comprehensive" and specialist_input_msg is not None) else msg
|
||||
msg
|
||||
)
|
||||
core_out = run_agent_core(
|
||||
store=self.store,
|
||||
|
|
@ -1211,32 +1137,12 @@ class OclawGateway:
|
|||
on_tool_ui=on_tool_ui,
|
||||
should_stop=should_stop,
|
||||
skill_binding_role=str(manager_specialist or "generalist"),
|
||||
wire_policy_role="manager" if interaction_mode == "comprehensive" else str(requested_specialist),
|
||||
wire_policy_role=str(requested_specialist),
|
||||
),
|
||||
)
|
||||
executed_turn_uuid = str(getattr(core_out.outcome, "turn_uuid", "") or "")
|
||||
specialist_reply = str(core_out.outcome.final_text or "")
|
||||
if interaction_mode == "comprehensive":
|
||||
reply = self._manager_finalize_output(
|
||||
msg=msg,
|
||||
lang=lang,
|
||||
executor=executor,
|
||||
specialist=manager_specialist,
|
||||
specialist_reply=specialist_reply,
|
||||
memory_enabled=memory_enabled,
|
||||
on_token=None,
|
||||
)
|
||||
if self._looks_like_manager_instruction(reply, manager_instruction_text):
|
||||
if not self._looks_like_manager_instruction(specialist_reply, manager_instruction_text):
|
||||
reply = str(specialist_reply or "").strip()
|
||||
else:
|
||||
reply = (
|
||||
"抱æ‰ï¼Œæˆ‘æš‚æ—¶æ— æ³•ç»™å‡ºå<EFBFBD>¯å±•示的结果,请ç¨<EFBFBD>å<EFBFBD>Žå†<EFBFBD>试ã€?
|
||||
if not str(lang or "").startswith("en")
|
||||
else "Sorry, no user-safe result is available right now. Please try again later."
|
||||
)
|
||||
else:
|
||||
reply = specialist_reply
|
||||
reply = specialist_reply
|
||||
if not str(reply or "").strip():
|
||||
rs = getattr(core_out, "run_state", None)
|
||||
if rs is not None and str(getattr(rs, "status", "") or "") == "failed":
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class RouterDecision:
|
|||
mode: str # sync_direct | async_task
|
||||
reason: str
|
||||
skill_signal: str = ""
|
||||
interaction_mode: str = "comprehensive"
|
||||
interaction_mode: str = "expert"
|
||||
requested_specialist: str = "generalist"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal
|
||||
|
||||
InteractionMode = Literal["comprehensive", "expert"]
|
||||
InteractionMode = Literal["expert"]
|
||||
SpecialistId = str
|
||||
|
||||
ChannelId = Literal[
|
||||
|
|
@ -34,10 +34,9 @@ class StandardMessage:
|
|||
|
||||
|
||||
def normalize_interaction_mode(raw: Any) -> InteractionMode:
|
||||
mode = str(raw or "").strip().lower()
|
||||
if mode in {"expert", "specialist"}:
|
||||
return "expert"
|
||||
return "comprehensive"
|
||||
"""Product surface is expert-only; Manager/comprehensive mode is removed."""
|
||||
del raw
|
||||
return "expert"
|
||||
|
||||
|
||||
def normalize_requested_specialist(raw: Any) -> SpecialistId:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def test_channel_dispatch_uses_global_settings() -> None:
|
|||
}
|
||||
)
|
||||
interaction_mode, specialist, lang = _resolve_channel_dispatch(store, channel="whatsapp", account=None)
|
||||
assert interaction_mode == "comprehensive"
|
||||
assert interaction_mode == "expert"
|
||||
assert specialist == "ops"
|
||||
assert lang == "en"
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ def test_channel_dispatch_wechat_reads_weixin_settings() -> None:
|
|||
return str(data.get(key) or "")
|
||||
|
||||
mode, spec, lang = _resolve_channel_dispatch(_Store(), channel="wechat", account=None)
|
||||
assert mode == "comprehensive"
|
||||
assert mode == "expert"
|
||||
assert spec == "ops"
|
||||
assert lang == "zh"
|
||||
assert _get_channel_dispatch_setting(_Store(), "channel.dispatch.specialist.", "wechat") == "ops"
|
||||
|
|
|
|||
|
|
@ -266,7 +266,10 @@ def test_gateway_expert_plan_execution_mode_ignored_without_plan_agent(monkeypat
|
|||
assert "Plan mode is active" not in prompt_text
|
||||
|
||||
|
||||
def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
def test_gateway_forces_expert_when_comprehensive_requested(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Manager/comprehensive mode is removed; inbound comprehensive metadata must not change routing."""
|
||||
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
|
@ -274,12 +277,8 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
|
|||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
class _ManagerModel:
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
return LLMResponse(
|
||||
content='{"route":{"specialist":"generalist","reason":"needs image edits"},"dispatch":{"instruction_text":"Please edit the image background."}}',
|
||||
tool_calls=[],
|
||||
)
|
||||
def add_message(self, **_kwargs: object) -> None:
|
||||
raise AssertionError("comprehensive assignment message must not be written")
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
|
|
@ -287,21 +286,13 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
|
|||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
},
|
||||
)
|
||||
captured: dict = {}
|
||||
|
||||
def _run_agent_core(**kwargs):
|
||||
data = kwargs.get("data")
|
||||
captured["exec_text"] = getattr(getattr(data, "msg", None), "text", None)
|
||||
captured["persisted_user_text"] = getattr(data, "persisted_user_text", None)
|
||||
return SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer"))
|
||||
captured["wire_policy_role"] = getattr(data, "wire_policy_role", None)
|
||||
return SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer", turn_uuid="tu-1"), run_state=None)
|
||||
|
||||
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
|
||||
|
||||
|
|
@ -322,402 +313,14 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
|
|||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive", "selected_specialist": "ops"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=_ManagerModel()), specialist_executor_factory=_factory)
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
assert out.selected_specialist == "generalist"
|
||||
assert chosen.get("sid") == "generalist"
|
||||
assert captured.get("exec_text") == "Please edit the image background."
|
||||
assert captured.get("persisted_user_text") == "edit this image background"
|
||||
|
||||
|
||||
def test_gateway_comprehensive_mode_writes_task_assignment_reasoning(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
written: list[dict] = []
|
||||
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
||||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
def add_message(self, **kwargs: object) -> None:
|
||||
written.append(dict(kwargs))
|
||||
|
||||
class _ManagerModel:
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
return LLMResponse(
|
||||
content='{"route":{"specialist":"ops","reason":"ops task"},"dispatch":{"instruction_text":"请检查并修复网关启动失败。"}}',
|
||||
tool_calls=[],
|
||||
)
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
self.model = model
|
||||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.run_agent_core",
|
||||
lambda **kwargs: SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer")),
|
||||
)
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
msg = StandardMessage(
|
||||
session_id="sid-assign-1",
|
||||
tenant_id="t1",
|
||||
user_id="u1",
|
||||
role="user",
|
||||
channel="admin_chat",
|
||||
text="网关起不来,帮我修复",
|
||||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive"},
|
||||
)
|
||||
_ = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
|
||||
reasoning_rows = [x for x in written if str(x.get("event_type") or "") == "reasoning"]
|
||||
assert reasoning_rows, "expected task-assignment reasoning row"
|
||||
content = str(reasoning_rows[-1].get("content") or "")
|
||||
assert "任务分配" in content
|
||||
assert "specialist=ops" in content
|
||||
assert "请检查并修复网关启动失败" in content
|
||||
|
||||
|
||||
def test_gateway_comprehensive_ignores_wiki_inject_flags(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
||||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
class _ManagerModel:
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
return LLMResponse(
|
||||
content=(
|
||||
'{"route":{"specialist":"generalist","reason":"general",'
|
||||
'"need_wiki_inject":true,"wiki_query":"router issue"},'
|
||||
'"dispatch":{"instruction_text":"请先分析问题并给出结论。",'
|
||||
'"need_wiki_inject":true,"wiki_query":"router issue"}}'
|
||||
),
|
||||
tool_calls=[],
|
||||
)
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
self.model = model
|
||||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
},
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _run_agent_core(**kwargs):
|
||||
data = kwargs.get("data")
|
||||
msg = getattr(data, "msg", None)
|
||||
captured["metadata"] = dict(getattr(msg, "metadata", {}) or {})
|
||||
return SimpleNamespace(outcome=SimpleNamespace(final_text="ok"))
|
||||
|
||||
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
msg = StandardMessage(
|
||||
session_id="sid-wi-1",
|
||||
tenant_id="t1",
|
||||
user_id="u1",
|
||||
role="user",
|
||||
channel="admin_chat",
|
||||
text="分析一下",
|
||||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
md = captured.get("metadata") or {}
|
||||
assert isinstance(md, dict)
|
||||
assert "need_wiki_inject" not in md
|
||||
assert "wiki_query" not in md
|
||||
|
||||
def test_gateway_comprehensive_mode_has_manager_final_pass(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
||||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
class _ManagerModel:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
self.calls += 1
|
||||
if self.calls == 1:
|
||||
return LLMResponse(
|
||||
content='{"route":{"specialist":"generalist","reason":"general"},"dispatch":{"instruction_text":"Analyze the finance report and provide key points."}}',
|
||||
tool_calls=[],
|
||||
)
|
||||
return LLMResponse(content="final_from_manager", tool_calls=[])
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
self.model = model
|
||||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.run_agent_core",
|
||||
lambda **kwargs: SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer")),
|
||||
)
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
model = _ManagerModel()
|
||||
msg = StandardMessage(
|
||||
session_id="sid-d1",
|
||||
tenant_id="t1",
|
||||
user_id="u1",
|
||||
role="user",
|
||||
channel="admin_chat",
|
||||
text="analyze finance report",
|
||||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive", "selected_specialist": "ops"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=model))
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
assert out.selected_specialist == "generalist"
|
||||
assert out.reply_text == "final_from_manager"
|
||||
|
||||
|
||||
def test_gateway_comprehensive_mode_dynamic_agent_falls_back_to_generalist(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
||||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
class _ManagerModel:
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
return LLMResponse(
|
||||
content=(
|
||||
'{"route":{"specialist":"dyn:sql","reason":"needs ad-hoc expert"},'
|
||||
'"dispatch":{"instruction_text":"Write a SQL query to compute daily active users."},'
|
||||
'"dynamic_agent":{"name":"dyn:sql","system_prompt":"You are a SQL expert.","tool_policy":{"allow_tags":[],"allow_tools":[]},"reason":"dynamic"}}'
|
||||
),
|
||||
tool_calls=[],
|
||||
)
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
self.model = model
|
||||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory"',
|
||||
},
|
||||
)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def _run_agent_core(**kwargs):
|
||||
data = kwargs.get("data")
|
||||
captured["exec_text"] = getattr(getattr(data, "msg", None), "text", None)
|
||||
return SimpleNamespace(outcome=SimpleNamespace(final_text="generalist_answer", turn_uuid="t1"))
|
||||
|
||||
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
|
||||
|
||||
chosen: dict[str, str] = {}
|
||||
|
||||
def _factory(sid: str):
|
||||
chosen["sid"] = sid
|
||||
return _Exec(model=object())
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
msg = StandardMessage(
|
||||
session_id="sid-dyn-1",
|
||||
tenant_id="t1",
|
||||
user_id="u1",
|
||||
role="user",
|
||||
channel="admin_chat",
|
||||
text="How do I compute DAU from events table?",
|
||||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=_ManagerModel()), specialist_executor_factory=_factory)
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
assert out.selected_specialist == "generalist"
|
||||
assert chosen.get("sid") == "generalist"
|
||||
assert out.dispatch_reason == "dynamic_agent_disabled_fallback"
|
||||
assert captured.get("exec_text") == "Write a SQL query to compute daily active users."
|
||||
|
||||
|
||||
def test_gateway_comprehensive_mode_ignores_manager_self_and_dispatches_specialist(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
||||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
class _ManagerModel:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
self.calls += 1
|
||||
if self.calls == 1:
|
||||
return LLMResponse(
|
||||
content='{"route":{"kind":"manager_self","specialist":"generalist","reason":"can answer directly"},"dispatch":{"instruction_text":"请直接给用户简明答案。"}}',
|
||||
tool_calls=[],
|
||||
)
|
||||
return LLMResponse(content="manager_self_final", tool_calls=[])
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
self.model = model
|
||||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
},
|
||||
)
|
||||
|
||||
captured: dict[str, str] = {}
|
||||
|
||||
def _run_agent_core(**kwargs):
|
||||
data = kwargs["data"]
|
||||
captured["exec_text"] = str(getattr(data.msg, "text", ""))
|
||||
|
||||
class _Outcome:
|
||||
final_text = "specialist_result"
|
||||
|
||||
class _Out:
|
||||
outcome = _Outcome()
|
||||
|
||||
return _Out()
|
||||
|
||||
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
msg = StandardMessage(
|
||||
session_id="sid-self-1",
|
||||
tenant_id="t1",
|
||||
user_id="u1",
|
||||
role="user",
|
||||
channel="admin_chat",
|
||||
text="请直接回答这个简单问题",
|
||||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
assert out.selected_specialist == "generalist"
|
||||
assert captured.get("exec_text") == "请直接给用户简明答案。"
|
||||
assert out.reply_text == "manager_self_final"
|
||||
|
||||
|
||||
def test_gateway_comprehensive_mode_suppresses_instruction_echo(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
||||
def add_trace_event(self, **_kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
instruction = "用户说厉害;请友好回应,表达感谢并询问是否有具体问题。"
|
||||
|
||||
class _ManagerModel:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def chat(self, _messages, _tools, *, on_token=None):
|
||||
self.calls += 1
|
||||
if self.calls == 1:
|
||||
return LLMResponse(
|
||||
content=(
|
||||
'{"route":{"specialist":"generalist","reason":"smalltalk"},'
|
||||
f'"dispatch":{{"instruction_text":"{instruction}"}}'
|
||||
"}"
|
||||
),
|
||||
tool_calls=[],
|
||||
)
|
||||
# Simulate finalize path echoing instruction.
|
||||
return LLMResponse(content=instruction, tool_calls=[])
|
||||
|
||||
class _Exec:
|
||||
def __init__(self, model=None):
|
||||
self.model = model
|
||||
self.tools = object()
|
||||
self.system_prompt = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
},
|
||||
)
|
||||
|
||||
# Specialist output also echoes instruction -> should still be suppressed.
|
||||
def _run_agent_core(**kwargs):
|
||||
class _Outcome:
|
||||
final_text = instruction
|
||||
|
||||
class _Out:
|
||||
outcome = _Outcome()
|
||||
|
||||
return _Out()
|
||||
|
||||
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
msg = StandardMessage(
|
||||
session_id="sid-echo-1",
|
||||
tenant_id="t1",
|
||||
user_id="u1",
|
||||
role="user",
|
||||
channel="admin_chat",
|
||||
text="你真厉害",
|
||||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
assert "用户说厉害" not in out.reply_text
|
||||
assert "请友好回应" not in out.reply_text
|
||||
assert out.reply_text == "抱歉,我暂时无法给出可展示的结果,请稍后再试。"
|
||||
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=object()), specialist_executor_factory=_factory)
|
||||
assert out.interaction_mode == "expert"
|
||||
assert out.dispatch_reason == "expert_direct"
|
||||
assert out.selected_specialist == "ops"
|
||||
assert chosen.get("sid") == "ops"
|
||||
assert captured.get("exec_text") == "edit this image background"
|
||||
assert captured.get("wire_policy_role") == "ops"
|
||||
assert out.reply_text == "specialist_answer"
|
||||
|
||||
|
||||
def test_gateway_command_hook_uses_parsed_command_and_context(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
|
|||
|
|
@ -97,11 +97,13 @@ def test_router_skill_signal_from_metadata() -> None:
|
|||
assert d.skill_signal == "skills=4"
|
||||
|
||||
|
||||
def test_interaction_mode_normalization_supports_legacy_values() -> None:
|
||||
assert normalize_interaction_mode("comprehensive") == "comprehensive"
|
||||
def test_interaction_mode_normalization_is_expert_only() -> None:
|
||||
assert normalize_interaction_mode("comprehensive") == "expert"
|
||||
assert normalize_interaction_mode("expert") == "expert"
|
||||
assert normalize_interaction_mode("specialist") == "expert"
|
||||
assert normalize_interaction_mode("composite") == "comprehensive"
|
||||
assert normalize_interaction_mode("composite") == "expert"
|
||||
assert normalize_interaction_mode("") == "expert"
|
||||
assert normalize_interaction_mode(None) == "expert"
|
||||
|
||||
|
||||
def test_requested_specialist_normalization_defaults_to_generalist() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue