28 KiB
Componentes de Forja y su interrelación (bajo nivel)
Nota (post-simplificación 2026): Este documento describe el cableado del núcleo. La UI ahora es HTMX embebida en el propio core (no hay dashboard separado). La API REST está bajo el prefijo
/api.
- ¿Quieres la historia y el "por qué"? →
docs/explicacion.md.- ¿Las decisiones técnicas resumidas? →
ARCHITECTURE.md.- ¿Cómo arrancarlo? →
README.md.Rutas relativas a
core/src/forja_core/salvo que se diga otra cosa.
1. Grafo de dependencias de módulos (imports internos)
Edges = "X importa de Y". El grafo es un DAG; las capas de abajo no importan nada de las de arriba. Nivel = profundidad topológica.
NIVEL 0 (no importan nada del proyecto)
config ← Settings (pydantic-settings, lee .env)
observability.logging ← configure_logging / bind_trace_id / clear_trace_id
domain.agent ← AgentDefinition, LLMConfig, AgentVersionMeta
domain.guardrail ← GuardrailViolation
domain.policy ← PolicyDefinition, PolicyValidator, PolicyVersionMeta
registry.versioning ← compute_hash, unified_diff, DiffResult
llm.base ← LLMProvider (Protocol), Message, CompletionResult
runtime.state ← AgentState (TypedDict)
runtime.checkpointer ← build_checkpointer (AsyncSqliteSaver)
NIVEL 1
domain.execution ⇐ domain.guardrail
llm.mock / llm.azure / llm.openai ⇐ llm.base
registry.repository ⇐ domain.agent, registry.versioning
registry.policy_store ⇐ domain.policy
guardrails.validators ⇐ domain.guardrail
guardrails.base ⇐ domain.guardrail, domain.policy
api.persistence ⇐ domain.execution, domain.guardrail
api.middlewares ⇐ observability.logging
NIVEL 2
llm.factory ⇐ config, llm.base, llm.{mock,azure,openai}
registry.factory ⇐ config, registry.{repository,policy_store}
guardrails.guardrails_ai ⇐ domain.{guardrail,policy}, guardrails.validators
guardrails.composite ⇐ domain.{guardrail,policy}, guardrails.base
guardrails.nemo ⇐ domain.{guardrail,policy}
NIVEL 3
guardrails.factory ⇐ config, guardrails.{base,composite,guardrails_ai,nemo}
runtime.nodes ⇐ domain.{agent,policy}, guardrails.base, llm.base, runtime.state
runtime.graph ⇐ domain.{agent,policy}, guardrails.base, llm.base, runtime.{nodes,state}
NIVEL 4
runtime.orchestrator ⇐ domain.{agent,execution,guardrail,policy}, guardrails.base, llm.base, runtime.{checkpointer,graph}
NIVEL 5
api.deps ⇐ config, guardrails.{base,factory}, llm.{base,factory}, registry.{factory,policy_store,repository}, runtime.orchestrator
api.agents ⇐ api.deps, domain.agent, registry.versioning
api.policies ⇐ api.deps, domain.policy
api.violations ⇐ api.deps, api.persistence, domain.guardrail
api.executions ⇐ api.deps, api.persistence, domain.{agent,execution,policy}, registry.{policy_store,repository}, runtime.orchestrator
NIVEL 6
main ⇐ api (routers), api.middlewares, config, observability.logging → create_app(), app
(UI HTMX co-locada dentro del propio forja-core; mismo proceso, sin cliente HTTP intermedio)
Reglas que se cumplen y conviene mantener:
domain/no importa nada del resto del proyecto. Es el vocabulario; todos dependen de él.- Solo
api/importa deruntime.orchestrator(ydeps.pylo construye). El resto deapi/no toca LangGraph;runtime/no tocaapi/. - Solo los
factory.pyymain.pyimportanconfig.Settings. El resto recibe los objetos ya construidos. guardrails.compositeno conoce a sus sub-engines concretos (soloGuardrailEngine); los ensamblaguardrails.factory.
2. Inyección de dependencias — api/deps.py
Cinco objetos del dominio, cada uno construido una sola vez por proceso
(@lru_cache(maxsize=1)), más el orchestrator que los compone:
get_settings() ──────────────────────────────────────────────► Settings() (pydantic-settings ← .env)
│
├──► get_registry() = build_agent_registry(settings) ──► FileSystemAgentRegistry(settings.agents_dir)
├──► get_policy_store() = build_policy_store(settings) ──► FileSystemPolicyStore(settings.policies_dir)
├──► get_llm_provider() = build_llm_provider(settings) ──► Mock | AzureOpenAI | OpenAI (según settings.llm_provider)
├──► get_guardrail_engine() = build_guardrail_engine(settings)──► CompositeGuardrailEngine([GuardrailsAIEngine(), (NeMoGuardrailsEngine() si settings.guardrails_nemo_enabled)])
│
└──► get_orchestrator() = AgentOrchestrator(
provider = get_llm_provider(),
engine = get_guardrail_engine(),
data_dir = get_settings().data_dir,
)
Aliases que los routers piden por parámetro (Annotated[T, Depends(get_*)]):
| Alias | Tipo | Lo usan |
|---|---|---|
SettingsDep |
Settings |
executions (todos los endpoints), violations |
RegistryDep |
FileSystemAgentRegistry |
agents (todos), executions (invoke, get, list, approve, reject) |
PolicyStoreDep |
FileSystemPolicyStore |
policies (todos), executions (invoke, get, list, approve, reject) |
OrchestratorDep |
AgentOrchestrator |
executions (invoke, get, list, approve, reject) |
No hay
LLMProviderDepniGuardrailEngineDep: el provider y el engine solo se inyectan alAgentOrchestrator(víaget_orchestrator), nunca a un router.Lifecycle: las
get_*son perezosas → se llaman en la primera request que las necesita. Los tests hacendeps.get_settings.cache_clear()(etc.) trasmonkeypatch.setenv("DATA_DIR", tmp_path)para reconstruir todo apuntando a un directorio temporal.main.create_app()llama aSettings()directamente (para configurar el logging), no adeps.get_settings(); son instancias distintas pero leen la misma config.
3. Contratos entre componentes (firmas exactas)
3.1 Proveedor LLM — llm/base.py
class Message(BaseModel): role: Literal["system","user","assistant"]; content: str
class CompletionResult(BaseModel): content: str; model: str; tokens_in: int; tokens_out: int; latency_ms: int
class LLMProvider(Protocol):
name: str
async def complete(self, messages: list[Message], schema: dict[str,Any] | None = None,
temperature: float = 0.2, max_tokens: int = 2000) -> CompletionResult: ...
Implementaciones: MockProvider (llm/mock.py, determinista — elige una de
_CANONICAL_RESPONSES = {"sip":…, "mos":…, "hss":…} buscando esas subcadenas en el
input, en ese orden; si no, una respuesta genérica), AzureOpenAIProvider
(llm/azure.py), OpenAIProvider (llm/openai.py). Selector: build_llm_provider(settings)
en llm/factory.py (match settings.llm_provider). El nodo llm_reason lo invoca
así: await provider.complete(messages=[Message("system", agent_def.system_prompt), Message("user", state["user_input"])], temperature=agent_def.llm.temperature, max_tokens=agent_def.llm.max_tokens).
3.2 Motor de guardrails — guardrails/base.py
class GuardrailEngine(Protocol):
name: str
async def validate_input (self, payload: str, policy: PolicyDefinition, trace_id: UUID) -> list[GuardrailViolation]: ...
async def validate_output(self, payload: dict[str, Any], policy: PolicyDefinition, trace_id: UUID) -> list[GuardrailViolation]: ...
class GuardrailViolation(BaseModel):
trace_id: UUID; timestamp: datetime
stage: Literal["input","output"]; validator: str
severity: Literal["info","warning","block"]; message: str; blocked: bool
Implementaciones:
-
CompositeGuardrailEngine(engines: list[GuardrailEngine])(guardrails/composite.py) —validate_*haceasyncio.gather(*(e.validate_*(...) for e in engines))y aplana las listas. LanzaValueErrorsienginesestá vacío. -
GuardrailsAIEngine()(guardrails/guardrails_ai.py) — el real. Mantiene dos registrostype → función:INPUT_VALIDATORS = {"detect_pii", "prompt_injection", "toxic_language", "forbidden_topics"}OUTPUT_VALIDATORS = {"schema_match", "pii_leakage", "forbidden_action_keywords", "telco_safety_rules"}
_run(...)recorrepolicy.input_validators/policy.output_validators(cada uno unPolicyValidator(type, config)), buscatypeen el registro, y llamafn(payload, validator.config, trace_id, kind). Sifnlanza ypolicy.on_validator_error == "fail_closed"⇒ añade unaGuardrailViolation(severity="block", blocked=True, message=f"validator failed: {exc}", validator=v.type). Si eltypeno existe en el registro ⇒ log warning y continúa (no bloquea). -
NeMoGuardrailsEngine(allowed_keywords)(guardrails/nemo.py) — stub; solo se añade al composite sisettings.guardrails_nemo_enabled.validate_input: si el texto no menciona ningún keyword permitido ⇒ una violaciónseverity="warning",blocked=False.validate_output: siempre[].
Selector: build_guardrail_engine(settings) en guardrails/factory.py.
Contrato de una función validadora (todas las de guardrails/validators.py):
def <validador>(payload: <str|dict>, config: dict[str, Any], trace_id: UUID, stage: str) -> list[GuardrailViolation]
- Entrada (
validate_input):payloadesstr(el texto del usuario).detect_pii,prompt_injection,toxic_language,forbidden_topics. - Salida (
validate_output):payloadesdict(el JSON parseado del LLM).schema_match,pii_leakage,forbidden_action_keywords,telco_safety_rules. configviene literal del YAML de la política (p. ej.{entities:[...], severity_on_match:"block"}).detect_pii: usa_presidio_analyzer()(singleton perezoso depresidio_analyzer.AnalyzerEnginecon el modelo spaCyen_core_web_sm); si Presidio no está instalado o el modelo no carga, hace fallback a_pii_regex_fallback(regex de EMAIL / PHONE 3-3-3 / ES_NIF\d{8}[A-HJ-NP-TV-Z]/ IP).
3.3 Registry de agentes — registry/repository.py → FileSystemAgentRegistry(root: Path)
| Método | Devuelve | Lee |
|---|---|---|
list_agents() |
list[AgentDefinition] |
recorre root/*/index.yaml, devuelve la versión activa de cada uno |
get_agent(name, version=None) |
AgentDefinition |
index.yaml["active_version"] (o version) → get_version |
get_version(name, version) |
AgentDefinition |
root/<name>/versions/<version>.yaml (lanza FileNotFoundError) |
list_versions(name) |
list[AgentVersionMeta] |
index.yaml["versions"] |
upsert_version(name, body, message, author) |
AgentVersionMeta |
escribe versions/<id>.yaml (con compute_hash) y actualiza index.yaml (si body.state=="active" cambia active_version) |
diff_versions(name, v1, v2) |
DiffResult |
versioning.unified_diff entre los dos YAML |
3.4 Store de políticas — registry/policy_store.py → FileSystemPolicyStore(root: Path)
list_policies() -> list[PolicyDefinition], get_policy(name, version=None) -> PolicyDefinition (lanza FileNotFoundError), list_versions(name) -> list[PolicyVersionMeta].
class PolicyValidator(BaseModel): type: str; config: dict[str,Any] = {}
class PolicyDefinition(BaseModel):
name: str; version: str; description: str
input_validators: list[PolicyValidator]
output_validators: list[PolicyValidator]
on_validator_error: Literal["fail_open","fail_closed"] = "fail_closed"
class PolicyVersionMeta(BaseModel): id: str; hash: str; author: str; message: str; created_at: datetime
3.5 Orchestrator — runtime/orchestrator.py → AgentOrchestrator
AgentOrchestrator(*, provider: LLMProvider, engine: GuardrailEngine, data_dir: Path)
async invoke (*, agent_def: AgentDefinition, policy: PolicyDefinition, user_input: str,
trace_id: UUID | None = None) -> AgentExecution
async resume (*, agent_def, policy, trace_id: UUID, decision: dict[str,Any]) -> AgentExecution
async snapshot(*, agent_def, policy, trace_id: UUID) -> AgentExecution | None
- Cada uno abre su propio
AsyncSqliteSaver(runtime/checkpointer.build_checkpointer(data_dir)→data_dir/checkpoints.sqlite, context manager async) y construye el grafo conbuild_graph(...). invoke: arma elAgentStateinicial (status="running", listas vacías…) y hacegraph.ainvoke(state, config={"configurable": {"thread_id": str(trace_id)}}). Si el grafo lanza, marcacrashed=True. Luego_snapshot(...)→AgentExecution.resume:graph.ainvoke(Command(resume=decision), config={thread_id: trace_id})— reanuda elinterrupt()condecision.snapshot:graph.aget_state(config); sistate.valuesestá vacío →None; si no,_build_execution(...).
3.6 El grafo — runtime/graph.py + runtime/nodes.py + runtime/state.py
# runtime/state.py
class AgentState(TypedDict, total=False):
trace_id: str; agent_name: str; agent_version: str; user_input: str
messages: list[dict]; raw_llm_output: str | None; parsed_output: dict | None
proposed_actions: list[dict]; violations: list[dict]
decision_path: Annotated[list[dict], operator.add] # ← reducer: cada nodo AÑADE pasos
status: str; error: str | None
human_decision: dict | None; final_output: dict | None
# runtime/nodes.py
NodeFn = Callable[[AgentState], Awaitable[dict[str, Any]]]
# factories que devuelven un NodeFn, parametrizadas con engine/policy/provider/agent_def:
build_node_validate_input(engine, policy) build_node_llm_reason(provider, agent_def)
build_node_validate_output(engine, policy) build_node_propose_actions()
build_node_approve_gate(agent_def) build_node_finalize()
# cada nodo añade un DecisionStep a decision_path: {step, timestamp, duration_ms, detail}
# runtime/graph.py
build_graph(*, agent_def, policy, provider, engine, checkpointer: BaseCheckpointSaver) -> CompiledGraph
Cableado (nodos y aristas):
START ─► validate_input
validate_input ──(conditional)──► END si status == "blocked_by_guardrail"
llm_reason en otro caso
llm_reason ──(conditional)──────► END si status == "failed"
validate_output en otro caso
validate_output ──(conditional)─► END si status in {"blocked_by_guardrail","failed"}
propose_actions en otro caso
propose_actions ──► approve_gate ──► finalize ──► END (aristas fijas)
approve_gate: si hay acciones conrisk_score >= agent_def.risk_threshold_for_hitlorequires_approval⇒decision = interrupt({"awaiting_actions": risky})→ el grafo se pausa ahí (LangGraph persiste el estado en el checkpointer); al reanudar conCommand(resume=decision),interrupt()devuelvedecisiony el nodo continúa, escribiendohuman_decision.finalize: sihuman_decision.rejected⇒status="failed",error="rejected_by_human". Si no,final_output = {**parsed_output, "approved_actions": <acciones cuyo id está en approved_action_ids; o todas si no hubo HITL>},status="completed".
3.7 De StateSnapshot a AgentExecution — orchestrator._build_execution
class AgentExecution(BaseModel):
trace_id: UUID; agent_name: str; agent_version: str
status: Literal["running","awaiting_approval","blocked_by_guardrail","completed","failed"]
started_at: datetime; finished_at: datetime | None
decision_path: list[DecisionStep]; violations: list[GuardrailViolation]
proposed_actions: list[ProposedAction]; needs_human_for: list[ProposedAction] | None
final_output: dict[str,Any] | None; error: str | None
class DecisionStep(BaseModel): step: str; timestamp: datetime; duration_ms: int; detail: dict[str,Any]
class ProposedAction(BaseModel): id: str; action: str; target: str; risk_score: int (1..5); rollback_plan: str; requires_approval: bool
class AgentExecutionSummary(BaseModel): trace_id; agent_name; agent_version; status; started_at; finished_at; n_violations: int; n_proposed_actions: int
Reglas para derivar status a partir del StateSnapshot de LangGraph:
status = state.values.get("status", "running").- Si
crashedystatusno es terminal ⇒status = "failed"(error = error_del_state or "internal_error"). - Si
state.next(hay un nodo pendiente) ystatusno es terminal ⇒status = "awaiting_approval"(es elinterrupt()deapprove_gate). needs_human_for: solo sistatus == "awaiting_approval"⇒[a for a in proposed_actions if a.risk_score >= agent_def.risk_threshold_for_hitl or a.requires_approval]; en otro casoNone.started_at = decision_path[0].timestamp(onowsi vacío);finished_at = nowsolo sistatuses terminal.- Terminales =
{"completed","failed","blocked_by_guardrail"}.
4. Cadenas de llamada por endpoint (api/)
Todos pasan primero por TraceIdMiddleware (lee/crea X-Trace-Id, lo bind-ea a structlog, lo devuelve en la respuesta). Montaje (en main.create_app): agents.router→/agents, executions.invoke_router→/agents, executions.router→/executions, policies.router→/policies, violations.router→/violations, más GET /health.
| Endpoint | Handler (api/…) |
Deps inyectadas | Llama a → Escribe |
|---|---|---|---|
POST /agents/{name}/invoke |
executions.invoke_agent |
Registry, PolicyStore, Orchestrator, Settings | registry.get_agent(name) → policies.get_policy(agent.guardrails[0]) → orchestrator.invoke(agent_def, policy, body.input) → _record_execution(data_dir, trace_id, name, version) (escribe execution_index.json) → si status terminal: append_execution (executions.jsonl) → por cada violación: append_violation (violations.jsonl). 404 si el agente no existe; 422 si no tiene política. |
GET /executions |
executions.list_executions |
Registry, PolicyStore, Orchestrator, Settings | read_execution_summaries(data_dir) (lee executions.jsonl) + por cada trace_id en execution_index.json no presente ya: registry.get_version + policies.get_policy + orchestrator.snapshot → AgentExecutionSummary; fusiona. (Así aparecen también las awaiting_approval, que no están en el JSONL.) |
GET /executions/{trace_id} |
executions.get_execution |
Registry, PolicyStore, Orchestrator, Settings | _resolve(...) (lee execution_index.json → reconstruye agent_def/policy; 404 si no está, 500 si la config referida no existe) → orchestrator.snapshot(...) → 404 si None. |
POST /executions/{trace_id}/approve |
executions.approve_execution |
Registry, PolicyStore, Orchestrator, Settings | _resolve → _ensure_awaiting (orchestrator.snapshot; 409 si no está en awaiting_approval) → orchestrator.resume(decision={approved_action_ids: body.approved_action_ids, comment: body.comment, rejected: False}) → append_execution (executions.jsonl). |
POST /executions/{trace_id}/reject |
executions.reject_execution |
Registry, PolicyStore, Orchestrator, Settings | _resolve → _ensure_awaiting → orchestrator.resume(decision={approved_action_ids: [], rejected: True, reason: body.reason}) → append_execution. |
GET /agents |
agents.list_agents |
Registry | registry.list_agents() |
GET /agents/{name} |
agents.get_agent |
Registry | registry.get_agent(name) (404 si no existe) |
GET /agents/{name}/versions |
agents.list_versions |
Registry | registry.list_versions(name) (404) |
GET /agents/{name}/versions/{version} |
agents.get_version |
Registry | registry.get_version(name, version) (404) |
GET /agents/{name}/versions/{v_from}/diff/{v_to} |
agents.diff_versions |
Registry | registry.diff_versions(...) → DiffResult |
GET /policies |
policies.list_policies |
PolicyStore | store.list_policies() |
GET /policies/{name}/versions |
policies.list_versions |
PolicyStore | store.list_versions(name) (404) |
GET /violations?trace_id=&severity= |
violations.list_violations |
Settings | read_violations(data_dir) (lee violations.jsonl) + filtros opcionales por trace_id / severity. |
GET /health |
(en main.py) |
— | {"status":"ok"} |
Modelos de petición (en executions.py): InvokeRequest{input: str, version: str | None}, ApproveRequest{approved_action_ids: list[str]=[], comment: str | None}, RejectRequest{reason: str}. Modelos de respuesta = los de domain/ (response_model=…).
Helpers internos de executions.py (índice de ejecuciones, en data_dir/execution_index.json):
_index_path, _load_index, _record_execution(data_dir, trace_id, agent_name, version),
_resolve(registry, policies, data_dir, trace_id) → (AgentDefinition, PolicyDefinition) (con HTTPException 404/500),
_ensure_awaiting(orchestrator, agent_def, policy, trace_id) (HTTPException 404/409).
5. Persistencia: quién escribe / lee qué
| Artefacto | Formato | Lo escribe | Lo lee |
|---|---|---|---|
agents/<n>/versions/<v>.yaml, agents/<n>/index.yaml |
YAML | FileSystemAgentRegistry.upsert_version (a mano en este MVP) |
FileSystemAgentRegistry.get_* / list_* / diff_versions |
policies/<n>/versions/<v>.yaml, policies/<n>/index.yaml |
YAML | a mano | FileSystemPolicyStore.get_policy / list_* |
data/checkpoints.sqlite |
SQLite (LangGraph AsyncSqliteSaver) |
el grafo, en cada ainvoke/transición de nodo (incl. el interrupt()) — vía runtime/checkpointer.build_checkpointer(data_dir) |
orchestrator.snapshot / resume (graph.aget_state / Command(resume=…)) |
data/execution_index.json |
JSON {trace_id: {agent_name, version}} |
executions._record_execution — en cada invoke |
executions._resolve, executions.list_executions |
data/executions.jsonl |
JSONL append-only (AgentExecution por línea) |
api/persistence.append_execution — en invoke si el status es terminal y siempre tras approve/reject |
api/persistence.read_execution_summaries (endpoint GET /executions) |
data/violations.jsonl |
JSONL append-only (GuardrailViolation por línea) |
api/persistence.append_violation — en invoke, una por violación de la ejecución |
api/persistence.read_violations (endpoint GET /violations) |
Hashing/versionado: registry/versioning.compute_hash(yaml_text) = SHA-256 del YAML con espacios finales recortados; unified_diff(a, b, label_a, label_b) = difflib.unified_diff. (En los index.yaml de ejemplo el hash es "pending" y nadie lo valida al cargar.)
6. UI HTMX embebida en Core
La interfaz es una sola página (GET /) servida desde el mismo proceso
forja-core (Jinja2 + HTMX vía CDN, sin servicio separado), organizada como
las seis etapas del ciclo de vida: Define → Run → Approve → Train → Promote →
Audit. Cada etapa es un fragmento (GET /fragments/*) que se re-renderiza
cuando cualquier botón de acción dispara el evento global forja-refresh.
Los botones llaman directamente a los endpoints /api/* (con json-enc);
no hay cliente HTTP intermedio ni lógica duplicada. Las rutas multipágina
antiguas redirigen con 301 a las anclas de su etapa.
7. Arranque y ciclo de vida
Proceso core (uvicorn forja_core.main:app):
- Import de
forja_core.main⇒ se ejecutaapp = create_app():Settings()→configure_logging(level=settings.log_level)→FastAPI(...)→app.add_middleware(TraceIdMiddleware)→ registraGET /health→from forja_core.api import agents, executions, policies, violations→include_router×5. - Las dependencias (
deps.get_registry,get_policy_store,get_llm_provider,get_guardrail_engine,get_orchestrator) no se construyen aún; se construyen y cachean en la primera request que las inyecta. - Cada request:
TraceIdMiddleware.dispatch→ router → resuelveDepends(...)(que pueden disparar la construcción perezosa) → handler → respuesta conX-Trace-Id.
Contenedores (docker-compose.yml): servicio core (core/Dockerfile,
uvicorn forja_core.main:app --host 0.0.0.0 --port 8000 para API + UI HTMX,
HEALTHCHECK → curl /health, monta ./agents:rw, ./policies:rw, ./data:rw,
env DATA_DIR=/app/data, AGENTS_DIR=/app/agents, POLICIES_DIR=/app/policies,
env_file: .env). Un solo servicio.
Settings (env vars) — config.py:
| Campo | Env var | Default | Lo consume |
|---|---|---|---|
llm_provider |
LLM_PROVIDER |
mock |
build_llm_provider |
llm_fallback_provider |
LLM_FALLBACK_PROVIDER |
"" |
build_llm_provider → FallbackLLMProvider (vacío = sin fallback) |
azure_openai_* |
AZURE_OPENAI_* |
"" / 2024-08-01-preview |
AzureOpenAIProvider |
openai_api_key / openai_model |
OPENAI_API_KEY / OPENAI_MODEL |
"" / gpt-4o |
OpenAIProvider |
guardrails_nemo_enabled |
GUARDRAILS_NEMO_ENABLED |
False |
build_guardrail_engine |
guardrails_nemo_allowed_keywords |
GUARDRAILS_NEMO_ALLOWED_KEYWORDS |
"" |
keywords CSV del stub topical-rails (vacío = sin chequeo) |
log_level |
LOG_LEVEL |
INFO |
configure_logging |
training_backend |
TRAINING_BACKEND |
mock |
build_training_backend |
azure_ml_* |
AZURE_ML_* |
varios | AzureMLTrainingBackend (stub si falta workspace) |
data_dir |
DATA_DIR |
./data |
checkpointer, logs JSONL, training runs, evaluaciones, promociones |
agents_dir |
AGENTS_DIR |
./agents |
build_agent_registry, escenarios de evaluación |
policies_dir |
POLICIES_DIR |
./policies |
build_policy_store |
datasets_dir |
DATASETS_DIR |
./datasets |
build_dataset_store |
models_dir |
MODELS_DIR |
./models |
build_model_store |
8. Aristas y "gotchas"
llm_fallback_provider: si está configurado y difiere del primario,build_llm_providerdevuelve unFallbackLLMProviderque delega en el secundario cuando el primario agota sus reintentos._build_singleusamatchsincase _:; al ser el tipo unLiteralde tres valores es exhaustivo, pero un valor inesperado caería en "ninguna rama".- NeMo:
NeMoGuardrailsEnginesolo entra alCompositeGuardrailEnginesiGUARDRAILS_NEMO_ENABLED=true; por defecto el composite tiene un solo engine. detect_piise comporta distinto según el entorno: conpresidio-analyzerinstalado (imagen Docker) usa Presidio +en_core_web_sm; sin él (venv local típico), regex fallback. La políticadefaultpide solo recognizers de patrón (EMAIL_ADDRESS,ES_NIF,IP_ADDRESS,IBAN_CODE) para evitar falsos positivos del NER.AsyncSqliteSaverliga su conexiónaiosqliteal event loop activo: por esobuild_checkpointeres un async context manager y el orchestrator lo abre y cierra en cada operación (no se reusa entre llamadas). Requiereaiosqlite<0.21.data/debe existir y ser escribible por el proceso/usuario del contenedor (agent, uid 1000); si no,invokefallará al escribir el log/checkpoint.make test/make lint/make smokeasumen que el venv está activado (los binariospytest/ruff/mypy/dockeren elPATH).tests/integration/monta la app conTestClientapuntando a los assets reales del repo (agents/,policies/);tests/unit/test_api_*usantests/fixtures/(versiones mínimas). Ambos hacendeps.*.cache_clear().