First commit

This commit is contained in:
2026-10-30 22:04:44 -03:00
commit 645ecdfb07
2794 changed files with 282545 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import pytest
from types import SimpleNamespace
from app.agents.runtime import AgentRuntimeMixin
class DummyAgent(AgentRuntimeMixin):
def __init__(self):
self.settings = SimpleNamespace(CACHE_TTL_SECONDS=10)
self.calls = 0
self.cache = None
self.rag_service = None
self.telemetry = None
class LLM:
async def ainvoke(inner, messages):
self.calls += 1
return 'ok'
self.llm = LLM()
@pytest.mark.asyncio
async def test_agent_runtime_without_cache_invokes_llm():
agent = DummyAgent()
answer = await agent._invoke_llm_cached({'user_text':'oi'}, 'dummy', [{'role':'user','content':'oi'}])
assert answer == 'ok'
assert agent.calls == 1

View File

@@ -0,0 +1,50 @@
from __future__ import annotations
import base64
import hashlib
from fastapi import FastAPI
from fastapi.testclient import TestClient
from agent_framework.security.authentication import ApiKeyAuthenticationProvider, BasicAuthenticationProvider
from agent_framework.security.middleware import AuthenticationMiddleware
def _basic(value: str) -> str:
return "Basic " + base64.b64encode(value.encode()).decode()
def test_basic_authentication_protects_endpoint_and_keeps_health_public():
app = FastAPI()
app.add_middleware(
AuthenticationMiddleware,
provider=BasicAuthenticationProvider("tia", "sha256:" + hashlib.sha256(b"secret").hexdigest()),
public_paths=["/health"],
)
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/protected")
async def protected():
return {"status": "protected"}
client = TestClient(app)
assert client.get("/health").status_code == 200
assert client.get("/protected").status_code == 401
assert client.get("/protected", headers={"Authorization": _basic("tia:wrong")}).status_code == 401
assert client.get("/protected", headers={"Authorization": _basic("tia:secret")}).status_code == 200
def test_api_key_authentication():
app = FastAPI()
app.add_middleware(AuthenticationMiddleware, provider=ApiKeyAuthenticationProvider("plain:key-123"))
@app.get("/protected")
async def protected():
return {"status": "ok"}
client = TestClient(app)
assert client.get("/protected").status_code == 401
assert client.get("/protected", headers={"x-api-key": "key-123"}).status_code == 200

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import base64
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
from agent_framework.security import (
AuthenticationPolicy,
BasicAuthenticationProvider,
NoAuthenticationProvider,
PolicyAuthenticationMiddleware,
)
def _basic(client_id: str, secret: str) -> str:
value = base64.b64encode(f"{client_id}:{secret}".encode()).decode()
return f"Basic {value}"
def test_policy_middleware_public_protected_and_default_deny():
app = FastAPI()
policies = [
AuthenticationPolicy("public", NoAuthenticationProvider(), paths=("/health",)),
AuthenticationPolicy(
"messages",
BasicAuthenticationProvider("tia", "plain:secret"),
paths=("/gateway/*",),
),
]
app.add_middleware(PolicyAuthenticationMiddleware, policies=policies)
@app.get("/health")
async def health():
return {"ok": True}
@app.get("/gateway/message")
async def message(request: Request):
return {"subject": request.state.auth_principal.subject}
@app.get("/unknown")
async def unknown():
return {"unexpected": True}
client = TestClient(app)
assert client.get("/health").status_code == 200
assert client.get("/gateway/message").status_code == 401
authenticated = client.get("/gateway/message", headers={"Authorization": _basic("tia", "secret")})
assert authenticated.status_code == 200
assert authenticated.json()["subject"] == "tia"
assert client.get("/unknown").status_code == 401
def test_policy_method_filter():
app = FastAPI()
policies = [AuthenticationPolicy("post-only", NoAuthenticationProvider(), paths=("/resource",), methods=frozenset({"POST"}))]
app.add_middleware(PolicyAuthenticationMiddleware, policies=policies)
@app.get("/resource")
async def get_resource():
return {"method": "GET"}
@app.post("/resource")
async def post_resource():
return {"method": "POST"}
client = TestClient(app)
assert client.post("/resource").status_code == 200
assert client.get("/resource").status_code == 401

19
tests/unit/test_cache.py Normal file
View File

@@ -0,0 +1,19 @@
import asyncio
import pytest
from agent_framework.cache.cache import InMemoryCache, DistributedCache
@pytest.mark.asyncio
async def test_in_memory_cache_ttl_expires():
cache = InMemoryCache()
await cache.set('k', {'v': 1}, ttl_seconds=1)
assert await cache.get('k') == {'v': 1}
await asyncio.sleep(1.05)
assert await cache.get('k') is None
@pytest.mark.asyncio
async def test_distributed_cache_promotes_l2_to_l1():
l1, l2 = InMemoryCache(), InMemoryCache()
cache = DistributedCache(l1, l2)
await l2.set('x', 'from-l2')
assert await cache.get('x') == 'from-l2'
assert await l1.get('x') == 'from-l2'

View File

@@ -0,0 +1,12 @@
import pytest
from agent_framework.cache.cache import DistributedCache, InMemoryCache
@pytest.mark.asyncio
async def test_distributed_cache_populates_l1_from_l2():
l1 = InMemoryCache(); l2 = InMemoryCache()
await l2.set("k", {"v": 1})
cache = DistributedCache(l1, l2)
assert await cache.get("k") == {"v": 1}
await l2.delete("k")
assert await cache.get("k") == {"v": 1}

View File

@@ -0,0 +1,5 @@
def test_core_imports():
import agent_framework.cache.cache
import agent_framework.rag.rag_service
import agent_framework.checkpoints.langgraph_saver
import agent_framework.observability.telemetry

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
from types import ModuleType, SimpleNamespace
import sys
import pytest
from agent_framework.guardrails.calibrated.llm_client import GuardrailLLMClient
from agent_framework.llm.providers import OCICompatibleOpenAIProvider
from agent_framework.observability.context import (
clear_observability_context,
get_observability_context,
set_observability_context,
)
def test_openai_langfuse_wrapper_is_disabled_inside_active_framework_trace(monkeypatch):
"""A correlated request must never create a standalone OpenAI-generation trace."""
clear_observability_context()
set_observability_context(request_id="req-123", trace_id="trace-123")
monkeypatch.setenv("ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION", "true")
provider = OCICompatibleOpenAIProvider.__new__(OCICompatibleOpenAIProvider)
provider.telemetry = None # compatibility path: no Telemetry explicitly injected
settings = SimpleNamespace(
ENABLE_LANGFUSE=True,
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=True,
)
fake_openai = ModuleType("openai")
class AsyncOpenAI:
pass
AsyncOpenAI.__module__ = "openai"
fake_openai.AsyncOpenAI = AsyncOpenAI
monkeypatch.setitem(sys.modules, "openai", fake_openai)
client_cls = provider._resolve_async_openai(settings)
# Standard OpenAI client = framework owns observability/correlation.
assert client_cls.__module__.startswith("openai")
assert "langfuse" not in client_cls.__module__
clear_observability_context()
@pytest.mark.asyncio
async def test_guardrail_sync_bridge_preserves_observability_context_in_worker(monkeypatch):
"""Legacy sync guardrail bridge must carry request/trace ContextVars to its worker."""
clear_observability_context()
set_observability_context(request_id="req-guardrail", trace_id="trace-guardrail")
import agent_framework.guardrails.framework_llm_client as framework_client
async def fake_classifier(llm, task, payload, **kwargs):
ctx = get_observability_context()
return {"request_id": ctx.request_id, "trace_id": ctx.trace_id, "task": task}
monkeypatch.setattr(framework_client, "classify_with_framework_llm", fake_classifier)
result = GuardrailLLMClient._run_framework_classifier("TOX", {"text": "ok"})
assert result["request_id"] == "req-guardrail"
assert result["trace_id"] == "trace-guardrail"
assert result["task"] == "TOX"
clear_observability_context()

View File

@@ -0,0 +1,160 @@
import base64
import json
from types import SimpleNamespace
import pytest
from agent_framework.checkpoints.langgraph_saver import (
RepositoryCheckpointSaver,
_TYPED_SERDE_MARKER,
_decode_checkpoint_value,
_encode_checkpoint_value,
_encode_pending_write_value,
)
Interrupt = type("Interrupt", (), {})
Interrupt.__module__ = "langgraph.types"
class FakeSerde:
def dumps_typed(self, value):
assert type(value).__module__ == "langgraph.types"
assert type(value).__qualname__ == "Interrupt"
return "msgpack", b"interrupt-payload"
def loads_typed(self, typed):
type_name, payload = typed
assert type_name == "msgpack"
assert payload == b"interrupt-payload"
return Interrupt()
class MemoryRepo:
def __init__(self):
self.value = None
async def put(self, thread_id, value):
self.value = value
async def get_latest(self, thread_id):
return self.value
def test_checkpoint_interrupt_only_in_special_channel_round_trips():
serde = FakeSerde()
raw_interrupt = Interrupt()
checkpoint = {
"id": "cp1",
"channel_values": {
"__root__": {
"__interrupt__": [raw_interrupt],
"business": {"ok": True},
},
"ordinary": {"value": 1},
},
}
encoded = _encode_checkpoint_value(serde, checkpoint)
leaf = encoded["channel_values"]["__root__"]["__interrupt__"][0]
assert leaf[_TYPED_SERDE_MARKER] is True
assert leaf["type"] == "msgpack"
assert base64.b64decode(leaf["data"]) == b"interrupt-payload"
assert encoded["channel_values"]["ordinary"] == {"value": 1}
decoded = _decode_checkpoint_value(serde, encoded)
assert type(decoded["channel_values"]["__root__"]["__interrupt__"][0]).__module__ == "langgraph.types"
assert decoded["channel_values"]["ordinary"] == {"value": 1}
def test_checkpoint_interrupt_outside_special_channel_is_rejected():
serde = FakeSerde()
checkpoint = {
"id": "cp1",
"channel_values": {"ordinary": [Interrupt()]},
}
with pytest.raises(TypeError, match="não serializável"):
_encode_checkpoint_value(serde, checkpoint)
def test_checkpoint_unknown_object_inside_interrupt_channel_is_rejected():
serde = FakeSerde()
class Unknown:
pass
checkpoint = {
"id": "cp1",
"channel_values": {"__root__": {"__interrupt__": [Unknown()]}},
}
with pytest.raises(TypeError, match="não serializável"):
_encode_checkpoint_value(serde, checkpoint)
def test_plain_checkpoint_keeps_plain_json_shape():
serde = FakeSerde()
checkpoint = {
"id": "cp1",
"channel_values": {"x": {"nested": [1, "a", True, None]}},
"channel_versions": {"x": "1"},
}
encoded = _encode_checkpoint_value(serde, checkpoint)
assert encoded == checkpoint
assert _TYPED_SERDE_MARKER not in json.dumps(encoded)
def test_pending_write_interrupt_requires_interrupt_branch():
serde = FakeSerde()
encoded = _encode_pending_write_value(
serde,
{"__interrupt__": [Interrupt()]},
path="$.pending_writes[t].__root__",
)
assert encoded["__interrupt__"][0][_TYPED_SERDE_MARKER] is True
with pytest.raises(TypeError, match="não serializável"):
_encode_pending_write_value(
serde,
{"ordinary": [Interrupt()]},
path="$.pending_writes[t].__root__",
)
def test_aput_encodes_only_checkpoint_interrupt_and_keeps_other_sections_strict():
repo = MemoryRepo()
settings = SimpleNamespace(CHECKPOINT_REPOSITORY_PROVIDER="memory")
saver = RepositoryCheckpointSaver(settings, repository=repo)
saver.serde = FakeSerde()
checkpoint = {
"id": "cp1",
"channel_values": {"__root__": {"__interrupt__": [Interrupt()]}},
}
import asyncio
asyncio.run(saver.aput(
{"configurable": {"thread_id": "t1"}},
checkpoint,
{"source": "test"},
{"x": 1},
))
assert repo.value["metadata"] == {"source": "test"}
assert repo.value["new_versions"] == {"x": 1}
assert repo.value["checkpoint"]["channel_values"]["__root__"]["__interrupt__"][0][_TYPED_SERDE_MARKER] is True
def test_aput_does_not_enable_typed_serde_for_metadata():
repo = MemoryRepo()
settings = SimpleNamespace(CHECKPOINT_REPOSITORY_PROVIDER="memory")
saver = RepositoryCheckpointSaver(settings, repository=repo)
saver.serde = FakeSerde()
import asyncio
with pytest.raises(TypeError, match="metadata"):
asyncio.run(saver.aput(
{"configurable": {"thread_id": "t1"}},
{"id": "cp1", "channel_values": {"x": 1}},
{"bad": Interrupt()},
{},
))

View File

