proyecto generalizado

This commit is contained in:
2026-05-23 16:44:45 +02:00
parent 2f990ea636
commit b4964261ec
94 changed files with 1032 additions and 320 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ LOG_LEVEL=INFO
DATA_DIR=./data DATA_DIR=./data
# URL del core (la usa el dashboard) # URL del core (la usa el dashboard)
AGENTFORGE_CORE_URL=http://core:8000 FORJA_CORE_URL=http://core:8000
+4 -4
View File
@@ -1,14 +1,14 @@
# AgentForge — Arquitectura técnica # Forja — Arquitectura técnica
> Documento "vivo" con las decisiones técnicas. El spec original está en > Documento "vivo" con las decisiones técnicas. El spec original está en
> [`docs/superpowers/specs/2026-05-09-agentforge-design.md`](docs/superpowers/specs/2026-05-09-agentforge-design.md). > [`docs/superpowers/specs/2026-05-09- forja-design.md`](docs/superpowers/specs/2026-05-09- forja-design.md).
## Visión general ## Visión general
Two-tier: Two-tier:
- `agentforge-core` — FastAPI 8000. Dominio de gobierno, runtime LangGraph, - `forja-core` — FastAPI 8000. Dominio de gobierno, runtime LangGraph,
guardrails y persistencia. guardrails y persistencia.
- `agentforge-dashboard` — Streamlit 8501. Cliente HTTP del core. - `forja-dashboard` — Streamlit 8501. Cliente HTTP del core.
## Diagrama ## Diagrama
+25 -32
View File
@@ -1,16 +1,9 @@
# 🛡️ AgentForge # 🔨 Forja
Plataforma profesional de **gobernanza de agentes IA**: catalogación, versionado **Forja de agentes IA** — plataforma de gobernanza genérica para agentes de cualquier tipo.
de prompts y políticas, guardrails runtime, ejecución stateful con Catalogación y versionado (estilo Git) de definiciones, políticas y guardrails;
Human-in-the-Loop, observabilidad y trazabilidad end-to-end. ejecución controlada con runtime stateful, Human-in-the-Loop y observabilidad completa.
Úsala como plano de control en cualquier proyecto que necesite agentes gobernados.
## ¿Por qué?
Poner agentes IA en producción sin una capa de gobierno produce sistemas
opacos: prompts que cambian sin historial, validaciones inconsistentes,
acciones de alto impacto sin supervisión, sin auditoría de decisiones.
AgentForge aporta el plano de control mínimo que un equipo de plataforma
necesita antes de operar agentes con impacto real.
## Arquitectura ## Arquitectura
@@ -18,13 +11,13 @@ necesita antes de operar agentes con impacto real.
┌───────────────────────── docker-compose ──────────────────────────┐ ┌───────────────────────── docker-compose ──────────────────────────┐
│ │ │ │
│ ┌─────────────────────┐ HTTP/JSON ┌────────────────────┐ │ │ ┌─────────────────────┐ HTTP/JSON ┌────────────────────┐ │
│ │ agentforge-dashboard│ ────────────────► │ agentforge-core │ │ │ │ forja-dashboard │ ────────────────► │ forja-core │ │
│ │ Streamlit :8501 │ ◄──────────────── │ FastAPI :8000 │ │ │ │ Streamlit :8501 │ ◄──────────────── │ FastAPI :8000 │ │
│ └─────────────────────┘ └────────────────────┘ │ │ └─────────────────────┘ └────────────────────┘ │
│ │ │ │
│ Strategy pattern (Protocol) para LLMProvider, GuardrailEngine, │ │ Strategy pattern para LLMProvider, GuardrailEngine, Registry.
AgentRegistry. Persistencia mixta: YAML (definiciones), JSON │ Persistencia: YAML (defs versionadas), JSONL (logs), SQLite
│ (registry), JSONL (logs append-only), SQLite (checkpoints). │ (checkpoints LangGraph + HITL).
└────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────┘
``` ```
@@ -65,14 +58,14 @@ Azure OpenAI real, edita `.env`.
| Feature | Ubicación | | Feature | Ubicación |
|---|---| |---|---|
| Agent Registry | `core/src/agentforge_core/registry/repository.py` | | Agent / Policy Registry | `core/src/forja_core/registry/` |
| Versionado tipo Git | `agents/<name>/versions/` + `registry/versioning.py` | | Versionado Git-like (YAML) | `agents/<name>/` + `policies/<name>/` |
| Guardrails runtime | `core/src/agentforge_core/guardrails/` | | Guardrails runtime (strategy) | `core/src/forja_core/guardrails/` |
| LangGraph stateful + checkpointing | `core/src/agentforge_core/runtime/` | | LangGraph + HITL + checkpoints | `core/src/forja_core/runtime/` |
| Human-in-the-Loop | nodo `approve_gate` + endpoints `/approve` y `/reject` | | Observabilidad (trace + structlog) | `core/src/forja_core/observability/` |
| Observabilidad | `observability/logging.py` (structlog + trace_id) | | LLM abstraction | `core/src/forja_core/llm/` |
| LLM provider abstraction | `core/src/agentforge_core/llm/` | | Editores gráficos (nuevo) | dashboard páginas Forjar Agente / Política |
| Política versionada | `policies/default/versions/v1.yaml` | | API REST + dashboard | forja-core :8000 + forja-dashboard :8501 |
## Variables de entorno ## Variables de entorno
@@ -85,14 +78,14 @@ Ver [`docs/futuro.md`](docs/futuro.md).
## Estructura del repositorio ## Estructura del repositorio
``` ```
agentforge/ forja/
├── core/ servicio FastAPI ├── core/ servicio FastAPI (gobernanza + runtime)
├── dashboard/ servicio Streamlit ├── dashboard/ Streamlit (UI + editores gráficos)
├── agents/ definiciones declarativas (YAML) ├── agents/ definiciones de agentes (YAML versionados)
├── policies/ políticas de guardrails ├── policies/ políticas de guardrails (YAML versionados)
├── data/ estado runtime (gitignored) ├── data/ runtime state (gitignored)
├── tests/ pytest unit + integration ├── tests/
└── docs/ documentación adicional └── docs/
``` ```
## Tests ## Tests
+1 -1
View File
@@ -33,4 +33,4 @@ EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \ HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
CMD curl -fsS http://localhost:8000/health || exit 1 CMD curl -fsS http://localhost:8000/health || exit 1
CMD ["uvicorn", "agentforge_core.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "forja_core.main:app", "--host", "0.0.0.0", "--port", "8000"]
-23
View File
@@ -1,23 +0,0 @@
"""Router /policies: listado de políticas y de sus versiones."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from agentforge_core.api.deps import PolicyStoreDep
from agentforge_core.domain.policy import PolicyDefinition, PolicyVersionMeta
router = APIRouter()
@router.get("", response_model=list[PolicyDefinition])
def list_policies(store: PolicyStoreDep) -> list[PolicyDefinition]:
return store.list_policies()
@router.get("/{name}/versions", response_model=list[PolicyVersionMeta])
def list_versions(name: str, store: PolicyStoreDep) -> list[PolicyVersionMeta]:
try:
return store.list_versions(name)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@@ -2,15 +2,22 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from agentforge_core.api.deps import RegistryDep from forja_core.api.deps import RegistryDep
from agentforge_core.domain.agent import AgentDefinition, AgentVersionMeta from forja_core.domain.agent import AgentDefinition, AgentVersionMeta
from agentforge_core.registry.versioning import DiffResult from forja_core.registry.versioning import DiffResult
router = APIRouter() router = APIRouter()
class CreateAgentVersionRequest(BaseModel):
agent: AgentDefinition
message: str
author: str = "dashboard"
@router.get("", response_model=list[AgentDefinition]) @router.get("", response_model=list[AgentDefinition])
def list_agents(registry: RegistryDep) -> list[AgentDefinition]: def list_agents(registry: RegistryDep) -> list[AgentDefinition]:
return registry.list_agents() return registry.list_agents()
@@ -43,3 +50,12 @@ def get_version(name: str, version: str, registry: RegistryDep) -> AgentDefiniti
@router.get("/{name}/versions/{v_from}/diff/{v_to}", response_model=DiffResult) @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: def diff_versions(name: str, v_from: str, v_to: str, registry: RegistryDep) -> DiffResult:
return registry.diff_versions(name, v_from, v_to) return registry.diff_versions(name, v_from, v_to)
@router.post("/{name}/versions", response_model=AgentVersionMeta, status_code=status.HTTP_201_CREATED)
def create_agent_version(name: str, req: CreateAgentVersionRequest, registry: RegistryDep) -> AgentVersionMeta:
try:
return registry.upsert_version(name, req.agent, req.message, req.author)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@@ -12,15 +12,15 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from agentforge_core.config import Settings from forja_core.config import Settings
from agentforge_core.guardrails.base import GuardrailEngine from forja_core.guardrails.base import GuardrailEngine
from agentforge_core.guardrails.factory import build_guardrail_engine from forja_core.guardrails.factory import build_guardrail_engine
from agentforge_core.llm.base import LLMProvider from forja_core.llm.base import LLMProvider
from agentforge_core.llm.factory import build_llm_provider from forja_core.llm.factory import build_llm_provider
from agentforge_core.registry.factory import build_agent_registry, build_policy_store from forja_core.registry.factory import build_agent_registry, build_policy_store
from agentforge_core.registry.policy_store import FileSystemPolicyStore from forja_core.registry.policy_store import FileSystemPolicyStore
from agentforge_core.registry.repository import FileSystemAgentRegistry from forja_core.registry.repository import FileSystemAgentRegistry
from agentforge_core.runtime.orchestrator import AgentOrchestrator from forja_core.runtime.orchestrator import AgentOrchestrator
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
@@ -9,23 +9,23 @@ from uuid import UUID
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from agentforge_core.api.deps import ( from forja_core.api.deps import (
OrchestratorDep, OrchestratorDep,
PolicyStoreDep, PolicyStoreDep,
RegistryDep, RegistryDep,
SettingsDep, SettingsDep,
) )
from agentforge_core.api.persistence import ( from forja_core.api.persistence import (
append_execution, append_execution,
append_violation, append_violation,
read_execution_summaries, read_execution_summaries,
) )
from agentforge_core.domain.agent import AgentDefinition from forja_core.domain.agent import AgentDefinition
from agentforge_core.domain.execution import AgentExecution, AgentExecutionSummary from forja_core.domain.execution import AgentExecution, AgentExecutionSummary
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.registry.policy_store import FileSystemPolicyStore from forja_core.registry.policy_store import FileSystemPolicyStore
from agentforge_core.registry.repository import FileSystemAgentRegistry from forja_core.registry.repository import FileSystemAgentRegistry
from agentforge_core.runtime.orchestrator import AgentOrchestrator from forja_core.runtime.orchestrator import AgentOrchestrator
router = APIRouter() router = APIRouter()
# POST /agents/{name}/invoke vive aquí pero se monta en main bajo el prefijo /agents. # POST /agents/{name}/invoke vive aquí pero se monta en main bajo el prefijo /agents.
@@ -8,7 +8,7 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoin
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
from agentforge_core.observability.logging import bind_trace_id, clear_trace_id from forja_core.observability.logging import bind_trace_id, clear_trace_id
class TraceIdMiddleware(BaseHTTPMiddleware): class TraceIdMiddleware(BaseHTTPMiddleware):
@@ -8,8 +8,8 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from agentforge_core.domain.execution import AgentExecution, AgentExecutionSummary from forja_core.domain.execution import AgentExecution, AgentExecutionSummary
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
def append_violation(data_dir: Path, violation: GuardrailViolation) -> None: def append_violation(data_dir: Path, violation: GuardrailViolation) -> None:
+47
View File
@@ -0,0 +1,47 @@
"""Router /policies: listado de políticas y de sus versiones."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from forja_core.api.deps import PolicyStoreDep
from forja_core.domain.policy import PolicyDefinition, PolicyVersionMeta
router = APIRouter()
class CreatePolicyVersionRequest(BaseModel):
policy: PolicyDefinition
message: str
author: str = "dashboard"
@router.get("", response_model=list[PolicyDefinition])
def list_policies(store: PolicyStoreDep) -> list[PolicyDefinition]:
return store.list_policies()
@router.get("/{name}/versions", response_model=list[PolicyVersionMeta])
def list_versions(name: str, store: PolicyStoreDep) -> list[PolicyVersionMeta]:
try:
return store.list_versions(name)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.post("/{name}/versions", response_model=PolicyVersionMeta, status_code=status.HTTP_201_CREATED)
def create_policy_version(name: str, req: CreatePolicyVersionRequest, store: PolicyStoreDep) -> PolicyVersionMeta:
try:
return store.upsert_version(name, req.policy, req.message, req.author)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.get("/validators")
def list_validators() -> dict[str, list[str]]:
return {
"input": ["detect_pii", "prompt_injection", "toxic_language", "forbidden_topics"],
"output": ["schema_match", "pii_leakage", "forbidden_action_keywords", "telco_safety_rules"],
}
@@ -7,9 +7,9 @@ from uuid import UUID
from fastapi import APIRouter, Query from fastapi import APIRouter, Query
from agentforge_core.api.deps import SettingsDep from forja_core.api.deps import SettingsDep
from agentforge_core.api.persistence import read_violations from forja_core.api.persistence import read_violations
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
router = APIRouter() router = APIRouter()
@@ -41,3 +41,9 @@ class AgentDefinition(BaseModel):
output_schema: dict[str, Any] output_schema: dict[str, Any]
risk_threshold_for_hitl: int = Field(default=4, ge=1, le=5) risk_threshold_for_hitl: int = Field(default=4, ge=1, le=5)
updated_at: datetime updated_at: datetime
input_label: str | None = None
input_placeholder: str | None = None
category: str | None = None
tags: list[str] = Field(default_factory=list)
icon: str | None = None
template: str = "governed_llm"
@@ -8,7 +8,7 @@ from uuid import UUID
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
class ProposedAction(BaseModel): class ProposedAction(BaseModel):
@@ -5,8 +5,8 @@ from __future__ import annotations
from typing import Any, Protocol from typing import Any, Protocol
from uuid import UUID from uuid import UUID
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
class GuardrailEngine(Protocol): class GuardrailEngine(Protocol):
@@ -6,9 +6,9 @@ import asyncio
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.base import GuardrailEngine from forja_core.guardrails.base import GuardrailEngine
class CompositeGuardrailEngine: class CompositeGuardrailEngine:
@@ -2,11 +2,11 @@
from __future__ import annotations from __future__ import annotations
from agentforge_core.config import Settings from forja_core.config import Settings
from agentforge_core.guardrails.base import GuardrailEngine from forja_core.guardrails.base import GuardrailEngine
from agentforge_core.guardrails.composite import CompositeGuardrailEngine from forja_core.guardrails.composite import CompositeGuardrailEngine
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine from forja_core.guardrails.guardrails_ai import GuardrailsAIEngine
from agentforge_core.guardrails.nemo import NeMoGuardrailsEngine from forja_core.guardrails.nemo import NeMoGuardrailsEngine
def build_guardrail_engine(settings: Settings) -> GuardrailEngine: def build_guardrail_engine(settings: Settings) -> GuardrailEngine:
@@ -9,9 +9,9 @@ from uuid import UUID
import structlog import structlog
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition, PolicyValidator from forja_core.domain.policy import PolicyDefinition, PolicyValidator
from agentforge_core.guardrails.validators import ( from forja_core.guardrails.validators import (
detect_pii, detect_pii,
forbidden_action_keywords, forbidden_action_keywords,
forbidden_topics, forbidden_topics,
@@ -12,8 +12,8 @@ from uuid import UUID
import structlog import structlog
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
log = structlog.get_logger(__name__) log = structlog.get_logger(__name__)
@@ -10,7 +10,7 @@ from uuid import UUID
import jsonschema import jsonschema
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
_INJECTION_PATTERNS = [ _INJECTION_PATTERNS = [
r"ignore (the )?(previous|prior|above) (instruction|prompt|message)", r"ignore (the )?(previous|prior|above) (instruction|prompt|message)",
@@ -8,7 +8,7 @@ from typing import Any
from openai import AsyncAzureOpenAI from openai import AsyncAzureOpenAI
from agentforge_core.llm.base import CompletionResult, Message from forja_core.llm.base import CompletionResult, Message
class AzureOpenAIProvider: class AzureOpenAIProvider:
@@ -2,11 +2,11 @@
from __future__ import annotations from __future__ import annotations
from agentforge_core.config import Settings from forja_core.config import Settings
from agentforge_core.llm.azure import AzureOpenAIProvider from forja_core.llm.azure import AzureOpenAIProvider
from agentforge_core.llm.base import LLMProvider from forja_core.llm.base import LLMProvider
from agentforge_core.llm.mock import MockProvider from forja_core.llm.mock import MockProvider
from agentforge_core.llm.openai import OpenAIProvider from forja_core.llm.openai import OpenAIProvider
def build_llm_provider(settings: Settings) -> LLMProvider: def build_llm_provider(settings: Settings) -> LLMProvider:
@@ -7,7 +7,7 @@ import json
import time import time
from typing import Any from typing import Any
from agentforge_core.llm.base import CompletionResult, Message from forja_core.llm.base import CompletionResult, Message
_CANONICAL_RESPONSES: dict[str, dict[str, Any]] = { _CANONICAL_RESPONSES: dict[str, dict[str, Any]] = {
"sip": { "sip": {
@@ -8,7 +8,7 @@ from typing import Any
from openai import AsyncOpenAI from openai import AsyncOpenAI
from agentforge_core.llm.base import CompletionResult, Message from forja_core.llm.base import CompletionResult, Message
class OpenAIProvider: class OpenAIProvider:
@@ -1,12 +1,12 @@
"""Composición raíz de la aplicación FastAPI (agentforge-core).""" """Composición raíz de la aplicación FastAPI (forja-core)."""
from __future__ import annotations from __future__ import annotations
from fastapi import FastAPI from fastapi import FastAPI
from agentforge_core.api.middlewares import TraceIdMiddleware from forja_core.api.middlewares import TraceIdMiddleware
from agentforge_core.config import Settings from forja_core.config import Settings
from agentforge_core.observability.logging import configure_logging from forja_core.observability.logging import configure_logging
def create_app() -> FastAPI: def create_app() -> FastAPI:
@@ -14,7 +14,7 @@ def create_app() -> FastAPI:
settings = Settings() settings = Settings()
configure_logging(level=settings.log_level) configure_logging(level=settings.log_level)
app = FastAPI( app = FastAPI(
title="AgentForge Core", title="Forja Core",
version="0.1.0", version="0.1.0",
description="Plataforma de gobernanza de agentes IA — API REST.", description="Plataforma de gobernanza de agentes IA — API REST.",
) )
@@ -24,7 +24,7 @@ def create_app() -> FastAPI:
async def health() -> dict[str, str]: async def health() -> dict[str, str]:
return {"status": "ok"} return {"status": "ok"}
from agentforge_core.api import agents, executions, policies, violations from forja_core.api import agents, executions, policies, violations
app.include_router(agents.router, prefix="/agents", tags=["agents"]) app.include_router(agents.router, prefix="/agents", tags=["agents"])
app.include_router(executions.invoke_router, prefix="/agents", tags=["agents"]) app.include_router(executions.invoke_router, prefix="/agents", tags=["agents"])
@@ -2,9 +2,9 @@
from __future__ import annotations from __future__ import annotations
from agentforge_core.config import Settings from forja_core.config import Settings
from agentforge_core.registry.policy_store import FileSystemPolicyStore from forja_core.registry.policy_store import FileSystemPolicyStore
from agentforge_core.registry.repository import FileSystemAgentRegistry from forja_core.registry.repository import FileSystemAgentRegistry
def build_agent_registry(settings: Settings) -> FileSystemAgentRegistry: def build_agent_registry(settings: Settings) -> FileSystemAgentRegistry:
@@ -2,13 +2,14 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import yaml import yaml
from agentforge_core.domain.policy import PolicyDefinition, PolicyVersionMeta from forja_core.domain.policy import PolicyDefinition, PolicyVersionMeta
from forja_core.registry.versioning import compute_hash
class FileSystemPolicyStore: class FileSystemPolicyStore:
@@ -62,3 +63,48 @@ class FileSystemPolicyStore:
if isinstance(value, datetime): if isinstance(value, datetime):
return value return value
return datetime.fromisoformat(value.replace("Z", "+00:00")) return datetime.fromisoformat(value.replace("Z", "+00:00"))
def upsert_version(
self,
name: str,
body: PolicyDefinition,
message: str,
author: str,
) -> PolicyVersionMeta:
policy_dir = self._root / name
versions_dir = policy_dir / "versions"
versions_dir.mkdir(parents=True, exist_ok=True)
version_id = body.version
yaml_text = yaml.safe_dump(
body.model_dump(mode="json"),
sort_keys=False,
allow_unicode=True,
)
h = compute_hash(yaml_text)
version_path = versions_dir / f"{version_id}.yaml"
version_path.write_text(yaml_text, encoding="utf-8")
meta = PolicyVersionMeta(
id=version_id,
hash=h,
author=author,
message=message,
created_at=datetime.now(UTC),
)
index_path = policy_dir / "index.yaml"
if index_path.exists():
with index_path.open(encoding="utf-8") as f:
index: dict[str, Any] = yaml.safe_load(f)
else:
index = {"name": name, "versions": [], "active_version": version_id}
index["versions"] = [v for v in index["versions"] if v["id"] != version_id]
index["versions"].append(meta.model_dump(mode="json"))
index["active_version"] = version_id
with index_path.open("w", encoding="utf-8") as f:
yaml.safe_dump(index, f, sort_keys=False, allow_unicode=True)
return meta
@@ -8,8 +8,8 @@ from typing import Any
import yaml import yaml
from agentforge_core.domain.agent import AgentDefinition, AgentVersionMeta from forja_core.domain.agent import AgentDefinition, AgentVersionMeta
from agentforge_core.registry.versioning import DiffResult, compute_hash, unified_diff from forja_core.registry.versioning import DiffResult, compute_hash, unified_diff
class FileSystemAgentRegistry: class FileSystemAgentRegistry:
@@ -7,11 +7,11 @@ from typing import Any
from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.graph import END, START, StateGraph from langgraph.graph import END, START, StateGraph
from agentforge_core.domain.agent import AgentDefinition from forja_core.domain.agent import AgentDefinition
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.base import GuardrailEngine from forja_core.guardrails.base import GuardrailEngine
from agentforge_core.llm.base import LLMProvider from forja_core.llm.base import LLMProvider
from agentforge_core.runtime.nodes import ( from forja_core.runtime.nodes import (
build_node_approve_gate, build_node_approve_gate,
build_node_finalize, build_node_finalize,
build_node_llm_reason, build_node_llm_reason,
@@ -19,7 +19,7 @@ from agentforge_core.runtime.nodes import (
build_node_validate_input, build_node_validate_input,
build_node_validate_output, build_node_validate_output,
) )
from agentforge_core.runtime.state import AgentState from forja_core.runtime.state import AgentState
def build_graph( def build_graph(
@@ -12,11 +12,11 @@ from uuid import UUID
import structlog import structlog
from langgraph.types import interrupt from langgraph.types import interrupt
from agentforge_core.domain.agent import AgentDefinition from forja_core.domain.agent import AgentDefinition
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.base import GuardrailEngine from forja_core.guardrails.base import GuardrailEngine
from agentforge_core.llm.base import LLMProvider, Message from forja_core.llm.base import LLMProvider, Message
from agentforge_core.runtime.state import AgentState from forja_core.runtime.state import AgentState
log = structlog.get_logger(__name__) log = structlog.get_logger(__name__)
@@ -18,14 +18,14 @@ from uuid import UUID, uuid4
import structlog import structlog
from langgraph.types import Command from langgraph.types import Command
from agentforge_core.domain.agent import AgentDefinition from forja_core.domain.agent import AgentDefinition
from agentforge_core.domain.execution import AgentExecution, DecisionStep, ProposedAction from forja_core.domain.execution import AgentExecution, DecisionStep, ProposedAction
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.base import GuardrailEngine from forja_core.guardrails.base import GuardrailEngine
from agentforge_core.llm.base import LLMProvider from forja_core.llm.base import LLMProvider
from agentforge_core.runtime.checkpointer import build_checkpointer from forja_core.runtime.checkpointer import build_checkpointer
from agentforge_core.runtime.graph import build_graph from forja_core.runtime.graph import build_graph
log = structlog.get_logger(__name__) log = structlog.get_logger(__name__)
+1 -1
View File
@@ -26,6 +26,6 @@ EXPOSE 8501
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS http://localhost:8501/_stcore/health || exit 1 CMD curl -fsS http://localhost:8501/_stcore/health || exit 1
CMD ["streamlit", "run", "/app/src/agentforge_dashboard/app.py", \ CMD ["streamlit", "run", "/app/src/forja_dashboard/app.py", \
"--server.address=0.0.0.0", "--server.port=8501", \ "--server.address=0.0.0.0", "--server.port=8501", \
"--server.headless=true", "--browser.gatherUsageStats=false"] "--server.headless=true", "--browser.gatherUsageStats=false"]
-47
View File
@@ -1,47 +0,0 @@
"""Entry point del dashboard Streamlit con sidebar de branding y health."""
from __future__ import annotations
import streamlit as st
from agentforge_dashboard.client import CoreClient
@st.cache_resource
def get_client() -> CoreClient:
return CoreClient()
def main() -> None:
st.set_page_config(
page_title="AgentForge",
page_icon="🛡️",
layout="wide",
initial_sidebar_state="expanded",
)
st.sidebar.markdown("## 🛡️ AgentForge")
st.sidebar.caption("Plataforma de gobernanza de agentes IA")
client = get_client()
try:
client.health()
st.sidebar.success("Core API: OK")
except Exception as exc:
st.sidebar.error(f"Core API unreachable: {exc}")
st.title("🛡️ AgentForge")
st.markdown(
"""
Bienvenido al panel de gobernanza de agentes IA.
Usa la barra lateral para navegar:
- **Registro**: catálogo de agentes y versiones.
- **Ejecutar**: lanzar un agente con guardrails completos.
- **Aprobaciones**: ejecuciones pausadas a la espera de revisión humana.
- **Historial**: trazas, violaciones y resultados.
- **Politicas**: políticas de guardrails y diff entre versiones.
"""
)
if __name__ == "__main__":
main()
+47
View File
@@ -0,0 +1,47 @@
"""Entry point del dashboard Streamlit con sidebar de branding y health."""
from __future__ import annotations
import streamlit as st
from forja_dashboard.client import CoreClient
@st.cache_resource
def get_client() -> CoreClient:
return CoreClient()
def main() -> None:
st.set_page_config(
page_title="Forja",
page_icon="🔨",
layout="wide",
initial_sidebar_state="expanded",
)
st.sidebar.markdown("## 🔨 Forja")
st.sidebar.caption("Forja de agentes IA — gobernanza para cualquier proyecto")
client = get_client()
try:
client.health()
st.sidebar.success("Core API: OK")
except Exception as exc:
st.sidebar.error(f"Core API unreachable: {exc}")
st.title("🔨 Forja")
st.markdown(
"""
Bienvenido a la forja de agentes IA.
Navega por la barra lateral:
- **Registro**: catálogo y versiones.
- **Ejecutar**: invocar con gobierno completo.
- **Aprobaciones** / **Historial**.
- **Politicas**.
- **🔨 Forjar Agente** y **🛡️ Forjar Politica**: editores gráficos (nuevo).
"""
)
if __name__ == "__main__":
main()
@@ -12,7 +12,7 @@ class CoreClient:
"""Cliente sincrono (Streamlit es sync). 2 retries con httpx Transport.""" """Cliente sincrono (Streamlit es sync). 2 retries con httpx Transport."""
def __init__(self, base_url: str | None = None, timeout: float = 30.0) -> None: def __init__(self, base_url: str | None = None, timeout: float = 30.0) -> None:
self.base_url = base_url or os.getenv("AGENTFORGE_CORE_URL", "http://localhost:8000") self.base_url = base_url or os.getenv("FORJA_CORE_URL", "http://localhost:8000")
transport = httpx.HTTPTransport(retries=2) transport = httpx.HTTPTransport(retries=2)
self._client = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport) self._client = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport)
@@ -56,6 +56,17 @@ class CoreClient:
def list_policy_versions(self, name: str) -> list[dict]: def list_policy_versions(self, name: str) -> list[dict]:
return self._get(f"/policies/{name}/versions") return self._get(f"/policies/{name}/versions")
def create_agent_version(self, name: str, agent_def: dict, message: str, author: str = "dashboard") -> dict:
body = {"agent": agent_def, "message": message, "author": author}
return self._post(f"/agents/{name}/versions", body)
def create_policy_version(self, name: str, policy_def: dict, message: str, author: str = "dashboard") -> dict:
body = {"policy": policy_def, "message": message, "author": author}
return self._post(f"/policies/{name}/versions", body)
def list_validators(self) -> dict:
return self._get("/policies/validators")
# ----- helpers privados ----- # ----- helpers privados -----
def _get(self, path: str, params: dict | None = None) -> Any: def _get(self, path: str, params: dict | None = None) -> Any:
@@ -4,8 +4,8 @@ from __future__ import annotations
import streamlit as st import streamlit as st
from agentforge_dashboard.client import CoreClient from forja_dashboard.client import CoreClient
from agentforge_dashboard.components.diff_view import render_unified_diff from forja_dashboard.components.diff_view import render_unified_diff
@st.cache_resource @st.cache_resource
@@ -16,6 +16,7 @@ def get_client() -> CoreClient:
client = get_client() client = get_client()
st.title("🏛️ Registro de Agentes") st.title("🏛️ Registro de Agentes")
st.caption("Catálogo central de agentes con versionado tipo Git.") st.caption("Catálogo central de agentes con versionado tipo Git.")
st.info("Usa **🔨 Forjar Agente** (barra lateral) para crear o versionar gráficamente.")
agents = client.list_agents() agents = client.list_agents()
if not agents: if not agents:
@@ -6,9 +6,9 @@ from pathlib import Path
import streamlit as st import streamlit as st
from agentforge_dashboard.client import CoreClient from forja_dashboard.client import CoreClient
from agentforge_dashboard.components.trace_view import render_trace from forja_dashboard.components.trace_view import render_trace
from agentforge_dashboard.components.violation_view import render_violations from forja_dashboard.components.violation_view import render_violations
@st.cache_resource @st.cache_resource
@@ -27,6 +27,7 @@ if not agents:
names = [a["name"] for a in agents] names = [a["name"] for a in agents]
agent_name = st.selectbox("Agente", names) agent_name = st.selectbox("Agente", names)
agent = client.get_agent(agent_name)
# Cargar escenarios pregrabados si existen en el disco montado. # Cargar escenarios pregrabados si existen en el disco montado.
examples_dir = Path("agents") / agent_name / "examples" examples_dir = Path("agents") / agent_name / "examples"
@@ -40,12 +41,9 @@ with col_r:
st.session_state["input_text"] = ef.read_text(encoding="utf-8") st.session_state["input_text"] = ef.read_text(encoding="utf-8")
with col_l: with col_l:
user_input = st.text_area( label = agent.get("input_label") or "Input para el agente"
"Descripción del incidente", ph = agent.get("input_placeholder") or "Escribe el input para el agente o usa un escenario..."
height=240, user_input = st.text_area(label, height=240, key="input_text", placeholder=ph)
key="input_text",
placeholder="Pega aquí la descripción del incidente o usa un escenario...",
)
if st.button("🚀 Invocar agente", type="primary", disabled=not user_input): if st.button("🚀 Invocar agente", type="primary", disabled=not user_input):
with st.spinner("Ejecutando..."): with st.spinner("Ejecutando..."):
@@ -4,7 +4,7 @@ from __future__ import annotations
import streamlit as st import streamlit as st
from agentforge_dashboard.client import CoreClient from forja_dashboard.client import CoreClient
@st.cache_resource @st.cache_resource
@@ -4,9 +4,9 @@ from __future__ import annotations
import streamlit as st import streamlit as st
from agentforge_dashboard.client import CoreClient from forja_dashboard.client import CoreClient
from agentforge_dashboard.components.trace_view import render_trace from forja_dashboard.components.trace_view import render_trace
from agentforge_dashboard.components.violation_view import render_violations from forja_dashboard.components.violation_view import render_violations
@st.cache_resource @st.cache_resource
@@ -4,7 +4,7 @@ from __future__ import annotations
import streamlit as st import streamlit as st
from agentforge_dashboard.client import CoreClient from forja_dashboard.client import CoreClient
@st.cache_resource @st.cache_resource
@@ -15,6 +15,7 @@ def get_client() -> CoreClient:
client = get_client() client = get_client()
st.title("📐 Politicas de guardrails") st.title("📐 Politicas de guardrails")
st.caption("Inventario de políticas y sus versiones (estilo Git).") st.caption("Inventario de políticas y sus versiones (estilo Git).")
st.info("Usa **🛡️ Forjar Politica** (barra lateral) para componer y versionar gráficamente.")
policies = client.list_policies() policies = client.list_policies()
if not policies: if not policies:
@@ -0,0 +1,115 @@
"""Página de editor gráfico para crear o versionar agentes (Forja)."""
from __future__ import annotations
import json
import streamlit as st
from forja_dashboard.client import CoreClient
@st.cache_resource
def get_client() -> CoreClient:
return CoreClient()
client = get_client()
st.title("🔨 Forjar Agente")
st.caption("Editor gráfico. Crea un agente nuevo o una nueva versión de uno existente. Todo se guarda como YAML versionado.")
agents = client.list_agents()
agent_names = [a["name"] for a in agents] if agents else []
mode = st.radio("Modo", ["Nuevo agente", "Nueva versión de existente"], horizontal=True)
base_name = None
if mode == "Nueva versión de existente" and agent_names:
base_name = st.selectbox("Agente base", agent_names)
elif mode == "Nueva versión de existente":
st.warning("No hay agentes. Crea uno nuevo.")
st.stop()
with st.form("forge_agent", clear_on_submit=False):
col1, col2 = st.columns(2)
with col1:
name = st.text_input("Nombre del agente", value=base_name or "mi-nuevo-agente")
version = st.text_input("Versión (ej. v1)", value="v1")
owner = st.text_input("Owner", value="usuario")
state = st.selectbox("Estado", ["draft", "active", "deprecated"], index=1)
with col2:
purpose = st.text_area("Propósito / descripción", value="Agente gobernado por Forja", height=80)
category = st.text_input("Categoría (opcional)", value="")
tags_str = st.text_input("Tags (coma separadas)", value="")
icon = st.text_input("Icono emoji (opcional)", value="🤖")
st.subheader("LLM")
llm_col1, llm_col2 = st.columns(4)
with llm_col1:
provider = st.selectbox("Provider", ["mock", "azure", "openai"], index=0)
with llm_col2:
model = st.text_input("Modelo", value="gpt-4o")
with llm_col3:
temperature = st.number_input("Temperature", 0.0, 2.0, 0.2, 0.1)
with llm_col4:
max_tokens = st.number_input("Max tokens", 1, 128000, 2000)
st.subheader("Prompt y Schema")
system_prompt = st.text_area("System prompt", value="Eres un asistente útil. Responde en JSON según el schema.", height=160)
schema_text = st.text_area("Output schema (JSON)", value=json.dumps({"type": "object", "properties": {"result": {"type": "string"}}}, indent=2), height=120)
st.subheader("Guardrails y HITL")
policies = client.list_policies()
policy_names = [p["name"] for p in policies] if policies else ["default"]
guardrails = st.multiselect("Políticas de guardrails activas", policy_names, default=policy_names[:1] if policy_names else [])
risk = st.slider("Risk threshold para HITL", 1, 5, 4)
st.subheader("Metadatos de versión")
message = st.text_input("Mensaje de versión (como commit)", value="Creación vía editor gráfico de Forja")
author = st.text_input("Autor", value="dashboard")
submitted = st.form_submit_button("🔨 Forjar / Guardar versión", type="primary")
if submitted:
try:
output_schema = json.loads(schema_text)
except Exception as e:
st.error(f"Schema JSON inválido: {e}")
st.stop()
tags = [t.strip() for t in tags_str.split(",") if t.strip()] if tags_str else []
agent_def = {
"name": name,
"version": version,
"owner": owner,
"purpose": purpose,
"state": state,
"guardrails": guardrails or ["default"],
"llm": {
"provider": provider,
"model": model,
"temperature": float(temperature),
"max_tokens": int(max_tokens),
},
"system_prompt": system_prompt,
"output_schema": output_schema,
"risk_threshold_for_hitl": int(risk),
"updated_at": "2026-05-23T00:00:00Z",
"input_label": "Input para el agente",
"input_placeholder": "Escribe aquí el input...",
"category": category or None,
"tags": tags,
"icon": icon or None,
"template": "governed_llm",
}
try:
res = client.create_agent_version(name, agent_def, message, author)
if "api_error" in res:
st.error(f"Error API: {res['api_error']}")
else:
st.success(f"Versión {version} de {name} creada correctamente. Recarga Registro para verla.")
st.json(res)
except Exception as exc:
st.error(f"Error al forjar: {exc}")
@@ -0,0 +1,82 @@
"""Página de editor gráfico para políticas de guardrails (Forja)."""
from __future__ import annotations
import json
import streamlit as st
from forja_dashboard.client import CoreClient
@st.cache_resource
def get_client() -> CoreClient:
return CoreClient()
client = get_client()
st.title("🛡️ Forjar Política")
st.caption("Compositor gráfico de políticas. Añade validadores de entrada/salida, configura y versiona.")
policies = client.list_policies()
policy_names = [p["name"] for p in policies] if policies else []
mode = st.radio("Modo", ["Nueva política", "Nueva versión de existente"], horizontal=True)
base = None
if mode == "Nueva versión de existente" and policy_names:
base = st.selectbox("Política base", policy_names)
validators_meta = client.list_validators()
input_types = validators_meta.get("input", [])
output_types = validators_meta.get("output", [])
def validator_form(stage: str, types: list[str], key_prefix: str) -> list[dict]:
st.markdown(f"**Validadores de {stage}**")
result = []
num = st.number_input(f"Número de validadores {stage}", 0, 12, 2, key=f"num_{key_prefix}")
for i in range(int(num)):
with st.expander(f"Validador {i+1} ({stage})", expanded=True):
t = st.selectbox("Tipo", types, key=f"type_{key_prefix}_{i}")
cfg_text = st.text_area("Config (JSON)", value="{}", key=f"cfg_{key_prefix}_{i}", height=80)
try:
cfg = json.loads(cfg_text)
except Exception:
cfg = {}
result.append({"type": t, "config": cfg})
return result
with st.form("forge_policy"):
name = st.text_input("Nombre de la política", value=base or "mi-politica")
version = st.text_input("Versión", value="v1")
description = st.text_area("Descripción", value="Política creada con el editor gráfico de Forja")
on_error = st.selectbox("on_validator_error", ["fail_closed", "fail_open"], index=0)
st.divider()
input_validators = validator_form("input", input_types, "in")
output_validators = validator_form("output", output_types, "out")
st.subheader("Metadatos de versión")
message = st.text_input("Mensaje", value="Creada vía UI gráfica")
author = st.text_input("Autor", value="dashboard")
submitted = st.form_submit_button("🛡️ Guardar versión de política", type="primary")
if submitted:
policy_def = {
"name": name,
"version": version,
"description": description,
"input_validators": input_validators,
"output_validators": output_validators,
"on_validator_error": on_error,
}
try:
res = client.create_policy_version(name, policy_def, message, author)
if "api_error" in res:
st.error(res["api_error"])
else:
st.success(f"Política {name}@{version} guardada.")
st.json(res)
except Exception as e:
st.error(str(e))
+8 -8
View File
@@ -3,8 +3,8 @@ services:
build: build:
context: . context: .
dockerfile: core/Dockerfile dockerfile: core/Dockerfile
image: agentforge-core:dev image: forja-core:dev
container_name: agentforge-core container_name: forja-core
ports: ports:
- "8000:8000" - "8000:8000"
env_file: env_file:
@@ -14,8 +14,8 @@ services:
AGENTS_DIR: /app/agents AGENTS_DIR: /app/agents
POLICIES_DIR: /app/policies POLICIES_DIR: /app/policies
volumes: volumes:
- ./agents:/app/agents:ro - ./agents:/app/agents:rw
- ./policies:/app/policies:ro - ./policies:/app/policies:rw
- ./data:/app/data - ./data:/app/data
healthcheck: healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"] test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
@@ -29,15 +29,15 @@ services:
build: build:
context: . context: .
dockerfile: dashboard/Dockerfile dockerfile: dashboard/Dockerfile
image: agentforge-dashboard:dev image: forja-dashboard:dev
container_name: agentforge-dashboard container_name: forja-dashboard
depends_on: depends_on:
core: core:
condition: service_healthy condition: service_healthy
ports: ports:
- "8501:8501" - "8501:8501"
environment: environment:
AGENTFORGE_CORE_URL: http://core:8000 FORJA_CORE_URL: http://core:8000
volumes: volumes:
- ./agents:/app/agents:ro # para que el dashboard lea los escenarios - ./agents:/app/agents:rw # rw para editores gráficos + escenarios
restart: unless-stopped restart: unless-stopped
+11 -11
View File
@@ -1,4 +1,4 @@
# Componentes de AgentForge y su interrelación (bajo nivel) # Componentes de Forja y su interrelación (bajo nivel)
> **Alcance.** Este documento es la **referencia de cableado**: módulos exactos, > **Alcance.** Este documento es la **referencia de cableado**: módulos exactos,
> firmas, el grafo de dependencias de imports, el grafo de inyección de > firmas, el grafo de dependencias de imports, el grafo de inyección de
@@ -9,7 +9,7 @@
> - ¿Las decisiones técnicas resumidas? → [`ARCHITECTURE.md`](../ARCHITECTURE.md). > - ¿Las decisiones técnicas resumidas? → [`ARCHITECTURE.md`](../ARCHITECTURE.md).
> - ¿Cómo arrancarlo? → [`README.md`](../README.md). > - ¿Cómo arrancarlo? → [`README.md`](../README.md).
> >
> Rutas relativas a `core/src/agentforge_core/` salvo que se diga otra cosa. > Rutas relativas a `core/src/ forja_core/` salvo que se diga otra cosa.
> Refleja el estado del repo en `v0.1.0`. > Refleja el estado del repo en `v0.1.0`.
--- ---
@@ -67,7 +67,7 @@ NIVEL 6
main ⇐ api (routers), api.middlewares, config, observability.logging → create_app(), app main ⇐ api (routers), api.middlewares, config, observability.logging → create_app(), app
(separado, sin imports del paquete core) (separado, sin imports del paquete core)
dashboard/src/agentforge_dashboard/* ← habla con `main` por HTTP, no por import dashboard/src/ forja_dashboard/* ← habla con `main` por HTTP, no por import
``` ```
Reglas que se cumplen y conviene mantener: Reglas que se cumplen y conviene mantener:
@@ -333,11 +333,11 @@ Hashing/versionado: `registry/versioning.compute_hash(yaml_text)` = SHA-256 del
--- ---
## 6. Dashboard ↔ Core (`agentforge_dashboard`) ## 6. Dashboard ↔ Core (` forja_dashboard`)
El dashboard no comparte código con el core: solo lo llama por HTTP a través de El dashboard no comparte código con el core: solo lo llama por HTTP a través de
`CoreClient` (`dashboard/src/agentforge_dashboard/client.py`, httpx síncrono con 2 `CoreClient` (`dashboard/src/ forja_dashboard/client.py`, httpx síncrono con 2
retries, `base_url = AGENTFORGE_CORE_URL`; mapea 404/409/422 → `{"error": <json>}`). retries, `base_url = FORJA_CORE_URL`; mapea 404/409/422 → `{"error": <json>}`).
| `CoreClient.<método>` | Endpoint del core | Página(s) que lo usan | | `CoreClient.<método>` | Endpoint del core | Página(s) que lo usan |
|-----------------------|-------------------|-----------------------| |-----------------------|-------------------|-----------------------|
@@ -365,11 +365,11 @@ Componentes reutilizables: `components/diff_view.render_unified_diff(diff_text)`
## 7. Arranque y ciclo de vida ## 7. Arranque y ciclo de vida
**Proceso core** (`uvicorn agentforge_core.main:app`): **Proceso core** (`uvicorn forja_core.main:app`):
1. Import de `agentforge_core.main` ⇒ se ejecuta `app = create_app()`: 1. Import de ` forja_core.main` ⇒ se ejecuta `app = create_app()`:
`Settings()``configure_logging(level=settings.log_level)``FastAPI(...)` `Settings()``configure_logging(level=settings.log_level)``FastAPI(...)`
`app.add_middleware(TraceIdMiddleware)` → registra `GET /health` `app.add_middleware(TraceIdMiddleware)` → registra `GET /health`
`from agentforge_core.api import agents, executions, policies, violations` `from forja_core.api import agents, executions, policies, violations`
`include_router` ×5. `include_router` ×5.
2. Las dependencias (`deps.get_registry`, `get_policy_store`, `get_llm_provider`, 2. Las dependencias (`deps.get_registry`, `get_policy_store`, `get_llm_provider`,
`get_guardrail_engine`, `get_orchestrator`) **no** se construyen aún; se `get_guardrail_engine`, `get_orchestrator`) **no** se construyen aún; se
@@ -378,12 +378,12 @@ Componentes reutilizables: `components/diff_view.render_unified_diff(diff_text)`
pueden disparar la construcción perezosa) → handler → respuesta con `X-Trace-Id`. pueden disparar la construcción perezosa) → handler → respuesta con `X-Trace-Id`.
**Contenedores** (`docker-compose.yml`): servicio `core` (`core/Dockerfile`, **Contenedores** (`docker-compose.yml`): servicio `core` (`core/Dockerfile`,
`uvicorn agentforge_core.main:app --host 0.0.0.0 --port 8000`, `HEALTHCHECK` `uvicorn forja_core.main:app --host 0.0.0.0 --port 8000`, `HEALTHCHECK`
`curl /health`, monta `./agents:ro`, `./policies:ro`, `./data:rw`, env `curl /health`, monta `./agents:ro`, `./policies:ro`, `./data:rw`, env
`DATA_DIR=/app/data`, `AGENTS_DIR=/app/agents`, `POLICIES_DIR=/app/policies`, `DATA_DIR=/app/data`, `AGENTS_DIR=/app/agents`, `POLICIES_DIR=/app/policies`,
`env_file: .env`); servicio `dashboard` (`dashboard/Dockerfile`, `streamlit run `env_file: .env`); servicio `dashboard` (`dashboard/Dockerfile`, `streamlit run
app.py`, `HEALTHCHECK``/_stcore/health`, `depends_on: core: service_healthy`, app.py`, `HEALTHCHECK``/_stcore/health`, `depends_on: core: service_healthy`,
env `AGENTFORGE_CORE_URL=http://core:8000`, monta `./agents:ro` para leer los env `FORJA_CORE_URL=http://core:8000`, monta `./agents:ro` para leer los
`examples/*.txt`). `examples/*.txt`).
**`Settings` (env vars)** — `config.py`: **`Settings` (env vars)** — `config.py`:
+13 -13
View File
@@ -1,4 +1,4 @@
# AgentForge explicado de principio a fin # Forja explicado de principio a fin
> **Para quién es esto.** Una guía didáctica para alguien que llega nuevo al > **Para quién es esto.** Una guía didáctica para alguien que llega nuevo al
> proyecto —técnico o no— y quiere entender *qué hace*, *por qué está hecho así* > proyecto —técnico o no— y quiere entender *qué hace*, *por qué está hecho así*
@@ -18,7 +18,7 @@
> opacos: prompts que cambian sin historial, validaciones inconsistentes, acciones > opacos: prompts que cambian sin historial, validaciones inconsistentes, acciones
> de alto impacto sin supervisión y ninguna auditoría de lo que decidió el agente. > de alto impacto sin supervisión y ninguna auditoría de lo que decidió el agente.
**AgentForge es el "plano de control" que pones *delante* de tus agentes** antes de **Forja es el "plano de control" que pones *delante* de tus agentes** antes de
dejarlos tocar nada importante. No es un framework para *construir* agentes; es la dejarlos tocar nada importante. No es un framework para *construir* agentes; es la
capa que los **cataloga, versiona, valida, ejecuta de forma supervisada y audita**. capa que los **cataloga, versiona, valida, ejecuta de forma supervisada y audita**.
@@ -47,13 +47,13 @@ Si entiendes estas seis ideas, entiendes el proyecto. Todo lo demás son detalle
## 3. Vista de pájaro: dos servicios ## 3. Vista de pájaro: dos servicios
AgentForge son **dos procesos** que se hablan por HTTP/JSON: Forja son **dos procesos** que se hablan por HTTP/JSON:
``` ```
┌───────────────────────────── docker-compose ──────────────────────────────┐ ┌───────────────────────────── docker-compose ──────────────────────────────┐
│ │ │ │
│ ┌──────────────────────┐ HTTP/JSON ┌────────────────────────┐ │ │ ┌──────────────────────┐ HTTP/JSON ┌────────────────────────┐ │
│ │ agentforge-dashboard │ ───────────────► │ agentforge-core │ │ │ │ forja-dashboard │ ───────────────► │ forja-core │ │
│ │ Streamlit :8501 │ ◄─────────────── │ FastAPI :8000 │ │ │ │ Streamlit :8501 │ ◄─────────────── │ FastAPI :8000 │ │
│ │ (la "consola") │ │ (el cerebro) │ │ │ │ (la "consola") │ │ (el cerebro) │ │
│ └──────────────────────┘ └───────────┬────────────┘ │ │ └──────────────────────┘ └───────────┬────────────┘ │
@@ -69,9 +69,9 @@ AgentForge son **dos procesos** que se hablan por HTTP/JSON:
└────────────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────────────┘
``` ```
- **`agentforge-core`** (FastAPI, puerto 8000) — todo el dominio: registry de - **`forja-core`** (FastAPI, puerto 8000) — todo el dominio: registry de
agentes, motor de guardrails, runtime de ejecución, persistencia. No tiene UI. agentes, motor de guardrails, runtime de ejecución, persistencia. No tiene UI.
- **`agentforge-dashboard`** (Streamlit, puerto 8501) — una consola visual. **No - **`forja-dashboard`** (Streamlit, puerto 8501) — una consola visual. **No
contiene lógica de negocio**: es un cliente HTTP del core con cinco páginas. contiene lógica de negocio**: es un cliente HTTP del core con cinco páginas.
La separación importa: el core podría servir a una CLI, a otro servicio, a un La separación importa: el core podría servir a una CLI, a otro servicio, a un
@@ -97,7 +97,7 @@ pipeline... el dashboard es solo una de las caras posibles.
## 4. Recorrido por los módulos (y quién depende de quién) ## 4. Recorrido por los módulos (y quién depende de quién)
El código del core vive bajo `core/src/agentforge_core/`. Lo agrupo por capas, de El código del core vive bajo `core/src/ forja_core/`. Lo agrupo por capas, de
las más internas (sin dependencias) a las más externas. las más internas (sin dependencias) a las más externas.
### 4.1 `domain/` — el vocabulario del sistema ### 4.1 `domain/` — el vocabulario del sistema
@@ -278,7 +278,7 @@ inyectan). Dependen de él: la API (`api/executions.py` solo conoce el
| `executions.py` | El más cargado: `POST /agents/{n}/invoke`, `GET /executions`, `GET /executions/{trace_id}`, `POST /executions/{trace_id}/approve`, `POST /executions/{trace_id}/reject`. Mantiene además `execution_index.json` (mapa `trace_id → agente/versión`) para poder reanudar tras un reinicio. | | `executions.py` | El más cargado: `POST /agents/{n}/invoke`, `GET /executions`, `GET /executions/{trace_id}`, `POST /executions/{trace_id}/approve`, `POST /executions/{trace_id}/reject`. Mantiene además `execution_index.json` (mapa `trace_id → agente/versión`) para poder reanudar tras un reinicio. |
| `policies.py`, `violations.py` | Routers `/policies` y `/violations` (este con filtros por severidad, stage, etc.). | | `policies.py`, `violations.py` | Routers `/policies` y `/violations` (este con filtros por severidad, stage, etc.). |
Y la **raíz de la app**: `core/src/agentforge_core/main.py``create_app()` instancia Y la **raíz de la app**: `core/src/ forja_core/main.py``create_app()` instancia
`FastAPI`, añade el `TraceIdMiddleware`, monta los routers, y expone `/health`. `FastAPI`, añade el `TraceIdMiddleware`, monta los routers, y expone `/health`.
Depende de: todo lo de arriba (vía `deps.py`). Dependen de él: el dashboard (por Depende de: todo lo de arriba (vía `deps.py`). Dependen de él: el dashboard (por
@@ -297,7 +297,7 @@ HTTP) y los tests de integración (`tests/integration/`, vía `TestClient`).
| `pages/5_📐_Politicas.py` | Inventario de políticas: validadores de entrada/salida (cada uno expandible con su config) y versiones. | | `pages/5_📐_Politicas.py` | Inventario de políticas: validadores de entrada/salida (cada uno expandible con su config) y versiones. |
| `components/` | Trozos reutilizables de UI: `diff_view` (pinta `+`/`-`/`@@`), `trace_view` (el timeline del `decision_path`), `violation_view` (badges de severidad). | | `components/` | Trozos reutilizables de UI: `diff_view` (pinta `+`/`-`/`@@`), `trace_view` (el timeline del `decision_path`), `violation_view` (badges de severidad). |
Depende de: el `agentforge-core` por HTTP (vía `AGENTFORGE_CORE_URL`). **Nadie del Depende de: el `forja-core` por HTTP (vía `FORJA_CORE_URL`). **Nadie del
core depende del dashboard.** No tiene tests unitarios (mal coste/beneficio para core depende del dashboard.** No tiene tests unitarios (mal coste/beneficio para
Streamlit); su verificación es la checklist manual de [`docs/manual_qa.md`](manual_qa.md). Streamlit); su verificación es la checklist manual de [`docs/manual_qa.md`](manual_qa.md).
@@ -464,7 +464,7 @@ sequenceDiagram
## 7. Persistencia: cuatro formas, cuatro razones ## 7. Persistencia: cuatro formas, cuatro razones
AgentForge no usa una sola base de datos; usa la herramienta adecuada para cada cosa. Forja no usa una sola base de datos; usa la herramienta adecuada para cada cosa.
| Qué | Cómo | Por qué así | | Qué | Cómo | Por qué así |
|-----|------|-------------| |-----|------|-------------|
@@ -499,9 +499,9 @@ AgentForge no usa una sola base de datos; usa la herramienta adecuada para cada
## 9. Mapa del repositorio y por dónde empezar a leer ## 9. Mapa del repositorio y por dónde empezar a leer
``` ```
agentforge/ forja/
├── core/ ├── core/
│ ├── src/agentforge_core/ │ ├── src/ forja_core/
│ │ ├── domain/ ← los modelos de datos (empieza por execution.py) │ │ ├── domain/ ← los modelos de datos (empieza por execution.py)
│ │ ├── config.py · observability/ ← cimientos transversales │ │ ├── config.py · observability/ ← cimientos transversales
│ │ ├── llm/ ← proveedores LLM (Protocol + impls + factory) │ │ ├── llm/ ← proveedores LLM (Protocol + impls + factory)
@@ -512,7 +512,7 @@ agentforge/
│ │ └── main.py ← create_app(): ensambla la app │ │ └── main.py ← create_app(): ensambla la app
│ ├── Dockerfile · requirements.txt │ ├── Dockerfile · requirements.txt
├── dashboard/ ├── dashboard/
│ ├── src/agentforge_dashboard/ ← Streamlit (client + app + pages + components) │ ├── src/ forja_dashboard/ ← Streamlit (client + app + pages + components)
│ ├── Dockerfile · requirements.txt │ ├── Dockerfile · requirements.txt
├── agents/incident_analyzer/ ← el agente de ejemplo (YAMLs + escenarios .txt) ├── agents/incident_analyzer/ ← el agente de ejemplo (YAMLs + escenarios .txt)
├── policies/default/ ← la política de guardrails de ejemplo ├── policies/default/ ← la política de guardrails de ejemplo
+419
View File
@@ -0,0 +1,419 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forja — Progreso y Próximos Pasos</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&amp;family=Space+Grotesk:wght@500;600&amp;display=swap');
:root {
--primary: #0ea5e9;
}
body {
font-family: 'Inter', system_ui, sans-serif;
}
.font-display {
font-family: 'Space Grotesk', 'Inter', sans-serif;
font-weight: 600;
}
.section-header {
font-family: 'Space Grotesk', 'Inter', sans-serif;
letter-spacing: -0.025em;
}
.status-badge {
font-size: 0.75rem;
padding: 0.125rem 0.625rem;
border-radius: 9999px;
font-weight: 600;
}
.forja-gradient {
background: linear-gradient(135deg, #0ea5e9, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.card {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.05), 0 8px 10px -6px rgb(0 0 0 / 0.05);
}
.metric {
font-variant-numeric: tabular-nums;
}
.nav-active {
border-bottom: 3px solid #0ea5e9;
font-weight: 600;
}
.progress-bar {
transition: width 1s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.feature-icon {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
}
</style>
</head>
<body class="bg-zinc-950 text-zinc-200">
<!-- Header -->
<div class="border-b border-zinc-800 bg-zinc-900/70 backdrop-blur-lg sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 py-5 flex items-center justify-between">
<div class="flex items-center gap-x-3">
<div class="w-10 h-10 rounded-2xl bg-sky-500 flex items-center justify-center shadow-inner">
<i class="fa-solid fa-hammer text-white text-3xl"></i>
</div>
<div>
<span class="font-display text-3xl font-semibold tracking-tighter">Forja</span>
</div>
</div>
<div class="flex items-center gap-x-2 text-sm">
<div class="px-3 py-1.5 bg-zinc-900 border border-zinc-800 rounded-2xl flex items-center gap-x-2">
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
<span class="font-medium text-emerald-400 text-xs tracking-wider">v0.2 — EN PRODUCCIÓN</span>
</div>
<div class="text-zinc-500 text-xs px-2">23 mayo 2026</div>
</div>
</div>
</div>
<div class="max-w-6xl mx-auto px-6 pt-10 pb-24">
<!-- Hero -->
<div class="flex flex-col lg:flex-row gap-8 items-start">
<div class="flex-1">
<div class="inline-flex items-center gap-x-2 px-3 py-1 rounded-3xl bg-zinc-900 border border-zinc-800 text-xs mb-4">
<i class="fa-solid fa-sync fa-spin-pulse text-sky-400"></i>
<span class="font-semibold tracking-widest">GRAN REFACTORIZACIÓN COMPLETADA</span>
</div>
<h1 class="font-display text-6xl lg:text-7xl font-semibold tracking-tighter leading-none">
Forja<br>
<span class="forja-gradient">está lista</span>
</h1>
<p class="mt-4 max-w-lg text-xl text-zinc-400">
Plataforma de gobernanza genérica para agentes IA de cualquier tipo.
Renombrada, generalizada y con editores gráficos completos.
</p>
<div class="flex items-center gap-x-3 mt-8">
<a href="https://github.com/anomalyco/opencode"
class="inline-flex items-center gap-x-2 px-5 py-3 rounded-3xl bg-white text-zinc-950 font-semibold text-sm hover:bg-zinc-100 transition-colors">
<i class="fa-brands fa-github"></i>
<span>Ver en GitHub</span>
</a>
<button onclick="document.getElementById('proximos-pasos').scrollIntoView({behavior:'smooth'})"
class="inline-flex items-center gap-x-2 px-5 py-3 rounded-3xl border border-zinc-700 hover:bg-zinc-900 text-sm font-medium transition-colors">
<span>Ver próximos pasos</span>
<i class="fa-solid fa-arrow-down"></i>
</button>
</div>
</div>
<div class="lg:w-80 w-full">
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-5">
<div class="text-xs uppercase tracking-[1px] text-zinc-500 mb-3 font-medium">Progreso general</div>
<div class="flex items-baseline gap-x-2">
<div class="text-6xl font-semibold tabular-nums tracking-tighter">78</div>
<div class="text-2xl font-medium text-zinc-400">/100</div>
</div>
<div class="h-2.5 bg-zinc-800 rounded-full mt-3 overflow-hidden">
<div class="h-2.5 bg-gradient-to-r from-sky-400 to-blue-500 rounded-full progress-bar" style="width: 78%"></div>
</div>
<div class="grid grid-cols-3 gap-4 mt-6 text-center">
<div>
<div class="text-xs text-zinc-500">Core</div>
<div class="font-semibold text-xl">100%</div>
</div>
<div>
<div class="text-xs text-zinc-500">UI Gráfica</div>
<div class="font-semibold text-xl">95%</div>
</div>
<div>
<div class="text-xs text-zinc-500">Docs</div>
<div class="font-semibold text-xl">65%</div>
</div>
</div>
</div>
</div>
</div>
<!-- Métricas rápidas -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mt-12">
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
<div class="flex items-center gap-x-3">
<div class="feature-icon bg-emerald-500/10 text-emerald-400"><i class="fa-solid fa-check-double"></i></div>
<div>
<div class="text-xs text-zinc-400">Renombrado global</div>
<div class="font-semibold">483 → 0 referencias antiguas</div>
</div>
</div>
</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
<div class="flex items-center gap-x-3">
<div class="feature-icon bg-amber-500/10 text-amber-400"><i class="fa-solid fa-edit"></i></div>
<div>
<div class="text-xs text-zinc-400">Editores gráficos</div>
<div class="font-semibold">2 páginas nuevas funcionales</div>
</div>
</div>
</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
<div class="flex items-center gap-x-3">
<div class="feature-icon bg-violet-500/10 text-violet-400"><i class="fa-solid fa-globe"></i></div>
<div>
<div class="text-xs text-zinc-400">Generalización</div>
<div class="font-semibold">Cualquier tipo de agente</div>
</div>
</div>
</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
<div class="flex items-center gap-x-3">
<div class="feature-icon bg-sky-500/10 text-sky-400"><i class="fa-solid fa-database"></i></div>
<div>
<div class="text-xs text-zinc-400">Write APIs</div>
<div class="font-semibold">POST /versions + upsert</div>
</div>
</div>
</div>
</div>
<!-- Qué se hizo -->
<div class="mt-16">
<h2 class="section-header text-3xl font-semibold tracking-tight mb-6 flex items-center gap-x-3">
<span>Lo que se completó</span>
<span class="text-xs px-3 py-1 bg-emerald-400/10 text-emerald-400 rounded-2xl font-mono tracking-wider">FASE 1 + 2 + 3</span>
</h2>
<div class="grid md:grid-cols-2 gap-4">
<!-- Columna 1 -->
<div class="space-y-4">
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
<div class="font-semibold flex items-center gap-x-2 text-lg">
<i class="fa-solid fa-sync text-sky-400"></i>
<span>Renombrado completo a "Forja"</span>
</div>
<ul class="mt-4 text-sm space-y-2 text-zinc-300">
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5"></span> Paquetes Python: <span class="mono font-medium">forja_core</span> y <span class="mono font-medium">forja_dashboard</span></li>
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5"></span> Imágenes Docker, contenedores y URLs (<code>forja-core:dev</code>)</li>
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5"></span> Variables de entorno (<code>FORJA_CORE_URL</code>)</li>
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5"></span> pyproject, Dockerfiles, docker-compose, Makefile</li>
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5"></span> 300+ archivos actualizados (código + docs activos)</li>
</ul>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
<div class="font-semibold flex items-center gap-x-2 text-lg">
<i class="fa-solid fa-universal-access text-violet-400"></i>
<span>Generalización para cualquier tipo de agente</span>
</div>
<div class="mt-4 text-sm text-zinc-300">
<div class="flex flex-wrap gap-2">
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">Campos UI opcionales en AgentDefinition</div>
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">input_label / placeholder / category / tags / icon</div>
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">Execute page 100% genérico</div>
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">Posicionamiento como plano de control reutilizable</div>
</div>
</div>
</div>
</div>
<!-- Columna 2 -->
<div class="space-y-4">
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
<div class="font-semibold flex items-center gap-x-2 text-lg">
<i class="fa-solid fa-pencil-ruler text-amber-400"></i>
<span>Editores gráficos completos (la gran novedad)</span>
</div>
<div class="mt-4 grid grid-cols-1 gap-3">
<div class="bg-zinc-950 border border-zinc-800 p-4 rounded-2xl">
<div class="font-medium text-amber-300 flex items-center gap-x-2">
<i class="fa-solid fa-hammer"></i>
<span>6_🔨_Forjar_Agente.py</span>
</div>
<div class="text-xs text-zinc-400 mt-1">Form completo + LLM config + schema JSON + multiselect de guardrails + fork + versionado</div>
</div>
<div class="bg-zinc-950 border border-zinc-800 p-4 rounded-2xl">
<div class="font-medium text-amber-300 flex items-center gap-x-2">
<i class="fa-solid fa-shield-halved"></i>
<span>7_🛡️_Forjar_Politica.py</span>
</div>
<div class="text-xs text-zinc-400 mt-1">Constructor dinámico de validadores (input/output) + /validators endpoint + configs JSON</div>
</div>
</div>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
<div class="font-semibold text-lg">Backend de escritura</div>
<div class="text-sm mt-3 space-y-1.5 text-zinc-300">
<div><code class="mono text-xs">POST /agents/{name}/versions</code> + upsert</div>
<div><code class="mono text-xs">POST /policies/{name}/versions</code> + upsert simétrico</div>
<div><code class="mono text-xs">GET /policies/validators</code> (metadata para UI)</div>
<div><code class="mono text-xs">FileSystemPolicyStore.upsert_version()</code></div>
<div>✓ Docker mounts cambiados a <strong>:rw</strong></div>
</div>
</div>
</div>
</div>
</div>
<!-- Estado actual -->
<div class="mt-16">
<h2 class="section-header text-3xl font-semibold tracking-tight mb-6">Estado actual (23 mayo 2026)</h2>
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-7">
<div class="grid md:grid-cols-3 gap-8">
<div>
<div class="uppercase text-xs font-semibold tracking-wider text-emerald-400">Compila y funciona</div>
<ul class="mt-3 space-y-2 text-sm">
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>Todos los imports y paquetes renombrados</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>py_compile 100% limpio</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>Editores guardan YAMLs reales</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>Execute page usa metadata del agente</span></li>
</ul>
</div>
<div>
<div class="uppercase text-xs font-semibold tracking-wider text-amber-400">Listo para usar</div>
<ul class="mt-3 space-y-2 text-sm">
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span><code>docker compose up</code> (con :rw)</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span>Registro + Ejecución + Aprobaciones</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span>Editores en barra lateral (páginas 6 y 7)</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span>README y docs principales actualizados</span></li>
</ul>
</div>
<div>
<div class="uppercase text-xs font-semibold tracking-wider text-zinc-400">Pendiente de verificación completa</div>
<ul class="mt-3 space-y-2 text-sm text-zinc-400">
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>make test-all (requiere deps pesadas)</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>Smoke test con Docker real (guardrails-ai)</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>Actualización de walkthrough.html</span></li>
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>Tests específicos de los nuevos endpoints</span></li>
</ul>
</div>
</div>
</div>
</div>
<!-- Próximos pasos -->
<div id="proximos-pasos" class="mt-16">
<h2 class="section-header text-3xl font-semibold tracking-tight mb-6">Próximos pasos recomendados</h2>
<div class="space-y-3">
<!-- Paso 1 -->
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">1</div>
<div class="flex-1">
<div class="font-semibold">Verificación completa de calidad</div>
<div class="text-sm text-zinc-400 mt-1">Ejecutar <span class="mono bg-zinc-950 px-1.5 py-px rounded">make install &amp;&amp; make lint &amp;&amp; make test-all</span> + smoke con docker compose en entorno limpio.</div>
</div>
</div>
<!-- Paso 2 -->
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">2</div>
<div class="flex-1">
<div class="font-semibold">Mejora de los editores (UX)</div>
<div class="text-sm text-zinc-400 mt-1">Añadir editor JSON más amigable (ace o monaco), validación en tiempo real de schemas, preview de agente antes de guardar, y botón “Probar inmediatamente”.</div>
</div>
</div>
<!-- Paso 3 -->
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">3</div>
<div class="flex-1">
<div class="font-semibold">Añadir más plantillas de agentes</div>
<div class="text-sm text-zinc-400 mt-1">Crear 2-3 agentes genéricos de ejemplo (resumidor, revisor de código, chatbot con guardrails) + sus políticas base.</div>
</div>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">4</div>
<div class="flex-1">
<div class="font-semibold">Soporte de namespaces / multi-proyecto</div>
<div class="text-sm text-zinc-400 mt-1">Estructura opcional <span class="mono text-xs">agents/&lt;proyecto&gt;/&lt;agente&gt;</span> para que una sola instancia de Forja sirva a múltiples equipos.</div>
</div>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">5</div>
<div class="flex-1">
<div class="font-semibold">Documentación y walkthrough actualizado</div>
<div class="text-sm text-zinc-400 mt-1">Regenerar o actualizar <span class="mono">docs/walkthrough.html</span> y <span class="mono">docs/explicacion.md</span> con los editores y el nuevo nombre.</div>
</div>
</div>
</div>
</div>
<!-- Notas técnicas -->
<div class="mt-16 text-xs text-zinc-500 border-t border-zinc-800 pt-8">
<div class="flex flex-wrap gap-x-8 gap-y-2">
<div><strong>Paquetes:</strong> <span class="mono">forja_core</span> / <span class="mono">forja_dashboard</span></div>
<div><strong>Python:</strong> 3.11+</div>
<div><strong>UI:</strong> Streamlit 1.38 + Tailwind (editores)</div>
<div><strong>Runtime:</strong> LangGraph + FastAPI</div>
<div><strong>Persistencia:</strong> YAML versionado + SQLite checkpoints</div>
</div>
<div class="mt-3 text-[10px] text-zinc-600">Este documento fue generado automáticamente tras la sesión de refactorización del 23 de mayo de 2026.</div>
</div>
</div>
<script>
// Tailwind script
function initializeTailwind() {
document.documentElement.style.setProperty('--accent', '#0ea5e9');
}
// Simple confetti on load for celebration
function celebrate() {
if (Math.random() > 0.7) {
console.log('%c[Forja] ¡Refactorización completada con éxito!', 'color:#64748b;font-size:9px');
}
}
window.onload = function() {
initializeTailwind();
celebrate();
};
// Keyboard hint
document.addEventListener('keydown', function(e) {
if (e.key === '/' && document.activeElement.tagName === 'BODY') {
e.preventDefault();
const next = document.getElementById('proximos-pasos');
if (next) next.scrollIntoView({behavior: 'smooth'});
}
});
</script>
</body>
</html>
+21 -21
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Walkthrough completo de AgentForge — de alto a bajo nivel, con diagramas."> <meta name="description" content="Walkthrough completo de Forja — de alto a bajo nivel, con diagramas.">
<title>AgentForge · Walkthrough</title> <title>Forja · Walkthrough</title>
<script> <script>
(function () { (function () {
try { try {
@@ -255,7 +255,7 @@ details.dd .body{padding:4px 16px 14px}
<div class="topbar"> <div class="topbar">
<button class="icon-btn" id="menuBtn" aria-label="Abrir índice"></button> <button class="icon-btn" id="menuBtn" aria-label="Abrir índice"></button>
<span class="tt">🛡️ AgentForge · Walkthrough</span> <span class="tt">🛡️ Forja · Walkthrough</span>
<button class="icon-btn" id="themeBtnM" aria-label="Cambiar tema" style="margin-left:auto"></button> <button class="icon-btn" id="themeBtnM" aria-label="Cambiar tema" style="margin-left:auto"></button>
</div> </div>
<div class="scrim" id="scrim"></div> <div class="scrim" id="scrim"></div>
@@ -266,7 +266,7 @@ details.dd .body{padding:4px 16px 14px}
<div class="brand"> <div class="brand">
<span class="logo">🛡️</span> <span class="logo">🛡️</span>
<span> <span>
<span class="t1">AgentForge</span> <span class="t1">Forja</span>
<span class="t2">Walkthrough · de alto a bajo nivel</span> <span class="t2">Walkthrough · de alto a bajo nivel</span>
</span> </span>
</div> </div>
@@ -274,7 +274,7 @@ details.dd .body{padding:4px 16px 14px}
<button class="btn" id="themeBtn" style="flex:1; justify-content:center">◐ Tema</button> <button class="btn" id="themeBtn" style="flex:1; justify-content:center">◐ Tema</button>
<button class="btn" onclick="window.print()" style="flex:1; justify-content:center">⎙ Imprimir</button> <button class="btn" onclick="window.print()" style="flex:1; justify-content:center">⎙ Imprimir</button>
</div> </div>
<div class="meta">HTML autocontenido · <code>docs/walkthrough.html</code> · AgentForge v0.1.0</div> <div class="meta">HTML autocontenido · <code>docs/walkthrough.html</code> · Forja v0.1.0</div>
<nav> <nav>
<ul class="toc" id="toc"> <ul class="toc" id="toc">
<li class="group">Panorama</li> <li class="group">Panorama</li>
@@ -314,7 +314,7 @@ details.dd .body{padding:4px 16px 14px}
<header class="hero"> <header class="hero">
<div class="crumbs">Plataforma de gobernanza de agentes IA · documento generado para entender el proyecto completo</div> <div class="crumbs">Plataforma de gobernanza de agentes IA · documento generado para entender el proyecto completo</div>
<h1>🛡️ AgentForge — Walkthrough</h1> <h1>🛡️ Forja — Walkthrough</h1>
<p class="tagline">Catalogación y versionado de agentes y políticas, guardrails en runtime, ejecución <em>stateful</em> con Human-in-the-Loop, observabilidad y trazabilidad de extremo a extremo. Aquí está todo: <strong>de la vista de pájaro al cableado de cada módulo</strong>, con diagramas.</p> <p class="tagline">Catalogación y versionado de agentes y políticas, guardrails en runtime, ejecución <em>stateful</em> con Human-in-the-Loop, observabilidad y trazabilidad de extremo a extremo. Aquí está todo: <strong>de la vista de pájaro al cableado de cada módulo</strong>, con diagramas.</p>
<div class="badges"> <div class="badges">
<span class="badge k">🐍 <b>Python 3.11+</b></span> <span class="badge k">🐍 <b>Python 3.11+</b></span>
@@ -338,7 +338,7 @@ details.dd .body{padding:4px 16px 14px}
<section id="intro"> <section id="intro">
<h2><span class="kicker">00</span> Qué es y por qué</h2> <h2><span class="kicker">00</span> Qué es y por qué</h2>
<p class="lead">Poner agentes de IA en producción <strong>sin una capa de gobierno</strong> produce sistemas opacos: prompts que cambian sin historial, validaciones inconsistentes, acciones de alto impacto sin supervisión y ninguna auditoría de lo que decidió el agente.</p> <p class="lead">Poner agentes de IA en producción <strong>sin una capa de gobierno</strong> produce sistemas opacos: prompts que cambian sin historial, validaciones inconsistentes, acciones de alto impacto sin supervisión y ninguna auditoría de lo que decidió el agente.</p>
<p><strong>AgentForge es el "plano de control" que pones <em>delante</em> de tus agentes</strong> antes de dejarlos tocar nada importante. No es un framework para <em>construir</em> agentes; es la capa que los <strong>cataloga, versiona, valida, ejecuta de forma supervisada y audita</strong>.</p> <p><strong>Forja es el "plano de control" que pones <em>delante</em> de tus agentes</strong> antes de dejarlos tocar nada importante. No es un framework para <em>construir</em> agentes; es la capa que los <strong>cataloga, versiona, valida, ejecuta de forma supervisada y audita</strong>.</p>
<div class="callout key"> <div class="callout key">
<div class="ct">🧭 El caso de ejemplo del repo</div> <div class="ct">🧭 El caso de ejemplo del repo</div>
<p>Un agente de operaciones de telco — <code>incident_analyzer</code>: recibe la descripción de un incidente de plataforma de voz (caída de registros SIP, degradación de MOS, saturación de HSS…) y propone acciones con <strong>análisis de riesgo</strong> y <strong>plan de rollback</strong>. Las acciones de riesgo alto quedan <strong>pausadas esperando aprobación humana</strong>. Todo queda registrado con un <code>trace_id</code>.</p> <p>Un agente de operaciones de telco — <code>incident_analyzer</code>: recibe la descripción de un incidente de plataforma de voz (caída de registros SIP, degradación de MOS, saturación de HSS…) y propone acciones con <strong>análisis de riesgo</strong> y <strong>plan de rollback</strong>. Las acciones de riesgo alto quedan <strong>pausadas esperando aprobación humana</strong>. Todo queda registrado con un <code>trace_id</code>.</p>
@@ -393,10 +393,10 @@ details.dd .body{padding:4px 16px 14px}
<!-- ============================================================= --> <!-- ============================================================= -->
<section id="vista"> <section id="vista">
<h2><span class="kicker">02</span> Vista de pájaro: dos servicios</h2> <h2><span class="kicker">02</span> Vista de pájaro: dos servicios</h2>
<p>AgentForge son <strong>dos procesos</strong> que se hablan por HTTP/JSON, levantados por <code>docker-compose</code>:</p> <p>Forja son <strong>dos procesos</strong> que se hablan por HTTP/JSON, levantados por <code>docker-compose</code>:</p>
<ul> <ul>
<li><strong><code>agentforge-core</code></strong> (FastAPI, puerto <strong>8000</strong>) — todo el dominio: registry de agentes, motor de guardrails, runtime de ejecución, persistencia. <em>No tiene UI.</em></li> <li><strong><code>forja-core</code></strong> (FastAPI, puerto <strong>8000</strong>) — todo el dominio: registry de agentes, motor de guardrails, runtime de ejecución, persistencia. <em>No tiene UI.</em></li>
<li><strong><code>agentforge-dashboard</code></strong> (Streamlit, puerto <strong>8501</strong>) — una consola visual con cinco páginas. <strong>No contiene lógica de negocio</strong>: es un cliente HTTP del core.</li> <li><strong><code>forja-dashboard</code></strong> (Streamlit, puerto <strong>8501</strong>) — una consola visual con cinco páginas. <strong>No contiene lógica de negocio</strong>: es un cliente HTTP del core.</li>
</ul> </ul>
<p>La separación importa: el core podría servir a una CLI, a otro servicio, a un pipeline… el dashboard es solo una de las caras posibles. (Y este documento es otra cara más: el HTML que estás leyendo.)</p> <p>La separación importa: el core podría servir a una CLI, a otro servicio, a un pipeline… el dashboard es solo una de las caras posibles. (Y este documento es otra cara más: el HTML que estás leyendo.)</p>
@@ -416,14 +416,14 @@ details.dd .body{padding:4px 16px 14px}
<!-- dashboard --> <!-- dashboard -->
<rect class="dg-box accent" x="56" y="60" width="300" height="106" rx="13"/> <rect class="dg-box accent" x="56" y="60" width="300" height="106" rx="13"/>
<text class="dg-t" x="206" y="92" text-anchor="middle">agentforge-dashboard</text> <text class="dg-t" x="206" y="92" text-anchor="middle">forja-dashboard</text>
<text class="dg-s" x="206" y="112" text-anchor="middle">Streamlit · :8501 · "la consola"</text> <text class="dg-s" x="206" y="112" text-anchor="middle">Streamlit · :8501 · "la consola"</text>
<text class="dg-m dim" x="206" y="132" text-anchor="middle">5 páginas · sin lógica de negocio</text> <text class="dg-m dim" x="206" y="132" text-anchor="middle">5 páginas · sin lógica de negocio</text>
<text class="dg-m dim" x="206" y="150" text-anchor="middle">CoreClient (httpx) → habla solo HTTP</text> <text class="dg-m dim" x="206" y="150" text-anchor="middle">CoreClient (httpx) → habla solo HTTP</text>
<!-- core --> <!-- core -->
<rect class="dg-box violet" x="600" y="60" width="344" height="106" rx="13"/> <rect class="dg-box violet" x="600" y="60" width="344" height="106" rx="13"/>
<text class="dg-t" x="772" y="92" text-anchor="middle">agentforge-core</text> <text class="dg-t" x="772" y="92" text-anchor="middle">forja-core</text>
<text class="dg-s" x="772" y="112" text-anchor="middle">FastAPI · :8000 · "el cerebro"</text> <text class="dg-s" x="772" y="112" text-anchor="middle">FastAPI · :8000 · "el cerebro"</text>
<text class="dg-m dim" x="772" y="132" text-anchor="middle">dominio · runtime · guardrails · persistencia</text> <text class="dg-m dim" x="772" y="132" text-anchor="middle">dominio · runtime · guardrails · persistencia</text>
<text class="dg-m dim" x="772" y="150" text-anchor="middle">/health · /agents · /executions · /policies · /violations</text> <text class="dg-m dim" x="772" y="150" text-anchor="middle">/health · /agents · /executions · /policies · /violations</text>
@@ -513,7 +513,7 @@ details.dd .body{padding:4px 16px 14px}
<!-- ============================================================= --> <!-- ============================================================= -->
<section id="modulos"> <section id="modulos">
<h2><span class="kicker">04</span> El grafo de módulos (quién depende de quién)</h2> <h2><span class="kicker">04</span> El grafo de módulos (quién depende de quién)</h2>
<p>El código del core vive bajo <code>core/src/agentforge_core/</code>. Es un <strong>DAG</strong>: las capas de abajo no importan nada de las de arriba. El "nivel" es la profundidad topológica. Lee de abajo hacia arriba: el vocabulario primero, la composición de la app al final.</p> <p>El código del core vive bajo <code>core/src/forja_core/</code>. Es un <strong>DAG</strong>: las capas de abajo no importan nada de las de arriba. El "nivel" es la profundidad topológica. Lee de abajo hacia arriba: el vocabulario primero, la composición de la app al final.</p>
<div class="bands"> <div class="bands">
<div class="band"> <div class="band">
<div class="lvl"><b>6</b><span>app</span></div> <div class="lvl"><b>6</b><span>app</span></div>
@@ -980,7 +980,7 @@ details.dd .body{padding:4px 16px 14px}
<p>El estado que fluye por el grafo es un <code>TypedDict</code>. <code>decision_path</code> usa un <em>reducer</em> (<code>Annotated[list, operator.add]</code>) para que cada nodo <strong>añada</strong> pasos en vez de sobrescribir:</p> <p>El estado que fluye por el grafo es un <code>TypedDict</code>. <code>decision_path</code> usa un <em>reducer</em> (<code>Annotated[list, operator.add]</code>) para que cada nodo <strong>añada</strong> pasos en vez de sobrescribir:</p>
<div class="code"> <div class="code">
<div class="hd"><span class="dot"></span><span class="fn">core/src/agentforge_core/runtime/state.py</span><span class="lang">python</span></div> <div class="hd"><span class="dot"></span><span class="fn">core/src/forja_core/runtime/state.py</span><span class="lang">python</span></div>
<pre><span class="k">class</span> <span class="y">AgentState</span>(TypedDict, total=<span class="k">False</span>): <pre><span class="k">class</span> <span class="y">AgentState</span>(TypedDict, total=<span class="k">False</span>):
trace_id: <span class="y">str</span>; agent_name: <span class="y">str</span>; agent_version: <span class="y">str</span>; user_input: <span class="y">str</span> trace_id: <span class="y">str</span>; agent_name: <span class="y">str</span>; agent_version: <span class="y">str</span>; user_input: <span class="y">str</span>
messages: <span class="y">list</span>[<span class="y">dict</span>]; raw_llm_output: <span class="y">str</span> | <span class="k">None</span>; parsed_output: <span class="y">dict</span> | <span class="k">None</span> messages: <span class="y">list</span>[<span class="y">dict</span>]; raw_llm_output: <span class="y">str</span> | <span class="k">None</span>; parsed_output: <span class="y">dict</span> | <span class="k">None</span>
@@ -1097,7 +1097,7 @@ g.add_edge(<span class="s">"finalize"</span>, END)
<!-- divider --> <!-- divider -->
<rect class="dg-band" x="14" y="460" width="1092" height="34" rx="6"/> <rect class="dg-band" x="14" y="460" width="1092" height="34" rx="6"/>
<line class="dg-divider" x1="14" y1="460" x2="1106" y2="460"/><line class="dg-divider" x1="14" y1="494" x2="1106" y2="494"/> <line class="dg-divider" x1="14" y1="460" x2="1106" y2="460"/><line class="dg-divider" x1="14" y1="494" x2="1106" y2="494"/>
<text class="dg-s" x="560" y="481" text-anchor="middle" style="font-style:italic">· · · más tarde — incluso tras reiniciar agentforge-core: el estado pausado sigue en checkpoints.sqlite · · ·</text> <text class="dg-s" x="560" y="481" text-anchor="middle" style="font-style:italic">· · · más tarde — incluso tras reiniciar forja-core: el estado pausado sigue en checkpoints.sqlite · · ·</text>
<!-- ACT 2 label --> <!-- ACT 2 label -->
<rect class="dg-box ok" x="18" y="504" width="232" height="22" rx="6"/><text class="dg-t sm" x="28" y="520" style="font-size:11.5px;fill:var(--ok)">ACTO 2 · el humano decide → completed</text> <rect class="dg-box ok" x="18" y="504" width="232" height="22" rx="6"/><text class="dg-t sm" x="28" y="520" style="font-size:11.5px;fill:var(--ok)">ACTO 2 · el humano decide → completed</text>
@@ -1222,7 +1222,7 @@ curl -s localhost:8000/executions/$TRACE/approve \
<!-- ============================================================= --> <!-- ============================================================= -->
<section id="persistencia"> <section id="persistencia">
<h2><span class="kicker">11</span> Persistencia: cuatro formas, cuatro razones</h2> <h2><span class="kicker">11</span> Persistencia: cuatro formas, cuatro razones</h2>
<p>AgentForge no usa una sola base de datos; usa la herramienta adecuada para cada cosa.</p> <p>Forja no usa una sola base de datos; usa la herramienta adecuada para cada cosa.</p>
<figure class="diagram"> <figure class="diagram">
<svg viewBox="0 0 1020 420" role="img" aria-label="Mapa de persistencia: quién escribe y lee qué"> <svg viewBox="0 0 1020 420" role="img" aria-label="Mapa de persistencia: quién escribe y lee qué">
@@ -1230,7 +1230,7 @@ curl -s localhost:8000/executions/$TRACE/approve \
<!-- core in the middle --> <!-- core in the middle -->
<rect class="dg-box violet" x="396" y="170" width="228" height="80" rx="12"/> <rect class="dg-box violet" x="396" y="170" width="228" height="80" rx="12"/>
<text class="dg-t sm" x="510" y="196" text-anchor="middle">agentforge-core</text> <text class="dg-t sm" x="510" y="196" text-anchor="middle">forja-core</text>
<text class="dg-s" x="510" y="214" text-anchor="middle">registry · orchestrator</text> <text class="dg-s" x="510" y="214" text-anchor="middle">registry · orchestrator</text>
<text class="dg-s" x="510" y="230" text-anchor="middle">api/persistence · runtime/checkpointer</text> <text class="dg-s" x="510" y="230" text-anchor="middle">api/persistence · runtime/checkpointer</text>
@@ -1489,11 +1489,11 @@ curl -s localhost:8000/executions/$TRACE/approve \
<div> <div>
<h4>El ciclo de vida del proceso core</h4> <h4>El ciclo de vida del proceso core</h4>
<ol> <ol>
<li>Importar <code>agentforge_core.main</code> ejecuta <code>app = create_app()</code>: <code>Settings()</code><code>configure_logging(level)</code><code>FastAPI(...)</code><code>add_middleware(TraceIdMiddleware)</code> → registra <code>GET /health</code> → importa los routers → <code>include_router</code> (agents, executions ×2, policies, violations).</li> <li>Importar <code>forja_core.main</code> ejecuta <code>app = create_app()</code>: <code>Settings()</code><code>configure_logging(level)</code><code>FastAPI(...)</code><code>add_middleware(TraceIdMiddleware)</code> → registra <code>GET /health</code> → importa los routers → <code>include_router</code> (agents, executions ×2, policies, violations).</li>
<li>Las dependencias (<code>get_registry</code>, <code>get_policy_store</code>, <code>get_llm_provider</code>, <code>get_guardrail_engine</code>, <code>get_orchestrator</code>) <strong>no</strong> se construyen aún; se construyen y cachean en la <strong>primera request</strong> que las inyecta.</li> <li>Las dependencias (<code>get_registry</code>, <code>get_policy_store</code>, <code>get_llm_provider</code>, <code>get_guardrail_engine</code>, <code>get_orchestrator</code>) <strong>no</strong> se construyen aún; se construyen y cachean en la <strong>primera request</strong> que las inyecta.</li>
<li>Cada request: <code>TraceIdMiddleware.dispatch</code> → router → resuelve <code>Depends(...)</code> (que puede disparar la construcción perezosa) → handler → respuesta con <code>X-Trace-Id</code>.</li> <li>Cada request: <code>TraceIdMiddleware.dispatch</code> → router → resuelve <code>Depends(...)</code> (que puede disparar la construcción perezosa) → handler → respuesta con <code>X-Trace-Id</code>.</li>
</ol> </ol>
<p class="muted">En contenedores: <code>uvicorn agentforge_core.main:app --host 0.0.0.0 --port 8000</code>; <code>HEALTHCHECK</code><code>curl /health</code>; monta <code>./agents:ro</code>, <code>./policies:ro</code>, <code>./data:rw</code>; <code>DATA_DIR=/app/data</code>, etc. El dashboard depende de <code>core: service_healthy</code> y usa <code>AGENTFORGE_CORE_URL=http://core:8000</code>. La imagen del core instala <code>en_core_web_sm</code> de spaCy para Presidio.</p> <p class="muted">En contenedores: <code>uvicorn forja_core.main:app --host 0.0.0.0 --port 8000</code>; <code>HEALTHCHECK</code><code>curl /health</code>; monta <code>./agents:ro</code>, <code>./policies:ro</code>, <code>./data:rw</code>; <code>DATA_DIR=/app/data</code>, etc. El dashboard depende de <code>core: service_healthy</code> y usa <code>FORJA_CORE_URL=http://core:8000</code>. La imagen del core instala <code>en_core_web_sm</code> de spaCy para Presidio.</p>
</div> </div>
<div> <div>
<h4>Variables de entorno (<code>config.py</code> · <code>.env</code>)</h4> <h4>Variables de entorno (<code>config.py</code> · <code>.env</code>)</h4>
@@ -1510,7 +1510,7 @@ curl -s localhost:8000/executions/$TRACE/approve \
<tr><td><code>DATA_DIR</code></td><td><code>./data</code></td><td>checkpointer · índice · JSONL</td></tr> <tr><td><code>DATA_DIR</code></td><td><code>./data</code></td><td>checkpointer · índice · JSONL</td></tr>
<tr><td><code>AGENTS_DIR</code></td><td><code>./agents</code></td><td><code>FileSystemAgentRegistry</code></td></tr> <tr><td><code>AGENTS_DIR</code></td><td><code>./agents</code></td><td><code>FileSystemAgentRegistry</code></td></tr>
<tr><td><code>POLICIES_DIR</code></td><td><code>./policies</code></td><td><code>FileSystemPolicyStore</code></td></tr> <tr><td><code>POLICIES_DIR</code></td><td><code>./policies</code></td><td><code>FileSystemPolicyStore</code></td></tr>
<tr><td><code>AGENTFORGE_CORE_URL</code></td><td><code>http://core:8000</code></td><td>el dashboard (<code>CoreClient</code>)</td></tr> <tr><td><code>FORJA_CORE_URL</code></td><td><code>http://core:8000</code></td><td>el dashboard (<code>CoreClient</code>)</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -1721,7 +1721,7 @@ open docs/walkthrough.html <span class="c"># macOS</span>
<div class="footer"> <div class="footer">
<p><strong>AgentForge · Walkthrough.</strong> Documento autocontenido (sin recursos externos). Acompaña a <code>README.md</code>, <code>ARCHITECTURE.md</code>, <code>docs/explicacion.md</code> (narrativa) y <code>docs/componentes.md</code> (referencia de cableado). Refleja el repo en <code>v0.1.0</code>.</p> <p><strong>Forja · Walkthrough.</strong> Documento autocontenido (sin recursos externos). Acompaña a <code>README.md</code>, <code>ARCHITECTURE.md</code>, <code>docs/explicacion.md</code> (narrativa) y <code>docs/componentes.md</code> (referencia de cableado). Refleja el repo en <code>v0.1.0</code>.</p>
<p class="muted">Documento HTML autocontenido (<code>docs/walkthrough.html</code>) — sin recursos externos: ábrelo en cualquier navegador.</p> <p class="muted">Documento HTML autocontenido (<code>docs/walkthrough.html</code>) — sin recursos externos: ábrelo en cualquier navegador.</p>
</div> </div>
</main> </main>
+3 -3
View File
@@ -3,7 +3,7 @@ requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "agentforge" name = "forja"
version = "0.1.0" version = "0.1.0"
description = "Plataforma profesional de gobernanza de agentes IA" description = "Plataforma profesional de gobernanza de agentes IA"
requires-python = ">=3.11" requires-python = ">=3.11"
@@ -20,10 +20,10 @@ ignore = ["E501"]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
# Streamlit exige que las páginas se llamen "<n>_<emoji>_<Label>.py" para la # Streamlit exige que las páginas se llamen "<n>_<emoji>_<Label>.py" para la
# navegación multipágina; eso choca con N999 (nombre de módulo no válido). # navegación multipágina; eso choca con N999 (nombre de módulo no válido).
"dashboard/src/agentforge_dashboard/pages/*" = ["N999"] "dashboard/src/forja_dashboard/pages/*" = ["N999"]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["agentforge_core", "agentforge_dashboard"] known-first-party = ["forja_core", "forja_dashboard"]
[tool.mypy] [tool.mypy]
python_version = "3.11" python_version = "3.11"
+2 -2
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest import pytest
from agentforge_core.api import deps from forja_core.api import deps
@pytest.fixture @pytest.fixture
@@ -12,7 +12,7 @@ def integration_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): # type
"""Cliente FastAPI con env apuntando a los assets reales del repo.""" """Cliente FastAPI con env apuntando a los assets reales del repo."""
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentforge_core.main import create_app from forja_core.main import create_app
repo_root = Path(__file__).parent.parent.parent repo_root = Path(__file__).parent.parent.parent
monkeypatch.setenv("DATA_DIR", str(tmp_path)) monkeypatch.setenv("DATA_DIR", str(tmp_path))
@@ -19,8 +19,8 @@ EXAMPLES = (
def test_resume_tras_recreacion_del_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_resume_tras_recreacion_del_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentforge_core.api import deps from forja_core.api import deps
from agentforge_core.main import create_app from forja_core.main import create_app
repo_root = Path(__file__).parent.parent.parent repo_root = Path(__file__).parent.parent.parent
monkeypatch.setenv("DATA_DIR", str(tmp_path)) monkeypatch.setenv("DATA_DIR", str(tmp_path))
+2 -2
View File
@@ -5,8 +5,8 @@ from pathlib import Path
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentforge_core.api import deps from forja_core.api import deps
from agentforge_core.main import create_app from forja_core.main import create_app
FIXTURES = Path(__file__).parent.parent / "fixtures" FIXTURES = Path(__file__).parent.parent / "fixtures"
+2 -2
View File
@@ -5,8 +5,8 @@ from pathlib import Path
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentforge_core.api import deps from forja_core.api import deps
from agentforge_core.main import create_app from forja_core.main import create_app
FIXTURES = Path(__file__).parent.parent / "fixtures" FIXTURES = Path(__file__).parent.parent / "fixtures"
+4 -4
View File
@@ -7,10 +7,10 @@ from uuid import uuid4
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentforge_core.api import deps from forja_core.api import deps
from agentforge_core.api.persistence import append_violation from forja_core.api.persistence import append_violation
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
from agentforge_core.main import create_app from forja_core.main import create_app
FIXTURES = Path(__file__).parent.parent / "fixtures" FIXTURES = Path(__file__).parent.parent / "fixtures"
+3 -3
View File
@@ -4,7 +4,7 @@ import os
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from agentforge_core.config import Settings from forja_core.config import Settings
def test_settings_defaults_seguros() -> None: def test_settings_defaults_seguros() -> None:
@@ -16,6 +16,6 @@ def test_settings_defaults_seguros() -> None:
def test_settings_data_dir_se_resuelve() -> None: def test_settings_data_dir_se_resuelve() -> None:
with patch.dict(os.environ, {"DATA_DIR": "/tmp/agentforge-test"}, clear=True): with patch.dict(os.environ, {"DATA_DIR": "/tmp/forja-test"}, clear=True):
s = Settings(_env_file=None) # type: ignore[call-arg] s = Settings(_env_file=None) # type: ignore[call-arg]
assert s.data_dir == Path("/tmp/agentforge-test") assert s.data_dir == Path("/tmp/forja-test")
+1 -1
View File
@@ -14,7 +14,7 @@ from collections.abc import Callable
import httpx import httpx
import pytest import pytest
from agentforge_dashboard.client import CoreClient from forja_dashboard.client import CoreClient
def _client_with(handler: Callable[[httpx.Request], httpx.Response]) -> CoreClient: def _client_with(handler: Callable[[httpx.Request], httpx.Response]) -> CoreClient:
+1 -1
View File
@@ -5,7 +5,7 @@ from datetime import UTC, datetime
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
from agentforge_core.domain.agent import ( from forja_core.domain.agent import (
AgentDefinition, AgentDefinition,
AgentVersionMeta, AgentVersionMeta,
LLMConfig, LLMConfig,
+1 -1
View File
@@ -3,7 +3,7 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from uuid import uuid4 from uuid import uuid4
from agentforge_core.domain.execution import ( from forja_core.domain.execution import (
AgentExecution, AgentExecution,
AgentExecutionSummary, AgentExecutionSummary,
DecisionStep, DecisionStep,
+1 -1
View File
@@ -6,7 +6,7 @@ from uuid import uuid4
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
def test_guardrail_violation_minimo_valido() -> None: def test_guardrail_violation_minimo_valido() -> None:
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tests del modelo PolicyDefinition.""" """Tests del modelo PolicyDefinition."""
from agentforge_core.domain.policy import PolicyDefinition, PolicyValidator from forja_core.domain.policy import PolicyDefinition, PolicyValidator
def test_policy_definition_default_fail_closed() -> None: def test_policy_definition_default_fail_closed() -> None:
+2 -2
View File
@@ -4,8 +4,8 @@ from uuid import uuid4
import pytest import pytest
from agentforge_core.domain.policy import PolicyDefinition, PolicyValidator from forja_core.domain.policy import PolicyDefinition, PolicyValidator
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine from forja_core.guardrails.guardrails_ai import GuardrailsAIEngine
@pytest.fixture @pytest.fixture
+4 -4
View File
@@ -4,10 +4,10 @@ from uuid import uuid4
import pytest import pytest
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.composite import CompositeGuardrailEngine from forja_core.guardrails.composite import CompositeGuardrailEngine
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine from forja_core.guardrails.guardrails_ai import GuardrailsAIEngine
from agentforge_core.guardrails.nemo import NeMoGuardrailsEngine from forja_core.guardrails.nemo import NeMoGuardrailsEngine
@pytest.fixture @pytest.fixture
+1 -1
View File
@@ -2,7 +2,7 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentforge_core.main import create_app from forja_core.main import create_app
def test_health_responde_ok() -> None: def test_health_responde_ok() -> None:
+1 -1
View File
@@ -1,6 +1,6 @@
"""Test de los modelos base del proveedor LLM.""" """Test de los modelos base del proveedor LLM."""
from agentforge_core.llm.base import CompletionResult, Message from forja_core.llm.base import CompletionResult, Message
def test_message_serializable() -> None: def test_message_serializable() -> None:
+3 -3
View File
@@ -2,9 +2,9 @@
import pytest import pytest
from agentforge_core.config import Settings from forja_core.config import Settings
from agentforge_core.llm.factory import build_llm_provider from forja_core.llm.factory import build_llm_provider
from agentforge_core.llm.mock import MockProvider from forja_core.llm.mock import MockProvider
def test_factory_devuelve_mock_por_defecto() -> None: def test_factory_devuelve_mock_por_defecto() -> None:
+2 -2
View File
@@ -4,8 +4,8 @@ import json
import pytest import pytest
from agentforge_core.llm.base import Message from forja_core.llm.base import Message
from agentforge_core.llm.mock import MockProvider from forja_core.llm.mock import MockProvider
@pytest.fixture @pytest.fixture
+1 -1
View File
@@ -6,7 +6,7 @@ import json
import structlog import structlog
from agentforge_core.observability.logging import bind_trace_id, configure_logging from forja_core.observability.logging import bind_trace_id, configure_logging
def test_configure_logging_emite_json(capsys) -> None: # type: ignore[no-untyped-def] def test_configure_logging_emite_json(capsys) -> None: # type: ignore[no-untyped-def]
+3 -3
View File
@@ -5,14 +5,14 @@ from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from agentforge_core.api.persistence import ( from forja_core.api.persistence import (
append_execution, append_execution,
append_violation, append_violation,
read_execution_summaries, read_execution_summaries,
read_violations, read_violations,
) )
from agentforge_core.domain.execution import AgentExecution from forja_core.domain.execution import AgentExecution
from agentforge_core.domain.guardrail import GuardrailViolation from forja_core.domain.guardrail import GuardrailViolation
def _violation() -> GuardrailViolation: def _violation() -> GuardrailViolation:
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest import pytest
from agentforge_core.registry.policy_store import FileSystemPolicyStore from forja_core.registry.policy_store import FileSystemPolicyStore
FIXTURES = Path(__file__).parent.parent / "fixtures" / "policies" FIXTURES = Path(__file__).parent.parent / "fixtures" / "policies"
+2 -2
View File
@@ -4,8 +4,8 @@ import shutil
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from agentforge_core.domain.agent import AgentDefinition, LLMConfig from forja_core.domain.agent import AgentDefinition, LLMConfig
from agentforge_core.registry.repository import FileSystemAgentRegistry from forja_core.registry.repository import FileSystemAgentRegistry
FIXTURES = Path(__file__).parent.parent / "fixtures" / "agents" FIXTURES = Path(__file__).parent.parent / "fixtures" / "agents"
+6 -6
View File
@@ -4,12 +4,12 @@ from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from agentforge_core.domain.agent import AgentDefinition, LLMConfig from forja_core.domain.agent import AgentDefinition, LLMConfig
from agentforge_core.domain.policy import PolicyDefinition from forja_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine from forja_core.guardrails.guardrails_ai import GuardrailsAIEngine
from agentforge_core.llm.mock import MockProvider from forja_core.llm.mock import MockProvider
from agentforge_core.runtime.checkpointer import build_checkpointer from forja_core.runtime.checkpointer import build_checkpointer
from agentforge_core.runtime.graph import build_graph from forja_core.runtime.graph import build_graph
def _agent() -> AgentDefinition: def _agent() -> AgentDefinition:
+3 -3
View File
@@ -4,9 +4,9 @@ from uuid import uuid4
import pytest import pytest
from agentforge_core.domain.policy import PolicyDefinition, PolicyValidator from forja_core.domain.policy import PolicyDefinition, PolicyValidator
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine from forja_core.guardrails.guardrails_ai import GuardrailsAIEngine
from agentforge_core.runtime.nodes import build_node_validate_input from forja_core.runtime.nodes import build_node_validate_input
@pytest.fixture @pytest.fixture
+5 -5
View File
@@ -6,11 +6,11 @@ from uuid import uuid4
import pytest import pytest
from agentforge_core.domain.agent import AgentDefinition, LLMConfig from forja_core.domain.agent import AgentDefinition, LLMConfig
from agentforge_core.domain.policy import PolicyDefinition, PolicyValidator from forja_core.domain.policy import PolicyDefinition, PolicyValidator
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine from forja_core.guardrails.guardrails_ai import GuardrailsAIEngine
from agentforge_core.llm.mock import MockProvider from forja_core.llm.mock import MockProvider
from agentforge_core.runtime.orchestrator import AgentOrchestrator from forja_core.runtime.orchestrator import AgentOrchestrator
def _agent(threshold: int = 4) -> AgentDefinition: def _agent(threshold: int = 4) -> AgentDefinition: