sync update from LAN repo (source dhk-1 @ 3b1ec5b4)

This commit is contained in:
gozeros committed 2026-11-05 10:56:34 +08:00
1 parent da6521b6cb
commit 264e2200d6
995 files changed
+148105 -13399

No files matched your search

@@ -20,13 +20,20 @@ from types import SimpleNamespace
import pytest
import app.gateway.roundtable_model_fallback as mf
import app.gateway.routers.multi_agent as multi_agent
import app.gateway.routers.parallel_agents as parallel_agents
from app.gateway.roundtable_model_fallback import (
fallback_model_chain,
is_llm_error_message,
reason_text,
)
from app.gateway.routers._strategy_selection_seed import default_strategy_agent_roles
from app.gateway.routers._strategy_selection_seed import (
STRATEGY_INTENT_AGENT_ID,
STRATEGY_JUDGE_AGENT_ID,
STRATEGY_SECURITY_AGENT_ID,
STRATEGY_SOLUTION_AGENT_ID,
default_strategy_agent_roles,
)
from deerflow.agents.middlewares.llm_error_handling_middleware import (
LLM_ERROR_MARKER,
LLMErrorHandlingMiddleware,
@@ -248,6 +255,38 @@ def test_parallel_agent_substantive_content_accepts_chinese_and_english() -> Non
assert not parallel_agents._has_substantive_content("<think>internal reasoning only</think>!!!")
def test_strategy_stage_tool_surface_allows_clarification_only_for_intent() -> None:
assert "ask_clarification" not in parallel_agents._excluded_tools_for_agent(STRATEGY_INTENT_AGENT_ID)
for agent_id in (
STRATEGY_SECURITY_AGENT_ID,
STRATEGY_JUDGE_AGENT_ID,
STRATEGY_SOLUTION_AGENT_ID,
):
assert "ask_clarification" in parallel_agents._excluded_tools_for_agent(agent_id)
def test_strategy_task_message_matches_stage_clarification_contract() -> None:
judge_message = parallel_agents._build_task_message(
"策略评判",
"比较候选策略并输出结构化评分。",
agent_id=STRATEGY_JUDGE_AGENT_ID,
)
assert "【本阶段禁止反问】" in judge_message
assert "不得调用 `ask_clarification`" in judge_message
assert "数据限制与待核验" in judge_message
assert "不要重复输出同一正文或多个版本" in judge_message
intent_message = parallel_agents._build_task_message(
"意图理解",
"整理用户需求。",
agent_id=STRATEGY_INTENT_AGENT_ID,
)
assert "【需要向用户确认时】" in intent_message
assert "调用 `ask_clarification` 工具" in intent_message
assert "【本阶段禁止反问】" not in intent_message
def test_parallel_agent_extracts_provider_reasoning_without_mixing_answer() -> None:
answer, reasoning = parallel_agents._extract_ai_stream_parts(
{
@@ -779,6 +818,149 @@ def test_reason_text() -> None:
assert reason_text("不存在的码") == "调用失败"
@pytest.mark.asyncio
async def test_thinking_only_seat_does_not_switch_model(monkeypatch) -> None:
"""Reasoning-only output is an incomplete delivery, not a model failure."""
calls: list[str] = []
async def fake_stream(_client, _base, _headers, _thread_id, body):
calls.append(body["context"]["model_name"])
yield None, [
'data: {"messages":[{"type":"ai","content":"<think>仍在分析约束</think>"}]}'
]
monkeypatch.setattr(multi_agent, "_stream_upstream", fake_stream)
monkeypatch.setattr(
multi_agent,
"fallback_model_chain",
lambda _model: ["slow-thinking-model", "backup-model"],
)
monkeypatch.setattr(
multi_agent,
"append_seat_skill_directive",
lambda _agent_id, task, **_kwargs: task,
)
monkeypatch.setattr(multi_agent, "_spawn_detached_broadcast", lambda *_args, **_kwargs: None)
frames = [
frame
async for frame in multi_agent._special_run(
client=object(),
base="http://gateway",
headers={},
agent_threads={"seat-a": "seat-thread"},
agent_name="seat-a",
new_message="完成分析",
skill_stop_names=[],
model_name="slow-thinking-model",
)
]
assert calls == ["slow-thinking-model"]
assert not any(
isinstance(frame, dict) and frame.get("status") == "model_switch"
for frame in frames
)
assert frames[-1]["status"] == "invalid_delivery"
assert frames[-1]["reason"] == "thinking_only"
@pytest.mark.asyncio
async def test_real_model_error_still_switches_to_next_model(monkeypatch) -> None:
calls: list[str] = []
async def fake_stream(_client, _base, _headers, _thread_id, body):
model = body["context"]["model_name"]
calls.append(model)
message = (
{
"type": "ai",
"content": "大模型服务繁忙,请稍后再试。",
"additional_kwargs": {LLM_ERROR_MARKER: "busy"},
}
if model == "broken-model"
else {"type": "ai", "content": "这是备用模型完成的有效交付正文。"}
)
yield None, [f"data: {json.dumps({'messages': [message]}, ensure_ascii=False)}"]
monkeypatch.setattr(multi_agent, "_stream_upstream", fake_stream)
monkeypatch.setattr(
multi_agent,
"fallback_model_chain",
lambda _model: ["broken-model", "backup-model"],
)
monkeypatch.setattr(
multi_agent,
"append_seat_skill_directive",
lambda _agent_id, task, **_kwargs: task,
)
monkeypatch.setattr(multi_agent, "_spawn_detached_broadcast", lambda *_args, **_kwargs: None)
frames = [
frame
async for frame in multi_agent._special_run(
client=object(),
base="http://gateway",
headers={},
agent_threads={"seat-a": "seat-thread"},
agent_name="seat-a",
new_message="完成分析",
skill_stop_names=[],
model_name="broken-model",
)
]
assert calls == ["broken-model", "backup-model"]
assert any(
isinstance(frame, dict) and frame.get("status") == "model_switch"
for frame in frames
)
assert frames[-1]["model_used"] == "backup-model"
assert frames[-1]["content"] == "这是备用模型完成的有效交付正文。"
@pytest.mark.asyncio
async def test_background_thinking_only_does_not_switch_model(monkeypatch) -> None:
import app.gateway.roundtable_inprocess_gateway as gateway_module
calls: list[str] = []
async def fake_run_agent_turn(*_args, **kwargs):
calls.append(kwargs["model"])
return [{"type": "ai", "content": "<think>继续进行长思考</think>"}]
monkeypatch.setattr(gateway_module, "_run_agent_turn", fake_run_agent_turn)
monkeypatch.setattr(
gateway_module,
"fallback_model_chain",
lambda _model: ["slow-thinking-model", "backup-model"],
)
gateway = gateway_module.InProcessRoundtableGateway(
SimpleNamespace(state=SimpleNamespace()),
user_id="u1",
agents=[],
)
messages, switches, used_model = await gateway._run_turn_with_fallback(
thread_id="seat-thread",
agent_name="seat-a",
message="完成分析",
requested_model="slow-thinking-model",
excluded_tools=[],
allowed_tools=None,
skill_stop_names=[],
thinking_enabled=True,
reasoning_effort="medium",
recursion_limit=1000,
stage="step2_seat",
)
assert calls == ["slow-thinking-model"]
assert switches == []
assert used_model == "slow-thinking-model"
assert messages[-1]["content"] == "<think>继续进行长思考</think>"
# ── 引擎:单席位异常隔离 + 模型容错记录 → 系统提示气泡 ──────────────────────────
_AGENTS = [SeatRef("seat-a", "情报"), SeatRef("seat-b", "环境"), SeatRef("seat-c", "方案")]
@@ -800,7 +982,15 @@ class _ChainGateway:
self.switch_agent = switch_agent
self.seen: list[str] = []
async def run_leader(self, *, thread_ids, coordinator_name, message, model) -> LeaderTurn:
async def run_leader(
self,
*,
thread_ids,
coordinator_name,
message,
model,
finalize_only: bool = False,
) -> LeaderTurn:
return LeaderTurn(final_text="总控说明", dispatched=[])
async def run_seat(self, *, thread_ids, agent_id, task, model) -> SeatTurn: