feat(api): añade persistencia JSONL append-only y DI con factories cacheadas
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
"""Dependencias compartidas inyectadas en los routers FastAPI.
|
||||||
|
|
||||||
|
Cada `get_*` está cacheada con `lru_cache(maxsize=1)`: la app construye registry,
|
||||||
|
policy store, proveedor LLM, motor de guardrails y orchestrator una sola vez. Los
|
||||||
|
tests llaman a `.cache_clear()` para reconstruirlos contra un `DATA_DIR` temporal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from agentforge_core.config import Settings
|
||||||
|
from agentforge_core.guardrails.base import GuardrailEngine
|
||||||
|
from agentforge_core.guardrails.factory import build_guardrail_engine
|
||||||
|
from agentforge_core.llm.base import LLMProvider
|
||||||
|
from agentforge_core.llm.factory import build_llm_provider
|
||||||
|
from agentforge_core.registry.factory import build_agent_registry, build_policy_store
|
||||||
|
from agentforge_core.registry.policy_store import FileSystemPolicyStore
|
||||||
|
from agentforge_core.registry.repository import FileSystemAgentRegistry
|
||||||
|
from agentforge_core.runtime.orchestrator import AgentOrchestrator
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_registry() -> FileSystemAgentRegistry:
|
||||||
|
return build_agent_registry(get_settings())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_policy_store() -> FileSystemPolicyStore:
|
||||||
|
return build_policy_store(get_settings())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_llm_provider() -> LLMProvider:
|
||||||
|
return build_llm_provider(get_settings())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_guardrail_engine() -> GuardrailEngine:
|
||||||
|
return build_guardrail_engine(get_settings())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_orchestrator() -> AgentOrchestrator:
|
||||||
|
settings = get_settings()
|
||||||
|
return AgentOrchestrator(
|
||||||
|
provider=get_llm_provider(),
|
||||||
|
engine=get_guardrail_engine(),
|
||||||
|
data_dir=settings.data_dir,
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Helpers de persistencia append-only en JSONL para auditoría.
|
||||||
|
|
||||||
|
`executions.jsonl` y `violations.jsonl` son logs append-only: una línea JSON por
|
||||||
|
evento, nunca se reescriben. Sirven para reconstruir el historial y los listados.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from agentforge_core.domain.execution import AgentExecution, AgentExecutionSummary
|
||||||
|
from agentforge_core.domain.guardrail import GuardrailViolation
|
||||||
|
|
||||||
|
|
||||||
|
def append_violation(data_dir: Path, violation: GuardrailViolation) -> None:
|
||||||
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with (data_dir / "violations.jsonl").open("a", encoding="utf-8") as f:
|
||||||
|
f.write(violation.model_dump_json() + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def append_execution(data_dir: Path, execution: AgentExecution) -> None:
|
||||||
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with (data_dir / "executions.jsonl").open("a", encoding="utf-8") as f:
|
||||||
|
f.write(execution.model_dump_json() + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_violations(data_dir: Path) -> list[GuardrailViolation]:
|
||||||
|
path = data_dir / "violations.jsonl"
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
out: list[GuardrailViolation] = []
|
||||||
|
with path.open(encoding="utf-8") as f:
|
||||||
|
for raw in f:
|
||||||
|
line = raw.strip()
|
||||||
|
if line:
|
||||||
|
out.append(GuardrailViolation.model_validate_json(line))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def read_execution_summaries(data_dir: Path) -> list[AgentExecutionSummary]:
|
||||||
|
path = data_dir / "executions.jsonl"
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
out: list[AgentExecutionSummary] = []
|
||||||
|
with path.open(encoding="utf-8") as f:
|
||||||
|
for raw in f:
|
||||||
|
line = raw.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
execution = AgentExecution.model_validate_json(line)
|
||||||
|
out.append(
|
||||||
|
AgentExecutionSummary(
|
||||||
|
trace_id=execution.trace_id,
|
||||||
|
agent_name=execution.agent_name,
|
||||||
|
agent_version=execution.agent_version,
|
||||||
|
status=execution.status,
|
||||||
|
started_at=execution.started_at,
|
||||||
|
finished_at=execution.finished_at,
|
||||||
|
n_violations=len(execution.violations),
|
||||||
|
n_proposed_actions=len(execution.proposed_actions),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Tests de los helpers de persistencia JSONL append-only."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from agentforge_core.api.persistence import (
|
||||||
|
append_execution,
|
||||||
|
append_violation,
|
||||||
|
read_execution_summaries,
|
||||||
|
read_violations,
|
||||||
|
)
|
||||||
|
from agentforge_core.domain.execution import AgentExecution
|
||||||
|
from agentforge_core.domain.guardrail import GuardrailViolation
|
||||||
|
|
||||||
|
|
||||||
|
def _violation() -> GuardrailViolation:
|
||||||
|
return GuardrailViolation(
|
||||||
|
trace_id=uuid4(),
|
||||||
|
timestamp=datetime.now(UTC),
|
||||||
|
stage="input",
|
||||||
|
validator="DetectPII",
|
||||||
|
severity="block",
|
||||||
|
message="email detectado",
|
||||||
|
blocked=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execution(status: str = "completed") -> AgentExecution:
|
||||||
|
return AgentExecution(
|
||||||
|
trace_id=uuid4(),
|
||||||
|
agent_name="incident_analyzer",
|
||||||
|
agent_version="v1",
|
||||||
|
status=status, # type: ignore[arg-type]
|
||||||
|
started_at=datetime.now(UTC),
|
||||||
|
finished_at=datetime.now(UTC),
|
||||||
|
decision_path=[],
|
||||||
|
violations=[],
|
||||||
|
proposed_actions=[],
|
||||||
|
needs_human_for=None,
|
||||||
|
final_output={},
|
||||||
|
error=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_append_violation_escribe_jsonl(tmp_path: Path) -> None:
|
||||||
|
append_violation(tmp_path, _violation())
|
||||||
|
line = (tmp_path / "violations.jsonl").read_text(encoding="utf-8").strip()
|
||||||
|
assert json.loads(line)["validator"] == "DetectPII"
|
||||||
|
|
||||||
|
|
||||||
|
def test_append_execution_es_append_only(tmp_path: Path) -> None:
|
||||||
|
append_execution(tmp_path, _execution())
|
||||||
|
append_execution(tmp_path, _execution())
|
||||||
|
lines = (tmp_path / "executions.jsonl").read_text(encoding="utf-8").strip().splitlines()
|
||||||
|
assert len(lines) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_violations_filtra_lineas_vacias(tmp_path: Path) -> None:
|
||||||
|
append_violation(tmp_path, _violation())
|
||||||
|
with (tmp_path / "violations.jsonl").open("a", encoding="utf-8") as f:
|
||||||
|
f.write("\n")
|
||||||
|
out = read_violations(tmp_path)
|
||||||
|
assert len(out) == 1
|
||||||
|
assert out[0].validator == "DetectPII"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_violations_inexistente_devuelve_vacio(tmp_path: Path) -> None:
|
||||||
|
assert read_violations(tmp_path) == []
|
||||||
|
assert read_execution_summaries(tmp_path) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_execution_summaries_resume_campos(tmp_path: Path) -> None:
|
||||||
|
append_execution(tmp_path, _execution())
|
||||||
|
summaries = read_execution_summaries(tmp_path)
|
||||||
|
assert len(summaries) == 1
|
||||||
|
s = summaries[0]
|
||||||
|
assert s.agent_name == "incident_analyzer"
|
||||||
|
assert s.status == "completed"
|
||||||
|
assert s.n_violations == 0
|
||||||
|
assert s.n_proposed_actions == 0
|
||||||
Reference in New Issue
Block a user