@@ -0,0 +1,325 @@
from __future__ import annotations
import asyncio
import pytest
from types import SimpleNamespace
from agent_framework.checkpoints.langgraph_saver import (
RepositoryCheckpointSaver,
_durable_config,
)
class _Repo:
def __init__(self) -> None:
self.saved = None
async def put(self, thread_id, checkpoint):
self.saved = (thread_id, checkpoint)
async def get_latest(self, thread_id):
return self.saved[1] if self.saved and self.saved[0] == thread_id else None
def test_private_pregel_runtime_is_not_durable() -> None:
config = {
"tags": ["x"],
"configurable": {
"thread_id": "t1",
"tenant": "default",
"__pregel_runtime": "stringified-runtime",
"__pregel_store": "stringified-store",
},
}
cleaned = _durable_config(config)
assert cleaned == {
"tags": ["x"],
"configurable": {"thread_id": "t1", "tenant": "default"},
}
assert "__pregel_runtime" in config["configurable"]
def test_saver_strips_private_runtime_before_put_and_restore() -> None:
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
config = {
"configurable": {
"thread_id": "t1",
"__pregel_runtime": "Runtime(...) persisted by JSON backend",
}
}
checkpoint = {"id": "cp1", "v": 1}
returned = asyncio.run(saver.aput(config, checkpoint, {}, {}))
assert returned["configurable"] == {"thread_id": "t1", "checkpoint_ns": "", "checkpoint_id": "cp1"}
assert repo.saved is not None
persisted = repo.saved[1]
assert "__pregel_runtime" not in persisted["config"]["configurable"]
# Also protects legacy records already stored with a stringified runtime.
persisted["config"]["configurable"]["__pregel_runtime"] = "legacy-string"
restored = saver._make_tuple(persisted)
restored_config = restored.config if hasattr(restored, "config") else restored["config"]
assert "__pregel_runtime" not in restored_config["configurable"]
def test_nested_pregel_runtime_is_removed_from_checkpoint_and_pending_writes() -> None:
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
config = {"configurable": {"thread_id": "t-nested"}}
checkpoint = {
"id": "cp-nested",
"v": 1,
"channel_values": {
"__pregel_tasks": {"durable": True},
"task": {
"config": {
"configurable": {
"thread_id": "t-nested",
"__pregel_runtime": "Runtime(...) nested",
"business_key": "kept",
}
}
}
},
}
asyncio.run(saver.aput(config, checkpoint, {}, {}))
asyncio.run(
saver.aput_writes(
config,
[
(
"tasks",
{
"nested": {
"configurable": {
"__pregel_runtime": "Runtime(...) write",
"subject": "TIM Fashion",
}
}
},
)
],
"task-1",
)
)
assert repo.saved is not None
persisted = repo.saved[1]
assert persisted["checkpoint"]["channel_values"]["__pregel_tasks"] == {"durable": True}
task_config = persisted["checkpoint"]["channel_values"]["task"]["config"]["configurable"]
assert "__pregel_runtime" not in task_config
assert task_config["business_key"] == "kept"
pending_cfg = persisted["pending_writes"][0]["value"]["nested"]["configurable"]
assert "__pregel_runtime" not in pending_cfg
assert pending_cfg["subject"] == "TIM Fashion"
def test_restore_rebuilds_fresh_canonical_config_instead_of_rebinding_stored_config() -> None:
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
legacy_payload = {
"thread_id": "thread-legacy",
"checkpoint_id": "cp-legacy",
"config": {
"tags": ["old-run"],
"configurable": {
"thread_id": "thread-legacy",
"checkpoint_ns": "wf",
"checkpoint_id": "cp-legacy",
"tenant": "default",
"__pregel_runtime": "stringified-runtime",
"__pregel_store": "stringified-store",
},
},
"checkpoint": {"id": "cp-legacy", "v": 1},
"metadata": {},
}
asyncio.run(repo.put("thread-legacy", legacy_payload))
# Simulate a new invocation. Runtime/private keys supplied by a previous run
# must never be rebound from the persisted checkpoint tuple.
request = {
"tags": ["new-run"],
"configurable": {
"thread_id": "thread-legacy",
"checkpoint_ns": "wf",
"__pregel_runtime": "request-runtime-placeholder",
},
}
restored = asyncio.run(saver.aget_tuple(request))
restored_config = restored.config if hasattr(restored, "config") else restored["config"]
assert restored_config == {
"configurable": {
"thread_id": "thread-legacy",
"checkpoint_ns": "wf",
"checkpoint_id": "cp-legacy",
}
}
assert "tags" not in restored_config
assert "tenant" not in restored_config["configurable"]
assert "__pregel_runtime" not in restored_config["configurable"]
def test_legacy_checkpoint_structural_json_strings_are_recovered() -> None:
from agent_framework.checkpoints.langgraph_saver import _normalize_checkpoint, _normalize_config
legacy = {
"v": 1,
"id": "cp-json",
"channel_values": '{"state":{"ok":true}}',
"channel_versions": '{"state":"0001"}',
"versions_seen": '{"node":"{\\"state\\":\\"0001\\"}"}',
}
normalized = _normalize_checkpoint(legacy)
assert normalized["channel_values"] == {"state": {"ok": True}}
assert normalized["channel_versions"] == {"state": "0001"}
assert normalized["versions_seen"] == {"node": {"state": "0001"}}
cfg = _normalize_config({"configurable": '{"thread_id":"t1","checkpoint_ns":""}'})
assert cfg["configurable"] == {"thread_id": "t1", "checkpoint_ns": ""}
def test_checkpoint_serialization_never_silently_stringifies_unknown_objects() -> None:
import pytest
from agent_framework.checkpoints.langgraph_saver import _strict_json_value
class RuntimeLike:
def override(self):
return self
with pytest.raises(TypeError, match="Checkpoint contém valor não serializável"):
_strict_json_value({"channel_values": {"bad": RuntimeLike()}}, path="$.checkpoint")
def test_typed_serializer_bridge_is_limited_to_langgraph_interrupt_pending_write() -> None:
import base64
import json
Interrupt = type("Interrupt", (), {"__module__": "langgraph.types"})
class FakeSerde:
def dumps_typed(self, value):
assert type(value).__module__ == "langgraph.types"
assert type(value).__qualname__ == "Interrupt"
return ("fake_interrupt", json.dumps({"value": value.value}).encode("utf-8"))
def loads_typed(self, payload):
type_name, raw = payload
assert type_name == "fake_interrupt"
obj = Interrupt()
obj.value = json.loads(raw.decode("utf-8"))["value"]
return obj
interrupt = Interrupt()
interrupt.value = "pause"
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
saver.serde = FakeSerde()
config = {"configurable": {"thread_id": "typed-thread"}}
asyncio.run(saver.aput(config, {"id": "cp-typed", "channel_values": {}}, {}, {}))
# Mirrors the real LangGraph shape observed in the runtime log:
# pending_writes -> __root__ -> __interrupt__ -> [Interrupt(...)]
asyncio.run(saver.aput_writes(
config,
[("__root__", {"__interrupt__": [interrupt]})],
"task-typed",
))
persisted = repo.saved[1]
stored = persisted["pending_writes"][0]["value"]["__interrupt__"][0]
assert stored["__agent_framework_langgraph_typed__"] is True
assert stored["type"] == "fake_interrupt"
assert base64.b64decode(stored["data"]).decode("utf-8") == '{"value": "pause"}'
# Critical compatibility assertion: checkpoint/metadata/new_versions remain plain JSON.
assert persisted["checkpoint"] == {"id": "cp-typed", "channel_values": {}, "channel_versions": {}, "versions_seen": {}}
assert persisted["metadata"] == {}
restored = saver._make_tuple(persisted)
pending = restored.pending_writes if hasattr(restored, "pending_writes") else restored["pending_writes"]
assert pending[0][0] == "task-typed"
assert pending[0][1] == "__root__"
restored_interrupt = pending[0][2]["__interrupt__"][0]
assert type(restored_interrupt).__module__ == "langgraph.types"
assert type(restored_interrupt).__qualname__ == "Interrupt"
assert restored_interrupt.value == "pause"
def test_non_interrupt_non_json_pending_write_still_fails_loudly() -> None:
class RuntimeLike:
pass
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
config = {"configurable": {"thread_id": "bad-pending-thread"}}
asyncio.run(saver.aput(config, {"id": "cp-bad", "channel_values": {}}, {}, {}))
with pytest.raises(TypeError, match="Checkpoint contém valor não serializável"):
asyncio.run(saver.aput_writes(config, [("__root__", {"bad": RuntimeLike()})], "task-bad"))
def test_non_json_checkpoint_does_not_fall_back_to_typed_serde() -> None:
class RuntimeLike:
pass
class ExplodingSerde:
def dumps_typed(self, value):
raise AssertionError("global typed serde fallback must not be used")
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
saver.serde = ExplodingSerde()
config = {"configurable": {"thread_id": "bad-checkpoint-thread"}}
with pytest.raises(TypeError, match="Checkpoint contém valor não serializável"):
asyncio.run(saver.aput(
config,
{"id": "cp-bad", "channel_values": {"bad": RuntimeLike()}},
{},
{},
))
def test_json_pending_writes_remain_plain_json() -> None:
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
config = {"configurable": {"thread_id": "plain-thread"}}
asyncio.run(saver.aput(config, {"id": "cp-plain", "channel_values": {}}, {}, {}))
asyncio.run(saver.aput_writes(config, [("result", {"ok": True})], "task-plain"))
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", []))

View File

@@ -0,0 +1,14 @@
import pytest
from types import SimpleNamespace
from agent_framework.checkpoints.langgraph_saver import RepositoryCheckpointSaver
@pytest.mark.asyncio
async def test_repository_checkpoint_saver_put_get(tmp_path):
settings = SimpleNamespace(CHECKPOINT_REPOSITORY_PROVIDER='sqlite', SQLITE_DB_PATH=str(tmp_path/'db.sqlite'))
saver = RepositoryCheckpointSaver(settings)
config = {'configurable': {'thread_id': 't1'}}
next_config = await saver.aput(config, {'id': 'cp1', 'channel_values': {'x': 1}}, {'source': 'test'}, {})
assert next_config['configurable']['checkpoint_id'] == 'cp1'
tup = await saver.aget_tuple(config)
checkpoint = tup.checkpoint if hasattr(tup, 'checkpoint') else tup['checkpoint']
assert checkpoint['id'] == 'cp1'

View File

@@ -0,0 +1,29 @@
import pytest
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
class FakeTelemetry:
def __init__(self): self.events=[]
async def event(self, name, payload=None, kind='event'):
self.events.append((name, payload or {}, kind))
def span(self, name, **attrs):
class CM:
async def __aenter__(self_inner): return None
async def __aexit__(self_inner, exc_type, exc, tb): return False
return CM()
@pytest.mark.asyncio
async def test_langgraph_node_emits_started_completed():
telemetry = FakeTelemetry()
tracer = LangGraphDeepTelemetry(telemetry)
async with tracer.node('router', {'session_id': 's1'}):
pass
names = [e[0] for e in telemetry.events]
assert 'langgraph.node.started' in names
assert 'langgraph.node.completed' in names
@pytest.mark.asyncio
async def test_langgraph_edge_event():
telemetry = FakeTelemetry()
tracer = LangGraphDeepTelemetry(telemetry)
await tracer.edge('routing', 'billing', {'session_id': 's1'}, {'confidence': 0.9})
assert telemetry.events[0][0] == 'langgraph.edge.selected'

View File

@@ -0,0 +1,106 @@
from types import SimpleNamespace
import pytest
from agent_framework.llm.base import LLMProvider
from agent_framework.llm.providers import (
MockLLMProvider,
OCISDKProvider,
_extract_reasoning_content,
_extract_openai_message_content,
_extract_finish_reason,
)
from agent_framework.llm.types import LLMResponse
class LegacyOnlyProvider(LLMProvider):
async def ainvoke(self, messages, **kwargs) -> str:
return "legacy-answer"
@pytest.mark.asyncio
async def test_legacy_provider_gets_rich_response_fallback_without_breaking_contract():
provider = LegacyOnlyProvider()
legacy = await provider.ainvoke([{"role": "user", "content": "hello"}])
rich = await provider.ainvoke_response([{"role": "user", "content": "hello"}])
assert legacy == "legacy-answer"
assert isinstance(legacy, str)
assert isinstance(rich, LLMResponse)
assert rich.content == legacy
assert rich.reasoning_content is None
@pytest.mark.asyncio
async def test_mock_ainvoke_remains_string_and_rich_api_is_opt_in():
provider = MockLLMProvider()
messages = [{"role": "user", "content": "hello"}]
legacy = await provider.ainvoke(messages)
rich = await provider.ainvoke_response(messages)
assert isinstance(legacy, str)
assert legacy == rich.content
assert rich.provider == "mock"
assert rich.model == "mock-llm"
assert rich.reasoning_content is None
assert rich.usage["total_tokens"] > 0
def test_openai_compatible_reasoning_content_attribute_is_extracted():
message = SimpleNamespace(content="answer", reasoning_content="model reasoning")
assert _extract_reasoning_content(message) == "model reasoning"
def test_openai_compatible_reasoning_content_model_extra_is_extracted():
message = SimpleNamespace(content="answer", model_extra={"reasoning_content": "extra reasoning"})
assert _extract_reasoning_content(message) == "extra reasoning"
def test_missing_reasoning_content_is_none():
message = SimpleNamespace(content="answer")
assert _extract_reasoning_content(message) is None
def test_oci_sdk_reasoning_is_extracted_from_choice_message():
message = SimpleNamespace(content="answer", reasoning_content="oci reasoning")
choice = SimpleNamespace(message=message)
chat_response = SimpleNamespace(choices=[choice])
response = SimpleNamespace(data=SimpleNamespace(chat_response=chat_response))
assert OCISDKProvider._extract_reasoning_content(response) == "oci reasoning"
def test_openai_compatible_plain_string_content_is_preserved():
message = SimpleNamespace(content='{"decision":"KEEP"}')
assert _extract_openai_message_content(message) == '{"decision":"KEEP"}'
def test_openai_compatible_list_content_is_joined():
message = SimpleNamespace(content=[
SimpleNamespace(text='{"decision":'),
{"text": '"KEEP"}'},
])
assert _extract_openai_message_content(message) == '{"decision":"KEEP"}'
def test_openai_compatible_dict_message_and_content_are_supported():
message = {"content": [{"text": "{\"ok\":true}"}]}
assert _extract_openai_message_content(message) == '{"ok":true}'
def test_openai_compatible_unknown_content_fails_closed_to_empty_string():
message = SimpleNamespace(content=object())
assert _extract_openai_message_content(message) == ""
def test_openai_compatible_reasoning_is_not_used_as_answer_fallback():
message = SimpleNamespace(content=None, reasoning_content='{"decision":"KEEP"}')
assert _extract_openai_message_content(message) == ""
assert _extract_reasoning_content(message) == '{"decision":"KEEP"}'
def test_finish_reason_is_extracted_from_object_and_dict():
assert _extract_finish_reason(SimpleNamespace(finish_reason="length")) == "length"
assert _extract_finish_reason({"finish_reason": "stop"}) == "stop"

