tests/fixtures/agents/incident_analyzer/ que se actualizará en Fase 8.
No afecta a ningún import de código Python.
detect_pii, prompt_injection,
toxic_language. Cambia forbidden_topics a temas relevantes
para trading (["manipulación de mercado", "insider trading", "credenciales de broker"]).
El schema_match de output pasa a validar el output schema genérico de trading:
campos requeridos [proposed_actions] con subcampos [id, action, target,
risk_score, rollback_plan, requires_approval]. Cambia forbidden_action_keywords
a términos peligrosos de trading (["DROP TABLE", "rm -rf", "liquidate all",
"market order all"]). Reemplaza telco_safety_rules con
trading_safety_rules (se implementa en Fase 2).
build_guardrail_engine(), la lista allowed_keywords del
NeMoGuardrailsEngine contiene términos telecom (sip, ims, cscf, sbc,
mos, hss). Reemplazar con terminología de trading:
["equity", "forex", "fx", "portfolio", "signal", "position", "order",
"market", "risk", "hedge", "rebalance", "execution", "volatility", "spread"].
NeMo actúa como guardrail de off-topic: si el input no menciona ningún keyword de
la lista, emite un warning (no bloquea). Esto evita que el agente procese peticiones
fuera de dominio.
LLMConfig: cambiar el Literal["mock", "azure", "openai"]
a Literal["mock", "azure", "openai", "anthropic"].AgentDefinition: añadir
hitl_timeout_minutes: int | None = Field(default=None, ge=1, le=1440).
Cuando es None, el HITL no expira. Cuando tiene valor, el background checker
de Fase 6 auto-rechaza la ejecución si el operador no decide en ese tiempo.
Valor recomendado para producción: 30 min equities, 60 min FX.dry_run: bool al TypedDict AgentState.
El orquestrador lo inyecta en el estado inicial al invocar. El nodo
finalize lo lee para omitir la emisión de approved_actions
y marcar el output con "dry_run": True.
El campo se inicializa a False en orchestrator.invoke()
para preservar retrocompatibilidad.
Settings:
anthropic_api_key: str = ""Literal de llm_provider y
llm_fallback_provider para incluir "anthropic".hitl_sla_check_interval_seconds: int = 60 — frecuencia del background
checker que auto-rechaza HITLs expirados.system_halted: bool = False — flag de kill switch (en memoria;
en producción se persistiría en Redis/DB, pero para MVP es suficiente).telco_safety_rules() completa (líneas 265–300).
Añadir trading_safety_rules(payload, config, trace_id, stage) con estas reglas
declarativas activables vía config:
no_order_without_stop_loss: para cada acción con
action in ["BUY","SELL","SHORT","COVER"], el campo rollback_plan
no puede ser vacío, "n/a" ni "none". Bloquea si incumple.no_leveraged_action_without_hedge: si la acción tiene
"leverage" o "margin" en su descripción, debe existir una acción
paralela con action="HEDGE" en el mismo output. Bloquea si incumple.no_mass_liquidation_without_tranche: si target contiene
"all", "*" o "portfolio" y la acción es
SELL/CLOSE, el rollback_plan debe mencionar "tranche"
o "tramo". Bloquea si incumple.require_notional_on_trade_actions: BUY/SELL deben tener el campo
"notional_usd" presente y ser un número positivo. Severity configurable.list[GuardrailViolation].
position_size_limit(payload, config, trace_id, stage).
Config keys:
max_notional_usd: float — si cualquier acción tiene
notional_usd mayor que este límite, bloquea.max_pct_adv: float (opcional, future) — % máximo del Average Daily
Volume. Si no hay dato de ADV en el payload, se ignora la validación.severity_on_breach: "block" | "warning" — default "block".payload.get("proposed_actions", []), extrae
notional_usd de cada acción (si presente), compara contra el límite.
El mensaje de violación incluye el instrumento, el notional real y el límite configurado.
notional_usd no está presente en la acción, el validador emite
un warning indicando que el campo es requerido (nunca bloquea por ausencia,
ese chequeo es de trading_safety_rules).
forbidden_instruments(payload, config, trace_id, stage).
Config keys:
instruments: list[str] — lista de tickers/símbolos bloqueados
(e.g. ["GME", "AMC", "BBBY"] para restricted list, o instrumentos
en embargo regulatorio).severity_on_match: "block" | "warning" — default "block".target de cada acción propuesta. La comparación es
case-insensitive. Si el ticker está en la lista, bloquea y reporta cuál instrumento
y qué acción lo referenciaba.
market_hours_check(payload, config, trace_id, stage).
Config keys:
asset_class: "equity" | "fx" | "crypto" — crypto nunca bloquea
(24/7); equity bloquea fuera de horario; FX emite warning (mercado casi 24/5).timezone: str — e.g. "America/New_York" para NYSE/NASDAQ,
"Europe/London" para LSE. Default "America/New_York".severity_outside_hours: "block" | "warning" | "info" — default
"warning".equity, horario NYSE es 09:30–16:00 ET de lunes a viernes,
excluidas holidays principales (usa datetime.now(tz)). Para fx,
solo bloquear fin de semana. Para crypto, nunca bloquear.
zoneinfo (Python 3.9+, ya en el env).
No añadir dependencias externas.
INPUT_VALIDATORS.
guardrails_ai.py:
INPUT_VALIDATORS: añadir "market_hours_check": market_hours_check.OUTPUT_VALIDATORS: quitar "telco_safety_rules": telco_safety_rules,
añadir "trading_safety_rules": trading_safety_rules,
"position_size_limit": position_size_limit,
"forbidden_instruments": forbidden_instruments.validators al inicio del archivo para
referenciar las funciones nuevas y quitar telco_safety_rules.
AnthropicProvider que implementa el LLMProvider Protocol.
Puntos clave:
anthropic con cliente AsyncAnthropic.cache_control: {"type": "ephemeral"} en el campo system
de la API de Messages. Esto activa el cache de 5 minutos de Anthropic.
Los system prompts de trading (instrucciones de análisis, reglas de riesgo) son
largos y no cambian entre invocaciones del mismo agente → máximo aprovechamiento.list[Message] al formato de la API:
el primer mensaje con role="system" va en el campo system
(con cache_control), el resto van en messages.claude-sonnet-4-6, claude-opus-4-7,
claude-haiku-4-5-20251001. Default en agentes: claude-sonnet-4-6.CompletionResult.tokens_in y tokens_out desde
response.usage. Incluir también cache_read_input_tokens y
cache_creation_input_tokens en el log de structlog para observabilidad
de costes.model en CompletionResult se toma de
response.model.api_key: str y model: str.
match settings.llm_provider:
case "anthropic": return AnthropicProvider(api_key=settings.anthropic_api_key, model=settings.anthropic_model).
anthropic_model: str = "claude-sonnet-4-6" a Settings
(en P1.T3 se puede añadir junto con la API key).
anthropic>=0.40,<1.0.
Verificar que no hay conflictos con las dependencias existentes (httpx, pydantic).
El SDK de Anthropic usa httpx internamente, que ya está en requirements;
asegurarse de que el rango de versiones es compatible.
# Anthropic / Claude (solo si LLM_PROVIDER=anthropic)ANTHROPIC_API_KEY=ANTHROPIC_MODEL=claude-sonnet-4-6HITL_SLA_CHECK_INTERVAL_SECONDS=60.
Actualizar el comentario de LLM_PROVIDER para incluir anthropic.
incident_analyzer/index.yaml. Versión activa: v1.
Campos: name: signal_analyzer, lista de versiones con id/hash/author/message/created_at,
active_version: v1.
purpose: analiza señales de mercado (precio, volumen, indicadores técnicos,
contexto macro) y propone entradas/salidas con análisis de riesgo.guardrails: [trading_equity]risk_threshold_for_hitl: 3 (más conservador: risk ≥ 3 requiere aprobación).hitl_timeout_minutes: 30llm.provider: mock, model: claude-sonnet-4-6,
temperature: 0.1 (determinismo en señales).system_prompt: Eres un analista cuantitativo senior. Recibes datos de mercado.
Devuelves JSON con: instrument, direction (BUY/SELL/HOLD),
rationale (string), confidence (0.0-1.0), timeframe
(intraday/swing/position), proposed_actions (lista con id, action, target,
quantity, notional_usd, risk_score 1-5, stop_loss, rollback_plan, requires_approval).
Reglas: nunca proponer sin stop_loss; acciones >$500k requieren requires_approval=true;
no proponer sobre instrumentos con spreads anómalos sin avisarlo en rationale.output_schema: JSON Schema completo de lo anterior.01: Acción tech supera estimaciones de earnings; RSI en 65; precio rompe
resistencia; pedir entrada larga.02: VIX spike al 28; flows defensivos; sesión risk-off; evaluar reducción
de exposición en renta variable.03: EUR/USD con divergencia RSI bearish en H4; momentum negativo tras
datos de inflación; candidato a entrada corta con gestión del riesgo.active_version: v1.
purpose: recibe snapshot de cartera (posiciones actuales + pesos objetivo)
y propone órdenes de rebalanceo optimizando coste de transacción.guardrails: [trading_equity]risk_threshold_for_hitl: 2 — casi todo requiere aprobación humana; las
operaciones de cartera afectan múltiples posiciones simultáneamente.hitl_timeout_minutes: 60temperature: 0.0 — rebalanceo es matemático, máximo determinismo.rebalance_summary (delta total en USD, coste
estimado de transacción), proposed_actions (una por instrumento afectado).01: rebalanceo trimestral de una cartera 60/40 que ha derivado a 72/28 por el
rally de renta variable. 02: tilt táctico defensivo incrementando bonos al 45%
por entorno macro adverso.
risk_threshold_for_hitl: 3,
hitl_timeout_minutes: 45.
Output schema: risk_summary (VaR 1d, max_drawdown_pct, concentration_top3),
alerts (lista de alertas activas con severity), proposed_actions
(hedges, reducciones, stops).
risk_threshold_for_hitl: 4 (la aprobación de la orden ya vino del agente
anterior; aquí solo se planifica la ejecución). Output schema: execution_strategy
(TWAP/VWAP/IS/POV), slices (lista de sub-órdenes con timestamp, qty, venue),
estimated_market_impact_bps, proposed_actions.
docker compose up core y hacer
GET /agents. Verificar que los 4 nuevos agentes aparecen en el listado
y que sus versiones se pueden leer correctamente. Comprobar que el hash de contenido
se calcula sin errores.
No requiere cambio de código — es validación funcional.
detect_pii (block EMAIL, IBAN),
prompt_injection (block), market_hours_check
(asset_class: equity, tz: America/New_York, severity: warning),
forbidden_topics (block: ["insider", "material non-public",
"manipulación de mercado"]).
schema_match (block, schema validando
proposed_actions con campos de trading), pii_leakage (block),
forbidden_action_keywords (block: ["liquidate all", "margin call ignore",
"DROP TABLE"]), trading_safety_rules (block: all rules),
position_size_limit (block, max_notional_usd: 1000000),
forbidden_instruments (block: restricted list de ejemplo),
on_validator_error: fail_closed
market_hours_check con asset_class: fx (solo bloquea
en weekend), sin límite de horario intradía. El límite de notional es mayor:
max_notional_usd: 5000000 (FX es mercado más líquido).
trading_safety_rules activa solo
no_order_without_stop_loss y no_leveraged_action_without_hedge.
max_notional_usd: 250000 (mayor volatilidad).
market_hours_check ausente o con asset_class: crypto
(nunca bloquea). forbidden_instruments incluye tokens con
restricciones regulatorias.
default se convierte en la política base de trading genérica:
usa todos los validadores de trading pero con límites permisivos. Es la fallback para
agentes que no especifiquen política de asset class concreta.
GET /policies y comprobar que aparecen
trading_equity, trading_fx, trading_crypto.
Hacer GET /policies/trading_equity y verificar que los validadores
se deserializan correctamente sin errores de Pydantic.
dry_run: bool = False a InvokeRequest.
En el handler invoke_agent(), añadir dry_run=body.dry_run
a la llamada orchestrator.invoke(). El orquestrador inyectará el valor
en el estado inicial del grafo (initial["dry_run"] = dry_run).
Si dry_run=True, la ejecución NO se escribe en el JSONL de historial
(solo en el checkpointer temporal). Añadir campo dry_run: bool
a AgentExecution en domain/execution.py para que el
response body refleje el modo.
build_node_finalize(), añadir al inicio:
if state.get("dry_run"): final_output = {**parsed, "approved_actions": final_actions, "dry_run": True}
El comportamiento es idéntico excepto que el output está marcado con
"dry_run": True. El orquestrador y la API usarán ese flag para
no escribir la ejecución en el log append-only de produción.
También se puede añadir un step en el decision_path indicando
dry_run=True para trazabilidad.
GET /control/status: devuelve {"halted": bool, "pending_hitl_count": int,
"message": str}. Lee el estado global del sistema.POST /control/halt: activa el kill switch. Comportamiento:
system_halted = True en el estado de la app
(variable de módulo o en Settings — MVP puede ser un módulo singleton).execution_index.json, encuentra todas las ejecuciones en
awaiting_approval y las rechaza automáticamente con reason
"kill_switch_activated" usando orchestrator.resume().{"halted": true, "auto_rejected": N}.POST /control/resume: desactiva el kill switch (restaura operación normal).invoke_agent() de executions.py, añadir una
comprobación al inicio: si system_halted, devolver 503.
control.router con prefix /control,
tag ["control"].lifespan con asyncio.create_task para
el background checker de SLA HITL. La tarea corre en bucle cada
settings.hitl_sla_check_interval_seconds segundos.@asynccontextmanager async def lifespan(app): task = asyncio.create_task(sla_checker()); yield; task.cancel().
runtime/sla.py con la función async
run_hitl_sla_checker(settings, registry, policies, orchestrator).
Algoritmo en cada tick:
execution_index.json.awaiting_approval:
agent_def del registry.agent_def.hitl_timeout_minutes no es None:
execution.started_at + timedelta(minutes=hitl_timeout_minutes)
con datetime.now(UTC).orchestrator.resume(decision={"approved_action_ids": [],
"rejected": True, "reason": "hitl_sla_expired"}) y escribir en JSONL.dry_run: bool = False a invoke().
Incluirlo en el dict initial: initial["dry_run"] = dry_run.
No se propaga a resume() — el dry_run se decide en la invocación inicial
y queda embebido en el estado del checkpointer.
docs/manual_qa.md con los pasos de prueba manual:
signal_analyzer con dry_run=true,
verificar que el output tiene "dry_run": true y que NO aparece en
el historial (GET /executions no lo lista).POST /control/halt,
verificar que la ejecución pasa a failed con error kill_switch_activated,
verificar que intentar invocar devuelve 503. Luego POST /control/resume y
verificar que vuelve a funcionar.hitl_timeout_minutes: 1 en un agente de test,
invocar, esperar 65 segundos, verificar auto-rechazo.st.title: "▶️ Ejecutar Agente" → sin cambio (genérico).st.text_area label: "Descripción del incidente"
→ "Señal de mercado / Instrucción de trading".placeholder: actualizar con ejemplo de señal de trading.dry_run = st.checkbox("🔬 Modo simulación (dry-run)", value=False)
antes del botón de invocar.{"input": user_input, "dry_run": dry_run} al client.execution["final_output"]["dry_run"] es True, mostrar
badge amarillo "SIMULACIÓN — sin efecto real".notional_usd: si presente, mostrar en grande con color según tamaño
(<100K verde, 100K–1M amarillo, >1M rojo).stop_loss: mostrar si presente.quantity: mostrar si presente.asset_class: badge de tipo de activo si presente.st.set_page_config title a "AgentForge Trading"
y el icono a 📈. Actualizar la página principal (si existe contenido) para describir
la plataforma como fábrica de agentes de trading.
create_app(): title="AgentForge Trading",
description="Plataforma de gobernanza de agentes IA para trading — API REST.",
version="0.2.0". Esto actualiza la UI de /docs de Swagger.
(Este cambio puede hacerse junto con P6.T4 que también modifica main.py.)
trading_equity, trading_fx, trading_crypto
aparecen en el selector y que sus validadores se muestran correctamente.
Si hay algún problema de renderizado con los nuevos tipos de validador, ajustar
la visualización aquí.
tests/fixtures/agents/incident_analyzer/.tests/fixtures/agents/signal_analyzer/ con index y v1 (copia
simplificada de los YAML de producción, con provider: mock).test_agent/versions/v1.yaml: cambiar el output_schema
y system_prompt para que sean de trading (los tests que usan test_agent
deben funcionar con el nuevo schema de validación de políticas trading).test_policy puede simplificarse a solo schema_match
para mantener los tests unitarios simples. Añadir fixture trading_equity
para los tests de integración que usan signal_analyzer.
incident_analyzer y al output schema
telecom con signal_analyzer y el output schema de trading.
El MockProvider devuelve un JSON hardcodeado — actualizar el mock response
en conftest.py para que devuelva el formato de señal de trading:
{"instrument": "AAPL", "direction": "BUY", "rationale": "...", "confidence": 0.8,
"proposed_actions": [{"id": "a1", "action": "BUY", "target": "AAPL",
"quantity": 100, "notional_usd": 18500, "risk_score": 3,
"stop_loss": "18000", "rollback_plan": "Vender si cae -5%", "requires_approval": false}]}.
Los tests de PII, HITL y restart no necesitan cambios lógicos, solo las fixtures.
trading_safety_rules: test happy path (con stop_loss), test que bloquea
sin stop_loss, test que bloquea mass liquidation sin tranche, test leveraged sin hedge.position_size_limit: test que pasa si notional < max,
test que bloquea si notional > max, test que emite warning si notional ausente.forbidden_instruments: test que pasa con ticker permitido,
test que bloquea con ticker restringido (case-insensitive).market_hours_check: mockear datetime.now para simular
dentro/fuera de horario para equity, fx, crypto.AgentDefinition con hitl_timeout_minutes=None
(default), con valor válido (30), con valor fuera de rango (0 falla, 1441 falla).
Verificar que el Literal de LLMConfig.provider acepta "anthropic".
signal_analyzer con dry_run=True:
completed.final_output debe contener "dry_run": True.executions.jsonl) no debe contener la ejecución.TestClient:
GET /control/status devuelve {"halted": false, ...} por defecto.POST /control/halt cambia el estado a halted: true.POST /agents/test_agent/invoke devuelve 503.POST /control/resume restaura operación normal.telco_safety_rules en
test_guardrails_ai.py. Actualizar los mocks de AgentDefinition
en test_runtime_nodes.py y test_runtime_orchestrator.py
para usar el nuevo schema de trading (los tests de lógica de nodos son agnósticos
del dominio — solo necesitan que el fixture sea válido).
make test-all (unit + integration) y make lint
(ruff + mypy). Goal: 0 errores, 0 warnings de tipo. Si hay fallos residuales,
resolverlos antes de marcar la fase completa. Esta tarea actúa como gate de calidad
para todo el trabajo anterior.
signal_analyzer y los nuevos escenarios.control
(kill switch), el background SLA checker, y los 4 nuevos tipos de agentes.
Añadir sección "Flujo de una orden de trading" que muestre cómo los guardrails
de trading se aplican en el grafo LangGraph.
description de "Plataforma profesional de gobernanza de agentes IA"
a "Plataforma de gobernanza de agentes IA para trading — fábrica de agentes con guardrails, HITL y auditoría".
Sin cambios en dependencias ni en la configuración de herramientas.
make smoke (docker compose up + curl /health) con todos los
cambios del repurpose. Verificar:
GET /agents devuelve los 4 agentes de trading.GET /policies devuelve las 3 políticas de trading + default.GET /control/status devuelve {"halted": false, ...}.Las fases son mayoritariamente independientes pero tienen algunas dependencias duras:
Secuencia óptima por sesión: Fase 0 → Fases 1+2 (juntas, sin dep entre sí) → Fases 3+4+5 (paralelas) → Fase 6 → Fase 7 → Fase 8 (gate de calidad) → Fases 9+10.