sync update from LAN repo (source dhk-1 @ 3b1ec5b4)
This commit is contained in:
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:
|
||||
|
||||
Reference in new issue
Block a user