View File

@@ -0,0 +1,44 @@
from types import SimpleNamespace
from agent_framework.memory.long_term_store import (
InMemoryLongTermMemoryStore,
OracleAutonomousLongTermMemoryStore,
SQLiteLongTermMemoryStore,
create_long_term_memory_store,
)
def settings(provider: str):
return SimpleNamespace(
LONG_TERM_MEMORY_PROVIDER=provider,
LONG_TERM_MEMORY_SQLITE_PATH=":memory:",
LONG_TERM_MEMORY_TABLE="agentfw_long_term_memory",
LONG_TERM_MEMORY_ORACLE_TABLE=None,
ADB_USER="user",
ADB_PASSWORD="password",
ADB_DSN="service_high",
ADB_WALLET_LOCATION=None,
ADB_WALLET_PASSWORD=None,
ADB_TABLE_PREFIX="AGENTFW",
)
def test_factory_memory():
assert isinstance(create_long_term_memory_store(settings("memory")), InMemoryLongTermMemoryStore)
def test_factory_sqlite():
assert isinstance(create_long_term_memory_store(settings("sqlite")), SQLiteLongTermMemoryStore)
def test_factory_autonomous():
store = create_long_term_memory_store(settings("autonomous"))
assert isinstance(store, OracleAutonomousLongTermMemoryStore)
assert store.table == "AGENTFW_LONG_TERM_MEMORY"
def test_factory_oracle_alias():
assert isinstance(
create_long_term_memory_store(settings("oracle")),
OracleAutonomousLongTermMemoryStore,
)

View File

@@ -0,0 +1,219 @@
import json
from types import SimpleNamespace
import pytest
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.models import IntentDefinition
from agent_framework.routing.multi_intent import MultiIntentPlanner
def _planner():
return MultiIntentPlanner([
IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancelar pedido"], mcp_tools=["cancelar_pedido"]),
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["segunda via"], mcp_tools=["consultar_fatura"]),
], {
"enabled": True,
}, transactional_tools={"cancelar_pedido"})
def test_builds_validated_plan_and_keeps_primary_first():
plan = _planner().plan("quero cancelar pedido e preciso da segunda via")
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
assert plan.operations[1].disposition == "execute"
assert plan.operations[1].tools == ["consultar_fatura"]
def test_confirmation_with_secondary_does_not_lose_confirmation():
plan = _planner().plan_confirmation_secondary(
"sim e preciso da segunda via", primary_intent="cancel", primary_agent="orders"
)
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
assert MultiIntentPlanner.public_messages(plan) == []
def test_transactional_intent_is_primary_even_when_mentioned_second():
plan = _planner().plan("preciso da segunda via e quero cancelar pedido")
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
def test_multiword_keyword_accepts_intermediate_qualifier_without_yaml_variant():
plan = _planner().plan("quero cancelar meu pedido e preciso da segunda via")
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
def test_first_of_multiple_transactions_is_primary_and_next_is_deferred():
planner = MultiIntentPlanner([
IntentDefinition(name="invoice", agent="billing", priority=30, keywords=["fatura"], mcp_tools=["consultar_fatura"]),
IntentDefinition(name="return", agent="support", priority=20, keywords=["devolver pedido"], mcp_tools=["solicitar_devolucao"]),
IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancelar pedido"], mcp_tools=["cancelar_pedido"]),
], transactional_tools={"solicitar_devolucao", "cancelar_pedido"})
plan = planner.plan(
"quero consultar minha fatura. quero devolver pedido. quero cancelar pedido"
)
assert plan is not None
assert [item.intent for item in plan.operations] == ["return", "invoice", "cancel"]
assert [item.disposition for item in plan.operations] == ["execute", "execute", "defer"]
def test_sentence_boundary_does_not_leak_previous_intent_into_knowledge_query():
planner = MultiIntentPlanner([
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["fatura"]),
IntentDefinition(name="knowledge", agent="support", priority=30, keywords=["tarifação"]),
])
plan = planner.plan(
"quero ver minha fatura e o valor. depois gostaria de saber como funciona a tarifação do plano"
)
assert plan is not None
knowledge = next(item for item in plan.operations if item.intent == "knowledge")
assert knowledge.source_text == "depois gostaria de saber como funciona a tarifação do plano"
assert not knowledge.source_text.startswith("valor")
def test_does_not_plan_single_intent_or_invent_unknown_intent():
assert _planner().plan("quero cancelar pedido") is None
assert _planner().plan("quero cancelar pedido e saber o clima") is None
def test_known_primary_with_actionable_unknown_creates_off_context_topic():
plan = _planner().plan("quero cancelar pedido e alterar meu CNPJ")
assert plan is not None
assert plan.operations[1].intent == "off_context"
assert plan.operations[1].disposition == "unsupported"
def test_confirmation_with_unknown_secondary_keeps_confirmation():
plan = _planner().plan_confirmation_secondary(
"sim e altere meu CNPJ", primary_intent="cancel", primary_agent="orders"
)
assert plan is not None
assert plan.operations[0].intent == "cancel"
assert plan.operations[1].intent == "off_context"
def test_single_word_keyword_does_not_match_a_longer_word_prefix():
planner = MultiIntentPlanner([
IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancela"]),
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["segunda via"]),
])
plan = planner.plan("sim, pode cancelar e preciso da segunda via")
assert plan is not None
assert all(item.intent != "cancel" for item in plan.operations)
assert [item.intent for item in plan.operations] == ["invoice", "off_context"]
def test_semantic_fallback_hydrates_ownership_from_configured_intents():
plan = _planner().plan_from_semantic({
"confidence": 0.92,
"operations": [
{"intent": "invoice", "source_text": "me encaminhe o boleto"},
{"intent": "cancel", "source_text": "não quero mais o pedido"},
],
})
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
assert plan.operations[0].agent == "orders"
assert plan.operations[0].tools == ["cancelar_pedido"]
def test_semantic_fallback_rejects_invented_intent_and_low_confidence():
assert _planner().plan_from_semantic({
"confidence": 0.99,
"operations": [
{"intent": "cancel", "source_text": "não quero mais o pedido"},
{"intent": "invented", "source_text": "faça outra coisa"},
],
}) is None
assert _planner().plan_from_semantic({
"confidence": 0.2,
"operations": [
{"intent": "cancel", "source_text": "não quero mais o pedido"},
{"intent": "invoice", "source_text": "me encaminhe o boleto"},
],
}) is None
def test_semantic_confirmation_preserves_primary_transaction():
plan = _planner().plan_confirmation_from_semantic(
{
"confidence": 0.9,
"operations": [
{"intent": "invoice", "source_text": "também gostaria do boleto"},
],
},
primary_intent="cancel",
primary_agent="orders",
)
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
@pytest.mark.asyncio
async def test_router_uses_semantic_multi_intent_only_after_deterministic_miss(tmp_path):
routing = tmp_path / "routing.yaml"
routing.write_text("""
router:
fallback_agent: billing
intents:
- name: cancel
agent: orders
description: Encerrar uma encomenda existente
keywords: [cancelar pedido]
mcp_tools: [cancelar_pedido]
- name: invoice
agent: billing
description: Obter documento de cobrança ou boleto
keywords: [segunda via]
mcp_tools: [consultar_fatura]
""", encoding="utf-8")
policies = tmp_path / "tool_policies.yaml"
policies.write_text("""
defaults:
operation_type: read_only
tool_policies:
cancelar_pedido:
operation_type: transactional
require_confirmation: true
""", encoding="utf-8")
class SemanticMultiIntentLLM:
calls = 0
async def ainvoke(self, messages, **kwargs):
self.calls += 1
assert kwargs["generation_name"] == "llm.router.multi_intent"
return json.dumps({
"confidence": 0.94,
"operations": [
{"intent": "cancel", "source_text": "encerrar minha encomenda"},
{"intent": "invoice", "source_text": "encaminhe o boleto"},
],
})
llm = SemanticMultiIntentLLM()
router = EnterpriseRouter(SimpleNamespace(
ROUTING_CONFIG_PATH=str(routing),
TOOL_POLICIES_PATH=str(policies),
ENABLE_LLM_ROUTER=True,
ENABLE_ROUTE_STICKINESS=False,
), llm=llm)
decision = await router.route({
"user_text": "quero encerrar minha encomenda e encaminhe o boleto",
"context": {},
})
assert llm.calls == 1
assert decision.method == "llm"
assert decision.intent == "cancel"
assert decision.agent == "orders"
assert decision.metadata["multi_intent_classifier"] == "llm"
assert [
item["intent"]
for item in decision.metadata["multi_intent_plan"]["operations"]
] == ["cancel", "invoice"]

View File

