From 32b85c2f84cb55226384d8043e58e30622388da4 Mon Sep 17 00:00:00 2001 From: "cristiano.hoshikawa" Date: Mon, 3 Aug 2026 10:51:44 -0300 Subject: [PATCH] New features: Route Stickness, Handoff, Clarification, Read-Only/Transactional, Long Term Memory --- IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md | 64 + .../README.md | 88 + .../agent_template_backend/.env | 209 + .../agent_template_backend/Dockerfile | 6 + .../agent_template_backend/README.md | 4219 +++++++++++++++++ .../README_ENTERPRISE_TEMPLATE.md | 54 + .../agent_template_backend/app/__init__.py | 0 .../app/agents/README.md | 15 + .../app/agents/billing_agent.py | 129 + .../app/agents/orders_agent.py | 129 + .../app/agents/product_agent.py | 129 + .../app/agents/prompting.py | 15 + .../app/agents/runtime.py | 137 + .../app/agents/support_agent.py | 129 + .../app/examples/__init__.py | 1 + .../app/examples/grl_examples.py | 37 + .../app/examples/ic_examples.py | 34 + .../app/examples/mcp_examples.py | 43 + .../app/examples/noc_examples.py | 37 + .../app/examples/observer_examples.py | 28 + .../agent_template_backend/app/main.py | 552 +++ .../app/mcp_gateway_client_factory.py | 16 + .../app/observability/__init__.py | 0 .../app/observability/telemetry_observer.py | 84 + .../agent_template_backend/app/state.py | 53 + .../app/workflow_actions/__init__.py | 1 + .../app/workflow_actions/devolucao.py | 13 + .../app/workflows/agent_graph.py | 887 ++++ .../agent_template_backend/config/agents.yaml | 33 + .../agents/retail_orders/guardrails.yaml | 8 + .../config/agents/retail_orders/judges.yaml | 7 + .../agents/retail_orders/prompt_policy.yaml | 6 + .../agents/telecom_contas/guardrails.yaml | 8 + .../config/agents/telecom_contas/judges.yaml | 20 + .../agents/telecom_contas/prompt_policy.yaml | 6 + .../config/guardrails.yaml | 12 + .../config/identity.yaml | 55 + .../agent_template_backend/config/judges.yaml | 18 + .../config/mcp_parameter_mapping.yaml | 92 + .../config/mcp_servers.docker.yaml | 12 + .../config/mcp_servers.yaml | 30 + .../config/prompt_policy.yaml | 19 + .../config/routing.yaml | 128 + .../config/tool_policies.yaml | 26 + .../agent_template_backend/config/tools.yaml | 101 + ...AO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md | 95 + .../docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md | 45 + .../CONVERSATION_SUMMARY_MEMORY_BACKEND.md | 48 + .../docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md | 14 + .../docs/FRAMEWORK_CHANNEL_INPUT_MODE.md | 84 + .../docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md | 127 + ...EMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md | 42 + .../LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md | 5 + .../docs/TESTE_LONG_TERM_MEMORY.md | 82 + .../docs/VALIDACAO_BACKEND_IC_NOC_GRL.md | 62 + .../docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt | 3 + .../agent_template_backend/llm_profiles.yaml | 80 + .../agent_template_backend/requirements.txt | 23 + .../scripts/test_long_term_memory.py | 29 + .../test_transactional_workflow_template.py | 42 + .../workflows/devolucao_pedido.active.yaml | 1 + .../workflows/devolucao_pedido.v1.yaml | 27 + Tuning-Performance/README.md | 8 + docs/ADR_TRANSACTIONAL_WORKFLOW_ENGINE.md | 17 + .../docs/TRANSACTIONAL_WORKFLOWS_PT.md | 84 + .../__pycache__/__init__.cpython-313.pyc | Bin 253 -> 266 bytes .../gateway_policy_context.cpython-313.pyc | Bin 1433 -> 1433 bytes .../__pycache__/observer.cpython-313.pyc | Bin 13256 -> 13256 bytes ...untime_mcp_gateway_adapter.cpython-313.pyc | Bin 2130 -> 2130 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 489 -> 489 bytes .../composite_publisher.cpython-313.pyc | Bin 2444 -> 2444 bytes .../__pycache__/event_builder.cpython-313.pyc | Bin 1202 -> 1202 bytes .../__pycache__/factory.cpython-313.pyc | Bin 3664 -> 3664 bytes .../__pycache__/publisher.cpython-313.pyc | Bin 1814 -> 1814 bytes .../tim_payload_mapper.cpython-313.pyc | Bin 8393 -> 8393 bytes .../__pycache__/tim_sequence.cpython-313.pyc | Bin 15277 -> 15277 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 456 -> 456 bytes .../__pycache__/kafka.cpython-313.pyc | Bin 1754 -> 1754 bytes .../__pycache__/langfuse.cpython-313.pyc | Bin 22279 -> 22279 bytes .../__pycache__/oci_streaming.cpython-313.pyc | Bin 1511 -> 1511 bytes .../__pycache__/pubsub.cpython-313.pyc | Bin 7382 -> 7382 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 345 -> 345 bytes .../usage_repository.cpython-313.pyc | Bin 15628 -> 15628 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 155 -> 155 bytes .../cache/__pycache__/cache.cpython-313.pyc | Bin 16465 -> 16465 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 158 -> 158 bytes .../__pycache__/adapters.cpython-313.pyc | Bin 4114 -> 4114 bytes .../channels/__pycache__/base.cpython-313.pyc | Bin 1738 -> 1738 bytes .../__pycache__/gateway.cpython-313.pyc | Bin 5713 -> 5713 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 865 -> 865 bytes .../checkpoint_repository.cpython-313.pyc | Bin 27072 -> 27072 bytes .../langgraph_saver.cpython-313.pyc | Bin 11186 -> 11186 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 182 -> 195 bytes .../agent_registry.cpython-313.pyc | Bin 6889 -> 6889 bytes .../__pycache__/settings.cpython-313.pyc | Bin 12092 -> 12231 bytes .../src/agent_framework/config/settings.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 156 -> 156 bytes .../__pycache__/oci_streaming.cpython-313.pyc | Bin 3260 -> 3260 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 274 -> 287 bytes .../mcp_gateway_client.cpython-313.pyc | Bin 3300 -> 3313 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 662 -> 662 bytes .../__pycache__/client.cpython-313.pyc | Bin 4552 -> 4552 bytes .../__pycache__/config.cpython-313.pyc | Bin 4987 -> 4987 bytes .../__pycache__/models.cpython-313.pyc | Bin 4480 -> 4480 bytes .../__pycache__/router.cpython-313.pyc | Bin 14761 -> 14761 bytes .../__pycache__/session_store.cpython-313.pyc | Bin 3471 -> 3471 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 1443 -> 1443 bytes .../__pycache__/base.cpython-313.pyc | Bin 1317 -> 1317 bytes .../__pycache__/config_loader.cpython-313.pyc | Bin 6891 -> 6891 bytes .../__pycache__/custom_rails.cpython-313.pyc | Bin 4338 -> 4338 bytes .../__pycache__/executor.cpython-313.pyc | Bin 295 -> 295 bytes .../framework_llm_client.cpython-313.pyc | Bin 16000 -> 16000 bytes .../langgraph_adapters.cpython-313.pyc | Bin 3763 -> 3763 bytes .../__pycache__/llm_rails.cpython-313.pyc | Bin 6584 -> 6584 bytes .../output_supervisor.cpython-313.pyc | Bin 16045 -> 16045 bytes .../parallel_executor.cpython-313.pyc | Bin 19853 -> 19853 bytes .../__pycache__/pipeline.cpython-313.pyc | Bin 10831 -> 10831 bytes .../__pycache__/rail_action.cpython-313.pyc | Bin 596 -> 596 bytes .../__pycache__/rail_decision.cpython-313.pyc | Bin 1547 -> 1547 bytes .../__pycache__/rail_result.cpython-313.pyc | Bin 946 -> 946 bytes .../__pycache__/rails.cpython-313.pyc | Bin 29583 -> 29583 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 2927 -> 2927 bytes .../__pycache__/_compat.cpython-313.pyc | Bin 2387 -> 2387 bytes .../__pycache__/config.cpython-313.pyc | Bin 7579 -> 7579 bytes .../contestation_validation.cpython-313.pyc | Bin 21084 -> 21084 bytes .../__pycache__/contracts.cpython-313.pyc | Bin 6831 -> 6831 bytes .../__pycache__/input_size.cpython-313.pyc | Bin 3488 -> 3488 bytes .../__pycache__/llm_adapter.cpython-313.pyc | Bin 3723 -> 3723 bytes .../__pycache__/llm_client.cpython-313.pyc | Bin 12872 -> 12872 bytes .../__pycache__/llm_rails.cpython-313.pyc | Bin 8823 -> 8823 bytes .../output_sanitization.cpython-313.pyc | Bin 12202 -> 12202 bytes .../__pycache__/pipeline.cpython-313.pyc | Bin 12593 -> 12593 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 415 -> 415 bytes .../__pycache__/_context.cpython-313.pyc | Bin 6803 -> 6803 bytes .../ausencia_oferta_proativa.cpython-313.pyc | Bin 10682 -> 10682 bytes .../__pycache__/dlex_in.cpython-313.pyc | Bin 1246 -> 1246 bytes .../__pycache__/dlex_out.cpython-313.pyc | Bin 1533 -> 1533 bytes .../__pycache__/fallback.cpython-313.pyc | Bin 14612 -> 14612 bytes .../__pycache__/out_of_scope.cpython-313.pyc | Bin 17572 -> 17572 bytes .../prompts/__pycache__/pinj.cpython-313.pyc | Bin 9764 -> 9764 bytes .../__pycache__/ragsec.cpython-313.pyc | Bin 1047 -> 1047 bytes .../__pycache__/revprec.cpython-313.pyc | Bin 10328 -> 10328 bytes .../__pycache__/safe_out.cpython-313.pyc | Bin 984 -> 984 bytes .../prompts/__pycache__/tox.cpython-313.pyc | Bin 571 -> 571 bytes .../toxicidade_output.cpython-313.pyc | Bin 865 -> 865 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 581 -> 581 bytes .../supervision_template.cpython-313.pyc | Bin 2160 -> 2160 bytes .../__pycache__/tts_rules.cpython-313.pyc | Bin 1115 -> 1115 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 1609 -> 1609 bytes .../rails/__pycache__/alcada.cpython-313.pyc | Bin 4932 -> 4932 bytes .../rails/__pycache__/anatel.cpython-313.pyc | Bin 9259 -> 9259 bytes .../__pycache__/confirmation.cpython-313.pyc | Bin 10949 -> 10949 bytes .../rails/__pycache__/dlex_in.cpython-313.pyc | Bin 3221 -> 3221 bytes .../__pycache__/dlex_out.cpython-313.pyc | Bin 3273 -> 3273 bytes .../rails/__pycache__/ragsec.cpython-313.pyc | Bin 5355 -> 5355 bytes .../rails/__pycache__/revprec.cpython-313.pyc | Bin 5363 -> 5363 bytes .../rails/__pycache__/tox.cpython-313.pyc | Bin 8178 -> 8178 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 5685 -> 5685 bytes .../correspondencia_item.cpython-313.pyc | Bin 9012 -> 9012 bytes .../__pycache__/groundedness.cpython-313.pyc | Bin 8512 -> 8512 bytes .../intencao_cancelar.cpython-313.pyc | Bin 9170 -> 9170 bytes .../quantidade_coerente.cpython-313.pyc | Bin 8954 -> 8954 bytes .../servico_correto.cpython-313.pyc | Bin 8662 -> 8662 bytes .../verbalizacao_prematura.cpython-313.pyc | Bin 8497 -> 8497 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 479 -> 479 bytes .../rules/__pycache__/alcada.cpython-313.pyc | Bin 1823 -> 1823 bytes .../__pycache__/oos_blocklist.cpython-313.pyc | Bin 3002 -> 3002 bytes .../__pycache__/pinj_patterns.cpython-313.pyc | Bin 3664 -> 3664 bytes .../__pycache__/tox_blocklist.cpython-313.pyc | Bin 1448 -> 1448 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 376 -> 389 bytes .../__pycache__/mcp_mapper.cpython-313.pyc | Bin 4660 -> 4673 bytes .../__pycache__/models.cpython-313.pyc | Bin 3112 -> 3125 bytes .../__pycache__/resolver.cpython-313.pyc | Bin 4681 -> 4694 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 560 -> 560 bytes .../judges/__pycache__/judge.cpython-313.pyc | Bin 36374 -> 36374 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 167 -> 167 bytes .../__pycache__/_compat.cpython-313.pyc | Bin 2207 -> 2207 bytes .../__pycache__/llm_client.cpython-313.pyc | Bin 6044 -> 6044 bytes .../__pycache__/models.cpython-313.pyc | Bin 880 -> 880 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 175 -> 175 bytes .../prompts/__pycache__/aluc.cpython-313.pyc | Bin 6390 -> 6390 bytes .../prompts/__pycache__/csi.cpython-313.pyc | Bin 1259 -> 1259 bytes .../__pycache__/fallback.cpython-313.pyc | Bin 7946 -> 7946 bytes .../prompts/__pycache__/rqlt.cpython-313.pyc | Bin 1097 -> 1097 bytes .../prompts/__pycache__/vctn.cpython-313.pyc | Bin 568 -> 568 bytes .../llm/__pycache__/__init__.cpython-313.pyc | Bin 153 -> 153 bytes .../llm/__pycache__/base.cpython-313.pyc | Bin 761 -> 761 bytes .../profile_resolver.cpython-313.pyc | Bin 9221 -> 9221 bytes .../llm/__pycache__/providers.cpython-313.pyc | Bin 33718 -> 33718 bytes .../mcp/__pycache__/__init__.cpython-313.pyc | Bin 441 -> 454 bytes .../mcp/__pycache__/client.cpython-313.pyc | Bin 18394 -> 18407 bytes .../mcp/__pycache__/models.cpython-313.pyc | Bin 2050 -> 2063 bytes .../mcp/__pycache__/registry.cpython-313.pyc | Bin 4664 -> 4677 bytes .../__pycache__/tool_policy.cpython-313.pyc | Bin 3860 -> 4688 bytes .../__pycache__/tool_router.cpython-313.pyc | Bin 13784 -> 13999 bytes .../src/agent_framework/mcp/tool_policy.py | 13 +- .../src/agent_framework/mcp/tool_router.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 1103 -> 1077 bytes .../long_term_extractor.cpython-313.pyc | Bin 2455 -> 2455 bytes .../long_term_memory.cpython-313.pyc | Bin 6781 -> 6781 bytes .../long_term_models.cpython-313.pyc | Bin 1690 -> 1690 bytes .../long_term_store.cpython-313.pyc | Bin 29128 -> 29128 bytes .../message_history.cpython-313.pyc | Bin 8327 -> 8301 bytes .../summary_memory.cpython-313.pyc | Bin 12478 -> 12452 bytes .../__pycache__/summary_store.cpython-313.pyc | Bin 9488 -> 9462 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 182 -> 156 bytes .../__pycache__/identity.cpython-313.pyc | Bin 3407 -> 3407 bytes .../__pycache__/session.cpython-313.pyc | Bin 2676 -> 2650 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 1336 -> 1336 bytes .../__pycache__/context.cpython-313.pyc | Bin 7328 -> 7328 bytes .../control_events.cpython-313.pyc | Bin 2016 -> 2016 bytes .../__pycache__/decorators.cpython-313.pyc | Bin 1727 -> 1727 bytes .../__pycache__/event_bus.cpython-313.pyc | Bin 3574 -> 3574 bytes .../__pycache__/grl_events.cpython-313.pyc | Bin 437 -> 437 bytes .../guardrail_events.cpython-313.pyc | Bin 1992 -> 1992 bytes .../__pycache__/ic_events.cpython-313.pyc | Bin 567 -> 567 bytes .../informational_events.cpython-313.pyc | Bin 407 -> 407 bytes .../__pycache__/judge_events.cpython-313.pyc | Bin 1352 -> 1352 bytes .../langfuse_enterprise.cpython-313.pyc | Bin 3716 -> 3716 bytes .../langgraph_telemetry.cpython-313.pyc | Bin 4905 -> 4905 bytes .../__pycache__/llm_advisors.cpython-313.pyc | Bin 2879 -> 2879 bytes .../__pycache__/noc_contract.cpython-313.pyc | Bin 5016 -> 5016 bytes .../__pycache__/noc_events.cpython-313.pyc | Bin 548 -> 548 bytes .../__pycache__/noc_otel.cpython-313.pyc | Bin 6263 -> 6263 bytes .../__pycache__/observer.cpython-313.pyc | Bin 5570 -> 5570 bytes .../__pycache__/otel.cpython-313.pyc | Bin 3353 -> 3353 bytes .../streaming_events.cpython-313.pyc | Bin 1776 -> 1776 bytes .../streaming_exporter.cpython-313.pyc | Bin 1295 -> 1295 bytes .../__pycache__/telemetry.cpython-313.pyc | Bin 50530 -> 50530 bytes .../tim_backoffice_contract.cpython-313.pyc | Bin 1736 -> 1736 bytes .../__pycache__/token_cost.cpython-313.pyc | Bin 8538 -> 8538 bytes .../workflow_events.cpython-313.pyc | Bin 3271 -> 3271 bytes .../oci/__pycache__/__init__.cpython-313.pyc | Bin 153 -> 153 bytes .../oci/__pycache__/auth.cpython-313.pyc | Bin 3185 -> 3185 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 187 -> 161 bytes .../__pycache__/mongodb_store.cpython-313.pyc | Bin 7621 -> 7621 bytes .../__pycache__/oracle_store.cpython-313.pyc | Bin 44897 -> 44897 bytes .../__pycache__/sqlite_store.cpython-313.pyc | Bin 14986 -> 14960 bytes .../rag/__pycache__/__init__.cpython-313.pyc | Bin 651 -> 651 bytes .../embedding_provider.cpython-313.pyc | Bin 6468 -> 6468 bytes .../__pycache__/graph_store.cpython-313.pyc | Bin 5469 -> 5469 bytes .../rag/__pycache__/ingest.cpython-313.pyc | Bin 12522 -> 12522 bytes .../__pycache__/rag_service.cpython-313.pyc | Bin 9991 -> 9991 bytes .../__pycache__/vector_store.cpython-313.pyc | Bin 18597 -> 18597 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 162 -> 162 bytes .../session_repository.cpython-313.pyc | Bin 8531 -> 8531 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 393 -> 367 bytes .../__pycache__/config_loader.cpython-313.pyc | Bin 2264 -> 2238 bytes .../__pycache__/continuity.cpython-313.pyc | Bin 13206 -> 13180 bytes .../enterprise_router.cpython-313.pyc | Bin 14250 -> 14224 bytes .../__pycache__/models.cpython-313.pyc | Bin 2414 -> 2388 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 316 -> 290 bytes .../__pycache__/agent_runtime.cpython-313.pyc | Bin 82290 -> 82264 bytes .../sse/__pycache__/__init__.cpython-313.pyc | Bin 153 -> 153 bytes .../sse/__pycache__/events.cpython-313.pyc | Bin 9529 -> 9529 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 160 -> 160 bytes .../router_supervisor.cpython-313.pyc | Bin 353 -> 353 bytes .../__pycache__/supervisor.cpython-313.pyc | Bin 4630 -> 4630 bytes .../src/agent_framework/workflows/__init__.py | 11 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 688 bytes .../__pycache__/models.cpython-313.pyc | Bin 0 -> 3636 bytes .../__pycache__/registry.cpython-313.pyc | Bin 0 -> 2342 bytes .../__pycache__/repository.cpython-313.pyc | Bin 0 -> 2695 bytes .../__pycache__/runtime.cpython-313.pyc | Bin 0 -> 9220 bytes .../__pycache__/tool_executor.cpython-313.pyc | Bin 0 -> 1908 bytes .../src/agent_framework/workflows/models.py | 52 + .../src/agent_framework/workflows/registry.py | 32 + .../agent_framework/workflows/repository.py | 34 + .../src/agent_framework/workflows/runtime.py | 134 + .../workflows/tool_executor.py | 33 + templates/agent_template_backend/README.md | 6 + .../app/__pycache__/__init__.cpython-313.pyc | Bin 185 -> 145 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 32264 -> 32297 bytes ...mcp_gateway_client_factory.cpython-313.pyc | Bin 993 -> 993 bytes .../app/__pycache__/state.cpython-313.pyc | Bin 2493 -> 2526 bytes .../__pycache__/billing_agent.cpython-313.pyc | Bin 5256 -> 5216 bytes .../__pycache__/orders_agent.cpython-313.pyc | Bin 5164 -> 5124 bytes .../__pycache__/product_agent.cpython-313.pyc | Bin 5280 -> 5240 bytes .../__pycache__/prompting.cpython-313.pyc | Bin 1182 -> 1142 bytes .../__pycache__/runtime.cpython-313.pyc | Bin 384 -> 344 bytes .../__pycache__/support_agent.cpython-313.pyc | Bin 5176 -> 5136 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 216 -> 216 bytes .../__pycache__/grl_examples.cpython-313.pyc | Bin 1798 -> 1798 bytes .../__pycache__/ic_examples.cpython-313.pyc | Bin 1754 -> 1754 bytes .../__pycache__/mcp_examples.cpython-313.pyc | Bin 1896 -> 1896 bytes .../__pycache__/noc_examples.cpython-313.pyc | Bin 1843 -> 1843 bytes .../observer_examples.cpython-313.pyc | Bin 1352 -> 1352 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 199 -> 159 bytes .../telemetry_observer.cpython-313.pyc | Bin 4810 -> 4770 bytes .../app/workflow_actions/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 199 bytes .../__pycache__/devolucao.cpython-313.pyc | Bin 0 -> 977 bytes .../app/workflow_actions/devolucao.py | 13 + .../__pycache__/agent_graph.cpython-313.pyc | Bin 54189 -> 54222 bytes .../config/tool_policies.yaml | 5 + .../workflows/devolucao_pedido.active.yaml | 1 + .../workflows/devolucao_pedido.v1.yaml | 27 + .../conftest.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 788 bytes ...tool_policies.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 13734 bytes ...nal_workflows.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 8874 bytes ...st_transactional_workflows.cpython-313.pyc | Bin 0 -> 3962 bytes tests/unit/test_tool_policies.py | 14 + tests/unit/test_transactional_workflows.py | 76 + 303 files changed, 9063 insertions(+), 1 deletion(-) create mode 100644 IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/README.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/.env create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/Dockerfile create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README_ENTERPRISE_TEMPLATE.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/__init__.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/README.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/billing_agent.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/orders_agent.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/product_agent.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/prompting.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/runtime.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/support_agent.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/__init__.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/grl_examples.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/ic_examples.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/mcp_examples.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/noc_examples.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/observer_examples.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/main.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/mcp_gateway_client_factory.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/__init__.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/telemetry_observer.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/state.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/__init__.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/devolucao.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/guardrails.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/judges.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/guardrails.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/judges.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/guardrails.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/identity.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/judges.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_parameter_mapping.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.docker.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/prompt_policy.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/routing.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tool_policies.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tools.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/llm_profiles.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/requirements.txt create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/scripts/test_long_term_memory.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/tests/test_transactional_workflow_template.py create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.active.yaml create mode 100644 Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.v1.yaml create mode 100644 Tuning-Performance/README.md create mode 100644 docs/ADR_TRANSACTIONAL_WORKFLOW_ENGINE.md create mode 100644 libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md create mode 100644 libs/agent_framework/src/agent_framework/workflows/__init__.py create mode 100644 libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/workflows/models.py create mode 100644 libs/agent_framework/src/agent_framework/workflows/registry.py create mode 100644 libs/agent_framework/src/agent_framework/workflows/repository.py create mode 100644 libs/agent_framework/src/agent_framework/workflows/runtime.py create mode 100644 libs/agent_framework/src/agent_framework/workflows/tool_executor.py create mode 100644 templates/agent_template_backend/app/workflow_actions/__init__.py create mode 100644 templates/agent_template_backend/app/workflow_actions/__pycache__/__init__.cpython-313.pyc create mode 100644 templates/agent_template_backend/app/workflow_actions/__pycache__/devolucao.cpython-313.pyc create mode 100644 templates/agent_template_backend/app/workflow_actions/devolucao.py create mode 100644 templates/agent_template_backend/workflows/devolucao_pedido.active.yaml create mode 100644 templates/agent_template_backend/workflows/devolucao_pedido.v1.yaml create mode 100644 tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/unit/__pycache__/test_tool_policies.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/unit/__pycache__/test_transactional_workflows.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/unit/__pycache__/test_transactional_workflows.cpython-313.pyc create mode 100644 tests/unit/test_transactional_workflows.py diff --git a/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md b/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md new file mode 100644 index 0000000..79d8405 --- /dev/null +++ b/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md @@ -0,0 +1,64 @@ +# Implementação — workflows transacionais determinísticos + +## Entrega + +Foi adicionada ao `agent_framework_oci` uma capacidade opcional para executar transações multi-etapas como workflows determinísticos compilados em LangGraph. + +### Módulo novo + +`libs/agent_framework/src/agent_framework/workflows/` + +- `models.py`: contratos Pydantic e validação estrutural; +- `repository.py`: resolução de versão ativa e leitura de YAML imutável; +- `registry.py`: registro desacoplado de actions sync/async; +- `runtime.py`: compilação, cache e execução do StateGraph; +- `tool_executor.py`: integração com a política da tool; +- `__init__.py`: API pública. + +### Política expandida + +`ToolPolicy` agora aceita: + +```yaml +execution: + mode: direct_tool | workflow | agent + workflow: nome_do_workflow + version: active | 1 +``` + +O default permanece `direct_tool`, preservando compatibilidade. + +### Configuração + +Foram adicionados: + +- `ENABLE_TRANSACTIONAL_WORKFLOWS=false`; +- `WORKFLOWS_PATH=./workflows`. + +### Template + +Inclui um exemplo completo de devolução de pedido com: + +- confirmação e campos obrigatórios pela política; +- workflow YAML versionado; +- actions de domínio no backend; +- bifurcação determinística baseada no resultado da validação. + +## Validação realizada + +- `tests/unit/test_tool_policies.py`: 4 testes aprovados; +- compilação Python de framework, template e novos testes: aprovada; +- o teste funcional novo do LangGraph foi criado, mas não pôde ser executado neste container porque `langgraph` não está instalado no ambiente. A dependência já está declarada no `pyproject.toml` do framework. + +## Escopo e segurança + +Esta entrega cria o motor e a integração de política. Para operações críticas em produção ainda é necessário conectar: + +- execution store persistente; +- idempotência de negócio nas actions/APIs; +- autorização por escopo; +- telemetria IC/NOC específica de workflow; +- compensação/Saga quando aplicável; +- estratégia corporativa de timeout e retry. + +Esses itens foram explicitamente documentados para evitar a falsa impressão de que retry por si só garante segurança transacional. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/README.md b/Tuning-Performance/Deterministic_Transactional_Workflow/README.md new file mode 100644 index 0000000..f20daf8 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/README.md @@ -0,0 +1,88 @@ +# Deterministic Transactional Workflow + +Esta variante contém um `agent_template_backend` funcional que conecta o fluxo +conversacional do framework ao motor determinístico de workflows transacionais. + +## Por que este nome + +`Deterministic_Transactional_Workflow` é mais preciso que apenas +`Transactional_Workflow`: a confirmação transacional já existia. O diferencial +desta variante é executar uma sequência multi-etapas por um grafo determinístico, +em vez de deixar o LLM escolher cada etapa. + +## Fluxo demonstrado + +1. O router seleciona `orders_agent`. +2. O runtime coleta `order_id` e `reason` por clarification. +3. O framework solicita confirmação. +4. Após uma confirmação explícita, a policy de `solicitar_devolucao` seleciona + `execution.mode: workflow`. +5. O `WorkflowToolExecutor` carrega `devolucao_pedido.active.yaml`. +6. O LangGraph executa `validar_pedido` e `registrar_devolucao`. +7. O agente responde com protocolo e `workflow_execution_id`. + +## Executar + +```bash +cd Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend +pip install -e ../../../libs/agent_framework +pip install -r requirements.txt +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +## Frases de teste + +```text +Quero devolver o pedido 123 porque me arrependi da compra. +``` + +Depois: + +```text +Sim, confirmo. +``` + +Resultado esperado: protocolo `DEV-123`, status `REQUESTED` e um +`workflow_execution_id`. + +Clarification em turnos separados: + +```text +Quero devolver uma compra. +O pedido é o 123. +Eu me arrependi da compra. +Sim, confirmo. +``` + +Cancelamento: + +```text +Quero devolver o pedido 123 porque veio diferente do anunciado. +Não, cancele. +``` + +Nesse caso o workflow não deve iniciar. + +## Configuração principal + +- `.env`: `ENABLE_TRANSACTIONAL_WORKFLOWS=true` +- `.env`: `WORKFLOWS_PATH=./workflows` +- `config/tool_policies.yaml`: associa `solicitar_devolucao` ao workflow. +- `workflows/devolucao_pedido.active.yaml`: define a versão ativa. +- `app/workflow_actions/devolucao.py`: contém as actions do domínio. +- `app/agents/runtime.py`: integração do executor com o fluxo conversacional. + +## Evidências + +Procure os eventos: + +- `IC.TRANSACTIONAL_WORKFLOW_STARTED` +- `IC.TRANSACTIONAL_WORKFLOW_COMPLETED` +- `IC.TRANSACTIONAL_WORKFLOW_FAILED` + +O resultado também contém: + +- `execution_mode=workflow` +- `workflow_name` +- `workflow_version` +- `workflow_execution_id` diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/.env b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/.env new file mode 100644 index 0000000..a4f9224 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/.env @@ -0,0 +1,209 @@ +############################################################################### +# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA +# Este arquivo é lido por Pydantic Settings no framework e no backend template. +############################################################################### + +APP_NAME=ai-agent-template +APP_ENV=local +LOG_LEVEL=INFO +API_HOST=0.0.0.0 +API_PORT=8000 +CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +############################################################################### +# LLM - OCI Generative AI como provider principal +############################################################################### +# Opções: mock, oci_openai, oci_sdk, openai_compatible +LLM_PROVIDER=oci_sdk +LLM_TEMPERATURE=0.2 +LLM_MAX_TOKENS=2048 +LLM_TIMEOUT_SECONDS=120 + +# OCI OpenAI-compatible endpoint +OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com +OCI_GENAI_MODEL=openai.gpt-4.1 +OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS +OCI_GENAI_PROJECT_OCID= + +#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com +#OCI_GENAI_MODEL=openai.gpt-4.1 +#OCI_GENAI_API_KEY= +#OCI_GENAI_PROJECT_OCID= + + +# OCI_AUTH_MODE=config_file|instance_principal|resource_principal +OCI_AUTH_MODE=config_file +# OCI SDK / signer / profiles +OCI_CONFIG_FILE=~/.oci/config +OCI_PROFILE=LATINOAMERICA-Chicago +OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q +OCI_REGION=us-chicago-1 + +############################################################################### +# Persistência +############################################################################### +# Opções: memory, autonomous, mongodb +SESSION_REPOSITORY_PROVIDER=autonomous +MEMORY_REPOSITORY_PROVIDER=autonomous +CHECKPOINT_REPOSITORY_PROVIDER=autonomous + +# Autonomous Database +ADB_USER=admin +ADB_PASSWORD=Moniquinha19721972 +ADB_DSN=oradb23ai_high +ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai +ADB_WALLET_PASSWORD=Moniquinha1972 +ADB_TABLE_PREFIX=AGENTFW + +# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente +MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017 +MONGODB_DATABASE=agent_platform + +# Redis +REDIS_URL=redis://localhost:6379/0 +ENABLE_REDIS_CACHE=false + +############################################################################### +# RAG / Vector / Graph +############################################################################### +VECTOR_STORE_PROVIDER=autonomous +GRAPH_STORE_PROVIDER=autonomous +RAG_TOP_K=5 +EMBEDDING_PROVIDER=oci +OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0 +RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json + +############################################################################### +# Observabilidade +############################################################################### +ENABLE_LANGFUSE=true + # Opcional: verbose, compact +LANGFUSE_TRACE_MODE=compact +# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow +LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC. +LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion +LANGFUSE_IGNORE_HEALTHCHECKS=true +LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics +LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312 +LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915 +LANGFUSE_HOST=http://localhost:3005 +ENABLE_OTEL=false +OTEL_EXPORTER_OTLP_ENDPOINT= +OTEL_SERVICE_NAME=ai-agent-template +ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true +ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false + +############################################################################### +# Analytics / Observer corporativo +############################################################################### +# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo. +ENABLE_ANALYTICS=false +# Providers aceitos: oci_streaming,pubsub,noop +ANALYTICS_PROVIDERS=oci_streaming +# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente. +AGENT_PUBSUB_TOPIC= +GCP_PUBSUB_TOPIC_PATH= +GCP_PROJECT_ID= +GCP_PUBSUB_TOPIC= +GCP_PUBSUB_TIMEOUT_SECONDS=30 +# Credencial GCP segue padrão Google: +# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json + +############################################################################### +# OCI Streaming +############################################################################### +ENABLE_OCI_STREAMING=false +OCI_STREAM_ENDPOINT= +OCI_STREAM_OCID= +OCI_STREAM_PARTITION_KEY=agent-events + +############################################################################### +# Guardrails, Judges, Supervisor +############################################################################### +ENABLE_INPUT_GUARDRAILS=true +ENABLE_OUTPUT_GUARDRAILS=true +ENABLE_JUDGES=true +ENABLE_SUPERVISOR=true +ENABLE_OUTPUT_SUPERVISOR=true +ENABLE_PARALLEL_GUARDRAILS=true +GUARDRAILS_FAIL_FAST=true +OUTPUT_SUPERVISOR_MAX_RETRIES=3 +GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml +JUDGES_CONFIG_PATH=./config/judges.yaml +PROMPT_POLICY_PATH=./config/prompt_policy.yaml + +############################################################################### +# Gateway de canais +############################################################################### +DEFAULT_CHANNEL=web +# embedded = backend may parse simple/native channel payloads. +# external = backend only accepts GatewayRequest normalized by an external Channel Gateway. +FRAMEWORK_CHANNEL_INPUT_MODE=embedded +ENABLE_VOICE_ADAPTER=true +ENABLE_WHATSAPP_ADAPTER=true +ENABLE_TEXT_ADAPTER=true + +################################################# +# ENTERPRISE ROUTING +################################################# +# Arquivo YAML com intents, keywords, políticas de estado e fallback. +ROUTING_CONFIG_PATH=./config/routing.yaml +# true = usa LLM para classificar quando keywords/estado não resolverem. +# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência. +ENABLE_LLM_ROUTER=true + +# Semantic route stickiness (optional). +# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE. +# There are no regexes or deterministic language rules. +ENABLE_ROUTE_STICKINESS=true +ROUTE_STICKINESS_LLM_PROFILE=route_continuity +ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90 +ROUTE_STICKINESS_HISTORY_TURNS=2 +ROUTE_STICKINESS_MAX_TOKENS=80 +HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa. +END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato. + +############################################################################### +# MCP / Tools +############################################################################### +ENABLE_MCP_TOOLS=true +MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml +TOOLS_CONFIG_PATH=./config/tools.yaml +MCP_TOOL_TIMEOUT_SECONDS=30 + +# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes +ROUTING_MODE=router + +# Usage/cost accounting +USAGE_REPOSITORY_PROVIDER=autonomous +IDENTITY_CONFIG_PATH=./config/identity.yaml +MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml + +# ----------------------------------------------------------------------------- +# ConversationSummaryMemory / compressão de contexto conversacional +# ----------------------------------------------------------------------------- +ENABLE_CONVERSATION_SUMMARY_MEMORY=true +MEMORY_CONTEXT_STRATEGY=summary +MEMORY_HISTORY_LIMIT=80 +MEMORY_RECENT_MESSAGES_LIMIT=8 +MEMORY_SUMMARY_TRIGGER_MESSAGES=20 +MEMORY_MAX_SUMMARY_CHARS=6000 +MEMORY_SUMMARY_USE_LLM=true +MEMORY_INJECT_RECENT_MESSAGES=true +MEMORY_INJECT_SUMMARY=true + +############################################################################### +# LONG-TERM MEMORY +############################################################################### +ENABLE_LONG_TERM_MEMORY=true +LONG_TERM_MEMORY_PROVIDER=sqlite +LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db +LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory +# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY +# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY +LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20 +LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70 +LONG_TERM_MEMORY_AUTO_EXTRACT=true +LONG_TERM_MEMORY_INJECT_CONTEXT=true +ENABLE_TRANSACTIONAL_WORKFLOWS=true +WORKFLOWS_PATH=./workflows diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/Dockerfile b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/Dockerfile new file mode 100644 index 0000000..273fe01 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-slim +WORKDIR /app +COPY agent_framework /agent_framework +COPY agent_template_backend /app +RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README.md new file mode 100644 index 0000000..8483e89 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README.md @@ -0,0 +1,4219 @@ +# Tutorial — Implementação de um Agente usando `agent_template_backend` + +Este tutorial ensina como implementar um novo agente a partir do `agent_template_backend`, usando o framework como motor corporativo de execução. + +A ideia central é simples: + +```text +Framework = motor reutilizável +Agente = regra de negócio específica +MCP Server = fronteira padronizada com sistemas externos +Config YAML = comportamento alterável sem recompilar código +IC/NOC/GRL = rastreabilidade de negócio, operação e governança +``` + +![img_1.png](img_1.png) + +O objetivo é que cada novo agente implemente apenas sua lógica de domínio — prompts, regras de negócio, ferramentas, schemas e nós específicos — sem recriar motores que já pertencem ao framework. + +--- + +## 1. Visão geral da arquitetura + +O template separa o que é genérico do que é específico. + +```text +agent_template_backend/ +├── app/ +│ ├── main.py # API FastAPI, gateway, sessão, SSE e entrada do workflow +│ ├── state.py # Contrato de estado compartilhado do LangGraph +│ ├── workflows/ +│ │ └── agent_graph.py # Workflow corporativo com router, guardrails, agentes, judges e persistência +│ ├── agents/ +│ │ ├── runtime.py # Recursos comuns para agentes: MCP, RAG, cache, IC, LLM +│ │ ├── billing_agent.py # Exemplo de agente de faturas +│ │ ├── product_agent.py # Exemplo de agente de produtos +│ │ ├── orders_agent.py # Exemplo de agente de pedidos +│ │ └── support_agent.py # Exemplo de agente de suporte +│ └── examples/ # Exemplos de IC, NOC, GRL, MCP e observer +├── config/ +│ ├── agents.yaml # Registro dos agentes disponíveis +│ ├── routing.yaml # Intents, keywords, fallback e decisão de rota +│ ├── tools.yaml # Catálogo das ferramentas disponíveis para o backend +│ ├── mcp_servers.yaml # Endpoints MCP locais +│ ├── mcp_servers.docker.yaml # Endpoints MCP em Docker Compose +│ ├── mcp_parameter_mapping.yaml # Mapeamento entre chaves canônicas e parâmetros das tools +│ ├── identity.yaml # Resolução de identidade de negócio +│ ├── guardrails.yaml # Guardrails globais +│ ├── judges.yaml # Judges globais +│ ├── prompt_policy.yaml # Política global de prompt +│ └── agents// # Configurações isoladas por agente +├── data/ +│ └── agent_framework.db # Banco local de exemplo, quando aplicável +├── Dockerfile +├── requirements.txt +└── .env # Configuração local +``` + +### 1.1. O que pertence ao framework + +O framework deve concentrar os motores reutilizáveis: + +- LangGraph e montagem do workflow. +- Checkpoint. +- Memória. +- Session repository. +- Channel gateway. +- Enterprise Router. +- Supervisor. +- Guardrails. +- Output Supervisor. +- Judges. +- Telemetria Langfuse/OpenTelemetry. +- Analytics IC/NOC/GRL. +- MCP Tool Router. +- Cache. +- RAG genérico. + +### 1.2. O que pertence ao agente + +O agente deve concentrar apenas customizações de domínio: + +- Prompts específicos. +- Regras de negócio. +- Schemas próprios. +- Tools específicas. +- Clients de sistemas externos, preferencialmente encapsulados atrás de MCP. +- Mapeamento de parâmetros. +- Nós especializados, se houver. +- ICs de negócio da jornada. + +Quando uma regra só faz sentido para um domínio, ela pertence ao agente. Quando uma capacidade deve ser usada por vários agentes, ela pertence ao framework. + +--- + +## 2. Fluxo de execução do template + +O fluxo principal começa em `app/main.py`, no endpoint `/gateway/message`. + +```text +Canal / Frontend / API + ↓ +POST /gateway/message + ↓ +ChannelGateway.normalize() + ↓ +IdentityResolver + ↓ +SessionRepository + ↓ +MemoryRepository + ↓ +AgentWorkflow.ainvoke() + ↓ +LangGraph + ↓ +Input Guardrails + ↓ +Enterprise Router ou Supervisor + ↓ +Agente especializado + ↓ +MCP Tool Router / RAG / Cache / LLM + ↓ +Output Supervisor + ↓ +Output Guardrails + ↓ +Judges + ↓ +Supervisor Review + ↓ +Persistência / Checkpoint / Memória + ↓ +Resposta +``` + +O `AgentWorkflow`, em `app/workflows/agent_graph.py`, normalmente já contém nós corporativos como: + +```text +input_guardrails +routing_decision +billing_agent +product_agent +orders_agent +support_agent +handoff +supervisor_agent +output_supervisor +output_guardrails +judge +supervisor_review +persist +``` + +Para criar um novo agente, normalmente você altera: + +```text +app/agents/.py +app/workflows/agent_graph.py +app/state.py, se precisar de campos novos +config/agents.yaml +config/routing.yaml +config/tools.yaml +config/mcp_servers.yaml +config/mcp_parameter_mapping.yaml +config/identity.yaml +config/agents//prompt_policy.yaml +config/agents//guardrails.yaml +config/agents//judges.yaml +.env +``` + +--- + +## 3. Pré-requisitos + +### 3.1. Requisitos locais + +- Python 3.12 ou 3.13. +- `pip` ou `uv`. +- Projeto `agent_framework` disponível no mesmo workspace, caso o template use instalação local. +- Servidores MCP, se o agente usar tools. +- Redis, Oracle Autonomous Database, MongoDB e Langfuse são opcionais conforme configuração. + +Estrutura recomendada: + +```text +workspace/ +├── agent_framework/ +└── agent_template_backend/ +``` + +### 3.2. Instalação local + +Dentro do diretório `agent_template_backend`: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Se o `agent_framework` estiver em desenvolvimento local: + +```bash +pip install -e ../agent_framework +``` + +Em Windows PowerShell: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +pip install -e ..\agent_framework +``` + +--- + +## 4. Configuração do `.env` + +O `.env` define quais motores serão ativados. Ele não é apenas um arquivo de propriedades: ele muda o comportamento do agente em tempo de execução. + +Exemplo seguro para desenvolvimento local: + +```env +APP_NAME=ai-agent-template +APP_ENV=local +LOG_LEVEL=INFO +API_HOST=0.0.0.0 +API_PORT=8000 +CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +LLM_PROVIDER=mock +LLM_TEMPERATURE=0.2 +LLM_MAX_TOKENS=2048 +LLM_TIMEOUT_SECONDS=120 + +SESSION_REPOSITORY_PROVIDER=memory +MEMORY_REPOSITORY_PROVIDER=memory +CHECKPOINT_REPOSITORY_PROVIDER=memory +USAGE_REPOSITORY_PROVIDER=memory + +ENABLE_REDIS_CACHE=false +REDIS_URL=redis://localhost:6379/0 +CACHE_TTL_SECONDS=300 + +VECTOR_STORE_PROVIDER=memory +GRAPH_STORE_PROVIDER=memory +RAG_TOP_K=5 +EMBEDDING_PROVIDER=mock + +ENABLE_LANGFUSE=false +LANGFUSE_HOST=http://localhost:3005 +ENABLE_OTEL=false +OTEL_SERVICE_NAME=ai-agent-template + +ENABLE_ANALYTICS=false +ANALYTICS_PROVIDERS=noop +ENABLE_OCI_STREAMING=false +OCI_STREAM_ENDPOINT= +OCI_STREAM_OCID= +OCI_STREAM_PARTITION_KEY=agent-events + +ENABLE_INPUT_GUARDRAILS=true +ENABLE_OUTPUT_GUARDRAILS=true +ENABLE_OUTPUT_SUPERVISOR=true +ENABLE_JUDGES=true +ENABLE_SUPERVISOR=true +ENABLE_PARALLEL_GUARDRAILS=true +GUARDRAILS_FAIL_FAST=true +OUTPUT_SUPERVISOR_MAX_RETRIES=3 +GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml +JUDGES_CONFIG_PATH=./config/judges.yaml +PROMPT_POLICY_PATH=./config/prompt_policy.yaml + +ROUTING_CONFIG_PATH=./config/routing.yaml +ROUTING_MODE=router +ENABLE_LLM_ROUTER=false + +ENABLE_MCP_TOOLS=true +MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml +TOOLS_CONFIG_PATH=./config/tools.yaml +MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml +MCP_TOOL_TIMEOUT_SECONDS=30 + +IDENTITY_CONFIG_PATH=./config/identity.yaml +``` + +### 4.1. Como raciocinar sobre o `.env` + +Antes de testar um novo agente, responda: + +```text +O LLM será mock ou real? +A memória será local ou banco? +O checkpoint precisa sobreviver a restart? +As tools MCP serão chamadas de verdade ou simuladas? +O roteamento será por regra/intent ou supervisor? +Guardrails, judges e supervisor devem bloquear, revisar ou só observar? +Langfuse/OTEL/Streaming serão usados neste ambiente? +``` + +Para um primeiro teste, use `LLM_PROVIDER=mock`, persistência em `memory` e MCP mock/local. Depois evolua para LLM real, banco, Langfuse e serviços reais. + +Para usar Oracle Autonomous Database, ajuste: + +```env +SESSION_REPOSITORY_PROVIDER=autonomous +MEMORY_REPOSITORY_PROVIDER=autonomous +CHECKPOINT_REPOSITORY_PROVIDER=autonomous +USAGE_REPOSITORY_PROVIDER=autonomous + +ADB_USER= +ADB_PASSWORD= +ADB_DSN= +ADB_WALLET_LOCATION= +ADB_WALLET_PASSWORD= +ADB_TABLE_PREFIX=AGENTFW +``` + +Para usar Langfuse: + +```env +ENABLE_LANGFUSE=true +LANGFUSE_PUBLIC_KEY= +LANGFUSE_SECRET_KEY= +LANGFUSE_HOST=http://localhost:3005 +``` + + +--- + +## 5. Criando um novo agente + +Neste exemplo, vamos criar um agente chamado `financeiro_agent` para atendimento financeiro genérico. + +### 5.1. Antes do código: o que é um agente neste framework? + +Um agente é uma classe de domínio que recebe o `state` do LangGraph, interpreta a intenção escolhida pelo roteador ou supervisor, coleta evidências, chama tools/RAG/LLM quando necessário e retorna uma decisão para o workflow continuar. + +Ele não deve decidir sozinho tudo que o framework já decide. Por exemplo: + +```text +O agente não cria sessão. +O agente não abre SSE. +O agente não compila LangGraph. +O agente não cria checkpoint. +O agente não executa guardrails globais. +O agente não chama sistema externo diretamente quando existe MCP Tool Router. +``` + +O agente deve responder perguntas como: + +```text +Qual problema de negócio estou resolvendo? +Quais dados preciso para responder com segurança? +Quais tools podem fornecer esses dados? +Quais regras de domínio impedem ou autorizam uma ação? +Qual resposta deve ser devolvida ao usuário? +Quais eventos IC preciso emitir para auditoria da jornada? +``` + +### 5.2. Responsabilidades do arquivo `app/agents/financeiro_agent.py` + +Esse arquivo deve conter a lógica específica do agente financeiro. Ele deve: + +1. Receber o `state`. +2. Separar `context`, `session`, `business_context` e `tool_arguments`. +3. Emitir IC de início usando `AgentRuntimeMixin`. +4. Coletar contexto de tools MCP, se houver, usando o MCP Tool Router do framework. +5. Coletar contexto RAG, se houver, usando o RAG genérico do framework. +6. Montar um prompt de domínio. +7. Chamar o LLM pelo runtime comum, com cache e telemetria. +8. Montar uma resposta padronizada. +9. Emitir IC de conclusão. +10. Retornar dados para o workflow. + + +### 5.2.1. Entendendo `state`, `context`, `session`, `business_context` e `tool_arguments` + +Antes de copiar o código do agente, o desenvolvedor precisa entender **de onde vêm os dados**. Em um agente corporativo, o erro mais comum é pegar qualquer campo diretamente do `state` sem saber se aquele dado veio do canal, do gateway, do identity resolver, do roteador ou do usuário. + +O `state` é o envelope completo da execução do LangGraph. Dentro dele normalmente existe um `context`, que é o contexto normalizado pelo framework. + +Dentro de `context`, se o projeto usa **Agent Gateway / Global Supervisor**, é comum existir também um bloco `session`: + +```python +ctx = state.get("context") or {} +session = ctx.get("session") or {} +``` + +O papel de cada bloco é diferente: + +```text +state + Estado completo do workflow atual. Carrega texto, intent, route, resposta parcial, + resultados MCP, dados de guardrail, checkpoint e outros campos técnicos. + +context + Contexto normalizado da mensagem atual. Normalmente vem do Channel Gateway, + Identity Resolver e Agent Gateway. + +session + Dados da sessão e do canal. Ajuda a saber quem está conversando, por qual canal, + em qual tenant, qual sessão global está ativa e qual backend/agente está atendendo. + +business_context + Dados de negócio já normalizados. Exemplo: customer_key, contract_key, + interaction_key, session_key, protocol_id, invoice_id, order_id. + +tool_arguments + Parâmetros explícitos já preparados para tools/MCP. Quando existe, deve ter + prioridade sobre inferências feitas pelo agente. +``` + +A ordem de confiança recomendada é: + +```text +1. tool_arguments explícitos +2. business_context resolvido pelo framework +3. context normalizado +4. session e session.metadata, quando vierem do Agent Gateway +5. state direto +6. texto original do usuário, apenas para extração complementar +``` + +Essa ordem evita dois problemas: + +```text +Problema 1: ignorar dados já resolvidos pelo Gateway/Identity Resolver. +Problema 2: sobrescrever um parâmetro canônico com um valor bruto e menos confiável. +``` + +Exemplo prático: se o `business_context.customer_key` já foi resolvido pelo framework, o agente não deve preferir um `user_id` genérico da sessão apenas porque ele existe. O `user_id` identifica o usuário no canal; o `customer_key` identifica o cliente no negócio. + +Mesmo que um agente simples não use `session` diretamente, existe uma diferença entre **sessão técnica** e **contexto de negócio**. + +### 5.2.2. Entendendo a classe `AgentRuntimeMixin` de `runtime.py` + +Antes de escrever um agente novo, o desenvolvedor precisa entender por que quase todos os exemplos herdam de: + +```python +from app.agents.runtime import AgentRuntimeMixin +``` + +O `AgentRuntimeMixin` é uma camada de conveniência operacional para o agente. Ele não é o agente, não é o workflow e não contém regra de negócio. Ele existe para evitar que cada agente tenha que reimplementar, de forma diferente, as mesmas capacidades técnicas. + +Em termos simples: + +```text +AgentRuntimeMixin = caixa de ferramentas padronizada do agente +FinanceiroAgent = regra de negócio que usa essa caixa de ferramentas +AgentWorkflow = motor LangGraph que chama o agente +Framework = infraestrutura corporativa completa +``` + +Sem o `AgentRuntimeMixin`, cada desenvolvedor tenderia a escrever código próprio para: + +```text +emitir IC/NOC/GRL +chamar MCP Tool Router +chamar RAG +montar cache de LLM +chamar LLM +montar chave de cache +tratar ausência de observer, cache, RAG ou tools +``` + +Isso geraria agentes inconsistentes. Um agente emitiria IC de um jeito, outro chamaria MCP diretamente, outro ignoraria cache, outro quebraria quando o observer estivesse desabilitado. O mixin evita esse problema. + +#### 5.2.2.1. O que o `AgentRuntimeMixin` oferece + +No template, o `AgentRuntimeMixin` concentra métodos utilitários como: + +| Método | Para que serve | Quando o agente usa | +|---|---|---| +| `_emit_ic()` | Emite evento de negócio/auditoria | início, fim, decisão de negócio, contexto coletado | +| `_emit_noc()` | Emite evento operacional | erro técnico, timeout, fallback, indisponibilidade | +| `_emit_grl()` | Emite evento de governança customizado | regra de domínio bloqueou ou sanitizou algo | +| `_retrieve_rag_context()` | Consulta o RAG genérico do framework | agente precisa de contexto documental | +| `_collect_mcp_context()` | Chama as tools MCP declaradas no `state.mcp_tools` | agente precisa consultar sistemas externos | +| `_cache_get()` | Lê cache genérico | uso avançado, normalmente indireto | +| `_cache_set()` | Grava cache genérico | uso avançado, normalmente indireto | +| `_llm_cache_key()` | Monta chave estável de cache do LLM | normalmente usado internamente | +| `_invoke_llm_cached()` | Chama o LLM com cache e telemetria | agente precisa gerar resposta com LLM | + +O desenvolvedor deve pensar assim: + +```text +Eu escrevo a regra de negócio no run(). +Quando precisar de infraestrutura, chamo um helper do AgentRuntimeMixin. +``` + +#### 5.2.2.2. O que o `AgentRuntimeMixin` não deve fazer + +O mixin não deve conter regra de negócio específica, por exemplo: + +```text +calcular contestação de fatura +consultar protocolo ANATEL diretamente +abrir SR Siebel diretamente +classificar cancelamento TIM +calcular valor de boleto financeiro +validar produto de varejo específico +``` + +Essas regras pertencem ao agente ou ao MCP Server do domínio. + +A fronteira correta é: + +```text +AgentRuntimeMixin + sabe chamar MCP, RAG, cache, LLM e observer + +Agente específico + sabe quais evidências precisa, quais regras aplicar e como responder + +MCP Server + sabe falar com sistema real, mock, banco, REST, SOAP ou serviço legado +``` + +#### 5.2.2.3. Como o mixin recebe seus recursos + +O `AgentRuntimeMixin` não cria `llm`, `tool_router`, `rag_service`, `cache` ou `observer`. Ele espera que o workflow injete esses objetos no construtor do agente. + +Por isso, no agente aparece este padrão: + +```python +class FinanceiroAgent(AgentRuntimeMixin): + name = "financeiro_agent" + + def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None): + self.llm = llm + self.telemetry = telemetry + self.tool_router = tool_router + self.rag_service = rag_service + self.cache = cache + self.settings = settings + self.observer = observer +``` + +Isso significa: + +```text +llm = motor de geração configurado pelo framework +telemetry = spans/eventos técnicos +tool_router = roteador MCP padronizado +rag_service = busca documental/grafo/vetor +cache = cache Redis/memory/etc. +settings = configurações carregadas do .env/YAML +observer = emissor IC/NOC/GRL +``` + +O agente recebe esses objetos prontos. Ele não deve criar uma nova instância por conta própria dentro do `run()`. + +#### 5.2.2.4. Como `_emit_ic()`, `_emit_noc()` e `_emit_grl()` ajudam + +Um agente precisa ser auditável, mas não deveria quebrar se a observabilidade estiver desligada. + +Por isso, os métodos de emissão do mixin são **fail-open**: se não houver `observer`, ou se ocorrer erro ao emitir evento, a jornada de negócio continua. + +Exemplo de IC: + +```python +await self._emit_ic( + "IC.FINANCEIRO_AGENT_STARTED", + state, + {"business_component": "financeiro"}, + component="agent.financeiro.start", +) +``` + +O desenvolvedor não precisa montar manualmente todos os metadados básicos. O mixin já tenta incluir informações como: + +```text +session_id +conversation_key +tenant_id +agent_id +route +intent +message_id +channel_id +``` + +A regra prática é: + +```text +Use _emit_ic() para marco de negócio. +Use _emit_noc() para problema operacional. +Use _emit_grl() para governança específica do domínio. +``` + +#### 5.2.2.5. Como `_collect_mcp_context()` funciona + +O método `_collect_mcp_context(state)` lê a lista de tools já escolhidas pelo roteador: + +```python + tools = state.get("mcp_tools") or [] +``` + +Depois chama o `tool_router` do framework para cada tool. O agente não precisa saber se a tool usa HTTP, Docker, mock ou serviço real. + +Fluxo conceitual: + +```text +routing.yaml escolhe intent + ↓ +intent define mcp_tools + ↓ +state.mcp_tools recebe a lista de tools + ↓ +AgentRuntimeMixin._collect_mcp_context() + ↓ +MCP Tool Router + ↓ +MCP Server + ↓ +resultado normalizado volta ao agente +``` + +Exemplo no agente: + +```python +tool_context = await self._collect_mcp_context(state) +``` + +O desenvolvedor deve usar esse método quando basta chamar as tools definidas pela intent. + +Se o agente precisar escolher argumentos especiais por tool, pular tools perigosas, exigir confirmação ou montar parâmetros adicionais, ele pode implementar um método próprio no agente e chamar o router de forma mais controlada, como no exemplo do `BackofficeAgent`. + +#### 5.2.2.6. Como `_retrieve_rag_context()` funciona + +O método `_retrieve_rag_context(state)` consulta o RAG genérico configurado no framework. + +Ele usa como texto base: + +```text +state.sanitized_input ou state.user_text +``` + +E tenta definir um namespace de busca a partir de: + +```text +agent_profile.rag_namespace +agent_id +route +default +``` + +Também pode usar informações do `business_context`, como `customer_key` ou `contract_key`, para enriquecer busca em grafo ou contexto relacionado. + +Exemplo: + +```python +rag_context, rag_metadata = await self._retrieve_rag_context(state) +``` + +O agente usa `rag_context` no prompt e pode retornar `rag_metadata` para auditoria/debug. + +Regra prática: + +```text +Use RAG quando a resposta depende de documento, política, base de conhecimento ou conteúdo não codificado. +Não use RAG para substituir uma consulta operacional que deve ser feita por tool MCP. +``` + +#### 5.2.2.7. Como `_invoke_llm_cached()` funciona + +O método `_invoke_llm_cached()` chama o LLM passando mensagens no formato chat: + +```python +answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) +``` + +Antes de chamar o LLM, ele monta uma chave de cache considerando elementos como: + +```text +nome do agente +tenant_id +agent_id +intent +customer_key +contract_key +interaction_key +texto do usuário +conteúdo do prompt +``` + +Se já existir resposta no cache, o método retorna o valor cacheado. Se não existir, chama o LLM, grava no cache e retorna a resposta. + +Isso evita que cada agente implemente cache de forma diferente. + +O desenvolvedor deve entender que o cache é útil para prompts determinísticos ou consultas repetidas, mas deve ser usado com cuidado em ações sensíveis. O agente não deve confirmar operação externa apenas porque uma resposta de LLM veio de cache. Confirmações operacionais devem depender de retorno real da tool. + +#### 5.2.2.8. Quando usar `_collect_mcp_context()` e quando criar lógica própria + +Use `_collect_mcp_context()` quando: + +```text +a intent já definiu as tools corretas +os parâmetros canônicos já estão no business_context +a execução pode chamar todas as tools da lista +nenhuma tool representa ação sensível +``` + +Crie lógica própria no agente quando: + +```text +uma tool só pode ser chamada após confirmação explícita +uma tool exige argumentos adicionais derivados da mensagem +uma tool deve ser pulada se faltar campo obrigatório +uma tool de registro/alteração não pode rodar automaticamente +uma sequência de tools depende do resultado anterior +``` + +Exemplo de regra segura: + +```python +if tool.startswith("registrar_") and not action_text: + return {"ok": False, "skipped": True, "reason": "ação sem confirmação explícita"} +``` + +Isso é regra de domínio e deve ficar no agente, não no mixin. + +#### 5.2.2.9. Como o dev deve ler o `run()` de um agente que herda o mixin + +Ao abrir um agente, o desenvolvedor deve procurar esta estrutura mental: + +```text +1. O agente emite IC de início? +2. Ele lê context/session/business_context de forma organizada? +3. Ele valida dados obrigatórios do domínio? +4. Ele chama MCP usando o mixin ou lógica própria controlada? +5. Ele chama RAG quando precisa de conhecimento documental? +6. Ele monta prompt com evidências, e não com chute? +7. Ele chama LLM via _invoke_llm_cached()? +8. Ele emite IC/NOC/GRL relevantes? +9. Ele retorna answer, next_state, mcp_results e metadados úteis? +``` + +Se o agente faz isso, ele está usando o framework corretamente. + +#### 5.2.2.10. Exemplo mínimo de uso correto do mixin + +```python +async def run(self, state): + await self._emit_ic("IC.FINANCEIRO_STARTED", state, component="agent.financeiro.start") + + ctx = state.get("context") or {} + business_context = ctx.get("business_context") or state.get("business_context") or {} + + if not business_context.get("customer_key"): + return { + "answer": "Informe o identificador do cliente para continuar.", + "next_state": "WAITING_CUSTOMER_KEY", + "mcp_results": [], + } + + mcp_results = await self._collect_mcp_context(state) + rag_context, rag_metadata = await self._retrieve_rag_context(state) + + messages = [ + {"role": "system", "content": "Você é um agente financeiro corporativo."}, + {"role": "user", "content": f"Evidências MCP: {mcp_results}\nContexto RAG: {rag_context}"}, + ] + + answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) + + await self._emit_ic("IC.FINANCEIRO_COMPLETED", state, {"mcp_count": len(mcp_results)}, component="agent.financeiro.completed") + + return { + "answer": answer, + "next_state": "FINANCEIRO_ACTIVE", + "mcp_results": mcp_results, + "rag_metadata": rag_metadata, + } +``` + +Esse exemplo mostra a intenção do mixin: o desenvolvedor escreve o raciocínio do agente, mas delega infraestrutura para métodos padronizados. + +#### 5.2.2.11. Erros comuns ao usar o `AgentRuntimeMixin` + +```text +Herdar de AgentRuntimeMixin, mas chamar REST diretamente dentro do agente. +Criar outro cache manual em vez de usar _invoke_llm_cached(). +Emitir eventos diretamente em formatos diferentes do observer. +Colocar regra de domínio dentro do runtime.py. +Usar _collect_mcp_context() para tool de ação sem confirmação. +Ignorar business_context e pegar parâmetros soltos do payload. +Tratar session_id global e backend_session_id como se fossem a mesma coisa. +Sobrescrever métodos internos do mixin sem necessidade. +``` + +A regra mais importante é: + +```text +O mixin padroniza capacidades técnicas. +O agente decide como aplicar essas capacidades ao domínio. +``` + + +### 5.2.3. Entendendo `messages`: arquitetura conversacional do agente + +Depois de entender `state`, `context`, `session`, `business_context`, `tool_arguments` e `AgentRuntimeMixin`, falta entender uma peça central: `messages`. + +Em um agente, `messages` não é apenas uma lista de textos. Ele é o **contrato conversacional** que será enviado ao LLM naquela chamada. É nesse contrato que o agente organiza instruções, pergunta do usuário, evidências, contexto RAG, resultados MCP, memória resumida e formato esperado da resposta. + +Um exemplo mínimo é: + +```python +messages = [ + { + "role": "system", + "content": "Você é um agente financeiro. Não invente dados.", + }, + { + "role": "user", + "content": "Quero consultar meu pagamento.", + }, +] +``` + +Esse formato é comum em frameworks e provedores modernos de IA conversacional. Ele aparece, com pequenas variações, em OpenAI Chat Completions/Responses API, OCI Generative AI OpenAI-compatible, LangChain `ChatModel`, LangGraph, Semantic Kernel, LlamaIndex e em arquiteturas com tool calling e MCP. + +A ideia é simples: + +```text +O agente monta uma conversa canônica. +O AgentRuntimeMixin chama o provider LLM padronizado. +O provider adapta essa conversa para o backend real. +``` + +Isso permite que o agente continue escrevendo `messages` de forma previsível, mesmo que por baixo o projeto use OCI Generative AI, OpenAI-compatible endpoint, LangChain, Llama local, mock ou outro provider. + +#### 5.2.3.1. Papéis principais de uma mensagem + +Cada item de `messages` possui pelo menos um `role` e um `content`. + +| Role | Para que serve | +|---|---| +| `system` | Define identidade, limites, políticas, regras e comportamento do agente. | +| `user` | Representa a solicitação atual do usuário ou uma instrução contextualizada pelo framework. | +| `assistant` | Representa respostas anteriores do modelo, quando o histórico é incluído explicitamente. | +| `tool` | Representa resultado de ferramenta em fluxos com tool calling estruturado. | +| `developer` | Em alguns provedores, representa instruções intermediárias do desenvolvedor ou da aplicação. | + +No template, o padrão mais simples usa principalmente: + +```text +system → quem é o agente, o que ele pode fazer e o que ele não pode fazer +user → mensagem atual + evidências + contexto de negócio + MCP + RAG +``` + +Esse padrão é intencionalmente simples para manter compatibilidade com vários runtimes. + +#### 5.2.3.2. O que deve ir no `system` + +O `system` deve conter regras estáveis e de maior prioridade. Ele responde: + +```text +Quem é este agente? +Qual domínio ele atende? +Quais limites ele deve respeitar? +O que ele nunca deve inventar? +Quando ele deve pedir mais dados? +Quando ele deve recusar uma ação? +Qual tom e formato de resposta deve usar? +``` + +Exemplo: + +```python +system_content = apply_agent_profile_prompt( + state, + """ + Você é um agente financeiro corporativo. + Use somente dados fornecidos por MCP, RAG ou business_context. + Não confirme pagamento, baixa, acordo ou contestação sem evidência de tool. + Se faltar identificador obrigatório, peça apenas esse dado. + Responda de forma curta, operacional e auditável. + """.strip(), +) +``` + +Regras críticas devem ficar no `system`, não escondidas no meio do `user`. + +#### 5.2.3.3. O que deve ir no `user` + +O `user` deve trazer o pedido atual e o contexto necessário para responder. No agente corporativo, ele normalmente contém: + +```text +mensagem atual do usuário +intent escolhida pelo roteador +route/agente ativo +business_context normalizado +resultados MCP +contexto RAG +metadados relevantes de sessão +instrução de formato para a resposta +``` + +Exemplo: + +```python +messages = [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": ( + "Mensagem do usuário:\n" + f"{user_text}\n\n" + "Intent e rota escolhidas pelo framework:\n" + f"intent={state.get('intent')} route={state.get('route')}\n\n" + "Contexto de negócio normalizado:\n" + f"customer_key={business_context.get('customer_key')}\n" + f"contract_key={business_context.get('contract_key')}\n" + f"interaction_key={business_context.get('interaction_key')}\n\n" + "Resultados MCP:\n" + f"{tool_context}\n\n" + "Contexto RAG:\n" + f"{rag_context or '[sem contexto RAG]'}\n\n" + "Instrução de resposta:\n" + "Responda somente com base nas evidências acima. " + "Se uma evidência obrigatória estiver ausente, diga que não foi encontrada." + ), + }, +] +``` + +Observe que o exemplo não joga o `state` inteiro no prompt. Ele seleciona os campos relevantes. + +#### 5.2.3.4. Relação entre `messages`, memória e histórico + +`messages` não é a memória persistente do agente. + +```text +Memória persistente + Fica no repositório/memória do framework. + Pode sobreviver a várias interações. + Pode ser resumida, compactada ou consultada. + +messages + É o payload enviado ao LLM em uma chamada específica. + Pode incluir um resumo de memória. + Pode incluir parte do histórico. + Não deve virar um dump completo da conversa. +``` + +Se o framework já carregou histórico ou resumo de conversa, o agente deve usar apenas o trecho necessário. Duplicar histórico manualmente aumenta custo, latência e risco de inconsistência. + +#### 5.2.3.5. Relação entre `messages`, MCP e RAG + +MCP e RAG produzem evidências. O LLM usa essas evidências para redigir a resposta. + +```text +MCP Tool Router + consulta sistemas, mocks, serviços ou ações externas + retorna dados estruturados + +RAG + busca contexto documental + retorna trechos relevantes e metadados + +messages + organizam essas evidências em uma conversa para o LLM +``` + +Um bom agente deixa claro para o LLM o que é evidência e o que é instrução. + +Evite misturar tudo em um texto sem estrutura. Prefira blocos: + +```text +Instruções: +- Não invente dados. + +Mensagem do usuário: +... + +Evidências MCP: +... + +Contexto RAG: +... + +Formato esperado: +... +``` + +Essa organização melhora a rastreabilidade e reduz alucinação. + +#### 5.2.3.6. Compatibilidade com frameworks de mercado + +O padrão de `messages` é compatível com a maior parte do ecossistema de IA conversacional, mas existem diferenças entre provedores. + +| Framework/provedor | Compatibilidade conceitual | Atenção | +|---|---|---| +| OpenAI Chat/Responses | Alta | Roles, tool calls e formatos multimodais podem variar por API. | +| OCI Generative AI OpenAI-compatible | Alta | Normalmente aceita formato semelhante ao OpenAI-compatible. | +| LangChain `ChatModel` | Alta | Pode converter dicts para `SystemMessage`, `HumanMessage`, `AIMessage`. | +| LangGraph | Alta | O state pode carregar `messages` ou o agente pode montar messages por chamada. | +| Semantic Kernel | Alta | Usa conceitos equivalentes de chat history e roles. | +| LlamaIndex | Alta | Pode adaptar para chat engine ou completion engine. | +| Anthropic Messages API | Média/Alta | Pode exigir adaptações de system prompt e roles. | +| Modelos locais | Variável | Alguns esperam chat template específico. | + +Por isso, o agente não deve chamar diretamente SDKs específicos. Ele monta `messages` e delega a chamada para: + +```python +answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) +``` + +Assim, a adaptação para o provider fica centralizada no runtime/framework. + +#### 5.2.3.7. Pitfalls comuns ao montar `messages` + +**Pitfall 1 — Enviar o `state` inteiro ao LLM** + +Ruim: + +```python +{"role": "user", "content": f"State completo: {state}"} +``` + +Melhor: + +```python +{"role": "user", "content": f"customer_key={business_context.get('customer_key')}"} +``` + +O `state` pode conter dados técnicos, campos sensíveis, histórico, checkpoint e informações desnecessárias. + +**Pitfall 2 — Mandar objetos enormes sem curadoria** + +Ruim: + +```python +f"Resultados completos: {mcp_results}" +``` + +Melhor: + +```python +resumo_tools = [ + { + "tool": r.get("tool_name") or r.get("tool"), + "ok": r.get("ok"), + "status": r.get("status"), + "evidence": r.get("evidence") or r.get("summary"), + } + for r in mcp_results +] +``` + +Depois envie apenas o resumo necessário. + +**Pitfall 3 — Passar dados sensíveis sem necessidade** + +Ruim: + +```python +f"CPF completo: {cpf}" +``` + +Melhor: + +```python +f"Cliente identificado: {'sim' if customer_key else 'não'}" +``` + +Quando precisar enviar identificador, prefira chave canônica, hash ou valor mascarado, conforme política do projeto. + +**Pitfall 4 — Deixar o LLM inventar quando a tool falhou** + +Ruim: + +```text +Responda sobre o pagamento do cliente. +``` + +Melhor: + +```text +A tool consultar_pagamentos_financeiro retornou erro ou ausência de dados. +Não confirme pagamento. Informe que a evidência não foi encontrada. +``` + +**Pitfall 5 — Confundir instrução com evidência** + +Ruim: + +```text +O cliente pagou e você deve responder que está tudo certo. +``` + +Melhor: + +```text +Evidência MCP: +- consultar_pagamentos_financeiro: status=COMPENSADO + +Instrução: +- Explique o status de forma objetiva. +``` + +**Pitfall 6 — Colocar regra crítica só no `user`** + +Regra de comportamento permanente deve ir no `system`. O `user` deve carregar o pedido e o contexto daquela interação. + +**Pitfall 7 — Duplicar histórico** + +Se o framework já incluiu resumo de memória, não reenvie toda a conversa manualmente. + +**Pitfall 8 — Não pedir formato de resposta** + +Em contexto corporativo, peça resposta curta, operacional, rastreável e baseada em evidência. + +#### 5.2.3.8. Modelo recomendado de `messages` para agentes corporativos + +Use este padrão como referência: + +```python +system_content = apply_agent_profile_prompt( + state, + """ + Você é um agente corporativo especializado no domínio financeiro. + Use somente evidências vindas de business_context, MCP e RAG. + Não invente protocolo, cliente, contrato, status, pagamento ou ação operacional. + Se faltar dado obrigatório, peça apenas esse dado. + Responda de forma curta, operacional e auditável. + """.strip(), +) + +messages = [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": ( + "Mensagem do usuário:\n" + f"{user_text}\n\n" + "Contexto de sessão resumido:\n" + f"channel={session.get('channel')} tenant_id={session.get('tenant_id')}\n" + f"global_session_id={session.get('global_session_id')}\n\n" + "Contexto de negócio:\n" + f"customer_key={business_context.get('customer_key')}\n" + f"contract_key={business_context.get('contract_key')}\n" + f"interaction_key={business_context.get('interaction_key')}\n\n" + "Intent e rota:\n" + f"intent={state.get('intent')} route={state.get('route')}\n\n" + "Evidências MCP:\n" + f"{mcp_evidence}\n\n" + "Contexto RAG:\n" + f"{rag_context or '[sem contexto RAG]'}\n\n" + "Formato esperado:\n" + "1. Resposta direta ao usuário.\n" + "2. Não cite detalhes internos de arquitetura.\n" + "3. Se faltou evidência, diga claramente o que faltou." + ), + }, +] +``` + +Esse padrão ajuda o desenvolvedor a separar: + +```text +Regras permanentes → system +Pedido e contexto atual → user +Evidências de tools → bloco MCP +Conhecimento documental → bloco RAG +Sessão/canal → contexto resumido +Formato de saída → instrução final +``` + +#### 5.2.3.9. Como revisar `messages` durante desenvolvimento + +Durante o desenvolvimento, antes de culpar o LLM, revise o payload enviado para ele. + +Perguntas úteis: + +```text +O system prompt contém as regras mais importantes? +O user prompt contém a pergunta real do usuário? +O business_context certo foi incluído? +Os resultados MCP aparecem como evidência, e não como instrução inventada? +O RAG trouxe contexto útil ou só ruído? +Há dados sensíveis desnecessários? +O prompt está grande demais? +O formato de resposta esperado está claro? +``` + +Uma boa prática é emitir um IC de debug em ambiente não produtivo ou logar uma versão sanitizada do prompt, nunca o prompt bruto com dados sensíveis. + + +### 5.2.4. Recursos avançados agora padronizados pelo framework + +Nos primeiros exemplos deste tutorial, o agente usa diretamente métodos simples como `_collect_mcp_context()` e `_invoke_llm_cached()`. Isso é suficiente para agentes simples. Porém, em agentes reais migrados para o framework, como um Backoffice/ANATEL, aparecem necessidades adicionais: + +```text +normalizar tools por intent; +ler context/session/business_context/tool_arguments sempre da mesma forma; +montar argumentos MCP com aliases; +bloquear tools de ação quando falta payload obrigatório; +executar tools uma a uma com eventos de observabilidade; +montar messages sem despejar o state inteiro no prompt; +gerar fallback controlado quando o LLM falha. +``` + +Essas necessidades não são exclusivas do Backoffice. Por isso, a partir desta versão, elas passam a ser tratadas como **capacidades reutilizáveis do framework**, e não como código que cada agente deve copiar. + +#### 5.2.4.1. `RuntimeContext`: leitura canônica do state + +O framework passa a oferecer um objeto conceitual chamado `RuntimeContext`, obtido pelo agente com: + +```python +runtime = self.get_runtime_context(state) +``` + +Esse objeto organiza: + +```text +runtime.state → state completo do LangGraph +runtime.context → context normalizado +runtime.session → dados de sessão/canal vindos do Gateway +runtime.session_metadata → metadata da sessão +runtime.business_context → identidade de negócio canônica +runtime.tool_arguments → parâmetros explícitos para tools +runtime.sanitized_input → texto sanitizado pelos guardrails +runtime.original_text → texto original, quando necessário para extração controlada +``` + +O desenvolvedor não precisa ficar repetindo: + +```python +ctx = state.get("context") or {} +session = ctx.get("session") or {} +business_context = ctx.get("business_context") or state.get("business_context") or {} +``` + +Ele pode usar: + +```python +runtime = self.get_runtime_context(state) +customer_key = runtime.pick("customer_key", "cpf", "cnpj", "msisdn") +``` + +A ordem de confiança continua padronizada: + +```text +1. tool_arguments +2. business_context +3. context +4. session +5. session.metadata +6. state +``` + +#### 5.2.4.2. `normalize_tools_by_intent()`: fallback de tools sem tirar poder do router + +Em um agente ideal, o `EnterpriseRouter` escolhe a intent e injeta `mcp_tools` no `state`. Mas, em testes, chamadas diretas ou migrações, o agente pode ser executado sem essa injeção. + +Para isso, o framework oferece: + +```python +normalized_state = self.normalize_tools_by_intent( + state, + default_tools_by_intent=DEFAULT_TOOLS_BY_INTENT, + default_intent="financeiro_pagamentos", + route=self.name, +) +``` + +A regra é: + +```text +Se state['mcp_tools'] veio do router, use essas tools. +Se não veio, use o fallback declarado pelo agente. +Remova duplicidades. +Preserve ordem estável. +Defina intent, route e active_agent quando estiverem ausentes. +``` + +Isso evita que cada agente implemente seu próprio `_normalize_state_tools()`. + +#### 5.2.4.3. `build_tool_arguments()`: argumentos MCP canônicos + +O agente pode montar argumentos MCP sem conhecer todos os detalhes do mapper: + +```python +args = self.build_tool_arguments( + state, + tool_name="consultar_titulo_financeiro", + intent=state.get("intent"), + aliases={ + "customer_key": ["customer_id", "cpf", "cnpj"], + "contract_key": ["contract_id", "invoice_id"], + }, +) +``` + +Esse método monta argumentos como: + +```text +query +operator_instructions +customer_key +contract_key +interaction_key +session_key +parâmetros explícitos de tool_arguments +aliases configurados pelo domínio +``` + +Depois disso, o `MCPToolRouter` ainda aplica o `mcp_parameter_mapping.yaml`. Ou seja: + +```text +build_tool_arguments() monta o contrato canônico. +mcp_parameter_mapping.yaml traduz para o nome esperado por cada MCP Server. +``` + +#### 5.2.4.4. Política de execução de tools sensíveis + +Nem toda tool é apenas consulta. Algumas tools executam ações, como registrar parecer, abrir solicitação, cancelar serviço ou criar protocolo. + +Essas tools devem ser declaradas com política em `config/tools.yaml`: + +```yaml +tools: + registrar_acao_backoffice: + description: Registra ação operacional no backoffice. + mcp_server: backoffice + enabled: true + tool_type: action + requires: [protocol_id, action_text, operator_session] + confirmation_required: false + args_schema: + protocol_id: string + action_text: string + operator_session: string +``` + +Com isso, o framework consegue bloquear a chamada antes de chegar ao MCP quando falta campo obrigatório: + +```text +Tool registrar_acao_backoffice escolhida. +Framework monta argumentos. +Framework verifica requires. +Se action_text estiver ausente, retorna skipped=true. +Agente emite IC/NOC de domínio, se necessário. +``` + +Isso evita que cada agente escreva manualmente: + +```python +if tool.startswith("registrar_") and not arguments.get("action_text"): + ... +``` + +#### 5.2.4.5. `execute_tools_for_intent()`: execução padronizada das tools + +O agente pode executar tools selecionadas pela intent com: + +```python +mcp_results = await self.execute_tools_for_intent( + state, + tools=state.get("mcp_tools") or [], + aliases=TOOL_ALIASES, +) +``` + +Esse método cuida de: + +```text +montar argumentos; +aplicar política de execução; +chamar _call_mcp_tool(); +normalizar resultado; +emitir IC.MCP_TOOL_CALLED; +emitir IC.TOOL_CALLED; +emitir NOC.MCP_TOOL_FAILED quando houver falha; +retornar skipped=true quando uma política bloquear a execução. +``` + +O agente ainda pode emitir ICs específicos de negócio depois disso. Exemplo: `AGA.010` para Speech Analytics, `AGA.011` para Cliente/IMDB, `AGA.020` para TAIS/templates. + +#### 5.2.4.6. `build_messages()`: messages padronizado + +Para evitar que cada agente monte prompts de forma diferente, o framework oferece: + +```python +messages = self.build_messages( + state, + system_prompt=system_prompt, + mcp_results=mcp_results, + rag_context=rag_context, + rag_metadata=rag_metadata, +) +``` + +Esse builder separa: + +```text +system prompt; +mensagem do usuário; +intent e route; +business_context; +resultados MCP; +contexto RAG; +metadados RAG; +seções extras. +``` + +O objetivo é reduzir estes erros: + +```text +enviar state inteiro para o LLM; +misturar regra permanente com evidência; +incluir dados sensíveis sem necessidade; +esquecer de informar que uma tool falhou; +duplicar histórico que o framework já carrega. +``` + +#### 5.2.4.7. Quando customizar e quando usar o framework + +Use o framework para: + +```text +ler contexto; +normalizar tools; +montar argumentos MCP; +aplicar política de execução; +chamar MCP; +montar messages; +chamar LLM com cache; +emitir eventos técnicos genéricos. +``` + +Use o agente para: + +```text +definir regras de negócio; +definir aliases específicos do domínio; +definir prompts do domínio; +definir ICs específicos da jornada; +definir estados conversacionais como WAITING_*; +tratar compatibilidade de migração; +decidir fallback textual específico do domínio. +``` + +Essa separação permite que um agente real tenha customizações fortes sem virar um motor paralelo ao framework. + + +### 5.3. Criar o arquivo do agente + +Crie: + +```text +app/agents/financeiro_agent.py +``` + +Código-base comentado: + +```python +from app.agents.prompting import apply_agent_profile_prompt +from app.agents.runtime import AgentRuntimeMixin + + +class FinanceiroAgent(AgentRuntimeMixin): + # Este nome precisa bater com o nome usado no workflow e nas configurações. + name = "financeiro_agent" + + def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None): + # Estes objetos são injetados pelo workflow/framework. + # O agente usa, mas não cria esses motores. + self.llm = llm + self.telemetry = telemetry + self.tool_router = tool_router + self.rag_service = rag_service + self.cache = cache + self.settings = settings + self.observer = observer + + async def run(self, state): + # 1. Marca o início da jornada de negócio deste agente. + await self._emit_ic( + "IC.FINANCEIRO_AGENT_STARTED", + state, + {"business_component": "financeiro"}, + component="agent.financeiro.start", + ) + + # 2. Separa os blocos do contrato do framework. + # O agente lê esses blocos, mas quem cria/normaliza é o framework. + ctx = state.get("context") or {} + session = ctx.get("session") or {} + session_metadata = session.get("metadata") or {} + business_context = ctx.get("business_context") or state.get("business_context") or {} + tool_arguments = ctx.get("tool_arguments") or state.get("tool_arguments") or {} + + # 3. Interpreta a mensagem atual usando o texto já sanitizado pelos guardrails, + # mas preserva o texto original apenas quando precisar extrair identificadores. + user_text = state.get("sanitized_input") or state.get("user_text") or "" + original_text = ( + ctx.get("message") + or ctx.get("text") + or ctx.get("query") + or session.get("last_user_message") + or state.get("user_text") + or user_text + ) + + # 4. Chama tools MCP selecionadas pelo roteamento, quando configuradas. + # O agente não precisa saber se a tool usa REST, SOAP, DB ou mock. + tool_context = await self._collect_tool_context(state) + + if tool_context: + await self._emit_ic( + "IC.FINANCEIRO_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.financeiro.mcp", + ) + + # 5. Recupera contexto documental, se o RAG estiver habilitado. + rag_context, rag_metadata = await self._retrieve_rag_context(state) + + # 6. Monta a mensagem para o LLM. + # O system prompt define comportamento e limites do agente. + # O user prompt leva dados, evidências e contexto. + messages = [ + { + "role": "system", + "content": apply_agent_profile_prompt( + state, + "Você é um agente financeiro. Responda com clareza, usando dados das ferramentas quando disponíveis. Não confirme ações financeiras sem evidência e confirmação explícita." + ), + }, + { + "role": "user", + "content": ( + f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n" + f"Sessão: {session}\n" + f"Intent: {state.get('intent')}\n" + f"Dados MCP: {tool_context}\n" + f"Contexto RAG: {rag_context}" + ), + }, + ] + + # 7. Chama o LLM usando o runtime comum, com cache e telemetria. + answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) + + # 8. Retorna no contrato esperado pelo workflow. + result = { + "answer": f"[FinanceiroAgent] {answer}", + "next_state": "FINANCEIRO_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + } + + # 9. Marca o fim da jornada de negócio. + await self._emit_ic( + "IC.FINANCEIRO_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + }, + component="agent.financeiro.completed", + ) + + return result + + async def _collect_tool_context(self, state): + # Este método delega para o MCP Tool Router do framework. + # As tools chamadas dependem da intent definida em routing.yaml. + return await self._collect_mcp_context(state) +``` + +### 5.3.1. Como adaptar esse exemplo para um agente real + +No exemplo acima, `session`, `business_context` e `tool_arguments` aparecem no prompt para fins didáticos. Em produção, o desenvolvedor deve evitar jogar objetos enormes diretamente no prompt. O ideal é selecionar apenas os campos necessários. + +Exemplo de raciocínio para um agente financeiro: + +```text +session.channel → útil para ajustar linguagem ou entender origem da conversa. +session.tenant_id → útil para isolamento multi-tenant. +business_context.customer_key → útil para consultar cliente/título/pagamento. +business_context.contract_key → útil para consultar contrato, fatura ou pedido. +business_context.interaction_key → útil para rastrear protocolo/chamado/interação. +tool_arguments → útil quando o Gateway ou Identity Resolver já preparou parâmetros exatos. +``` + +Uma função utilitária comum dentro do agente é um `pick()` com ordem de precedência explícita: + +```python +def pick(name: str, *, tool_arguments, business_context, ctx, session, session_metadata, state): + if name in tool_arguments: + return tool_arguments.get(name) + if isinstance(business_context, dict) and name in business_context: + return business_context.get(name) + if name in ctx: + return ctx.get(name) + if name in session: + return session.get(name) + if name in session_metadata: + return session_metadata.get(name) + return state.get(name) +``` + +Essa função deixa claro que o agente não está “adivinhando” de onde vem o dado. Ele está seguindo uma política de confiança. + +### 5.3.2. Onde entra o Agent Gateway nesse código? + +Quando existe Agent Gateway / Global Supervisor, ele pode enriquecer a mensagem antes de enviá-la ao backend do agente. Exemplos de dados que podem chegar em `context.session`: + +```json +{ + "session": { + "global_session_id": "s1", + "backend_session_id": "default:financeiro_agent:s1", + "active_backend": "financeiro", + "channel": "web", + "tenant_id": "default", + "metadata": { + "selected_backend": "financeiro", + "last_reason": "Backend escolhido por regras: matches=['pagamento']" + } + } +} +``` + +O agente não deve usar esse bloco para tomar decisão de negócio final. Ele deve usá-lo para contexto técnico, rastreabilidade e continuidade da conversa. A decisão de negócio deve continuar baseada em `business_context`, tools MCP, RAG e regras de domínio. + +### 5.4. Como saber se o agente está bem implementado? + +Um agente está bem implementado quando: + +```text +Ele conhece regras de negócio, mas não conhece detalhes de infraestrutura. +Ele usa o runtime comum para LLM, RAG, cache, MCP e IC. +Ele retorna um contrato simples para o workflow. +Ele não duplica guardrail, checkpoint, sessão, memória ou telemetria. +Ele consegue ser testado isoladamente com state simulado. +``` + +--- + +## 6. Registrando o agente no workflow + +### 6.1. Antes do código: o que é o workflow? + +O workflow é o caminho controlado pelo LangGraph. Ele define a ordem de execução: + +```text +entrada → guardrails → roteamento → agente → revisão → persistência → resposta +``` + +Criar a classe do agente não basta. O LangGraph só executa nós que foram registrados no grafo. + +O registro no workflow responde três perguntas: + +```text +Qual classe implementa o agente? +Qual nome de nó representa esse agente no grafo? +Para onde o fluxo segue depois que o agente responde? +``` + +### 6.2. Importar o agente + +Edite: + +```text +app/workflows/agent_graph.py +``` + +Adicione: + +```python +from app.agents.financeiro_agent import FinanceiroAgent +``` + +### 6.3. Instanciar o agente + +No `__init__` da classe `AgentWorkflow`, depois da criação de `agent_kwargs`: + +```python +self.financeiro = FinanceiroAgent(llm, **agent_kwargs) +``` + +Essa linha injeta no agente os mesmos motores compartilhados pelos demais agentes: LLM, telemetry, MCP Tool Router, RAG, cache, settings e observer. + +### 6.4. Criar o nó do LangGraph + +Em `_build_graph()`: + +```python +builder.add_node("financeiro_agent", self._node("financeiro_agent", self.financeiro_agent)) +``` + +O primeiro `financeiro_agent` é o nome do nó no grafo. O segundo `self.financeiro_agent` é o método wrapper que será chamado quando o fluxo chegar nesse nó. + +### 6.5. Adicionar rota condicional + +No dicionário de `builder.add_conditional_edges("routing_decision", ...)`, inclua: + +```python +"financeiro_agent": "financeiro_agent", +``` + +Exemplo: + +```python +builder.add_conditional_edges( + "routing_decision", + lambda s: s.get("route", "billing_agent"), + { + "billing_agent": "billing_agent", + "product_agent": "product_agent", + "orders_agent": "orders_agent", + "support_agent": "support_agent", + "financeiro_agent": "financeiro_agent", + "handoff": "handoff", + "supervisor_agent": "supervisor_agent", + }, +) +``` + +Essa tabela conecta a decisão do roteador com o nó real do grafo. + +### 6.6. Conectar o nó ao Output Supervisor + +```python +builder.add_edge("financeiro_agent", "output_supervisor") +``` + +Essa linha é importante porque a resposta do agente não deve ir direto ao usuário. Ela passa antes por output supervisor, output guardrails, judges, supervisor review e persistência. + +### 6.7. Criar o método wrapper + +Na classe `AgentWorkflow`: + +```python +async def financeiro_agent(self, state): + async with self.langgraph_telemetry.node("financeiro_agent", state): + async with self.telemetry.span( + "workflow.agent.financeiro", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"intent": state.get("intent")}, + ): + return await self.financeiro.run(state) +``` + +O wrapper adiciona telemetria ao redor do agente. A lógica de negócio continua dentro de `FinanceiroAgent.run()`. + +### 6.8. Adicionar ao modo supervisor + +No método `supervisor_agent()`, ajuste o mapa de handlers: + +```python +handlers = { + "billing_agent": self.billing.run, + "product_agent": self.product.run, + "orders_agent": self.orders.run, + "support_agent": self.support.run, + "financeiro_agent": self.financeiro.run, +} +``` + +Isso permite que o supervisor chame o novo agente quando `ROUTING_MODE=supervisor` ou quando houver handoff supervisionado. + +### 6.9. Erros comuns neste capítulo + +```text +Criar a classe do agente, mas esquecer add_node. +Adicionar add_node, mas esquecer add_conditional_edges. +Adicionar rota, mas esquecer add_edge para output_supervisor. +Usar nome diferente em routing.yaml, workflow e classe. +Chamar self.financeiro.run direto sem wrapper de telemetria. +``` + +--- + +## 7. Ajustando o estado do agente + +### 7.1. Antes do código: o que é o state? + +O `state` é o objeto que trafega entre os nós do LangGraph. Ele funciona como a memória de curto prazo da execução atual. + +Ele não é o banco de dados, não é a memória conversacional completa e não deve virar um repositório gigante de informações. + +Use o `state` para dados que precisam circular entre nós, por exemplo: + +```text +texto do usuário +intent escolhida +rota escolhida +resposta parcial +resultado de uma tool +próximo estado da conversa +flags de decisão +``` + +Não use o `state` para: + +```text +histórico longo de conversa +arquivos grandes +respostas completas de sistemas externos sem necessidade +conteúdo bruto de documentos +logs extensos +``` + +### 7.2. Quando alterar `app/state.py` + +Edite: + +```text +app/state.py +``` + +Somente adicione novos campos se o agente precisar compartilhar informações específicas com outros nós. + +Exemplo: + +```python +class AgentState(TypedDict, total=False): + # campos existentes... + financial_context: dict[str, Any] + financial_decision: dict[str, Any] +``` + +### 7.3. Critério de decisão + +Antes de criar um campo novo, pergunte: + +```text +Outro nó precisa ler este dado? +Este dado precisa sobreviver ao próximo passo do workflow? +Este dado é pequeno e estruturado? +Este dado ajuda na auditoria ou na decisão? +``` + +Se a resposta for não, deixe o dado local ao agente ou grave em repositório apropriado. + +--- + +## 8. Registrando o agente em `config/agents.yaml` + +### 8.1. Antes do YAML: para que serve `agents.yaml`? + +O `agents.yaml` é o cadastro oficial dos agentes disponíveis. Ele não executa o agente sozinho, mas informa ao framework quais agentes existem, quais configurações isoladas eles usam e quais metadados descrevem o domínio. + +Ele responde: + +```text +Qual é o agent_id? +Qual nome amigável aparece em listagens e debug? +Onde estão prompt, guardrails e judges específicos? +Qual domínio esse agente atende? +Quais metadados ajudam roteamento, auditoria e operação? +``` + +### 8.2. Exemplo de registro + +Edite: + +```text +config/agents.yaml +``` + +Adicione: + +```yaml +agents: + - agent_id: financeiro_agent + name: Financeiro Agent + description: Agente para dúvidas financeiras, pagamentos, saldos, acordos e segunda via. + prompt_policy_path: ./config/agents/financeiro_agent/prompt_policy.yaml + routing_config_path: ./config/routing.yaml + guardrails_config_path: ./config/agents/financeiro_agent/guardrails.yaml + judges_config_path: ./config/agents/financeiro_agent/judges.yaml + mcp_servers_config_path: ./config/mcp_servers.yaml + tools_config_path: ./config/tools.yaml + metadata: + domain: financeiro + system_prefix: | + Você está executando o financeiro_agent. + Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id. + Não misture histórico ou decisões de outros agentes. +``` + +### 8.3. Cuidados + +O `agent_id` precisa ser consistente com: + +```text +nome do nó no workflow +nome usado em routing.yaml +session_id canônico +pasta config/agents// +metadados de observabilidade +``` + +Evite renomear `agent_id` depois que o agente já estiver em produção, porque isso pode quebrar histórico, memória, checkpoint e métricas. + +--- + +## 9. Criando configurações isoladas do agente + +### 9.1. Antes do YAML: por que isolar configuração por agente? + +Cada agente pode ter política de prompt, guardrails e judges próprios. Um agente financeiro pode exigir confirmação explícita antes de uma ação. Um agente de suporte pode permitir respostas mais abertas. Um agente jurídico pode exigir evidência documental. + +Por isso, evite colocar tudo no arquivo global. Use configuração global para regras corporativas e configuração local para regras do domínio. + +Crie: + +```text +config/agents/financeiro_agent/ +``` + +### 9.2. `prompt_policy.yaml` + +Esse arquivo define a postura base do agente. + +```yaml +id: financeiro_agent_prompt_policy +version: 1 +description: Prompt base isolado do agente financeiro. +system_prefix: | + Você é um agente corporativo especializado em atendimento financeiro. + Seja claro, objetivo, auditável e não invente dados. + Quando precisar executar uma ação, use ferramentas configuradas. + Quando faltar informação obrigatória, peça apenas o dado necessário. +``` + +Use este arquivo para regras persistentes de comportamento, não para regras temporárias de teste. + +### 9.3. `guardrails.yaml` + +Esse arquivo complementa os guardrails globais. + +```yaml +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true + - code: PINJ + enabled: true +output: + - code: REVPREC + enabled: true + - code: CMP + enabled: true +``` + +Use guardrail quando a resposta precisa ser bloqueada, sanitizada ou revisada por regra. + +### 9.4. `judges.yaml` + +Judges avaliam qualidade, aderência, groundedness e outros critérios após a resposta ser produzida. + +```yaml +judges: + - name: response_quality + enabled: true + threshold: 0.7 + - name: groundedness + enabled: true + threshold: 0.6 +``` + +Use judge para avaliar resposta. Use guardrail para bloquear ou proteger. Use prompt para orientar comportamento. + +--- + +## 10. Configurando roteamento em `config/routing.yaml` + +### 10.1. Antes do YAML: o que é roteamento? + +Roteamento é a decisão de qual agente deve tratar a mensagem. + +Em um sistema multiagente, o usuário não deveria precisar saber qual agente chamar. Ele escreve uma mensagem, e o framework decide a rota. + +O roteador normalmente considera: + +```text +texto do usuário +estado atual da conversa +keywords +examples +prioridade +agent_id solicitado +políticas de estado +LLM router, se habilitado +``` + +### 10.2. Quando criar uma intent nova? + +Crie uma intent quando existir uma categoria clara de solicitação que deve ir para um agente específico. + +Exemplo de intent financeira: + +```yaml +intents: + - name: financeiro_pagamentos + domain: financeiro + agent: financeiro_agent + description: Dúvidas sobre pagamento, saldo, fatura, boleto, acordo, contestação e segunda via. + priority: 15 + mcp_tools: + - consultar_titulo_financeiro + - consultar_pagamentos_financeiro + keywords: + - pagamento + - boleto + - saldo + - acordo + - financeiro + - segunda via + - vencimento + - cobrança + - contestação + examples: + - Quero consultar meu pagamento. + - Preciso da segunda via do boleto. + - Meu pagamento ainda não foi baixado. +``` + +### 10.3. O que significa `mcp_tools` na intent? + +`mcp_tools` indica quais tools devem ser disponibilizadas/coletadas quando essa intent for escolhida. Assim, o agente não precisa decidir manualmente cada chamada em todos os casos simples. + +O fluxo fica: + +```text +routing.yaml escolhe intent +intent aponta agent +intent declara mcp_tools +AgentRuntimeMixin coleta contexto MCP +agente usa os dados na resposta +``` + +### 10.4. Políticas de estado + +Se a conversa já estiver em um estado específico, a próxima mensagem pode precisar voltar ao mesmo agente, mesmo que o texto seja curto. + +Exemplo: + +```yaml +state_policies: + - state: WAITING_FINANCEIRO_CONFIRMATION + agent: financeiro_agent + description: Mantém confirmações curtas no fluxo financeiro. +``` + +Isso evita que uma resposta como “sim” seja roteada para o agente errado. + +### 10.5. Router versus supervisor + +No modo router: + +```env +ROUTING_MODE=router +``` + +O framework escolhe uma rota de forma mais direta, normalmente por regras, keywords, examples e score. + +No modo supervisor: + +```env +ROUTING_MODE=supervisor +``` + +Um supervisor pode decidir a sequência de agentes, handoff ou combinação de respostas. + +Use router quando o domínio for bem mapeado. Use supervisor quando a conversa exigir decomposição, múltiplos agentes ou decisão mais flexível. + +--- + +## 11. Configurando tools em `config/tools.yaml` + +### 11.1. Antes do YAML: o que é uma tool? + +Uma tool é uma capacidade externa que o agente pode usar para obter dados ou executar uma ação. + +Exemplos: + +```text +consultar fatura +consultar pagamento +abrir protocolo +buscar pedido +cancelar serviço +consultar base de conhecimento +``` + +A tool não é necessariamente o sistema real. Ela é o contrato que o backend conhece. O sistema real fica atrás do MCP Server. + +### 11.2. Declarando tools + +Edite: + +```text +config/tools.yaml +``` + +Adicione: + +```yaml +tools: + consultar_titulo_financeiro: + description: Consulta um título financeiro por cliente e contrato. + mcp_server: financeiro + enabled: true + args_schema: + customer_id: string + contract_id: string + + consultar_pagamentos_financeiro: + description: Consulta pagamentos financeiros por cliente. + mcp_server: financeiro + enabled: true + args_schema: + customer_id: string +``` + +### 11.3. Como pensar sobre uma tool + +Antes de declarar uma tool, defina: + +```text +Qual pergunta de negócio ela responde? +Ela só consulta ou executa uma ação? +Quais parâmetros são obrigatórios? +Quais parâmetros vêm da identidade canônica? +Qual MCP Server implementa a tool? +Qual timeout e fallback são aceitáveis? +O resultado tem dados sensíveis que precisam ser mascarados? +``` + +O backend não deve chamar diretamente HTTP/SOAP/DB de sistemas de negócio quando essa chamada puder ser padronizada via MCP Tool Router. + +--- + +## 12. Configurando servidores MCP + +### 12.1. Antes do YAML: o que é o MCP Server? + +O MCP Server é o adaptador entre o mundo do agente e os sistemas reais. Ele permite que o backend converse com ferramentas de forma padronizada, sem conhecer detalhes de REST, SOAP, banco, filas ou mocks. + +O desenho é: + +```text +Agente + ↓ +MCP Tool Router do framework + ↓ +MCP Server do domínio + ↓ +Sistema real, mock, banco, REST, SOAP ou serviço interno +``` + +### 12.2. Configuração local + +Edite: + +```text +config/mcp_servers.yaml +``` + +Exemplo: + +```yaml +servers: + financeiro: + transport: http + endpoint: http://localhost:8300/mcp + enabled: true + description: MCP Server Financeiro local. +``` + +### 12.3. Configuração em Docker Compose + +Edite: + +```text +config/mcp_servers.docker.yaml +``` + +Exemplo: + +```yaml +servers: + financeiro: + transport: http + endpoint: http://financeiro-mcp:8300/mcp + enabled: true + description: MCP Server Financeiro em Docker. +``` + +### 12.4. Como evitar erro comum de endpoint + +Localmente, `localhost` funciona porque backend e MCP rodam na mesma máquina. + +Dentro do Docker Compose, `localhost` dentro do container do backend aponta para o próprio container do backend, não para o container do MCP. Por isso, em Docker, use o nome do serviço: + +```text +http://financeiro-mcp:8300/mcp +``` + +--- + +## 13. Configurando mapeamento de parâmetros MCP + +### 13.1. Antes do YAML: por que existe mapeamento? + +O framework trabalha com chaves canônicas para não depender dos nomes específicos de cada sistema. + +Exemplo: + +```text +customer_key = cliente canônico no framework +contract_key = contrato/fatura/pedido/título canônico +interaction_key = interação externa +session_key = sessão técnica +``` + +Mas cada tool pode esperar nomes diferentes: + +```text +customer_id +cpf +msisdn +clientCode +contract_id +invoice_id +order_id +``` + +O `mcp_parameter_mapping.yaml` faz essa tradução sem obrigar o agente a conhecer os nomes internos de cada MCP. + +### 13.2. Exemplo + +Edite: + +```text +config/mcp_parameter_mapping.yaml +``` + +```yaml +mcp_parameter_mapping: + defaults: + use_mock: true + tools: + consultar_titulo_financeiro: + map: + customer_key: customer_id + contract_key: contract_id + interaction_key: interaction_id + session_key: session_id + consultar_pagamentos_financeiro: + map: + customer_key: customer_id + session_key: session_id +``` + +Interpretação: + +```text +customer_key -> chave canônica no framework +customer_id -> parâmetro esperado pela tool MCP +``` + +### 13.3. Como validar o mapeamento + +Se a tool recebe parâmetro errado, investigue nesta ordem: + +```text +payload enviado ao /gateway/message +config/identity.yaml +business_context resolvido +config/mcp_parameter_mapping.yaml +args_schema da tool +assinatura real no MCP Server +``` + +--- + +## 14. Configurando identidade de negócio + +### 14.1. Antes do YAML: o que é identidade de negócio? + +Identidade de negócio é a normalização das chaves que representam o cliente, contrato, pedido, protocolo, sessão ou interação. + +Sem essa camada, cada canal envia um nome diferente e cada tool espera outro nome. O resultado é erro de parâmetro, tool sem dado obrigatório ou consulta ao cliente errado. + +O `identity.yaml` responde: + +```text +De onde posso extrair customer_key? +De onde posso extrair contract_key? +De onde posso extrair interaction_key? +De onde posso extrair session_key? +Quais chaves são obrigatórias? +``` + +### 14.2. Exemplo + +Edite: + +```text +config/identity.yaml +``` + +```yaml +identity: + version: "2" + required: + - session_key + keys: + customer_key: + description: Cliente canônico. + sources: + - business_context.customer_key + - context.business_context.customer_key + - context.session.metadata.customer_key + - customer_key + - customer_id + - cpf + - cnpj + - user_id + contract_key: + description: Contrato, pedido, fatura ou título principal. + sources: + - business_context.contract_key + - context.business_context.contract_key + - context.session.metadata.contract_key + - contract_key + - contract_id + - invoice_id + - order_id + interaction_key: + description: Chave externa da interação. + sources: + - business_context.interaction_key + - context.business_context.interaction_key + - context.session.metadata.interaction_key + - interaction_key + - call_id + - message_id + - protocol_id + session_key: + description: Sessão técnica estável. + sources: + - business_context.session_key + - context.business_context.session_key + - context.session.backend_session_id + - context.session.global_session_id + - context.session.metadata.session_key + - session_key + - conversation_key + - session_id +``` + +### 14.3. Como pensar sobre identidade + +Use o mínimo necessário. Não torne tudo obrigatório. Para uma pergunta genérica, talvez só `session_key` seja suficiente. Para consultar um título financeiro, talvez `customer_key` e `contract_key` sejam obrigatórios. + +A identidade resolvida aparece em `business_context` dentro do `state` e é usada pelo `MCP Tool Router`. + +### 14.4. Relação entre SessionContext e BusinessContext + +Quando o Agent Gateway está presente, ele pode criar ou transportar dados de sessão. Esses dados são importantes, mas não substituem a identidade de negócio. + +```text +SessionContext responde: + Quem está falando? + Por qual canal? + Qual sessão global está ativa? + Qual backend está atendendo? + Qual foi a razão da última decisão de rota? + +BusinessContext responde: + Qual cliente deve ser consultado? + Qual contrato/fatura/pedido está em discussão? + Qual protocolo/chamado/interação identifica o caso? + Qual chave deve ser enviada para a tool MCP? +``` + +Regra prática: + +```text +Use session para continuidade, rastreabilidade e canal. +Use business_context para consultar sistemas, chamar MCP e tomar decisão de negócio. +Use tool_arguments quando parâmetros já vierem explicitamente preparados. +``` + +Exemplo de erro comum: + +```text +Usar session.user_id como customer_key sem validar identity.yaml. +``` + +O correto é deixar o `IdentityResolver` transformar `user_id`, `cpf`, `msisdn`, `customer_id` ou outro identificador em uma chave canônica como `customer_key`. + +--- + +## 15. Implementando ou conectando um MCP Server + +### 15.1. Antes do código: qual é o papel do MCP Server? + +O MCP Server é onde fica a integração com sistemas externos ou mocks de domínio. Ele permite que o agente use uma tool sem conhecer implementação técnica. + +O backend sabe chamar: + +```text +consultar_titulo_financeiro(customer_id, contract_id) +``` + +Mas não sabe, nem deveria saber, se essa consulta usa: + +```text +REST +SOAP +banco Oracle +arquivo mock +serviço legado +fila +sistema interno +``` + +### 15.2. Contrato conceitual das tools + +Exemplo conceitual: + +```python +async def consultar_titulo_financeiro(customer_id: str, contract_id: str, session_id: str | None = None): + return { + "customer_id": customer_id, + "contract_id": contract_id, + "status": "ABERTO", + "valor": 129.90, + "vencimento": "2026-06-20", + } + + +async def consultar_pagamentos_financeiro(customer_id: str, session_id: str | None = None): + return { + "customer_id": customer_id, + "pagamentos": [ + {"data": "2026-06-01", "valor": 129.90, "status": "COMPENSADO"} + ], + } +``` + +### 15.3. Critério para mock versus real + +Use mock quando: + +```text +o sistema real não está disponível +você está testando roteamento e contrato +você quer validar frontend/backend sem depender de VPN +você quer montar testes automatizados determinísticos +``` + +Use integração real quando: + +```text +o contrato já foi validado +os parâmetros estão corretos +o timeout e fallback foram definidos +há observabilidade para sucesso e falha +há dados seguros para teste +``` + +Para desenvolvimento, você pode usar `use_mock: true` no `mcp_parameter_mapping.yaml` ou implementar um MCP Server local com respostas simuladas. + +--- + +## 16. IC, NOC e GRL no novo agente + +### 16.1. Antes dos eventos: por que eles existem? + +IC, NOC e GRL não são logs comuns. Eles existem para rastrear a execução de forma corporativa. + +```text +IC = evento de negócio ou jornada do agente +NOC = evento operacional, erro, indisponibilidade, timeout ou degradação +GRL = evento de governança, guardrail, bloqueio, revisão ou sanitização +``` + +Use `logger.info()` para diagnóstico simples. Use IC/NOC/GRL quando o evento precisa aparecer em auditoria, observabilidade ou análise operacional. + +### 16.2. IC — eventos de negócio + +Use ICs dentro do agente para registrar passos relevantes da jornada. + +Exemplo: + +```python +await self._emit_ic( + "IC.FINANCEIRO_AGENT_STARTED", + state, + {"business_component": "financeiro"}, + component="agent.financeiro.start", +) +``` + +Sugestão mínima por agente: + +```text +IC._AGENT_STARTED +IC._MCP_CONTEXT_COLLECTED +IC._RAG_CONTEXT_RETRIEVED +IC._AGENT_COMPLETED +IC._BUSINESS_DECISION +IC._ACTION_REQUESTED +IC._ACTION_COMPLETED +``` + +### 16.3. NOC — eventos operacionais + +NOC deve ser usado para saúde técnica, indisponibilidade, erro, timeout, fallback e degradação. + +Exemplo: + +```python +await self.observer.emit_noc( + "NOC.FINANCEIRO_TOOL_TIMEOUT", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "tool": "consultar_titulo_financeiro", + }, + component="agent.financeiro.tool", +) +``` + +### 16.4. GRL — guardrails + +A maior parte dos GRLs já é emitida pelo workflow em: + +```text +input_guardrails +output_supervisor +output_guardrails +``` + +Só implemente GRL dentro do agente quando houver uma validação de domínio específica que não caiba nos guardrails globais. + +### 16.5. Quando não criar evento novo + +Não crie IC/NOC/GRL para cada linha de código. Crie eventos para decisões importantes: + +```text +entrada validada +contexto MCP coletado +decisão de negócio tomada +ação externa solicitada +ação externa concluída +fallback técnico acionado +resposta bloqueada ou revisada +workflow concluído +``` + +--- + +## 17. Build e execução local + +### 17.1. Antes dos comandos: o que significa subir o backend? + +Subir o backend significa iniciar a API que recebe mensagens, normaliza canal, resolve identidade, abre sessão, executa o workflow e devolve resposta. + +Ele pode subir mesmo sem MCP real, desde que a configuração esteja em mock ou que as tools não sejam obrigatórias para o teste. + +### 17.2. Rodar backend local + +Dentro de `agent_template_backend`: + +```bash +source .venv/bin/activate +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +Windows PowerShell: + +```powershell +.\.venv\Scripts\Activate.ps1 +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +### 17.3. Validações imediatas + +Verifique saúde: + +```bash +curl http://localhost:8000/health +``` + +Listar agentes: + +```bash +curl http://localhost:8000/agents +``` + +Listar tools MCP conhecidas: + +```bash +curl http://localhost:8000/debug/mcp/tools +``` + +### 17.4. Como interpretar o resultado + +```text +/health ok → API subiu. +/agents lista → agents.yaml foi carregado. +/debug/mcp/tools → tools.yaml e mcp_servers.yaml foram carregados. +``` + +Se `/health` funciona mas `/agents` não lista o agente, o problema provavelmente está em `config/agents.yaml`. Se `/debug/mcp/tools` não mostra a tool, o problema provavelmente está em `tools.yaml` ou `mcp_servers.yaml`. + +--- + +## 18. Subindo MCP Servers + +### 18.1. Antes dos comandos: quando preciso subir MCP? + +Você precisa subir MCP quando a intent escolhida usa `mcp_tools` e o agente depende dessas tools para responder. + +Não precisa subir MCP para testar apenas: + +```text +health check +registro de agentes +roteamento básico +mock LLM sem tools +fluxo conversacional simples sem consulta externa +``` + +### 18.2. Subir MCP Server local + +Se os MCP Servers forem processos Python separados, suba cada um em uma porta distinta. + +Exemplo: + +```bash +cd ../mcp_servers/financeiro_mcp_server +source .venv/bin/activate +uvicorn main:app --host 0.0.0.0 --port 8300 --reload +``` + +Depois confirme que o endpoint configurado em `config/mcp_servers.yaml` está correto: + +```yaml +servers: + financeiro: + endpoint: http://localhost:8300/mcp +``` + +### 18.3. Testar tool pelo backend + +Teste pelo backend, não diretamente pelo MCP. Assim você valida o caminho completo: + +```text +backend → MCP Tool Router → MCP Server → resposta +``` + +```bash +curl -X POST http://localhost:8000/debug/mcp/call/consultar_titulo_financeiro \ + -H "Content-Type: application/json" \ + -d '{ + "business_context": { + "customer_key": "12345", + "contract_key": "ABC-999", + "session_key": "sessao-teste" + }, + "original_context": { + "session_id": "sessao-teste" + } + }' +``` + +### 18.4. Como interpretar erros MCP + +```text +Tool não encontrada → tools.yaml ou nome da tool errado. +Servidor não encontrado → mcp_servers.yaml não tem o mcp_server indicado pela tool. +Connection refused → MCP Server não está rodando ou porta errada. +Parâmetro obrigatório ausente → identity.yaml ou mcp_parameter_mapping.yaml incorreto. +Timeout → MCP lento, endpoint errado, VPN, DNS ou sistema real indisponível. +``` + +--- + +## 19. Build com Docker + +O Dockerfile do template espera copiar `agent_framework` e `agent_template_backend`. Portanto, rode o build a partir do diretório pai que contém ambos. + +Estrutura esperada: + +```text +workspace/ +├── agent_framework/ +└── agent_template_backend/ +``` + +Build: + +```bash +cd workspace +docker build -t agent-template-backend:local -f agent_template_backend/Dockerfile . +``` + +Run: + +```bash +docker run --rm -p 8000:8000 \ + --env-file agent_template_backend/.env \ + agent-template-backend:local +``` + +Health check: + +```bash +curl http://localhost:8000/health +``` + +--- + +## 20. Docker Compose sugerido + +Crie um `docker-compose.yaml` no diretório pai, se quiser subir backend, Redis, Langfuse e MCP Servers juntos. + +Exemplo simplificado: + +```yaml +services: + backend: + build: + context: . + dockerfile: agent_template_backend/Dockerfile + env_file: + - agent_template_backend/.env + ports: + - "8000:8000" + depends_on: + - redis + - financeiro-mcp + + redis: + image: redis:7 + ports: + - "6379:6379" + + financeiro-mcp: + build: + context: ./mcp_servers/financeiro_mcp_server + ports: + - "8300:8300" +``` + +Quando estiver em Docker, use `config/mcp_servers.docker.yaml` e ajuste o `.env`: + +```env +MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.docker.yaml +``` + +--- + +## 21. Testando o agente pelo Gateway + +### 21.1. Teste simples + +```bash +curl -X POST http://localhost:8000/gateway/message \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "agent_id": "financeiro_agent", + "tenant_id": "default", + "payload": { + "text": "Quero consultar meu pagamento", + "session_id": "teste-financeiro-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999", + "message_id": "msg-001" + } + }' +``` + +A resposta deve conter metadados como: + +```json +{ + "channel": "web", + "session_id": "default:financeiro_agent:teste-financeiro-001", + "text": "...", + "metadata": { + "route": "financeiro_agent", + "intent": "financeiro_pagamentos", + "mcp_results": [], + "business_context": { + "customer_key": "12345", + "contract_key": "ABC-999" + } + } +} +``` + +### 21.2. Teste de roteamento sem fixar `agent_id` + +```bash +curl -X POST http://localhost:8000/gateway/message \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "tenant_id": "default", + "payload": { + "text": "Meu pagamento ainda não foi baixado", + "session_id": "teste-router-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999" + } + }' +``` + +### 21.3. Teste de SSE + +Enviar mensagem com SSE: + +```bash +curl -X POST http://localhost:8000/gateway/message/sse \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "agent_id": "financeiro_agent", + "tenant_id": "default", + "payload": { + "text": "Preciso da segunda via do boleto", + "session_id": "teste-sse-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999" + } + }' +``` + +Abrir stream: + +```bash +curl -N http://localhost:8000/gateway/events/default:financeiro_agent:teste-sse-001 +``` + +Eventos esperados: + +```text +connected +flow.start +session.upserted +message.received +workflow.started +workflow.completed +message.responded +flow.end +``` + +--- + +## 22. Testando debug endpoints + +### 22.1. Roteamento + +```bash +curl -X POST http://localhost:8000/debug/route \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Quero consultar meu pagamento", + "context": { + "agent_id": "financeiro_agent", + "tenant_id": "default" + } + }' +``` + +### 22.2. Identidade + +```bash +curl -X POST http://localhost:8000/debug/identity \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "teste-id-001", + "customer_id": "12345", + "contract_id": "ABC-999", + "message_id": "msg-001" + }' +``` + +### 22.3. Mensagens da sessão + +```bash +curl http://localhost:8000/sessions/default:financeiro_agent:teste-financeiro-001/messages +``` + +### 22.4. Checkpoint + +```bash +curl http://localhost:8000/sessions/default:financeiro_agent:teste-financeiro-001/checkpoint +``` + +### 22.5. Uso/custo + +```bash +curl http://localhost:8000/debug/usage +``` + +--- + +## 23. Checklist de validação funcional + +Use este checklist antes de considerar o agente pronto. + +### 23.1. Configuração + +- [ ] `.env` sem credenciais reais versionadas. +- [ ] `LLM_PROVIDER` correto. +- [ ] `ROUTING_MODE` definido: `router` ou `supervisor`. +- [ ] `ENABLE_MCP_TOOLS` ajustado conforme necessidade. +- [ ] `MCP_SERVERS_CONFIG_PATH` aponta para o YAML correto. +- [ ] `IDENTITY_CONFIG_PATH` aponta para `config/identity.yaml`. +- [ ] Persistência local ou Autonomous configurada. + +### 23.2. Agente + +- [ ] Arquivo criado em `app/agents/.py`. +- [ ] Classe implementa `async def run(self, state)`. +- [ ] Agente herda `AgentRuntimeMixin`. +- [ ] Agente usa `get_runtime_context()` ou padrão equivalente para ler `state/context/session/business_context`. +- [ ] Agente usa `normalize_tools_by_intent()` quando precisa de fallback de tools por intent. +- [ ] Agente usa `build_tool_arguments()` ou `execute_tools_for_intent()` quando precisa de aliases/política de tools. +- [ ] Tools de ação em `tools.yaml` possuem `tool_type`, `requires` e, quando necessário, `confirmation_required`. +- [ ] Dev entende que `AgentRuntimeMixin` é infraestrutura compartilhada, não regra de negócio. +- [ ] Agente usa `_emit_ic()`, `_emit_noc()` ou `_emit_grl()` em vez de emitir observabilidade em formato próprio. +- [ ] Agente usa `_collect_mcp_context()` para consultas simples às tools declaradas em `routing.yaml`. +- [ ] Agente usa `_retrieve_rag_context()` quando precisa de contexto documental. +- [ ] Agente usa `_invoke_llm_cached()` para chamada LLM com cache e telemetria. +- [ ] Dev entende que `messages` é o contrato conversacional enviado ao LLM, não a memória persistente. +- [ ] `messages` separa regras permanentes no `system` e pedido/evidências no `user`. +- [ ] `messages` inclui apenas campos necessários de `session`, `business_context`, MCP e RAG. +- [ ] Agente não envia `state` completo, objetos enormes ou dados sensíveis desnecessários ao LLM. +- [ ] Agente deixa claro no prompt quando MCP/RAG falharam, para evitar resposta inventada. +- [ ] Agente não chama REST, banco, SOAP ou serviço externo diretamente quando isso deveria estar atrás de MCP. +- [ ] Agente separa `context`, `session`, `business_context` e `tool_arguments` antes de tomar decisões. +- [ ] Agente usa `business_context` para decisões de negócio e `session` para continuidade/rastreabilidade. +- [ ] Prompts específicos aplicam `apply_agent_profile_prompt()`. +- [ ] Tools são chamadas via `_collect_mcp_context()`. +- [ ] RAG é chamado via `_retrieve_rag_context()`, se aplicável. +- [ ] LLM é chamado via `_invoke_llm_cached()`. +- [ ] Retorno contém `answer`, `next_state`, `mcp_results` e, se aplicável, `rag`. + +### 23.3. Workflow + +- [ ] Agente importado em `agent_graph.py`. +- [ ] Agente instanciado no `__init__`. +- [ ] Nó adicionado no `StateGraph`. +- [ ] Rota adicionada em `add_conditional_edges`. +- [ ] Edge criada para `output_supervisor`. +- [ ] Handler adicionado no modo supervisor, se necessário. + +### 23.4. Roteamento + +- [ ] Intent adicionada em `config/routing.yaml`. +- [ ] Keywords suficientes. +- [ ] Examples coerentes. +- [ ] `agent` da intent bate com o nome do nó do workflow. +- [ ] `mcp_tools` da intent existem em `config/tools.yaml`. + +### 23.5. MCP + +- [ ] Tool declarada em `config/tools.yaml`. +- [ ] MCP Server declarado em `config/mcp_servers.yaml`. +- [ ] Mapeamento declarado em `config/mcp_parameter_mapping.yaml`. +- [ ] Tool testada via `/debug/mcp/call/{tool_name}`. +- [ ] Timeout e fallback definidos. + +### 23.6. Observabilidade + +- [ ] ICs de início e fim emitidos. +- [ ] ICs de coleta MCP/RAG emitidos quando aplicável. +- [ ] NOCs emitidos em erros técnicos relevantes. +- [ ] GRLs globais aparecem em input/output. +- [ ] Langfuse ou outro provider recebe traces, se habilitado. + +### 23.7. Testes + +- [ ] `/health` retorna `status=ok`. +- [ ] `/agents` lista o agente novo. +- [ ] `/debug/route` escolhe o agente correto. +- [ ] `/debug/identity` resolve as chaves esperadas. +- [ ] `/gateway/message` retorna resposta correta. +- [ ] `/gateway/message/sse` publica eventos. +- [ ] `/sessions/{session_id}/messages` mostra histórico. +- [ ] `/sessions/{session_id}/checkpoint` mostra checkpoint. + +--- + +## 24. Boas práticas de customização + +### Faça + +- Coloque regra de negócio no agente, não no framework. +- Use MCP para acesso a sistemas externos. +- Use `RuntimeContext`, `build_tool_arguments()` e `execute_tools_for_intent()` antes de criar helpers locais duplicados no agente. +- Use `identity.yaml` para normalizar chaves de negócio. +- Use `mcp_parameter_mapping.yaml` para adaptar nomes de parâmetros. +- Use IC para eventos de negócio. +- Use NOC para falhas técnicas. +- Use GRL para decisões de segurança/validação. +- Monte `messages` com separação clara entre instrução, pedido, evidência MCP, contexto RAG e formato de saída. +- Mantenha prompts por agente em `config/agents//prompt_policy.yaml`. +- Mantenha guardrails e judges isolados quando o agente tiver regras próprias. + +### Evite + +- Criar outro workflow fora de `AgentWorkflow` sem necessidade. +- Chamar REST/DB direto dentro do agente quando a chamada deveria ser tool MCP. +- Criar checkpointer próprio. +- Criar memória paralela fora do framework. +- Emitir telemetria em formato incompatível com `AgentObserver`. +- Colocar regra específica de um agente dentro do framework. +- Misturar histórico de agentes diferentes na mesma sessão. +- Enviar o `state` inteiro ou dumps grandes de tools/RAG diretamente dentro de `messages`. +- Colocar regras críticas apenas no `user` prompt quando deveriam estar no `system`. + +--- + +## 25. Troubleshooting + +### 25.1. `/gateway/message` retorna rota errada + +Verifique: + +```bash +curl -X POST http://localhost:8000/debug/route \ + -H "Content-Type: application/json" \ + -d '{"text":"sua frase de teste","context":{"agent_id":"financeiro_agent"}}' +``` + +Depois revise: + +```text +config/routing.yaml +keywords +examples +priority +ROUTING_MODE +ENABLE_LLM_ROUTER +``` + +### 25.2. Tool MCP não é chamada + +Verifique: + +```text +A intent em routing.yaml possui mcp_tools. +A tool existe em tools.yaml. +O MCP Server está em mcp_servers.yaml. +ENABLE_MCP_TOOLS=true. +O mapeamento existe em mcp_parameter_mapping.yaml. +A identidade tem as chaves necessárias. +``` + +### 25.3. Tool recebe parâmetro errado + +Revise: + +```text +config/identity.yaml +config/mcp_parameter_mapping.yaml +payload enviado ao /gateway/message +``` + +Use: + +```bash +curl -X POST http://localhost:8000/debug/identity \ + -H "Content-Type: application/json" \ + -d '{"session_id":"s1","customer_id":"123","contract_id":"C1"}' +``` + +### 25.4. SSE dá MIME type incorreto + +O endpoint correto é: + +```text +GET /gateway/events/{session_id} +``` + +O `session_id` precisa ser a chave canônica completa retornada pelo gateway: + +```text +tenant_id:agent_id:session_id_original +``` + +Exemplo: + +```text +default:financeiro_agent:teste-sse-001 +``` + +### 25.5. Langfuse não mostra traces + +Verifique: + +```env +ENABLE_LANGFUSE=true +LANGFUSE_PUBLIC_KEY= +LANGFUSE_SECRET_KEY= +LANGFUSE_HOST=http://localhost:3005 +``` + +E confira: + +```bash +curl http://localhost:8000/health +curl http://localhost:8000/debug/env +``` + +### 25.6. Banco Autonomous não conecta + +Para desenvolvimento, simplifique primeiro: + +```env +SESSION_REPOSITORY_PROVIDER=memory +MEMORY_REPOSITORY_PROVIDER=memory +CHECKPOINT_REPOSITORY_PROVIDER=memory +USAGE_REPOSITORY_PROVIDER=memory +``` + +Depois volte para `autonomous` quando wallet, DSN e variáveis estiverem corretos. + +--- + + +### 25.7. LLM responde inventando ou ignorando evidências + +Quando o LLM inventa dados, confirma uma ação inexistente ou ignora uma tool, nem sempre o problema está no modelo. Muitas vezes o problema está em como `messages` foi montado. + +Verifique: + +```text +O system prompt proíbe claramente inventar dados? +O user prompt separa evidências MCP de instruções? +A falha da tool foi informada explicitamente ao LLM? +O agente enviou um dump confuso de mcp_results em vez de um resumo útil? +O RAG trouxe documentos relevantes ou ruído? +O prompt pediu formato de resposta claro? +Há histórico duplicado confundindo a resposta? +``` + +Exemplo de correção: + +```text +Ruim: + Responda sobre o pagamento do cliente usando os dados abaixo: [...] + +Melhor: + A tool consultar_pagamentos_financeiro retornou ok=false. + Não confirme pagamento. + Informe que a evidência de pagamento não foi encontrada. +``` + +Em ambiente de desenvolvimento, registre uma versão sanitizada de `messages` para revisar o que realmente chegou ao LLM. Nunca registre prompts brutos com CPF, token, credencial, dados sensíveis ou payloads grandes de sistemas externos. + +## 26. Modelo mínimo de entrega de um novo agente + +Ao finalizar uma implementação, a entrega mínima deve conter: + +```text +app/agents/.py +config/agents.yaml +config/routing.yaml +config/tools.yaml +config/mcp_servers.yaml +config/mcp_parameter_mapping.yaml +config/identity.yaml +config/agents//prompt_policy.yaml +config/agents//guardrails.yaml +config/agents//judges.yaml +app/workflows/agent_graph.py +app/state.py, se necessário +.env.example ou documentação de variáveis +README.md com testes curl +``` + +--- + +## 27. Exemplo de teste completo + +```bash +# 1. Health +curl http://localhost:8000/health + +# 2. Agentes +curl http://localhost:8000/agents + +# 3. Tools MCP +curl http://localhost:8000/debug/mcp/tools + +# 4. Roteamento +curl -X POST http://localhost:8000/debug/route \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Quero consultar meu pagamento", + "context": {"agent_id": "financeiro_agent", "tenant_id": "default"} + }' + +# 5. Identidade +curl -X POST http://localhost:8000/debug/identity \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "teste-final-001", + "customer_id": "12345", + "contract_id": "ABC-999" + }' + +# 6. Mensagem real +curl -X POST http://localhost:8000/gateway/message \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "agent_id": "financeiro_agent", + "tenant_id": "default", + "payload": { + "text": "Quero consultar meu pagamento", + "session_id": "teste-final-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999", + "message_id": "msg-final-001" + } + }' + +# 7. Histórico +curl http://localhost:8000/sessions/default:financeiro_agent:teste-final-001/messages + +# 8. Checkpoint +curl http://localhost:8000/sessions/default:financeiro_agent:teste-final-001/checkpoint +``` + +--- + +## 28. Agent Gateway / Global Supervisor + +Este capítulo é uma tratativa à parte. Em uma arquitetura com vários agentes, não basta saber construir um backend de agente isolado. Em algum momento o frontend recebe uma mensagem do usuário e precisa decidir **qual backend de agente deve tratar aquela conversa**. + +Essa decisão não deve ficar espalhada no frontend, nem duplicada dentro de cada agente. Para isso existe o **Agent Gateway**, também chamado aqui de **Global Supervisor**. + +### 28.1. Antes do código: qual problema o Agent Gateway resolve? + +Imagine que a empresa tenha três backends independentes: + +```text +Backend Contas + resolve fatura, pagamento, consumo, segunda via, contestação + +Backend Ofertas + resolve planos, contratação, upgrade, retenção, desconto + +Backend Suporte + resolve internet lenta, sinal, rede, modem, falha técnica +``` + +Sem um gateway global, o frontend teria que saber regras como: + +```text +Se a mensagem tem "fatura", chamar Contas. +Se a mensagem tem "plano", chamar Ofertas. +Se a mensagem tem "internet lenta", chamar Suporte. +``` + +Isso parece simples no começo, mas vira problema quando: + +- surgem muitos agentes; +- uma conversa começa em Contas e depois muda para Ofertas; +- uma mensagem é ambígua, como “quero cancelar”; +- cada canal, Web, WhatsApp e Voz, começa a implementar sua própria regra; +- o desenvolvedor precisa manter roteamento, sessão e handoff em vários lugares. + +O **Agent Gateway** centraliza essa decisão. + +Ele recebe a mensagem normalizada do canal, descobre o backend correto e encaminha a requisição para o backend escolhido. + +```text +Usuário + ↓ +Frontend / Canal + ↓ +Agent Gateway / Global Supervisor + ↓ +Backend Contas | Backend Ofertas | Backend Suporte | Outros backends +``` + +O Gateway **não substitui o agente**. Ele não deve conter regra de negócio de fatura, oferta ou suporte. Ele apenas decide **quem deve receber a mensagem**. + +### 28.2. Diferença entre Supervisor do agente e Global Supervisor + +Dentro de um backend de agente, você pode ter um supervisor local. Esse supervisor decide entre caminhos internos do próprio agente. + +Exemplo dentro do agente de Contas: + +```text +Mensagem: "Minha fatura veio alta" + +Supervisor local do Backend Contas decide: + - explicar fatura + - consultar pagamentos + - abrir contestação + - chamar humano +``` + +O **Global Supervisor** decide em um nível acima: + +```text +Mensagem: "Minha internet está lenta" + +Global Supervisor decide: + - isso não é Contas + - isso deve ir para Suporte +``` + +A separação correta é: + +```text +Global Supervisor / Agent Gateway + decide o backend + +Supervisor local do backend + decide o fluxo interno do agente + +Agente especializado + executa a lógica de negócio +``` + +Essa separação evita que o framework ou o gateway fiquem contaminados com detalhes específicos de um domínio. + +### 28.3. O que pertence ao Agent Gateway + +O Gateway deve cuidar de responsabilidades transversais entre backends: + +```text +agent_gateway/ + app/main.py + expõe /gateway/message, /gateway/events/{session_id}, /debug/route, + /backends, /backends/health e /health + + app/settings.py + lê variáveis de ambiente do gateway global + + config/backends.yaml + declara quais backends existem, suas URLs, domínios, keywords e prioridade + + .env.example + documenta o modo de roteamento, TTL de sessão, timeout e provider LLM +``` + +O Gateway pode usar motores do framework para: + +- roteamento global; +- sessão global; +- client HTTP para backends; +- supervisor LLM; +- observabilidade; +- publicação de eventos; +- proxy SSE. + +No arquivo `agent_gateway/app/main.py`, o gateway usa componentes do framework como: + +```python +from agent_framework.global_supervisor import ( + BackendClient, + BackendRegistry, + GlobalRouteRequest, + GlobalSupervisorRouter, + InMemoryGlobalSessionStore, +) +``` + +Isso significa que o gateway não está criando um mecanismo paralelo de roteamento. Ele está usando uma camada própria do framework para governar múltiplos backends. + +### 28.4. O que não pertence ao Agent Gateway + +O Gateway não deve implementar regras específicas como: + +```text +consultar_fatura +consultar_pagamentos +abrir_contestacao +consultar_imdb +buscar_speech_analytics +abrir_sr_siebel +calcular_pro_rata +resolver_ean +``` + +Essas funcionalidades pertencem aos backends especializados ou aos MCP servers. + +Uma regra prática: + +```text +Se a lógica depende do negócio de um agente específico, ela não deve ficar no Gateway. +Se a lógica decide qual backend deve tratar a conversa, ela pode ficar no Gateway. +``` + +### 28.5. Estrutura do projeto `agent_gateway` + +A estrutura mínima observada no projeto é: + +```text +agent_gateway/ + app/ + main.py + settings.py + config/ + backends.yaml + docs/ + ARQUITETURA_GLOBAL_SUPERVISOR.md + .env.example + Dockerfile + README.md + requirements.txt +``` + +Cada arquivo tem uma responsabilidade clara: + +| Arquivo | Responsabilidade | +|---|---| +| `app/main.py` | expõe endpoints HTTP, chama o router global, encaminha mensagens aos backends e faz proxy SSE | +| `app/settings.py` | centraliza variáveis do gateway global | +| `config/backends.yaml` | cadastra backends disponíveis e regras de roteamento por domínio/keyword | +| `.env.example` | documenta como ligar/desligar modos de roteamento e providers | +| `Dockerfile` | empacota o gateway como serviço separado | +| `docs/ARQUITETURA_GLOBAL_SUPERVISOR.md` | explica a arquitetura conceitual | + +### 28.6. Como o desenvolvedor deve pensar antes de configurar o Gateway + +Antes de editar `config/backends.yaml`, o desenvolvedor deve responder quatro perguntas: + +```text +1. Quais backends de agente existem? +2. Qual é o domínio de responsabilidade de cada backend? +3. Quais palavras ou exemplos indicam cada domínio? +4. O que deve acontecer quando a mensagem for ambígua? +``` + +Exemplo: + +```text +Mensagem: "Quero cancelar" +``` + +Essa mensagem pode significar: + +```text +Cancelar serviço avulso → talvez Contas ou Ofertas +Cancelar plano inteiro → talvez Ofertas ou Retenção +Cancelar por problema rede → talvez Suporte +``` + +Nesse caso, o router por keyword pode não ser suficiente. O modo `hybrid` pode manter o backend ativo se a conversa já tiver contexto, ou chamar o supervisor LLM se houver conflito. + +### 28.7. Configurando os backends em `config/backends.yaml` + +O arquivo principal de configuração do Gateway é: + +```text +agent_gateway/config/backends.yaml +``` + +Exemplo: + +```yaml +default_backend: contas + +backends: + contas: + url: http://localhost:8001 + description: Backend responsável por faturas, contas, pagamentos, consumo, segunda via e contestação. + domains: [contas, fatura, pagamento, consumo, contestacao] + keywords: [fatura, conta, boleto, pagamento, consumo, segunda via, contestar, contestação, valor, cobrança] + examples: + - Quero consultar minha fatura + - Minha conta veio alta + - Preciso da segunda via do boleto + priority: 10 + default_agent_id: telecom_contas + + ofertas: + url: http://localhost:8002 + description: Backend responsável por ofertas, planos, upgrades, retenção e contratação. + domains: [ofertas, planos, retenção, contratação] + keywords: [oferta, plano, contratar, upgrade, desconto, promoção, pacote, retenção, cancelar serviço] + examples: + - Quero trocar meu plano + - Tem alguma oferta para mim? + - Quero cancelar um serviço + priority: 20 + default_agent_id: telecom_ofertas + + suporte: + url: http://localhost:8003 + description: Backend responsável por suporte técnico, falhas, rede, internet e atendimento operacional. + domains: [suporte, técnico, rede, internet] + keywords: [internet, sinal, rede, suporte, técnico, problema, falha, sem conexão, modem] + examples: + - Minha internet está lenta + - Estou sem sinal + - Preciso de suporte técnico + priority: 30 + default_agent_id: telecom_suporte +``` + +O desenvolvedor não deve preencher esse YAML como uma lista aleatória de palavras. Ele deve pensar em **famílias de intenção**. + +Exemplo correto: + +```text +Família: contas + assuntos: fatura, pagamento, consumo, segunda via, contestação +``` + +Exemplo ruim: + +```text +Família: qualquer coisa que tenha "valor" +``` + +A palavra “valor” pode aparecer em fatura, oferta, desconto, contestação ou cobrança. Palavras genéricas devem ser usadas com cuidado. + +### 28.8. Escolhendo o modo de roteamento global + +O `.env` do gateway possui a variável: + +```env +GLOBAL_ROUTING_MODE=hybrid +``` + +Os modos possíveis são: + +| Modo | Como decide | Quando usar | +|---|---|---| +| `router` | usa regras, keywords, domínios e prioridade | desenvolvimento local, testes determinísticos, ambientes com baixa ambiguidade | +| `supervisor` | usa LLM para escolher backend | domínios muito parecidos ou mensagens muito abertas | +| `hybrid` | mantém backend ativo, usa regra e chama LLM em conflito | recomendado para produção inicial | + +A decisão prática é: + +```text +Se você quer previsibilidade total, use router. +Se você quer interpretação semântica forte, use supervisor. +Se você quer equilíbrio entre contexto, regra e LLM, use hybrid. +``` + +Para a maioria dos projetos corporativos, comece com: + +```env +GLOBAL_ROUTING_MODE=hybrid +GLOBAL_KEEP_ACTIVE_BACKEND=true +GLOBAL_USE_SUPERVISOR_ON_CONFLICT=true +GLOBAL_MIN_ROUTER_CONFIDENCE=0.55 +``` + +### 28.9. Entendendo sessão global e sessão do backend + +O Gateway mantém uma sessão global, por exemplo: + +```text +global_session_id = s1 +``` + +O backend pode manter outra sessão interna, por exemplo: + +```text +backend_session_id = default:telecom_contas:s1 +``` + +O código do Gateway ajusta a resposta para manter os dois identificadores no `metadata`: + +```json +{ + "session_id": "s1", + "metadata": { + "global_session_id": "s1", + "backend_session_id": "default:telecom_contas:s1", + "selected_backend": "contas" + } +} +``` + +Essa separação é importante porque o usuário conversa com uma sessão global, mas cada backend pode precisar de sua própria chave interna para memória, checkpoint e histórico. + +### 28.9.1. Como o Gateway deve entregar sessão ao backend + +Para que o agente consiga entender de onde veio a conversa, o Gateway deve encaminhar a sessão dentro de `context.session` ou em uma estrutura equivalente normalizada pelo framework. + +Exemplo de payload conceitual que chega ao backend: + +```json +{ + "channel": "web", + "tenant_id": "default", + "agent_id": "financeiro_agent", + "payload": { + "text": "Quero consultar meu pagamento", + "session_id": "s1", + "customer_id": "12345" + }, + "context": { + "session": { + "global_session_id": "s1", + "backend_session_id": "default:financeiro_agent:s1", + "active_backend": "financeiro", + "channel": "web", + "tenant_id": "default", + "metadata": { + "selected_backend": "financeiro", + "route_confidence": 0.82 + } + }, + "business_context": { + "customer_key": "12345", + "session_key": "default:financeiro_agent:s1" + } + } +} +``` + +O desenvolvedor do agente deve entender que `context.session` não é “mais um lugar para buscar qualquer parâmetro”. Ele é o contrato de continuidade da conversa. Para chamadas MCP, prefira sempre `business_context` e `tool_arguments`. + +### 28.10. Subindo o Agent Gateway localmente + +Entre no diretório do gateway: + +```bash +cd agent_gateway +``` + +Copie o arquivo de ambiente: + +```bash +cp .env.example .env +``` + +Configure o `PYTHONPATH` para enxergar o framework: + +```bash +export PYTHONPATH=../agent_framework/src:. +``` + +Suba o serviço: + +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8010 --reload +``` + +Valide o health: + +```bash +curl http://localhost:8010/health +``` + +Resposta esperada: + +```json +{ + "status": "ok", + "app": "agent-gateway-global-supervisor", + "routing_mode": "hybrid", + "backends": ["contas", "ofertas", "suporte"], + "llm_provider": "mock" +} +``` + +Se esse endpoint não responder, o problema ainda está no gateway, não nos backends. + +### 28.11. Subindo os backends de agente + +O Gateway só roteia corretamente se os backends configurados em `backends.yaml` estiverem de pé. + +Exemplo local: + +```text +Gateway http://localhost:8010 +Contas http://localhost:8001 +Ofertas http://localhost:8002 +Suporte http://localhost:8003 +Frontend http://localhost:5173 +``` + +Cada backend precisa expor, no mínimo: + +```text +GET /health +POST /gateway/message +GET /gateway/events/{session_id} +``` + +O endpoint `/backends/health` do Gateway verifica a saúde dos backends: + +```bash +curl http://localhost:8010/backends/health +``` + +Use esse teste antes de culpar o roteamento. Se o backend está fora do ar, o Gateway pode até escolher corretamente, mas falhará no encaminhamento. + +### 28.12. Testando apenas a decisão de rota + +Antes de enviar uma mensagem real para o backend, teste a decisão: + +```bash +curl -X POST http://localhost:8010/debug/route \ + -H 'content-type: application/json' \ + -d '{ + "channel": "web", + "payload": { + "text": "Minha fatura veio alta", + "session_id": "s1" + } + }' +``` + +Resultado esperado: + +```json +{ + "backend_id": "contas", + "confidence": 0.8, + "reason": "Backend escolhido por regras: matches=['fatura']" +} +``` + +O desenvolvedor deve interpretar o resultado assim: + +```text +backend_id → para qual backend o gateway mandaria a mensagem +confidence → quão forte foi a decisão +reason → por que a decisão foi tomada +``` + +Se o backend escolhido estiver errado, ajuste `domains`, `keywords`, `examples`, `priority` ou o modo de roteamento. + +### 28.13. Enviando mensagem real pelo Gateway + +Depois que a decisão de rota estiver correta, envie a mensagem real: + +```bash +curl -X POST http://localhost:8010/gateway/message \ + -H 'content-type: application/json' \ + -d '{ + "channel": "web", + "payload": { + "text": "Minha fatura veio alta", + "session_id": "s1", + "msisdn": "11999999999" + } + }' +``` + +O Gateway fará: + +```text +1. Receber a mensagem. +2. Emitir IC.GLOBAL_GATEWAY_RECEIVED. +3. Criar uma GlobalRouteRequest. +4. Chamar GlobalSupervisorRouter. +5. Escolher o backend. +6. Emitir IC.GLOBAL_BACKEND_SELECTED. +7. Encaminhar para o /gateway/message do backend. +8. Guardar o active_backend da sessão. +9. Acrescentar metadados de rota na resposta. +10. Emitir IC.GLOBAL_GATEWAY_COMPLETED. +``` + +### 28.14. Handoff entre backends + +O handoff acontece quando um backend percebe que a conversa deve mudar de domínio. + +Exemplo: + +```text +Usuário começou em Contas: + "Minha fatura veio alta" + +Depois perguntou: + "Tem algum plano melhor para reduzir esse valor?" +``` + +O backend de Contas pode responder com metadata pedindo troca: + +```json +{ + "metadata": { + "handover_backend": "ofertas" + } +} +``` + +O Gateway detecta esse campo e chama automaticamente o novo backend. + +O desenvolvedor precisa entender que handoff não é erro. É uma transição controlada entre domínios. + +### 28.15. Proxy SSE pelo Gateway + +O Gateway também possui endpoint: + +```text +GET /gateway/events/{session_id} +``` + +Esse endpoint faz proxy do SSE do backend ativo. + +Fluxo: + +```text +Frontend abre EventSource no Gateway + ↓ +Gateway espera existir sessão global + ↓ +Gateway descobre active_backend + ↓ +Gateway monta URL SSE do backend + ↓ +Gateway repassa os eventos text/event-stream para o frontend +``` + +Teste: + +```bash +curl -N http://localhost:8010/gateway/events/s1 +``` + +Eventos esperados no início: + +```text +event: connected +data: {"session_id":"s1","component":"agent_gateway"} + +``` + +Depois que uma mensagem for enviada para `/gateway/message`, o Gateway deve emitir algo como: + +```text +event: backend.selected +data: {"session_id":"s1","backend_id":"contas","backend_session_id":"s1"} +``` + +Se aparecer erro de MIME type, o backend ativo provavelmente não está retornando `text/event-stream` em `/gateway/events/{session_id}`. + +### 28.16. IC e NOC do Agent Gateway + +O Gateway deve emitir eventos próprios, diferentes dos eventos internos dos agentes. + +Eventos encontrados no projeto: + +| Evento | Significado | +|---|---| +| `IC.GLOBAL_GATEWAY_RECEIVED` | Gateway recebeu mensagem do canal | +| `IC.GLOBAL_BACKEND_SELECTED` | Gateway escolheu um backend | +| `IC.GLOBAL_BACKEND_HANDOVER` | Houve troca de backend durante a conversa | +| `IC.GLOBAL_GATEWAY_COMPLETED` | Gateway concluiu o encaminhamento | +| `NOC.005` | falha operacional no Gateway ou na chamada ao backend | +| `NOC.006` | conclusão HTTP observada pelo middleware | + +Esses eventos não substituem os IC/NOC/GRL do backend. Eles complementam a visão ponta a ponta. + +Em uma rastreabilidade completa, você deve conseguir enxergar: + +```text +IC.GLOBAL_GATEWAY_RECEIVED +IC.GLOBAL_BACKEND_SELECTED +IC.BACKEND_WORKFLOW_STARTED +IC.TOOL_CALLED +GRL.INPUT_STARTED +GRL.OUTPUT_COMPLETED +IC.BACKEND_WORKFLOW_COMPLETED +IC.GLOBAL_GATEWAY_COMPLETED +``` + +### 28.17. Como integrar o frontend ao Agent Gateway + +O frontend não deve chamar diretamente cada backend de agente. + +Em vez disso, ele deve apontar para: + +```text +POST http://localhost:8010/gateway/message +GET http://localhost:8010/gateway/events/{session_id} +``` + +O frontend continua enviando uma mensagem normalizada: + +```json +{ + "channel": "web", + "payload": { + "text": "Minha fatura veio alta", + "session_id": "s1" + } +} +``` + +O frontend não precisa saber se a mensagem foi para Contas, Ofertas ou Suporte. Essa informação pode aparecer em `metadata.selected_backend`, mas não deve virar regra de negócio no frontend. + +### 28.18. Build do Gateway com Docker + +O Dockerfile do Gateway usa: + +```dockerfile +FROM python:3.12-slim +WORKDIR /app +COPY agent_framework /agent_framework +COPY agent_gateway /app +RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"] +``` + +Isso pressupõe que, no contexto de build, existam os diretórios: + +```text +agent_framework/ +agent_gateway/ +``` + +Build: + +```bash +docker build -t agent-gateway:local -f agent_gateway/Dockerfile . +``` + +Run: + +```bash +docker run --rm -p 8010:8010 \ + --env-file agent_gateway/.env \ + agent-gateway:local +``` + +### 28.19. Checklist de implementação do Agent Gateway + +Antes de considerar o Gateway pronto, valide: + +```text +[ ] /health responde. +[ ] /backends lista todos os backends esperados. +[ ] /backends/health consegue chamar cada backend. +[ ] /debug/route escolhe o backend correto para mensagens óbvias. +[ ] /debug/route explica o motivo da decisão. +[ ] /gateway/message encaminha para o backend escolhido. +[ ] response.metadata.selected_backend aparece na resposta. +[ ] response.metadata.global_route_decision aparece na resposta. +[ ] /debug/sessions mostra active_backend após primeira mensagem. +[ ] /gateway/events/{session_id} retorna text/event-stream. +[ ] handoff_backend funciona quando um backend solicita troca. +[ ] IC.GLOBAL_* aparece na observabilidade. +[ ] NOC.005 aparece em falhas reais de backend. +``` + +### 28.20. Erros comuns no Agent Gateway + +#### Erro 1: Gateway escolhe backend errado + +Causas comuns: + +```text +keywords genéricas demais +priority mal definida +examples insuficientes +GLOBAL_MIN_ROUTER_CONFIDENCE muito baixo +modo router usado para domínio ambíguo +``` + +Correção: + +```text +1. Teste /debug/route. +2. Leia o campo reason. +3. Ajuste domains, keywords e examples. +4. Se continuar ambíguo, use hybrid ou supervisor. +``` + +#### Erro 2: Gateway escolhe certo, mas retorna 502 + +Isso normalmente significa que o backend escolhido está fora do ar ou não expõe `/gateway/message`. + +Teste: + +```bash +curl http://localhost:8001/health +curl -X POST http://localhost:8001/gateway/message \ + -H 'content-type: application/json' \ + -d '{"channel":"web","payload":{"text":"teste","session_id":"s1"}}' +``` + +#### Erro 3: SSE retorna `application/json` em vez de `text/event-stream` + +O backend ativo precisa expor SSE corretamente. + +Teste direto no backend: + +```bash +curl -i -N http://localhost:8001/gateway/events/s1 +``` + +O header esperado é: + +```text +content-type: text/event-stream +``` + +#### Erro 4: Sessão global existe, mas o backend ativo não aparece + +Verifique: + +```bash +curl http://localhost:8010/debug/sessions +``` + +Depois envie uma mensagem por `/gateway/message`. O `active_backend` só é definido depois que o Gateway roteia uma mensagem com sucesso. + +### 28.21. Como explicar essa arquitetura para um novo desenvolvedor + +Uma forma simples de ensinar é: + +```text +O backend de agente sabe resolver um tipo de problema. +O Gateway sabe escolher qual backend deve resolver o problema. +O framework fornece os motores reutilizáveis para ambos. +``` + +Portanto, ao implementar um novo agente, o desenvolvedor deve fazer duas integrações: + +```text +1. Criar o backend especializado usando agent_template_backend. +2. Registrar esse backend no agent_gateway/config/backends.yaml. +``` + +Ele não deve alterar o frontend para cada novo agente. Também não deve colocar regra de negócio do novo agente dentro do Gateway. + + +--- + +## 29. Conclusão + +O `agent_template_backend` fornece a espinha dorsal corporativa para novos agentes. A implementação de um agente novo deve se limitar ao domínio: prompts, regras, tools, clients, schemas e decisões específicas. + +O padrão correto é: + +```text +Framework = motor reutilizável +Agente = customização de negócio +MCP = fronteira padronizada com sistemas externos +Config YAML = comportamento alterável sem mexer no motor +IC/NOC/GRL = rastreabilidade corporativa +``` + +Um desenvolvedor não deve apenas copiar arquivos. Ele deve entender que cada alteração representa uma decisão arquitetural: + +```text +Criar agente → define a lógica de domínio. +Registrar workflow → torna o agente executável pelo LangGraph. +Ajustar state → compartilha dados entre nós. +Configurar agents → declara o agente para o framework. +Configurar routing → ensina o framework quando chamar o agente. +Configurar tools → declara capacidades externas. +Configurar MCP → conecta tools a sistemas ou mocks. +Configurar identity→ normaliza chaves de negócio. +Emitir IC/NOC/GRL → torna a execução auditável. +Testar gateway → valida o fluxo real fim a fim. +``` + +Seguindo esse modelo, novos agentes podem ser criados com padronização, escalabilidade, rastreabilidade e manutenção mais simples. + + +## 30. Entrega final com Agent Gateway + +Ao final da implementação, a entrega recomendada deve conter quatro projetos ou diretórios claramente separados: + +```text +agent_framework/ + biblioteca reutilizável com motores de workflow, routing, guardrails, + judges, supervisor, memória, checkpoint, observabilidade e MCP tool router + +agent_template_backend/ + backend especializado de um agente, com domínio, prompts, tools, + state, workflow e configurações próprias + +agent_gateway/ + global supervisor que roteia conversas entre vários backends de agentes + +agent_frontend/ + interface Web, WhatsApp ou Voz que conversa com o Agent Gateway +``` + +A relação correta é: + +```text +Frontend + chama Agent Gateway + +Agent Gateway + escolhe o backend + +Backend do agente + executa o workflow especializado + +MCP Server + executa ou simula ferramentas de negócio + +Framework + fornece os motores reutilizáveis para gateway e backends +``` + +### 30.1. Sequência final de subida local + +Uma sequência local completa pode ser: + +```bash +# 1. Subir MCP do agente, se existir +cd mcp_servers/meu_agente_mcp +uvicorn app.main:app --host 0.0.0.0 --port 9001 --reload + +# 2. Subir backend do agente Contas +cd agent_template_backend +cp .env.example .env +uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload + +# 3. Subir Agent Gateway +cd agent_gateway +cp .env.example .env +export PYTHONPATH=../agent_framework/src:. +uvicorn app.main:app --host 0.0.0.0 --port 8010 --reload + +# 4. Subir frontend +cd agent_frontend +npm install +npm run dev +``` + +### 30.2. Sequência final de testes + +```bash +# Gateway vivo +curl http://localhost:8010/health + +# Backends registrados +curl http://localhost:8010/backends + +# Saúde dos backends +curl http://localhost:8010/backends/health + +# Decisão de rota +curl -X POST http://localhost:8010/debug/route \ + -H 'content-type: application/json' \ + -d '{"channel":"web","payload":{"text":"Minha fatura veio alta","session_id":"s1"}}' + +# Mensagem real ponta a ponta +curl -X POST http://localhost:8010/gateway/message \ + -H 'content-type: application/json' \ + -d '{"channel":"web","payload":{"text":"Minha fatura veio alta","session_id":"s1","msisdn":"11999999999"}}' + +# Sessões globais +curl http://localhost:8010/debug/sessions + +# SSE pelo Gateway +curl -N http://localhost:8010/gateway/events/s1 +``` + +### 30.3. Critério de aceite arquitetural + +A implementação está arquiteturalmente correta quando: + +```text +[ ] o frontend não conhece URLs individuais dos backends de agentes; +[ ] o Gateway não contém regra de negócio específica de fatura, oferta ou suporte; +[ ] cada backend continua independente; +[ ] cada backend usa os motores do framework; +[ ] o Gateway usa o GlobalSupervisorRouter do framework; +[ ] o roteamento global é observável; +[ ] cada troca de backend gera metadados e evento de handoff; +[ ] os MCP servers continuam plugáveis por backend/agente; +[ ] a sessão global e a sessão do backend são preservadas no metadata; +[ ] o desenvolvedor consegue testar rota antes de testar execução real. +``` + +Com esse desenho, adicionar um novo agente não exige reescrever o frontend nem copiar lógica entre backends. O desenvolvedor cria o backend especializado, registra no Agent Gateway e deixa o framework cuidar dos motores transversais. + +## Política read-only/transacional + +Este template inclui o arquivo opcional `config/tool_policies.yaml`. Use `operation_type: read_only` para consultas e `operation_type: transactional` com `require_confirmation: true` para ações que só podem executar após confirmação booleana explícita. Se o arquivo for removido ou não existir em um template antigo, os campos legados de `config/tools.yaml` continuam válidos. + +## Workflows transacionais determinísticos + +Além da execução direta de MCP tools, uma operação transacional pode usar um workflow LangGraph determinístico após `clarification` e confirmação explícita. Configure `execution.mode: workflow` em `config/tool_policies.yaml`, mantenha as definições versionadas em `workflows/` e implemente as actions do domínio no projeto do agente. O runtime genérico está em `agent_framework.workflows`. + +Consulte `libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md` e o exemplo `workflows/devolucao_pedido.v1.yaml`. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README_ENTERPRISE_TEMPLATE.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README_ENTERPRISE_TEMPLATE.md new file mode 100644 index 0000000..cae516e --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/README_ENTERPRISE_TEMPLATE.md @@ -0,0 +1,54 @@ +# Agent Template Backend Enterprise + +Este folder é uma cópia completa do `agent_template_backend`, sem cortes de +arquitetura. Ele mantém workflow, router, output supervisor, guardrails, +analytics, observer, MCP, memória, checkpoints e configurações. + +A diferença é que a lógica de negócio dos agentes de exemplo foi removida da +execução e preservada comentada nos próprios arquivos: + +- `app/agents/billing_agent.py` +- `app/agents/product_agent.py` +- `app/agents/orders_agent.py` +- `app/agents/support_agent.py` + +## O que o desenvolvedor deve alterar + +1. Escolher ou criar um agente em `app/agents/`. +2. Implementar o método `run()`. +3. Ajustar prompts e tools, se necessário. +4. Emitir ICs de negócio relevantes para a jornada. +5. Manter NOC/GRL nos pontos operacionais e de guardrails. + +## O que já está integrado + +- `AgentObserver` +- `observer.emit_ic()` +- `observer.emit_noc()` +- `observer.emit_grl()` +- `AnalyticsPublisher` +- OCI Streaming +- GCP Pub/Sub +- OutputSupervisor +- GuardrailPipeline com suporte a execução paralela/fail-fast no framework +- MCP Tool Router +- LangGraph +- Memory +- Checkpoint +- Langfuse / OpenTelemetry + +## Exemplos adicionados + +Veja `app/examples/`: + +- `ic_examples.py` +- `noc_examples.py` +- `grl_examples.py` +- `mcp_examples.py` +- `observer_examples.py` + +## Convenção rápida + +- IC = evento de negócio / curadoria / informacional. +- NOC = evento operacional / saúde técnica. +- GRL = evento de guardrail / segurança / validação. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/__init__.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/README.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/README.md new file mode 100644 index 0000000..2917425 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/README.md @@ -0,0 +1,15 @@ +# Agentes do Template Backend Enterprise + +Os arquivos desta pasta preservam a estrutura real esperada pelo workflow, mas +não executam lógica de negócio pronta. + +Cada agente mostra: + +- como emitir IC; +- como emitir NOC; +- como emitir GRL; +- como coletar MCP via `_collect_tool_context()`; +- como recuperar RAG via `_retrieve_rag_context()`; +- onde chamar LLM/cache. + +A implementação original do exemplo está comentada no fim de cada arquivo. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/billing_agent.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/billing_agent.py new file mode 100644 index 0000000..aa60099 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/billing_agent.py @@ -0,0 +1,129 @@ +from app.agents.prompting import apply_agent_profile_prompt +from app.agents.runtime import AgentRuntimeMixin + + +class BillingAgent(AgentRuntimeMixin): + name = "billingAgent" + + def __init__( + self, + llm, + telemetry=None, + tool_router=None, + rag_service=None, + cache=None, + settings=None, + observer=None, + memory=None, + summary_memory=None, + ): + self.llm = llm + self.telemetry = telemetry + self.tool_router = tool_router + self.rag_service = rag_service + self.cache = cache + self.settings = settings + self.observer = observer + self.memory = memory + self.summary_memory = summary_memory + + async def run(self, state): + await self._emit_ic( + "IC.BILLING_AGENT_STARTED", + state, + {"business_component": "faturas"}, + component="agent.billing.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.BILLING_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.billing.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="BillingAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.BILLING_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.billing.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + await self.prepare_memory_context(state) + + messages = self.build_messages( + state, + system_prompt=apply_agent_profile_prompt( + state, + "Você é um agente especialista em faturas. Responda com clareza, objetividade e sem sugerir ações não solicitadas. Use dados MCP quando disponíveis.", + ), + mcp_results=tool_context, + rag_context=rag_context, + rag_metadata=rag_metadata, + ) + + answer = await self._invoke_llm_cached(state, "BillingAgent", messages) + result = { + "answer": f"[BillingAgent] {answer}", + "next_state": "BILLING_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.BILLING_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.billing.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/orders_agent.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/orders_agent.py new file mode 100644 index 0000000..f557bed --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/orders_agent.py @@ -0,0 +1,129 @@ +from app.agents.prompting import apply_agent_profile_prompt +from app.agents.runtime import AgentRuntimeMixin + + +class OrdersAgent(AgentRuntimeMixin): + name = "orders_agent" + + def __init__( + self, + llm, + telemetry=None, + tool_router=None, + rag_service=None, + cache=None, + settings=None, + observer=None, + memory=None, + summary_memory=None, + ): + self.llm = llm + self.telemetry = telemetry + self.tool_router = tool_router + self.rag_service = rag_service + self.cache = cache + self.settings = settings + self.observer = observer + self.memory = memory + self.summary_memory = summary_memory + + async def run(self, state): + await self._emit_ic( + "IC.ORDERS_AGENT_STARTED", + state, + {"business_component": "pedidos"}, + component="agent.orders.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.ORDERS_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.orders.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="OrdersAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.ORDERS_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.orders.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + await self.prepare_memory_context(state) + + messages = self.build_messages( + state, + system_prompt=apply_agent_profile_prompt( + state, + "Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.", + ), + mcp_results=tool_context, + rag_context=rag_context, + rag_metadata=rag_metadata, + ) + + answer = await self._invoke_llm_cached(state, "OrdersAgent", messages) + result = { + "answer": f"[OrdersAgent] {answer}", + "next_state": "ORDER_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.ORDERS_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.orders.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/product_agent.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/product_agent.py new file mode 100644 index 0000000..34433f5 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/product_agent.py @@ -0,0 +1,129 @@ +from app.agents.prompting import apply_agent_profile_prompt +from app.agents.runtime import AgentRuntimeMixin + + +class ProductAgent(AgentRuntimeMixin): + name = "productAgent" + + def __init__( + self, + llm, + telemetry=None, + tool_router=None, + rag_service=None, + cache=None, + settings=None, + observer=None, + memory=None, + summary_memory=None, + ): + self.llm = llm + self.telemetry = telemetry + self.tool_router = tool_router + self.rag_service = rag_service + self.cache = cache + self.settings = settings + self.observer = observer + self.memory = memory + self.summary_memory = summary_memory + + async def run(self, state): + await self._emit_ic( + "IC.PRODUCT_AGENT_STARTED", + state, + {"business_component": "produtos"}, + component="agent.product.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.PRODUCT_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.product.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="ProductAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.PRODUCT_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.product.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + await self.prepare_memory_context(state) + + messages = self.build_messages( + state, + system_prompt=apply_agent_profile_prompt( + state, + "Você é um agente especialista em produtos, planos e serviços. Explique sem fazer oferta proativa e sem executar ações sem confirmação. Use dados MCP quando disponíveis.", + ), + mcp_results=tool_context, + rag_context=rag_context, + rag_metadata=rag_metadata, + ) + + answer = await self._invoke_llm_cached(state, "ProductAgent", messages) + result = { + "answer": f"[ProductAgent] {answer}", + "next_state": "PRODUCT_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.PRODUCT_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.product.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/prompting.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/prompting.py new file mode 100644 index 0000000..255422b --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/prompting.py @@ -0,0 +1,15 @@ +from __future__ import annotations + + +def apply_agent_profile_prompt(state: dict, default_prompt: str) -> str: + """Adiciona o prefixo de prompt configurado para o agent_template selecionado. + + Cada agent_id pode definir metadata.system_prefix em config/agents.yaml. Isso + mantém prompts isolados sem duplicar o código dos agentes especializados. + """ + profile = state.get("agent_profile") or (state.get("context") or {}).get("agent_profile") or {} + metadata = profile.get("metadata") or {} + prefix = (metadata.get("system_prefix") or "").strip() + if not prefix: + return default_prompt + return f"{prefix}\n\n{default_prompt}" diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/runtime.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/runtime.py new file mode 100644 index 0000000..1b5c57f --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/runtime.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from agent_framework.runtime import ( + AgentRuntimeMixin as FrameworkAgentRuntimeMixin, + MessageBuilder, + RuntimeContext, +) +from agent_framework.workflows import FileWorkflowRepository, WorkflowRuntime, WorkflowToolExecutor + +# Importa as actions do domínio para registrá-las no registry global do framework. +import app.workflow_actions # noqa: F401 + +logger = logging.getLogger("app.agents.transactional_workflow_runtime") + + +class AgentRuntimeMixin(FrameworkAgentRuntimeMixin): + """Runtime do template com execução determinística opt-in por tool policy. + + O fluxo conversacional, clarification e confirmação continuam no runtime + oficial. Depois da confirmação, tools com ``execution.mode: workflow`` são + desviadas para o WorkflowToolExecutor. Todas as demais seguem pelo MCP. + """ + + _workflow_tool_executor: WorkflowToolExecutor | None = None + + def _transactional_workflows_enabled(self) -> bool: + return bool(getattr(getattr(self, "settings", None), "ENABLE_TRANSACTIONAL_WORKFLOWS", False)) + + def _get_workflow_tool_executor(self) -> WorkflowToolExecutor: + executor = getattr(self, "_workflow_tool_executor", None) + if executor is not None: + return executor + + settings = getattr(self, "settings", None) + configured_path = getattr(settings, "WORKFLOWS_PATH", "./workflows") if settings else "./workflows" + workflow_path = Path(configured_path) + if not workflow_path.is_absolute(): + workflow_path = Path.cwd() / workflow_path + + runtime = WorkflowRuntime(FileWorkflowRepository(workflow_path)) + executor = WorkflowToolExecutor(runtime) + self._workflow_tool_executor = executor + logger.info("Transactional workflow runtime initialized path=%s", workflow_path) + return executor + + async def _call_mcp_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + state: dict[str, Any], + ) -> dict[str, Any]: + args = dict(arguments or {}) + policy = self._resolve_tool_execution_policy(tool_name, args) + execution = dict(policy.get("execution") or {}) + + if self._transactional_workflows_enabled() and execution.get("mode") == "workflow": + executor = self._get_workflow_tool_executor() + await self._emit_ic( + "IC.TRANSACTIONAL_WORKFLOW_STARTED", + state, + { + "tool_name": tool_name, + "workflow_name": execution.get("workflow") or tool_name, + "workflow_version": execution.get("version", "active"), + }, + component="agent_runtime.transactional_workflow", + ) + result = await executor.execute_from_policy( + tool_name=tool_name, + arguments=args, + policy=policy, + ) + if result is None: + return await super()._call_mcp_tool(tool_name, args, state) + + completed = result.get("status") == "COMPLETED" + normalized = { + "ok": completed, + "tool_name": tool_name, + "execution_mode": "workflow", + "workflow_name": result.get("workflow_name"), + "workflow_version": result.get("workflow_version"), + "workflow_execution_id": result.get("execution_id"), + "status": result.get("status"), + "data": result.get("output") or {}, + "workflow_state": result.get("state") or {}, + "error": result.get("error"), + "cached": False, + } + state["workflow_execution"] = { + "execution_id": normalized["workflow_execution_id"], + "workflow_name": normalized["workflow_name"], + "workflow_version": normalized["workflow_version"], + "status": normalized["status"], + } + await self._emit_ic( + "IC.TRANSACTIONAL_WORKFLOW_COMPLETED" if completed else "IC.TRANSACTIONAL_WORKFLOW_FAILED", + state, + { + "tool_name": tool_name, + **state["workflow_execution"], + "error": normalized.get("error"), + }, + component="agent_runtime.transactional_workflow", + ) + return normalized + + return await super()._call_mcp_tool(tool_name, args, state) + + def build_direct_mcp_answer( + self, + state: dict[str, Any], + tool_results: list[dict[str, Any]], + *, + agent_label: str, + ) -> str | None: + for result in tool_results or []: + if result.get("execution_mode") != "workflow" or not result.get("ok"): + continue + nodes = result.get("data") or {} + registration = nodes.get("registrar_devolucao") or {} + protocol = registration.get("protocol") + status = registration.get("status") + if protocol: + return ( + f"[{agent_label}] Devolução registrada com sucesso. " + f"Protocolo {protocol}, status {status}. " + f"Execução do workflow: {result.get('workflow_execution_id')}." + ) + return super().build_direct_mcp_answer(state, tool_results, agent_label=agent_label) + + +__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"] diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/support_agent.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/support_agent.py new file mode 100644 index 0000000..b4f0244 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/agents/support_agent.py @@ -0,0 +1,129 @@ +from app.agents.prompting import apply_agent_profile_prompt +from app.agents.runtime import AgentRuntimeMixin + + +class SupportAgent(AgentRuntimeMixin): + name = "support_agent" + + def __init__( + self, + llm, + telemetry=None, + tool_router=None, + rag_service=None, + cache=None, + settings=None, + observer=None, + memory=None, + summary_memory=None, + ): + self.llm = llm + self.telemetry = telemetry + self.tool_router = tool_router + self.rag_service = rag_service + self.cache = cache + self.settings = settings + self.observer = observer + self.memory = memory + self.summary_memory = summary_memory + + async def run(self, state): + await self._emit_ic( + "IC.SUPPORT_AGENT_STARTED", + state, + {"business_component": "suporte"}, + component="agent.support.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.SUPPORT_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.support.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="SupportAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.SUPPORT_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.support.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + await self.prepare_memory_context(state) + + messages = self.build_messages( + state, + system_prompt=apply_agent_profile_prompt( + state, + "Você é um agente de suporte de varejo para troca, devolução e garantia.", + ), + mcp_results=tool_context, + rag_context=rag_context, + rag_metadata=rag_metadata, + ) + + answer = await self._invoke_llm_cached(state, "SupportAgent", messages) + result = { + "answer": f"[SupportAgent] {answer}", + "next_state": "SUPPORT_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.SUPPORT_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.support.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/__init__.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/__init__.py new file mode 100644 index 0000000..3f95e96 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/__init__.py @@ -0,0 +1 @@ +"""Exemplos de uso do template backend enterprise.""" diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/grl_examples.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/grl_examples.py new file mode 100644 index 0000000..8dadac8 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/grl_examples.py @@ -0,0 +1,37 @@ +"""Exemplos de GRL. + +GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos +pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão +manual apenas para validações customizadas do agente. +""" + +from typing import Any + + +async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None: + await observer.emit_grl( + "OBSERVE", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "rail_code": rail_code, + "reason": reason, + }, + component="examples.grl", + ) + + +async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None: + await observer.emit_grl( + "004", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "rail_code": rail_code, + "reason": reason, + "action": "block", + }, + component="examples.grl", + ) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/ic_examples.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/ic_examples.py new file mode 100644 index 0000000..f6daa57 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/ic_examples.py @@ -0,0 +1,34 @@ +"""Exemplos de IC - Item de Controle. + +ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria, +analytics, BigQuery ou qualquer publisher configurado no framework. +""" + +from typing import Any + + +async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None: + await observer.emit_ic( + "IC.FATURA_CONSULTADA", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "invoice_id": invoice_id, + }, + component="examples.ic", + ) + + +async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None: + await observer.emit_ic( + "IC.ACAO_CONCLUIDA", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "action_name": action_name, + "ok": ok, + }, + component="examples.ic", + ) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/mcp_examples.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/mcp_examples.py new file mode 100644 index 0000000..613f10c --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/mcp_examples.py @@ -0,0 +1,43 @@ +"""Exemplos de MCP + IC. + +O AgentRuntimeMixin já possui _collect_mcp_context(), mas este arquivo mostra o +padrão para chamadas explícitas ao tool_router quando necessário. +""" + +from typing import Any + + +async def exemplo_chamada_mcp(tool_router: Any, observer: Any, state: dict[str, Any], tool_name: str, payload: dict[str, Any]) -> Any: + session_id = state.get("conversation_key") or state.get("session_id") + + await observer.emit_ic( + "IC.MCP_TOOL_CALLED", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "tool_name": tool_name, + }, + component="examples.mcp", + ) + + result = await tool_router.call( + tool_name, + payload, + business_context=(state.get("context") or {}).get("business_context") or {}, + original_context=state.get("context") or {}, + ) + + await observer.emit_ic( + "IC.TOOL_CALLED", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "tool_name": tool_name, + "ok": getattr(result, "ok", None), + }, + component="examples.mcp", + ) + + return result diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/noc_examples.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/noc_examples.py new file mode 100644 index 0000000..2b38a15 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/noc_examples.py @@ -0,0 +1,37 @@ +"""Exemplos de NOC. + +NOC representa telemetria operacional. O workflow do template já emite NOC.001, +NOC.005 e NOC.006. Estes exemplos mostram eventos adicionais que a squad pode +emitir em pontos críticos. +""" + +from typing import Any + + +async def exemplo_api_invalida(observer: Any, state: dict[str, Any], api_url: str, status_code: int, latency_ms: int) -> None: + await observer.emit_noc( + "002", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "apiUrl": api_url, + "statusCode": status_code, + "latencyMs": latency_ms, + }, + component="examples.noc", + ) + + +async def exemplo_latencia_banco(observer: Any, state: dict[str, Any], resource_name: str, latency_ms: int) -> None: + await observer.emit_noc( + "003", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "resourceName": resource_name, + "latencyMs": latency_ms, + }, + component="examples.noc", + ) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/observer_examples.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/observer_examples.py new file mode 100644 index 0000000..926b553 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/examples/observer_examples.py @@ -0,0 +1,28 @@ +"""Resumo prático do Observer corporativo. + +Use este arquivo como cola rápida para IC, NOC e GRL. +""" + +from typing import Any + + +async def emitir_eventos_basicos(observer: Any, state: dict[str, Any]) -> None: + session_id = state.get("conversation_key") or state.get("session_id") + + await observer.emit_ic( + "IC.EXEMPLO_NEGOCIO", + {"session_id": session_id, "agent_id": state.get("agent_id")}, + component="examples.observer", + ) + + await observer.emit_noc( + "EXEMPLO_OPERACIONAL", + {"session_id": session_id, "agent_id": state.get("agent_id")}, + component="examples.observer", + ) + + await observer.emit_grl( + "OBSERVE", + {"session_id": session_id, "agent_id": state.get("agent_id"), "rail_code": "CUSTOM"}, + component="examples.observer", + ) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/main.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/main.py new file mode 100644 index 0000000..d51bbc2 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/main.py @@ -0,0 +1,552 @@ +from __future__ import annotations + +import logging +from uuid import uuid4 +import time + +from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +from agent_framework.channels.base import ChannelResponse +from agent_framework.channels.gateway import ChannelGateway +from agent_framework.config.agent_registry import AgentProfileRegistry +from agent_framework.config.settings import settings +from agent_framework.analytics.factory import create_analytics_publisher +from agent_framework.observer import configure as configure_global_observer +from agent_framework.llm.providers import create_llm +from agent_framework.memory.message_history import create_memory +from agent_framework.memory.summary_memory import create_conversation_summary_memory +from agent_framework.mcp.tool_router import create_mcp_tool_router +from agent_framework.models.identity import AgentIdentity +from agent_framework.identity import IdentityResolver, BusinessContext +from agent_framework.models.session import ChatMessage, SessionContext +from agent_framework.observability.telemetry import Telemetry +from agent_framework.observability.context import set_observability_context, clear_observability_context +from agent_framework.repositories.session_repository import create_session_repository +from agent_framework.checkpoints.checkpoint_repository import create_checkpoint_repository +from agent_framework.cache.cache import create_cache +from agent_framework.billing.usage_repository import create_usage_repository +from agent_framework.sse.events import SSEHub +from app.workflows.agent_graph import AgentWorkflow +from app.observability.telemetry_observer import TelemetryBackedAgentObserver + +logging.basicConfig(level=settings.LOG_LEVEL) +logger = logging.getLogger("agent_template_backend") + +app = FastAPI(title="Agent Template Backend FIRST-ready") +app.add_middleware( + CORSMiddleware, + allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +telemetry = Telemetry(settings) +usage_repository = create_usage_repository(settings) +llm = create_llm(settings, telemetry=telemetry, usage_repository=usage_repository) +memory = create_memory(settings) +summary_memory = create_conversation_summary_memory(settings, message_history=memory, llm=llm, telemetry=telemetry) +sessions = create_session_repository(settings) +checkpoints = create_checkpoint_repository(settings) +cache = create_cache(settings, telemetry=telemetry) +gateway = ChannelGateway(input_mode=settings.FRAMEWORK_CHANNEL_INPUT_MODE) +analytics = create_analytics_publisher(settings) +observer = TelemetryBackedAgentObserver(telemetry=telemetry) +configure_global_observer({ + "enabled": getattr(settings, "ENABLE_ANALYTICS", False), + "providers": getattr(settings, "ANALYTICS_PROVIDERS", "oci_streaming"), + "topic_path": getattr(settings, "GCP_PUBSUB_TOPIC_PATH", None) or getattr(settings, "AGENT_PUBSUB_TOPIC", None), +}) +tool_router = create_mcp_tool_router(settings, telemetry=telemetry) +identity_resolver = IdentityResolver.from_yaml(settings.IDENTITY_CONFIG_PATH) +agent_profiles = AgentProfileRegistry(settings) +sse_hub = SSEHub(settings, telemetry=telemetry) +workflow = AgentWorkflow(llm, memory, telemetry, analytics, settings, observer=observer, tool_router=tool_router, summary_memory=summary_memory) + +logger.info("LLM provider carregado: %s", llm.__class__.__name__) +logger.info("Langfuse habilitado: %s host=%s", telemetry.is_enabled(), settings.LANGFUSE_HOST) +logger.info("Analytics habilitado: %s providers=%s", getattr(settings, "ENABLE_ANALYTICS", False), getattr(settings, "ANALYTICS_PROVIDERS", "")) +logger.info("Agentes disponíveis: %s", [p.agent_id for p in agent_profiles.list_profiles()]) +logger.info("Framework channel input mode: %s", gateway.input_mode) + +@app.middleware("http") +async def observability_context_middleware(request: Request, call_next): + clear_observability_context() + request_id = request.headers.get("x-request-id") or str(uuid4()) + set_observability_context( + request_id=request_id, + channel=request.headers.get("x-channel") or "http", + ura_call_id=request.headers.get("x-ura-call-id"), + ) + started = time.time() + try: + response = await call_next(request) + response.headers["x-request-id"] = request_id + await telemetry.event("http.request.completed", { + "method": request.method, + "path": request.url.path, + "status_code": response.status_code, + "duration_ms": int((time.time() - started) * 1000), + }, kind="http") + return response + except Exception as exc: + await telemetry.event("http.request.failed", { + "method": request.method, + "path": request.url.path, + "error": str(exc), + "duration_ms": int((time.time() - started) * 1000), + }, kind="http") + raise + finally: + clear_observability_context() + + +class GatewayRequest(BaseModel): + channel: str = "web" + payload: dict + agent_id: str | None = None + tenant_id: str | None = None + + +def _metadata_value(payload: dict, key: str): + metadata = payload.get("metadata") + if isinstance(metadata, dict): + return metadata.get(key) + return None + + +def _extract_workflow_id(payload: dict) -> str | None: + return ( + payload.get("workflow_id") + or payload.get("workflowId") + or _metadata_value(payload, "workflow_id") + or _metadata_value(payload, "workflowId") + ) + + +def _format_root_span_name(template: str | None, values: dict) -> str: + template = template or "agent.gateway_message" + try: + return template.format(**{k: v or "unknown" for k, v in values.items()}) + except Exception: + logger.warning("LANGFUSE_ROOT_SPAN_NAME inválido: %s", template) + return "agent.gateway_message" + + +def _resolve_identity(req: GatewayRequest, msg) -> tuple[AgentIdentity, dict, BusinessContext, list[str]]: + payload = req.payload or {} + context = dict(msg.context or {}) + tenant_id = req.tenant_id or payload.get("tenant_id") or context.get("tenant_id") or "default" + agent_id = req.agent_id or payload.get("agent_id") or context.get("agent_id") or agent_profiles.default_agent_id + profile = agent_profiles.get(agent_id) + + # 1) Identidade técnica do framework: isola tenant/agente/sessão. + context.update({"tenant_id": tenant_id, "agent_id": profile.agent_id, "agent_profile": profile.__dict__}) + identity = AgentIdentity.from_context(context, session_id=msg.session_id) + + # 2) Identidade de negócio: chaves canônicas vindas do front/canal. + # Estas chaves são estáveis na sessão e seguem até agentes e MCP Router. + previous_business_context = context.get("business_context") or context.get("identity") or {} + business_context = identity_resolver.resolve( + {**payload, **context}, + session_id=identity.conversation_key(), + previous=previous_business_context, + ) + missing_identity_keys = identity_resolver.validate(business_context) + context.update({ + "business_context": business_context.model_dump(), + "business_keys": business_context.to_context_dict(), + "identity_missing": missing_identity_keys, + "conversation_key": identity.conversation_key(), + "original_session_id": msg.session_id, + }) + return identity, context, business_context, missing_identity_keys + + +async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False) -> dict: + try: + msg = await gateway.normalize(req.channel, req.payload) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + payload = req.payload or {} + identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg) + agent_session_id = identity.conversation_key() + message_id = payload.get("message_id") or str(uuid4()) + workflow_id = _extract_workflow_id(payload) + set_observability_context( + session_id=agent_session_id, + user_id=msg.user_id, + tenant_id=identity.tenant_id, + agent_id=identity.agent_id, + channel=msg.channel, + message_id=message_id, + workflow_id=workflow_id, + ura_call_id=payload.get("ura_call_id") or normalized_context.get("ura_call_id") or business_context.interaction_key, + ) + + stream = sse_hub.stream_for(agent_session_id) + async with stream.lock: + await sse_hub.emit(agent_session_id, "flow.start", {"session_id": agent_session_id, "message_id": message_id, "agent_id": identity.agent_id}) if emit_sse else None + + session = await sessions.get(agent_session_id) + if not session: + context_fields = { + k: v + for k, v in normalized_context.items() + if k in SessionContext.model_fields + and k not in {"tenant_id", "agent_id", "session_id", "user_id", "channel", "channel_id"} + } + session = SessionContext( + tenant_id=identity.tenant_id, + agent_id=identity.agent_id, + session_id=agent_session_id, + user_id=msg.user_id, + channel=msg.channel, + channel_id=msg.channel_id, + **context_fields, + ) + + session.tenant_id = identity.tenant_id + session.agent_id = identity.agent_id + session.channel = msg.channel + session.channel_id = msg.channel_id or session.channel_id + await sessions.upsert(session) + session.metadata = { + **(session.metadata or {}), + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + "original_context": normalized_context, + } + await sse_hub.emit(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}) if emit_sse else None + + await memory.append( + agent_session_id, + ChatMessage( + role="user", + content=msg.text, + metadata={ + **normalized_context, + "agent_id": identity.agent_id, + "tenant_id": identity.tenant_id, + "message_id": message_id, + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + }, + ), + ) + await sse_hub.emit(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}) if emit_sse else None + history = [m.model_dump(mode="json") for m in await memory.list(agent_session_id)] + + cms_input = { + "channel": req.channel, + "tenant_id": req.tenant_id, + "agent_id": req.agent_id, + "payload": payload, + } + trace_context = { + "text": msg.text, + "channel": msg.channel, + "channel_id": msg.channel_id, + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "conversation_key": agent_session_id, + "workflow_id": workflow_id, + "message_id": message_id, + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + } + root_span_name = _format_root_span_name( + getattr(settings, "LANGFUSE_ROOT_SPAN_NAME", "agent.gateway_message"), + { + "workflow_id": workflow_id, + "channel": msg.channel, + "agent_id": identity.agent_id, + "tenant_id": identity.tenant_id, + }, + ) + root_tags = ["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"] + if workflow_id: + root_tags.append(f"workflow:{workflow_id}") + + async with telemetry.span( + root_span_name, + session_id=agent_session_id, + user_id=session.user_id, + channel=msg.channel, + workflow_id=workflow_id, + input=cms_input, + tags=root_tags, + _root_span=True, + ) as root_span: + await telemetry.event("gateway.message.received", trace_context) + await sse_hub.emit(agent_session_id, "workflow.started", trace_context) if emit_sse else None + result = await workflow.ainvoke( + { + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "session_id": agent_session_id, + "conversation_key": agent_session_id, + "workflow_id": workflow_id, + "agent_profile": normalized_context["agent_profile"], + # Chave estável de LTM. Nunca use session_id como identidade de longo prazo. + "long_term_memory_subject_key": business_context.customer_key or session.user_id, + "customer_key": business_context.customer_key, + "user_id": session.user_id, + "business_context": business_context.model_dump(), + "user_text": msg.text, + "history": history, + "context": { + **normalized_context, + "session": session.model_dump(mode="json"), + "original_session_id": msg.session_id, + "session_id": agent_session_id, + "conversation_key": agent_session_id, + "workflow_id": workflow_id, + "user_id": session.user_id, + "channel": msg.channel, + "message_id": message_id, + "business_context": business_context.model_dump(), + "business_keys": business_context.to_context_dict(), + "identity_missing": missing_identity_keys, + }, + } + ) + + await checkpoints.put(agent_session_id, {"state": result, "message_id": message_id}) + await sse_hub.emit(agent_session_id, "workflow.completed", {"session_id": agent_session_id, "route": result.get("route"), "intent": result.get("intent")}) if emit_sse else None + + answer = result.get("final_answer") or result.get("answer") or "" + await memory.append( + agent_session_id, + ChatMessage( + role="assistant", + content=answer, + metadata={ + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "message_id": f"assistant-{message_id}", + "route": result.get("route"), + "intent": result.get("intent"), + "route_decision": result.get("route_decision"), + "judges": result.get("judge_results"), + }, + ), + ) + + await telemetry.event( + "gateway.message.responded", + { + "session_id": agent_session_id, + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "route": result.get("route"), + "intent": result.get("intent"), + "answer_chars": len(answer), + }, + ) + + response = ChannelResponse( + channel=msg.channel, + session_id=agent_session_id, + text=answer, + metadata={ + "channel_id": msg.channel_id, + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "original_session_id": msg.session_id, + "conversation_key": agent_session_id, + "workflow_id": workflow_id, + "message_id": message_id, + "route": result.get("route"), + "intent": result.get("intent"), + "route_decision": result.get("route_decision"), + "domain": result.get("domain"), + "mcp_tools": result.get("mcp_tools"), + "mcp_results": result.get("mcp_results"), + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + "judges": result.get("judge_results"), + "guardrails": result.get("guardrail_decisions"), + "long_term_memory": { + "subject_key": business_context.customer_key or session.user_id, + "loaded": result.get("long_term_memories", []), + "context": result.get("long_term_memory_context", ""), + "load_error": result.get("long_term_memory_load_error"), + "write_result": result.get("long_term_memory_write_result", {}), + }, + }, + ) + rendered = await gateway.render(response) + root_span.set_output(rendered) + await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None + await sse_hub.emit(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}) if emit_sse else None + return rendered + + +@app.get("/health") +async def health(): + return { + "status": "ok", + "llm_provider": settings.LLM_PROVIDER, + "llm_class": llm.__class__.__name__, + "langfuse_enabled": telemetry.is_enabled(), + "agents": [p.agent_id for p in agent_profiles.list_profiles()], + "default_agent_id": agent_profiles.default_agent_id, + "routing_mode": settings.ROUTING_MODE, + "sse_enabled": settings.ENABLE_SSE, + "session_repository": settings.SESSION_REPOSITORY_PROVIDER, + "memory_repository": settings.MEMORY_REPOSITORY_PROVIDER, + "long_term_memory": { + "enabled": getattr(settings, "ENABLE_LONG_TERM_MEMORY", False), + "provider": getattr(settings, "LONG_TERM_MEMORY_PROVIDER", None), + "sqlite_path": getattr(settings, "LONG_TERM_MEMORY_SQLITE_PATH", None), + "table": getattr(settings, "LONG_TERM_MEMORY_TABLE", None), + "auto_extract": getattr(settings, "LONG_TERM_MEMORY_AUTO_EXTRACT", None), + "inject_context": getattr(settings, "LONG_TERM_MEMORY_INJECT_CONTEXT", None), + }, + "checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER, + "usage_repository": settings.USAGE_REPOSITORY_PROVIDER, + "identity_config_path": settings.IDENTITY_CONFIG_PATH, + "mcp_parameter_mapping_path": settings.MCP_PARAMETER_MAPPING_PATH, + "framework_channel_input_mode": settings.FRAMEWORK_CHANNEL_INPUT_MODE, + "legacy_channel_gateway_mode": settings.CHANNEL_GATEWAY_MODE, + } + + +@app.get("/agents") +async def list_agents(): + return {"default_agent_id": agent_profiles.default_agent_id, "agents": [p.__dict__ for p in agent_profiles.list_profiles()]} + + +@app.get("/debug/env") +async def debug_env(): + return { + "APP_ENV": settings.APP_ENV, + "LLM_PROVIDER": settings.LLM_PROVIDER, + "ENABLE_LANGFUSE": settings.ENABLE_LANGFUSE, + "LANGFUSE_HOST": settings.LANGFUSE_HOST, + "TELEMETRY_ENABLED": telemetry.is_enabled(), + "SQLITE_DB_PATH": settings.SQLITE_DB_PATH, + "SESSION_REPOSITORY_PROVIDER": settings.SESSION_REPOSITORY_PROVIDER, + "MEMORY_REPOSITORY_PROVIDER": settings.MEMORY_REPOSITORY_PROVIDER, + "CHECKPOINT_REPOSITORY_PROVIDER": settings.CHECKPOINT_REPOSITORY_PROVIDER, + "AGENTS_CONFIG_PATH": settings.AGENTS_CONFIG_PATH, + "ROUTING_CONFIG_PATH": settings.ROUTING_CONFIG_PATH, + "ROUTING_MODE": settings.ROUTING_MODE, + "FRAMEWORK_CHANNEL_INPUT_MODE": settings.FRAMEWORK_CHANNEL_INPUT_MODE, + "CHANNEL_GATEWAY_MODE": settings.CHANNEL_GATEWAY_MODE, + } + + +@app.get("/test-llm") +async def test_llm(): + async with telemetry.span("debug.test_llm", input={"message": "Diga apenas OK"}): + answer = await llm.ainvoke([ + {"role": "system", "content": "Responda de forma curta."}, + {"role": "user", "content": "Diga apenas OK"}, + ]) + telemetry.flush() + return {"provider": llm.__class__.__name__, "answer": answer} + + +@app.post("/debug/route") +async def debug_route(req: GatewayRequest): + msg = await gateway.normalize(req.channel, req.payload) + identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg) + state = { + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "session_id": msg.session_id or "debug-session", + "conversation_key": identity.conversation_key(), + "agent_profile": context["agent_profile"], + "user_text": msg.text, + "sanitized_input": msg.text, + "history": [], + "context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()}, + } + if settings.ROUTING_MODE == "supervisor": + plan = await workflow.supervisor.route_plan(state) + return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata} + decision = await workflow.router.route(state) + data = decision.model_dump(mode="json") + data["mode"] = "router" + return data + + + + +@app.post("/debug/identity") +async def debug_identity(req: GatewayRequest): + msg = await gateway.normalize(req.channel, req.payload) + identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg) + return { + "technical_identity": { + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "conversation_key": identity.conversation_key(), + "original_session_id": msg.session_id, + }, + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + "context_keys": sorted(context.keys()), + } + +@app.get("/debug/usage") +async def debug_usage(tenant_id: str | None = None, session_id: str | None = None): + return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id) + + +@app.get("/debug/mcp/tools") +async def debug_mcp_tools(): + return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()} + + +@app.post("/debug/mcp/call/{tool_name}") +async def debug_mcp_call(tool_name: str, arguments: dict | None = None): + arguments = arguments or {} + ctx = arguments.get("business_context") or arguments.get("identity") or {} + result = await tool_router.call( + tool_name, + arguments, + business_context=ctx, + original_context=arguments, + ) + return result.model_dump(mode="json") + + +@app.post("/gateway/message") +async def gateway_message(req: GatewayRequest): + return await _process_gateway_message(req, emit_sse=False) + + +@app.post("/gateway/message/sse") +async def gateway_message_sse(req: GatewayRequest): + return await _process_gateway_message(req, emit_sse=True) + + +@app.get("/gateway/events/{session_id}") +async def gateway_events(session_id: str, request: Request): + last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0" + return StreamingResponse( + sse_hub.subscribe(session_id, int(last)), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}, + ) + + +@app.get("/sessions/{session_id}/messages") +async def get_session_messages(session_id: str, limit: int = 50): + return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]} + + +@app.get("/sessions/{session_id}/checkpoint") +async def get_session_checkpoint(session_id: str): + return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)} + + +@app.on_event("shutdown") +async def shutdown(): + telemetry.shutdown() diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/mcp_gateway_client_factory.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/mcp_gateway_client_factory.py new file mode 100644 index 0000000..5a32d15 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/mcp_gateway_client_factory.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import os + +from agent_framework.gateways import MCPGatewayClient + + +def build_mcp_gateway_client() -> MCPGatewayClient | None: + if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true": + return None + + return MCPGatewayClient( + base_url=os.getenv("MCP_GATEWAY_URL", "http://localhost:8300"), + token=os.getenv("MCP_GATEWAY_TOKEN") or None, + timeout_seconds=int(os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")), + ) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/__init__.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/telemetry_observer.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/telemetry_observer.py new file mode 100644 index 0000000..92f07a1 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/observability/telemetry_observer.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +"""Observer adapter that emits IC/NOC/GRL through framework Telemetry only. + +This avoids a second Langfuse root trace created by AgentObserver -> +AnalyticsPublisher while preserving the events inside the active request span. +""" + +from datetime import datetime, timezone +from typing import Any + + +def _normalize_ic_code(code: str) -> str: + code = str(code or "UNKNOWN").strip() + return code if code.startswith(("IC.", "AGA.", "NOC.", "GRL.")) else f"IC.{code}" + + +def _normalize_noc_code(code: str) -> str: + code = str(code or "UNKNOWN").strip() + return code if code.startswith("NOC.") else f"NOC.{code}" + + +def _normalize_grl_code(code: str) -> str: + code = str(code or "UNKNOWN").strip() + return code if code.startswith("GRL.") else f"GRL.{code}" + + +def _kind_for(event_type: str) -> str: + if event_type.startswith(("IC.", "AGA.")): + return "ic" + if event_type.startswith("NOC."): + return "noc" + if event_type.startswith("GRL."): + return "grl" + return "event" + + +class TelemetryBackedAgentObserver: + """Drop-in subset of AgentObserver backed by Telemetry.event. + + Do not publish through AnalyticsPublisher here. Analytics publishing may be + configured with a Langfuse provider, and that path creates an extra root + trace for business events such as IC.AGENT_COMPLETED/NOC.006. Telemetry.event + uses the active span/trace context, so these events appear inside the single + request trace. + """ + + def __init__(self, telemetry: Any, *, source: str = "agent_framework") -> None: + self.telemetry = telemetry + self.source = source + + async def emit( + self, + event_type: str, + payload: dict[str, Any] | None = None, + *, + metadata: dict[str, Any] | None = None, + source: str | None = None, + ) -> dict[str, Any]: + body = dict(payload or {}) + meta = dict(metadata or {}) + body.setdefault("tag", event_type) + event = { + "eventType": event_type, + "source": source or self.source, + "eventDate": datetime.now(timezone.utc).isoformat(), + "body": body, + "metadata": meta, + } + try: + await self.telemetry.event(event_type, event, kind=_kind_for(event_type)) + except TypeError: + # Compatibility with older Telemetry.event signatures. + await self.telemetry.event(event_type, event) + return event + + async def emit_ic(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]: + return await self.emit(_normalize_ic_code(code), payload, metadata={**metadata, "ic": True}) + + async def emit_noc(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]: + return await self.emit(_normalize_noc_code(code), payload, metadata={**metadata, "noc": True}) + + async def emit_grl(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]: + return await self.emit(_normalize_grl_code(code), payload, metadata={**metadata, "grl": True}) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/state.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/state.py new file mode 100644 index 0000000..cc19c03 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/state.py @@ -0,0 +1,53 @@ +from typing import Any, TypedDict + + +class AgentState(TypedDict, total=False): + tenant_id: str + agent_id: str + session_id: str + conversation_key: str + workflow_id: str + agent_profile: dict[str, Any] + user_text: str + sanitized_input: str + route: str + intent: str + route_decision: dict[str, Any] + answer: str + final_answer: str + history: list[dict[str, Any]] + context: dict[str, Any] + guardrail_decisions: list[dict[str, Any]] + judge_results: list[dict[str, Any]] + next_state: str + domain: str + mcp_tools: list[str] + mcp_results: list[dict[str, Any]] + available_mcp_tools: list[str] + selected_tool_call: dict[str, Any] + pending_tool_call: dict[str, Any] + transaction_status: str + confirmation_required: bool + confirmation_received: bool + tool_policy_result: dict[str, Any] + missing_parameters: list[str] + supervisor_plan: dict[str, Any] + supervisor_results: list[dict[str, Any]] + active_agent: str + route_bypassed: bool + continuity_signal: dict[str, Any] + session_control: str + session_ended: bool + human_handoff_requested: bool + blocked: bool + supervisor_action: str + supervisor_guidance: str + supervisor_attempt: int + supervisor_handover_reason: str + output_supervisor_results: list[dict[str, Any]] + output_guardrails_already_applied: bool + long_term_memories: list[dict[str, Any]] + long_term_memory_context: str + long_term_memory_write_result: dict[str, Any] + long_term_memory_subject_key: str + long_term_memory_load_error: str diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/__init__.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/__init__.py new file mode 100644 index 0000000..6be8ce7 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/__init__.py @@ -0,0 +1 @@ +from . import devolucao # noqa: F401 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/devolucao.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/devolucao.py new file mode 100644 index 0000000..6111cf1 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflow_actions/devolucao.py @@ -0,0 +1,13 @@ +"""Actions de domínio permanecem no agente; o runtime está no framework.""" +from agent_framework.workflows import workflow_action + + +@workflow_action("validar_pedido") +async def validar_pedido(params: dict, state: dict) -> dict: + return {"valid": bool(params.get("order_id"))} + + +@workflow_action("registrar_devolucao") +async def registrar_devolucao(params: dict, state: dict) -> dict: + # Substitua pela chamada real ao serviço/MCP e use chave idempotente. + return {"protocol": f"DEV-{params['order_id']}", "status": "REQUESTED"} diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py new file mode 100644 index 0000000..b8ed7bc --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py @@ -0,0 +1,887 @@ +from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer +from langgraph.graph import END, START, StateGraph + +from agent_framework.guardrails.pipeline import GuardrailPipeline +from agent_framework.guardrails.output_supervisor import OutputSupervisor +from agent_framework.guardrails.rail_action import RailAction +from agent_framework.guardrails.rail_result import RailResult +from agent_framework.judges.judge import JudgePipeline +from agent_framework.routing.enterprise_router import EnterpriseRouter +from agent_framework.supervisor.supervisor import Supervisor +from agent_framework.observability.workflow_events import WorkflowTelemetry +from agent_framework.observability.guardrail_events import GuardrailTelemetry +from agent_framework.observability.judge_events import JudgeTelemetry +from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry +from agent_framework.observability.observer import AgentObserver +from app.agents.billing_agent import BillingAgent +from app.agents.product_agent import ProductAgent +from app.agents.orders_agent import OrdersAgent +from app.agents.support_agent import SupportAgent +from app.state import AgentState +from agent_framework.rag.rag_service import RagService +from agent_framework.rag.embedding_provider import create_embedding_provider +from agent_framework.cache.cache import create_cache +from agent_framework.memory.long_term_memory import create_long_term_memory_manager + + +class LegacyOutputGuardrailRail: + """Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo. + + O framework antigo retornava decisões allowed=True/False. O OutputSupervisor + corporativo trabalha com RailAction (allow/sanitize/retry/block/handover). + Este adapter evita reescrever todos os rails agora e mantém compatibilidade. + """ + + code = "LEGACY_OUTPUT_GUARDRAILS" + + def __init__(self, pipeline: GuardrailPipeline): + self.pipeline = pipeline + + async def evaluate(self, candidate: str, context: dict): + final, decisions = await self.pipeline.run_output(candidate, context) + serialized = [d.model_dump() for d in decisions] + + blocked = [d for d in decisions if not getattr(d, "allowed", True)] + if blocked: + first = blocked[0] + code = (getattr(first, "code", "") or "").upper() + action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK + return RailResult( + code=code or self.code, + action=action, + reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"), + guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."), + sanitized_text=final, + metadata={"legacy_decisions": serialized}, + ) + + if final != candidate: + return RailResult( + code=self.code, + action=RailAction.SANITIZE, + reason="Resposta sanitizada por guardrail de saída legado.", + sanitized_text=final, + metadata={"legacy_decisions": serialized}, + ) + + return RailResult( + code=self.code, + action=RailAction.ALLOW, + reason="Resposta aprovada pelos guardrails de saída legados.", + sanitized_text=final, + metadata={"legacy_decisions": serialized}, + ) + + +class AgentWorkflow: + """Workflow principal com dois modos de roteamento. + + Modos suportados por configuração: + ROUTING_MODE=router + input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails + + ROUTING_MODE=supervisor + input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação + + Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id. + """ + + def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None, summary_memory=None): + self.llm = llm + self.memory = memory + self.telemetry = telemetry + self.analytics = analytics + self.observer = observer or AgentObserver(analytics=analytics) + self.settings = settings + self.tool_router = tool_router + self.summary_memory = summary_memory + self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry) + self.guardrails = GuardrailPipeline( + observer=self.observer, + enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)), + fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)), + ) + self.output_supervisor_engine = OutputSupervisor( + rails=[LegacyOutputGuardrailRail(self.guardrails)], + observer=self.observer, + max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)), + enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)), + fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)), + ) + self.judges = JudgePipeline() + self.supervisor = Supervisor() + self.workflow_telemetry = WorkflowTelemetry(telemetry) + self.guardrail_telemetry = GuardrailTelemetry(telemetry) + self.judge_telemetry = JudgeTelemetry(telemetry) + self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry) + self.cache = create_cache(settings) + self.embedding_provider = create_embedding_provider(settings) + self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry) + self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry) + agent_kwargs = {"telemetry": telemetry, "tool_router": getattr(self, "tool_router", None), "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer, "memory": memory, "summary_memory": summary_memory} + self.billing = BillingAgent(llm, **agent_kwargs) + self.product = ProductAgent(llm, **agent_kwargs) + self.orders = OrdersAgent(llm, **agent_kwargs) + self.support = SupportAgent(llm, **agent_kwargs) + + # The existing agent constructors intentionally keep their stable API. + # Long-term memory is injected as a runtime capability after creation. + for agent in (self.billing, self.product, self.orders, self.support): + agent.long_term_memory_manager = self.long_term_memory_manager + self.graph = self._build_graph() + + def _node(self, name, fn): + async def _wrapped(state): + async with self.langgraph_telemetry.node(name, state): + return await fn(state) + return _wrapped + + def _build_graph(self): + builder = StateGraph(AgentState) + builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails)) + builder.add_node("load_long_term_memory", self._node("load_long_term_memory", self.load_long_term_memory)) + builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision)) + builder.add_node("billing_agent", self._node("billing_agent", self.billing_agent)) + builder.add_node("product_agent", self._node("product_agent", self.product_agent)) + builder.add_node("orders_agent", self._node("orders_agent", self.orders_agent)) + builder.add_node("support_agent", self._node("support_agent", self.support_agent)) + builder.add_node("handoff", self._node("handoff", self.handoff)) + builder.add_node("human_handoff", self._node("human_handoff", self.human_handoff)) + builder.add_node("end_session", self._node("end_session", self.end_session)) + builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent)) + builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor)) + builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails)) + builder.add_node("judge", self._node("judge", self.judge)) + builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review)) + builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory)) + builder.add_node("persist", self._node("persist", self.persist)) + + builder.add_edge(START, "input_guardrails") + builder.add_conditional_edges( + "input_guardrails", + self._after_input_guardrails, + {"blocked": "persist", "continue": "load_long_term_memory"}, + ) + builder.add_edge("load_long_term_memory", "routing_decision") + builder.add_conditional_edges( + "routing_decision", + lambda s: s.get("route", "billing_agent"), + { + "billing_agent": "billing_agent", + "product_agent": "product_agent", + "orders_agent": "orders_agent", + "support_agent": "support_agent", + "handoff": "handoff", + "human_handoff": "human_handoff", + "end_session": "end_session", + "supervisor_agent": "supervisor_agent", + }, + ) + builder.add_edge("billing_agent", "output_supervisor") + builder.add_edge("product_agent", "output_supervisor") + builder.add_edge("orders_agent", "output_supervisor") + builder.add_edge("support_agent", "output_supervisor") + builder.add_edge("handoff", "output_supervisor") + builder.add_edge("human_handoff", "output_supervisor") + builder.add_edge("end_session", "output_supervisor") + builder.add_edge("supervisor_agent", "output_supervisor") + builder.add_edge("output_supervisor", "output_guardrails") + builder.add_edge("output_guardrails", "judge") + builder.add_edge("judge", "supervisor_review") + builder.add_edge("supervisor_review", "persist_long_term_memory") + builder.add_edge("persist_long_term_memory", "persist") + builder.add_edge("persist", END) + + return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings)) + + def _after_input_guardrails(self, state): + return "blocked" if state.get("blocked") else "continue" + + async def input_guardrails(self, state): + if state.get("session_ended") is True: + answer = str(getattr( + self.settings, + "SESSION_ALREADY_ENDED_MESSAGE", + "Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.", + )) + await self.telemetry.event( + "session.message.rejected_after_end", + {"session_id": state.get("conversation_key") or state.get("session_id")}, + ) + return { + "answer": answer, + "final_answer": answer, + "blocked": True, + "session_control": "END_SESSION", + "session_ended": True, + "next_state": "SESSION_ENDED", + } + async with self.telemetry.span( + "workflow.input_guardrails", + session_id=state.get("conversation_key") or state.get("session_id"), + input=state.get("user_text"), + ): + history_texts = [m.get("content", "") for m in state.get("history", [])] + await self.observer.emit_grl( + "001", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "input", + }, + component="workflow.input_guardrails.start", + ) + sanitized, decisions = await self.guardrails.run_input( + state["user_text"], + { + **(state.get("context") or {}), + "history_texts": history_texts, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "agent_profile": state.get("agent_profile") or {}, + }, + ) + for _decision in decisions: + await self.guardrail_telemetry.evaluated("input", _decision) + await self.observer.emit_grl( + "002" if _decision.allowed else "004", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "input", + "rail_code": getattr(_decision, "code", None), + "allowed": bool(_decision.allowed), + "reason": getattr(_decision, "reason", None), + }, + component="workflow.input_guardrails.decision", + ) + if not _decision.allowed: + await self.guardrail_telemetry.blocked("input", _decision) + await self.telemetry.event( + "guardrails.input.completed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "decisions": [d.model_dump() for d in decisions], + }, + ) + await self.observer.emit_grl( + "009", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "input", + "blocked": any(not d.allowed for d in decisions), + "decision_count": len(decisions), + }, + component="workflow.input_guardrails.final", + ) + if any(not d.allowed for d in decisions): + return { + "sanitized_input": sanitized, + "answer": "Não consegui seguir com essa mensagem por regra de segurança.", + "final_answer": "Não consegui seguir com essa mensagem por regra de segurança.", + "guardrail_decisions": [d.model_dump() for d in decisions], + "route": "blocked", + "blocked": True, + } + return { + "sanitized_input": sanitized, + "guardrail_decisions": [d.model_dump() for d in decisions], + "blocked": False, + } + + async def routing_decision(self, state): + mode = getattr(self.settings, "ROUTING_MODE", "router") + async with self.telemetry.span( + "workflow.routing_decision", + session_id=state.get("conversation_key") or state.get("session_id"), + input={ + "mode": mode, + "text": state.get("sanitized_input") or state.get("user_text"), + "previous_state": state.get("next_state"), + }, + ): + if mode == "supervisor": + plan = await self.supervisor.route_plan(state) + await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence}) + return { + "route": "supervisor_agent", + "intent": plan.intent, + "supervisor_plan": { + "agents": plan.agents, + "intent": plan.intent, + "confidence": plan.confidence, + "reason": plan.reason, + "metadata": plan.metadata, + }, + "route_decision": { + "route": "supervisor_agent", + "agent": "supervisor", + "intent": plan.intent, + "confidence": plan.confidence, + "reason": plan.reason, + "method": "supervisor", + "metadata": plan.metadata, + }, + } + + decision = await self.router.route(state) + await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence}) + await self.observer.emit_ic( + "ROUTE_SELECTED", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "route": decision.route, + "intent": decision.intent, + "confidence": decision.confidence, + "method": getattr(decision, "method", None), + }, + component="workflow.routing_decision", + ) + return { + "route": decision.route, + "intent": decision.intent, + "route_decision": decision.model_dump(mode="json"), + "domain": decision.domain, + "mcp_tools": decision.mcp_tools, + "next_state": decision.next_state, + "active_agent": decision.agent, + "route_bypassed": decision.method == "continuity", + "session_control": (decision.metadata or {}).get("session_control", ""), + "human_handoff_requested": (decision.metadata or {}).get("session_control") == "HUMAN_HANDOFF", + "session_ended": (decision.metadata or {}).get("session_control") == "END_SESSION", + "continuity_signal": { + "decision": (decision.metadata or {}).get("continuity_decision"), + "confidence": decision.confidence if decision.method == "continuity" else None, + "reason": decision.reason if decision.method == "continuity" else None, + "profile": (decision.metadata or {}).get("continuity_profile"), + } if decision.method == "continuity" else {}, + } + + async def billing_agent(self, state): + async with self.telemetry.span( + "workflow.agent.billing", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"intent": state.get("intent")}, + ): + return await self.billing.run(state) + + async def product_agent(self, state): + async with self.telemetry.span( + "workflow.agent.product", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"intent": state.get("intent")}, + ): + return await self.product.run(state) + + async def orders_agent(self, state): + async with self.telemetry.span( + "workflow.agent.orders", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"intent": state.get("intent")}, + ): + return await self.orders.run(state) + + async def support_agent(self, state): + async with self.telemetry.span( + "workflow.agent.support", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"intent": state.get("intent")}, + ): + return await self.support.run(state) + + async def supervisor_agent(self, state): + """Executa um ou mais agentes no modo supervisor e consolida a resposta. + + Este nó mantém o desenho de supervisor sem obrigar o restante do workflow + a conhecer quantos agentes foram acionados. Cada execução especializada + recebe o mesmo estado, mas com route/active_agent atualizados. + """ + plan = state.get("supervisor_plan") or {} + agents = plan.get("agents") or ["billing_agent"] + handlers = { + "billing_agent": self.billing.run, + "product_agent": self.product.run, + "orders_agent": self.orders.run, + "support_agent": self.support.run, + } + partials = [] + mcp_results = [] + async with self.telemetry.span( + "workflow.supervisor_agent", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"agents": agents, "intent": state.get("intent")}, + ): + for agent_name in agents: + handler = handlers.get(agent_name) + if handler is None: + continue + child_state = {**state, "route": agent_name, "active_agent": agent_name} + result = await handler(child_state) + partials.append({"agent": agent_name, "answer": result.get("answer", "")}) + mcp_results.extend(result.get("mcp_results") or []) + + if len(partials) == 1: + answer = partials[0]["answer"] + else: + joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials) + answer = ( + "[Supervisor] Consolidação de múltiplos agentes acionados.\n" + f"{joined}" + ) + return { + "answer": answer, + "supervisor_results": partials, + "mcp_results": mcp_results, + "next_state": "SUPERVISOR_ACTIVE", + } + + async def handoff(self, state): + async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")): + target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent") + answer = ( + "Vou redirecionar sua solicitação para o especialista correto. " + f"Destino sugerido: {target or 'agente especializado'}." + ) + return {"answer": answer} + + async def human_handoff(self, state): + session_id = state.get("conversation_key") or state.get("session_id") + async with self.telemetry.span("workflow.human_handoff", session_id=session_id): + answer = str(getattr(self.settings, "HUMAN_HANDOFF_MESSAGE", "Vou encaminhar seu atendimento para uma pessoa.")) + await self.telemetry.event( + "session.human_handoff.requested", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "reason": (state.get("route_decision") or {}).get("reason"), + }, + ) + return { + "answer": answer, + "session_control": "HUMAN_HANDOFF", + "human_handoff_requested": True, + "session_ended": False, + "next_state": "HUMAN_HANDOFF_REQUESTED", + } + + async def end_session(self, state): + session_id = state.get("conversation_key") or state.get("session_id") + async with self.telemetry.span("workflow.end_session", session_id=session_id): + answer = str(getattr(self.settings, "END_SESSION_MESSAGE", "Atendimento encerrado. Obrigado pelo contato.")) + await self.telemetry.event( + "session.end.requested", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "reason": (state.get("route_decision") or {}).get("reason"), + }, + ) + return { + "answer": answer, + "session_control": "END_SESSION", + "session_ended": True, + "human_handoff_requested": False, + "next_state": "SESSION_ENDED", + } + + async def output_supervisor(self, state): + """Valida a resposta candidata com o OutputSupervisor corporativo. + + Este nó não substitui o roteador/supervisor multiagente. Ele roda após o + agente gerar `answer` e antes dos judges/persistência, produzindo campos + supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver. + """ + if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)): + return { + "output_guardrails_already_applied": False, + "supervisor_action": "disabled", + "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + } + + candidate = state.get("answer") or "" + context = { + **(state.get("context") or {}), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "session_id": state.get("conversation_key") or state.get("session_id"), + "route": state.get("route"), + "intent": state.get("intent"), + "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + } + async with self.telemetry.span( + "workflow.output_supervisor", + session_id=state.get("conversation_key") or state.get("session_id"), + input=candidate, + ): + decision = await self.output_supervisor_engine.evaluate(candidate, context) + action = decision.action.value + await self.telemetry.event( + "output_supervisor.completed", + { + "session_id": context["session_id"], + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "action": action, + "approved": decision.approved, + "guidance": decision.guidance, + }, + ) + + await self.observer.emit_ic( + "IC.OUTPUT_SUPERVISOR_COMPLETED", + { + "session_id": context["session_id"], + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "route": state.get("route"), + "intent": state.get("intent"), + "action": action, + "approved": decision.approved, + "result_count": len(decision.results), + }, + component="workflow.output_supervisor", + ) + + if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}: + final_answer = decision.candidate + elif decision.action == RailAction.HANDOVER: + final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista." + else: + final_answer = decision.fallback_message + + return { + "answer": final_answer, + "final_answer": final_answer, + "supervisor_action": action, + "supervisor_guidance": decision.guidance, + "supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0), + "supervisor_handover_reason": decision.handover_reason, + "output_supervisor_results": [ + { + "code": r.code, + "action": r.action.value, + "reason": r.reason, + "guidance": r.guidance, + "metadata": r.metadata, + } + for r in decision.results + ], + "output_guardrails_already_applied": True, + "guardrail_decisions": state.get("guardrail_decisions", []) + + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + } + + async def output_guardrails(self, state): + if state.get("output_guardrails_already_applied"): + return {"final_answer": state.get("final_answer") or state.get("answer") or ""} + + async with self.telemetry.span( + "workflow.output_guardrails", + session_id=state.get("conversation_key") or state.get("session_id"), + input=state.get("answer"), + ): + await self.observer.emit_grl( + "001", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "output", + "route": state.get("route"), + "intent": state.get("intent"), + }, + component="workflow.output_guardrails.start", + ) + final, decisions = await self.guardrails.run_output( + state["answer"], state.get("context", {}) + ) + for _decision in decisions: + await self.guardrail_telemetry.evaluated("output", _decision) + await self.observer.emit_grl( + "002" if _decision.allowed else "004", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "output", + "rail_code": getattr(_decision, "code", None), + "allowed": bool(_decision.allowed), + "reason": getattr(_decision, "reason", None), + }, + component="workflow.output_guardrails.decision", + ) + if not _decision.allowed: + await self.guardrail_telemetry.blocked("output", _decision) + await self.telemetry.event( + "guardrails.output.completed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "decisions": [d.model_dump() for d in decisions], + }, + ) + await self.observer.emit_grl( + "009", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "output", + "blocked": any(not d.allowed for d in decisions), + "decision_count": len(decisions), + }, + component="workflow.output_guardrails.final", + ) + return { + "final_answer": final, + "guardrail_decisions": state.get("guardrail_decisions", []) + + [d.model_dump() for d in decisions], + } + + async def judge(self, state): + async with self.telemetry.span( + "workflow.judge", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"question": state.get("user_text"), "answer": state.get("final_answer")}, + ): + judge_context = dict(state.get("context", {}) or {}) + judge_context["mcp_results"] = state.get("mcp_results", []) + judge_context["evidence"] = state.get("mcp_results", []) or judge_context.get("evidence") + judge_context["route"] = state.get("route") + judge_context["intent"] = state.get("intent") + # Judge sampling must see the finalized transaction state. These + # fields are populated by the agent/tool runtime before this node. + for key in ( + "transaction_status", + "confirmation_required", + "confirmation_received", + "tool_policy_result", + "selected_tool_call", + "pending_tool_call", + ): + judge_context[key] = state.get(key) + judge_context["transactional_tools"] = [ + result.get("tool_name") + for result in state.get("mcp_results", []) + if isinstance(result, dict) + and ( + (result.get("metadata") or {}).get("operation_type") == "transactional" + or result.get("awaiting_confirmation") + or result.get("transaction_status") + ) + ] + results = await self.judges.evaluate_all( + state["user_text"], state["final_answer"], judge_context + ) + for _result in results: + await self.judge_telemetry.evaluated(_result) + await self.telemetry.event( + "judges.completed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "results": [r.model_dump() for r in results], + }, + ) + return {"judge_results": [r.model_dump() for r in results]} + + async def supervisor_review(self, state): + async with self.telemetry.span( + "workflow.supervisor_review", + session_id=state.get("conversation_key") or state.get("session_id"), + input=state.get("final_answer"), + ): + ok, answer = await self.supervisor.review( + state["final_answer"], state.get("context", {}) + ) + await self.telemetry.event( + "supervisor.review.completed", + {"session_id": state.get("session_id"), "approved": ok}, + ) + return {"final_answer": answer if ok else answer} + + async def load_long_term_memory(self, state): + """Carrega LTM antes do roteamento e mantém o resultado no estado. + + A carga explícita evita depender apenas do agente selecionado para realizar + a recuperação e facilita o diagnóstico de identidade/namespace. + """ + try: + memories = await self.long_term_memory_manager.load(state) + serialized = [] + context_lines = [] + for item in memories or []: + if hasattr(item, "model_dump"): + data = item.model_dump(mode="json") + elif hasattr(item, "__dict__"): + data = dict(item.__dict__) + elif isinstance(item, dict): + data = dict(item) + else: + data = {"value": str(item)} + serialized.append(data) + key = data.get("key") or data.get("memory_key") or data.get("category") or "memory" + value = data.get("value") or data.get("memory_value") + if value not in (None, ""): + context_lines.append(f"- {key}: {value}") + + return { + "long_term_memories": serialized, + "long_term_memory_context": "\n".join(context_lines), + } + except Exception as exc: + await self.telemetry.event( + "long_term_memory.load.failed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "subject_key": state.get("long_term_memory_subject_key"), + "error": str(exc), + }, + ) + return { + "long_term_memories": [], + "long_term_memory_context": "", + "long_term_memory_load_error": str(exc), + } + + async def persist_long_term_memory(self, state): + try: + result = await self.long_term_memory_manager.persist_turn(state) + await self.telemetry.event( + "long_term_memory.persist.completed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "subject_key": state.get("long_term_memory_subject_key"), + "result": result, + }, + ) + return {"long_term_memory_write_result": result} + except Exception as exc: + await self.telemetry.event( + "long_term_memory.persist.failed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "subject_key": state.get("long_term_memory_subject_key"), + "error": str(exc), + }, + ) + return {"long_term_memory_write_result": {"saved": 0, "error": str(exc)}} + + async def persist(self, state): + async with self.telemetry.span( + "workflow.persist", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"route": state.get("route"), "intent": state.get("intent")}, + ): + await self.observer.emit_ic( + "AGENT_COMPLETED", + { + "session_id": state.get("conversation_key") or state["session_id"], + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "route": state.get("route"), + "intent": state.get("intent"), + "route_decision": state.get("route_decision"), + "judges": state.get("judge_results", []), + "mcp_tools": state.get("mcp_tools", []), + "mcp_results": state.get("mcp_results", []), + }, + ) + + await self.observer.emit_noc( + "006", + { + "session_id": state.get("conversation_key") or state["session_id"], + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "route": state.get("route"), + "intent": state.get("intent"), + "answer_chars": len(state.get("final_answer") or ""), + }, + component="workflow.persist", + ) + + await self.telemetry.event( + "agent.completed", + { + "session_id": state.get("conversation_key") or state["session_id"], + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "route": state.get("route"), + "intent": state.get("intent"), + "answer_chars": len(state.get("final_answer") or ""), + }, + ) + return state + + async def ainvoke(self, state): + thread_id = state.get("conversation_key") or state["session_id"] + config = {"configurable": {"thread_id": thread_id}} + async with self.telemetry.span( + "workflow.langgraph.ainvoke", + session_id=state.get("conversation_key") or state.get("session_id"), + user_id=state.get("context", {}).get("user_id"), + input={"user_text": state.get("user_text")}, + tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",], + ): + await self.workflow_telemetry.started("agent_workflow", state) + await self.observer.emit_noc( + "001", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "channel_id": (state.get("context") or {}).get("channel"), + "message_id": (state.get("context") or {}).get("message_id"), + "ura_call_id": (state.get("context") or {}).get("ura_call_id"), + }, + component="workflow.ainvoke", + ) + await self.observer.emit_ic( + "AGENT_STARTED", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "channel_id": (state.get("context") or {}).get("channel"), + "message_id": (state.get("context") or {}).get("message_id"), + "user_text_chars": len(state.get("user_text") or ""), + }, + component="workflow.ainvoke", + ) + try: + result = await self.graph.ainvoke(state, config=config) + await self.workflow_telemetry.completed("agent_workflow", result) + return result + except Exception as exc: + await self.workflow_telemetry.failed("agent_workflow", exc) + await self.observer.emit_noc( + "005", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "error": str(exc), + "exception_type": exc.__class__.__name__, + }, + component="workflow.ainvoke", + ) + raise diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents.yaml new file mode 100644 index 0000000..7d245a5 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents.yaml @@ -0,0 +1,33 @@ +default_agent_id: telecom_contas +agents: + - agent_id: telecom_contas + name: Agente Telecom Contas + description: Template de atendimento para faturas, produtos e suporte de telecom. + prompt_policy_path: ./config/agents/telecom_contas/prompt_policy.yaml + routing_config_path: ./config/routing.yaml + guardrails_config_path: ./config/agents/telecom_contas/guardrails.yaml + judges_config_path: ./config/agents/telecom_contas/judges.yaml + mcp_servers_config_path: ./config/mcp_servers.yaml + tools_config_path: ./config/tools.yaml + metadata: + domain: telecom + system_prefix: | + Você está executando o agent_template telecom_contas. + Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id. + Não misture histórico ou decisões de outros agentes. + + - agent_id: retail_orders + name: Agente Retail Pedidos + description: Template de varejo para pedidos, produtos, troca/devolução e garantia. + prompt_policy_path: ./config/agents/retail_orders/prompt_policy.yaml + routing_config_path: ./config/routing.yaml + guardrails_config_path: ./config/agents/retail_orders/guardrails.yaml + judges_config_path: ./config/agents/retail_orders/judges.yaml + mcp_servers_config_path: ./config/mcp_servers.yaml + tools_config_path: ./config/tools.yaml + metadata: + domain: retail + system_prefix: | + Você está executando o agent_template retail_orders. + Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id. + Não misture histórico ou decisões de outros agentes. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/guardrails.yaml new file mode 100644 index 0000000..9fe094a --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/guardrails.yaml @@ -0,0 +1,8 @@ +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true +output: + - code: REVPREC + enabled: true diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/judges.yaml new file mode 100644 index 0000000..62fc7c7 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/judges.yaml @@ -0,0 +1,7 @@ +judges: + - name: response_quality + enabled: true + threshold: 0.7 + - name: groundedness + enabled: true + threshold: 0.6 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml new file mode 100644 index 0000000..f872a2b --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml @@ -0,0 +1,6 @@ +id: retail_orders_prompt_policy +version: 1 +description: Prompt base isolado do agente de varejo/pedidos. +system_prefix: | + Você é um agente corporativo de varejo especializado em pedidos, entrega, troca, devolução e garantia. + Seja claro, objetivo e não use regras de negócio de telecom neste agente. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/guardrails.yaml new file mode 100644 index 0000000..9fe094a --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/guardrails.yaml @@ -0,0 +1,8 @@ +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true +output: + - code: REVPREC + enabled: true diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/judges.yaml new file mode 100644 index 0000000..d488063 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/judges.yaml @@ -0,0 +1,20 @@ +enabled: true +fail_closed: true +profile: judge + +judges: + - name: response_quality + enabled: true + threshold: 0.7 + + - name: groundedness + enabled: true + threshold: 0.6 + + - name: sentiment + enabled: true + fail_on_negative: false + + - name: tone + enabled: true + fail_closed: true \ No newline at end of file diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml new file mode 100644 index 0000000..42732c4 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml @@ -0,0 +1,6 @@ +id: telecom_contas_prompt_policy +version: 1 +description: Prompt base isolado do agente de telecom/contas. +system_prefix: | + Você é um agente corporativo de atendimento telecom especializado em faturas, produtos, VAS e suporte. + Seja claro, objetivo e não prometa execução operacional sem ferramenta ou confirmação válida. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/guardrails.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/guardrails.yaml new file mode 100644 index 0000000..44887eb --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/guardrails.yaml @@ -0,0 +1,12 @@ +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true +output: + - code: REVPREC + enabled: true + - code: PINJ + enabled: true + - code: DLEX_OUT + enabled: true \ No newline at end of file diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/identity.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/identity.yaml new file mode 100644 index 0000000..5f20147 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/identity.yaml @@ -0,0 +1,55 @@ +identity: + version: "2" + required: + - session_key + keys: + customer_key: + description: Cliente/assinante/consumidor canônico. + sources: + - business_context.customer_key + - customer_key + - msisdn + - customer_id + - user_id + - ani + - from + contract_key: + description: Contrato, conta, fatura, pedido ou asset principal. + sources: + - business_context.contract_key + - contract_key + - invoice_id + - current_invoice_number + - order_id + - pedido_id + - asset_id + interaction_key: + description: Chave externa da interação/call/chat vinda do canal. + sources: + - business_context.interaction_key + - interaction_key + - ura_call_id + - call_id + - message_id + account_key: + description: Conta de cobrança/conta comercial. + sources: + - business_context.account_key + - account_key + - account_id + - billing_account_id + resource_key: + description: Recurso/linha/produto/asset específico. + sources: + - business_context.resource_key + - resource_key + - asset_id + - product_id + - sku + session_key: + description: Sessão técnica estável já escopada por tenant e agente. + sources: + - business_context.session_key + - session_key + - conversation_key + - session_id diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/judges.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/judges.yaml new file mode 100644 index 0000000..c091619 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/judges.yaml @@ -0,0 +1,18 @@ +enabled: true +fail_closed: true +profile: judge +judges: +- name: response_quality + enabled: true + threshold: 0.7 +- name: groundedness + enabled: true + threshold: 0.6 +- name: sentiment + enabled: true + fail_on_negative: false +- name: tone + enabled: true + fail_closed: true +sample_rate: 0.25 +always_run_for_transactional: true diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_parameter_mapping.yaml new file mode 100644 index 0000000..5b29ccf --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_parameter_mapping.yaml @@ -0,0 +1,92 @@ +mcp_parameter_mapping: + defaults: + use_mock: true + tools: + consultar_fatura: + map: + customer_key: msisdn + contract_key: invoice_id + interaction_key: ura_call_id + session_key: session_id + extract: + mes_referencia: + from: message + type: int + strategy: month_name_pt + description: 'Extrair mês citado na mensagem. janeiro=1, fevereiro=2, março=3, + abril=4, maio=5, junho=6, julho=7, agosto=8, setembro=9, outubro=10, novembro=11, + dezembro=12. + + ' + consultar_pagamentos: + map: + customer_key: msisdn + interaction_key: ura_call_id + session_key: session_id + consultar_plano: + map: + customer_key: msisdn + resource_key: asset_id + contract_key: asset_id + session_key: session_id + listar_servicos: + map: + customer_key: msisdn + session_key: session_id + consultar_pedido: + map: + customer_key: customer_id + session_key: session_id + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + consultar_entrega: + map: + session_key: session_id + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + solicitar_troca: + map: + session_key: session_id + defaults: + reason: Solicitação aberta pelo atendimento conversacional. + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + solicitar_devolucao: + map: + session_key: session_id + defaults: + reason: Solicitação aberta pelo atendimento conversacional. + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.docker.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.docker.yaml new file mode 100644 index 0000000..8101130 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.docker.yaml @@ -0,0 +1,12 @@ +servers: + telecom: + transport: http + endpoint: http://telecom-mcp:8100/mcp + enabled: true + description: MCP Server Telecom via docker-compose. + + retail: + transport: http + endpoint: http://retail-mcp:8200/mcp + enabled: true + description: MCP Server Retail via docker-compose. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.yaml new file mode 100644 index 0000000..fe638a2 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/mcp_servers.yaml @@ -0,0 +1,30 @@ +# MCP servers registry. +# transport=http keeps the legacy framework mock contract: +# GET /tools/list +# POST /tools/call +# transport=fastmcp uses official MCP Streamable HTTP, typically endpoint http://host:port/mcp +# transport=sse uses official MCP SSE, typically endpoint http://host:port/sse +servers: + # telecom: + # enabled: true + # transport: fastmcp + # endpoint: http://localhost:8001/mcp + # description: Telecom FastMCP server using official MCP protocol + # + # retail: + # enabled: true + # transport: fastmcp + # endpoint: http://localhost:8002/mcp + # description: Retail FastMCP server using official MCP protocol + + telecom: + enabled: true + transport: http + endpoint: http://localhost:8100/mcp + description: Telecom legacy HTTP mock MCP server + + retail: + enabled: true + transport: http + endpoint: http://localhost:8200/mcp + description: Retail legacy HTTP mock MCP server diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/prompt_policy.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/prompt_policy.yaml new file mode 100644 index 0000000..af4398f --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/prompt_policy.yaml @@ -0,0 +1,19 @@ +tone: + style: "claro, objetivo, empático" + forbidden_phrases: + - "procure atendimento humano" +vocabulary: + preferred: + fatura: "fatura" + contestacao: "contestação" +intents: + billing_agent: + - fatura + - boleto + - cobrança + - segunda via + product_agent: + - plano + - produto + - oferta + - serviço diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/routing.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/routing.yaml new file mode 100644 index 0000000..2dbe95e --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/routing.yaml @@ -0,0 +1,128 @@ +# Roteamento enterprise configurável com MCP-aware intents. +router: + # mode também pode ser definido por variável de ambiente ROUTING_MODE. + # Valores: router | supervisor + mode: router + fallback_agent: billing_agent + confidence_threshold: 0.65 + allow_handoff: true + +state_policies: + - state: WAITING_BILLING_CONFIRMATION + agent: billing_agent + description: Mantém mensagens curtas como "sim" ou "não" no fluxo de fatura. + - state: WAITING_PRODUCT_CONFIRMATION + agent: product_agent + description: Mantém confirmações no fluxo de produtos/serviços. + - state: WAITING_ORDER_CONFIRMATION + agent: orders_agent + description: Mantém confirmações no fluxo de pedidos. + - state: WAITING_SUPPORT_CONFIRMATION + agent: support_agent + description: Mantém confirmações no fluxo de suporte retail. + - state: COLLECTING_BILLING_PARAMETERS + agent: billing_agent + description: Mantém a coleta de parâmetros no fluxo de faturamento. + - state: COLLECTING_PRODUCT_PARAMETERS + agent: product_agent + description: Mantém a coleta de parâmetros no fluxo de produtos e serviços. + - state: COLLECTING_ORDER_PARAMETERS + agent: orders_agent + description: Mantém a coleta de parâmetros no fluxo de pedidos. + - state: COLLECTING_SUPPORT_PARAMETERS + agent: support_agent + description: Mantém a coleta de parâmetros no fluxo transacional de suporte retail. + +intents: + - name: billing_invoice_explanation + domain: telecom + agent: billing_agent + description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores. + priority: 10 + mcp_tools: + - consultar_fatura + - consultar_pagamentos + keywords: + - fatura + - conta + - cobrança + - boleto + - vencimento + - segunda via + - contestar + - valor alto + - invoice + examples: + - Minha fatura veio alta. + - Quero entender uma cobrança. + - Preciso da segunda via da conta. + + - name: product_services_information + domain: telecom + agent: product_agent + description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios. + priority: 20 + mcp_tools: + - consultar_plano + - listar_servicos + keywords: + - plano + - serviço + - pacote + - internet + - roaming + - vas + - benefício + - assinatura + examples: + - Quais serviços estão ativos no meu plano? + - Quero saber sobre meu pacote de internet. + - Tenho roaming internacional? + + - name: retail_order_tracking + domain: retail + agent: orders_agent + description: Consulta de pedido, entrega, rastreamento, atraso e status de compra. + priority: 30 + mcp_tools: + - consultar_pedido + - consultar_entrega + keywords: + - pedido + - entrega + - rastreio + - rastreamento + - encomenda + - compra + - atraso + - correios + examples: + - Meu pedido não chegou. + - Quero rastrear minha entrega. + - Qual é o status da minha compra? + + - name: retail_support_exchange_return + domain: retail + agent: support_agent + description: Suporte, troca, devolução, garantia e problema com produto. + priority: 25 + mcp_tools: + - consultar_pedido + - solicitar_troca + - solicitar_devolucao + keywords: + - solicitar devolução + - devolver pedido + - solicitar troca + - troca + - devolução + - devolver + - garantia + - defeito + - produto quebrado + - suporte + - arrependimento + examples: + - Quero trocar um produto. + - Meu produto veio com defeito. + - Como faço uma devolução? diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tool_policies.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tool_policies.yaml new file mode 100644 index 0000000..3c4fd05 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tool_policies.yaml @@ -0,0 +1,26 @@ +version: 1 + +# Arquivo opcional da aplicação. A ausência mantém o comportamento dos +# templates anteriores e as políticas legadas declaradas em tools.yaml. +defaults: + operation_type: read_only + require_confirmation: false + +tool_policies: + solicitar_troca: + operation_type: transactional + require_confirmation: true + + solicitar_devolucao: + operation_type: transactional + require_confirmation: true + requires: [order_id, reason] + execution: + mode: workflow + workflow: devolucao_pedido + version: active + +# Exemplo para uma operação real que só pode executar após confirmação: +# cancelar_servico: +# operation_type: transactional +# require_confirmation: true diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tools.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tools.yaml new file mode 100644 index 0000000..d85fae1 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/config/tools.yaml @@ -0,0 +1,101 @@ +tools: + consultar_fatura: + description: Consulta dados resumidos de fatura por msisdn/invoice_id. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + invoice_id: string + selection_keywords: + - fatura + - conta + - boleto + consultar_pagamentos: + description: Consulta histórico de pagamentos do cliente. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + selection_keywords: + - pagamento + - pagamentos + consultar_plano: + description: Consulta plano ativo e atributos comerciais. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + asset_id: string + selection_keywords: + - plano + listar_servicos: + description: Lista serviços ativos e adicionais VAS. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + selection_keywords: + - serviços + - servicos + - vas + consultar_pedido: + description: Consulta pedido de varejo por order_id/customer_id. + mcp_server: retail + enabled: true + args_schema: + order_id: string + customer_id: string + selection_keywords: + - consultar pedido + - status do pedido + - pedido + consultar_entrega: + description: Consulta entrega e rastreamento do pedido. + mcp_server: retail + enabled: true + args_schema: + order_id: string + selection_keywords: + - entrega + - rastreio + - rastreamento + - transportadora + - previsão + solicitar_troca: + description: Simula abertura de solicitação de troca. + mcp_server: retail + enabled: true + tool_type: action + requires: + - order_id + - reason + confirmation_required: true + args_schema: + order_id: string + reason: string + selection_keywords: + - solicitar troca + - trocar + - troca + - defeito + - quebrado + solicitar_devolucao: + description: Simula abertura de solicitação de devolução. + mcp_server: retail + enabled: true + tool_type: action + requires: + - order_id + - reason + confirmation_required: true + args_schema: + order_id: string + reason: string + selection_keywords: + - solicitar devolução + - solicitar devolucao + - devolver pedido + - devolver + - devolução + - devolucao + - arrependimento diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md new file mode 100644 index 0000000..d81efdf --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md @@ -0,0 +1,95 @@ +# Atualização do Template Backend — Analytics, Observer, NOC/GRL e OutputSupervisor + +Esta versão do `agent_template_backend` foi atualizada para consumir as novidades transportadas para o `agent_framework`. + +## 1. Analytics e Pub/Sub + +O backend não chama mais diretamente apenas o publisher antigo de eventos. Agora ele cria um `AnalyticsPublisher`: + +```python +from agent_framework.analytics.factory import create_analytics_publisher +from agent_framework.observability.observer import AgentObserver + +analytics = create_analytics_publisher(settings) +observer = AgentObserver(analytics=analytics) +``` + +Com isso, o mesmo backend pode publicar em: + +- OCI Streaming +- GCP Pub/Sub +- CompositePublisher, quando `ANALYTICS_PROVIDERS=oci_streaming,pubsub` +- Noop, quando analytics estiver desligado + +## 2. Configuração mínima + +```env +ENABLE_ANALYTICS=true +ANALYTICS_PROVIDERS=pubsub +GCP_PUBSUB_TOPIC_PATH=projects//topics/ +GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json +``` + +Para publicar simultaneamente em OCI Streaming e GCP Pub/Sub: + +```env +ENABLE_ANALYTICS=true +ANALYTICS_PROVIDERS=oci_streaming,pubsub +ENABLE_OCI_STREAMING=true +OCI_STREAM_ENDPOINT= +OCI_STREAM_OCID= +GCP_PUBSUB_TOPIC_PATH=projects//topics/ +``` + +## 3. Observer corporativo + +O workflow recebeu emissão automática dos principais eventos corporativos: + +- `NOC.001`: início do workflow +- `NOC.005`: exceção fatal no workflow +- `NOC.006`: fim do workflow antes da resposta final +- `IC.AGENT_COMPLETED`: evento informacional de conclusão +- `GRL.001` a `GRL.009`: emitidos pelo `OutputSupervisor` + +## 4. OutputSupervisor + +Foi inserido um novo nó LangGraph: + +```text +agent -> output_supervisor -> output_guardrails -> judge -> supervisor_review -> persist +``` + +O `OutputSupervisor` não substitui o supervisor de roteamento. Ele valida a saída candidata do agente usando o contrato corporativo: + +- `allow` +- `sanitize` +- `retry` +- `block` +- `handover` +- `observe` + +Para compatibilidade com os guardrails já existentes, o template inclui o adapter `LegacyOutputGuardrailRail`, que converte decisões antigas `allowed=True/False` para `RailAction`. + +## 5. Campos adicionados ao AgentState + +```python +supervisor_action: str +supervisor_guidance: str +supervisor_attempt: int +supervisor_handover_reason: str +output_supervisor_results: list[dict] +output_guardrails_already_applied: bool +``` + +## 6. Arquivos alterados + +- `agent_template_backend/app/main.py` +- `agent_template_backend/app/workflows/agent_graph.py` +- `agent_template_backend/app/state.py` +- `agent_template_backend/.env` +- `agent_template_backend/requirements.txt` +- `agent_framework/src/agent_framework/config/settings.py` + +## 7. Observação importante + +O `OutputSupervisor` roda os guardrails de saída por meio do adapter legado e marca `output_guardrails_already_applied=True`. Assim o nó `output_guardrails` permanece no grafo para compatibilidade, mas evita reexecutar a mesma validação quando o supervisor já aplicou os rails. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md new file mode 100644 index 0000000..83975af --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md @@ -0,0 +1,45 @@ +# Como usar IC, NOC e GRL no Template Backend + +## IC — Item de Controle + +Use IC para registrar eventos de negócio relevantes. + +```python +await observer.emit_ic( + "IC.FATURA_CONSULTADA", + {"session_id": session_id, "invoice_id": invoice_id}, + component="billing_agent", +) +``` + +## NOC — Evento operacional + +Use NOC para saúde técnica, latência, erros e checkpoints operacionais. + +```python +await observer.emit_noc( + "003", + {"session_id": session_id, "resourceName": "ADB", "latencyMs": 120}, + component="repository", +) +``` + +## GRL — Evento de guardrail + +Normalmente o framework emite GRL automaticamente. Use manualmente apenas para +rails customizados dentro do agente. + +```python +await observer.emit_grl( + "OBSERVE", + {"session_id": session_id, "rail_code": "CUSTOM_POLICY"}, + component="custom_rail", +) +``` + +## Onde já existe no template + +- `app/workflows/agent_graph.py` emite IC/NOC no ciclo do workflow. +- `app/agents/runtime.py` emite IC para MCP/tools. +- `app/agents/*_agent.py` contém exemplos dentro do método `run()`. +- `app/examples/` contém exemplos isolados. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md new file mode 100644 index 0000000..3f981ac --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md @@ -0,0 +1,48 @@ +# Backends atualizados para ConversationSummaryMemory + +Esta versão dos backends foi compatibilizada com a versão do framework que adiciona `ConversationSummaryMemory`. + +## O que mudou + +- `app/main.py` agora inicializa `create_conversation_summary_memory(...)` junto com `create_memory(...)`. +- `AgentWorkflow` recebe `summary_memory` e repassa para os agentes. +- Os agentes não montam mais prompts manuais para o LLM; agora usam `build_messages()` do framework. +- Antes da chamada ao LLM, os agentes executam `await self.prepare_memory_context(state)`. +- Quando habilitado por `.env`, o prompt passa a receber: + - resumo acumulado da conversa; + - últimas mensagens completas; + - mensagem atual; + - BusinessContext; + - MCP results; + - RAG context e metadata. + +## Configuração + +```env +ENABLE_CONVERSATION_SUMMARY_MEMORY=true +MEMORY_CONTEXT_STRATEGY=summary +MEMORY_HISTORY_LIMIT=80 +MEMORY_RECENT_MESSAGES_LIMIT=8 +MEMORY_SUMMARY_TRIGGER_MESSAGES=20 +MEMORY_MAX_SUMMARY_CHARS=6000 +MEMORY_SUMMARY_USE_LLM=true +MEMORY_INJECT_RECENT_MESSAGES=true +MEMORY_INJECT_SUMMARY=true +``` + +## Backends alterados + +- `backoffice_convertido_framework` +- `agent_template_backend` +- `agent_template_backend_day_zero` + +## Observação importante + +Estes backends esperam que o pacote `agent_framework` instalado/conectado seja a versão com os módulos: + +- `agent_framework.memory.summary_memory` +- `agent_framework.memory.summary_store` +- `AgentRuntimeMixin.prepare_memory_context()` +- `AgentRuntimeMixin.build_messages()` com injeção de memória + +Use junto com o ZIP `agent_framework_conversation_summary_memory.zip`. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md new file mode 100644 index 0000000..5c41732 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md @@ -0,0 +1,14 @@ +# Exemplos implementados no template + +Este projeto entrega as capacidades transversais habilitadas como referência: + +- route stickiness semântica com o perfil `route_continuity`; +- decisões `CONTINUE`, `ROUTE`, `HUMAN_HANDOFF` e `END_SESSION`; +- nós globais `human_handoff` e `end_session`; +- persistência de `active_agent`, `route_bypassed`, `continuity_signal` e controle de sessão; +- rejeição de novas mensagens depois de `session_ended=true`; +- políticas MCP `read_only` e `transactional` no backend; +- exemplo `solicitar_devolucao` com `require_confirmation: true`. + +Para confirmar a transação, envie `confirmed: true` ou `confirmation: true` como booleano. Handoff e encerramento não chamam agentes de domínio nem MCP. + diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md new file mode 100644 index 0000000..c7bd3b2 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md @@ -0,0 +1,84 @@ +# FRAMEWORK_CHANNEL_INPUT_MODE + +This backend setting controls what kind of channel input the Agent Framework backend accepts. + +It replaces the ambiguous use of `CHANNEL_GATEWAY_MODE` inside the backend. + +## Values + +```env +FRAMEWORK_CHANNEL_INPUT_MODE=embedded +``` + +The backend may use internal channel adapters to interpret simple/native channel payloads. This is useful for demos, labs, local frontend, curl tests, and simple environments. + +```env +FRAMEWORK_CHANNEL_INPUT_MODE=external +``` + +The backend accepts only a normalized `GatewayRequest` produced by an external Channel Gateway. It does not parse native WhatsApp, Voice, Teams, or other channel payloads. + +## Recommended enterprise setup + +In the external channel gateway service: + +```env +CHANNEL_GATEWAY_RUNTIME_MODE=adapter +``` + +In this backend: + +```env +FRAMEWORK_CHANNEL_INPUT_MODE=external +``` + +Flow: + +```text +External channel / browser / customer adapter + ↓ +channel_gateway:7000 + CHANNEL_GATEWAY_RUNTIME_MODE=adapter + ↓ GatewayRequest +agent_template_backend:8000 + FRAMEWORK_CHANNEL_INPUT_MODE=external + ↓ +LangGraph / Agents / MCP / Guardrails +``` + +## Valid direct request to backend in external mode + +```bash +curl -s -X POST "http://localhost:8000/gateway/message" \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "tenant_id": "default", + "agent_id": "telecom_contas", + "payload": { + "message": "Quero consultar minha fatura", + "session_id": "backend-external-ok-001" + } + }' | jq +``` + +## Invalid direct request to backend in external mode + +```bash +curl -i -s -X POST "http://localhost:8000/gateway/message" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Quero consultar minha fatura", + "session_id": "raw-payload-error-001" + }' +``` + +Expected result: HTTP 422. + +## Legacy compatibility + +`CHANNEL_GATEWAY_MODE` is still present as a legacy alias for older environments, but new deployments should use: + +```env +FRAMEWORK_CHANNEL_INPUT_MODE=embedded|external +``` diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md new file mode 100644 index 0000000..849fda1 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md @@ -0,0 +1,127 @@ +# Guardrails paralelos fail-fast e Observer IC + +## O que foi implementado + +### 1. ParallelRailExecutor + +Arquivo principal: + +```text +agent_framework/src/agent_framework/guardrails/parallel_executor.py +``` + +Também foi criado um alias de compatibilidade: + +```text +agent_framework/src/agent_framework/guardrails/executor.py +``` + +Esse alias evita erro quando algum código antigo importar: + +```python +from agent_framework.guardrails.executor import ParallelRailExecutor +``` + +### 2. Execução paralela no GuardrailPipeline + +Arquivo alterado: + +```text +agent_framework/src/agent_framework/guardrails/pipeline.py +``` + +O pipeline continua retornando o contrato antigo: + +```python +(texto_final, list[RailDecision]) +``` + +mas internamente pode executar rails em paralelo com fail-fast. + +### 3. Execução paralela no OutputSupervisor + +Arquivo alterado: + +```text +agent_framework/src/agent_framework/guardrails/output_supervisor.py +``` + +O `OutputSupervisor` agora usa `ParallelRailExecutor` quando habilitado. + +### 4. Configuração + +Novas configurações: + +```env +ENABLE_PARALLEL_GUARDRAILS=true +GUARDRAILS_FAIL_FAST=true +``` + +Também foram adicionadas em: + +```text +agent_framework/src/agent_framework/config/settings.py +.env +.env.example +agent_template_backend/.env +agent_template_backend_day_zero/.env +``` + +### 5. Observer IC + +O `AgentObserver` já tinha `emit_ic()`. + +Foi complementada a API global compatível com FIRST/TIM: + +```python +from agent_framework.observer import ic, aic, noc, anoc, grl, agrl +``` + +Exemplos: + +```python +ic("AGENT_COMPLETED", data={"session_id": "..."}) +await aic("MCP_TOOL_CALLED", data={"tool_name": "consultar_fatura"}) +``` + +### 6. ICs automáticos no template backend + +O backend emite agora: + +```text +IC.AGENT_STARTED +IC.ROUTE_SELECTED +IC.MCP_TOOL_CALLED +IC.TOOL_CALLED +IC.AGENT_COMPLETED +``` + +Além dos eventos já existentes: + +```text +NOC.001 +NOC.005 +NOC.006 +GRL.001 ... GRL.009 +``` + +## Validações executadas + +Foram executadas validações locais com `PYTHONPATH=agent_framework/src`: + +```bash +python3 -m compileall -q agent_framework/src/agent_framework agent_template_backend/app agent_template_backend_day_zero/app +``` + +Smoke tests executados: + +```text +1. Import de ParallelRailExecutor via agent_framework.guardrails +2. Import de ParallelRailExecutor via agent_framework.guardrails.executor +3. Execução fail-fast: FastBlock cancela SlowAllow +4. GuardrailPipeline paralelo retorna RailDecision legado +5. OutputSupervisor paralelo retorna RailAction.BLOCK +6. API global observer.ic/noc/grl/aic/anoc/agrl +``` + +Observação: o import completo do `agent_template_backend.app.workflows.agent_graph` depende de `langgraph`, que não está instalado no sandbox de validação. O arquivo foi validado por `compileall`, e a dependência já consta em `agent_template_backend/requirements.txt`. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md new file mode 100644 index 0000000..edcd2c7 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md @@ -0,0 +1,42 @@ +# Implementação IC/NOC/GRL preservando lógica existente + +Esta versão mantém a lógica original dos agentes do `agent_template_backend` e adiciona observabilidade corporativa. + +## IC adicionados nos agentes + +Cada agente agora emite eventos de negócio sem alterar a resposta final: + +- `IC.BILLING_AGENT_STARTED` / `IC.BILLING_AGENT_COMPLETED` +- `IC.ORDERS_AGENT_STARTED` / `IC.ORDERS_AGENT_COMPLETED` +- `IC.PRODUCT_AGENT_STARTED` / `IC.PRODUCT_AGENT_COMPLETED` +- `IC.SUPPORT_AGENT_STARTED` / `IC.SUPPORT_AGENT_COMPLETED` +- `IC._MCP_CONTEXT_COLLECTED` quando houver dados MCP +- `IC._RAG_CONTEXT_RETRIEVED` quando RAG estiver habilitado + +O mixin `AgentRuntimeMixin` também emite: + +- `IC.MCP_TOOL_CALLED` antes da chamada MCP +- `IC.TOOL_CALLED` após a chamada MCP + +## NOC + +O workflow já emite eventos operacionais principais: + +- `NOC.001` no início da execução +- `NOC.005` em exceção fatal +- `NOC.006` na persistência/finalização + +## GRL + +O backend agora também exemplifica emissão GRL no workflow: + +- `GRL.001` início do pipeline de guardrails +- `GRL.002` decisão allow +- `GRL.004` decisão block +- `GRL.009` decisão final agregada + +Quando `OutputSupervisor` está habilitado, ele continua sendo o principal mecanismo corporativo de supervisão de saída. + +## Garantia + +A lógica original dos agentes não foi substituída por stubs. As chamadas LLM, MCP, RAG, cache e os retornos originais foram preservados. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md new file mode 100644 index 0000000..bc2638b --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md @@ -0,0 +1,5 @@ +# Langfuse single trace observer fix + +This backend now uses `TelemetryBackedAgentObserver` instead of publishing IC/NOC/GRL through `AgentObserver(analytics=...)`. + +Why: when analytics includes the Langfuse provider, observer events such as `IC.AGENT_COMPLETED` and `NOC.006` may create a second root trace with little detail. Emitting those events through `Telemetry.event(...)` keeps them inside the active request/workflow trace. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md new file mode 100644 index 0000000..cfe5969 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md @@ -0,0 +1,82 @@ +# Teste e diagnóstico de Long-Term Memory + +## O que foi corrigido + +1. A LTM agora é carregada explicitamente antes do roteamento. +2. O estado recebe uma chave estável em `long_term_memory_subject_key`, baseada em `business_context.customer_key` e, como fallback, `user_id`. +3. O resultado de carga e persistência aparece em `metadata.long_term_memory` da resposta. +4. `/health` informa a configuração efetiva de LTM carregada pelo processo. +5. Falhas de leitura e gravação geram eventos `long_term_memory.load.failed` e `long_term_memory.persist.failed`. + +## Teste + +Primeira sessão: + +```bash +curl -s http://localhost:8000/gateway/message \ + -H 'Content-Type: application/json' \ + -d '{ + "channel":"web", + "payload":{ + "text":"Meu nome preferido é Cris e minha linguagem preferida é Python.", + "session_id":"ltm-session-001", + "user_id":"ltm-user-001", + "customer_id":"ltm-customer-001" + } + }' +``` + +Verifique na resposta: + +```json +"long_term_memory": { + "subject_key": "ltm-customer-001", + "write_result": { + "saved": 2 + } +} +``` + +Nova sessão, mesma identidade: + +```bash +curl -s http://localhost:8000/gateway/message \ + -H 'Content-Type: application/json' \ + -d '{ + "channel":"web", + "payload":{ + "text":"Qual é meu nome preferido e qual linguagem eu prefiro?", + "session_id":"ltm-session-002", + "user_id":"ltm-user-001", + "customer_id":"ltm-customer-001" + } + }' +``` + +Na segunda resposta, confira: + +- `metadata.long_term_memory.subject_key` igual à primeira chamada; +- `metadata.long_term_memory.loaded` com registros; +- `metadata.long_term_memory.context` preenchido; +- ausência de `load_error`. + +## Diagnóstico rápido + +```bash +curl -s http://localhost:8000/health +``` + +A seção `long_term_memory` deve mostrar: + +```json +{ + "enabled": true, + "provider": "sqlite", + "sqlite_path": "./data/agent_framework.db", + "table": "agentfw_long_term_memory", + "auto_extract": true, + "inject_context": true +} +``` + +Execute o backend com o diretório do projeto como diretório de trabalho. Como o caminho SQLite é relativo, iniciar a aplicação em outro diretório pode criar ou consultar outro arquivo `./data/agent_framework.db`. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md new file mode 100644 index 0000000..a9e4458 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md @@ -0,0 +1,62 @@ +# Validação da versão com IC/NOC/GRL + +Validações executadas nesta geração: + +1. `python -m compileall -q agent_template_backend/app` + - Resultado: OK. + +2. Smoke test dos agentes com LLM fake e Observer fake: + - `BillingAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + - `OrdersAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + - `ProductAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + - `SupportAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + +3. Verificação de regressão: + - Nenhum agente retorna `Template Enterprise ativo`. + - A lógica LLM/MCP/RAG/cache existente foi preservada. + +## Eventos adicionados + +### IC + +Nos agentes: + +- `IC.BILLING_AGENT_STARTED` +- `IC.BILLING_MCP_CONTEXT_COLLECTED` +- `IC.BILLING_RAG_CONTEXT_RETRIEVED` +- `IC.BILLING_AGENT_COMPLETED` +- `IC.ORDERS_AGENT_STARTED` +- `IC.ORDERS_MCP_CONTEXT_COLLECTED` +- `IC.ORDERS_RAG_CONTEXT_RETRIEVED` +- `IC.ORDERS_AGENT_COMPLETED` +- `IC.PRODUCT_AGENT_STARTED` +- `IC.PRODUCT_MCP_CONTEXT_COLLECTED` +- `IC.PRODUCT_RAG_CONTEXT_RETRIEVED` +- `IC.PRODUCT_AGENT_COMPLETED` +- `IC.SUPPORT_AGENT_STARTED` +- `IC.SUPPORT_MCP_CONTEXT_COLLECTED` +- `IC.SUPPORT_RAG_CONTEXT_RETRIEVED` +- `IC.SUPPORT_AGENT_COMPLETED` + +No runtime MCP: + +- `IC.MCP_TOOL_CALLED` +- `IC.TOOL_CALLED` + +### NOC + +Já integrados no workflow: + +- `NOC.001` início da execução +- `NOC.005` erro fatal +- `NOC.006` finalização/persistência + +### GRL + +No workflow de guardrails: + +- `GRL.001` início da avaliação +- `GRL.002` allow +- `GRL.004` block +- `GRL.009` decisão final + diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt new file mode 100644 index 0000000..fac4bf4 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt @@ -0,0 +1,3 @@ +compileall app: OK +Arquivos de exemplos IC/NOC/GRL adicionados. +Agentes preservam implementação original comentada. diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/llm_profiles.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/llm_profiles.yaml new file mode 100644 index 0000000..908b382 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/llm_profiles.yaml @@ -0,0 +1,80 @@ +profiles: + default: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + max_tokens: 2048 + supervisor: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 700 + route_continuity: + provider: oci_openai + model: openai.gpt-4.1-mini + temperature: 0 + max_tokens: 80 + timeout_seconds: 5 + router: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 500 + guardrail: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 600 + grl: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 700 + judge: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 800 + rag_rewriter: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 300 + rag_compressor: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 1200 + rag_generation: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.1 + max_tokens: 1800 + summary_memory: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.1 + max_tokens: 1200 + noc: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 700 + billing_agent: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + product_agent: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + backoffice_agent: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + mcp_parameter_extraction: + provider: oci_openai + model: openai.gpt-4.1-mini + temperature: 0 + max_tokens: 80 + timeout_seconds: 5 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/requirements.txt b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/requirements.txt new file mode 100644 index 0000000..71214bd --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/requirements.txt @@ -0,0 +1,23 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +pydantic>=2.8.0 +pydantic-settings>=2.4.0 +python-dotenv>=1.0.1 +langgraph>=0.2.60 +langchain-core>=0.3.0 +openai>=1.60.0 +oci>=2.130.0 +oracledb>=2.4.0 +pymongo>=4.8.0 +redis>=5.0.0 +PyYAML>=6.0.2 + +langfuse>=3.0.0 +httpx>=0.27.0 +opentelemetry-api>=1.27.0 +opentelemetry-sdk>=1.27.0 +opentelemetry-exporter-otlp-proto-http>=1.27.0 + +pytest>=8.0.0 +pytest-asyncio>=0.23.0 +google-cloud-pubsub>=2.28.0 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/scripts/test_long_term_memory.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/scripts/test_long_term_memory.py new file mode 100644 index 0000000..52e2a8d --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/scripts/test_long_term_memory.py @@ -0,0 +1,29 @@ +import asyncio +import tempfile +from types import SimpleNamespace +from agent_framework.memory.long_term_memory import create_long_term_memory_manager + +async def main(): + with tempfile.TemporaryDirectory() as d: + settings = SimpleNamespace( + ENABLE_LONG_TERM_MEMORY=True, + LONG_TERM_MEMORY_PROVIDER='sqlite', + LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db', + LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory', + LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20, + LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70, + LONG_TERM_MEMORY_AUTO_EXTRACT=True, + ) + manager = create_long_term_memory_manager(settings) + first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}} + assert (await manager.persist_turn(first))['saved'] >= 3 + second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}} + values = {item.key:item.value for item in await manager.load(second)} + assert values['preferred_name'].lower() == 'cris' + assert values['preferred_language'].lower() == 'python' + assert values['current_project'].lower() == 'atlas' + isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}} + assert await manager.load(isolated) == [] + print('OK: persistência, recuperação entre sessões e isolamento validados') + +asyncio.run(main()) diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/tests/test_transactional_workflow_template.py b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/tests/test_transactional_workflow_template.py new file mode 100644 index 0000000..eafd928 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/tests/test_transactional_workflow_template.py @@ -0,0 +1,42 @@ +from pathlib import Path + +import pytest + +pytest.importorskip("langgraph") + +from agent_framework.workflows import FileWorkflowRepository, WorkflowRuntime, WorkflowToolExecutor +import app.workflow_actions # noqa: F401 + + +@pytest.mark.asyncio +async def test_devolucao_workflow_executes_deterministically(): + root = Path(__file__).resolve().parents[1] + runtime = WorkflowRuntime(FileWorkflowRepository(root / "workflows")) + executor = WorkflowToolExecutor(runtime) + policy = { + "execution": { + "mode": "workflow", + "workflow": "devolucao_pedido", + "version": "active", + } + } + result = await executor.execute_from_policy( + tool_name="solicitar_devolucao", + arguments={"order_id": "123", "reason": "arrependimento", "confirmed": True}, + policy=policy, + ) + assert result is not None + assert result["status"] == "COMPLETED" + assert result["output"]["registrar_devolucao"]["protocol"] == "DEV-123" + + +@pytest.mark.asyncio +async def test_non_workflow_policy_keeps_direct_tool_path(): + root = Path(__file__).resolve().parents[1] + executor = WorkflowToolExecutor(WorkflowRuntime(FileWorkflowRepository(root / "workflows"))) + result = await executor.execute_from_policy( + tool_name="consultar_pedido", + arguments={"order_id": "123"}, + policy={"execution": {"mode": "direct_tool"}}, + ) + assert result is None diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.active.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.active.yaml new file mode 100644 index 0000000..b825518 --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.active.yaml @@ -0,0 +1 @@ +version: 1 diff --git a/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.v1.yaml b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.v1.yaml new file mode 100644 index 0000000..d2ff1aa --- /dev/null +++ b/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/workflows/devolucao_pedido.v1.yaml @@ -0,0 +1,27 @@ +name: devolucao_pedido +version: 1 +start: validar_pedido +nodes: + - id: validar_pedido + action: validar_pedido + input: + order_id: $.input.order_id + - id: registrar_devolucao + action: registrar_devolucao + retry: 1 + input: + order_id: $.input.order_id + reason: $.input.reason +edges: + - from: validar_pedido + to: registrar_devolucao + when: + path: $.nodes.validar_pedido.valid + equals: true + - from: validar_pedido + to: END + when: + path: $.nodes.validar_pedido.valid + equals: false + - from: registrar_devolucao + to: END diff --git a/Tuning-Performance/README.md b/Tuning-Performance/README.md new file mode 100644 index 0000000..049b205 --- /dev/null +++ b/Tuning-Performance/README.md @@ -0,0 +1,8 @@ +# Tuning-Performance + +Variantes e documentos de referência para comparar funcionalidades e impacto de performance. + +- `Normal`: baseline do template. +- `Route_Stickness`: continuidade de rota, handoff e políticas transacionais conversacionais. +- `Long_Term_Memory`: memória de longo prazo. +- `Deterministic_Transactional_Workflow`: transações multi-etapas executadas por workflow LangGraph determinístico após clarification e confirmação. diff --git a/docs/ADR_TRANSACTIONAL_WORKFLOW_ENGINE.md b/docs/ADR_TRANSACTIONAL_WORKFLOW_ENGINE.md new file mode 100644 index 0000000..b45197d --- /dev/null +++ b/docs/ADR_TRANSACTIONAL_WORKFLOW_ENGINE.md @@ -0,0 +1,17 @@ +# ADR — Motor de workflows transacionais no Agent Framework OCI + +## Decisão + +Adicionar ao framework uma capacidade opcional de execução determinística baseada em LangGraph. O motor é genérico; definições YAML e actions de domínio permanecem nos agentes. + +## Razão + +Operações multi-etapas com efeitos colaterais não devem depender do LLM para escolher a sequência crítica. A solução reduz tokens, latência e variação, além de melhorar auditoria, testes e versionamento. + +## Compatibilidade + +`execution.mode` assume `direct_tool`. Projetos existentes continuam usando MCP diretamente. A adoção de workflow é explícita por tool e pode ser controlada por `ENABLE_TRANSACTIONAL_WORKFLOWS`. + +## Limites desta entrega + +A base inclui validação, versionamento por arquivo, registry, execução sync/async, condições, retry por nó, cache de grafos e adapter de policy. Persistência corporativa de execution records, compensação/Saga, autorização por escopo e emissão de IC/NOC específica devem ser conectadas às abstrações existentes de cada deployment antes do uso em transações financeiras críticas. diff --git a/libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md b/libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md new file mode 100644 index 0000000..6b16220 --- /dev/null +++ b/libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md @@ -0,0 +1,84 @@ +# Workflows transacionais determinísticos + +## Objetivo + +O framework passa a oferecer um executor genérico de transações multi-etapas usando LangGraph como detalhe interno. O LLM permanece responsável por interpretação, roteamento, clarification e preparação da confirmação. Depois da confirmação explícita, passos críticos podem ser executados por um grafo determinístico, auditável e versionado. + +## Separação de responsabilidades + +O framework fornece carregamento, validação, compilação, cache, execução, retry por nó e integração com `tool_policies.yaml`. O projeto do agente mantém os YAMLs do domínio e as actions que chamam APIs ou MCPs. + +```text +LLM/router -> clarification -> transactional confirmation + -> WorkflowToolExecutor -> WorkflowRuntime/LangGraph + -> actions de domínio -> APIs/MCP +``` + +## Política + +```yaml +tool_policies: + solicitar_devolucao: + operation_type: transactional + require_confirmation: true + requires: [order_id, reason] + execution: + mode: workflow + workflow: devolucao_pedido + version: active +``` + +`direct_tool` é o padrão e mantém compatibilidade. `workflow` ativa o executor determinístico. `agent` fica reservado para orquestrações não determinísticas explicitamente autorizadas. + +## Arquivos e versionamento + +```text +workflows/devolucao_pedido.active.yaml # version: 1 +workflows/devolucao_pedido.v1.yaml # definição imutável +``` + +Uma execução resolve a versão ativa no início. Para reprodutibilidade, integrações persistentes devem guardar `workflow_name`, `workflow_version` e `execution_id`. + +## Actions + +```python +from agent_framework.workflows import workflow_action + +@workflow_action("registrar_devolucao") +async def registrar_devolucao(params: dict, state: dict) -> dict: + return {"protocol": "...", "status": "REQUESTED"} +``` + +As actions devem ser idempotentes quando causarem efeitos externos. O framework aceita `retry` por nó, mas retry seguro depende de chave idempotente no serviço de destino. + +## Condições suportadas + +Cada edge aceita `path` JSON-like (`$.input...` ou `$.nodes...`) e um operador: `equals`, `not_equals`, `exists` ou `in`. Transições críticas não são escolhidas por LLM. + +## Uso programático + +```python +from agent_framework.workflows import FileWorkflowRepository, WorkflowRuntime + +runtime = WorkflowRuntime(FileWorkflowRepository(settings.WORKFLOWS_PATH)) +result = await runtime.arun("devolucao_pedido", payload) +``` + +Para integração com policy: + +```python +from agent_framework.workflows import WorkflowToolExecutor + +executor = WorkflowToolExecutor(runtime) +result = await executor.execute_from_policy( + tool_name=tool_name, + arguments=arguments, + policy=resolved_policy, +) +``` + +Quando o retorno for `None`, a aplicação continua pelo caminho legado `direct_tool`. + +## Produção + +Antes de habilitar em produção, configure checkpointer persistente, idempotência nas actions, autorização, timeout na camada de integração e telemetria com `transaction_id`, `workflow_execution_id`, versão, nó e tentativa. O runtime não transforma automaticamente uma API não idempotente em uma operação segura. diff --git a/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc index cea7442a3fcd78e9af9fea971b4a47b675d9d400..3cb376f15c43a61550e7b08cefb8093ab8e32e2a 100644 GIT binary patch delta 62 zcmey%*u}*CnU|M~0SM;m=TGD=SC7`u%`4GQNi0d!PfRP1FV8Q^)=x}N%`1sdD@x2w O1@YtalQSpINdy3BH5KLn delta 49 zcmeBT`pd}unU|M~0SJQs-1#|i*6umtr0 delta 20 acmbQqJ(HXJGcPX}0}urN%iYM`#|i*9dIfj@ diff --git a/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc index 414c5213996771659d01d97fcc626364f668fe18..7300893fd31215c0495c06bb1660c6528d158d63 100644 GIT binary patch delta 20 acmX?+ej=UwGcPX}0}#yB&)>+s*BAgvCI)B# delta 20 acmX?+ej=UwGcPX}0}urN%iYMm*BAgx@CNGu diff --git a/libs/agent_framework/src/agent_framework/__pycache__/runtime_mcp_gateway_adapter.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/runtime_mcp_gateway_adapter.cpython-313.pyc index 3c784bfce968699df31c99dff8dc9aa0e6c43505..562f4db5a6674d345bd5bae442112547a6acf800 100644 GIT binary patch delta 20 acmca4a7lpsGcPX}0}#yB&)>-H&H(^DPX#Ri delta 20 acmca4a7lpsGcPX}0}urN%iYNB&H(^G83nKa diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc index 4082162979c3f1989df888ef52c4daaed4af0329..ffb0cb857c288400b81652c4dceb67409f4c539d 100644 GIT binary patch delta 20 acmaFK{F0gbGcPX}0}#yB&)>*>j}ZVs3+soCN?lRs}o& delta 20 acmdnQxrvkeGcPX}0}urN%iYMmoCN?o83n!o diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc index eba357398bd21a6668ef1245ac573c84e58a26e6..1586e5374eb7cdbbd65486d3a1bb1f0d2644010d 100644 GIT binary patch delta 20 acmca0b3umtGcPX}0}#yB&)>-H$_D^GwFN~0 delta 20 acmca0b3umtGcPX}0}urN%iYNB$_D^Je+9?@ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc index 6068c50a54dd5c79546cba0a9a75df4e0e506a08..b9d5d60d1a897c4a7f211949ce4f409b2f4d6eec 100644 GIT binary patch delta 20 acmbQnH;s?`GcPX}0}#yB&)>++%?+sPXPc!;|2@> delta 20 acmX@+s$Ql4gJqADk delta 20 acmZ2mzP6nEGcPX}0}urN%iYMm$Ql4j00zPU diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc index d9890758bfc4de7ace23e7b86617329bb1021c4a..3eb59a0f316d9dd6759489e136bcd2178afe20be 100644 GIT binary patch delta 20 acmX@Xe1e(#GcPX}0}#y9&)>+smk|Iu>jiHB delta 20 acmX@Xe1e(#GcPX}0}urN%iYMmmk|Ixt_AS` diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc index 90d199ff49b773c50be5ecddfdd54abd363f3ad3..6cc8bb37ef8d7653a6711c1f2fe978c58d57bc35 100644 GIT binary patch delta 20 acmcb`dyAL*GcPX}0}#y9&)>*>o(%v$Cjppo diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc index 991504a68d7c16a49305b68e5831e065f1f4d852..ce24c06619a2e1928fc810a6faa1f09415a55778 100644 GIT binary patch delta 22 ccmZo)$JoA(k^3_*FBbz4%+t@`$o(e_08Q!!*#H0l delta 22 ccmZo)$JoA(k^3_*FBbz41pmw3$o(e_08p(5TL1t6 diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc index e4946e41decb692c21fdb9a25870ef4fbee7772d..d00300c961ed6ee49f4f3d9f48b173bd046bc28d 100644 GIT binary patch delta 20 acmaFP{hXWoGcPX}0}#y9&)>*>hZO)o=LO&Z delta 20 acmaFP{hXWoGcPX}0}urN%iYL*hZO)rss>^J diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc index ec831934594d4d32628d786714f13c2b84c0e61c..5c251f36002532e20738f3d256ead43a8a35d35b 100644 GIT binary patch delta 20 acmca+dCijhGcPX}0}#y9&)>*>S_S|_&ITX= delta 20 acmca+dCijhGcPX}0}urN%iYL*S_S||kp`jw diff --git a/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc index a5f45cf4e2cae04f092c94ecad7d729852ebec9d..53bb2f90d6611f71626a7c82bc18e5106fdbc018 100644 GIT binary patch delta 20 acmcb~bd!nuGcPX}0}#y9&)>-H#|QvCZv`L# delta 20 acmcb~bd!nuGcPX}0}urN%iYNB#|QvFG6kXl diff --git a/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc index 58fe8c4f1826e7de170a54b78571ea1fda74e842..38aaf2789e82f5c193e420aabba802e22126f26b 100644 GIT binary patch delta 20 acmeCF>Z#)X%*)Hg00i^&^EYxc+5!MUg9T*( delta 20 acmeCF>Z#)X%*)Hg00hDRayN1_+5!MXMg`{p diff --git a/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc index 78aea25ab3c5b2a40246d9178ccab8361d589041..dec5d15e778fc544d77cfaa216a682a55d11441c 100644 GIT binary patch delta 19 ZcmbQuIGd6CGcPX}0}#yB&!5OW0RS#h1i}CS delta 19 ZcmbQuIGd6CGcPX}0}urN%bmzQ0RS+V1w{Y= diff --git a/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc b/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc index d71517e1e6f9c01a18822b2885e3329db80e8622..031db81895d5f62eb3819fa88ba649124473b170 100644 GIT binary patch delta 22 ccmccEz<9BNk^3_*FBbz4%+=4|$nE9;08u#xL;wH) delta 22 ccmccEz<9BNk^3_*FBbz41pmw3$nE9;08|48#{d8T diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc index 11df46240a0c6952b6d50ee69e12752aecf2bcd2..dfe01b6ade6bffb11efcd3dcf3c419cc7a59acf0 100644 GIT binary patch delta 19 ZcmbQoIFFJ0GcPX}0}#yB&!5OW82~Q81j_&b delta 19 ZcmbQoIFFJ0GcPX}0}urN%bmzQ82~W{1x^3} diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc index c74d169d6ac4a72e57ed539417d14a39bab859d6..78a8552577f516f2d844f688aa098db1396aeb92 100644 GIT binary patch delta 20 acmbQFFiC;?GcPX}0}#yB&)>++E&u>Emjsmn delta 20 acmbQFFiC;?GcPX}0}urN%iYM$E&u>HVFeff diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc index a2adf2bfe8410edc60d25aa8a4ab39cb2841fff6..847f9cedaa7262b91461e38c99bbf17b78794701 100644 GIT binary patch delta 20 acmX@bdy1F)GcPX}0}#yB&)>+spA7&!p#_Zq delta 20 acmX@bdy1F)GcPX}0}urN%iYMmpA7&%YX%Si diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc index d4157f0ae4ac3b23896cbaea5e1147b1e7d5b478..bdfb79a44c3b368bebc851c95a0e642a8b372e9c 100644 GIT binary patch delta 20 acmcbpb5V!;GcPX}0}#yB&)>-HCI$dOB?WK* delta 20 acmcbpb5V!;GcPX}0}urN%iYNBCI$dQ?*;P! diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc index 46d96eabb5db398a3be18e88e8887de70a5c29ea..ee2e1319b7129aceba548c61abde7432d564b25b 100644 GIT binary patch delta 20 acmaFJ_K=PHGcPX}0}#yB&)>)$#tZ;HIt4)h delta 20 acmaFJ_K=PHGcPX}0}urN%iYKw#tZ;K1O>zZ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc index c61b5062cf33f9dfa5ec6e5ada016bd9a45fa2f8..eb3a70244b5e843dd7e89d61f792731dd55994ca 100644 GIT binary patch delta 22 ccmX?bneo77M()qNyj%=GFjqf+Blp%!09-5wU;qFB delta 22 ccmX?bneo77M()qNyj%=G5d1H9Blp%!0ABV7;{X5v diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc index 812334adf3fbbac53162c4d8ce3664c2dec280bd..dbf746610bcfa92805b5c4699a9e27f2ace01e3e 100644 GIT binary patch delta 20 acmdlKzA2phGcPX}0}#yB&)>+sTpIvGZUz1T delta 20 acmdlKzA2phGcPX}0}urN%iYMmTpIvJI0k_L diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc index ad52ea88d06393e7054cf400c9c266a624f4ab4c..af2746d4f8e25378e7c4b23cd3954fe9e3ad031e 100644 GIT binary patch delta 62 zcmdnSc$ks*>PYM7>Ee1{i delta 20 acmaE9`qGs9GcPX}0}urN%iYL*PYM7@_Xg1b diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc index 9a6ac892797667374818042f54f9af68ba8c8cdc..92755942228981e9ead12e95d2d6de96bd2da756 100644 GIT binary patch delta 886 zcmZ9LYe-XJ7{||>M>m~YVup~QO|~?gn-vyB1k=o7wvk7W;NW4)IlL~Otpo}5V^BnJ zz6hDwy^Jn;s!x6DOJDjh)M}0BLP2TXXJxn9`#c>ZI>9Jx-n$1}P zf3}@E(pFG5V%fyluvW>IjlGp{R<_GV=|c$*OCMoCHgK9Gnu2pgpNOVmkZ6Wz7S79N zo;gQU2}9Ino@fCsQ05|05-w8a5|I@y$p*<irSr@3@)UVixc*Pe74oX!241L^NrKM9Iy0Sb602n4|wE$32(L3fOB2RGueOugo=UX!Ksh|VVB*D$K*XA@U|#+TUlXNg+okj!LT;U2dG zXs^B?KEi@A{=XLPhpeSJ+9T6g?v6xZjn`dOSEDr4yS#kE8ef6Hbb-qTa z$K-T%hoQZtBitQHSXO=QbYYmWw_2*Gov{a6o7I!%9OLa_9OWs>GnD5jFHjOFFHv5h zyiwoTifxm4oI;sKnL(LFnM0XJSx}4Y1zQ&RG0A^IBH-%min^NpQNPRI(yid#T>en7 zE2^}BzcYxegb%l>O?D64stwzd8F^(DF|l}P&$w6--%!%$JySg{mZ$@c!Tbr)9=Dao z#De~Y39&T3(H0Z)`|~E$gXKLer9LV5Y{$8fLRrD#*`Z)mi3Fq3)~-+l)-XZ94I=nO z^a<}x>rU>BiR&Sy9&iRWV>Ds>oBUrV2rQ@?YJA_ns3{LcTM^PPk|)1Dj&yP;4( z;Mcjmg`-cmj)%J#8+N+cioY}i=T)ETSKe*rw(=e>5X}%jW*iSqK554>v;vF!~*WdSheLf{XT?roXZW_CPz6|$*=mmJu3QkUn zhn(r)i+0D|XfQVA9F81g?6LD)N;M@bJO>jfFHv5hR8eXulPIrI-k`j-dt#do&0;x+ zGLN!=vWT+8rPiTorlng(M$;_|jP#I|H8ld=H;9pEH-}wLl diff --git a/libs/agent_framework/src/agent_framework/config/settings.py b/libs/agent_framework/src/agent_framework/config/settings.py index 48c9fd7..c75359e 100644 --- a/libs/agent_framework/src/agent_framework/config/settings.py +++ b/libs/agent_framework/src/agent_framework/config/settings.py @@ -192,6 +192,8 @@ class Settings(BaseSettings): TOOLS_CONFIG_PATH: str = './config/tools.yaml' # Opcional. Se ausente, permanecem válidas as políticas legadas de tools.yaml. TOOL_POLICIES_PATH: str | None = './config/tool_policies.yaml' + ENABLE_TRANSACTIONAL_WORKFLOWS: bool = False + WORKFLOWS_PATH: str = './workflows' IDENTITY_CONFIG_PATH: str = './config/identity.yaml' MCP_PARAMETER_MAPPING_PATH: str = './config/mcp_parameter_mapping.yaml' MCP_TOOL_TIMEOUT_SECONDS: int = 30 diff --git a/libs/agent_framework/src/agent_framework/events/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/events/__pycache__/__init__.cpython-313.pyc index 06cad373692e360736e44a78bf947cecef28b7d2..25db31fdecf9f31c03d0db97008a77806f73b4d8 100644 GIT binary patch delta 19 ZcmbQkIERt@GcPX}0}#yB&!5OW5dbcB1jPUV delta 19 ZcmbQkIERt@GcPX}0}urN%bmzQ5dbi~1xNq@ diff --git a/libs/agent_framework/src/agent_framework/events/__pycache__/oci_streaming.cpython-313.pyc b/libs/agent_framework/src/agent_framework/events/__pycache__/oci_streaming.cpython-313.pyc index 5abf634ffbad815d2bf7a965cbe9a0a55b700d61..6f0b4bf43705b019007a2cec755ccba482b099d7 100644 GIT binary patch delta 20 acmdlZxkr-wGcPX}0}#yB&)>+skp}=gCk253 delta 20 acmdlZxkr-wGcPX}0}urN%iYMmkp}=i@dg9{ diff --git a/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc index 93f4ea3816910b0d9b10ff52653fdfe01a1e79c3..f0a3330139ce8795157c1b583357936b308b117f 100644 GIT binary patch delta 62 zcmbQlG@pt4GcPX}0}#yB&!5QMq@JRmn^&Trl30?cpO{u2U!Gr-t)G~lnpYB^R+N~V O3gXA-CudGvo(%w2@)f!O delta 49 zcmbQwG>M7(GcPX}0}urN%bm#GBo(fon^&Trl30?cpPZ9el$ll;pOc?okd|37aZ5G; Da_SKk diff --git a/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc index 38434ad760d0111f82cfc5d33732980332a23cf2..7a436a54248734b0001397df9a228d57373b0d28 100644 GIT binary patch delta 65 zcmaDN`B9SlGcPX}0}#yB&)>*>f>AwBKR2&LKP9mwQ9m)QJia`?C|f@&riA0_KR2&LKP9mwQ9n5+u_!aGGCn6izaTBMV)HM? G?_2=Yj}un_ diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/__init__.cpython-313.pyc index 4e268cad7a6f67d79b67b9483affe2fe716b6565..6bfab5ac6ffa49967ab12f64a2af07fe2e9141f4 100644 GIT binary patch delta 20 acmbQnI*pb4GcPX}0}#yB&)>-1%>)25T?E|# delta 20 acmbQnI*pb4GcPX}0}urN%iYM`%>)28Ck0>t diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/client.cpython-313.pyc index 8b4b6093a86fe654813aa45aced37b0c96f8efb9..7c4d8561a9b2a64593478ada038178ee41ac28c1 100644 GIT binary patch delta 20 acmX@1d_tM~GcPX}0}#yB&)>+sR}cU{bOpWu delta 20 acmX@1d_tM~GcPX}0}urN%iYMmR}cU~J_bPm diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/config.cpython-313.pyc b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/config.cpython-313.pyc index 64d7323b29aa95ee86b073f986c88c807188a2b6..16c4b37651b9c02c8aff6c909fbdfe43576eac48 100644 GIT binary patch delta 20 acmeyZ_FIknGcPX}0}#yB&)>*hAPfLT+6CVL delta 20 acmeyZ_FIknGcPX}0}urN%iYLbAPfLWqy}OD diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/models.cpython-313.pyc index 029d804d9fcf738ad57195d93daae8cd980fadc6..527a4a1ff875ea144d43a64f76aa4ec26361f371 100644 GIT binary patch delta 20 acmZorZcyg_%*)Hg00eXO^EYyr3IYH$Gz8`V delta 20 acmZorZcyg_%*)Hg00hDRayN393IYH&{sn0O diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/router.cpython-313.pyc index d7f28a87e766ac151b60e28407ab35fc3fbde1d8..ef4b2a9afc657f3a5a1b12c416a5a45a0d522f58 100644 GIT binary patch delta 20 acmZ2kyt0`4GcPX}0}#yB&)>*B&k_Je1O_Pp delta 20 acmZ2kyt0`4GcPX}0}urN%iYL5&k_Jg&IYUi diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/session_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/session_store.cpython-313.pyc index c89ab125b1c52c5f3e075842aa43378f4a38fc8f..43f44539c7289a674dba7e88f3d735f3e789aec3 100644 GIT binary patch delta 20 acmeB|?w98N%*)Hg00eXO^EYz0@B#oeXaxTN delta 20 acmeB|?w98N%*)Hg00hDRayN3f@B#ohG6jMF diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc index 3140462f0fc351f65f7e779f4bf6d26f10187627..794591bc4e47a68509bbd24506c27cd16539ffa9 100644 GIT binary patch delta 20 acmZ3?y_lQ(GcPX}0}#y9&)>*BgB1WaM+FoB delta 20 acmZ3?y_lQ(GcPX}0}urN%iYL5gB1Wd3I&z` diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc index 4df5677631e3f66a92d027465e96a875ea5f9af6..16295820759eae5ba95a1a210a81c8870eaf7cac 100644 GIT binary patch delta 20 acmZ3=wUmqdGcPX}0}#y9&)>)`#R>p5a|Dp8HU%00 diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc index 18512dd88d9ac86a6fcc832c4346755c97e79b6e..aa5b2b8ba588d2e11465954a17df9b9b0ffbca5e 100644 GIT binary patch delta 20 acmaED`r4HHGcPX}0}#y9&)>-XKnegyga%ar delta 20 acmaED`r4HHGcPX}0}urN%iYNRKneg#M+Vmb diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc index 92ec3518f7d01250d6161297a7312ac9fbf40de2..13504f391710bc46c6d98269aa73b65e3ace94c3 100644 GIT binary patch delta 20 acmeyQ_(_rbGcPX}0}#y9&)>-XTmS$?!Ui}1 delta 20 acmeyQ_(_rbGcPX}0}urN%iYNRTmS$_g$B9+ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/executor.cpython-313.pyc index dc425d0ab2e4640df4e436c896420e7839cb9a7c..d2a59de3b9be20b91c338c2cd6dd0f60238e60e9 100644 GIT binary patch delta 20 acmZ3^w490iGcPX}0}#y9&)>)`!w3L0p9G2k delta 20 acmZ3^w490iGcPX}0}urN%iYK=!w3L3Vg(EU diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc index 264dcbab5ebd8efa7e8a9639938a6b6b72d398ff..c0b1288dbb918c1ca650e3b1e4978317de1f1d09 100644 GIT binary patch delta 20 acmZpuZK&n`%*)Hg00i^&^EYyr+5rGT5CzBp delta 20 acmZpuZK&n`%*)Hg00hDRayN39+5rGV(*{Za diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/langgraph_adapters.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/langgraph_adapters.cpython-313.pyc index 39892d8d4acd7a4b59d0c3f372931394ca1edd83..359c587eab49338a9b4079510fadc46822d6bdf8 100644 GIT binary patch delta 20 acmdliyIGd|GcPX}0}#y9&)>+sf)4;YSOswa delta 20 acmdliyIGd|GcPX}0}urN%iYMmf)4;b8wK+K diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc index 231c3ca5df2c4795382d1a4b744294a31f55b42b..cbebd8d62047ed9a74851d9dbfbc3b06ca4be824 100644 GIT binary patch delta 20 acmdmCyu+CLGcPX}0}#y9&)>+sRuTX|Lj|`0 delta 20 acmdmCyu+CLGcPX}0}urN%iYMmRuTY01_n6* diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc index 8a88c26e217e3465b8bf312f107ea3e2bf558c10..0bbe55d57ec326aafb7ae6030ad4cb2973a16fd1 100644 GIT binary patch delta 20 acmZ2mySA45GcPX}0}#y9&)>+s$PNHV7zR%O delta 20 acmZ2mySA45GcPX}0}urN%iYMm$PNHX+Xm49 diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc index c2001d69db21407221d77f11cb926fd81f8e62ea..e3f5b3b2561dc5bf869ed54ad28b7fa53aadbdee 100644 GIT binary patch delta 22 ccmeC3&DcAek^3_*FBbz4%+t@`$lc@%080J_JOBUy delta 22 ccmeC3&DcAek^3_*FBbz41pmw3$lc@%08POLz5oCK diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc index 740e086aa605b13d57a3d645d93ac34874aebe60..1bee1db400febe72918c7b9bdca67ee1c1e8baaf 100644 GIT binary patch delta 20 acmX>faz2FnGcPX}0}#y9&)>-Hq6Gj(kOj{G delta 20 acmX>faz2FnGcPX}0}urN%iYNBq6Gj+QwC80 diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc index e518fa70bb7051524a722371c1ca2236a95cc241..35ae6178f58fd0ffe1a736522a8023a81ac24328 100644 GIT binary patch delta 20 acmcb@a)pKaGcPX}0}#y9&)>-H$pip9@C6nC delta 20 acmcb@a)pKaGcPX}0}urN%iYNB$pipCvjvy{ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc index e3a37cac29b666c22dab78a4ec46d43a69dcf503..a1aafa91a92c4f75fb8afa806a6d6f53c7a60d28 100644 GIT binary patch delta 19 ZcmeC?>E_}7%*)Hg00i^&^A~cn0RSyC1U~=( delta 19 ZcmeC?>E_}7%*)Hg00hDRau;&50RS&_1i=6R diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc index 2f156c25acc7613df3b0959632a27b20ee9af974..db02d0e48609cf24243e5fcba3ff9807ac6202bc 100644 GIT binary patch delta 20 acmdnQzKNatGcPX}0}#y9&)>+soEZQ(4Fxv< delta 20 acmdnQzKNatGcPX}0}urN%iYMmoEZQ*&;_{w diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc index 6602fd21d9912e13bc73b489db0a6ce0f9ef0f9d..bdfffe024abe26cfd47ffc36a02674a44ae865fc 100644 GIT binary patch delta 22 ccmeBw&e;E)k^3_*FBbz4%+t@`$lX#509OtN00000 delta 22 ccmeBw&e;E)k^3_*FBbz41pmw3$lX#509nxof&c&j diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc index 6faad3a2416beb0b8e66a6b63cdb15a1dfd30f0c..460b8117b6a1a9fcabfcb71daef5fe2854ed387b 100644 GIT binary patch delta 20 acmaDa_FjzpGcPX}0}#y9&)>+M!VLgIxdoH} delta 20 acmaDa_FjzpGcPX}0}urN%iYMG!VLgLd-H!3h98#|1V3 delta 20 acmcaCbXkb|GcPX}0}urN%iYNB!3h9BiUqg; diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/config.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/config.cpython-313.pyc index 3a057a3d254ddbbcad5d49288f5bbc381f1524f6..1d50f432141d72aaa1bb95b8133cbd066cee403b 100644 GIT binary patch delta 20 acmbPjJ=>c5GcPX}0}#y9&)>*BK^6cz-vx94 delta 20 acmbPjJ=>c5GcPX}0}urN%iYL5K^6c$q6PK< diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc index fe027d618294e43d187d9674f53580c29f591a4e..a2222fe953b9bfc09d8f40cb49dacfb44ecf99fc 100644 GIT binary patch delta 22 ccmcb!gz?T2M()qNyj%=GFi$^!BX?jB09dC6zyJUM delta 22 ccmcb!gz?T2M()qNyj%=G5d1H9BX?jB09$GYLI3~& diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc index 804112d9cb681e15a87b9cca996c246779456f13..dbabeedeef5585ae26c077c055c4e6414af72231 100644 GIT binary patch delta 20 acmZ2)y55xgGcPX}0}#y9&)>+sL<#^tBn6=W delta 20 acmZ2)y55xgGcPX}0}urN%iYMmL<#^v=LRDH diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc index 3a6e1eb7415e9c088c0f96eb0df0f7252caf593e..49b182aa5f0b85dd018c7f858f32625d53de1694 100644 GIT binary patch delta 20 acmZ1=y+E4#GcPX}0}#y9&)>*Bl@|aw7X>o_ delta 20 acmZ1=y+E4#GcPX}0}urN%iYL5l@|ay+6A=$ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc index e503c921f8cc8984b236d2569f6e4daea0aedbca..04c37f8cfb3c75c15b81730f4542458ee09e9e74 100644 GIT binary patch delta 20 acmeB{?Uv>K%*)Hg00i^&^EYxg@Bsid7zFtM delta 20 acmeB{?Uv>K%*)Hg00hDRayN1}@Bsif+XZ_7 diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc index be63e8f657593e811c9c9888a05cf2255a2ebdf7..fed7d53664d12c093b8511ca77cad1bd1a2eccd5 100644 GIT binary patch delta 20 acmX?+aw3KMGcPX}0}#y9&)>*xYXksCzy;y} delta 20 acmX?+aw3KMGcPX}0}urN%iYLrYXksFg9c;( diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc index f9b6bd9c947db57bb6a3a09ce5b1aac183196e40..f79860cb6e39f286fbbe389d6f51d3d9b718ad21 100644 GIT binary patch delta 20 acmezF^4*2|GcPX}0}#y9&)>+MqXYm-hz1`3 delta 20 acmezF^4*2|GcPX}0}urN%iYMGqXYm=O9r6; diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc index f7a4ad991036155697e76218c77bdc6c2484d5cf..bc6a8e1e27fd33a322644aac8087e7288a626f8f 100644 GIT binary patch delta 20 acmZ1#zbc;lGcPX}0}#y9&)>*BUmpNNSq1w5 delta 20 acmZ1#zbc;lGcPX}0}urN%iYL5UmpNQ90q*= diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc index 5f4e0e1d96bb613e6c6e1d14e19c3eb29719883c..e1abd2c68a7219d278eb280e0710b3927f43dd28 100644 GIT binary patch delta 20 acmdm(v@wbMGcPX}0}#y9&)>+cW(WX7-vyWe delta 20 acmdm(v@wbMGcPX}0}urN%iYMWW(WXAq6QiO diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc index fefcea12b5af42afb3f2ea21610a5bac2620799a..0884551846b7b10124c9e298526eb319a404ceaf 100644 GIT binary patch delta 20 acmbQwJfE5SGcPX}0}#y9&)>*Bg%JQWg9P*d delta 20 acmbQwJfE5SGcPX}0}urN%iYL5g%JQZMg?{N diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc index cdeb5fe31d8eeaf8d03414aaed511294f93b8eb7..7a1b44700a2f59e32e54aca2b720115e1940ef1f 100644 GIT binary patch delta 20 acmbPiI@y%_GcPX}0}#y9&)>-1Aq4+sUK0RBBL)os delta 20 acmdlLyepXdGcPX}0}urN%iYMmUK0RD<_3=d diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc index 5fad8587c6a376cefedd256dc023e58b2e974b44..2bd01ff011862ffcf82781298f364b1ca7d1667f 100644 GIT binary patch delta 20 acmcb|d5@F(GcPX}0}#y9&)>*>nFRnpHU+={ delta 20 acmcb|d5@F(GcPX}0}urN%iYL*nFRnr`36D& diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc index 558fed417b22bee8178a1b7e41760338f5852904..11dd10de63877a4d8a4f93f0ed2eaab9367c13f0 100644 GIT binary patch delta 20 acmey%{g<2jGcPX}0}#y9&)>-Xi4_1uR|X;g delta 20 acmey%{g<2jGcPX}0}urN%iYNRi4_1x8U~~Q diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc index 0bcda8cbe62278eb9c06e397d728f880cd96764e..113239377692eee6d27d663fdc1c6540d3e5ede9 100644 GIT binary patch delta 20 acmbPIG^L39GcPX}0}#y9&)>++X$b&9p9N_E delta 20 acmbPIG^L39GcPX}0}urN%iYM$X$b&CVg>5} diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc index f6b65e9634d704200c91eaf01c3977b2f1a493d6..905ecd1068fccf08eda78bcf2f1d9641811c0938 100644 GIT binary patch delta 22 ccmZ3|$+)DGk^3_*FBbz4%+t@`$UV~q08A7HQ~&?~ delta 22 ccmZ3|$+)DGk^3_*FBbz41pmw3$UV~q08ZBi)&Kwi diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc index c84cdeee38ac26b214a84d7f0f56895253be99ee..b5413c1419b855645a5ea3449cd8f4f755042c04 100644 GIT binary patch delta 20 acmZ4Dv&4t{GcPX}0}#y9&)>)`sRjT(v;{f< delta 20 acmZ4Dv&4t{GcPX}0}urN%iYK=sRjT+cLlrv diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc index 4dcf36d1cb49fa85faf3a79395640dcf55396d51..4911908597dfc652ea89a677b974b2905dd27f17 100644 GIT binary patch delta 20 acmbQvF`a|^GcPX}0}#y9&)>++!vX*@`UGhJ delta 20 acmbQvF`a|^GcPX}0}urN%iYM$!vX*`y#(t3 diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc index eab04b90756dc07a1520252cc82b95728944544e..4554b516ff966ca165630c23a12b353c95f6f321 100644 GIT binary patch delta 20 acmcZ+a3g^GGcPX}0}#y9&)>-Hs{sH*>mKgv&{ROE2 delta 20 acmcb?euJI+GcPX}0}urN%iYL*mKgv*zy>P- diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc index 2027ef8cb3aff1622ede1cb70bc7a4aa8a79be93..4baa1ebf1cd4775fc678ebf78409c90e3ed80bd4 100644 GIT binary patch delta 20 acmdnZvYUnbGcPX}0}#y9&)>*xzyts|2n5Rj delta 20 acmdnZvYUnbGcPX}0}urN%iYLrzyts~%LPpU diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc index 1cfdcab7bdd86c3fa092c117c034213aa9797b50..00322c01401ca7013677e95a58ec34f895ccb204 100644 GIT binary patch delta 20 acmaFJ_K=PHGcPX}0}#y9&)>)$#tZ;HK?Onp delta 20 acmaFJ_K=PHGcPX}0}urN%iYKw#tZ;K1O>zZ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/__init__.cpython-313.pyc index 1ee2199476eb31ab388544dc1c9bd33bc1d70df1..eb1d9a31ca9add96051e751aa3941f59c3ad8a50 100644 GIT binary patch delta 20 acmX@ga+HPpGcPX}0}#y9&)>*x#RLF3m;~tn delta 20 acmX@ga+HPpGcPX}0}urN%iYLr#RLF6TLo(X diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/supervision_template.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/supervision_template.cpython-313.pyc index 263f991726f228ba7ec474bcd8e604266465d5b1..4d96758e9682e4dd7784de9004c4cb55995508a9 100644 GIT binary patch delta 20 acmew$@Iiq4GcPX}0}#y9&)>+M$^igE1_g)! delta 20 acmew$@Iiq4GcPX}0}urN%iYMG$^igG$p#7l diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/tts_rules.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/tts_rules.cpython-313.pyc index 1b509f61732a5b162c46413861d6b5c6ed1155b2..bbf441d315485a760200227c7d048b2f5e84b671 100644 GIT binary patch delta 20 acmcc3ahrqtGcPX}0}#y9&)>)$zybh0ngulg delta 20 acmcc3ahrqtGcPX}0}urN%iYKwzybh3T?MxQ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc index c6f175821ffc1bbd390eb7aad03f11c00ead9b2c..7c5189f655aec8223253d420c994a0f3b8343c9c 100644 GIT binary patch delta 20 acmX@fbCQSqGcPX}0}#y9&)>*x#|8j9Tm=aL delta 20 acmX@fbCQSqGcPX}0}urN%iYLr#|8jC9|em5 diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc index bfead3571dee4518f1b18e192d34478a523a61cf..5b34ba65817ab32631a80e7ca28e7facd5ab13d2 100644 GIT binary patch delta 20 acmX@2c0`T)GcPX}0}#y9&)>*xDGUHUNd-Fq delta 20 acmX@2c0`T)GcPX}0}urN%iYLrDGUHX3+cpaK9tIt4`l delta 20 acmZ4OvD$adNh>#GcPX}0}#y9&)>+sOA7!-BnC48 delta 20 acmX>adNh>#GcPX}0}urN%iYMmOA7!<=LWR^ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc index 72a37fba11db97b9dada2a29ace0e775d20b2891..ad49474afcbb2d370aa6071bfb8c418d1cd1e4bf 100644 GIT binary patch delta 20 acmbO#IaQMTGcPX}0}#y9&)>-1#RC8~76lCe delta 20 acmbO#IaQMTGcPX}0}urN%iYM`#RC91*#(aP diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc index 89fe2f39c1f6204b42aa1fbfef706cd3dc7d59b1..efe66e0c1c035e4e3ff61791d9322222195674a8 100644 GIT binary patch delta 20 acmX>pc~X-5GcPX}0}#y9&)>+sj|TugHU+5w delta 20 acmX>pc~X-5GcPX}0}urN%iYMmj|Tui`35Th diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc index a0f1ef83f23baafb30bdca59afacc94562c9a952..cbb1164b38891978b494b0e783e7025266d33009 100644 GIT binary patch delta 20 acmaE@`C60vGcPX}0}#y9&)>-XKm-6q&IUIC delta 20 acmaE@`C60vGcPX}0}urN%iYNRKm-6tkp{T{ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc index 8e00a21425778f3a39d501e6a17942acd9da188e..1e25c612d8132ef2818e930e829c02a243414798 100644 GIT binary patch delta 20 acmeyY`B{_uGcPX}0}#y9&)>-XLIeOu4hByE delta 20 acmeyY`B{_uGcPX}0}urN%iYNRLIeOw(FV}~ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc index 616aadd2a1d287e2aff7c3c969ce52c0938e6553..07441aee62c65ba8a3829fefd87ff07cbe71fc1d 100644 GIT binary patch delta 20 acmexl|H+>FGcPX}0}#y9&)>-XTpj>Q2?m1z delta 20 acmexl|H+>FGcPX}0}urN%iYNRTpj>S%m)Pk diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/__init__.cpython-313.pyc index 73951307ce447435e7dc2ebbcdfe43bfdcbedaf1..6d92edd712ea63e563de448a8b15077dd65958ba 100644 GIT binary patch delta 20 acmdn0vsH)tGcPX}0}#y9&)>+cB?bUI%mp0) delta 20 acmdn0vsH)tGcPX}0}urN%iYMWB?bULj|HCq diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/correspondencia_item.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/correspondencia_item.cpython-313.pyc index 44306a64179bac71ffe2a15227afb6216750c15a..693e8a878634ef20a1212fa0675f962bcbf744e5 100644 GIT binary patch delta 20 acmdnuw#AM6GcPX}0}#y9&)>+csSE%>Sp{1F delta 20 acmdnuw#AM6GcPX}0}urN%iYMWsSE%^90lC~ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/groundedness.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/groundedness.cpython-313.pyc index 7d06e9de8dee2ff12fc13253a20f65edfb8481e6..ab54fd4bfb8f85f1a608aed5e1e4cdce8c09c277 100644 GIT binary patch delta 20 acmX@$bij%GGcPX}0}#y9&)>*xst5o*>Tp0jHEe1CL delta 20 acmccQe#xEtGcPX}0}urN%iYL*Tp0jJ@CLa6 diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/quantidade_coerente.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/quantidade_coerente.cpython-313.pyc index 45ee8e5bd204119ba1108f5f68f915f3f8bbf70b..8a5d4ee38f97758807fd137121cb4161889dd620 100644 GIT binary patch delta 20 acmez6`pcF3GcPX}0}#y9&)>-XUI_q9BnGMg delta 20 acmez6`pcF3GcPX}0}urN%iYNRUI_qB=LakR diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/servico_correto.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/servico_correto.cpython-313.pyc index a3debc2e2497169c10caf5c90af0514a3d0d6393..8bdf382ef7cf903706e163f72a17ca3c7c41f2f1 100644 GIT binary patch delta 20 acmccSe9f8rGcPX}0}#y9&)>*>S`h$7I|exb delta 20 acmccSe9f8rGcPX}0}urN%iYL*S`h$9{sy}M diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/verbalizacao_prematura.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/verbalizacao_prematura.cpython-313.pyc index 0aaff8e95b4f2d870fe2bfc9f965126380d5c8c1..d2b7d24cc4d207db7797c0a264ef406af3ed2f75 100644 GIT binary patch delta 20 acmdn!w9$$CGcPX}0}#y9&)>+crU(E(PX$N- delta 20 acmdn!w9$$CGcPX}0}urN%iYMWrU(E+5(UZt diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc index 7950104095149c647c99ec7d73de3c8b98b441cd..4e813efa47ef37ce8ddf256d058cc2a416d54877 100644 GIT binary patch delta 20 acmcc5e4m;7GcPX}0}#y9&)>*>g%JQgg9Wqz delta 20 acmcc5e4m;7GcPX}0}urN%iYL*g%JQjMg}$j diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc index 3e0aa5304c7615dada5a7ad1dbbb5b2aed446cb1..4934703eae1e19733ac70b26412fb852ab09e8e4 100644 GIT binary patch delta 20 acmbQwH=mFDGcPX}0}#y9&)>)`!VUm66$Fp~ delta 20 acmbQwH=mFDGcPX}0}urN%iYK=!VUm8*aZ>* diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/oos_blocklist.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/oos_blocklist.cpython-313.pyc index b4c48e41e54175784cbbc86b5116993af992f09a..ef9f3f39acf945abcdc375fed23a2e09057400d0 100644 GIT binary patch delta 20 acmdlbzDu0@GcPX}0}#y9&)>+so*Mu=l?8VI delta 20 acmdlbzDu0@GcPX}0}urN%iYMmo*Mu@SOxh2 diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc index d856571a10897991dedbc43118d8607c52f21f88..587b3d81ef62914356d21ccdc3e31ee661408102 100644 GIT binary patch delta 20 acmca0b3umtGcPX}0}#y9&)>-H$_D^Gyah%8 delta 20 acmca0b3umtGcPX}0}urN%iYNB$_D^Je+9?@ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc index d57555f189be3ee59cb1a48f3661bb6490d8816c..daddf1d387f9a52d8079f46af884add44f90bd9e 100644 GIT binary patch delta 20 acmZ3%y@H$jGcPX}0}#y9&)>*BmlXgv4+SFt delta 20 acmZ3%y@H$jGcPX}0}urN%iYL5mlXgx(gmde diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc index 6ccc946e8760e5f44d17aec26eb13e0c0fd763e1..c6acc34bbd373ae3aad4a72c947829d455dde7e9 100644 GIT binary patch delta 63 zcmeyt)XL2LnU|M~0SM;m=TGE*s-B{sn^&Trl30?cpO{u2U!Gr-t)G~lnpYB^R+N~V P3gXA-CudIPV$1^oj{+6b delta 50 zcmZo={=vlknU|M~0SJQs&DiyAun^&Trl30?cpPZ9el$ll;pOc?okd|37S)4Hs E0GwwLE&u=k diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc index b1ad7ac91ab817d464941af0cddde421650114cc..4c687e48a2eb4847b3319db8d66e87ba3eb9f71e 100644 GIT binary patch delta 65 zcmdm@a!`f)GcPX}0}#yB&)>-H#H^mCpPN^rpORRTsGpct9$%hcl&znbo|;z@pH`HZ Rn+oE`=O<@wPG>&L2LPlm7JL8z delta 52 zcmX@8vPFgaGcPX}0}%8&FG$O**j&PV GmJa})>Je4| diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc index 1875b54c452803d542b64c21263edb1935cbe05c..b476d0567f8364bc0305dc2843ce4725ee6ed9fa 100644 GIT binary patch delta 65 zcmZ1>u~mZmGcPX}0}#yB&)>*h&a9rKpPN^rpORRTsGpct9$%hcl&znbo|;z@pH`HZ Rn+oE`=O<@wp2OV30RW+%7Ha?i delta 52 zcmdlgu|k6TGcPX}0}urN%iYLb&MXzGpPN^rpORRTsGppZSd^Jo8K0A%Uyznrv3VtP G4+j9RIT6MH diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc index bbdd1edc2f72f1a068d94aa1496f07fc00431c3a..20ca015aea2f9eb0c2885f5591b87a18eae49b1f 100644 GIT binary patch delta 65 zcmX@9a!rN%GcPX}0}#yB&)>)$%&eZGpPN^rpORRTsGpct9$%hcl&znbo|;z@pH`HZ Rn+oE`=O<@wu3)~%2LQ3c7Rmqs delta 52 zcmcbna#Dr+GcPX}0}urN%iYKw%q$hIpPN^rpORRTsGppZSd^Jo8K0A%UyznrvALD` GA|C*}g%TwI diff --git a/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc index 8b627ee6238b676c96ec80b3c378db8077cedd94..953c1f9a9e3a2e2176576ca670a52e18c119007b 100644 GIT binary patch delta 20 acmdnMvVn#BGcPX}0}#y9&)>+c$^-y5Pz0&~ delta 20 acmdnMvVn#BGcPX}0}urN%iYMW$^-y869p^) diff --git a/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc index 1f0cfb9bd46399fc1363897d0187afdf4c26d2b2..c31ebd724d2f642b4c2df32baa3924f7a15a3b77 100644 GIT binary patch delta 22 ccmbO>hiTdzChpI?yj%=GFi$^!BR6*+089=B8~^|S delta 22 ccmbO>hiTdzChpI?yj%=GP~x4pk(;{@085Jo4FCWD diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc index 2a30b772feea0cfd23de7e7ed41d753884ff3902..8819c5113bab64c8f1016b4800dada84dac2112f 100644 GIT binary patch delta 19 ZcmZ3^xSWyuGcPX}0}#y9&!5OW2LLb=1m^$% delta 19 ZcmZ3^xSWyuGcPX}0}urN%bmzQ2LLiu1!({P diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/_compat.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/_compat.cpython-313.pyc index 0bb8c027fac5e9e97f190433eeaf5b5490896ba1..24ca7be0c617a09a3e43dba7e5cd355bb3b598ca 100644 GIT binary patch delta 20 acmbO)IA4(aGcPX}0}#y9&)>*Bg#!RKf&~`< delta 20 acmbO)IA4(aGcPX}0}urN%iYL5g#!RNMFp7v diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc index 0f6d371b6cfb97368e5745516db90456212643f4..d93219421ea48109727b3d9358afa069d6f9bb62 100644 GIT binary patch delta 20 acmbQEKS!VYGcPX}0}#y9&)>*BQ5*m|Q3Y85 delta 20 acmbQEKS!VYGcPX}0}urN%iYL5Q5*n06b0J= diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/models.cpython-313.pyc index d31b33721c6604de960e6166a91dca5976694194..ca31105315949a3554b8916077b57ab539470557 100644 GIT binary patch delta 20 acmeys_JNK2GcPX}0}#y9&)>+M$_xNNnFVhE delta 20 acmeys_JNK2GcPX}0}urN%iYMG$_xNQTm|s} diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc index 4c6a376fee56007325195f10e46f29deff7bdf82..cedce7428f7b7c4759e390fcf76a54720005551b 100644 GIT binary patch delta 19 ZcmZ3_xSo;wGcPX}0}#y9&!5P>1OPER1pfd4 delta 19 ZcmZ3_xSo;wGcPX}0}urN%bm!*1OPL91%Utn diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc index 90726b9e39da5b232600155278da9a08274c5692..cab6b1386c9d26d61d456779a20d9fc822e1ab2a 100644 GIT binary patch delta 20 acmexn_|1^}GcPX}0}#y9&)>-XS^@w_ss?NT delta 20 acmexn_|1^}GcPX}0}urN%iYNRS^@w|Z3gZD diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc index 95202ba30e9c1b2a79f51e081814493d559e6e8f..579652e3f3b34da2ac594f0789b6820ac6c028a3 100644 GIT binary patch delta 20 acmaFO`I?jaGcPX}0}#y9&)>-XfCT_SJ_Y9h delta 20 acmaFO`I?jaGcPX}0}urN%iYNRfCT_V0S0LR diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc index 9687b6a621e65fbc91cde5f5c6a13ef3686daabe..a7a3013f0d3c827f02fd2f26cfd9ea57fd7fbedf 100644 GIT binary patch delta 20 acmeCO>$2ni%*)Hg00i^&^EYz;mjeJgqy@+T delta 20 acmeCO>$2ni%*)Hg00hDRayN4SmjeJjX9h|D diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc index 329d5b800a88ff7f6e5f643ba1b03b3fad8cadf7..8983a27a9d39edffcae2cee5e25b17e07abcf7b7 100644 GIT binary patch delta 20 acmX@fagu}kGcPX}0}#y9&)>*x#{vL4$^`!a delta 20 acmX@fagu}kGcPX}0}urN%iYLr#{vL7jRk=K diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc index d82b59baad686a4142da867dcaf25ba643951b47..c4f3c1a8fba7abef3fb4cfa428154db5adeae3be 100644 GIT binary patch delta 20 acmdnNvV(>DGcPX}0}#y9&)>+c%LD*7kOaa2 delta 20 acmdnNvV(>DGcPX}0}urN%iYMW%LD*AQw2l- diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc index 01d19a393c725481928c56b755cc1d0394c071ad..644807836f0ac0e653a641cdec51b08bc68084ed 100644 GIT binary patch delta 19 ZcmbQqIFph4GcPX}0}#yB&!5QM2LLVy1iSzM delta 19 ZcmbQqIFph4GcPX}0}urN%bm#G2LLcm1wQ}) diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc index 5a14ed2cdf985f746c5f69db953f0083d2b51fcb..648393401c8759080a8265f0fc8716458145ec4e 100644 GIT binary patch delta 20 acmey#`jeIWGcPX}0}#yB&)>-XjtKxl)dmIt delta 20 acmey#`jeIWGcPX}0}urN%iYNRjtKxop9YBl diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc index 453ed4d209b3cc0339b644218642cfbc91ee0b3f..842b81337819a666eeb1e942320c2af499924e58 100644 GIT binary patch delta 20 acmZqmX!YR!%*)Hg00eXO^EYz;QU(A!LIu$P delta 20 acmZqmX!YR!%*)Hg00hDRayN4SQU(A%3+c#i$;ypPN^rpORRTsGpct9$%hcl&znbo|;z@pH`HZ Qn+oE`=O<@Q_Gioj0C^`BcmMzZ delta 51 zcmX@cypx&xGcPX}0}urN%iYMW#V8e|pPN^rpORRTsGppZSd^Jo8K0A%UyznrF*$}Y F3jmM#5Yqqv diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc index 2dd559a82f7fade7d7155ae354bfbaa7ffc400e2..ccc9304d4c412e5d540402b4fa5faef99f4d9f54 100644 GIT binary patch delta 67 zcmccB&-lEbk^3_*FBbz4%+=4|$i0wRJyt(AuS7p3u_RGHF|9nlJijPgKQTQuuOvRL TC^0t`#E;KU&fI*AS;PeZ0M-}M delta 54 zcmaFf&v>h!k^3_*FBbz41pmw3$i0wRDnLIsuS7p3u_RGHIVZ6wGp#Z{CqKU+Ewf_t IWo8i<0Q*W4EdT%j diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc index 2d11ab2e91715044dcba7fb8b37df27dba6eed9c..ab6c0249df60bb557ea299be273a3b596ba16afd 100644 GIT binary patch delta 65 zcmZn?=ojGr%*)Hg00eXO^EYyPFsaAt=jN5@rzDmn>L;d^$Cu|9W$P!Vr{L=$U7Gbc7o=rYY_4LO G$_fC4RuGo} diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc index 2af64c10fe6882f429d6ca5c9ce69174fa351f19..10dd06901e04091659ee7ca84dcdfa059684d38f 100644 GIT binary patch delta 65 zcmdm?a#V%;GcPX}0}#yB&)>)`!lWLrpPN^rpORRTsGpct9$%hcl&znbo|;z@pH`HZ Rn+oE`=O<@wwq*Lx4*;A67E%BJ delta 52 zcmX@AvO|UYGcPX}0}xDi&D+Q=!Xy=>pPN^rpORRTsGppZSd^Jo8K0A%UyznrvDuC3 GKR*DQ4iPy3 diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc index 3af5d3ccaa359b1bf912a062b2ecf0a622c38694..7995057056359bf0ac7874dac8af15ab6caf771e 100644 GIT binary patch delta 1741 zcmZuwU2GIp6ux(MW_EUVW{O3(J4=6tT380Q-C8ICDJp7%1gzQt(X=?3?oP|%?oM-e zmRh2=F&b-pv0jZ}Lh$Fo_+Vo6#YBDL$wZSjCA3U1XowHKRoVn&LgGC$ZAIfv=DX*f zd(N4A&-dN;TIV{n6;+iH8^63genysOwKRTxHtTi_CwgPxMu;eB)Q&7UgREpPx8_sF zcvG4EaA0aJ2(LWCcHT$Bh+qp5&cY`nB)JD z?{a5zMs{MY2H4J+TlKT20>0uE-GRU4noo?EjRCLf2GV+5AUXxVbk;rBRI2)Hmlclc zMsAiJKkJejJIv?EOazi`SDlJ$+ks5GIg^fX&Gap9dv+vWsrvb%<2!k0bi#(}`SADl zC}Huiw!K1${=gkQ+|I7^CR*CpKmGWe+`GW{FKua?eq!d#0^hcj%}*bmQ#-Emd7`s0 z0#`4vZ)5P0g;z#qZyh;0EVZNIq?Oo@iDD_kqNRo%)rdOQU~UTJyI*E z>&svGrGjG=UBiV|U;ga!ryiZet%~8*jrGpZ5Ae)Xcz_@gh5;j_kyRKlHL{t_%dN&M70BK`6jx? zAEPa?_ALv1?=`b)?%`+W&6Cqd<{GwNd2e%J+v;j32|1ekzSDZC__M6I0l;_y-iq8x1{}i2NU@~$X~$wljAPgN^iQi0cUQKH^~!i4*2mfFa3gjN2;L5`p@%?=f>{eo5NL|z z5CrT6cnky;C?N-=;(!nXbwiUjqan~noSJKI_-fF(h=YiNgY?y8s|bYFexjcwpWnAE zA-(xh{6hSr0ZslV&ln^w;mDc&@Nh0^}W!Ib6T-f@Q0Xma>k+Ro2sMLJc!a~LMPkTjj5d0x~|MwVoz%GDJyR_C;O^{b& zNNil@n04FWgk1FaAi~t>8LbCDNH1zFmGDeq^1okqAoVex>t**LGQ@H~xV3!@mSY&> vpHcg7D18I9-9*V7Xxpk7!`#0Sb+a(+9P(*zt# zd)`eg)z0^de!Jr2rxPR!0?V#Rrq@5 zh80;=yJ<0ZwN-Bh!N#h48=fTQ)8puVD14UYpd$^*pQLFv0Alh$doK+aWLGS~mfR6F zc&SX~2GJT)8iv6R!B=HmJOEksggEf-qPma}hu}r}@-Q8o4I#v`8#!LchDjJfFjH)d z_#mXTGP)~TSvf?)D9mez#lzrgzm!E$5S|NgKhp#EGVjM%R5b9hoLEW!<;iKZ(OAiJ zp8{VZE6d_y79Obk;B7{QS1H}73V*Ci>ha_VZlc!BPKsI^4l6(_H|Z9!h?(SiH$pC` zY!dZ7O#2LWnTAAs8tLcj30CgynI7NbAF8Bl9YIQKn7>_knd7n12FwH-gQ9-0o7E&7(Feq1FsB=}0h*O~wmfl07sT^6 z#TqW7*E?DmG2^aL3w#H?B*jUTVud-;Ci7#7&nRpLw)Fjj{2ct8vs8;1t~yKnJi#n{ z(~r#d5Umi5BE;E{I?(oPO(d9rcMdRK`-_7 W$>Oo6eLDg^cJ(oF;-`SXzxNyHdEgcR diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc index 014aa9e4bce7d9081b5ab0969c04c16423bd5c7e..823bf00126437c5de211dd09c55ed4ac6cc49c41 100644 GIT binary patch delta 1531 zcmb7EU1%It6rP#=o1Jl!O}c5e4YIpIH<>2wCTrWRB{pcV3L5jXZV8FHPG;|>)9lW~ zJF`hwLz^fTX?-YM3l_8wK3GB#t0S~vDWXCj`jSAQ@lqw?&vSx^APC-bQ<9=DdLO=t&t?6-)5;=GBGmTqHEg~8q^Kf zoJE?C`m1&cQ5i}pSL`_&nnl<#ZA%M~U!sR2G>lFnqvnFAk))cm$De0e@E`*kL?>Nj zmGDvU41q@hu}ZO2tIkk=$uwLlm$6-)Ms-J132r)&=Io~-R>tVH8Y`(pgN|Lp2I3Nq z%J!1vR?{&lCXz(+*LzoU{VTcW&OduMmR^aa@5XvpV!d}_k1dZ+E!XPng7AzqBy9-7 zs5mUW6DB`4?^aiLwQs1wsIo2uJ>L#01vg73Keb$%coAxS2_OZK1sDM+Fwhiy5bP%a zjxnHq9Zxw_t|C{5DdMg2@i=TNWfN6OMT^Z6ze?`yoDg5}4z->VMUz~Mr^H$BySOav zsY0l1me|})x2{Rp;#}4}tQR4H7Q(!)5u&zbqmUQ_hsWVbGT3H{b?;i+SH70h;9dY& z1TX+9((qh72!?0g@<_J_ai?_WyT2&M;lBc@b#g7T<%0|z?E-z&P ztVMFUD-j1VxKY?eCA=T}_eFo`YD;g%F()gGYx7BS9iHzah`&S<-TmU1Ua|YEl&e6n zgwMgoCt%AD5Vso$6UEy)izJ!Jh<}mc%;o0e!1D7xP@m&syhOr1>Gsco0Rn%vz=txy zcDj>KTJ@Kc^T`)%3$d_~p0?;#81{g$MCN-k@e|B^Jz6mxPJ4jyO;2j<9}IqxIG5MB zsRB}8FkrWYci_KpNMG;pI1hw#(#FAo;XK0ZZ!ut7WpfJi27)MF0)Tn`FZo}4^9lZ8 z!cE2|09@ql*5j*WEIXRo6;Rf8>|779=Xy|xY3m`dWI>7l9tNw=yPdr*cHU&%s3-_d zgc%FMJvIJ*d_~PI`*Zj($nvpBtf5!-Ur#_Vz5&4Za?GM~AN!frD(DElMczEn-#YIT R2gMD6KmIrs62(J2_-}%^mx=%Y delta 1310 zcmb7DU2GIp6y7`Cold9hc4?vA+7@Q95oR;8bg{J(O_7G5q$xD6CBn9DhTXf(w7awQ z&J?<LzCt$HNPF(Lsry?Z^7^+x#YP*(O(Iqw|a=8}F%whq( zk%_)W;E%3U6bm!V5pgU7z1=|vm$C7;3L9IV<}zon|QmnGh1dZTjVEf0$%NMY34ROVPs8!V82Qb+k|+^! z20D#x zr1p_^xNKAIJlhw4@0kp-I3ic{SYtt|YaIAih~0jX9B5089T$OkPrqe!cq~oXNy>)H zcEOrrLO>*Ccpov;|3&{xD!o ToolPolicy | None: diff --git a/libs/agent_framework/src/agent_framework/mcp/tool_router.py b/libs/agent_framework/src/agent_framework/mcp/tool_router.py index b006ff3..00157a4 100644 --- a/libs/agent_framework/src/agent_framework/mcp/tool_router.py +++ b/libs/agent_framework/src/agent_framework/mcp/tool_router.py @@ -82,11 +82,13 @@ class MCPToolRouter: confirmation_required = explicit.require_confirmation required.extend(explicit.requires) source = "tool_policies.yaml" + execution = explicit.execution.model_dump() if explicit is not None else {"mode": "direct_tool", "workflow": None, "version": "active"} return { "operation_type": operation_type, "require_confirmation": confirmation_required, "requires": list(dict.fromkeys(required)), "policy_source": source, + "execution": execution, } def validate_execution_policy( diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc index c6700debc6835f116b7a6dcbbfd12e80581708b0..0a0f164baa0800634db396dbba889556818fbf9a 100644 GIT binary patch delta 26 gcmX@lv6X}SGcPX}0}#yB&)>*BmzmLe@?qv40ARQWZvX%Q delta 52 zcmdnWah`+wGcPX}0}urN%iYL5msvVgKR2&LKP9mwQ9n5+u_!aGGCn6izaTBMLVxl~ G<{kjFI}!T; diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc index 6604383631e35d6bc19ca565e57847804a22a416..b7442fc0e7f05270e463f01681f85fa4e8369d6b 100644 GIT binary patch delta 20 acmbO(JYAUkGcPX}0}#yB&)>-1!wCR2garct delta 20 acmbO(JYAUkGcPX}0}urN%iYM`!wCR5P6dVl diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc index 1dd618819e816be1f242312e38f9dc77baeb4e4d..e86f11020f4bb7dc4f6d1dda0d02add86c4cd5a3 100644 GIT binary patch delta 20 acmexs^4Em>GcPX}0}#yB&)>*hBn1FTDFz4t delta 20 acmexs^4Em>GcPX}0}urN%iYLbBn1FV^9G9m diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc index c4fc37e9bb7c00136c474a68fa9accf1686faf03..d3271117cdae6672cfe3c26fe2d4220144a6e119 100644 GIT binary patch delta 20 acmbQmJBye5GcPX}0}#yB&)>-1&jtWBAq4#Z delta 20 acmbQmJBye5GcPX}0}urN%iYM`&jtWD>ji)S diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc index 9490719148668e5ec237541d0b615500d3642167..77ac2c288afdd18b84d5449d6610ee3a0cebf369 100644 GIT binary patch delta 22 ccmX@{nDNA8M()qNyj%=GFjqf+Blq4y0AKtEpa1{> delta 22 ccmX@{nDNA8M()qNyj%=G5d1H9Blq4y0Aj`nBLDyZ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc index 39e17a48b34c8096e389984a0492c922704f815f..d6c0550070cb9471e582a82d65a11b54d1753fda 100644 GIT binary patch delta 27 hcmZp7eCxpdnU|M~0SM;m=Wpcx$H?fkS%oQC4ghk22de-8 delta 53 zcmaFs(C*0nnU|M~0SJQs*>l$p_S^K0hIIskc_2h=?K diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc index 57ecc6ecf91ff816c10f17f59eca14ea69aa21e0..12d798c6054bfd976f4954d341b79089c3847490 100644 GIT binary patch delta 27 hcmbQ>_05y}GcPX}0}#yB&)>)`%f#rg*_r9A5&&<02YLVi delta 53 zcmez7Il+tjGcPX}0}urN%iYK=%OoA8pPN^rpORRTsGppZSd^Jo8K0A%Uyznrp}$#^ H>8ug}&vg<8 diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc index 72e58001ca31e8f1b2f321465b84f54255a97b84..6dbf7ed5e3483562dc393f84b166363d1205d817 100644 GIT binary patch delta 24 ecmdnSIERt@GcPX}0}#yB&!5O`%xFC^G8OvbzX}5GcPX}0}#yB&)>-H!V3UCLvbzX}5GcPX}0}urN%iYNB!V3UF4h6sf diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc index 022d26f9decc9a2e976a4882556f385b4ed9e56d..b08fe680dff633b9878d5ac8599de2ccf82eac2c 100644 GIT binary patch delta 27 hcmew&a!Z8!GcPX}0}#yB&)>++%*<%DS(DkD9RO-j27&+p delta 53 zcmca5@+c%L)KEWCYLv delta 20 acmdnNwS$ZMGcPX}0}urN%iYMW%L)KHE(KEn diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc index da2417ebc9edcde378fbb974fa5538c804cfbe74..68ac915279ec4e633cee0b77d63e9a032d443738 100644 GIT binary patch delta 20 acmZ2rxxkY9GcPX}0}#yB&)>*BRR#b&R|S0l delta 20 acmZ2rxxkY9GcPX}0}urN%iYL5RR#b*AqD^d diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/control_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/control_events.cpython-313.pyc index e4e97c59d602c96eab28addb5c633a1e9fe8c357..3e8308d771c937951fd4b546592cceeb4b01a72e 100644 GIT binary patch delta 20 acmaFB|A3$SGcPX}0}#yB&)>*>l^p;;S_RYq delta 20 acmaFB|A3$SGcPX}0}urN%iYL*l^p;>BnDRi diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/decorators.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/decorators.cpython-313.pyc index dba7960cee24df0ca40e250f947faf7ae4cde21f..3e59d703fda139ad70570eec8e10c6559789f378 100644 GIT binary patch delta 20 acmdnbyPuc)GcPX}0}#yB&)>+sg$)2Y=>=>6 delta 20 acmdnbyPuc)GcPX}0}urN%iYMmg$)2bvjy(} diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc index fedd876db7d3ae9b27f3cf85792eb8bd1822e92c..cec5a48c4ff40cc762dbd8121a015d7325b3d8b5 100644 GIT binary patch delta 20 acmew+{Y{$tGcPX}0}#yB&)>-Xnil{?fCe-G delta 20 acmew+{Y{$tGcPX}0}urN%iYNRnil{_N(Q$8 diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/grl_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/grl_events.cpython-313.pyc index 88712d87f86d78b25773a94e26997325fa455fff..a7c7b45760b1a6fdf61a34661c19340c161d2a13 100644 GIT binary patch delta 20 acmdnWyp@^zGcPX}0}#yB&)>+siV*-e>;*Ld delta 20 acmdnWyp@^zGcPX}0}urN%iYMmiV*-hwgtEV diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc index 02071887f874a0f231df96dc5fd74dc0fe604534..02de7c73c8857ed95241698d6df1dd9b5503a786 100644 GIT binary patch delta 20 acmX@Xe}bR;GcPX}0}#yB&)>+smmL5+ngxsi delta 20 acmX@Xe}bR;GcPX}0}urN%iYMmmmL5+c!vp{~VFbPa delta 20 acmdnavYmzdGcPX}0}urN%iYMW!vp|2D+NIS diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/informational_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/informational_events.cpython-313.pyc index d3f75e8b0960ed11f0ef07c08ad2de0aad0980e7..bcdbacf700ef97722ba7fc39be1320c283da5779 100644 GIT binary patch delta 20 acmbQvJe`^QGcPX}0}#yB&)>-1!w3K~JOtYS delta 20 acmbQvJe`^QGcPX}0}urN%iYM`!w3L21_fRK diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc index 8cd2c1a9d5f7c46f1c9e7f2cf535f4a329f664ec..1609099f13f7e55909b1c86f1cdaaa68b5a0c723 100644 GIT binary patch delta 20 acmX@Xb%KlgGcPX}0}#yB&)>*x%L)KG)`Cky~O3Iyo@ delta 20 acmZ3fwo;AzGcPX}0}urN%iYK=Cky~Q)CFt+ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc index a4a7127416d7b73d2ece4b8bbfaba8b2c0f9d71d..1ae15bfbb77a5e07ef71e23830bed97c499923e3 100644 GIT binary patch delta 20 acmdllwqK0*x!VLgBG6e$w delta 20 acmdllwqK0-1D+~ZQh6OkP delta 20 acmbQCK0}@RGcPX}0}urN%iYM`D+~ZTPzAdH diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_events.cpython-313.pyc index 80354a5710074795c25c887d0ec769eadcd833b0..8de88ae5c43f302ae66541a6af6fc1b5e21fa26b 100644 GIT binary patch delta 20 acmZ3&vV?{EGcPX}0}#yB&)>)`$pip2X#|A; delta 20 acmZ3&vV?{EGcPX}0}urN%iYK=$pip5GX)3$ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc index dffadfd861e1adaa9e469101799d980841e6fc68..2b64d41c7e5bcbc333edb19a48671bc65ce5fd10 100644 GIT binary patch delta 20 acmexv@ZEs>GcPX}0}#yB&)>+MBLM(Mrv>Z) delta 20 acmexv@ZEs>GcPX}0}urN%iYMGBLM(PaRzSy diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc index 9624da4a2098b259a95b4099e36cd81016c2b201..f2a759b6ee633018afa8dce97edb714e44bfc9d9 100644 GIT binary patch delta 20 acmX@4eMp=8GcPX}0}#yB&)>+sT@(O6s0F|P delta 20 acmX@4eMp=8GcPX}0}urN%iYMmT@(O9at1>H diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc index 48974e1421ff74bd00fcb225160e1bea2c354d31..f3ae6deaffa8e3729fb166ef0e96ff37d804bbb3 100644 GIT binary patch delta 20 acmbO!HB*ZFGcPX}0}#yB&)>++#|r>8)C8LV delta 20 acmbO!HB*ZFGcPX}0}urN%iYM$#|r>Bo&_EN diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc index d7cecc050e2a0a7cf8f2f581c0fa1ea1e07f0790..33c3b7353f8e4d3b4100ab38158bfcd0df2cf888 100644 GIT binary patch delta 20 acmeys`+=AHGcPX}0}#yB&)>-Xlnnqvkp=$% delta 20 acmeys`+=AHGcPX}0}urN%iYNRlnnqyTLyvv diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_exporter.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_exporter.cpython-313.pyc index 567abe5a36c29f1300d46a2cfec741efc2f466ca..5fcb1d7acb582b3595ccadde0211437632facf15 100644 GIT binary patch delta 20 acmeC@>gVGA%*)Hg00eXO^EYy{umS)v`~+11 delta 20 acmeC@>gVGA%*)Hg00hDRayN3bumS)y#st^^ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc index cc795493bf066daf6d5939ba6e9793856665ba41..e9cbe6ac29f1da38a5ce969c75954f6dd2ba31eb 100644 GIT binary patch delta 22 ccmaFV#r&v?nfo&@FBbz4%+=4|$Q^zZ09I)S6aWAK delta 22 ccmaFV#r&v?nfo&@FBbz41pmw3$Q^zZ09i8!mjD0& diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/tim_backoffice_contract.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/tim_backoffice_contract.cpython-313.pyc index aced2c7f0ff8658fca99858dc2921ed77123dee0..34add0f485ade3f7aa5a2109dd4d94b4aafe2202 100644 GIT binary patch delta 20 acmX@XdxDqyGcPX}0}#yB&)>+smkj_sQ3Zzp delta 20 acmX@XdxDqyGcPX}0}urN%iYMmmkj_v8wLsh diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc index f21b4657cd9c1ee328effcee3c8352dd6982186e..760aea30e28ebfefe89a66a85d92f825a1d43c01 100644 GIT binary patch delta 20 acmccRbjykRGcPX}0}#yB&)>-HuLuA|um!>Z delta 20 acmccRbjykRGcPX}0}urN%iYNBuLuB0dIm)R diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc index ef207e373a345b0610b4c9d51464187e91e683d4..258d9ab4c3023261762ddfd4f533cd069fd5865e 100644 GIT binary patch delta 20 acmX>ud0dkFGcPX}0}#yB&)>+shX(*X-vy!o delta 20 acmX>ud0dkFGcPX}0}urN%iYMmhX(*asRktg diff --git a/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc index 02ab3cda5fb831bb5f8a1c072d6a6926fbea072e..103017253a0bf0d4e62a75902e93c91cb297ccb4 100644 GIT binary patch delta 19 ZcmbQqIFph4GcPX}0}#yB&!5QM2LLVy1iSzM delta 19 ZcmbQqIFph4GcPX}0}urN%bm#G2LLcm1wQ}) diff --git a/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc b/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc index 2b1c3f3e4715503a650ee1a62a2d96f255c4f88c..6e1dabf39a815831aab9f30b6f17b157e2f8a362 100644 GIT binary patch delta 20 acmew;@lk^NGcPX}0}#yB&)>+M#sdIDO9h?) delta 20 acmew;@lk^NGcPX}0}urN%iYMG#sdIG6$T*y diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc index f4b9c045cc59969b90ccce3cf261e6a7943cd7ed..2fbd9dfe2a5fcee2a1bc30be2dacd041031604a9 100644 GIT binary patch delta 24 ecmdnZxR8qyPW_ diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/mongodb_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/mongodb_store.cpython-313.pyc index aaf4cd435bd42955b9d60604f4b0ec6b9bedf49a..1f4357f02685267d61423287cc0d45a84d57d0f9 100644 GIT binary patch delta 20 acmX?Vebk!!GcPX}0}#yB&)>+sOBMh^Xa(^A delta 20 acmX?Vebk!!GcPX}0}urN%iYMmOBMh{G6r-2 diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc index 2345191e5ccc55cd11c35e6c1cb955c9a415d85a..3ba9e42d13716e47925ed9bb77b9cc6a76af7f59 100644 GIT binary patch delta 22 ccmaEOkLlq(ChpI?yj%=GFjqf+BX`((0AkMvp#T5? delta 22 ccmaEOkLlq(ChpI?yj%=G5d1H9BX`((0A-m7Bme*a diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc index c65d58d5b580ef583bf263a2a01508d91278f272..e200c5cd30c3a9774991f930239d7ce533263fe1 100644 GIT binary patch delta 26 gcmeAw{ZPXFnU|M~0SM;m=WparVPte!+{I`K0C6k_jsO4v delta 52 zcmexR(pAd+nU|M~0SJQsFG$O*&|h4@ GXbAw-pAs4X diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc index 73cf2911ecdb2665ee769b7df635cd66271a4e4a..2f70874cd4d7c9fd3dd472cd3ca78d67cee385b8 100644 GIT binary patch delta 20 acmeBX?Plfv%*)Hg00eXO^EYxgFaZEDr3AbH delta 20 acmeBX?Plfv%*)Hg00hDRayN1}FaZEGZv{U9 diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc index 168db19eee4bc4286db47a95dd4ae6bfd88e0f50..616fe07e40cc98f4c5a309b0427bfb32508d8de9 100644 GIT binary patch delta 20 acmX?Nbi|1JGcPX}0}#yB&)>*xDG2~S_yt%1 delta 20 acmX?Nbi|1JGcPX}0}urN%iYLrDG2~V!Ufv^ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc index b4a986f441d3cbda73bd7311297efb1e7b8524a8..78e3a3abf7fc96543fdbb2f5c68ef371b6b98589 100644 GIT binary patch delta 20 acmcbsbyth~GcPX}0}#yB&)>)$BnkjRyakT{ delta 20 acmcbsbyth~GcPX}0}urN%iYKwBnkjUh6WM< diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc index 86953f058841d933f248129344efacf4474c2294..b056e8c6eacfc8bc6ea896bee3d038846b5cd691 100644 GIT binary patch delta 20 acmaEr_$rb6GcPX}0}#yB&)>*>-v9tjn+CrC delta 20 acmaEr_$rb6GcPX}0}urN%iYL*-v9tmWd}k4 diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc index 48a2b9b90835c496843ee975f6180f1e239fe24b..cb2b2befc10c58f079ffb9d8ea2f31713e44ea76 100644 GIT binary patch delta 20 acmZqoYxm>+%*)Hg00eXO^EYz;Q3C)xZ3X54 delta 20 acmZqoYxm>+%*)Hg00hDRayN4SQ3C)!HwI|{ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc index f31138aca6d5e5dd50a618714ebed18d32389489..40da196c2ecf4a79e205a1f5f538d60d1eccbe2b 100644 GIT binary patch delta 22 ccmZ2Fk#XrnM()qNyj%=GFjqf+Blj#108MlTY5)KL delta 22 ccmZ2Fk#XrnM()qNyj%=G5d1H9Blj#108l;#?EnA( diff --git a/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc index 04c7ea196ce73dc59c3973bd5f0270643e9dc706..ab5a21f735ed73e38bbaa4d39ee9416088f89cdd 100644 GIT binary patch delta 19 ZcmZ3)xQLPaGcPX}0}#yB&!5OW9RM#>1lIrn delta 19 ZcmZ3)xQLPaGcPX}0}urN%bmzQ9RM+#1zG?A diff --git a/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc index 39e77e82346a15a89a15ae98949c85f319d21982..ae7bc63c945035b30d5b812bcbf55d510d30aa19 100644 GIT binary patch delta 20 acmccYblHjfGcPX}0}#yB&)>-Hp$Gs(m<6o> delta 20 acmccYblHjfGcPX}0}urN%iYNBp$Gs+Vg@h( diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc index 2ef22ccde6764887b3f923ed27ad21456b51c432..17174f66bd4186bf3ba1bfe68c433fcb78d88114 100644 GIT binary patch delta 25 fcmeBVe$T}HnU|M~0SM;l=TGGR%4joLnlTRmSNjIX delta 51 zcmaFQ)XB{KnU|M~0SJQs-1&&cSwc`KtbI{<3<2RHx# delta 53 zcmdldcteo;GcPX}0}urN%iYM`&nO+OpPN^rpORRTsGppZSd^Jo8K0A%Uyznrp}%=H HqcS@H%bgL! diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc index 458330d339429d727b262dd3b7b7ad5813361642..1655f4900bea2d5a56a9af40cf74e49bb7d9a079 100644 GIT binary patch delta 27 hcmbQ1{wIz5GcPX}0}#y9&)>)$&ctZ9xt2-N5CC`k2aW&$ delta 53 zcmey9HZ7g|GcPX}0}urN%iYKw&Lkb7pPN^rpORRTsGppZSd^Jo8K0A%Uyznrp}#qq HNzxDi-1#lh&hc>~8UJpgs}2%Z1{ delta 53 zcmbP`zbc>mGcPX}0}%Y4nYWR-iIH diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc index 03ada8388b8c7cdeae4daf5a50008c84c5629b93..e6d1791ce454f23d8908c4a65a636098ed53ec9b 100644 GIT binary patch delta 27 hcmaDSbVZ2!GcPX}0}#y9&)>+c#>{B7*@sz)4FGC329E#$ delta 53 zcmca2^iGKTGcPX}0}urN%iYMW#w;D8pPN^rpORRTsGppZSd^Jo8K0A%Uyznrp}*OP HS&0n*#9I*> diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc index 29da32fc1d1b204f1fb0fe55bae63ee193f444ba..e1a0424598791213e466c4b32a970a1435f3a499 100644 GIT binary patch delta 24 ecmdnPw1|oOGcPX}0}#yB&!5P>meFS7+crwRZ;tOaub delta 20 acmdn#wbP6HGcPX}0}urN%iYMWrwRZ>b_MnT diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc index 32f0587a12565b5a8517003102db6d226074676a..c1bda3d060de5a93b8b8fd2debf4b8fc81859df7 100644 GIT binary patch delta 19 ZcmZ3$xPX!SGcPX}0}#y9&!5OW6#y>~1kwNi delta 19 ZcmZ3$xPX!SGcPX}0}urN%bmzQ6#y|&1yle4 diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/router_supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/router_supervisor.cpython-313.pyc index ab3c2068c824fcf1289c2db7c64e4098bc123656..45add6eb765c96dc8b76cda2e1d8db6b44895a38 100644 GIT binary patch delta 20 acmaFJ^pJ`BGcPX}0}#y9&)>)$#s~mCuLU>& delta 20 acmaFJ^pJ`BGcPX}0}urN%iYKw#s~mFas|2o diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc index f272d36c3ec91c6db2d2973f13bc3df48f80268c..b928242b6f62a3d8ff1e5416fdd616727c325c52 100644 GIT binary patch delta 20 acmbQHGEIg1GcPX}0}#y9&)>++Ed&5I&;+ai delta 20 acmbQHGEIg1GcPX}0}urN%iYM$Ed&5LlLamS diff --git a/libs/agent_framework/src/agent_framework/workflows/__init__.py b/libs/agent_framework/src/agent_framework/workflows/__init__.py new file mode 100644 index 0000000..604ff3c --- /dev/null +++ b/libs/agent_framework/src/agent_framework/workflows/__init__.py @@ -0,0 +1,11 @@ +from .models import WorkflowDefinition, WorkflowEdge, WorkflowNode, WorkflowRunResult +from .registry import DEFAULT_WORKFLOW_ACTIONS, WorkflowActionRegistry, workflow_action +from .repository import FileWorkflowRepository +from .runtime import WorkflowRuntime +from .tool_executor import WorkflowToolExecutor + +__all__ = [ + "WorkflowDefinition", "WorkflowEdge", "WorkflowNode", "WorkflowRunResult", + "WorkflowActionRegistry", "DEFAULT_WORKFLOW_ACTIONS", "workflow_action", + "FileWorkflowRepository", "WorkflowRuntime", "WorkflowToolExecutor", +] diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..536364c24b748c267a0cecfdbcf67769d597ca23 GIT binary patch literal 688 zcmZuu!D`z;5Z#qzNs;ZiEy3iNLMiAH{Xi+0#D=VbO=TNh3X5v5joP)=j3itid+s?O z(a$NpEtnj7>aE1LoVu%6DWsjl+nqPFZ)SG(?Y4)M-G5zwd~G1~Q#-XOzq0*%b=;sa z8siWV444>T5ED#dfpw;NBWw^GY~p}JTyQJS43CHhUUj#^CTT&dx;Mf$Ifmmi6ri#F z4nt?@`0g*PO8Az15exXV;9_&h7HO6ismNrj=KAv``|AzGoXO{PEGn~zA*vGm9}gbsxitAomHizLutY}rQgtADIm#$RaOM^ zq)38fv7!1)f@I0Ef-WFgG3`?^O9P&M$e#v+9A^J;`i0(Q9#E>6N$L4|E8RD0zLK-I r&{b9S-t(6Vew!HMTQvQFrne~mj^Z6U-=SAK6x>->)A-`RiOTu|8g|CB literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08349f9a1c0789bcf9a36251ec937af44423493e GIT binary patch literal 3636 zcmaJDO>Y~=b(TB){{E0G+mcOvS!Ljgb!@|uoVu{S9LqKxYpoQ86fD-{lHP>cm1mch zr8IzUWsm}Pkr)-w9^71NpK2f%`; z#cS>v5A)1;nRmv=d^3LLPm?avM^ za33~Y3T47uC@v$|K}FXMOR+RVH#06L?rBXe7wqUFQsnzeSt}@(!Sc=>9S91+A#5{%kI4dIf<%~05|}th7AQQ;C5fW~ zb4zr=Be{owYaiCjv7Q!o!6$iv*Dv{i*WbcQ0r&-_(5T1)QWz)^q)1T~EX6W$JM>e7 zeN-$P%Q?8_mKR7{0)S_N9az+Kxvc6X>#i-D7PDPi!S*P5R0EKs1+}PD%9dONl)+a1 z8!5o6=YjEb8Wi+_?bh_Aip5+g5?vr8+TP|jwyUJtuCi*2nhuI$s>N2Gkz6KZ`(#;H z7FAic1F{U7tCVpalI439rQD=MWx1#^(<*DasvEM*d?2000oXv=XDc8j5KHl9tP{Dq zao-`!CYm!ZE0)EyI~7Ycfwu=}+wdoLKD($}SumH9Rf@~r4lvEua%iwmnmyNuZ zEo*no!-TBK@^9eq3skb1<=$WpFRid3oWubt-5dw-3$htWuHUOgde_FQVx9JGCXPHg zQ%el4O;;!Cba1n~_xV7r`@-5(^-`T)*o=3s-+OZF>D#sVFrd!9qQh*kCG>!s?Z%l7 zk@ku`JNQn10@qUgEa5oBqQ-y#l%y$tMl%lYU@m-vSO2JHG z&&8$+C3PzTSGWy96J39jljt|J=I7ZS$nsde?^Zt@=^2wt5QI~5VorT zzt5^~Bo7ay3AC=KY2Cd<5)v{K5XTGVt6^TieqJ~S;u#W)tF7|$7j9YqYcllQ*LOklU&5TDFiO_Vj$ zns1523B4KWpzxfL0%2HX&D+R->prhu#`6>-go!68y%<2Ph zP^b&)Sb8;elW8S&@gVjRdX6nl`u7EEyFp8%|pzxEIPe(cM!dSv*^vGsEgmw&nZyRoXc5gC3R zj8x6fM!qnAdv@LY&B*7U)Pn=B6Wtpb8lI{Ald&#rC!3Zjtjo6ic`bo#h?*b&2 zss{h_uOlRyHBmheJ0{+tJdx+1@OYpbsW5#tHT0%xhBBeHrGqyh7eJI#Cq{6^jfFAI zt|DkxEHHO61ehJoTL7R~#5$hDYq9>d$!dR{_HQP;)-V6+;@Xv}RHrAP;y>xF#Rst3 z*Xckb*i)xH4*Z)twLVmH_iejE?$owOJpFv-lny+jed%}rl*6S%axG9vd z;+-ax4caUl?mhyA!Ox3H0IjJI?=ThV7# z6WYlw?%J3i?2!6E%~!B|$k2I)mzzBlZx)<-5!=PxcM8YqiDp|qKQwXk#(USNZcj}P zolXx;U%q-BRv$nY0gYe9WJx%Hb>Jx;qhdjy%1e%==2ipH!M6Gcx60!VLm(4uYxzEu z!_D81D+$4E1ak=58T|$l5a2_bZC@kNQKub^WM`drHad^i>Cr~$tvY>cv#0O*xmwQ% zGzYml9XZthHM+XN5yoE8ZpX?fG?$5B=Ul9S4@2k7pxD6^t(7HBFEMNw7tuY#Lxt?4(H#QQPQ)#A;Too$SV~B(AKf7^?F@T`o^}7f2~7MY{|9wAP$mEX literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a96f3a262cc226190ee320226465aadef1e224c GIT binary patch literal 2342 zcmZ`)Pi)gx7=O=p5+_ZXl0usnN<$aeg4PBG?Sauq6%0WOL5iD|i3sC5PL|Ht<=Ii! zX@_oy2{cWNLp7#digwsKO;eBCtr8b*bJs~!7D$tJn7A<&3ASDKeRk3X*1p5@_ulvC zec$i<Fi8VAmn#EG*`$c=;rr8EE0)Gbb^diMpH<06Wln@_;G;=-kF~W zj0agTMM^>`R1!xqY`19L>VtdB(9gtK-3a12}4wBlLC02;aEV5Ny0rHk*GOhG$gT;sJ&XPSDMS=fDwCB>EP+N|tGU znYyB^cxEgJL9jHvVxftYt7WelcDAh8N>-`N$#d{#R;g-+EmxRwO~bQnstN zWG$xt?+!K4Yb(nd9uLjV!}D^@sM)gY_HQ57P*VU)%)%HK$i}hZ8^UL?O$_D?_#I-} ztgfh<+JKy32vCTA^zQ<(NG^30;QR7=6E2iJgcSML8mw|VA(xwWpjeBvogCwa8@Oup zQffOpMfxh}S4oD;*Smr8Qx3%{BS^twaTE}JLmJx=sipuvVGY6tNVUbk7`;8Z-j@2I`(a|A(?7CeuT-4A z(e*^miRL!;r*Fk>#y&6qBJO2aC2FHhfFuOF$*Z3-5Q}6#)X(;@qhOjGAeXnaLqyq4 zh{5fQWRcT22_(k23ayY)zMoVmqL&6+4fM)ds#!EC7O+tb3FwyJu zjjg6vyPUme*AwUNTTZm_&l4Vbe_I`3;^X#Re-%H>e;3H|`4?bCw>(()0da*~ zp)eJ>A}zwyk|=b$I3pNbkxP>dD7N;q0P;{6bQ80X;}*#zS7_zKEv`vAO1G*x3J_r4 zwld?(;9exnp@$+S2HOm#lu+oJVBJ@}{9>#75o0B=#Qh-0|Aj6{by7_heZH^#mWv z!$AAD=y?--tlkeIU?ufo$}=2h2jI$$$nrI_T-PBZZd8`9*A?B*j~kceN{v~zUNbbq zlx2n$=p~W$A?1aD&+pIQSw>1kyacKDw;fP^kbHg!fA$p&XryLE8K=gSk z(PxAY9DEuP0zgKEK+m59HeVlA^O<(^vr>nAfS(MJcjKz5>zedPVz@zj zezs;*Sr89Gxu)981I5Dw4Z6wGZ=D${o}ZMbE*2)voWD3FkDZ!4cQG&dQN`yP|CGi* z?4@{I@uZJgj*WmdY!&M|Y|yHdJ|Y8uknAJU_1mtVJI9y!C22|gGWYHA6@Eop5tnmL z?};BzuXe7gtI418_up}*E;}XJQOZuKyf&q+U8t;`tT?@uwO!SP_GiIP3Si*&I@$Ne X`rcy>Np2(#KS&&1OC0?u?m_e~sb(C= literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60b2dd28df51b3429ac5405e01b2af8dbc2289d7 GIT binary patch literal 2695 zcmb7GO>ERw5Pn|U`x~;&lBG##hzU{BRh!+401A{q+fYD>2vN+2RzVoE_AZ8v9iF`# z0!47Dl2%nzRVmx{K&tithe{k#PmyxrR;PPN>V{Lrfm_ueA@$T5dv`+@3hGE6k7wr1 zdvD%+^SrTTOPB!t^7+(7g(u_>{HPM=3CzYQ2n$3cA~Q^e7|IAJ`-Xi(EM;Ap9p;93 z%DXZ*?5BQ_7yZM5p&$(kq=j@6G0;ZD;9j2>k%n?yBJ>zP5{wg(b=|OJOEYv6KDklZ zx&UwXeSK!+G2{8fP8;-6Hg8NHQL~z^p>M{EUqnsEN z**0>Kqr4arIal(FVUc&Gfas^eTp$r~wjI^-YIUYDb;>X`%b+ulF-3(jV1qKHYEBl; z9n|GX^-xmISXxm{&d8JbbAnn^7e^{8Q=Js#j4_p$sh}8>%RlLwA;^Z1r7(ONwv^0N zR_<!%FxZ&Q$YR+5jpOLkohF zOHJxlN|7x&C1$c z-UuPOFYOz{G}h7H%J-XPcip zZh!5l2AJAxa1sJ}hv;mY9m8$ck;qqP*13sj)tKfcUaQJg8=fuTf&h-y;uippMMKb+e=r19<KzWyLrk{LL4vY;XTv#tEWY z(e#`XD5}&1Ko;5{zzLaVz>`)2XnJw^`@Duj6A`y69lv@-GcD6;!WtPdtfNLjS3acF zpbm#Xazdt@Ri(TkD^7^2vLadP70Y3Bs^x?=Q`1dL)-$TZYC0&!<$OW)Mn}Lys;ni! zQ_Qo&Ax<6tq)acV)M2SSO}ByG#G@CGC?t|NVI1m#+1dFYwY%wpr(%8$>?T=jir;Ge zvUQOw6&KIg?Ymc+cF+3PqVf6bbJyo@%-vXz?XtU%E)|z9*j*>fu~EBWbbUwXZT(yQ z&iHEYz)J7H(zM+>VE3LZ?-*U(F>aq4pJmr#@moD#_LLIkSl4V|J=$98`LS;`+Orbv zS&zM8ckI94eZR$S|DYTjv>OK3qFYy^9V^j}MXnr8+QB4kf~94i1wjo)~b!R;VIS;fRwAjicj-nH85>DrkMlN zgeYJEdu&r*mC`=6%Luj_oB(Wf`%Cj4oP5dRRo?CVE);55dJaVtkxQF_6_UV4j7c?x zg^OrY3<2S$F8>UY0=Emzsi>6cqDY$j%E*u(GYIm%OmE+>X0}2(;q4OmOso)P+V}1t=AQIg= zKQlK|QcB~CyBBkdAKy{!?R!?DduBN_U7NdxU~z%+-cs|D-|jkAjvcofj;}Yy=Jn6? z(y7~Lzd3vFbm^?!K2UBvwAy%NsdJWHkN2#`_x&2*ch4-x-<=JwMdI^^=MI;Ya-?(d z;%ayQN_YSLW99Ba7cwUvC=bT%_EY6p+HOd%MO*D)tBaEa<8~#yMzNZ%hE0nKp9t|Z7cv2u6PG0Dd6t%uWXq!3`GTdIl60528*m#K z4WTnK{8cfrvOXohac7=z4Bzp@ALLU{n}d9tHvrudVO&_Y0DFRkQ3SaYz!HF;0Plao zZ_P|Wcc<7X-3o&)iX~;bTl6OELmPG*uZso`dOJNt`#}e-hM1U`KUs!h9+J*Sq~#&m U_B)9@BHd5@Elk^A1eiPiA4Gv}g#Z8m literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..206eb3eaa011834f54a803f38bdd8b9a532fc949 GIT binary patch literal 9220 zcmb7KeQX<7a(}zrC6^x}DT~ynEv;qA7HvzG9V?3C??`fNS(Y8rwAIK7y(U*OZF))X zlKyJZ7&)L&4k;odNv&LioZL%c`HCWPaX^w5Xq(dkch|qj&<&Zbk_&o=7U>^_9Q&HP z>jCY|TP`WnO7Hq?zMc2|_Pv?;&CKdSO^t&<8htf&ZnS}rpJTyHoIx-Tz5~RYBt$~Y zVKU69jEFRQm{nPLS`J%=IhCVz++lv$s#+<{AGQq(ssOY#WIJphcBqbFr|P6-;jnAC zMy(lkt8QAhANCA;RWHzPsvH7~Lr;N-`~_<4Hvug|ZNnin*M}vnCj2t}~**|dn(9zM5UT=2aAI0H8 zc`~l0Ra~n6Ks+IvtwA}J)Z*!+dNo?wI(-2iatk5MgZ}{HO)`%AqA5(q%l*uk?JBiN z^@M~;5XV-f5Ytzh!R#A@S;C})gc*em!=;4?M}oGhVNeWN%#n5E;3h&8%d)+&Ow@7wPfOwthT_6HSp77 zkXPXBc5?SX<*%sIt5?mf>P&j(zgYj>_|chOTfyOE1w_ zV4PY)GH|Dc+UfDx8485wt(iHIA`Iyrnf*#R7~E%&)N`F;<)~XlR)rcbL(DO@+=xg?$rx%c>73osXyG3z}{N z22f(Ms&fgD=>VrTVmHu|z_aId=2Dr{)MjiD%?!I~-+~NS#a27r`F35wwkBs=v*>A< z;q#u(tk7xH`wF(!oUOIE^~v{J?za43%{|ZFzgcs~H&;8&ywN|iHDA~9vA5&ynvXqu zv%=m-hJ3oIH}Bc{{k~c04ciR!iFfUtzMN-kR@ho}dasZC`hksj_c6czSI<@r5A`E^ z<1>xckM{VVY2_Z_DimwVRsRLVn{ZaMhfMkOK>~LZ;vZ?@Y0D&Sv6fm^!R>?M8ezuEiVIYk6?>X%jo?hS zM`p5NJFW>_R_5T45GHQ&1Z$3ynSg!txC#oD=tQ^57c){q(;a{!krHi{ufXMNIvZCq zs0Rj;N-Vx`d*ORYQNc{hq>^e{ib+q40p8#t%1wTpk~@=3CUnOM)J{XHnp6W8-5$jm z5GxE;vP#i3u8JoNT8r`$=$;EwI(k;t)E=mAho6QZe4Tvm^ns39tnt4Qoe6xm`>pOd z>pYupej;DLeO}Jj?7DuSX!A_($lF#IJ^tCJa!s3ap3PZdbJ12eeKl_rv36IksW<28 z%?iEczTWzoSk~MAZhG#}{Mp>*z4;CMvYvfeVPDbaoPu$UIBxq}Z8=X@R_I!E__7V# z=6B3*%KG=@9ecCfUKK=?MB!IXoIx=UUIgMza*C)-h=9ewsFo0`av=--$MslRN%g;w zBg7-(pJP=%7Dt zVoXlU>V>$n@Yh;89!-jX3k!EtpnAlk;)tY7KC4Qpv*N-X>?tMA%3@rB5lT{wCB-s+ z^o+tC+ThO6$k1JIN73`CB<-o&({e(-AOrlDr3`NsOt(j( z$qOm4PhtUH=QKGnp+5O|Hw|Y7o~&?X1g)e@X;9uppQq_NhuxOHJ8FtGBZXKXxowhL%bzRYx8Qa4WQisUqrt16HgsGr&!C0=NU& zRBT~u5Cy&}g>6AlTh(bx8DfK|AF5K=W~wET9O*4tOV%I)PF1SF??C`mMFK0;D(8-8RTemSCs;kO z!a6Ij&Q?}G@*0>{o6M16!F!Odo`%Co$KegDrLm~$d3RHcxe zax_R%m_6z)uf9UhWpD8nyaj4XnwGtVqlW=kj5=WFSfwW93bVb8;tsoms6(n!SOC+^ zqp>9;5KbleKfx8&g&l%ffgZyVFtfs*mHMn)3x;FKd(c?LW1h8$w`YpNvOTTLJ7b^s zl~kw*e0?NYk_daSf7#Fy3G(pN@RL)=Cr&?yR6F?cHeS(uZ-7*e?X+ypa0c3axW1Vas6IGG6&@Q$i*!06mshDHZ< zM+hAxifow})F%MvCMnKQ>qXYm3%3C0z}ZSjvE)aDNIj|U zG7^g5ZAs!;=}a7)Gf5T8PE-_bxtx#&9mPCAwSy*Ru-sH3I!eF=E_+IiC)Icw9cV_@ zaI|R>?Mg@&&cviW`o_n=rvzp_yAsJLm{NOyF#4hapdnaXC$rDpe*Q0>r*MPefb;TI zI3qryBx5pI`UaqvcaBGhxA8je{6AqA>xc-uuNcI@A=sxXh0FR~u|SV9Ojo*v7+_?$ zJ-9)>a=T&9ZX$srM-6n0WWR8vjqKuc3EyTk6tbk~4h#Wjo_VQ(Byo_sb7I=G*6k+Vj zN+=fMdduepv!mNom8fj2*aG^Er>z3=eusf>Jepk%L}tRg%m`%kGRRaJ?LemBy#ed& z3f~52qS*J_k9+{~NvVn~^;;&GfOS+wU*nq1i0X*f>OdSW$)YNQC8+?j(@+GD%!5W? zMg>nXO?fHcRz(;IKBSV$q;(!fs8`_uatC|{l9rMoh~bQD(q#$6=1f91Tu{)6V3r-f znlc;(9o+)-5s-%<7$T=*@`MDzC~!n#x_#(MR8C=gz(KtTbr8C#L)h1fVhP@>Af>^J zO@Zs;NJtv^Gw6TlOhmUrSOX`rWl}NF4Kdv!Ux{jHJE5g#B*P1_$<%h1^}z3zxKh^> zFpc(akbMavx7Z}k`fs<~YMGPsP1~of#TN0qJKx$lSNqo9X`#5~iD}1TQ|ru?ciN`+ zFR@O4;M28TbM^VP{WI3Zmi9tR&&Mr2bC(J&Pv%;lENC z{fq6J<|gOm_Y-#$^DkxxPGmQq%(staSC1{WZ=CD<-j*3#ad5cMxG~qbajt*9}2m~{l#tl?|0qpnz7BgbB$YyZR@||zV9c^fu&~B*q&?X&U(AE4c*0hG3ynf zVg0sWJ#>@Roev1(4-`AQ-f{f*k_E`$YWv|;Uw(3+kNL1`poRTtEi=$${iuEaCMYb_ zxS)Qai9x!>F|dta=wt`F`GpNutncQrW}5|Q%4>g*`b0$l0d*7s%^Qh;p0Y;d*rcqL z6%&dSS`}2Kz6%7XR0fpBK45KjnR;UXZ}n6ba#-|;2x@{|Sy@q&0$4D_8e+C`zlsP} zR9XeFz{loB)l;xn^##Z>=xB_}%Gt~XNlb%WXeDSC8T_L5jtw z&QqT&7jahIl8_bkH1?CQflp^r37Fl`H@Y8(m4hHs!r*Y-13C`a9<*hI`qL2g#R#x& zD}lRlShXq|X1W8kVJTo=d3p5@aLEB|7vPpB!7HCsqgV${3`o_POgurud8oGOE2z(7 zYmBxU+@z`du*B1I)e!y+g}5v?BQ&Lbt>6onTAqQ?+FFn>@P%34-taS5|B{1vn_qkB zm6v861#w$W+?I7eF=Z(_-8Xw~^yHmlv996fo*R2+*X8QgXI<-yflX6F(brn=b=>oH zObr%YzJjYQ=W3hfZaZ%|=Yn}xZ^6}{bM=3~ed5}E-$MMIU-;MDjNFKP;$J^CRCLw8 zHvGzPu4T*AFm#3X`VIH3q;AdI$MdV&r)LN9;`0AW%^~~|_o_y=%?BqRHPtmvf=E#kaS>Nr3TMcvVd0+R` z(C2lHMQ=mFE9SgnsbSqG-k!x;-*ou(=9!DHx6S$IF5YQ;Z_Rsasyfd5=P%xEe825( zTXyh7e(Q-l>$0b$?3rlpRIG67eD2iwPrQj=?A-T{{=eJxHQ{O{2F`PJ=WI0Fym8tF zb78Fg!=ZfJ*}ol{zxd;^kH)g$$=uLnUfedz+!k&Lb8Fx0y3_Tsxb4HCT-(`u-Z)*q zJLm15i@i5_XL3GM@E*8-n1N$l8exdtTM*iELK|LIZ%*vJ2k}){#B;+lcf8QOC)d4a z+LQI}o#KnF5Z!;x{fc{9%Ly$-k8jHLtA|O3c-K837JI|us?MVK(gO=y=l-Iueu-ym z8UR0_;kREwpjC`B+B#sBmxV)p}+r*YeZ7u3@L;pPW3P(aHX!AFW zLL{n65cM_9K0eGXi*5m8L39hPDw--WS<{0nkF$oYm9Gcka;&x&Otlcx!znf}j!Cd= z#F{h0yYc26K?v+rr?3q^e_6uTUZ$4`aXZ+@3}c~U?v6=eekF4^#4j^&h2RRInUK}o z!?JgWyAUCiWVV3uYTIG$fJoQqE-+EchA)_?8_`7lJ@z3~PBLsS&kOO;9afDs&0C0x zuVmahMgd%2hT~BY*$mZw5GXKYlgdPVGNYnd3en87sw~BT!_W>fnsocX(Idx3hK>&n z(s2PRok*`Skvl@6##=6yFb5z8{LqZ1TR|~{T}-h}w;tGkXk=)xG>JNeyLkn(Z{u!w zYO&Iuff^c~)SZC$5pzlh7SKwD3RKi8I&9QlR?2D=?1_EN%tWQ3U&?PX4a&xg9N+P1*jKgKb8 zKr+xS{(N9~`Zy}-g1z$sLOnj-AULJC{A5%pXflIg0M)f_vlJ z(RVw)x8ctd02S-j|Df@M4f(Cl{3Mu_Dccgu*-x{@=2rSWzyV(a3e7un%{xDM z_9ri9UyNtZE7=#4*^yMXd1toyV!?NDYG|>pV;*$tMB$W@JEbfUa)lXS9}+Uc9%G^O z9D9QO8XwQI_l*Zsjs4WIBhv zKz$=msRCrM0`i@~xi^qPOOJBjH3xi7Fy}!|yape`RkH%8qbwb;!)GEYZxE1oL?Rcc zY6fp_-@mwb|V0*(!s)&*5 z2xK~ke^g?)=GOGp6xx}%s?OnGmry;VTj4t7ghub0UY9PYrN0d^zQ`a_FJm%*=F%r* zba9N6eg`QG%$H2gUB(sfQU4O^K$j2={WA~^hWU)N|D3o#BTb)?diekEWXI3QfzL?K cL#x2lKCHJv=5sOon6F#I41;G&A9V8n1zk~_KmY&$ literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9428e453374c10bc13bf06efb047fd1db5695368 GIT binary patch literal 1908 zcmZ`)&2Jk;6rc5ec>NWpi5-7brdzivHAG2+R7$HRfm8^QNCstVQ52%7yI#9n?A+beRQfN+HA(zN6cYQ-A+U z(`b4Q+gHI=a}wCcj_sM6X?x7n?s}j?mz=s~Sf)F+T5i@# zT+cF`F{2-7H*Ult)^8BNI=M4g?`|WjK{1Mt1wOdaPnz~E76M#y&3ctZLCZx8n#OO{ zt$XfxxduVeDl0ckJT>kz<9~a;Id{Apx!KQdY-yFNlv=jsQOa|t6=2L4N6;}=;~Ub} zuKcd%4geDayMzP@5tM$1i9Lkf3q*tD7JNA=iSwv)f^^|(Ut?0Z+?Q1#J&jx;zQmSF z(y3fPI3photgVz3UAb`wyg+<9`6#>~`^l6P z-?kkrk$J`6Kl;vxjmI9~d7W6IZ>UHTu@OK}ow=ArSML9}rwWQ8tLr&1u z{`w-cqL{cQR)s>S$V0t$!`O1&Qn8q6cob)WtNv{8;IheF)KbcZXDypN>5J$~ z3)HF<0xSi7c=Xt&!ljC3cwDTRz$~~XIH_DJ!vOOzx~eWWv=XH-rk;RC9bW7+L6SMliyE1 z8rjOVBIDaPT9L{2aH^d?zZu(zZ9QnEC-%})+n=t*+PO=ciyMnSU*Epp%1!U6*>-kx zTWw|E`RU@X#?H*`)>LUN*iN0@OO5WPM)y(^yQztGG}DeI+ljO7Onx)Bk=uEFs+F1U zgh=|W4w2L0Ln04_*OX3_436x@wcWVZisyIK{NFLyf8h>6^xw{0z9k4x-wFA9M#}8USxULWChfLdV5V~f0aD0p?$3t z9osCZ_@rRR;7JKn3axL}5l1QgqFJu@C2>lt7IVG2Wt+AG;vl6J$DowO(a|Mzl%&+f zKLvx9J&#%Unx5%W`k1gYsDj;L_~djM0{0rQhvX0GgXfYUPjr-F`9en_iBu%!uI-(#5f0Fz&GV+XEJXA8mg?|XJ?#cfE7y!{< literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/workflows/models.py b/libs/agent_framework/src/agent_framework/workflows/models.py new file mode 100644 index 0000000..fad7d52 --- /dev/null +++ b/libs/agent_framework/src/agent_framework/workflows/models.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import Any, Literal +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class WorkflowNode(BaseModel): + id: str = Field(min_length=1) + action: str = Field(min_length=1) + input: dict[str, Any] = Field(default_factory=dict) + retry: int = Field(default=0, ge=0, le=10) + + +class WorkflowEdge(BaseModel): + model_config = ConfigDict(populate_by_name=True) + source: str = Field(alias="from", min_length=1) + target: str = Field(alias="to", min_length=1) + when: dict[str, Any] | None = None + priority: int = 100 + + +class WorkflowDefinition(BaseModel): + name: str = Field(min_length=1) + version: int = Field(ge=1) + start: str = Field(min_length=1) + nodes: list[WorkflowNode] + edges: list[WorkflowEdge] + + @model_validator(mode="after") + def validate_graph(self) -> "WorkflowDefinition": + ids = [node.id for node in self.nodes] + if len(ids) != len(set(ids)): + raise ValueError("Workflow possui IDs de nós duplicados") + known = set(ids) + if self.start not in known: + raise ValueError(f"Nó inicial inexistente: {self.start}") + for edge in self.edges: + if edge.source not in known: + raise ValueError(f"Origem inexistente: {edge.source}") + if edge.target not in known and edge.target not in {"END", "__end__"}: + raise ValueError(f"Destino inexistente: {edge.target}") + return self + + +class WorkflowRunResult(BaseModel): + execution_id: str + workflow_name: str + workflow_version: int + status: Literal["COMPLETED", "FAILED"] + output: dict[str, Any] = Field(default_factory=dict) + state: dict[str, Any] = Field(default_factory=dict) + error: str | None = None diff --git a/libs/agent_framework/src/agent_framework/workflows/registry.py b/libs/agent_framework/src/agent_framework/workflows/registry.py new file mode 100644 index 0000000..10ac3ea --- /dev/null +++ b/libs/agent_framework/src/agent_framework/workflows/registry.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from typing import Any + +WorkflowAction = Callable[[dict[str, Any], dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] + + +class WorkflowActionRegistry: + def __init__(self) -> None: + self._actions: dict[str, WorkflowAction] = {} + + def register(self, name: str, action: WorkflowAction, *, replace: bool = False) -> None: + if name in self._actions and not replace: + raise ValueError(f"Action já registrada: {name}") + self._actions[name] = action + + def get(self, name: str) -> WorkflowAction: + try: + return self._actions[name] + except KeyError as exc: + raise KeyError(f"Action de workflow não registrada: {name}") from exc + + def action(self, name: str | None = None): + def decorator(func: WorkflowAction) -> WorkflowAction: + self.register(name or func.__name__, func) + return func + return decorator + + +DEFAULT_WORKFLOW_ACTIONS = WorkflowActionRegistry() +workflow_action = DEFAULT_WORKFLOW_ACTIONS.action diff --git a/libs/agent_framework/src/agent_framework/workflows/repository.py b/libs/agent_framework/src/agent_framework/workflows/repository.py new file mode 100644 index 0000000..52123d7 --- /dev/null +++ b/libs/agent_framework/src/agent_framework/workflows/repository.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any +import yaml + +from .models import WorkflowDefinition + + +class FileWorkflowRepository: + """Carrega `.active.yaml` e `.vN.yaml` sem acoplar domínio ao framework.""" + + def __init__(self, root: str | Path): + self.root = Path(root) + + def get_active(self, name: str) -> WorkflowDefinition: + marker = self.root / f"{name}.active.yaml" + if not marker.exists(): + raise FileNotFoundError(f"Workflow ativo não encontrado: {marker}") + raw: dict[str, Any] = yaml.safe_load(marker.read_text(encoding="utf-8")) or {} + version = raw.get("version") + if not isinstance(version, int): + raise ValueError(f"Marcador ativo inválido: {marker}") + return self.get_version(name, version) + + def get_version(self, name: str, version: int) -> WorkflowDefinition: + path = self.root / f"{name}.v{version}.yaml" + if not path.exists(): + raise FileNotFoundError(f"Workflow não encontrado: {path}") + raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + definition = WorkflowDefinition.model_validate(raw) + if definition.name != name or definition.version != version: + raise ValueError(f"Nome/versão do conteúdo diverge do arquivo: {path}") + return definition diff --git a/libs/agent_framework/src/agent_framework/workflows/runtime.py b/libs/agent_framework/src/agent_framework/workflows/runtime.py new file mode 100644 index 0000000..f20b15f --- /dev/null +++ b/libs/agent_framework/src/agent_framework/workflows/runtime.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import inspect +from copy import deepcopy +from typing import Any +from uuid import uuid4 + +from .models import WorkflowDefinition, WorkflowRunResult +from .registry import DEFAULT_WORKFLOW_ACTIONS, WorkflowActionRegistry +from .repository import FileWorkflowRepository + + +def _resolve(path: str, state: dict[str, Any]) -> Any: + if not isinstance(path, str) or not path.startswith("$."): + return path + value: Any = state + for part in path[2:].split("."): + if not isinstance(value, dict): + return None + value = value.get(part) + return value + + +def _render(value: Any, state: dict[str, Any]) -> Any: + if isinstance(value, str): + return _resolve(value, state) + if isinstance(value, dict): + return {k: _render(v, state) for k, v in value.items()} + if isinstance(value, list): + return [_render(v, state) for v in value] + return value + + +def _matches(condition: dict[str, Any] | None, state: dict[str, Any]) -> bool: + if not condition: + return True + actual = _resolve(str(condition.get("path", "")), state) + if "equals" in condition: + return actual == condition["equals"] + if "not_equals" in condition: + return actual != condition["not_equals"] + if "exists" in condition: + return (actual is not None) is bool(condition["exists"]) + if "in" in condition: + return actual in condition["in"] + raise ValueError(f"Condição não suportada: {condition}") + + +class WorkflowRuntime: + """Executor determinístico genérico. O LangGraph é detalhe interno do framework.""" + + def __init__( + self, + repository: FileWorkflowRepository, + *, + actions: WorkflowActionRegistry | None = None, + checkpointer: Any | None = None, + telemetry: Any | None = None, + ) -> None: + self.repository = repository + self.actions = actions or DEFAULT_WORKFLOW_ACTIONS + self.checkpointer = checkpointer + self.telemetry = telemetry + self._compiled: dict[tuple[str, int], Any] = {} + + def _compile(self, definition: WorkflowDefinition): + try: + from langgraph.graph import END, StateGraph + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + "langgraph não está instalado; instale as dependências do agent-framework para habilitar workflows" + ) from exc + key = (definition.name, definition.version) + if key in self._compiled: + return self._compiled[key] + outgoing: dict[str, list[Any]] = {} + for edge in definition.edges: + outgoing.setdefault(edge.source, []).append(edge) + for edges in outgoing.values(): + edges.sort(key=lambda e: e.priority) + + builder = StateGraph(dict) + for node in definition.nodes: + action = self.actions.get(node.action) + + async def execute(state: dict[str, Any], *, _node=node, _action=action): + params = _render(_node.input, state) + attempts = _node.retry + 1 + last_error: Exception | None = None + for _ in range(attempts): + try: + result = _action(params, state) + if inspect.isawaitable(result): + result = await result + if not isinstance(result, dict): + raise TypeError(f"Action {_node.action} deve retornar dict") + updated = deepcopy(state) + updated.setdefault("nodes", {})[_node.id] = result + updated["current_node"] = _node.id + return updated + except Exception as exc: # retry configurado por nó + last_error = exc + assert last_error is not None + raise last_error + + builder.add_node(node.id, execute) + edges = outgoing.get(node.id, []) + if not edges: + builder.add_edge(node.id, END) + elif len(edges) == 1 and not edges[0].when: + builder.add_edge(node.id, END if edges[0].target in {"END", "__end__"} else edges[0].target) + else: + def route(state: dict[str, Any], *, _edges=tuple(edges)) -> str: + for edge in _edges: + if _matches(edge.when, state): + return "__end__" if edge.target in {"END", "__end__"} else edge.target + raise RuntimeError("Nenhuma transição do workflow correspondeu ao estado") + targets = {"__end__": END} + targets.update({e.target: e.target for e in edges if e.target not in {"END", "__end__"}}) + builder.add_conditional_edges(node.id, route, targets) + builder.set_entry_point(definition.start) + graph = builder.compile(checkpointer=self.checkpointer) + self._compiled[key] = graph + return graph + + async def arun(self, name: str, payload: dict[str, Any], *, version: int | None = None, execution_id: str | None = None) -> WorkflowRunResult: + definition = self.repository.get_version(name, version) if version else self.repository.get_active(name) + eid = execution_id or str(uuid4()) + initial = {"execution_id": eid, "input": deepcopy(payload), "nodes": {}, "current_node": None} + try: + state = await self._compile(definition).ainvoke(initial, config={"configurable": {"thread_id": eid}}) + return WorkflowRunResult(execution_id=eid, workflow_name=name, workflow_version=definition.version, status="COMPLETED", output=dict(state.get("nodes") or {}), state=state) + except Exception as exc: + return WorkflowRunResult(execution_id=eid, workflow_name=name, workflow_version=definition.version, status="FAILED", error=str(exc), state=initial) diff --git a/libs/agent_framework/src/agent_framework/workflows/tool_executor.py b/libs/agent_framework/src/agent_framework/workflows/tool_executor.py new file mode 100644 index 0000000..039669e --- /dev/null +++ b/libs/agent_framework/src/agent_framework/workflows/tool_executor.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Any + +from .runtime import WorkflowRuntime + + +class WorkflowToolExecutor: + """Ponte entre `tool_policies.yaml` e o runtime determinístico.""" + + def __init__(self, workflow_runtime: WorkflowRuntime): + self.workflow_runtime = workflow_runtime + + async def execute_from_policy( + self, + *, + tool_name: str, + arguments: dict[str, Any], + policy: dict[str, Any], + ) -> dict[str, Any] | None: + execution = dict(policy.get("execution") or {}) + if execution.get("mode", "direct_tool") != "workflow": + return None + workflow_name = execution.get("workflow") or tool_name + configured_version = execution.get("version", "active") + version = None if configured_version == "active" else int(configured_version) + result = await self.workflow_runtime.arun( + workflow_name, + arguments, + version=version, + execution_id=arguments.get("workflow_execution_id"), + ) + return result.model_dump() diff --git a/templates/agent_template_backend/README.md b/templates/agent_template_backend/README.md index 0cf81d7..8483e89 100644 --- a/templates/agent_template_backend/README.md +++ b/templates/agent_template_backend/README.md @@ -4211,3 +4211,9 @@ Com esse desenho, adicionar um novo agente não exige reescrever o frontend nem ## Política read-only/transacional Este template inclui o arquivo opcional `config/tool_policies.yaml`. Use `operation_type: read_only` para consultas e `operation_type: transactional` com `require_confirmation: true` para ações que só podem executar após confirmação booleana explícita. Se o arquivo for removido ou não existir em um template antigo, os campos legados de `config/tools.yaml` continuam válidos. + +## Workflows transacionais determinísticos + +Além da execução direta de MCP tools, uma operação transacional pode usar um workflow LangGraph determinístico após `clarification` e confirmação explícita. Configure `execution.mode: workflow` em `config/tool_policies.yaml`, mantenha as definições versionadas em `workflows/` e implemente as actions do domínio no projeto do agente. O runtime genérico está em `agent_framework.workflows`. + +Consulte `libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md` e o exemplo `workflows/devolucao_pedido.v1.yaml`. diff --git a/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc b/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc index 884409ed746324caa06d75871c191596fb69d6ae..f6a2b79f0ac62dab88b9458890c4272746a21aa0 100644 GIT binary patch delta 24 ecmdnVIFXV2GcPX}0}ya#7fj?fW;B?X9s~eIeg&NX delta 64 zcmbQpxRa6lGcPX}0}#y7&!5O`tQn!7n^&TrqVF5*86QxTpOub8;LP60B_qtOQ<9onkds)FTC6|$9=8~y{$_RIhgkqJl?~be delta 28 icmZ4ahq2=iBiCnMUM>b8c#ye~OGcQHd$Wh|!z=)b>j>ol diff --git a/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc b/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc index d6621835a07807c6a6b4ba4914d4e5640f3f740f..8d7093dfce13eaaaafdbe36e8ab554a88679460a 100644 GIT binary patch delta 20 acmaFJ{*ayfGcPX}0}ya#7i{Fd#tZ;FzXfan delta 20 acmaFJ{*ayfGcPX}0}urN%iYL*jTrz!C%`M1DEJ-caPfSnED~X5ki_@% diff --git a/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc b/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc index 0e9f1e1ccd115e065d40dcf3f436c542eb47c4e6..4e27b2050509f616535e4eeacb5853c87efd3d19 100644 GIT binary patch delta 27 hcmeCse4xSonU|M~0SGv=3pR3FFfrO}&S8=f0svtf20{P; delta 67 zcmaE$(V@xxnU|M~0SM;k=Wpb;VA4#{&&?~*Pto@c_KXiG%FjwoE-BVeOi#@#iBBs^ V%uOxNFUpS3PtMfe?7}1?1OU8p6~+Jn diff --git a/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc b/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc index f05ef2c6479b9fb2140f2b7910601bb299e3ab20..803273ebcdf8da5b54ba9c331ff49f25081bcc29 100644 GIT binary patch delta 27 hcmZ3Z(W1fqnU|M~0SGv=3pR3_Gcj6k&Sp9&2moCN2C4u6 delta 67 zcmZqCSfj!HnU|M~0SM;k=WpaTXVOg8&&?~*Pto@c_KXiG%FjwoE-BVeOi#@#iBBs^ V%uOxNFUpS3PtMfe?96mf5CE*x!K9g@pPN^rpQ7&@>=_?Wl%JKFTvDu`n4X$f5}#I- Vn44OjUz8o6pPZ?`*@ekO2mr|f77hRa diff --git a/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc b/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc index 27c5dd67ede2919acdfd9154c883b727601ef3d0..eda48f56cae61bf11461b26c11974f94f0c0418c 100644 GIT binary patch delta 27 hcmbQo`Hh47GcPX}0}ya#7i{DfU}m(~?7|$y2moEF1_=NF delta 67 zcmeyyF^`k`GcPX}0}#y7&)>)`z^s{|pPN^rpQ7&@>=_?Wl%JKFTvDu`n4X$f5}#I- Vn44OjUz8o6pPZ?`S%x`^5dg0!6^#G@ diff --git a/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc b/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc index 321fc08938aeca5f7295d480b1be6e2d3b8e0ef1..df7bf1540128d98ce4730c2b8fec5005d9603a10 100644 GIT binary patch delta 25 fcmZo*zQM%(nU|M~0SGv=3np?uW;B~D&X@@RPyPlj delta 64 zcmcb?)WFRBnU|M~0SM;k=TGE*tQn`Dn^&TrqVF5*86QxTpOu*x$)uU0pPN^rpQ7&@>=_?Wl%JKFTvDu`n4X$f5}#I- Vn44OjUz8o6pPZ?`*_G+EAON)l7F7TM diff --git a/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc b/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc index e011e2c1613531059c3e340e048f089dc4c4b424..a6540ec7a5c03336dbd6fbea30591f44c3e74e04 100644 GIT binary patch delta 19 Zcmcb?c!QDqGcPX}0}ya#7fj?n3jj3Q1tI_d delta 19 Zcmcb?c!QDqGcPX}0}urN%bm!5763VE1^fU2 diff --git a/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc b/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc index 417fdeaf32888e0d6d9e8772ec338891007c6a65..8fc575e974f16282f77fcefc646c95e92424d76c 100644 GIT binary patch delta 20 acmZqUYvbeo%*)Hg00f-b1sl14vjG4wh6L;Y delta 20 acmZqUYvbeo%*)Hg00hDRayN4SW&;2jppo diff --git a/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc b/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc index 8684f23bc51276468a617f04c595b2327971cf01..41a72082a32c7dcd6099f21ab72c7a84b01d603a 100644 GIT binary patch delta 20 acmaFC_kxf6GcPX}0}ya#7i{E?Wd{H~*98** delta 20 acmaFC_kxf6GcPX}0}urN%iYKw%MJiTKn3jp diff --git a/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc b/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc index 19cee2f2e4369e79e375c5bd76c1c65eab68959d..a8f3f88711923277c99fcb4759848611ad3eb33f 100644 GIT binary patch delta 20 acmdnYx0#RoGcPX}0}ya#7i{F#UUGGcPX}0}ya#7fj?fX0)4_o&W$yNCnjZ delta 64 zcmbQwc$|^@GcPX}0}#y7&!5O`teK{tn^&TrqVF5*86QxTpOu+sl2NljKR2&LKSkd+*fTz$C_gJTxujS>F+DY}BtESu VF*mh5zbHFCKRHu>^H#>U0sz)77k&T$ diff --git a/templates/agent_template_backend/app/workflow_actions/__init__.py b/templates/agent_template_backend/app/workflow_actions/__init__.py new file mode 100644 index 0000000..6be8ce7 --- /dev/null +++ b/templates/agent_template_backend/app/workflow_actions/__init__.py @@ -0,0 +1 @@ +from . import devolucao # noqa: F401 diff --git a/templates/agent_template_backend/app/workflow_actions/__pycache__/__init__.cpython-313.pyc b/templates/agent_template_backend/app/workflow_actions/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d217376044aa07f27746a8bbc8017869e7897083 GIT binary patch literal 199 zcmey&%ge<81i!iqveba|V-N=hn4pZ$0zk%8hG2$ZMsEf$#v(=qhIA%P=9i2>VNJ$c zoGGbg`8lP@iTQq-Ot%<{n1RA889oC^hFgv$sksF?i6yDU`ibeOc_r~Metc45a&~H7 zihg22fqr>@QFdBRetCRia!F=>Ua@|Bd}dx|NqoFsLFFwDo80`A(wtPgA`YO5AUlge Rj1SC=jEwgf#EaN~8~|9dGr<4= literal 0 HcmV?d00001 diff --git a/templates/agent_template_backend/app/workflow_actions/__pycache__/devolucao.cpython-313.pyc b/templates/agent_template_backend/app/workflow_actions/__pycache__/devolucao.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ec9aa1b6daa7d9bda590c8289fbfd5b1b8db60c GIT binary patch literal 977 zcmZ`%%WKp?9G;nE)9q_ZX@$0GH?~sUg55sxpa_;Og4Kde*Mk;9noO6pNhZu>Thxo_ z(W^o|x!&wS=&|?y16%gwfFOA4ZA(siawfZ3so=o;9y8zX_07cn{XIae-|xbmD+qv} z(n(iW+lW13d;kWZz<>r?g%1%$I@m-Lz)+T83uUOAijfgp+f)`cqiYQ8s7vhxR86qi z|6sc>U^A7TLT~kf2)%DdE(w_KVBI19$=ARodWflS1=z;E9uVEC;2^?xbfVL05V=0q zF^f(<%d2f_`FNku-I+7lMFH{w>1}&t-?Wm%Jhx|gu47R%#E$C_u7#EeVm!klE5cl( zII7aX7PNGhCakw&$IC)Y0$tJqpSLLhpbXZP4z^<$C>=c2v+z2&t!%@Bvc@w>7d%U- zgQ@8{1;o`YLOiZiaCC@hM&zNgE|ZPuI$w^kA9|ugR!rtMQ{UXO>|GoR^xazoe)M72EI4llhu|d)W z&))TkQ}ysYIx5cOcVK}KO$9>u4Ltr1md-VR2J3^@8gQbCAY7;oH-S)JoJ1W(vGigE opvmO=Je$C$8t_IFA@~T^1W@Xu#g;x^i4$Lj+&oVpbVQW=16iEzC;$Ke literal 0 HcmV?d00001 diff --git a/templates/agent_template_backend/app/workflow_actions/devolucao.py b/templates/agent_template_backend/app/workflow_actions/devolucao.py new file mode 100644 index 0000000..6111cf1 --- /dev/null +++ b/templates/agent_template_backend/app/workflow_actions/devolucao.py @@ -0,0 +1,13 @@ +"""Actions de domínio permanecem no agente; o runtime está no framework.""" +from agent_framework.workflows import workflow_action + + +@workflow_action("validar_pedido") +async def validar_pedido(params: dict, state: dict) -> dict: + return {"valid": bool(params.get("order_id"))} + + +@workflow_action("registrar_devolucao") +async def registrar_devolucao(params: dict, state: dict) -> dict: + # Substitua pela chamada real ao serviço/MCP e use chave idempotente. + return {"protocol": f"DEV-{params['order_id']}", "status": "REQUESTED"} diff --git a/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc b/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc index 17e9714aafd4f09ab474f9c15738539760267ec6..c5a3581927113268636bb146f308f6553041f9df 100644 GIT binary patch delta 61 zcmZ3xocY{xX0Fe?yj%=Gz?r>~D}q(Ywj?#TASbaTwOBtfJvFZ+9>$MPN=(j9%}deW JoXqNV4gg?%6)gY& delta 28 icmX@NoO$hXX0Fe?yj%=G@E~&|R|G4g%;pwWuX6x|6$rxs diff --git a/templates/agent_template_backend/config/tool_policies.yaml b/templates/agent_template_backend/config/tool_policies.yaml index 66c9854..3c4fd05 100644 --- a/templates/agent_template_backend/config/tool_policies.yaml +++ b/templates/agent_template_backend/config/tool_policies.yaml @@ -14,6 +14,11 @@ tool_policies: solicitar_devolucao: operation_type: transactional require_confirmation: true + requires: [order_id, reason] + execution: + mode: workflow + workflow: devolucao_pedido + version: active # Exemplo para uma operação real que só pode executar após confirmação: # cancelar_servico: diff --git a/templates/agent_template_backend/workflows/devolucao_pedido.active.yaml b/templates/agent_template_backend/workflows/devolucao_pedido.active.yaml new file mode 100644 index 0000000..b825518 --- /dev/null +++ b/templates/agent_template_backend/workflows/devolucao_pedido.active.yaml @@ -0,0 +1 @@ +version: 1 diff --git a/templates/agent_template_backend/workflows/devolucao_pedido.v1.yaml b/templates/agent_template_backend/workflows/devolucao_pedido.v1.yaml new file mode 100644 index 0000000..d2ff1aa --- /dev/null +++ b/templates/agent_template_backend/workflows/devolucao_pedido.v1.yaml @@ -0,0 +1,27 @@ +name: devolucao_pedido +version: 1 +start: validar_pedido +nodes: + - id: validar_pedido + action: validar_pedido + input: + order_id: $.input.order_id + - id: registrar_devolucao + action: registrar_devolucao + retry: 1 + input: + order_id: $.input.order_id + reason: $.input.reason +edges: + - from: validar_pedido + to: registrar_devolucao + when: + path: $.nodes.validar_pedido.valid + equals: true + - from: validar_pedido + to: END + when: + path: $.nodes.validar_pedido.valid + equals: false + - from: registrar_devolucao + to: END diff --git a/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc006d715c6ef90211606fb5d721424d53358e03 GIT binary patch literal 788 zcmb7C&ubGw6n>N4WVRdIPz%P2jj0fJnHio2 zXk0)5@4*Kj)u4(vDw;S|OL1%R+SRm36``8p__X72M|Z5+T-D`n5gP6)G_qxlVywPA zeb4*yX;Qy&zB-M4JFNr0&~WO7i{1L|i!~n_i<)u4Pp@YI{EYE4_Ht@EuT}j&9=;rD zCslGz3~ULhERCXAQW?aNc%pOrDV5Jp%;_pecUUAzhtn<_#C%s-g16MQX;rdrFQk%@ zCT;Dqs9n0MTtYg1+2@Q9ZW#cl*$+Y)L_%G8)EknsY?<_ik_ov%g<#xJZ*Vr?fn;j# ztfcU)=IUiLPAJzBhC<<<)))p&ZckV=Z4hcwvmxT}B{KtDOBc$nZ*MsNH&o7pZrtvN>>J9gwQdp{s4Fb`|dGZ|B172AMd|9!Yc`08R1(;crC$eA5entzP7)Eb>Qqd2f4l6 oA$oW3P<|_W2 z?kpu!Z2+cC6S++g{xP8ZBVY%WlAy?s1_9Fk2#`MpinhC4u}cgbAV5(R{%2b*3g}nQ zxpU{SOHw2)rFBpbmS^tc+}F8x?)mOHm-i(p&cS1Rul#0I;<$gui1P?`fWQCO5O{}^ zIhjAtP4R?JV>oa=K!WrwOohxqnwtuf2+awek50u%jE05t@hOprG#omgn39N;=6bn7 zP7e2Ta%4PUw?UF}QjYd>Z}KE1$ND+aCdYxc%OcPYIRUg&mVkCG1Tv`)u~3G$#GI}h zm0YE0=;k!UQ*v>!T+*gwk?()-jc}u83UortpFtyS=fob4OER{}M=Z-Gwm7|4RC3iG8 ze?z@tkgG>?3z}Y0=Lw7wKvSMT>@_aVqN4T6URI*5JT7`Dd-xVi~4jwCjuB1ADu>AINHQGx$Ac0m(8vu#JaIjt&PFz(Njlxn1 z!wFTrwv;QeoRq507l~OZ6?IKFRF&WakZ$Zkx2l@BZt`lbQX$35unJ67y$eeXS+JKR z>JXe!6MOU>?$ZDt>ie%S5StNsn#2Smvc)tHfp@q$p!PH4UlA;A(3d}s0)|Hq%mzv> z?;OqveH7l|Py!|PU2>cfU?R`JN(4SD0%Fl_fFz`*1waR5MTzqP1g#WZ8_TMbWQ9xG zLN32-g$ey6OdLr%owjEzqS~nhzlnDEGc!O|xDTZMhBUY?4Ze4#E*)Q)+#EP^Pw4%i z=c%=Dz30m-li$9uDYmb?MunAUBZPt+hvTBliG-+}+`Kt#mp%&YHqXFva>nhvb$$-Y z*pD2LgGYH;IL67LlY$=b@0dv3C`dgwmkST%aLQe!?y%2mtWSya@43Okykn6 z)-Wqk_z}m=(OnV~W^gIAJg}R^l;{{Q$7j!t1;*L@$l@G+0`!@|wb$|ta^o!J58z!G zXlex)V#_ngDS{&fr*FBQrczxkMIh=gb%BRIc_Z1LJL0q=;jD z@wq3QR(y5!I%(b;T8=w)Id2U%(=&YeyK>6-2PK5#Hf}g$*^`{xlnBVVefH;`oPE96 zeng_vy4>;DXC&m*?TlhiGvbzl8R?Kae`j+3g%Xy#@N>~cx!d?BC2{JNCW#wOt7rR_ z-9sh*#OyzQ7K!gVHwj06axasDB=^bvio{l1av!TL>D1-C?bW7>u;Tt4Br(7wap3>A zBo?^L!Recc#iD5z^@Z$mZn0#Ab9qEjR@5K`ji|-K#f)IZs1m6xmo?NK*OrPzGp+8t zp@Ujoq^Jw@2Td+mZQ5H}ehKTaVaKu+$&~_X{3DJSm{;&V`*Y6sDdu^@)s1D$ zy8&_%Yk<1OntC^3_in=0QRr>oUn3i{%{AtOR7MmzRpAN>MjKc^klCTD_`y#aJp8D66_q zGA`#z6ej^fHFNVC)?g)Hpn`>&dYKS|Se@#;fzt_nD3@|Nl}#pPMJtPCwVbP5v7!!+ zB!k%VLr9JzIRV6qE^3t=0#r-HKH0N2;*T7+BX%oe{>X%%?U+9@>Sr7GN1inio&hxR zKQ>Gmp#o#;nk_?sfB+54uyQqW9YCH9bJWY)mE85BLCDvjGSpq>-vI^zM3ef`dxzGp z{Orlv;pgjvXX~jKR$lo)IP!$GNwW>JK5ZG^B7`XMU8?yr& zO=&eR1{teb3o}H4V^i#}3PZQ&YvNEtV83-?=+^Ug?4B2Nvmx#Fyc?@E#oXk&FtjOl zR=b93;?q^(#GR)osfi~V0{g8CC#vGpcJ!VvTt{w~%MCy>RJeKpI$9OR?j$IwiDM0c z{nmxCDh>jp_k7_xa=Tn^09YOZE3d#ehNl!8Lh8K2&$tj1CYwD7A7$ZWAkBHO(*u_t znDoH6qgb4`9#8_?g3Hh`t_fI<=^;66G#$68%D;K4DeY7|od)D$E|n@df{}E(8Ht=q16nDl zhF)5ZKL{;V=AStMped^9d7}U@&5F6DFOn>5VB{DQs)t7~gl9=IfdthuIR(UuxmA+W znD`u$=aIaC2lAg?3WSCPyhk&#?Pq9Az#$sZ$`1!Cf<83p7$wtgR_`rRFqqv37r z1iJl3u>$a?qB;l>Q64EkWk;~qH>iR~t=4m-CLURNX;X-P_rkX>Y(~#*w)cJ%;zC^? zbHPyf(H9L#h zfsLlLniqqN)j|t1M1f46fr1vR}r%?Dq;XHOkKNyhb!U$*bn}16*1U(TC6;JFwN=0PKq1&K_N9M z?>F>_95en>iGuR}p}WcbXb)%SxDta+PMkgAZE|wrv8xqV4Lg0?lMTWx1xLS2 zHa}Tn8)_UZ_9-RKHdJvR8>+iGJ9RSUeH(0VfAt7bp7lw&?XgSQwb|@T%5Eu;a@*%b z%AE0?N09OrpOo7lyOdp!xGyQYr9jHH7eYdZ!8cLT+B# ziivW1UMuYciJFWv6f`k|Q=XnZ$ znndD2AjdkVmH^2SF#22u)039do5Vz}Le*%(GBy^Nnx4x-xm|gGiaoNc^Qd_w>ZE&b9bDg=&9hgG` z&OYY!ZBK~GEwv|eh>O2=-Kkx(0T@n#)CU))hLbScsU!Q?sa@}$GeWjg2Vtka2$&fC z`z;U5mV~nf=y!i}m85f(`eLrY$o1Ig3bbk))2cIDv?_DG*mef=bD)a!`8oWm)NE1` zTXZS;3M>bLo?5C1!PPQ>ew&eu8UHgDNZURV@aklnZ5r`!I{p*%;Dl}WNG3=|p*HlR zj8KEcO~0$>S=*WNtZn3g)=od9we7$Fw8>mc&?eX)Ht$Nvxe2R zT`QTWPwg*a?P%BVdoadqs|Ff5{O*erChmuhg0~g>Gt91Gd)S($Lj?y>&Qy!0YA%(_ z2B`pAsXzs2e=(olQH_72S&hfJWct0}+Dq@o>e7*hl&wkGJIi(HB$zeC_J)|QiRr2^ zx^{t*nmF1J*l%4Jt%_+kY6muiQB=r`rRK#T!{x4HeJ+z5fMmE5(hv{V#KTn~dk35W zNW^SIV83-CTNMwxQ9H09WU)!cQuAVv;d0lpDwoL(+!L}7QHy^M3heQ#MXLB7t{UO$ z9NkoT-=>Nt1y^0pxED2GuGxdv=IEvxY1&kybFS-t#=Uall))yn|W;l@^>q z_g>3mDBCK!sGKlvD=`X7e(a3A@UMHA6-kK$EJ@D3?Sm!1pR?kEH=oS6pFh_mv@56U zt&vk%fdUv&Zj;*;kpY;*+}>x}J>a$gn9vzsj{=CfyW@->Xjudh)uVF9Bh7f{7c;(9 zs_;F2K4$#Mt$l+T?|P&e@BU)Ow@USYd&U9$_Av0fUeM-q;8imb>^8$sze{ZkL?+t& zl3v)}6SpH)Zo*~^DkGM2ukYCPrPFXF88A0q&z^^?O%!gY-6-}88&~ONE0%d(BPRH( zPNhfuhAg;$=eiT2Fmwba$MXeRQKt(Ict4MY;Wm>E z-XXxi`wn>C9a#LESsasx=mDr_1d8N|j%}Vr0#;3~3C^Fi;rvM};U6*#;=*U|hcl$6 zzI+&frlH>H&t5N^fu?G~yw`kyAY~lq9$lhV%$1)dp!mpJP}+*Hmf%(xdQ5HGVH!hp z3b%8UqKDLfx@fvNHBL;-rgy^ZTUN)9nGz&9#|-+m&l(we)^J3)<=z>dG~gbXVZu4% zFQ5i=%9f+3%&c>w$*xU7 zY;CI6dc1%Wjd$RH@WYQ6;PgapI9|}x4GXVLwNzr)Z0v)<0XY+u6MnT!3G1Nf}j|9=c&2(JjzYXsDQPLJR8+aL%i?eV1`WJOQg|A_J& z`5TDB*_<=+!o^2Hp67qV4gM#`eVg0hzVsj5bHCwE{&zUU_kGL(`Ls>oJ3dLK`0-DM W5es)Pscl%3%?2y@;O%Q$73N2{{ZAYA`pQ&Mkr&HY`;fk zBcz|B-Uzm^$9(;M>NjxaSbzqA<~bJZ57AIRPx<~bTGn4q%Om7WU}&8^K(QR!P{eIa z;Li9vS;4!8T;RGaBCT3>f8V11;c+JF{{p2AL>ZksA!g4Wpi}WnN^^7e7|-SO+MGgl%+u;4MD`0c!pgY6jWKG05%?zaamgyN zEkVbCEz2$?H~{uCGPjq>NHANaHf|6Xn^22T4hpN3RNQCG+@EagB$Y+hdDhH@VCGhz z`b}}>T-uhlQP9@%xtucrvhE;fBjj%S(TQ(|NhKS$1jJC zLn94#Jsz_A$zEZd@*rMrSnI#GwGK1M`X$+`0_?T^)Y+oFgbmAD=^Gb~QgWq_fF|Z+ z>uhT&u36N(`XVdJv2T*`iApVDqA`_btIT5Wvp3UX%cxtz{^d@g6Xt+x<~ zd5u+BDQptzab>GQ^IW?) z=Q7yvo4$LU8TuCI)!D(q3*Xv}inqlde!MO0`=y@zcw5-E7%F+2#=q0sVjKdO!`^`z zfSv(vr)P|bHVsU#pwd`QPR2wCkSaS)+n(c{P64R-qn3Y1Zl9be*P$beCNhMwM z2t23*j_NQYF(qemAmEBZlM+o}pUp9Y7PBc=9chvgxJghi3UkwzB1=k++X{|RW#S2K zvPw_HEphU^1PWil7!egSzAk3#Zz3%b=_ffct@cE+G$)zLK11@sftL*3mnC%!WG@oy z)(`bXsd~UxYSaoBjirg9W;OgRpsxrS0tP}<)^Pqk=)$%!n@N-#(7i@yJz!g0_q1>C zxJ7QUx-TPAaYEI-Fwa>@McI*vesbG{oZSZVEp8LXCKHnom~RuuB{`cI18xcH31u|3 z4SHnNZ8vPmMaP z3996vIU&l)#HcuWUQ%Ow(#j}UXdfV^0K8>Xj2f?zJN~=1_d;5qkbmWj)^|qRd3FKl zME6@JTx2W!Hp5n!a6wxMtYII|fF8(DC98}o>BpWEt?d-KD2`y+gRVjX9s#rtKonar z^u9)JPv7m*b{wVPdK1`Ue)HQ^-Dhqj8U{MW`&JJg7<~D}@q@z$U%JISX7}!mGSjgw zO_{0fQ8jeMLLz%3h_yx47*0n{4PA}wHECT@RcDopQIGBc1_av?bWK-?sw&Z}(Qzj* z0dXf?kHezFaf_K=fg;T-D_x(} z4hGRVP#C_uySrNtQ0am+nw64K=CMy!vT($JuG{VJG%1EbN~jK+#$b)XnFPvtjIn74 zYJp=%SLmThn#xLvtaLd`aUV$e;$)%G9YhpaDoP!;Mhp-+@sN$G}67-5b zV{+k8JxD3!v#Jwg3Y`$M3F&esEy^Mux4O^#9O!48owgg_LIbF91i^L$TL9=hYGWF= z-CgN)S31m!IoJ+&WtW?+&0X2+Tl5lFK{&)S9ASUZPVFV?zJM}v=%u!%k_U+ zv3_<#=j~H78}{AvYZWK19saDc_T4w$e&fcGo71;c*ZkTf<#Ht??UghBUr8&vkx~ zGl0f-7C7s@&voYcX0tl$!e@}%;c^fl&sEkIDkEBDB+oUz(~+-?%=nBJ`EpFL>-#qDba1dbsY~{M&p855e zn@u0QdS_+6rFUlKi-pP;uN|4=tKaQ?yEo58Z(KBh#zzaB_1@>AZ}mDg6L|o%T@Fnv z*08~1oZpE~?bH|Ai$14w?C1ryv<6g)*RGsQ9nRRe-3Z_Ja?)~$=e8F zD;w++RtVK`pOts>kF2yMyJVHjkjDLXp2;q)yqgE&`(cRhpKUkD(F?W0s+i{)whP1q z!fGR{?h)$HibkGzd3Ic5*m3O--Ht{y$_zvUWZ;)ZEV^!By7Gc@&K-zS^t351W{STl zP4n1p1)bALGvWkNiEvYqpNce*gkFFhWMciPSkGp1!X^(;mKX&X{?YK)IRE-y!{Nv% z>C`CrP)RWZUSR?}K{cVs>8WB6GXXL3({alYFoB9L8H5d;cWVCJVnhuTa{feTF6%)v zWx(sgSStc zVFVgOunorwVEBe{neGAO>pnE-B?)}t5o8gO#cw45=pH21BWOUd6+pC%b|a44fc7B3 z-y>blakUkEL9D1uBuxG2UgX(_U_XKb2wnmZ4Hmsr2%k)w(&N~C6v4{~1`rGaz_?1y zrL#u#<+y~VH`u(4$)V>#nBiak9U>ja7u=+I!4+xMxYiHkyOVju2EiA+Uaoo9!(9zm z`aBy1SFi?M!Bxb&^&u$(S8&bKu3%hSw{Mo~{j{R~_r0?nNAESwbO`ygNv%PesTc#F zF}%7EZqdRmx7EAn@cz<)v-I(_t^Y!B7Y$Z`W;}8pqg>^Ec=j< zbIfa48)g!0zB&e2&FiRY#p~>~LvwuidXvVl&2w9CTrq&gZ!K`vd!O5y=hvFmSr)V!=b$l?GuR Y{^4OUR}IsAms=oE7+eooz>I_H|If0iasU7T literal 0 HcmV?d00001 diff --git a/tests/unit/__pycache__/test_transactional_workflows.cpython-313.pyc b/tests/unit/__pycache__/test_transactional_workflows.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..afb7dd26ded9a41d0720264ee7247e46b869b0ac GIT binary patch literal 3962 zcmd5h%e5`HuDWc`dSTb3~YWC+GV*b*j~5R0rGJ3zqf8lu6K9}HqFjg3H>(Y_hk z*kr4`$stSC*1{fA%bvnvw+e3F8%Kxif#XbxD!HcEDynkgW|y}r6;9hd(tvCX8}_tQ zHE())y8HF(*YE3|)s~hJf$Pv8OIQ9BB;-eY@h+iGtS$iYEs=@LohBn3<0Pc{(>&wh zDV!EYD5E1j<{N2ZO(TBhm&iDuZu88%c&6K5&>khzajuV-ecQ+tdbrMHp*PlVTB9#c z@p1oCv=-+adR8$nkH)FfaUx$(Kh@dQ$$~zUR!h2(H+44aba=d>teMxfv^teHOyIYA z{B&6}^F`H(d5m+qUO0YD&6Yvm37ExFrUY|jId4($^uhf#+^ZLPLiP{|0yqW%Oo)V} z8|CUMGA|1YoX{*s&J7CGF@d2^nEbh!-vPIKQ1qrPUM31dRK zdA$}Pa%<`{LUQ2gW}gY;#X<8}d zbBd{kG(D#psgNWkqDG=B7LcCz)Pc=Z&eSq(+M~msN21I-X*5%w1j5^S?&)Na}CPO2J4;CM&Bo zXJ29Y_~0~ zb&@z8b$qU}6YwJL2nXLibdTKQou-n)l%nDIV4qFZK--e!><7{`4wSXLc>u|b$rR0~ zZLdg<&9J($#83& z0CNkC%2*RBP~=gCl{KdcccBckK(OG{I6^_yoM0xa7fVpVa&aFEpoWAReo%0W6R%%I zlULNNspbrv0gsR5uFXlj(VnmapxpwuF#s!g3UPSiYq8ooZC_`V_S&>}Vb{ZVETqy) z_(8ILQkC}Dv}c)*2I=xX(!O=MoA?qdq{G+r?<8s6TM2i~f4oG+DvjARcIyj!_k~CF z;tR&-wsh(d9eEty`Dj<)!e^CT0}uUn`0V`0Pg*-}etF}|+b8eLEgZJG`YWxeYA`iF zyd<{V?7z`}TdatCEV{=M_tZJJKDWiU?u2Zy`);`+_F1&g68j#D;Xlk&TP3?ys))Pq zd{Pm+EZSv>T~9Xc_{;d8$M5XB`^EiDR?lE%(}&g859d!ViP4*bHwJHCt%z}p#w{_v zB*t!^{O-(O&s4;2i*{RL_r^wTv1=tlLPu9Zq-}`%+312eel^S;OK=Ya?pT~YphFV8 z{LslA>!v^K9rD4;KfCe!n*IUv?`ybMCt(ZzpZbT5I)n4iyMQ%4La?9rs;=8BU`nmK zMeG=Qxn52V_Y3R0g$X#H=b{vHH7p^j)Z< z?QR|2G&4kiDHlyT}5}1POt{cQ2ZT=1{^V$XMjf;6Z+-2-~{oR!Js)og-w-6UOjOHh^W(qQJPlc0(%e4v!ghNzuu8BtP$*O7~+1m7irvEhDVKI0If#>G2JK^Oap-!cd3S)-ER;uHVmpq;5LGdxVhW*?XJ>)5|>t3X*1Rk!`3J;Wf GX#N8>Z>#wL literal 0 HcmV?d00001 diff --git a/tests/unit/test_tool_policies.py b/tests/unit/test_tool_policies.py index 8080cdf..d5754d0 100644 --- a/tests/unit/test_tool_policies.py +++ b/tests/unit/test_tool_policies.py @@ -82,3 +82,17 @@ def test_requires_confirmation_alias_is_supported(tmp_path): assert policy.operation_type == "transactional" assert policy.require_confirmation is True + + +def test_workflow_execution_policy_is_exposed(tmp_path): + path = tmp_path / "tool_policies.yaml" + path.write_text( + """defaults:\n operation_type: read_only\ntool_policies:\n refund:\n operation_type: transactional\n require_confirmation: true\n execution:\n mode: workflow\n workflow: refund_order\n version: 2\n""", + encoding="utf-8", + ) + registry = ToolPolicyRegistry(str(path)) + policy = registry.get("refund") + assert policy is not None + assert policy.execution.mode == "workflow" + assert policy.execution.workflow == "refund_order" + assert policy.execution.version == 2 diff --git a/tests/unit/test_transactional_workflows.py b/tests/unit/test_transactional_workflows.py new file mode 100644 index 0000000..1e8fbfe --- /dev/null +++ b/tests/unit/test_transactional_workflows.py @@ -0,0 +1,76 @@ +from pathlib import Path + +import pytest + +from agent_framework.workflows import ( + FileWorkflowRepository, + WorkflowActionRegistry, + WorkflowRuntime, + WorkflowToolExecutor, +) + + +@pytest.mark.asyncio +async def test_deterministic_workflow_routes_and_caches(tmp_path: Path): + (tmp_path / "refund.active.yaml").write_text("version: 1\n", encoding="utf-8") + (tmp_path / "refund.v1.yaml").write_text( + """name: refund +version: 1 +start: validate +nodes: + - id: validate + action: validate + input: {order_id: $.input.order_id} + - id: execute + action: execute + input: {order_id: $.input.order_id} +edges: + - from: validate + to: execute + when: {path: $.nodes.validate.valid, equals: true} + - from: validate + to: END + when: {path: $.nodes.validate.valid, equals: false} + - from: execute + to: END +""", + encoding="utf-8", + ) + actions = WorkflowActionRegistry() + actions.register("validate", lambda params, state: {"valid": params["order_id"] == "123"}) + actions.register("execute", lambda params, state: {"protocol": "P-1"}) + runtime = WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=actions) + + ok = await runtime.arun("refund", {"order_id": "123"}) + assert ok.status == "COMPLETED" + assert ok.output["execute"]["protocol"] == "P-1" + assert len(runtime._compiled) == 1 + + rejected = await runtime.arun("refund", {"order_id": "999"}) + assert rejected.status == "COMPLETED" + assert "execute" not in rejected.output + assert len(runtime._compiled) == 1 + + +@pytest.mark.asyncio +async def test_policy_adapter_runs_only_workflow_mode(tmp_path: Path): + (tmp_path / "job.active.yaml").write_text("version: 1\n", encoding="utf-8") + (tmp_path / "job.v1.yaml").write_text( + """name: job +version: 1 +start: one +nodes: + - id: one + action: one +edges: + - from: one + to: END +""", + encoding="utf-8", + ) + actions = WorkflowActionRegistry() + actions.register("one", lambda params, state: {"ok": True}) + adapter = WorkflowToolExecutor(WorkflowRuntime(FileWorkflowRepository(tmp_path), actions=actions)) + assert await adapter.execute_from_policy(tool_name="x", arguments={}, policy={"execution": {"mode": "direct_tool"}}) is None + result = await adapter.execute_from_policy(tool_name="x", arguments={}, policy={"execution": {"mode": "workflow", "workflow": "job", "version": "active"}}) + assert result["status"] == "COMPLETED"