fix: normalize Pro action targets and bound no-progress recovery
This commit is contained in:
1 parent
2ec563c079
commit
9d6833f092
7 files changed
+225
-20
No files matched your search
@@ -16,6 +16,8 @@ OPENAI_REASONING_EFFORT=high
|
|||||||
ARTEMIS_VIDEO_ENABLED=false
|
ARTEMIS_VIDEO_ENABLED=false
|
||||||
ARTEMIS_OUTPUTTER_ENABLED=true
|
ARTEMIS_OUTPUTTER_ENABLED=true
|
||||||
ARTEMIS_STEP_SUMMARIZER_ENABLED=true
|
ARTEMIS_STEP_SUMMARIZER_ENABLED=true
|
||||||
|
# Pro 连续无工具调用或参数校验失败的最大次数;耗尽后明确失败,避免空转。
|
||||||
|
ARTEMIS_OPERATOR_MAX_NO_PROGRESS_ATTEMPTS=3
|
||||||
|
|
||||||
# 视频分段、重试与单次分析超时(秒)
|
# 视频分段、重试与单次分析超时(秒)
|
||||||
ARTEMIS_VIDEO_CHUNK_SECONDS=60
|
ARTEMIS_VIDEO_CHUNK_SECONDS=60
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ from uuid import uuid4
|
|||||||
from langchain_core.messages import SystemMessage, ToolMessage
|
from langchain_core.messages import SystemMessage, ToolMessage
|
||||||
from langchain_core.tools import BaseTool
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
from artemis.core.tool_failure import is_tool_failure
|
from artemis.agents.validator.tool_declarations import normalize_coordinate_target
|
||||||
|
from artemis.config.settings import settings
|
||||||
from artemis.context import ArtemisContext
|
from artemis.context import ArtemisContext
|
||||||
|
from artemis.core.tool_failure import is_tool_failure
|
||||||
from artemis.data_engine.trace import (
|
from artemis.data_engine.trace import (
|
||||||
TraceSpan,
|
TraceSpan,
|
||||||
trace,
|
trace,
|
||||||
@@ -57,6 +59,11 @@ from artemis.utils.visualization import format_minimal_list_with_elements
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OperatorNoProgressError(RuntimeError):
|
||||||
|
"""The model repeatedly returned no usable tool call; stop without device actions."""
|
||||||
|
|
||||||
|
|
||||||
DEFERRING_TOOLS = {
|
DEFERRING_TOOLS = {
|
||||||
"ask_diagnoser",
|
"ask_diagnoser",
|
||||||
"video_analyzer",
|
"video_analyzer",
|
||||||
@@ -485,6 +492,8 @@ class OperatorNode:
|
|||||||
native_thoughts = []
|
native_thoughts = []
|
||||||
tool_limit_exceeded = False
|
tool_limit_exceeded = False
|
||||||
plan_gate_bounced = False
|
plan_gate_bounced = False
|
||||||
|
no_progress_attempts = 0
|
||||||
|
no_progress_limit = settings.ARTEMIS_OPERATOR_MAX_NO_PROGRESS_ATTEMPTS
|
||||||
|
|
||||||
for iteration in range(max_iterations):
|
for iteration in range(max_iterations):
|
||||||
if iteration == max_iterations - 1:
|
if iteration == max_iterations - 1:
|
||||||
@@ -568,8 +577,27 @@ class OperatorNode:
|
|||||||
logger.error(f"Failed to parse function_call fallback: {e}")
|
logger.error(f"Failed to parse function_call fallback: {e}")
|
||||||
|
|
||||||
if not response.tool_calls:
|
if not response.tool_calls:
|
||||||
logger.warning("LLM stopped without calling any tool. Encouraging action.")
|
no_progress_attempts += 1
|
||||||
break
|
if no_progress_attempts >= no_progress_limit:
|
||||||
|
raise OperatorNoProgressError(
|
||||||
|
f"模型连续 {no_progress_attempts} 次未提供可执行的工具调用,"
|
||||||
|
"已停止任务以避免空转;未执行新的设备动作。"
|
||||||
|
)
|
||||||
|
logger.warning("LLM returned no tool call; requesting a concrete tool call.")
|
||||||
|
current_messages.append(response)
|
||||||
|
current_messages.append(
|
||||||
|
SystemMessage(
|
||||||
|
content=(
|
||||||
|
"No tool call was returned, so no device action occurred. "
|
||||||
|
"Use the available tool interface now, not a prose promise or JSON "
|
||||||
|
"inside text. Use an index from the current element list or "
|
||||||
|
"normalized coordinates with target_description. If blocked, "
|
||||||
|
"use an observation/diagnostic tool; do not invent a click or "
|
||||||
|
"claim success. This recovery has a limited retry budget."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
current_messages.append(response)
|
current_messages.append(response)
|
||||||
|
|
||||||
@@ -840,9 +868,20 @@ class OperatorNode:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if validation_errors:
|
if validation_errors:
|
||||||
|
no_progress_attempts += 1
|
||||||
|
if no_progress_attempts >= no_progress_limit:
|
||||||
|
raise OperatorNoProgressError(
|
||||||
|
f"模型连续 {no_progress_attempts} 次未提供可执行的工具调用,"
|
||||||
|
"工具参数校验仍失败;已停止任务,未执行被拒绝的设备动作。"
|
||||||
|
)
|
||||||
logger.warning("Some action calls failed validation. Feeding back to LLM.")
|
logger.warning("Some action calls failed validation. Feeding back to LLM.")
|
||||||
continue
|
continue
|
||||||
|
no_progress_attempts = 0
|
||||||
else:
|
else:
|
||||||
|
if no_progress_attempts:
|
||||||
|
raise OperatorNoProgressError(
|
||||||
|
"模型在本轮工具调用预算内未恢复有效调用,已停止任务以避免空转。"
|
||||||
|
)
|
||||||
tool_limit_exceeded = True
|
tool_limit_exceeded = True
|
||||||
|
|
||||||
raw_thinking = "\n".join(raw_thoughts) if raw_thoughts else None
|
raw_thinking = "\n".join(raw_thoughts) if raw_thoughts else None
|
||||||
@@ -1122,6 +1161,9 @@ class OperatorNode:
|
|||||||
straight from the indexed list); a coordinate pair carries only the
|
straight from the indexed list); a coordinate pair carries only the
|
||||||
model's own ``target_description``. The two never mix.
|
model's own ``target_description``. The two never mix.
|
||||||
"""
|
"""
|
||||||
|
target = normalize_coordinate_target(target)
|
||||||
|
if isinstance(target, bool):
|
||||||
|
return None, "Error: Boolean values are not valid target indices."
|
||||||
if isinstance(target, (int, float)):
|
if isinstance(target, (int, float)):
|
||||||
try:
|
try:
|
||||||
target_int = int(target)
|
target_int = int(target)
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ class Settings(BaseSettings):
|
|||||||
ARTEMIS_VIDEO_ENABLED: bool = False
|
ARTEMIS_VIDEO_ENABLED: bool = False
|
||||||
ARTEMIS_OUTPUTTER_ENABLED: bool = True
|
ARTEMIS_OUTPUTTER_ENABLED: bool = True
|
||||||
ARTEMIS_STEP_SUMMARIZER_ENABLED: bool = True
|
ARTEMIS_STEP_SUMMARIZER_ENABLED: bool = True
|
||||||
|
# Consecutive Pro responses without a valid tool call before failing the task.
|
||||||
|
ARTEMIS_OPERATOR_MAX_NO_PROGRESS_ATTEMPTS: int = Field(default=3, ge=1, le=20)
|
||||||
|
|
||||||
# Android ADB Connectivity
|
# Android ADB Connectivity
|
||||||
ADB_HOST: str | None = Field(default=DEFAULT_ADB_HOST)
|
ADB_HOST: str | None = Field(default=DEFAULT_ADB_HOST)
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ async def test_operator_node_multiple_actions():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_operator_node_no_record_step():
|
async def test_operator_node_no_record_step():
|
||||||
from artemis.agents.operator.operator import OperatorNode
|
from artemis.agents.operator.operator import OperatorNode, OperatorNoProgressError
|
||||||
from artemis.context import ArtemisContext
|
from artemis.context import ArtemisContext
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
@@ -437,13 +437,11 @@ async def test_operator_node_no_record_step():
|
|||||||
|
|
||||||
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
||||||
node = OperatorNode(mock_ctx, transcript_config=LEGACY_TRANSCRIPT)
|
node = OperatorNode(mock_ctx, transcript_config=LEGACY_TRANSCRIPT)
|
||||||
node_update = await node(mock_state)
|
with pytest.raises(OperatorNoProgressError):
|
||||||
|
await node(mock_state)
|
||||||
|
|
||||||
mock_ctx.data_engine.record_step.assert_not_called()
|
mock_ctx.data_engine.record_step.assert_not_called()
|
||||||
|
|
||||||
update_dict = node_update
|
|
||||||
assert "structured_decisions" in update_dict
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_operator_dynamic_prompt():
|
async def test_operator_dynamic_prompt():
|
||||||
@@ -472,7 +470,13 @@ async def test_operator_dynamic_prompt():
|
|||||||
|
|
||||||
mock_llm = MagicMock()
|
mock_llm = MagicMock()
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.tool_calls = []
|
mock_response.tool_calls = [
|
||||||
|
{
|
||||||
|
"name": "click",
|
||||||
|
"args": {"target": [50, 50], "target_description": "button"},
|
||||||
|
"id": "call_click",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
async def mock_ainvoke(*args, **kwargs):
|
async def mock_ainvoke(*args, **kwargs):
|
||||||
messages = args[0]
|
messages = args[0]
|
||||||
@@ -574,7 +578,13 @@ async def test_operator_history_compression():
|
|||||||
|
|
||||||
mock_llm = MagicMock()
|
mock_llm = MagicMock()
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.tool_calls = []
|
mock_response.tool_calls = [
|
||||||
|
{
|
||||||
|
"name": "click",
|
||||||
|
"args": {"target": [50, 50], "target_description": "button"},
|
||||||
|
"id": "call_click",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
# Smarter file reading mock
|
# Smarter file reading mock
|
||||||
def mock_read_text(path_self, encoding=None):
|
def mock_read_text(path_self, encoding=None):
|
||||||
|
|||||||
@@ -69,8 +69,14 @@ def _state():
|
|||||||
def _llm(captured: list, usage_metadata=None):
|
def _llm(captured: list, usage_metadata=None):
|
||||||
mock_llm = MagicMock()
|
mock_llm = MagicMock()
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.tool_calls = []
|
response.tool_calls = [
|
||||||
response.content = "no action this turn"
|
{
|
||||||
|
"name": "click",
|
||||||
|
"args": {"target": [50, 50], "target_description": "button"},
|
||||||
|
"id": "call_click",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
response.content = "Click the button."
|
||||||
# usage_from_message only trusts a non-empty dict; a MagicMock attribute
|
# usage_from_message only trusts a non-empty dict; a MagicMock attribute
|
||||||
# (the default) reads as "no usage reported".
|
# (the default) reads as "no usage reported".
|
||||||
if usage_metadata is not None:
|
if usage_metadata is not None:
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""Regression coverage for quoted click targets and prose-only recovery loops."""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
|
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from artemis.agents.operator.operator import OperatorNode, OperatorNoProgressError
|
||||||
|
from artemis.config.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def operator(monkeypatch):
|
||||||
|
node = object.__new__(OperatorNode)
|
||||||
|
node.ctx = SimpleNamespace(transcript_ledger=None)
|
||||||
|
node._available_device_actions = lambda: {"click", "long_press", "input_text"}
|
||||||
|
node._plan_changed_this_turn = lambda: True
|
||||||
|
node._max_burst_actions = lambda: 4
|
||||||
|
state = SimpleNamespace(
|
||||||
|
operator_raw_data={"width": 1280, "height": 2856},
|
||||||
|
indexed_elements=[{"center": [310, 2610], "text": "model menu", "resource_id": "model"}]
|
||||||
|
* 13,
|
||||||
|
open_incident=None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(settings, "ARTEMIS_OPERATOR_MAX_NO_PROGRESS_ATTEMPTS", 3)
|
||||||
|
return node, state
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["click", "long_press", "input_text"])
|
||||||
|
@pytest.mark.parametrize("target", ["13", "[240, 914]", "240, 914"])
|
||||||
|
def test_quoted_targets_keep_same_action_and_grounding(operator, name, target):
|
||||||
|
node, state = operator
|
||||||
|
expected_target = 13 if target == "13" else [240, 914]
|
||||||
|
args = {"target": target, "target_description": "model menu", "text": "test"}
|
||||||
|
actions, error = node._translate_and_validate_tool({"name": name, "args": args}, state)
|
||||||
|
expected, _ = node._translate_and_validate_tool(
|
||||||
|
{"name": name, "args": {**args, "target": expected_target}}, state
|
||||||
|
)
|
||||||
|
assert error is None
|
||||||
|
assert actions == expected
|
||||||
|
if target == "13":
|
||||||
|
assert actions[0]["target_resource_id"] == "model"
|
||||||
|
else:
|
||||||
|
assert actions[0]["target_description"] == "model menu"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target", ["0", "14", "model menu", True, "true"])
|
||||||
|
def test_invalid_index_is_not_guessed_or_dispatched(operator, target):
|
||||||
|
node, state = operator
|
||||||
|
actions, error = node._translate_and_validate_tool(
|
||||||
|
{"name": "click", "args": {"target": target}}, state
|
||||||
|
)
|
||||||
|
assert not actions
|
||||||
|
assert error
|
||||||
|
|
||||||
|
|
||||||
|
def test_quoted_coordinates_still_need_description(operator):
|
||||||
|
node, state = operator
|
||||||
|
actions, error = node._translate_and_validate_tool(
|
||||||
|
{"name": "click", "args": {"target": "[240, 914]"}}, state
|
||||||
|
)
|
||||||
|
assert not actions
|
||||||
|
assert "target_description" in error
|
||||||
|
|
||||||
|
|
||||||
|
def click(target="13"):
|
||||||
|
return AIMessage(
|
||||||
|
content="Open the model menu.",
|
||||||
|
tool_calls=[{"name": "click", "args": {"target": target}, "id": "call_click"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_loop(operator, monkeypatch, responses):
|
||||||
|
node, state = operator
|
||||||
|
invoke = AsyncMock(side_effect=responses)
|
||||||
|
monkeypatch.setattr("artemis.agents.operator.operator.acomplete", invoke)
|
||||||
|
messages = []
|
||||||
|
result = await node._invoke_llm_loop(Mock(), messages, [], [], state)
|
||||||
|
return result, messages, invoke
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prose_retry_keeps_context_and_recovers_once(operator, monkeypatch):
|
||||||
|
prose = AIMessage(content="I will click the model menu.")
|
||||||
|
result, messages, invoke = await run_loop(operator, monkeypatch, [prose, click()])
|
||||||
|
assert invoke.await_count == 2
|
||||||
|
assert len(result[0]) == 1
|
||||||
|
assert result[0][0]["coordinates"] == [310, 2610]
|
||||||
|
assert prose in messages
|
||||||
|
assert any(isinstance(m, SystemMessage) and "No tool call" in m.content for m in messages)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_invalid_target_can_be_corrected_before_limit(operator, monkeypatch):
|
||||||
|
result, messages, invoke = await run_loop(operator, monkeypatch, [click("14"), click("13")])
|
||||||
|
assert invoke.await_count == 2
|
||||||
|
assert len(result[0]) == 1
|
||||||
|
assert result[0][0]["coordinates"] == [310, 2610]
|
||||||
|
outputs = [m for m in messages if isinstance(m, ToolMessage)]
|
||||||
|
assert [m.status for m in outputs] == ["error", "success"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("limit", [1, 3])
|
||||||
|
async def test_empty_responses_fail_at_configured_limit(operator, monkeypatch, limit):
|
||||||
|
monkeypatch.setattr(settings, "ARTEMIS_OPERATOR_MAX_NO_PROGRESS_ATTEMPTS", limit)
|
||||||
|
invoke = AsyncMock(return_value=AIMessage(content="I will click now."))
|
||||||
|
monkeypatch.setattr("artemis.agents.operator.operator.acomplete", invoke)
|
||||||
|
node, state = operator
|
||||||
|
with pytest.raises(OperatorNoProgressError, match="未提供可执行的工具调用"):
|
||||||
|
await node._invoke_llm_loop(Mock(), [], [], [], state)
|
||||||
|
assert invoke.await_count == limit
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_invalid_calls_and_prose_share_bounded_recovery(operator, monkeypatch):
|
||||||
|
invoke = AsyncMock(side_effect=[click("missing"), AIMessage(content="Retry."), click("14")])
|
||||||
|
monkeypatch.setattr("artemis.agents.operator.operator.acomplete", invoke)
|
||||||
|
node, state = operator
|
||||||
|
messages = []
|
||||||
|
with pytest.raises(OperatorNoProgressError, match="校验仍失败"):
|
||||||
|
await node._invoke_llm_loop(Mock(), messages, [], [], state)
|
||||||
|
assert invoke.await_count == 3
|
||||||
|
assert any(isinstance(m, ToolMessage) and m.status == "error" for m in messages)
|
||||||
|
assert not any(isinstance(m, ToolMessage) and m.content == "Action Recorded" for m in messages)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_iteration_cap_cannot_restart_no_progress_next_turn(operator, monkeypatch):
|
||||||
|
monkeypatch.setattr("artemis.agents.operator.operator.OPERATOR_MAX_TOOL_ITERATIONS", 2)
|
||||||
|
invoke = AsyncMock(return_value=AIMessage(content="I will click now."))
|
||||||
|
monkeypatch.setattr("artemis.agents.operator.operator.acomplete", invoke)
|
||||||
|
node, state = operator
|
||||||
|
with pytest.raises(OperatorNoProgressError, match="预算内未恢复"):
|
||||||
|
await node._invoke_llm_loop(Mock(), [], [], [], state)
|
||||||
|
assert invoke.await_count == 2
|
||||||
@@ -141,11 +141,17 @@ def _transcript_state(**overrides):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
def _no_action_llm(captured: list):
|
def _action_llm(captured: list):
|
||||||
mock_llm = MagicMock()
|
mock_llm = MagicMock()
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.tool_calls = []
|
mock_response.tool_calls = [
|
||||||
mock_response.content = "no action this turn"
|
{
|
||||||
|
"name": "click",
|
||||||
|
"args": {"target": [50, 50], "target_description": "button"},
|
||||||
|
"id": "call_click",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_response.content = "Click the button."
|
||||||
|
|
||||||
async def mock_ainvoke(*args, **kwargs):
|
async def mock_ainvoke(*args, **kwargs):
|
||||||
captured.append(list(args[0]))
|
captured.append(list(args[0]))
|
||||||
@@ -166,7 +172,7 @@ async def test_transcript_mode_two_turns_build_four_regions(tmp_path):
|
|||||||
ctx.data_engine.global_base_dir = str(tmp_path)
|
ctx.data_engine.global_base_dir = str(tmp_path)
|
||||||
ctx.data_engine.get_agent_friendly_steps.return_value = []
|
ctx.data_engine.get_agent_friendly_steps.return_value = []
|
||||||
captured: list = []
|
captured: list = []
|
||||||
mock_llm = _no_action_llm(captured)
|
mock_llm = _action_llm(captured)
|
||||||
|
|
||||||
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
||||||
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
||||||
@@ -237,7 +243,7 @@ async def test_transcript_tail_marks_per_turn_notices_ephemeral():
|
|||||||
|
|
||||||
ctx = _transcript_ctx()
|
ctx = _transcript_ctx()
|
||||||
captured: list = []
|
captured: list = []
|
||||||
mock_llm = _no_action_llm(captured)
|
mock_llm = _action_llm(captured)
|
||||||
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
||||||
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
||||||
await node(
|
await node(
|
||||||
@@ -278,7 +284,7 @@ async def test_transcript_tail_marks_per_turn_notices_ephemeral():
|
|||||||
async def test_transcript_actionless_turn_commits_without_validator_message():
|
async def test_transcript_actionless_turn_commits_without_validator_message():
|
||||||
ctx = _transcript_ctx()
|
ctx = _transcript_ctx()
|
||||||
captured: list = []
|
captured: list = []
|
||||||
mock_llm = _no_action_llm(captured)
|
mock_llm = _action_llm(captured)
|
||||||
|
|
||||||
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
||||||
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
||||||
@@ -315,7 +321,7 @@ async def test_transcript_cold_start_builds_restored_history_block(tmp_path):
|
|||||||
]
|
]
|
||||||
|
|
||||||
captured: list = []
|
captured: list = []
|
||||||
mock_llm = _no_action_llm(captured)
|
mock_llm = _action_llm(captured)
|
||||||
|
|
||||||
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
||||||
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=True))
|
||||||
@@ -338,7 +344,7 @@ async def test_transcript_cold_start_builds_restored_history_block(tmp_path):
|
|||||||
async def test_flag_off_keeps_legacy_two_message_build_and_no_ledger():
|
async def test_flag_off_keeps_legacy_two_message_build_and_no_ledger():
|
||||||
ctx = _transcript_ctx()
|
ctx = _transcript_ctx()
|
||||||
captured: list = []
|
captured: list = []
|
||||||
mock_llm = _no_action_llm(captured)
|
mock_llm = _action_llm(captured)
|
||||||
|
|
||||||
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
with patch("artemis.agents.operator.operator.get_llm", return_value=mock_llm):
|
||||||
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=False))
|
node = OperatorNode(ctx, transcript_config=MemoryTranscriptConfig(enabled=False))
|
||||||
|
|||||||
Reference in new issue
Block a user