@@ -0,0 +1,322 @@
import pytest
from agent_framework.routing.pending_topics import drain_pending_topics
@pytest.mark.asyncio
async def test_secondary_topic_does_not_inherit_primary_transaction_state():
captured = {}
async def billing_agent(state):
captured.update(state)
return {
"answer": "Aqui está a sua segunda via.",
"mcp_results": [{"tool_name": "consultar_faturas", "ok": True}],
"rag": {"provider": "kbdb", "attempted": True, "status": "executed", "document_count": 1},
"rag_context": "Documento recuperado da base de conhecimento.",
}
state = {
"answer": "Você confirma o cancelamento do Tamboro?",
"sanitized_input": "quero cancelar Tamboro e quero minha segunda via",
"confirmation_required": True,
"active_transaction": {"tool_name": "cancelar_vas_avulso"},
"transaction_status": "AWAITING_CONFIRMATION",
"selected_tool_call": {"tool_name": "cancelar_vas_avulso"},
"pending_tool_call": {"tool_name": "cancelar_vas_avulso"},
"pending_domain_workflow": {"workflow_name": "cancelamento_vas_avulso"},
"missing_parameters": ["subject"],
"next_state": "WAITING_CONTESTACAO_CONFIRMATION",
"mcp_results": [{"tool_name": "consultar_vas"}],
"pending_topics": [{
"operation_id": "op-2",
"intent": "contas_invoice_query",
"agent": "faturas_agent",
"domain": "telecom_contas",
"tools": ["consultar_faturas"],
"source_text": "quero minha segunda via",
"disposition": "execute",
"status": "pending",
}],
}
result = await drain_pending_topics(
state, state["answer"], {"faturas_agent": billing_agent}
)
assert captured["active_transaction"] is None
assert captured["selected_tool_call"] == {}
assert captured["pending_tool_call"] == {}
assert captured["pending_domain_workflow"] is None
assert captured["next_state"] is None
assert captured["sanitized_input"] == "quero minha segunda via"
assert captured["mcp_results"] == []
assert captured["mcp_tools"] == ["consultar_faturas"]
assert captured["available_mcp_tools"] == ["consultar_faturas"]
assert captured["domain"] == "telecom_contas"
assert result.answer.startswith("Você confirma o cancelamento")
assert result.answer.endswith("Aqui está a sua segunda via.")
assert [item["agent"] for item in result.agent_responses] == ["agent", "faturas_agent"]
assert result.agent_responses[0]["primary"] is True
assert result.agent_responses[1]["primary"] is False
assert result.pending_topics == []
assert result.mcp_results == [
{"tool_name": "consultar_vas"},
{"tool_name": "consultar_faturas", "ok": True},
]
assert result.rag_results[0]["metadata"]["provider"] == "kbdb"
assert result.rag_results[0]["context"] == "Documento recuperado da base de conhecimento."
@pytest.mark.asyncio
async def test_deferred_transaction_is_promoted_after_primary_completes():
async def cancellation_agent(state):
assert state["user_text"] == "quero cancelar meu pedido"
return {
"answer": "Você confirma o cancelamento do pedido PED-1001?",
"transaction_status": "AWAITING_CONFIRMATION",
"confirmation_required": True,
"pending_tool_call": {
"tool_name": "cancelar_pedido",
"arguments": {"order_id": "PED-1001"},
},
"active_transaction": {
"tool_name": "cancelar_pedido",
"status": "AWAITING_CONFIRMATION",
},
}
state = {
"answer": "A devolução foi registrada com sucesso.",
"route": "support_agent",
"active_agent": "support_agent",
"intent": "retail_support_exchange_return",
"transaction_status": "COMPLETED",
"pending_topics": [{
"operation_id": "op-3",
"intent": "retail_order_cancel",
"agent": "orders_agent",
"domain": "retail",
"tools": ["consultar_pedido", "cancelar_pedido"],
"source_text": "quero cancelar meu pedido",
"disposition": "defer",
"status": "pending",
}],
}
result = await drain_pending_topics(
state, state["answer"], {"orders_agent": cancellation_agent}
)
assert result.pending_topics == []
assert result.state_patch["route"] == "orders_agent"
assert result.state_patch["intent"] == "retail_order_cancel"
assert result.state_patch["transaction_status"] == "AWAITING_CONFIRMATION"
assert result.state_patch["pending_tool_call"]["tool_name"] == "cancelar_pedido"
assert result.agent_responses[-1]["primary"] is True
assert result.agent_responses[-1]["agent"] == "orders_agent"
@pytest.mark.asyncio
async def test_stale_agent_responses_do_not_leak_into_new_turn_projection():
state = {
"answer": "Lista atual de serviços.",
"route": "vas_agent",
"active_agent": "vas_agent",
"intent": "contas_vas_information",
"agent_responses": [
{"agent": "contestacao_agent", "answer": "Você confirma Tamboro e Paramount+?", "primary": True, "status": "completed"},
{"operation_id": "op-2", "agent": "faturas_agent", "answer": "Resposta antiga de fatura.", "primary": False, "status": "completed"},
],
"pending_topics": [],
}
result = await drain_pending_topics(state, state["answer"], {})
assert [r["answer"] for r in result.agent_responses] == ["Lista atual de serviços."]
assert result.agent_responses[0]["agent"] == "vas_agent"
assert result.agent_responses[0]["primary"] is True
@pytest.mark.asyncio
async def test_completed_secondary_result_is_kept_as_durable_operation_result():
async def billing_agent(state):
return {
"answer": "Fatura consultada.",
"mcp_results": [{"tool_name": "consultar_faturas", "ok": True, "invoice_id": "INV-1"}],
}
state = {
"answer": "Cancelamento concluído.",
"route": "contestacao_agent",
"active_agent": "contestacao_agent",
"intent": "contas_vas_cancel",
"operation_results": {
"op-1": {"operation_id": "op-1", "status": "completed", "answer": "Cancelamento concluído."}
},
"pending_topics": [{
"operation_id": "op-2", "intent": "contas_invoice_query", "agent": "faturas_agent",
"domain": "telecom_contas", "tools": ["consultar_faturas"],
"source_text": "consulte minha fatura", "disposition": "execute", "status": "pending",
}],
}
result = await drain_pending_topics(state, state["answer"], {"faturas_agent": billing_agent})
assert result.operation_results["op-1"]["answer"] == "Cancelamento concluído."
assert result.operation_results["op-2"]["status"] == "completed"
assert result.operation_results["op-2"]["mcp_results"][0]["invoice_id"] == "INV-1"
assert [r["answer"] for r in result.agent_responses] == ["Cancelamento concluído.", "Fatura consultada."]
@pytest.mark.asyncio
async def test_primary_operation_is_not_promoted_while_waiting_confirmation():
state = {
"answer": "Você confirma o cancelamento?",
"route": "contestacao_agent",
"active_agent": "contestacao_agent",
"intent": "contas_vas_cancel",
"transaction_status": "AWAITING_CONFIRMATION",
"confirmation_required": True,
"pending_tool_call": {"tool_name": "cancelar_vas_avulso", "arguments": {"subject": "Tamboro"}},
"multi_intent_plan": {
"plan_id": "mip-1",
"operations": [
{"operation_id": "op-1", "intent": "contas_vas_cancel", "agent": "contestacao_agent", "domain": "telecom_contas", "tools": ["cancelar_vas_avulso"], "source_text": "cancele tamboro"},
],
},
"operation_results": {},
"pending_topics": [],
}
result = await drain_pending_topics(state, state["answer"], {})
assert "op-1" not in result.operation_results
@pytest.mark.asyncio
async def test_terminal_primary_operation_is_promoted_and_keeps_previous_results():
state = {
"answer": "Tamboro cancelado; Paramount+ não encontrado.",
"route": "contestacao_agent",
"active_agent": "contestacao_agent",
"intent": "state:WAITING_CONTESTACAO_CONFIRMATION",
"transaction_status": "COMPLETED",
"multi_intent_plan": {
"plan_id": "mip-1",
"operations": [
{"operation_id": "op-1", "intent": "contas_vas_cancel", "agent": "contestacao_agent", "domain": "telecom_contas", "tools": ["cancelar_vas_avulso"], "source_text": "cancele tamboro e paramount"},
{"operation_id": "op-2", "intent": "contas_invoice_query", "agent": "faturas_agent", "domain": "telecom_contas", "tools": ["consultar_faturas"], "source_text": "consulte minha fatura"},
],
},
"operation_results": {
"op-2": {"operation_id": "op-2", "status": "completed", "answer": "Fatura consultada."}
},
"mcp_results": [
{
"tool_name": "cancelar_vas_avulso",
"ok": True,
"result": {
"status": "COMPLETED",
"output": {
"cancelar_vas_avulso": {
"results": [
{"subject": "Tamboro Mensal", "success": True, "protocol": "123"},
{"subject": "Paramount+", "success": False, "reason": "service_not_found"},
]
}
},
},
}
],
"transaction_evidence": [{"transaction_id": "tx-1", "status": "COMPLETED"}],
"pending_topics": [],
}
result = await drain_pending_topics(state, state["answer"], {})
assert result.operation_results["op-2"]["answer"] == "Fatura consultada."
primary = result.operation_results["op-1"]
assert primary["plan_id"] == "mip-1"
assert primary["status"] == "completed"
assert primary["mcp_results"][0]["result"]["output"]["cancelar_vas_avulso"]["results"][1]["reason"] == "service_not_found"
assert primary["transaction_evidence"][0]["transaction_id"] == "tx-1"
assert [item["answer"] for item in result.agent_responses] == ["Tamboro cancelado; Paramount+ não encontrado."]
@pytest.mark.asyncio
async def test_persisted_plan_does_not_rehydrate_completed_secondary_topics_on_next_turn():
calls = {"faturas": 0, "suporte": 0}
async def billing_agent(state):
calls["faturas"] += 1
return {"answer": "Fatura executada novamente - ERRO."}
async def support_agent(state):
calls["suporte"] += 1
return {"answer": "RAG executado novamente - ERRO."}
plan = {
"plan_id": "mip-continue-1",
"operations": [
{"operation_id": "op-1", "intent": "contas_vas_cancel", "agent": "contestacao_agent", "domain": "telecom_contas", "tools": ["cancelar_vas_avulso"], "source_text": "cancele tamboro"},
{"operation_id": "op-2", "intent": "contas_invoice_query", "agent": "faturas_agent", "domain": "telecom_contas", "tools": ["consultar_faturas"], "source_text": "consulte minha fatura", "disposition": "execute", "status": "pending"},
{"operation_id": "op-3", "intent": "contas_knowledge", "agent": "suporte_contas_agent", "domain": "telecom_contas", "tools": [], "source_text": "explique infinity pos", "disposition": "execute", "status": "pending"},
],
}
state = {
"answer": "Cancelamento concluído.",
"route": "contestacao_agent",
"active_agent": "contestacao_agent",
"intent": "state:WAITING_CONTESTACAO_CONFIRMATION",
"transaction_status": "COMPLETED",
"route_decision": {"route": "contestacao_agent", "intent": "state:WAITING_CONTESTACAO_CONFIRMATION", "metadata": {}},
"multi_intent_plan": plan,
# Empty is authoritative: op-2/op-3 were already drained on turn 1.
"pending_topics": [],
"operation_results": {
"op-2": {"operation_id": "op-2", "plan_id": "mip-continue-1", "agent": "faturas_agent", "intent": "contas_invoice_query", "domain": "telecom_contas", "source_text": "consulte minha fatura", "status": "completed", "answer": "Fatura consultada no turno anterior."},
"op-3": {"operation_id": "op-3", "plan_id": "mip-continue-1", "agent": "suporte_contas_agent", "intent": "contas_knowledge", "domain": "telecom_contas", "source_text": "explique infinity pos", "status": "completed", "answer": "Infinity explicado no turno anterior."},
},
"mcp_results": [{"tool_name": "cancelar_vas_avulso", "ok": True, "result": {"status": "COMPLETED"}}],
}
result = await drain_pending_topics(
state,
state["answer"],
{"faturas_agent": billing_agent, "suporte_contas_agent": support_agent},
)
assert calls == {"faturas": 0, "suporte": 0}
assert [item["answer"] for item in result.agent_responses] == ["Cancelamento concluído."]
assert result.operation_results["op-2"]["answer"] == "Fatura consultada no turno anterior."
assert result.operation_results["op-3"]["answer"] == "Infinity explicado no turno anterior."
assert result.operation_results["op-1"]["status"] == "completed"
@pytest.mark.asyncio
async def test_completed_queued_topic_is_idempotently_skipped_but_result_remains_available_to_processing():
calls = 0
async def billing_agent(state):
nonlocal calls
calls += 1
return {"answer": "não deveria executar"}
plan = {
"plan_id": "mip-idempotent-1",
"operations": [
{"operation_id": "op-1", "intent": "primary", "agent": "primary_agent", "domain": "test", "tools": [], "source_text": "principal"},
{"operation_id": "op-2", "intent": "contas_invoice_query", "agent": "faturas_agent", "domain": "telecom_contas", "tools": ["consultar_faturas"], "source_text": "consulte minha fatura", "disposition": "execute", "status": "pending"},
],
}
existing = {
"operation_id": "op-2", "plan_id": "mip-idempotent-1", "agent": "faturas_agent",
"intent": "contas_invoice_query", "domain": "telecom_contas", "source_text": "consulte minha fatura",
"status": "completed", "answer": "Fatura já consultada.",
"mcp_results": [{"result": {"invoice_id": "INV-1"}}],
}
state = {
"answer": "Resposta atual.",
"route": "primary_agent", "active_agent": "primary_agent", "intent": "primary",
"multi_intent_plan": plan,
"pending_topics": [dict(plan["operations"][1])],
"operation_results": {"op-2": existing},
}
result = await drain_pending_topics(state, state["answer"], {"faturas_agent": billing_agent})
assert calls == 0
assert result.operation_results["op-2"]["mcp_results"][0]["result"]["invoice_id"] == "INV-1"
assert [item["answer"] for item in result.agent_responses] == ["Resposta atual."]
assert result.pending_topics == []

View File

@@ -0,0 +1,60 @@
from __future__ import annotations
import sys
import types
import pytest
from agent_framework.analytics.providers.pubsub import PubSubAnalyticsPublisher
class _FakeFuture:
def result(self, timeout: float | None = None) -> str:
return "fake-message-id"
class _FakePublisherClient:
def __init__(self) -> None:
self.calls: list[tuple[str, bytes, dict[str, str]]] = []
def publish(self, topic_path: str, *, data: bytes, **kwargs: str) -> _FakeFuture:
self.calls.append((topic_path, data, kwargs))
return _FakeFuture()
@pytest.fixture
def pubsub_publisher(monkeypatch: pytest.MonkeyPatch) -> PubSubAnalyticsPublisher:
client = _FakePublisherClient()
pubsub_v1 = types.ModuleType("google.cloud.pubsub_v1")
pubsub_v1.PublisherClient = lambda: client # type: ignore[attr-defined]
google_cloud = types.ModuleType("google.cloud")
google_cloud.pubsub_v1 = pubsub_v1 # type: ignore[attr-defined]
google = types.ModuleType("google")
google.cloud = google_cloud # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "google", google)
monkeypatch.setitem(sys.modules, "google.cloud", google_cloud)
monkeypatch.setitem(sys.modules, "google.cloud.pubsub_v1", pubsub_v1)
monkeypatch.setenv("PUBSUB_EXCLUDED_EVENT_TYPES", "GRL.NATIVE_OUTPUT_GUARDRAILS")
monkeypatch.setenv("PUBSUB_PAYLOAD_MODE", "legacy")
publisher = PubSubAnalyticsPublisher(topic_path="projects/test/topics/analytics")
publisher.client = client
return publisher
@pytest.mark.asyncio
async def test_excluded_event_is_not_sent_to_pubsub(pubsub_publisher: PubSubAnalyticsPublisher) -> None:
await pubsub_publisher.publish("GRL.NATIVE_OUTPUT_GUARDRAILS", {"session_id": "session-1"})
assert pubsub_publisher.client.calls == []
@pytest.mark.asyncio
async def test_non_excluded_event_is_sent_to_pubsub(pubsub_publisher: PubSubAnalyticsPublisher) -> None:
await pubsub_publisher.publish("GRL.002", {"session_id": "session-1"})
assert len(pubsub_publisher.client.calls) == 1
topic_path, _, attributes = pubsub_publisher.client.calls[0]
assert topic_path == "projects/test/topics/analytics"
assert attributes["event_type"] == "GRL.002"

12
tests/unit/test_rag.py Normal file
View File

@@ -0,0 +1,12 @@
import pytest
from types import SimpleNamespace
from agent_framework.rag.rag_service import RagService
@pytest.mark.asyncio
async def test_rag_service_retrieves_relevant_document():
settings = SimpleNamespace(VECTOR_STORE_PROVIDER='memory', GRAPH_STORE_PROVIDER='memory', RAG_TOP_K=2)
rag = RagService(settings)
await rag.add_documents(['fatura alta por roaming internacional', 'pedido de troca de aparelho'], namespace='billing')
result = await rag.retrieve('minha fatura veio alta', namespace='billing')
assert result.documents
assert 'fatura' in result.as_prompt_context().lower()

View File

