feat: add tool-call fallback and resilient batch exports
This commit is contained in:
1 parent
ab066733b8
commit
d31eb2a19d
32 files changed
+538
-72
No files matched your search
@@ -384,7 +384,7 @@ def _make_runner(mock_context, responses, max_turns=None):
|
||||
pending = list(responses)
|
||||
|
||||
async def _invoke(llm, tools, messages):
|
||||
runner.calls.append({"tools": list(tools), "messages": list(messages)})
|
||||
runner.calls.append({"llm": llm, "tools": list(tools), "messages": list(messages)})
|
||||
return pending.pop(0)
|
||||
|
||||
runner._invoke_model = AsyncMock(side_effect=_invoke)
|
||||
@@ -647,9 +647,11 @@ async def test_flash_stops_repeated_no_tool_responses(mock_context, monkeypatch,
|
||||
from artemis.config.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "ARTEMIS_FLASH_MAX_NO_PROGRESS_ATTEMPTS", attempts)
|
||||
responses = [AIMessage(content="I will click now.", tool_calls=[]) for _ in range(attempts)]
|
||||
monkeypatch.setattr(settings, "ARTEMIS_TOOL_FALLBACK_MAX_NO_PROGRESS_ATTEMPTS", 3)
|
||||
responses = [AIMessage(content="I will click now.", tool_calls=[]) for _ in range(attempts + 3)]
|
||||
with (
|
||||
patch("artemis.controllers.unified_controller.get_driver"),
|
||||
patch("artemis.agents.flash.runner.get_llm", return_value=Mock()) as get_fallback,
|
||||
patch(
|
||||
"artemis.agents.flash.runner.capture_screenshot_and_parse_ui",
|
||||
AsyncMock(return_value=_INITIAL_OBSERVATION),
|
||||
@@ -659,11 +661,55 @@ async def test_flash_stops_repeated_no_tool_responses(mock_context, monkeypatch,
|
||||
result = await runner.run(State(initial_goal="Open Settings"))
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert f"连续 {attempts} 次未调用工具" in result["explanation"]
|
||||
assert runner._invoke_model.await_count == attempts
|
||||
assert f"连续 {attempts} 次" in result["explanation"]
|
||||
assert "兜底模型连续 3 次" in result["explanation"]
|
||||
assert runner._invoke_model.await_count == attempts + 3
|
||||
get_fallback.assert_called_once_with(mock_context, name="operator", use_fallback=True)
|
||||
runner.executor.execute.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flash_uses_fallback_for_one_tool_turn_then_primary(mock_context):
|
||||
no_call = AIMessage(content="I will click now.")
|
||||
observe = AIMessage(
|
||||
content="Check the target.",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "ask_explorer",
|
||||
"args": {"question": "Find Settings"},
|
||||
"id": "fallback-observe",
|
||||
}
|
||||
],
|
||||
)
|
||||
click = AIMessage(
|
||||
content="Tap Settings.",
|
||||
tool_calls=[{"name": "click", "args": {"target": 1}, "id": "fallback-click"}],
|
||||
)
|
||||
fallback = Mock()
|
||||
responses = [no_call, no_call, no_call, observe, click, _report()]
|
||||
with (
|
||||
patch("artemis.controllers.unified_controller.get_driver"),
|
||||
patch("artemis.agents.flash.runner.get_llm", return_value=fallback),
|
||||
patch(
|
||||
"artemis.agents.flash.runner.capture_screenshot_and_parse_ui",
|
||||
AsyncMock(return_value=_INITIAL_OBSERVATION),
|
||||
),
|
||||
):
|
||||
runner = _make_runner(mock_context, responses, max_turns=0)
|
||||
result = await runner.run(State(initial_goal="Open Settings"))
|
||||
|
||||
primary = runner.calls[0]["llm"]
|
||||
assert [call["llm"] for call in runner.calls] == [
|
||||
primary,
|
||||
primary,
|
||||
primary,
|
||||
fallback,
|
||||
fallback,
|
||||
primary,
|
||||
]
|
||||
assert result["status"] == "completed"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _TurnRecord.result: the turn's execution result in the validator-report shape
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user