From 13ba1f7826b9b54d6c1c07fc242469fda347e522 Mon Sep 17 00:00:00 2001 From: Juan Date: Sun, 10 May 2026 15:33:27 +0200 Subject: [PATCH] feat(api): implementa router /agents (list, get, versions, get_version, diff) Co-Authored-By: Claude Opus 4.7 --- core/src/agentforge_core/api/agents.py | 49 ++++++++++++- .../agents/incident_analyzer/index.yaml | 8 +++ .../agents/incident_analyzer/versions/v1.yaml | 17 +++++ tests/unit/test_api_agents.py | 70 +++++++++++++++++++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/agents/incident_analyzer/index.yaml create mode 100644 tests/fixtures/agents/incident_analyzer/versions/v1.yaml create mode 100644 tests/unit/test_api_agents.py diff --git a/core/src/agentforge_core/api/agents.py b/core/src/agentforge_core/api/agents.py index e57bbde..6ca0828 100644 --- a/core/src/agentforge_core/api/agents.py +++ b/core/src/agentforge_core/api/agents.py @@ -1,5 +1,50 @@ -"""Router stub de /agents — se implementa en una task posterior.""" +"""Router /agents: listado, detalle, versiones y diff entre versiones.""" -from fastapi import APIRouter +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException + +from agentforge_core.api.deps import get_registry +from agentforge_core.domain.agent import AgentDefinition, AgentVersionMeta +from agentforge_core.registry.repository import FileSystemAgentRegistry +from agentforge_core.registry.versioning import DiffResult router = APIRouter() + +RegistryDep = Annotated[FileSystemAgentRegistry, Depends(get_registry)] + + +@router.get("", response_model=list[AgentDefinition]) +def list_agents(registry: RegistryDep) -> list[AgentDefinition]: + return registry.list_agents() + + +@router.get("/{name}", response_model=AgentDefinition) +def get_agent(name: str, registry: RegistryDep) -> AgentDefinition: + try: + return registry.get_agent(name) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@router.get("/{name}/versions", response_model=list[AgentVersionMeta]) +def list_versions(name: str, registry: RegistryDep) -> list[AgentVersionMeta]: + try: + return registry.list_versions(name) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@router.get("/{name}/versions/{version}", response_model=AgentDefinition) +def get_version(name: str, version: str, registry: RegistryDep) -> AgentDefinition: + try: + return registry.get_version(name, version) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@router.get("/{name}/versions/{v_from}/diff/{v_to}", response_model=DiffResult) +def diff_versions(name: str, v_from: str, v_to: str, registry: RegistryDep) -> DiffResult: + return registry.diff_versions(name, v_from, v_to) diff --git a/tests/fixtures/agents/incident_analyzer/index.yaml b/tests/fixtures/agents/incident_analyzer/index.yaml new file mode 100644 index 0000000..b808709 --- /dev/null +++ b/tests/fixtures/agents/incident_analyzer/index.yaml @@ -0,0 +1,8 @@ +name: incident_analyzer +versions: + - id: v1 + hash: testhash1 + author: Juan + message: fixture inicial + created_at: 2026-04-01T00:00:00Z +active_version: v1 diff --git a/tests/fixtures/agents/incident_analyzer/versions/v1.yaml b/tests/fixtures/agents/incident_analyzer/versions/v1.yaml new file mode 100644 index 0000000..569e574 --- /dev/null +++ b/tests/fixtures/agents/incident_analyzer/versions/v1.yaml @@ -0,0 +1,17 @@ +name: incident_analyzer +version: v1 +owner: Juan +purpose: Análisis de incidentes de red +state: active +guardrails: [default] +llm: + provider: mock + model: gpt-4o + temperature: 0.2 + max_tokens: 2000 +system_prompt: Eres un analista de incidentes. Responde SIEMPRE con JSON. +output_schema: + type: object + required: [severity] +risk_threshold_for_hitl: 4 +updated_at: 2026-04-01T00:00:00Z diff --git a/tests/unit/test_api_agents.py b/tests/unit/test_api_agents.py new file mode 100644 index 0000000..be6877e --- /dev/null +++ b/tests/unit/test_api_agents.py @@ -0,0 +1,70 @@ +"""Tests del router /agents (list, get, versions, get_version, diff).""" + +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from agentforge_core.api import deps +from agentforge_core.main import create_app + +FIXTURES = Path(__file__).parent.parent / "fixtures" + + +@pytest.fixture(autouse=True) +def patch_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("AGENTS_DIR", str(FIXTURES / "agents")) + monkeypatch.setenv("POLICIES_DIR", str(FIXTURES / "policies")) + for fn in ( + deps.get_settings, + deps.get_registry, + deps.get_policy_store, + deps.get_orchestrator, + deps.get_llm_provider, + deps.get_guardrail_engine, + ): + fn.cache_clear() + + +def test_list_agents_incluye_incident_analyzer() -> None: + r = TestClient(create_app()).get("/agents") + assert r.status_code == 200 + assert any(a["name"] == "incident_analyzer" for a in r.json()) + + +def test_get_agent_activo_devuelve_v1() -> None: + r = TestClient(create_app()).get("/agents/incident_analyzer") + assert r.status_code == 200 + assert r.json()["version"] == "v1" + + +def test_get_agent_inexistente_404() -> None: + r = TestClient(create_app()).get("/agents/inexistente") + assert r.status_code == 404 + + +def test_list_versions_devuelve_meta() -> None: + r = TestClient(create_app()).get("/agents/incident_analyzer/versions") + assert r.status_code == 200 + assert r.json()[0]["id"] == "v1" + + +def test_get_version_concreta() -> None: + r = TestClient(create_app()).get("/agents/incident_analyzer/versions/v1") + assert r.status_code == 200 + assert r.json()["name"] == "incident_analyzer" + + +def test_get_version_inexistente_404() -> None: + r = TestClient(create_app()).get("/agents/incident_analyzer/versions/v999") + assert r.status_code == 404 + + +def test_diff_misma_version_da_diff_vacio() -> None: + r = TestClient(create_app()).get("/agents/incident_analyzer/versions/v1/diff/v1") + assert r.status_code == 200 + body = r.json() + assert body["from_version"] == "v1" + assert body["to_version"] == "v1" + assert body["unified_diff"] == ""