@@ -0,0 +1,185 @@
from types import SimpleNamespace
from contextlib import contextmanager
import pytest
from agent_framework.rag.rag_service import RagService
def _settings(**overrides):
data = dict(
RAG_PROVIDER="kbdb", RAG_TOP_K=5,
KBDB_DB_USER="kb_user", KBDB_DB_PASSWORD="kb_pwd", KBDB_DB_DSN="kb_tp",
KBDB_DB_WALLET_LOCATION=None, KBDB_DB_WALLET_PASSWORD=None,
ADB_USER=None, ADB_PASSWORD=None, ADB_DSN=None,
ADB_WALLET_LOCATION=None, ADB_WALLET_PASSWORD=None,
KBDB_SEARCH_TYPE="hybrid", KBDB_NODE_EXPANSION=True,
KBDB_NODE_MAX_RELATED=8, KBDB_GRAPH_CROSS_REF=False,
KBDB_MAX_CROSS_REF_HOPS=1, KBDB_DOCUMENT_TYPE="customer_safe",
KBDB_METADATA_JSON=None, KBDB_MIN_SCORE=None,
KBDB_IDENTIFY_DOCUMENT=True, KBDB_STORE_QUERY=True, KBDB_IDENTIFY_TOP_N=3,
)
data.update(overrides)
return SimpleNamespace(**data)
@pytest.mark.asyncio
async def test_kbdb_provider_adapts_serving_envelope(monkeypatch):
service = RagService(_settings())
def fake_search(query, k):
return {
"search_type": "hybrid", "confidence": "high", "low_confidence": False,
"top_score": 78.4, "warnings": [],
"seeds": [{"unit_id": 11, "rank": 1, "score": 78.4}],
"units": [
{"unit_id": 10, "content": "passo anterior", "provenance": "parent"},
{"unit_id": 11, "content": "resposta principal", "provenance": "seed"},
],
"documents": [{"document_id": 7, "title": "Politica"}],
}
monkeypatch.setattr(service._kbdb, "_search_sync", fake_search)
result = await service.retrieve("qual a regra?", namespace="billing_agent")
assert [d.id for d in result.documents] == ["10", "11"]
assert result.documents[1].score == 78.4
assert result.metadata["provider"] == "kbdb"
assert result.metadata["confidence"] == "high"
assert "resposta principal" in result.as_prompt_context()
@pytest.mark.asyncio
async def test_kbdb_provider_is_serving_only():
service = RagService(_settings())
with pytest.raises(RuntimeError, match="serving-only"):
await service.add_documents(["texto"])
def test_kbdb_connection_uses_same_wallet_semantics_without_adb_fallback(monkeypatch):
import sys
from agent_framework.rag.kbdb_service import KbdbRagService
captured = {}
class Defaults:
fetch_lobs = True
class Connection:
def close(self):
captured["closed"] = True
class FakeOracleDb:
defaults = Defaults()
@staticmethod
def connect(**kwargs):
captured.update(kwargs)
return Connection()
monkeypatch.setitem(sys.modules, "oracledb", FakeOracleDb)
settings = _settings(
KBDB_DB_USER="kb_user",
KBDB_DB_PASSWORD="kb_pwd",
KBDB_DB_DSN="kb_tp",
KBDB_DB_WALLET_LOCATION="/wallet/kb",
KBDB_DB_WALLET_PASSWORD="wallet_pwd",
ADB_USER="framework_user",
ADB_PASSWORD="framework_pwd",
ADB_DSN="framework_high",
ADB_WALLET_LOCATION="/wallet/framework",
ADB_WALLET_PASSWORD="framework_wallet_pwd",
)
service = KbdbRagService(settings)
with service._connect():
pass
assert captured["user"] == "kb_user"
assert captured["password"] == "kb_pwd"
assert captured["dsn"] == "kb_tp"
assert captured["config_dir"] == "/wallet/kb"
assert captured["wallet_location"] == "/wallet/kb"
assert captured["wallet_password"] == "wallet_pwd"
assert captured["closed"] is True
def test_kbdb_does_not_fallback_to_framework_adb_credentials():
from agent_framework.rag.kbdb_service import KbdbRagService
settings = _settings(
KBDB_DB_USER=None,
KBDB_DB_PASSWORD=None,
KBDB_DB_DSN=None,
ADB_USER="framework_user",
ADB_PASSWORD="framework_pwd",
ADB_DSN="framework_high",
)
with pytest.raises(RuntimeError, match="KBDB_DB_USER"):
KbdbRagService(settings)
def test_kbdb_discovers_number_flags_and_clob_output(monkeypatch):
import sys
from agent_framework.rag.kbdb_service import KbdbRagService
captured = {}
class Var:
def __init__(self, value=None):
self.value = value
def getvalue(self):
return self.value
class Cursor:
def execute(self, statement):
captured["signature_query"] = statement
def fetchall(self):
names = [
("P_SEARCH_TYPE", "VARCHAR2"), ("P_QUERY", "VARCHAR2"),
("P_TOP_K", "NUMBER"), ("P_NODE_EXPANSION", "NUMBER"),
("P_NODE_MAX_RELATED", "NUMBER"), ("P_DOCUMENT_TYPE", "VARCHAR2"),
("P_METADATA", "CLOB"), ("P_IDENTIFY_DOCUMENT", "NUMBER"),
("P_STORE_QUERY", "NUMBER"), ("P_IDENTIFY_TOP_N", "NUMBER"),
("P_RESULT", "CLOB"),
]
return [
(name, index, "OUT" if index == 11 else "IN", data_type, None)
for index, (name, data_type) in enumerate(names, start=1)
]
def var(self, data_type, size=None):
captured["out_type"] = data_type
return Var('{"units": [], "seeds": []}')
def callproc(self, name, binds):
captured["procedure"] = name
captured["binds"] = binds
class Connection:
def cursor(self):
return Cursor()
class FakeOracleDb:
DB_TYPE_JSON = "JSON"
DB_TYPE_CLOB = "CLOB"
DB_TYPE_VARCHAR = "VARCHAR"
service = KbdbRagService(_settings())
@contextmanager
def connect():
yield Connection()
monkeypatch.setitem(sys.modules, "oracledb", FakeOracleDb)
monkeypatch.setattr(service, "_connect", connect)
result = service._search_sync("como funciona?", 5)
assert result == {"units": [], "seeds": []}
assert captured["procedure"] == "PKG_KB_SERVING.search_knowledge_base"
assert captured["binds"][3] == 1
assert captured["binds"][7:10] == [1, 1, 3]
assert captured["out_type"] == "CLOB"

View File

@@ -0,0 +1,6 @@
from agent_framework.rag.graph_store import InMemoryGraphStore
def test_inmemory_graph_has_pgql_method_for_interface_parity():
graph = InMemoryGraphStore()
assert hasattr(graph, "pgql")

View File

@@ -0,0 +1,288 @@
from types import SimpleNamespace
import pytest
from agent_framework.rag.rag_service import RagResult
from agent_framework.rag.vector_store import VectorDocument
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
class DummyRag:
def __init__(self):
self.calls = []
async def retrieve(self, query, *, namespace="default", graph_node=None, rewrite=False, k=None):
self.calls.append((query, namespace, graph_node, rewrite))
return RagResult(
query=query,
documents=[VectorDocument(id="kb-1", content="Tarifação documentada", metadata={}, score=0.9)],
graph_neighbors=[],
latency_ms=3,
metadata={"provider": "kbdb", "confidence": "high", "low_confidence": False},
)
class Runtime(AgentRuntimeMixin):
pass
def _runtime(**settings):
rt = Runtime()
base = dict(
RAG_PROVIDER="kbdb",
SKIP_RAG_WHEN_MCP_SUFFICIENT=True,
ENABLE_RAG_QUERY_REWRITE=False,
ENABLE_RAG_CONTEXT_COMPRESSION=False,
RAG_GROUNDED_ONLY=False,
KBDB_GROUNDED_ONLY=True,
LONG_TERM_MEMORY_INJECT_CONTEXT=False,
)
base.update(settings)
rt.settings = SimpleNamespace(**base)
rt.rag_service = DummyRag()
rt.guardrail_pipeline = None
return rt
@pytest.mark.asyncio
async def test_successful_mcp_does_not_skip_rag_without_explicit_sufficiency():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "Como funciona a tarifação do plano Infinity Pós?",
"sanitized_input": "Como funciona a tarifação do plano Infinity Pós?",
"mcp_results": [{"ok": True, "tool_name": "qualquer_tool", "result": {"plano": "Controle 50GB"}}],
}
context, metadata = await rt._retrieve_rag_context(state)
assert rt.rag_service.calls
assert "Tarifação documentada" in context
assert metadata["provider"] == "kbdb"
assert metadata["status"] == "executed"
assert metadata["document_count"] == 1
@pytest.mark.asyncio
async def test_rag_skips_only_when_mcp_explicitly_declares_sufficiency():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "qual é meu plano?",
"sanitized_input": "qual é meu plano?",
"mcp_results": [{"ok": True, "result": {"plano": "Controle 50GB", "rag_sufficient": True}}],
}
context, metadata = await rt._retrieve_rag_context(state)
assert context == ""
assert not rt.rag_service.calls
assert metadata["reason"] == "mcp_explicitly_sufficient"
def test_kbdb_build_messages_injects_grounding_policy():
rt = _runtime()
state = {
"user_text": "Como funciona?",
"sanitized_input": "Como funciona?",
"context": {},
"business_context": {},
}
messages = rt.build_messages(
state,
system_prompt="system",
mcp_results=[{"ok": True, "result": {"plano": "Controle"}}],
rag_context="",
rag_metadata={"provider": "kbdb", "enabled": True, "status": "empty", "document_count": 0},
)
user = next(m["content"] for m in messages if m["role"] == "user")
assert "Política de grounding obrigatória" in user
assert "Não complete lacunas usando conhecimento paramétrico" in user
@pytest.mark.asyncio
@pytest.mark.parametrize("workflow_status", ["COMPLETED", "FAILED"])
async def test_rag_skips_when_all_turn_workflows_are_terminal(workflow_status):
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "sim",
"sanitized_input": "sim",
"mcp_results": [
{
"ok": True,
"tool_name": "cancelar_vas_avulso",
"result": {
"execution_id": "wf-1",
"workflow_name": "cancelamento_vas_avulso",
"status": workflow_status,
"output": {"answer": "resultado operacional"},
},
"metadata": {
"workflow_name": "cancelamento_vas_avulso",
"workflow_status": workflow_status,
},
}
],
}
context, metadata = await rt._retrieve_rag_context(state)
assert context == ""
assert not rt.rag_service.calls
assert metadata["status"] == "skipped"
assert metadata["reason"] == "all_turn_workflows_terminal"
assert metadata["workflow_statuses"] == [workflow_status]
@pytest.mark.asyncio
async def test_rag_still_runs_when_any_turn_workflow_is_not_terminal():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "sim",
"sanitized_input": "sim",
"mcp_results": [
{
"ok": True,
"result": {"execution_id": "wf-1", "workflow_name": "w1", "status": "COMPLETED"},
"metadata": {"workflow_status": "COMPLETED"},
},
{
"ok": True,
"result": {"execution_id": "wf-2", "workflow_name": "w2", "status": "RUNNING"},
"metadata": {"workflow_status": "RUNNING"},
},
],
}
context, metadata = await rt._retrieve_rag_context(state)
assert rt.rag_service.calls
assert "Tarifação documentada" in context
assert metadata["status"] == "executed"
@pytest.mark.asyncio
async def test_explicit_requires_rag_overrides_terminal_workflow_skip():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "sim",
"sanitized_input": "sim",
"mcp_results": [
{
"ok": True,
"result": {
"execution_id": "wf-1",
"workflow_name": "w1",
"status": "COMPLETED",
"requires_rag": True,
"rag_query": "explicar regra de negócio",
},
"metadata": {"workflow_status": "COMPLETED"},
}
],
}
context, metadata = await rt._retrieve_rag_context(state)
assert rt.rag_service.calls
assert rt.rag_service.calls[0][0] == "explicar regra de negócio"
assert "Tarifação documentada" in context
assert metadata["required_by_tool"] is True
@pytest.mark.asyncio
async def test_terminal_workflow_does_not_skip_rag_when_operation_is_pending():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "sim",
"sanitized_input": "sim",
"pending_topics": [
{"operation_id": "op-2", "intent": "invoice_query", "status": "pending"}
],
"operation_results": {},
"mcp_results": [
{
"ok": True,
"result": {"execution_id": "wf-1", "workflow_name": "w1", "status": "COMPLETED"},
"metadata": {"workflow_status": "COMPLETED"},
}
],
}
context, metadata = await rt._retrieve_rag_context(state)
assert rt.rag_service.calls
assert "Tarifação documentada" in context
assert metadata["status"] == "executed"
@pytest.mark.asyncio
async def test_terminal_workflow_does_not_skip_rag_when_knowledge_operation_is_unresolved():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "sim",
"sanitized_input": "sim",
"multi_intent_plan": {
"plan_id": "mip-1",
"operations": [
{"operation_id": "op-1", "intent": "cancel", "status": "completed"},
{"operation_id": "op-2", "intent": "account_knowledge", "status": "pending"},
],
},
# op-1 está resolvida, op-2 ainda não possui resultado terminal.
"operation_results": {"op-1": {"status": "completed"}},
"mcp_results": [
{
"ok": True,
"result": {"execution_id": "wf-1", "workflow_name": "w1", "status": "COMPLETED"},
"metadata": {"workflow_status": "COMPLETED"},
}
],
}
context, metadata = await rt._retrieve_rag_context(state)
assert rt.rag_service.calls
assert "Tarifação documentada" in context
assert metadata["status"] == "executed"
@pytest.mark.asyncio
async def test_completed_operation_results_prevent_stale_plan_from_counting_as_pending():
rt = _runtime()
state = {
"agent_id": "telecom_contas",
"user_text": "sim",
"sanitized_input": "sim",
"multi_intent_plan": {
"plan_id": "mip-1",
"operations": [
{"operation_id": "op-1", "intent": "cancel", "status": "pending"},
{"operation_id": "op-2", "intent": "invoice_query", "status": "pending"},
{"operation_id": "op-3", "intent": "account_knowledge", "status": "pending"},
],
},
"operation_results": {
"op-1": {"status": "completed"},
"op-2": {"status": "completed"},
"op-3": {"status": "completed"},
},
"mcp_results": [
{
"ok": True,
"result": {"execution_id": "wf-1", "workflow_name": "w1", "status": "COMPLETED"},
"metadata": {"workflow_status": "COMPLETED"},
}
],
}
context, metadata = await rt._retrieve_rag_context(state)
assert context == ""
assert not rt.rag_service.calls
assert metadata["status"] == "skipped"
assert metadata["reason"] == "all_turn_workflows_terminal"

View File

