ajuste no contas
This commit is contained in:
Binary file not shown.
Binary file not shown.
93
agent_framework_oci/tests/test_llm_context_compaction.py
Normal file
93
agent_framework_oci/tests/test_llm_context_compaction.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import pytest
|
||||
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
|
||||
class DummyRuntime(AgentRuntimeMixin):
|
||||
def __init__(self, llm=None):
|
||||
self.llm = llm
|
||||
self.cache = None
|
||||
self.telemetry = None
|
||||
self.settings = None
|
||||
|
||||
|
||||
class ContextFailThenOkLLM:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
self.calls.append((messages, kwargs))
|
||||
if len(self.calls) == 1:
|
||||
raise RuntimeError("Input length (134212) exceeds model's maximum context length (131072)")
|
||||
return "ok"
|
||||
|
||||
|
||||
def test_compact_llm_value_drops_recursive_runtime_payload_but_keeps_business_facts():
|
||||
runtime = DummyRuntime()
|
||||
payload = {
|
||||
"result": {
|
||||
"subject": "Tamboro Mensal",
|
||||
"success": True,
|
||||
"service": {"details": {"valor": "14,99"}},
|
||||
"state": {"session": {"secret": "must-not-leak"}, "huge": "x" * 20000},
|
||||
"business_events": [{"payload": "x" * 20000}],
|
||||
}
|
||||
}
|
||||
rendered = runtime._compact_llm_value(payload, max_chars=8000)
|
||||
assert "Tamboro Mensal" in rendered
|
||||
assert "14,99" in rendered
|
||||
assert "must-not-leak" not in rendered
|
||||
assert "business_events" not in rendered
|
||||
assert len(rendered) < 9000
|
||||
|
||||
|
||||
def test_build_messages_bounds_mcp_and_transaction_evidence():
|
||||
runtime = DummyRuntime()
|
||||
giant = {
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"ok": True,
|
||||
"result": {
|
||||
"subject": "Tamboro Mensal",
|
||||
"validatedAmount": "14,99",
|
||||
"state": {"payload": "x" * 100000},
|
||||
"services": [{"name": f"svc-{i}", "details": {"valor": "1,00"}} for i in range(100)],
|
||||
},
|
||||
}
|
||||
state = {
|
||||
"user_text": "isso mesmo, pode cancelar",
|
||||
"sanitized_input": "isso mesmo, pode cancelar",
|
||||
"intent": "state:WAITING_CONFIRMATION",
|
||||
"route": "contestacao_agent",
|
||||
"business_context": {"customer_key": "11999999999"},
|
||||
"transaction_evidence": [{
|
||||
"transaction_id": "tx-1",
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {"subject": "Tamboro Mensal"},
|
||||
"status": "COMPLETED",
|
||||
"result": giant,
|
||||
}],
|
||||
}
|
||||
messages = runtime.build_messages(state, system_prompt="system", mcp_results=[giant])
|
||||
total = sum(len(m["content"]) for m in messages)
|
||||
joined = "\n".join(m["content"] for m in messages)
|
||||
assert total < 50000
|
||||
assert "Tamboro Mensal" in joined
|
||||
assert "14,99" in joined
|
||||
assert "x" * 5000 not in joined
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_length_error_retries_once_with_compacted_messages():
|
||||
llm = ContextFailThenOkLLM()
|
||||
runtime = DummyRuntime(llm=llm)
|
||||
messages = [
|
||||
{"role": "system", "content": "s" * 30000},
|
||||
{"role": "user", "content": "u" * 120000},
|
||||
]
|
||||
answer = await runtime._invoke_llm_cached({}, "ContestacaoAgent", messages)
|
||||
assert answer == "ok"
|
||||
assert len(llm.calls) == 2
|
||||
first_chars = sum(len(m["content"]) for m in llm.calls[0][0])
|
||||
second_chars = sum(len(m["content"]) for m in llm.calls[1][0])
|
||||
assert second_chars < first_chars
|
||||
assert second_chars <= 61000
|
||||
Binary file not shown.
Binary file not shown.
@@ -295,3 +295,31 @@ def test_json_pending_writes_remain_plain_json() -> None:
|
||||
|
||||
persisted = repo.saved[1]
|
||||
assert persisted["pending_writes"][0]["value"] == {"ok": True}
|
||||
|
||||
|
||||
def test_stale_pending_write_cannot_replace_newer_checkpoint() -> None:
|
||||
"""A delayed write for cp1 must never make cp1 latest after cp2 exists."""
|
||||
repo = _Repo()
|
||||
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
|
||||
|
||||
cfg0 = {"configurable": {"thread_id": "tx-thread"}}
|
||||
cp1_cfg = asyncio.run(saver.aput(cfg0, {"id": "cp1", "v": 1, "channel_values": {"transaction_status": "COMPLETED"}}, {}, {}))
|
||||
cp2_cfg = asyncio.run(saver.aput(cp1_cfg, {"id": "cp2", "v": 1, "channel_values": {"transaction_status": "AWAITING_CONFIRMATION", "confirmation_required": True}}, {}, {}))
|
||||
|
||||
# Simulates aput_writes from the older cp1 finishing after cp2 was persisted.
|
||||
asyncio.run(saver.aput_writes(cp1_cfg, [("result", {"old": True})], "late-task"))
|
||||
|
||||
assert repo.saved is not None
|
||||
latest = repo.saved[1]
|
||||
assert latest["checkpoint_id"] == "cp2"
|
||||
assert latest["checkpoint"]["channel_values"]["transaction_status"] == "AWAITING_CONFIRMATION"
|
||||
assert "pending_writes" not in latest or not any(
|
||||
isinstance(item, dict) and item.get("task_id") == "late-task"
|
||||
for item in latest.get("pending_writes", [])
|
||||
)
|
||||
|
||||
# A write for the actual latest checkpoint is still accepted.
|
||||
asyncio.run(saver.aput_writes(cp2_cfg, [("result", {"new": True})], "current-task"))
|
||||
latest = repo.saved[1]
|
||||
assert latest["checkpoint_id"] == "cp2"
|
||||
assert any(item.get("task_id") == "current-task" for item in latest.get("pending_writes", []))
|
||||
|
||||
@@ -188,3 +188,31 @@ def test_snapshot_interrupts_deduplicates_task_and_persisted_shapes(tmp_path: Pa
|
||||
)
|
||||
|
||||
assert runtime._snapshot_interrupts(snapshot) == [payload]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aresume_ignores_stale_interrupt_after_advancing_to_terminal_node(tmp_path: Path, monkeypatch):
|
||||
_write_workflow(tmp_path)
|
||||
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
|
||||
state = _terminal_state("exec-stale")
|
||||
# Shape observed after resuming invoice_explanation: the durable snapshot can
|
||||
# still expose the interrupt from the old pause although current_node has
|
||||
# already advanced to the terminal handoff/finalization node.
|
||||
stale = SimpleNamespace(value={"node": "old_pause", "prompt": "old prompt"})
|
||||
task = SimpleNamespace(interrupts=(stale,))
|
||||
snapshot = SimpleNamespace(next=("finish__continue",), tasks=(task,), values=state)
|
||||
monkeypatch.setattr(runtime, "_compile", lambda definition: _FakeGraph(state, snapshot))
|
||||
|
||||
langgraph_module = ModuleType("langgraph")
|
||||
types_module = ModuleType("langgraph.types")
|
||||
class _Command:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
types_module.Command = _Command
|
||||
monkeypatch.setitem(sys.modules, "langgraph", langgraph_module)
|
||||
monkeypatch.setitem(sys.modules, "langgraph.types", types_module)
|
||||
|
||||
result = await runtime.aresume("terminal", "exec-stale", "nao")
|
||||
|
||||
assert result.status == "COMPLETED"
|
||||
assert result.pause is None
|
||||
assert result.state["current_node"] == "finish"
|
||||
|
||||
Reference in New Issue
Block a user