fix:timinggate非法工具会重试
This commit is contained in:
@@ -3,7 +3,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.tooling import ToolExecutionResult
|
||||
from src.core.tooling import ToolExecutionResult, ToolInvocation
|
||||
from src.llm_models.payload_content.tool_option import ToolCall
|
||||
from src.maisaka.chat_loop_service import ChatResponse, MaisakaChatLoopService
|
||||
from src.maisaka.context_messages import AssistantMessage, TIMING_GATE_INVALID_TOOL_HINT_SOURCE
|
||||
@@ -45,8 +45,12 @@ async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest
|
||||
runtime._enter_stop_state = _enter_stop_state
|
||||
engine = MaisakaReasoningEngine(runtime) # type: ignore[arg-type]
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def _fake_timing_gate_sub_agent(**kwargs: object) -> ChatResponse:
|
||||
nonlocal call_count
|
||||
del kwargs
|
||||
call_count += 1
|
||||
return _build_chat_response([
|
||||
ToolCall(call_id="invalid-timing-tool", func_name="finish", args={}),
|
||||
])
|
||||
@@ -61,6 +65,7 @@ async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest
|
||||
action, response, tool_results, tool_monitor_results = await engine._run_timing_gate(object()) # type: ignore[arg-type]
|
||||
|
||||
assert action == "no_reply"
|
||||
assert call_count == 3
|
||||
assert response.tool_calls[0].func_name == "finish"
|
||||
assert runtime.stopped is True
|
||||
assert tool_monitor_results == []
|
||||
@@ -68,10 +73,72 @@ async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest
|
||||
assert runtime._chat_history[0].source == TIMING_GATE_INVALID_TOOL_HINT_SOURCE
|
||||
assert "finish" in runtime._chat_history[0].processed_plain_text
|
||||
assert tool_results == [
|
||||
"- retry [非法 Timing 工具]: 返回了 finish,将重试 (1/3)",
|
||||
"- retry [非法 Timing 工具]: 返回了 finish,将重试 (2/3)",
|
||||
"- no_reply [非法 Timing 工具]: 返回了 finish,已停止本轮并等待新消息",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timing_gate_invalid_tool_retries_until_valid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
runtime = SimpleNamespace(
|
||||
_force_next_timing_continue=False,
|
||||
_chat_history=[],
|
||||
log_prefix="[test]",
|
||||
stopped=False,
|
||||
)
|
||||
|
||||
def _enter_stop_state() -> None:
|
||||
runtime.stopped = True
|
||||
|
||||
runtime._enter_stop_state = _enter_stop_state
|
||||
engine = MaisakaReasoningEngine(runtime) # type: ignore[arg-type]
|
||||
responses = [
|
||||
_build_chat_response([ToolCall(call_id="invalid-timing-tool", func_name="finish", args={})]),
|
||||
_build_chat_response([ToolCall(call_id="valid-timing-tool", func_name="continue", args={})]),
|
||||
]
|
||||
|
||||
async def _fake_timing_gate_sub_agent(**kwargs: object) -> ChatResponse:
|
||||
del kwargs
|
||||
return responses.pop(0)
|
||||
|
||||
async def _fake_invoke_tool_call(
|
||||
tool_call: ToolCall,
|
||||
latest_thought: str,
|
||||
anchor_message: object,
|
||||
*,
|
||||
append_history: bool = True,
|
||||
store_record: bool = True,
|
||||
) -> tuple[ToolInvocation, ToolExecutionResult, None]:
|
||||
del latest_thought, anchor_message, append_history, store_record
|
||||
return (
|
||||
ToolInvocation(tool_name=tool_call.func_name, call_id=tool_call.call_id),
|
||||
ToolExecutionResult(
|
||||
tool_name=tool_call.func_name,
|
||||
success=True,
|
||||
content="继续执行主流程",
|
||||
metadata={"timing_action": "continue"},
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(engine, "_run_timing_gate_sub_agent", _fake_timing_gate_sub_agent)
|
||||
monkeypatch.setattr(engine, "_invoke_tool_call", _fake_invoke_tool_call)
|
||||
|
||||
action, response, tool_results, tool_monitor_results = await engine._run_timing_gate(object()) # type: ignore[arg-type]
|
||||
|
||||
assert action == "continue"
|
||||
assert response.tool_calls[0].func_name == "continue"
|
||||
assert runtime.stopped is False
|
||||
assert len(runtime._chat_history) == 2
|
||||
assert all(message.source != TIMING_GATE_INVALID_TOOL_HINT_SOURCE for message in runtime._chat_history)
|
||||
assert tool_results == [
|
||||
"- retry [非法 Timing 工具]: 返回了 finish,将重试 (1/3)",
|
||||
"- continue [成功]: 继续执行主流程",
|
||||
]
|
||||
assert tool_monitor_results[0]["tool_name"] == "continue"
|
||||
|
||||
|
||||
def test_timing_gate_invalid_tool_hint_keeps_only_latest() -> None:
|
||||
old_hint = SimpleNamespace(source=TIMING_GATE_INVALID_TOOL_HINT_SOURCE)
|
||||
runtime = SimpleNamespace(_chat_history=[old_hint])
|
||||
|
||||
@@ -36,7 +36,6 @@ class ReplyerManager:
|
||||
generator_type = self._get_maisaka_generator_type() if replyer_type == "maisaka" else ""
|
||||
cache_key = f"{replyer_type}:{generator_type}:{stream_id}"
|
||||
if cache_key in self._repliers:
|
||||
logger.info(f"[ReplyerManager] 命中缓存 replyer: cache_key={cache_key}")
|
||||
return self._repliers[cache_key]
|
||||
|
||||
target_stream = chat_stream or _chat_manager.get_session_by_session_id(stream_id)
|
||||
|
||||
@@ -54,6 +54,7 @@ logger = get_logger("maisaka_reasoning_engine")
|
||||
|
||||
TIMING_GATE_CONTEXT_LIMIT = 24
|
||||
TIMING_GATE_MAX_TOKENS = 384
|
||||
TIMING_GATE_MAX_ATTEMPTS = 3
|
||||
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply", "wait"}
|
||||
HISTORY_SILENT_TOOL_NAMES = {"finish"}
|
||||
|
||||
@@ -247,36 +248,69 @@ class MaisakaReasoningEngine:
|
||||
if self._runtime._force_next_timing_continue:
|
||||
return self._build_forced_continue_timing_result()
|
||||
|
||||
response = await self._run_timing_gate_sub_agent(
|
||||
context_message_limit=TIMING_GATE_CONTEXT_LIMIT,
|
||||
system_prompt=self._build_timing_gate_system_prompt(),
|
||||
tool_definitions=get_timing_tools(),
|
||||
)
|
||||
tool_result_summaries: list[str] = []
|
||||
tool_monitor_results: list[dict[str, Any]] = []
|
||||
response: Any = None
|
||||
selected_tool_call: Optional[ToolCall] = None
|
||||
for tool_call in response.tool_calls:
|
||||
if tool_call.func_name in TIMING_GATE_TOOL_NAMES:
|
||||
selected_tool_call = tool_call
|
||||
invalid_tool_text = ""
|
||||
for attempt_index in range(TIMING_GATE_MAX_ATTEMPTS):
|
||||
response = await self._run_timing_gate_sub_agent(
|
||||
context_message_limit=TIMING_GATE_CONTEXT_LIMIT,
|
||||
system_prompt=self._build_timing_gate_system_prompt(),
|
||||
tool_definitions=get_timing_tools(),
|
||||
)
|
||||
selected_tool_call = None
|
||||
for tool_call in response.tool_calls:
|
||||
if tool_call.func_name in TIMING_GATE_TOOL_NAMES:
|
||||
selected_tool_call = tool_call
|
||||
break
|
||||
|
||||
if selected_tool_call is not None:
|
||||
break
|
||||
|
||||
if selected_tool_call is None:
|
||||
invalid_tool_names = [
|
||||
str(tool_call.func_name).strip()
|
||||
for tool_call in response.tool_calls
|
||||
if str(tool_call.func_name).strip()
|
||||
]
|
||||
invalid_tool_text = "、".join(invalid_tool_names) if invalid_tool_names else "无工具"
|
||||
logger.warning(
|
||||
f"{self._runtime.log_prefix} Timing Gate 未返回有效控制工具:{invalid_tool_text},将按 no_reply 处理"
|
||||
)
|
||||
self._append_timing_gate_invalid_tool_hint(invalid_tool_text)
|
||||
remaining_attempts = TIMING_GATE_MAX_ATTEMPTS - attempt_index - 1
|
||||
if remaining_attempts > 0:
|
||||
logger.warning(
|
||||
f"{self._runtime.log_prefix} Timing Gate 未返回有效控制工具:{invalid_tool_text},"
|
||||
f"将重试 ({attempt_index + 1}/{TIMING_GATE_MAX_ATTEMPTS})"
|
||||
)
|
||||
tool_result_summaries.append(
|
||||
f"- retry [非法 Timing 工具]: 返回了 {invalid_tool_text},将重试 "
|
||||
f"({attempt_index + 1}/{TIMING_GATE_MAX_ATTEMPTS})"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.warning(
|
||||
f"{self._runtime.log_prefix} Timing Gate 连续 {TIMING_GATE_MAX_ATTEMPTS} 次未返回有效控制工具:"
|
||||
f"{invalid_tool_text},将按 no_reply 处理"
|
||||
)
|
||||
self._runtime._enter_stop_state()
|
||||
tool_result_summaries.append(
|
||||
f"- no_reply [非法 Timing 工具]: 返回了 {invalid_tool_text},已停止本轮并等待新消息"
|
||||
)
|
||||
return "no_reply", response, tool_result_summaries, tool_monitor_results
|
||||
|
||||
if selected_tool_call is None:
|
||||
self._runtime._enter_stop_state()
|
||||
tool_result_summaries.append(
|
||||
"- no_reply [非法 Timing 工具]: 已停止本轮并等待新消息"
|
||||
)
|
||||
return "no_reply", response, tool_result_summaries, tool_monitor_results
|
||||
|
||||
if invalid_tool_text:
|
||||
self._runtime._chat_history = [
|
||||
message
|
||||
for message in self._runtime._chat_history
|
||||
if message.source != TIMING_GATE_INVALID_TOOL_HINT_SOURCE
|
||||
]
|
||||
|
||||
append_history = False
|
||||
store_record = selected_tool_call.func_name != "continue"
|
||||
invocation, result, tool_spec = await self._invoke_tool_call(
|
||||
|
||||
Reference in New Issue
Block a user