@@ -0,0 +1,39 @@
from pathlib import Path
import re
from urllib.parse import unquote
ROOT = Path(__file__).resolve().parents[2]
def _local_links(path: Path) -> list[str]:
text = path.read_text(encoding="utf-8")
return [
target for target in re.findall(r"\[[^]]*\]\(([^)]+)\)", text)
if not target.startswith(("http://", "https://", "#", "mailto:"))
]
def test_main_readmes_have_no_broken_local_links():
for name in ("README.md", "README_en.md"):
readme = ROOT / name
for target in _local_links(readme):
relative = unquote(target.split("#", 1)[0])
assert not relative or (readme.parent / relative).exists(), (name, target)
def test_every_feature_guide_is_listed_in_corresponding_main_readme():
pairs = (("pt-BR", "README.md"), ("en", "README_en.md"))
for language, readme_name in pairs:
readme = (ROOT / readme_name).read_text(encoding="utf-8")
guides = sorted((ROOT / "docs" / "features" / language).glob("[0-9][0-9]_*.md"))
assert guides
for guide in guides:
expected = f"docs/features/{language}/{guide.name}"
assert expected in readme, (readme_name, expected)
def test_portuguese_and_english_feature_catalogs_are_symmetric():
pt = {path.name for path in (ROOT / "docs" / "features" / "pt-BR").glob("*.md")}
en = {path.name for path in (ROOT / "docs" / "features" / "en").glob("*.md")}
assert pt == en

View File

@@ -0,0 +1,48 @@
import pytest
from agent_framework.checkpoints.checkpoint_repository import (
CheckpointRecoveryError,
InMemoryCheckpointRepository,
ResilientCheckpointRepository,
)
@pytest.mark.asyncio
async def test_integrity_envelope_and_recovery_skips_corrupt_latest():
raw = InMemoryCheckpointRepository()
repo = ResilientCheckpointRepository(raw, compact_every=100, keep_last=10, recovery_scan_limit=5)
await repo.put("thread-1", {"checkpoint_id": "ok-1", "checkpoint": {"id": "ok-1", "value": 1}})
await repo.put("thread-1", {"checkpoint_id": "ok-2", "checkpoint": {"id": "ok-2", "value": 2}})
# Simula corrupção no último registro persistido.
raw._data["thread-1"][-1]["payload"]["checkpoint"]["value"] = 999
recovered = await repo.get_latest("thread-1")
assert recovered["checkpoint_id"] == "ok-1"
assert recovered["checkpoint"]["value"] == 1
@pytest.mark.asyncio
async def test_compaction_keeps_last_n_checkpoints():
raw = InMemoryCheckpointRepository()
repo = ResilientCheckpointRepository(raw, compact_every=1, keep_last=3, recovery_scan_limit=10)
for i in range(7):
await repo.put("thread-compact", {"checkpoint_id": f"cp-{i}", "checkpoint": {"id": f"cp-{i}"}})
assert len(raw._data["thread-compact"]) <= 3
latest = await repo.get_latest("thread-compact")
assert latest["checkpoint_id"] == "cp-6"
@pytest.mark.asyncio
async def test_recovery_raises_when_only_corrupt_checkpoints_exist():
raw = InMemoryCheckpointRepository()
repo = ResilientCheckpointRepository(raw, compact_every=100, keep_last=10, recovery_scan_limit=5)
await repo.put("thread-bad", {"checkpoint_id": "bad", "checkpoint": {"id": "bad", "value": 1}})
raw._data["thread-bad"][-1]["payload"]["checkpoint"]["value"] = 2
with pytest.raises(CheckpointRecoveryError):
await repo.get_latest("thread-bad")

View File

@@ -0,0 +1,207 @@
from __future__ import annotations
import json
from types import SimpleNamespace
import pytest
from agent_framework.routing.continuity import SemanticRouteContinuity
from agent_framework.routing.models import IntentDefinition
class FakeLLM:
def __init__(self, response: dict | str):
self.response = response
self.calls = []
async def ainvoke(self, messages, **kwargs):
self.calls.append((messages, kwargs))
if isinstance(self.response, str):
return self.response
return json.dumps(self.response)
class FakeTelemetry:
def __init__(self):
self.events = []
async def event(self, name, payload):
self.events.append((name, payload))
def settings(**overrides):
values = {
"ENABLE_ROUTE_STICKINESS": True,
"ROUTE_STICKINESS_LLM_PROFILE": "route_continuity",
"ROUTE_STICKINESS_CONFIDENCE_THRESHOLD": 0.90,
"ROUTE_STICKINESS_HISTORY_TURNS": 2,
"ROUTE_STICKINESS_MAX_TOKENS": 80,
}
values.update(overrides)
return SimpleNamespace(**values)
def intents():
return [
IntentDefinition(
name="product_services_information",
agent="product_agent",
description="Planos, serviços, benefícios e mudança de plano.",
),
IntentDefinition(
name="billing_invoice_explanation",
agent="billing_agent",
description="Faturas, pagamentos, cobranças e contestação.",
),
]
def state(message="o que está incluso?"):
return {
"session_id": "s1",
"active_agent": "product_agent",
"intent": "product_services_information",
"domain": "telecom",
"route_decision": {
"intent": "product_services_information",
"domain": "telecom",
"mcp_tools": ["consultar_plano"],
},
"history": [
{"role": "user", "content": "qual é o meu plano?"},
{"role": "assistant", "content": "Seu plano atual é Controle 50GB."},
],
"user_text": message,
"sanitized_input": message,
}
@pytest.mark.asyncio
async def test_continue_bypasses_router_without_regex_rules():
llm = FakeLLM({"decision": "CONTINUE", "confidence": 0.97, "reason": "Continua o assunto do plano."})
telemetry = FakeTelemetry()
policy = SemanticRouteContinuity(settings(), llm, telemetry)
decision = await policy.evaluate(state(), intents=intents())
assert decision is not None
assert decision.agent == "product_agent"
assert decision.method == "continuity"
assert decision.metadata["route_bypassed"] is True
assert llm.calls[0][1]["profile_name"] == "route_continuity"
# LLM tuning belongs to llm_profiles.yaml; continuity must not override it.
assert "max_tokens" not in llm.calls[0][1]
assert "temperature" not in llm.calls[0][1]
prompt = json.loads(llm.calls[0][0][1]["content"])
assert prompt["current_message"] == "o que está incluso?"
assert "product_agent" not in prompt["other_agents"]
assert telemetry.events[-1][1]["route_bypassed"] is True
@pytest.mark.asyncio
async def test_route_result_falls_back_to_enterprise_router():
llm = FakeLLM({"decision": "ROUTE", "confidence": 0.98, "reason": "Novo assunto de cobrança."})
policy = SemanticRouteContinuity(settings(), llm)
decision = await policy.evaluate(
state("agora quero contestar uma cobrança"), intents=intents()
)
assert decision is None
@pytest.mark.asyncio
async def test_low_confidence_continue_falls_back_safely():
llm = FakeLLM({"decision": "CONTINUE", "confidence": 0.70, "reason": "Possível continuidade."})
policy = SemanticRouteContinuity(settings(), llm)
assert await policy.evaluate(state(), intents=intents()) is None
@pytest.mark.asyncio
async def test_invalid_output_falls_back_safely():
llm = FakeLLM("not-json")
policy = SemanticRouteContinuity(settings(), llm)
assert await policy.evaluate(state(), intents=intents()) is None
@pytest.mark.asyncio
async def test_no_active_agent_still_classifies_global_session_actions():
llm = FakeLLM({"decision": "ROUTE", "confidence": 1.0})
policy = SemanticRouteContinuity(settings(), llm)
current = state()
current.pop("active_agent")
assert await policy.evaluate(current, intents=intents()) is None
assert len(llm.calls) == 1
@pytest.mark.asyncio
async def test_human_handoff_is_returned_as_global_route():
llm = FakeLLM({
"decision": "HUMAN_HANDOFF",
"confidence": 0.99,
"reason": "O usuário pediu atendimento humano.",
})
policy = SemanticRouteContinuity(settings(), llm)
decision = await policy.evaluate(
state("quero falar com um atendente"), intents=intents()
)
assert decision is not None
assert decision.route == "human_handoff"
assert decision.agent == "human_handoff"
assert decision.intent == "human_handoff"
assert decision.handoff is True
assert decision.metadata["session_control"] == "HUMAN_HANDOFF"
assert decision.metadata["route_bypassed"] is True
@pytest.mark.asyncio
async def test_end_session_is_returned_as_global_route():
llm = FakeLLM({
"decision": "END_SESSION",
"confidence": 0.98,
"reason": "O usuário informou que não precisa continuar.",
})
policy = SemanticRouteContinuity(settings(), llm)
decision = await policy.evaluate(state("obrigado, era só isso"), intents=intents())
assert decision is not None
assert decision.route == "end_session"
assert decision.agent == "end_session"
assert decision.intent == "end_session"
assert decision.handoff is False
assert decision.metadata["session_control"] == "END_SESSION"
assert decision.metadata["route_bypassed"] is True
@pytest.mark.asyncio
async def test_global_session_actions_work_without_active_agent():
llm = FakeLLM({
"decision": "HUMAN_HANDOFF",
"confidence": 0.97,
"reason": "Solicitação explícita de pessoa.",
})
policy = SemanticRouteContinuity(settings(), llm)
current = state("quero uma pessoa")
current.pop("active_agent")
decision = await policy.evaluate(current, intents=intents())
assert decision is not None
assert decision.route == "human_handoff"
assert len(llm.calls) == 1
@pytest.mark.asyncio
async def test_continue_without_active_agent_falls_back_to_router():
llm = FakeLLM({"decision": "CONTINUE", "confidence": 0.99})
policy = SemanticRouteContinuity(settings(), llm)
current = state()
current.pop("active_agent")
assert await policy.evaluate(current, intents=intents()) is None
assert len(llm.calls) == 1

19
tests/unit/test_sse.py Normal file
View File

@@ -0,0 +1,19 @@
import pytest
from types import SimpleNamespace
from agent_framework.sse.events import SSEEvent, SSEHub
@pytest.mark.asyncio
async def test_sse_event_encoding():
encoded = SSEEvent(event='message', data={'text':'ok'}, id=10).encode()
assert 'id: 10' in encoded
assert 'event: message' in encoded
assert 'data: {"text": "ok"}' in encoded
@pytest.mark.asyncio
async def test_sse_hub_emit_and_replay(tmp_path):
settings = SimpleNamespace(SQLITE_DB_PATH=str(tmp_path/'db.sqlite'), SSE_KEEPALIVE_SECONDS=0.1, SSE_EVENT_REPLAY_LIMIT=10, SESSION_REPOSITORY_PROVIDER='sqlite', SSE_STORE_PROVIDER='sqlite')
hub = SSEHub(settings)
eid = await hub.emit('s1', 'flow.start', {'a': 1})
replayed = await hub.replay('s1', 0)
assert replayed[0].id == eid
assert replayed[0].event == 'flow.start'

View File

@@ -0,0 +1,30 @@
import pytest
from types import SimpleNamespace
from agent_framework.sse.events import SSEHub, SSEEvent
class MemorySSEStore:
def __init__(self):
self.rows=[]; self.next_id=1
def append_sse_event(self, session_id, event, payload):
row={"id": self.next_id, "session_id": session_id, "event_name": event, "payload": payload}
self.next_id += 1; self.rows.append(row); return row["id"]
def list_sse_events(self, session_id, after_id, limit):
return [r for r in self.rows if r["session_id"] == session_id and r["id"] > after_id][:limit]
@pytest.mark.asyncio
async def test_subscribe_skips_live_event_already_replayed():
hub = SSEHub(SimpleNamespace(SSE_KEEPALIVE_SECONDS=0.01, SSE_EVENT_REPLAY_LIMIT=100, SQLITE_DB_PATH=':memory:', SESSION_REPOSITORY_PROVIDER='sqlite'), telemetry=None)
hub.store = MemorySSEStore()
eid = await hub.emit("s1", "message.responded", {"text":"ok"})
# Same event remains in live queue and is also in replay store.
gen = hub.subscribe("s1", 0)
chunks=[]
chunks.append(await gen.__anext__()) # replay event
chunks.append(await gen.__anext__()) # connected
chunks.append(await gen.__anext__()) # keepalive, not duplicated event
assert chunks[0].startswith(f"id: {eid}")
assert "message.responded" in chunks[0]
assert "message.responded" not in chunks[2]
await gen.aclose()

View File

