Job 98194fa8d215 showed every extract call hitting the 32k cap with only ~20k chars visible despite reasoning effort=low - Gemini 2.5 Pro still burned ~25k thinking tokens per sheet. - EXTRACT_MAX_TOKENS default 32768 -> 65536 (model output ceiling) - new EXTRACT_REASONING_MAX_TOKENS (default 2048): OpenRouter reasoning max_tokens / Gemini thinking_budget; takes precedence over effort - log per-call reasoning token counts (usage.completion_tokens_details) and include thinking count in the finish_reason=length marker
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""SheetExtractorAgent fallback ladder tests (bare-list wrap + compact retry)."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from backend.agents.base import AgentScope, AgentUsage
|
|
from backend.agents.extractors import SheetExtractorAgent, _wrap_bare_list
|
|
|
|
|
|
def _scope():
|
|
return AgentScope(
|
|
scope_id="sheet:4",
|
|
payload={"page": {"page_number": 4, "base64": "QUJD"}},
|
|
)
|
|
|
|
|
|
def _objects(n=2):
|
|
return [
|
|
{
|
|
"object_id": f"obj-{i}",
|
|
"object_type": "equipment",
|
|
"category": "mechanical",
|
|
"name": f"RTU-{i}",
|
|
"source_text": f"RTU-{i}",
|
|
"confidence": "high",
|
|
}
|
|
for i in range(n)
|
|
]
|
|
|
|
|
|
def test_wrap_bare_list_builds_sheet_envelope():
|
|
wrapped = _wrap_bare_list(_objects(3), page_number=4)
|
|
assert wrapped["sheet"] == {}
|
|
assert len(wrapped["objects"]) == 3
|
|
|
|
|
|
def test_wrap_bare_list_passes_dicts_and_none_through():
|
|
assert _wrap_bare_list({"sheet": {}, "objects": []}, 1) == {"sheet": {}, "objects": []}
|
|
assert _wrap_bare_list(None, 1) is None
|
|
|
|
|
|
def test_run_accepts_bare_list_response():
|
|
agent = SheetExtractorAgent(usage=AgentUsage())
|
|
with patch("backend.agents.extractors.call_json",
|
|
return_value=_objects(5)) as mock_call:
|
|
result = agent.run(_scope())
|
|
assert not result.error
|
|
assert len(result.artifacts) == 1
|
|
sheet = result.artifacts[0]
|
|
assert sheet["page_number"] == 4
|
|
assert len(sheet["assertions"]) == 5
|
|
# No compact retry needed when the first call yields data.
|
|
assert mock_call.call_count == 1
|
|
# Reasoning knobs are forwarded (None when config is blank in tests).
|
|
assert "reasoning_effort" in mock_call.call_args.kwargs
|
|
assert "reasoning_max_tokens" in mock_call.call_args.kwargs
|
|
|
|
|
|
def test_run_compact_retry_after_hard_failure():
|
|
agent = SheetExtractorAgent(usage=AgentUsage())
|
|
with patch("backend.agents.extractors.call_json",
|
|
side_effect=[None, {"sheet": {"sheet_number": "A102"},
|
|
"objects": _objects(2)}]) as mock_call:
|
|
result = agent.run(_scope())
|
|
assert not result.error
|
|
assert result.artifacts[0]["sheet_number"] == "A102"
|
|
assert mock_call.call_count == 2
|
|
# Second call carried the compact suffix.
|
|
assert "COMPACT RETRY" in mock_call.call_args_list[1].kwargs["user_text"]
|
|
|
|
|
|
def test_run_fails_only_after_both_attempts_miss():
|
|
agent = SheetExtractorAgent(usage=AgentUsage())
|
|
with patch("backend.agents.extractors.call_json", return_value=None) as mock_call:
|
|
result = agent.run(_scope())
|
|
assert result.error == "no structured extraction returned"
|
|
assert mock_call.call_count == 2
|