@@ -0,0 +1,289 @@
from __future__ import annotations
import pytest
from agent_framework.analytics.providers.langfuse import LangfuseAnalyticsPublisher
from agent_framework.observability.context import clear_observability_context, set_observability_context
from agent_framework.observability.telemetry import Telemetry
class Settings:
ENABLE_LANGFUSE = False
ENABLE_OTEL = False
LANGFUSE_TRACE_MODE = "compact"
class FakeObservation:
_next_id = 1
def __init__(self, kwargs):
self.kwargs = kwargs
self.id = f"obs-{FakeObservation._next_id}"
self.trace_id = "trace-123"
FakeObservation._next_id += 1
self.updates = []
self.trace_updates = []
self.trace_io_updates = []
def update(self, **kwargs):
self.updates.append(kwargs)
def update_trace(self, **kwargs):
self.trace_updates.append(kwargs)
def set_trace_io(self, **kwargs):
self.trace_io_updates.append(kwargs)
class FakeContextManager:
def __init__(self, observation):
self.observation = observation
def __enter__(self):
return self.observation
def __exit__(self, exc_type, exc, tb):
return False
class FakePropagationContext:
def __init__(self, owner, kwargs):
self.owner = owner
self.kwargs = kwargs
def __enter__(self):
self.owner.propagations.append(self.kwargs)
def __exit__(self, exc_type, exc, tb):
return False
class FakeLangfuse:
def __init__(self, *, legacy_api: bool = False):
self.observations = []
self.propagations = []
self.trace_updates = []
self.flush_count = 0
self.api = FakeApi() if legacy_api else None
def start_as_current_observation(self, **kwargs):
observation = FakeObservation(kwargs)
self.observations.append(observation)
return FakeContextManager(observation)
def propagate_attributes(self, **kwargs):
return FakePropagationContext(self, kwargs)
def update_current_trace(self, **kwargs):
self.trace_updates.append(kwargs)
def flush(self):
self.flush_count += 1
class FakeIngestionResponse:
errors = []
successes = []
class FakeIngestion:
def __init__(self):
self.batches = []
def batch(self, *, batch, metadata=None):
self.batches.append({"batch": batch, "metadata": metadata})
return FakeIngestionResponse()
class FakeApi:
def __init__(self):
self.ingestion = FakeIngestion()
def telemetry_with_fake_langfuse(*, legacy_api: bool = False):
FakeObservation._next_id = 1
telemetry = Telemetry(Settings())
telemetry.enabled = True
telemetry.langfuse = FakeLangfuse(legacy_api=legacy_api)
return telemetry
@pytest.mark.asyncio
async def test_compact_keeps_root_output_and_shows_ic_aga_noc_as_spans():
clear_observability_context()
telemetry = telemetry_with_fake_langfuse()
async with telemetry.span("agent.gateway_message", session_id="s1", input={"request": "cms"}, _root_span=True) as span:
await telemetry.event("IC.INTERNAL", {"step": "visible"}, kind="ic")
await telemetry.event("NOC.001", {"step": "visible"}, kind="noc")
await telemetry.event("AGA.010", {"step": "visible"}, kind="ic")
span.set_output({"answer": "ok"})
names = [obs.kwargs["name"] for obs in telemetry.langfuse.observations]
assert names == ["agent.gateway_message", "IC.INTERNAL", "NOC.001", "AGA.010"]
root = telemetry.langfuse.observations[0]
assert root.updates[-1]["input"] == {"request": "cms"}
assert root.updates[-1]["output"] == {"answer": "ok"}
assert root.trace_io_updates[-1] == {"input": {"request": "cms"}, "output": {"answer": "ok"}}
for observation in telemetry.langfuse.observations[1:]:
assert observation.kwargs.get("trace_context") is None
assert observation.kwargs["as_type"] == "span"
ic = telemetry.langfuse.observations[1]
assert ic.kwargs["input"]["step"] == "visible"
assert ic.updates[-1]["input"]["step"] == "visible"
assert ic.updates[-1]["output"] == {"status": "ok"}
assert telemetry.langfuse.propagations[-1]["trace_name"] == "agent.gateway_message"
aggregated = root.updates[-1]["metadata"]["aggregated_events"]
assert [event["name"] for event in aggregated] == ["IC.INTERNAL", "NOC.001", "AGA.010"]
@pytest.mark.asyncio
async def test_analytics_control_event_is_a_span_and_not_a_trace_tag():
clear_observability_context()
set_observability_context(request_id="req-1", trace_id="req-1", session_id="s1")
langfuse = FakeLangfuse()
publisher = LangfuseAnalyticsPublisher(langfuse=langfuse)
envelope = {
"eventType": "IC.ORDER_CONFIRMED",
"source": "agent_framework",
"payload": {"tag": "IC.ORDER_CONFIRMED", "order_id": "order-1"},
"metadata": {"ic": True},
}
await publisher.publish("IC.ORDER_CONFIRMED", envelope)
assert [obs.kwargs["name"] for obs in langfuse.observations] == ["IC.ORDER_CONFIRMED"]
observation = langfuse.observations[0]
assert observation.kwargs["as_type"] == "span"
assert observation.kwargs["input"] == envelope
assert observation.updates[-1]["output"] == {"published": True}
assert len(langfuse.trace_updates) == 1
assert "tags" not in langfuse.trace_updates[0]
@pytest.mark.asyncio
async def test_compact_generation_records_io_model_parameters_and_usage_details():
clear_observability_context()
telemetry = telemetry_with_fake_langfuse()
async with telemetry.span("agent.gateway_message", session_id="s1", input={"request": "cms"}, _root_span=True):
async with telemetry.generation_span(
name="llm.test",
model="test-model",
input=[{"role": "user", "content": "ping"}],
metadata={"profile_name": "test"},
model_parameters={"temperature": 0.2, "max_tokens": 100},
) as generation:
generation.set_output("pong")
generation.set_usage({"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2, "cost_usd": 0.01})
generation = telemetry.langfuse.observations[1]
assert generation.kwargs["name"] == "llm.test"
assert generation.kwargs["as_type"] == "generation"
assert generation.kwargs["input"] == [{"role": "user", "content": "ping"}]
assert generation.kwargs["model"] == "test-model"
assert generation.kwargs["model_parameters"] == {"temperature": 0.2, "max_tokens": 100}
assert "usage" not in generation.kwargs
assert "usage_details" not in generation.kwargs
assert generation.updates[-1]["input"] == [{"role": "user", "content": "ping"}]
assert generation.updates[-1]["output"] == "pong"
assert generation.updates[-1]["usage_details"] == {"input": 1, "output": 1}
assert generation.updates[-1]["cost_details"] == {"total": 0.01}
assert generation.kwargs.get("trace_context") is None
@pytest.mark.asyncio
async def test_legacy_io_fallback_updates_same_root_and_generation_observations():
clear_observability_context()
telemetry = telemetry_with_fake_langfuse(legacy_api=True)
async with telemetry.span("agent.gateway_message", session_id="s1", input={"request": "cms"}, _root_span=True) as root:
async with telemetry.generation_span(
name="llm.test",
model="test-model",
input=[{"role": "user", "content": "ping"}],
model_parameters={"temperature": 0.2},
) as generation:
generation.set_output("pong")
generation.set_usage({"prompt_tokens": 2, "completion_tokens": 3, "total_tokens": 5})
root.set_output({"answer": "ok"})
batches = telemetry.langfuse.api.ingestion.batches
assert [event.type for item in batches for event in item["batch"]] == ["generation-update", "span-update"]
generation_event = batches[0]["batch"][0]
assert generation_event.body.id == "obs-2"
assert generation_event.body.trace_id == "trace-123"
assert generation_event.body.input == [{"role": "user", "content": "ping"}]
assert generation_event.body.output == "pong"
assert generation_event.body.usage_details == {"input": 2, "output": 3}
root_event = batches[1]["batch"][0]
assert root_event.body.id == "obs-1"
assert root_event.body.trace_id == "trace-123"
assert root_event.body.input == {"request": "cms"}
assert root_event.body.output == {"answer": "ok"}
assert len(telemetry.langfuse.observations) == 2
@pytest.mark.asyncio
async def test_langfuse_v4_module_level_propagation_sets_native_session_context():
"""SDK v4 propagation must receive the business session id natively."""
clear_observability_context()
telemetry = telemetry_with_fake_langfuse()
calls = []
def v4_propagate_attributes(**kwargs):
calls.append(kwargs)
return FakePropagationContext(telemetry.langfuse, kwargs)
# Simulates ``from langfuse import propagate_attributes`` from SDK v4.
telemetry._langfuse_propagate_attributes = v4_propagate_attributes
async with telemetry.span(
"agent.gateway_message",
session_id="default:telecom_contas:session-123",
user_id="11999999999",
agent_id="telecom_contas",
tenant_id="default",
input={"message": "hello"},
tags=["agent:telecom_contas"],
_root_span=True,
):
await telemetry.event("IC.TEST", {"ok": True}, kind="ic")
assert len(calls) == 1
assert calls[0]["session_id"] == "default:telecom_contas:session-123"
assert calls[0]["user_id"] == "11999999999"
assert calls[0]["trace_name"] == "agent.gateway_message"
assert calls[0]["metadata"]["agent_id"] == "telecom_contas"
assert calls[0]["metadata"]["tenant_id"] == "default"
assert calls[0]["tags"] == ["agent:telecom_contas"]
# Keep the legacy update as a compatibility fallback, but the v4 path above
# is now the authoritative way to materialize native sessionId.
root = telemetry.langfuse.observations[0]
assert any(
update.get("session_id") == "default:telecom_contas:session-123"
for update in root.trace_updates
)
def test_trace_attribute_propagation_keeps_legacy_client_method_fallback():
telemetry = telemetry_with_fake_langfuse()
telemetry._langfuse_propagate_attributes = None
cm = telemetry._start_trace_attribute_propagation(
"agent.gateway_message",
{
"session_id": "legacy-session",
"user_id": "legacy-user",
"agent_id": "legacy-agent",
},
)
assert cm is not None
with cm:
pass
assert telemetry.langfuse.propagations[-1]["session_id"] == "legacy-session"

View File

@@ -0,0 +1,23 @@
from types import SimpleNamespace
from agent_framework.observability.token_cost import TokenUsageCollector, TokenUsage
def test_token_usage_extracts_cached_and_reasoning_tokens():
usage = TokenUsage.from_openai_usage({
"prompt_tokens": 1000,
"completion_tokens": 500,
"total_tokens": 1600,
"prompt_tokens_details": {"cached_tokens": 250},
"completion_tokens_details": {"reasoning_tokens": 100},
})
assert usage.prompt_tokens == 1000
assert usage.cached_tokens == 250
assert usage.reasoning_tokens == 100
assert usage.total_tokens == 1600
def test_cost_tracker_uses_model_prices_json():
settings = SimpleNamespace(MODEL_PRICES_JSON='{"my-model":{"input_per_1m":"1","output_per_1m":"2","cached_input_per_1m":"0.1"}}', USD_BRL_RATE='5')
enriched = TokenUsageCollector(settings).enrich("my-model", {"prompt_tokens": 1000, "completion_tokens": 1000, "prompt_tokens_details": {"cached_tokens": 500}})
assert enriched["cost_usd"] > 0
assert abs(enriched["cost_brl"] - enriched["cost_usd"] * 5) < 1e-9

View File

@@ -0,0 +1,98 @@
from __future__ import annotations
from types import SimpleNamespace
from agent_framework.mcp.tool_policy import ToolPolicyRegistry
from agent_framework.mcp.tool_router import MCPToolRouter
class _Registry:
def __init__(self, tool=None):
self.tool = tool
def get_tool(self, _name):
return self.tool
def _router(policy_registry, legacy=None):
router = MCPToolRouter.__new__(MCPToolRouter)
router.tool_policies = policy_registry
router.registry = _Registry(legacy)
return router
def test_missing_policy_file_preserves_legacy_behavior(tmp_path):
policies = ToolPolicyRegistry(str(tmp_path / "missing.yaml"))
legacy = SimpleNamespace(
tool_type="action",
requires=["order_id"],
confirmation_required=True,
execution_policy={},
)
router = _router(policies, legacy)
allowed, reason, metadata = router.validate_execution_policy("alterar", {"order_id": "42"})
assert allowed is False
assert "confirmação" in reason
assert metadata["operation_type"] == "transactional"
assert metadata["policy_source"] == "tools.yaml"
def test_read_only_policy_executes_without_confirmation(tmp_path):
path = tmp_path / "tool_policies.yaml"
path.write_text(
"tool_policies:\n consultar:\n operation_type: read_only\n",
encoding="utf-8",
)
router = _router(ToolPolicyRegistry(str(path)))
allowed, reason, metadata = router.validate_execution_policy("consultar", {})
assert allowed is True
assert reason is None
assert metadata["operation_type"] == "read_only"
def test_transactional_policy_requires_literal_boolean_confirmation(tmp_path):
path = tmp_path / "tool_policies.yaml"
path.write_text(
"tool_policies:\n cancelar:\n operation_type: transactional\n require_confirmation: true\n",
encoding="utf-8",
)
router = _router(ToolPolicyRegistry(str(path)))
denied, _, _ = router.validate_execution_policy("cancelar", {"confirmed": "true"})
allowed, reason, metadata = router.validate_execution_policy("cancelar", {"confirmed": True})
assert denied is False
assert allowed is True
assert reason is None
assert metadata["policy_source"] == "tool_policies.yaml"
def test_requires_confirmation_alias_is_supported(tmp_path):
path = tmp_path / "tool_policies.yaml"
path.write_text(
"tool_policies:\n alterar:\n type: transactional\n requires_confirmation: true\n",
encoding="utf-8",
)
policy = ToolPolicyRegistry(str(path)).get("alterar")
assert policy.operation_type == "transactional"
assert policy.require_confirmation is True
def test_workflow_execution_policy_is_exposed(tmp_path):
path = tmp_path / "tool_policies.yaml"
path.write_text(
"""defaults:\n operation_type: read_only\ntool_policies:\n refund:\n operation_type: transactional\n require_confirmation: true\n execution:\n mode: workflow\n workflow: refund_order\n version: 2\n""",
encoding="utf-8",
)
registry = ToolPolicyRegistry(str(path))
policy = registry.get("refund")
assert policy is not None
assert policy.execution.mode == "workflow"
assert policy.execution.workflow == "refund_order"
assert policy.execution.version == 2

View File

@@ -0,0 +1,110 @@
from pathlib import Path
import pytest
from agent_framework.workflows import (
FileWorkflowRepository,
WorkflowActionRegistry,
WorkflowRuntime,
WorkflowToolExecutor,
)
@pytest.mark.asyncio
async def test_deterministic_workflow_routes_and_caches(tmp_path: Path):
(tmp_path / "refund.active.yaml").write_text("version: 1\n", encoding="utf-8")
(tmp_path / "refund.v1.yaml").write_text(
"""name: refund
version: 1
start: validate
nodes:
- id: validate
action: validate
input: {order_id: $.input.order_id}
- id: execute
action: execute
input: {order_id: $.input.order_id}
edges:
- from: validate
to: execute
when: {path: $.nodes.validate.valid, equals: true}
- from: validate
to: END
when: {path: $.nodes.validate.valid, equals: false}
- from: execute
to: END
""",
encoding="utf-8",
)
actions = WorkflowActionRegistry()
actions.register("validate", lambda params, state: {"valid": params["order_id"] == "123"})
actions.register("execute", lambda params, state: {"protocol": "P-1"})
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=actions)
ok = await runtime.arun("refund", {"order_id": "123"})
assert ok.status == "COMPLETED"
assert ok.output["execute"]["protocol"] == "P-1"
assert len(runtime._compiled) == 1
rejected = await runtime.arun("refund", {"order_id": "999"})
assert rejected.status == "COMPLETED"
assert "execute" not in rejected.output
assert len(runtime._compiled) == 1
@pytest.mark.asyncio
async def test_policy_adapter_runs_only_workflow_mode(tmp_path: Path):
(tmp_path / "job.active.yaml").write_text("version: 1\n", encoding="utf-8")
(tmp_path / "job.v1.yaml").write_text(
"""name: job
version: 1
start: one
nodes:
- id: one
action: one
edges:
- from: one
to: END
""",
encoding="utf-8",
)
actions = WorkflowActionRegistry()
actions.register("one", lambda params, state: {"ok": True})
adapter = WorkflowToolExecutor(WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=actions))
assert await adapter.execute_from_policy(tool_name="x", arguments={}, policy={"execution": {"mode": "direct_tool"}}) is None
result = await adapter.execute_from_policy(tool_name="x", arguments={}, policy={"execution": {"mode": "workflow", "workflow": "job", "version": "active"}})
assert result["status"] == "COMPLETED"
@pytest.mark.asyncio
async def test_offline_regression_mode_forces_deterministic_backend_even_if_langgraph_is_available(tmp_path: Path, monkeypatch):
(tmp_path / "offline.active.yaml").write_text("version: 1\n", encoding="utf-8")
(tmp_path / "offline.v1.yaml").write_text(
"""name: offline
version: 1
start: one
nodes:
- id: one
action: one
edges:
- from: one
to: END
""",
encoding="utf-8",
)
actions = WorkflowActionRegistry()
actions.register("one", lambda params, state: {"ok": True})
runtime = WorkflowRuntime(
FileWorkflowRepository(tmp_path),
actions=actions,
allow_deterministic_fallback=True,
)
def _must_not_compile(_definition):
raise AssertionError("offline regression mode must not compile LangGraph")
monkeypatch.setattr(runtime, "_compile", _must_not_compile)
result = await runtime.arun("offline", {})
assert result.status == "COMPLETED"
assert result.output["one"]["ok"] is True
assert runtime._compiled == {}

View File

@@ -0,0 +1,90 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_all_tuning_backend_graphs_support_pending_topics():
graphs = sorted((ROOT / "Tuning-Performance").glob("**/app/workflows/agent_graph.py"))
assert graphs
for path in graphs:
source = path.read_text(encoding="utf-8")
assert "from agent_framework.routing.pending_topics import drain_pending_topics" in source, path
assert '"pending_topics": drained.pending_topics' in source, path
assert '"handled_topics": drained.handled_topics' in source, path
assert '"mcp_results": drained.mcp_results' in source, path
assert '"rag_results": drained.rag_results' in source, path
assert 'list(drained.mcp_results) + list(drained.rag_results)' in source, path
def test_all_tuning_backend_states_expose_pending_topics():
states = sorted((ROOT / "Tuning-Performance").glob("**/app/state.py"))
assert states
for path in states:
source = path.read_text(encoding="utf-8")
assert "pending_topics: list[dict[str, Any]]" in source, path
assert "handled_topics: list[dict[str, Any]]" in source, path
assert "rag_results: list[dict[str, Any]]" in source, path
def test_canonical_templates_delegate_drain_to_framework_helper():
for template in ("agent_template_backend", "agent_template_backend_day_zero"):
graph = ROOT / "templates" / template / "app" / "workflows" / "agent_graph.py"
state = ROOT / "templates" / template / "app" / "state.py"
graph_source = graph.read_text(encoding="utf-8")
state_source = state.read_text(encoding="utf-8")
assert "from agent_framework.routing.pending_topics import drain_pending_topics" in graph_source
assert "drained = await drain_pending_topics(" in graph_source
assert '"pending_topics": drained.pending_topics' in graph_source
assert '"handled_topics": drained.handled_topics' in graph_source
assert '"mcp_results": drained.mcp_results' in graph_source
assert '"rag_results": drained.rag_results' in graph_source
assert 'list(drained.mcp_results) + list(drained.rag_results)' in graph_source
assert "MultiIntentPlanner.public_messages" not in graph_source
assert "pending_topics: list[dict[str, Any]]" in state_source
assert "handled_topics: list[dict[str, Any]]" in state_source
assert "rag_results: list[dict[str, Any]]" in state_source
def test_all_rag_agents_expose_metadata_and_context_as_state_evidence():
roots = [ROOT / "templates", ROOT / "Tuning-Performance"]
agents = sorted(
path
for root in roots
for path in root.glob("**/app/agents/*.py")
if '"rag": rag_metadata' in path.read_text(encoding="utf-8")
)
assert agents
for path in agents:
source = path.read_text(encoding="utf-8")
assert '"rag_context": rag_context' in source, path
assert '"rag_results": [{' in source, path
def test_templates_and_tuning_expose_rag_results_in_api_and_telemetry():
roots = [ROOT / "templates", ROOT / "Tuning-Performance"]
mains = sorted(path for root in roots for path in root.glob("**/app/main.py"))
graphs = sorted(path for root in roots for path in root.glob("**/app/workflows/agent_graph.py"))
assert mains and graphs
for path in mains:
assert '"rag_results": result.get("rag_results")' in path.read_text(encoding="utf-8"), path
for path in graphs:
assert '"rag_results": state.get("rag_results", [])' in path.read_text(encoding="utf-8"), path
def test_all_tuning_envs_expose_complete_kbdb_configuration():
envs = sorted((ROOT / "Tuning-Performance").glob("**/.env.example"))
assert envs
required = (
"RAG_PROVIDER=standard",
"KBDB_DB_USER=",
"KBDB_DB_DSN=",
"KBDB_IDENTIFY_DOCUMENT=true",
"KBDB_STORE_QUERY=true",
"KBDB_IDENTIFY_TOP_N=3",
"KBDB_GROUNDED_ONLY=true",
)
for path in envs:
source = path.read_text(encoding="utf-8")
for setting in required:
assert setting in source, (path, setting)

View File

@@ -0,0 +1,26 @@
from agent_framework.workflows.runtime import _exception_details, _type_shape
def _boom():
raise AttributeError("'str' object has no attribute 'override'")
def test_exception_details_contains_traceback():
try:
_boom()
except Exception as exc:
details = _exception_details(exc, runtime_context={"phase": "ainvoke"})
assert details["type"] == "AttributeError"
assert "_boom" in details["traceback"]
assert "override" in details["traceback"]
assert details["runtime_diagnostics"]["phase"] == "ainvoke"
def test_type_shape_exposes_types_not_values():
shape = _type_shape({"configurable": {"thread_id": "secret-thread", "__pregel_runtime": "bad-runtime"}})
text = repr(shape)
assert "thread_id" in text
assert "__pregel_runtime" in text
assert "secret-thread" not in text
assert "bad-runtime" not in text
assert "str" in text

View File

@@ -0,0 +1,14 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = ROOT / 'agent_template_backend' / 'app' / 'workflows' / 'agent_graph.py'
def test_workflow_uses_framework_checkpointer_not_memory_saver():
src = WORKFLOW.read_text()
assert 'create_langgraph_checkpointer(self.settings)' in src
assert 'MemorySaver()' not in src
def test_workflow_wraps_nodes_with_langgraph_telemetry():
src = WORKFLOW.read_text()
assert 'self._node("input_guardrails", self.input_guardrails)' in src
assert 'async with self.langgraph_telemetry.node(name, state)' in src

View File

@@ -0,0 +1,218 @@
from pathlib import Path
from types import ModuleType, SimpleNamespace
import sys
import pytest
from agent_framework.workflows import FileWorkflowRepository, WorkflowActionRegistry, WorkflowRuntime
def _write_workflow(tmp_path: Path) -> None:
(tmp_path / "terminal.active.yaml").write_text("version: 1\n", encoding="utf-8")
(tmp_path / "terminal.v1.yaml").write_text(
"""name: terminal
version: 1
start: finish
nodes:
- id: finish
action: finish
edges:
- from: finish
to: END
""",
encoding="utf-8",
)
def _terminal_state(execution_id: str) -> dict:
return {
"execution_id": execution_id,
"workflow_name": "terminal",
"workflow_version": 1,
"input": {},
"nodes": {"finish": {"success": True}},
"vars": {"finish": {"success": True}},
"output": {"success": True},
"trace": [{"node": "finish", "action": "finish", "attempt": 1, "status": "COMPLETED"}],
"current_node": "finish",
}
class _FakeGraph:
def __init__(self, state: dict, snapshot):
self.state = state
self.snapshot = snapshot
async def ainvoke(self, *args, **kwargs):
return self.state
async def aget_state(self, config):
return self.snapshot
@pytest.mark.asyncio
async def test_arun_truthy_next_without_interrupt_is_completed_when_definition_is_terminal(tmp_path: Path, monkeypatch):
_write_workflow(tmp_path)
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
state = _terminal_state("exec-1")
# Regression shape observed in production: LangGraph still exposes a truthy
# next, but there is no real interrupt and the current node routes to END.
snapshot = SimpleNamespace(next=("finish__continue",), tasks=(), values=state)
monkeypatch.setattr(runtime, "_compile", lambda definition: _FakeGraph(state, snapshot))
result = await runtime.arun("terminal", {}, execution_id="exec-1")
assert result.status == "COMPLETED"
assert result.pause is None
assert result.state["current_node"] == "finish"
@pytest.mark.asyncio
async def test_aresume_truthy_next_without_interrupt_is_completed_when_definition_is_terminal(tmp_path: Path, monkeypatch):
_write_workflow(tmp_path)
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
state = _terminal_state("exec-2")
snapshot = SimpleNamespace(next=("finish__continue",), tasks=(), values=state)
monkeypatch.setattr(runtime, "_compile", lambda definition: _FakeGraph(state, snapshot))
# aresume imports langgraph.types.Command before invoking the compiled graph.
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-2", "sim")
assert result.status == "COMPLETED"
assert result.pause is None
@pytest.mark.asyncio
async def test_real_interrupt_still_has_precedence_over_structural_terminal(tmp_path: Path, monkeypatch):
_write_workflow(tmp_path)
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
state = _terminal_state("exec-3")
interrupt = SimpleNamespace(value={"node": "finish", "expected_input": {"key": "confirm"}})
task = SimpleNamespace(interrupts=(interrupt,))
snapshot = SimpleNamespace(next=("finish__pause",), tasks=(task,), values=state)
monkeypatch.setattr(runtime, "_compile", lambda definition: _FakeGraph(state, snapshot))
result = await runtime.arun("terminal", {}, execution_id="exec-3")
assert result.status == "PAUSED"
assert result.pause == {"node": "finish", "expected_input": {"key": "confirm"}}
@pytest.mark.asyncio
async def test_pending_nonterminal_without_interrupt_fails_closed_instead_of_faking_pause(tmp_path: Path, monkeypatch):
(tmp_path / "nonterminal.active.yaml").write_text("version: 1\n", encoding="utf-8")
(tmp_path / "nonterminal.v1.yaml").write_text(
"""name: nonterminal
version: 1
start: one
nodes:
- id: one
action: one
- id: two
action: two
edges:
- from: one
to: two
- from: two
to: END
""",
encoding="utf-8",
)
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
state = {
"execution_id": "exec-4",
"workflow_name": "nonterminal",
"workflow_version": 1,
"input": {},
"nodes": {"one": {"success": True}},
"vars": {},
"output": {},
"trace": [{"node": "one", "action": "one", "attempt": 1, "status": "COMPLETED"}],
"current_node": "one",
}
snapshot = SimpleNamespace(next=("two",), tasks=(), values=state)
monkeypatch.setattr(runtime, "_compile", lambda definition: _FakeGraph(state, snapshot))
result = await runtime.arun("nonterminal", {}, execution_id="exec-4")
assert result.status == "FAILED"
assert "trabalho pendente sem interrupt real" in (result.error or "")
assert result.pause is None
@pytest.mark.asyncio
async def test_persisted_interrupt_in_snapshot_values_is_real_pause(tmp_path: Path, monkeypatch):
"""LangGraph may persist interrupts in values['__interrupt__'] only.
Regression: this shape used to be mistaken for non-terminal pending work
when snapshot.next pointed at a framework-generated ``__pause`` node.
"""
_write_workflow(tmp_path)
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
state = _terminal_state("exec-values-interrupt")
pause_payload = {
"node": "finish",
"prompt": "Confirma?",
"expected_input": {"key": "resposta_usuario", "allowed_values": ["SIM", "NAO"]},
}
state["__interrupt__"] = [{"value": pause_payload, "id": "pause-1"}]
# current_node is deliberately non-terminal so the PAUSED decision must
# come from the persisted interrupt, not structural-terminal detection.
state["current_node"] = None
snapshot = SimpleNamespace(next=("finish__pause",), tasks=(), values=state)
monkeypatch.setattr(runtime, "_compile", lambda definition: _FakeGraph(state, snapshot))
result = await runtime.arun("terminal", {}, execution_id="exec-values-interrupt")
assert result.status == "PAUSED"
assert result.pause == pause_payload
assert result.error is None
def test_snapshot_interrupts_deduplicates_task_and_persisted_shapes(tmp_path: Path):
_write_workflow(tmp_path)
runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=WorkflowActionRegistry())
payload = {"node": "finish", "expected_input": {"key": "confirm"}}
task = SimpleNamespace(interrupts=(SimpleNamespace(value=payload),))
snapshot = SimpleNamespace(
tasks=(task,),
values={"__interrupt__": [{"value": payload, "id": "same-pause"}]},
interrupts=(),
)
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"