diff --git a/.env b/.env
index 674c89e..116eb7d 100644
--- a/.env
+++ b/.env
@@ -22,30 +22,31 @@ LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
OCI_GENAI_MODEL=openai.gpt-4.1
-OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
+OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
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_PROFILE=DEFAULT
+OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OCI_REGION=us-chicago-1
###############################################################################
# Persistência
###############################################################################
# Opções: memory, autonomous, mongodb
-SESSION_REPOSITORY_PROVIDER=sqlite
-MEMORY_REPOSITORY_PROVIDER=sqlite
-CHECKPOINT_REPOSITORY_PROVIDER=sqlite
-SQLITE_DB_PATH=./data/agent_framework.db
+SESSION_REPOSITORY_PROVIDER=autonomous
+MEMORY_REPOSITORY_PROVIDER=autonomous
+CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
-ADB_PASSWORD=Moniquinha1972
-ADB_DSN=oradb23ai_high
-ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
-ADB_WALLET_PASSWORD=Moniquinha1972
+ADB_PASSWORD=fjhsdf04954hf
+ADB_DSN=oradb23aidev_high
+ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
+ADB_WALLET_PASSWORD=fjhsdf04954hf
ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
@@ -70,13 +71,16 @@ RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
# Observabilidade
###############################################################################
ENABLE_LANGFUSE=true
-LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
-LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
-LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
+LANGFUSE_TRACE_MODE=verbose # Opcional: verbose, compact
+LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
+LANGFUSE_LEGACY_IO_FALLBACK=true
+LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
+LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
LANGFUSE_HOST=http://localhost:3005
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
+ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
###############################################################################
# Analytics / Observer corporativo
@@ -121,6 +125,9 @@ 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
@@ -134,21 +141,33 @@ ROUTING_CONFIG_PATH=./config/routing.yaml
# 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=false
+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=./agent_template_backend/config/mcp_servers.yaml
-TOOLS_CONFIG_PATH=./agent_template_backend/config/tools.yaml
+MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
+TOOLS_CONFIG_PATH=./config/tools.yaml
+TOOL_POLICIES_PATH=./config/tool_policies.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=sqlite
-IDENTITY_CONFIG_PATH=./agent_template_backend/config/identity.yaml
-MCP_PARAMETER_MAPPING_PATH=./agent_template_backend/config/mcp_parameter_mapping.yaml
+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
@@ -163,6 +182,18 @@ MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
+###############################################################################
+# MCP Gateway
+###############################################################################
+# true = framework routes tool calls to the dedicated MCP Gateway.
+# false = framework calls MCP servers directly from mcp_servers.yaml.
+MCP_GATEWAY_ENABLED=true
+MCP_GATEWAY_URL=http://localhost:8300
+MCP_GATEWAY_TIMEOUT_SECONDS=60
+# MCP_GATEWAY_TOKEN=
+MCP_GATEWAY_AGENT_ID=telecom_contas
+MCP_GATEWAY_TENANT_ID=default
+
###############################################################################
# LONG-TERM MEMORY
###############################################################################
diff --git a/.env.example b/.env.example
index 98ad2e4..116eb7d 100644
--- a/.env.example
+++ b/.env.example
@@ -141,12 +141,24 @@ ROUTING_CONFIG_PATH=./config/routing.yaml
# 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=false
+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
+TOOL_POLICIES_PATH=./config/tool_policies.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
@@ -194,4 +206,4 @@ 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
-LONG_TERM_MEMORY_INJECT_CONTEXT=true
\ No newline at end of file
+LONG_TERM_MEMORY_INJECT_CONTEXT=true
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 608fc10..03a129f 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,9 +4,7 @@
-
-
-
+
@@ -47,28 +45,31 @@
- {
- "keyToString": {
- "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "ModuleVcsDetector.initialDetectionPerformed": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.git.unshallow": "true",
- "SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "git-widget-placeholder": "main",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "settings.editor.selected.configurable": "configurable.group.appearance",
- "vue.rearranger.settings.migration": "true"
+
+}]]>
-
-
+
+
@@ -83,8 +84,14 @@
-
-
+
+
+
+
+
+
+
+
@@ -110,111 +117,7 @@
1782048304579
-
-
- 1782048747421
-
-
-
- 1782048747421
-
-
-
- 1782321346355
-
-
-
- 1782321346355
-
-
-
- 1782900906490
-
-
-
- 1782900906490
-
-
-
- 1782900910793
-
-
-
- 1782900910793
-
-
-
- 1782900913579
-
-
-
- 1782900913579
-
-
-
- 1782900914906
-
-
-
- 1782900914906
-
-
-
- 1782900923834
-
-
-
- 1782900923834
-
-
-
- 1782900925229
-
-
-
- 1782900925229
-
-
-
- 1782900926577
-
-
-
- 1782900926577
-
-
-
- 1782900927457
-
-
-
- 1782900927457
-
-
-
- 1782900928320
-
-
-
- 1782900928320
-
-
-
- 1782900928490
-
-
-
- 1782900928490
-
-
-
- 1782900939364
-
-
-
- 1782900939364
-
-
+
@@ -222,8 +125,6 @@
-
-
-
+
\ No newline at end of file
diff --git a/Documentacao/Long_Term_Memory_Implementation_Guide_EN.md b/Documentacao/Long_Term_Memory_Implementation_Guide_EN.md
new file mode 100644
index 0000000..6cb2e5a
--- /dev/null
+++ b/Documentacao/Long_Term_Memory_Implementation_Guide_EN.md
@@ -0,0 +1,520 @@
+### Long-Term Memory Implementation Guide
+
+### Concept
+
+Long-Term Memory (LTM) is the `agent_framework` capability that stores and retrieves durable facts beyond the lifetime of a conversation session.
+
+Unlike message history, which is normally associated with a `session_id`, Long-Term Memory is associated with the business identity of the user or customer. In the current implementation, this identity consists of:
+
+```text
+tenant_id
+agent_id
+customer_key
+```
+
+This allows an agent to retrieve preferences, identity information, projects and constraints even when a new session is created.
+
+### Purpose
+
+Long-Term Memory is used to:
+
+- maintain continuity across sessions;
+- personalize responses;
+- prevent users from repeating previously supplied information;
+- reduce the need to send the full conversation history to the model;
+- store preferences, current projects, preferred names and constraints;
+- isolate memory across tenants, agents and customers.
+
+Example:
+
+```text
+Session A:
+"Call me Cris. My preferred language is Python."
+
+Session B, with another session_id and the same customer_key:
+"What do you remember about me?"
+
+Expected response:
+"Your preferred name is Cris and your preferred language is Python."
+```
+
+### Memory type differences
+
+#### Conversation Memory
+
+Stores messages from the current conversation and is normally associated with the `session_id`.
+
+#### Summary Memory
+
+Stores a summary of the conversation to reduce the context size sent to the model.
+
+#### Long-Term Memory
+
+Stores durable facts across sessions and is associated with the business identity, primarily the `customer_key`.
+
+### Components
+
+#### LongTermMemoryManager
+
+Coordinates:
+
+- memory loading;
+- identity-based retrieval;
+- context rendering;
+- durable fact extraction;
+- fact persistence;
+- deduplication and updates.
+
+#### LongTermMemoryStore
+
+Persistence interface used by the manager.
+
+#### SQLiteLongTermMemoryStore
+
+Reference implementation based on SQLite.
+
+It is suitable for:
+
+- local development;
+- testing;
+- demonstrations;
+- low-scale environments.
+
+#### InMemoryLongTermMemoryStore
+
+In-memory implementation used for quick tests.
+
+Its content is lost when the backend process stops.
+
+#### LongTermMemoryExtractor
+
+Identifies durable facts in messages.
+
+Examples:
+
+```text
+preferred_name = Cris
+preferred_language = Python
+current_project = Atlas
+```
+
+#### LongTermMemoryItem
+
+Data model representing a persisted item, including identity, key, value, category, confidence and metadata.
+
+#### AgentRuntime
+
+Loads memory before agent execution and injects the rendered context into the prompt.
+
+#### persist_long_term_memory node
+
+LangGraph node responsible for persisting facts after the final response has been generated and validated.
+
+### File structure
+
+```text
+libs/
+└── agent_framework/
+ └── src/
+ └── agent_framework/
+ └── memory/
+ ├── __init__.py
+ ├── long_term_extractor.py
+ ├── long_term_memory.py
+ ├── long_term_models.py
+ └── long_term_store.py
+```
+
+### Execution flow
+
+```text
+User message
+ │
+ ▼
+AgentRuntime.prepare_memory_context()
+ │
+ ├── Conversation Memory
+ ├── Summary Memory
+ └── Long-Term Memory
+ │
+ ▼
+ long_term_memory_context
+ │
+ ▼
+ Agent prompt
+ │
+ ▼
+ Agent
+ │
+ ▼
+ Guardrails / Judges / Supervisor
+ │
+ ▼
+ persist_long_term_memory
+ │
+ ▼
+ LongTermMemoryExtractor
+ │
+ ▼
+ LongTermMemoryStore
+```
+
+### Framework configuration
+
+### New modules
+
+Copy:
+
+```text
+libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
+libs/agent_framework/src/agent_framework/memory/long_term_memory.py
+libs/agent_framework/src/agent_framework/memory/long_term_models.py
+libs/agent_framework/src/agent_framework/memory/long_term_store.py
+```
+
+### Update memory/__init__.py
+
+Export the Long-Term Memory components:
+
+```python
+from agent_framework.memory.long_term_memory import (
+ LongTermMemoryManager,
+ create_long_term_memory_manager,
+)
+from agent_framework.memory.long_term_models import LongTermMemoryItem
+from agent_framework.memory.long_term_store import (
+ InMemoryLongTermMemoryStore,
+ LongTermMemoryStore,
+ SQLiteLongTermMemoryStore,
+ create_long_term_memory_store,
+)
+```
+
+### Update settings.py
+
+Add:
+
+```python
+ENABLE_LONG_TERM_MEMORY: bool = False
+LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
+LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
+LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
+LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
+LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
+LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
+LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
+```
+
+### AgentRuntime integration
+
+The runtime must:
+
+1. verify that the feature is enabled;
+2. create the manager when needed;
+3. retrieve facts using the identity;
+4. populate the workflow state;
+5. inject the rendered context into the prompt.
+
+State fields:
+
+```python
+long_term_memories: list[dict]
+long_term_memory_context: str
+long_term_memory_write_result: dict
+```
+
+### AgentWorkflow initialization
+
+Create the manager in `AgentWorkflow`:
+
+```python
+self.long_term_memory_manager = create_long_term_memory_manager(
+ settings,
+ telemetry=telemetry,
+)
+```
+
+### Correct agent initialization
+
+Do not pass `long_term_memory_manager` through `agent_kwargs` when the constructors of `BillingAgent`, `ProductAgent`, `OrdersAgent` and `SupportAgent` do not declare that parameter.
+
+This initialization causes an error:
+
+```python
+agent_kwargs = {
+ "telemetry": telemetry,
+ "settings": settings,
+ "memory": memory,
+ "summary_memory": summary_memory,
+ "long_term_memory_manager": self.long_term_memory_manager,
+}
+
+self.billing = BillingAgent(llm, **agent_kwargs)
+```
+
+Resulting error:
+
+```text
+TypeError: BillingAgent.__init__() got an unexpected keyword argument
+'long_term_memory_manager'
+```
+
+The recommended approach is to create agents using their existing signatures and inject the manager as an attribute after initialization:
+
+```python
+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)
+
+for agent in (
+ self.billing,
+ self.product,
+ self.orders,
+ self.support,
+):
+ agent.long_term_memory_manager = self.long_term_memory_manager
+```
+
+This approach avoids changing every agent constructor and keeps the feature encapsulated in the framework.
+
+### LangGraph configuration
+
+Register the node:
+
+```python
+builder.add_node(
+ "persist_long_term_memory",
+ self._node(
+ "persist_long_term_memory",
+ self.persist_long_term_memory,
+ ),
+)
+```
+
+Update the edges:
+
+```python
+builder.add_edge(
+ "supervisor_review",
+ "persist_long_term_memory",
+)
+builder.add_edge(
+ "persist_long_term_memory",
+ "persist",
+)
+```
+
+Implement:
+
+```python
+async def persist_long_term_memory(
+ self,
+ state: AgentState,
+) -> dict[str, object]:
+ result = await self.long_term_memory_manager.persist_turn(state)
+
+ return {
+ "long_term_memory_write_result": result,
+ }
+```
+
+Final flow:
+
+```text
+supervisor_review
+ │
+ ▼
+persist_long_term_memory
+ │
+ ▼
+persist
+```
+
+### Environment variables
+
+```env
+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
+
+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
+```
+
+### SQLite database path
+
+A relative path is resolved from the directory in which the backend is started.
+
+To prevent different databases from being created accidentally, prefer an absolute path in development environments:
+
+```env
+LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
+```
+
+Create the directory before starting:
+
+```bash
+mkdir -p data
+```
+
+### Testing
+
+### Test 1 — Persistence
+
+Send:
+
+```json
+{
+ "session_id": "default:telecom_contas:memory-session-a",
+ "customer_key": "11999999999",
+ "message": "Call me Cris. My preferred language is Python and my current project is Atlas."
+}
+```
+
+### Test 2 — Retrieval in another session
+
+Use another `session_id` while keeping the same `customer_key`:
+
+```json
+{
+ "session_id": "default:telecom_contas:memory-session-b",
+ "customer_key": "11999999999",
+ "message": "What do you remember about me, my preferences and my project?"
+}
+```
+
+Expected result:
+
+```text
+Your preferred name is Cris.
+Your preferred language is Python.
+Your current project is Atlas.
+```
+
+### Test 3 — Isolation
+
+Use another customer:
+
+```json
+{
+ "session_id": "default:telecom_contas:memory-session-c",
+ "customer_key": "another-customer",
+ "message": "What is my preferred name and current project?"
+}
+```
+
+The data associated with `11999999999` must not be returned.
+
+### Test 4 — Frontend reset
+
+Restart or reset the frontend and verify that it still sends the same `customer_key`.
+
+Memory must survive a `session_id` change. Resetting the frontend does not delete the SQLite database.
+
+### Test 5 — Backend restart
+
+Restart Uvicorn and repeat the query.
+
+With:
+
+```env
+LONG_TERM_MEMORY_PROVIDER=sqlite
+```
+
+memory must remain available.
+
+With:
+
+```env
+LONG_TERM_MEMORY_PROVIDER=memory
+```
+
+memory is lost when the process stops.
+
+### Direct SQLite verification
+
+Find the database:
+
+```bash
+find . -name "agent_framework.db" -type f
+```
+
+Open it:
+
+```bash
+sqlite3 ./data/agent_framework.db
+```
+
+Query:
+
+```sql
+SELECT
+ tenant_id,
+ agent_id,
+ customer_key,
+ memory_type,
+ memory_key,
+ memory_value,
+ confidence,
+ created_at,
+ updated_at
+FROM agentfw_long_term_memory
+ORDER BY updated_at DESC;
+```
+
+### Success criteria
+
+The implementation is working when:
+
+- memory is retrieved with another `session_id`;
+- the same `customer_key` retrieves previous facts;
+- another `customer_key` cannot access those facts;
+- restarting the frontend does not erase memory;
+- restarting the backend does not erase memory when using SQLite;
+- the `persist_long_term_memory` node runs;
+- the prompt receives `long_term_memory_context`.
+
+### Best practices
+
+- Persist only durable facts.
+- Do not store the complete conversation as Long-Term Memory.
+- Isolate data by `tenant_id`, `agent_id` and `customer_key`.
+- Do not use `session_id` as the permanent user identity.
+- Persist only after final validations.
+- Avoid persisting temporary tool results.
+- Record telemetry for reads, writes, updates and failures.
+- Define retention and deletion policies.
+- Use an absolute SQLite path in environments with multiple working directories.
+- Move to an enterprise database for production and high-availability environments.
+
+### Reference implementation limitations
+
+The current implementation uses rule-based extraction and SQLite as the reference provider.
+
+Recommended future enhancements:
+
+- LLM-based fact extraction;
+- vector-based semantic memory;
+- episodic memory;
+- expiration and versioning;
+- semantic deduplication;
+- consent policies;
+- query and deletion APIs;
+- Oracle Autonomous Database provider;
+- encryption and sensitive-data classification.
diff --git a/Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md b/Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md
new file mode 100644
index 0000000..a9f36d0
--- /dev/null
+++ b/Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md
@@ -0,0 +1,272 @@
+# Agent Platform OCI — Manual Oficial de Agent Gateway e MCP Gateway
+
+## Objetivo
+
+Este documento consolida:
+- Arquitetura oficial
+- Inventário dos componentes
+- Procedimento completo de execução local
+- MCP Gateway
+- Agent Gateway
+- Backend Runtime
+- Frontend
+- Testes E2E
+- Troubleshooting
+- Decisões arquiteturais
+
+---
+
+# Arquitetura Oficial
+
+Frontend (5173)
+↓
+Agent Gateway (9000)
+↓
+Agent Template Backend / Runtime (8000)
+↓
+MCP Gateway (8300)
+↓
+Telecom MCP Server (8100)
+Retail MCP Server (8200)
+
+---
+
+# Portas Oficiais
+
+| Componente | Porta |
+|------------|--------|
+| Frontend | 5173 |
+| Agent Gateway | 9000 |
+| Backend Runtime | 8000 |
+| MCP Gateway | 8300 |
+| Telecom MCP Server | 8100 |
+| Retail MCP Server | 8200 |
+
+---
+
+# Variáveis Oficiais
+
+## Agent Template Backend
+
+ENABLE_MCP_TOOLS=true
+
+MCP_GATEWAY_ENABLED=true
+MCP_GATEWAY_URL=http://localhost:8300
+MCP_GATEWAY_TIMEOUT_SECONDS=60
+MCP_GATEWAY_AGENT_ID=telecom_contas
+MCP_GATEWAY_TENANT_ID=default
+
+## Agent Gateway
+
+DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
+AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
+
+## MCP Gateway
+
+MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
+
+---
+
+# Ordem de Inicialização
+
+1. Telecom MCP Server
+2. Retail MCP Server
+3. MCP Gateway
+4. Agent Template Backend
+5. Agent Gateway
+6. Frontend
+
+---
+
+# Terminal 1 — Telecom MCP Server
+
+cd mcp/servers/telecom_mcp_server
+
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+
+python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
+
+Validação:
+
+curl http://localhost:8100/health
+
+---
+
+# Terminal 2 — Retail MCP Server
+
+cd mcp/servers/retail_mcp_server
+
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+
+python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
+
+Validação:
+
+curl http://localhost:8200/health
+
+---
+
+# Terminal 3 — MCP Gateway
+
+cd apps/mcp_gateway
+
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+
+export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
+
+python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
+
+Validações:
+
+curl http://localhost:8300/health
+curl http://localhost:8300/ready
+curl http://localhost:8300/v1/tools
+
+Teste:
+
+curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke
+
+---
+
+# Terminal 4 — Agent Template Backend
+
+cd templates/agent_template_backend
+
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+
+python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
+
+Validações:
+
+curl http://localhost:8000/health
+curl http://localhost:8000/agents
+
+---
+
+# Terminal 5 — Agent Gateway
+
+cd apps/agent_gateway
+
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+
+export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
+export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
+
+python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
+
+Validações:
+
+curl http://localhost:9000/health
+
+Teste:
+
+curl -X POST http://localhost:9000/gateway/message
+
+---
+
+# Terminal 6 — Frontend
+
+cd agent_frontend
+
+npm install
+
+npm run dev -- --host 0.0.0.0 --port 5173
+
+Abrir:
+
+http://localhost:5173
+
+Backend URL:
+
+http://localhost:9000
+
+---
+
+# Fluxo de Tools
+
+Agent
+↓
+MCPToolRouter
+↓
+MCPGatewayClient
+↓
+MCP Gateway
+↓
+MCP Server
+
+---
+
+# Teste Integrado E2E
+
+Frontend
+↓
+Agent Gateway
+↓
+Backend Runtime
+↓
+MCP Gateway
+↓
+Telecom MCP Server
+
+Resultado esperado:
+
+- Agent Gateway recebe requisição
+- Runtime executa LangGraph
+- MCP Gateway resolve tool
+- MCP Server responde
+- Usuário recebe resposta
+
+---
+
+# Troubleshooting
+
+## Backend chamando MCP Server direto
+
+Confirmar:
+
+MCP_GATEWAY_ENABLED=true
+
+MCP_GATEWAY_URL=http://localhost:8300
+
+## Porta incorreta
+
+A porta oficial do MCP Gateway é:
+
+8300
+
+## Agent Gateway não encontra Backend
+
+Validar:
+
+curl http://localhost:8000/health
+
+## MCP Gateway não encontra MCP Server
+
+Validar:
+
+curl http://localhost:8100/health
+curl http://localhost:8200/health
+
+---
+
+# Decisões Arquiteturais Oficiais
+
+- Agent Gateway centraliza governança
+- Runtime executa LangGraph
+- Runtime executa LLM
+- MCP Gateway centraliza tools
+- MCP Servers executam tools
+- Backend usa MCP Gateway
+- gateway_runtime.env.example foi removido
+- MCP_GATEWAY_* fica no .env do backend
+- Porta oficial MCP Gateway = 8300
diff --git a/Documentacao/Manual_Desenvolvedor_AI_Agent_Framework_OCI.docx b/Documentacao/Manual_Desenvolvedor_AI_Agent_Framework_OCI.docx
index 1fdfdd4..22eac01 100644
Binary files a/Documentacao/Manual_Desenvolvedor_AI_Agent_Framework_OCI.docx and b/Documentacao/Manual_Desenvolvedor_AI_Agent_Framework_OCI.docx differ
diff --git a/Documentacao/Manual_Integracao_MCP_Servers_Agent_Framework.docx b/Documentacao/Manual_Integracao_MCP_Servers_Agent_Framework.docx
index d45e2bb..1294890 100644
Binary files a/Documentacao/Manual_Integracao_MCP_Servers_Agent_Framework.docx and b/Documentacao/Manual_Integracao_MCP_Servers_Agent_Framework.docx differ
diff --git a/Documentacao/Manual_Long_Term_Memory_PT.md b/Documentacao/Manual_Long_Term_Memory_PT.md
new file mode 100644
index 0000000..462a401
--- /dev/null
+++ b/Documentacao/Manual_Long_Term_Memory_PT.md
@@ -0,0 +1,520 @@
+### Manual de Implementação — Long-Term Memory
+
+### Conceito
+
+A Long-Term Memory (LTM) é a capacidade do `agent_framework` de armazenar e recuperar fatos duradouros além da duração de uma sessão de conversa.
+
+Diferentemente do histórico de mensagens, que normalmente está associado a um `session_id`, a memória de longo prazo é associada à identidade de negócio do usuário ou cliente. Na implementação atual, essa identidade é composta por:
+
+```text
+tenant_id
+agent_id
+customer_key
+```
+
+Isso permite que um agente recupere preferências, informações de identidade, projetos e restrições mesmo quando uma nova sessão é criada.
+
+### Para que serve
+
+A Long-Term Memory serve para:
+
+- manter continuidade entre sessões;
+- personalizar respostas;
+- evitar que o usuário repita informações já fornecidas;
+- reduzir a necessidade de enviar todo o histórico ao modelo;
+- armazenar preferências, projetos atuais, nomes preferidos e restrições;
+- isolar a memória entre tenants, agentes e clientes.
+
+Exemplo:
+
+```text
+Sessão A:
+"Me chame de Cris. Minha linguagem preferida é Python."
+
+Sessão B, com outro session_id e o mesmo customer_key:
+"O que você lembra sobre mim?"
+
+Resposta esperada:
+"Seu nome preferido é Cris e sua linguagem preferida é Python."
+```
+
+### Diferença entre os tipos de memória
+
+#### Conversation Memory
+
+Mantém as mensagens da conversa atual e normalmente está associada ao `session_id`.
+
+#### Summary Memory
+
+Mantém um resumo da conversa para reduzir o tamanho do contexto enviado ao modelo.
+
+#### Long-Term Memory
+
+Mantém fatos duradouros entre sessões e é associada à identidade de negócio, principalmente ao `customer_key`.
+
+### Componentes da funcionalidade
+
+#### LongTermMemoryManager
+
+Responsável por coordenar:
+
+- carregamento das memórias;
+- recuperação por identidade;
+- renderização do contexto;
+- extração de novos fatos;
+- persistência dos fatos;
+- deduplicação e atualização.
+
+#### LongTermMemoryStore
+
+Interface de persistência utilizada pelo manager.
+
+#### SQLiteLongTermMemoryStore
+
+Implementação de referência baseada em SQLite.
+
+É apropriada para:
+
+- desenvolvimento local;
+- testes;
+- demonstrações;
+- ambientes de baixa escala.
+
+#### InMemoryLongTermMemoryStore
+
+Implementação em memória utilizada para testes rápidos.
+
+O conteúdo é perdido quando o processo do backend é encerrado.
+
+#### LongTermMemoryExtractor
+
+Responsável por identificar fatos duradouros nas mensagens.
+
+Exemplos de fatos:
+
+```text
+preferred_name = Cris
+preferred_language = Python
+current_project = Atlas
+```
+
+#### LongTermMemoryItem
+
+Modelo que representa um item persistido, incluindo identidade, chave, valor, categoria, confiança e metadados.
+
+#### AgentRuntime
+
+Carrega a memória antes da execução do agente e injeta o contexto no prompt.
+
+#### Nó persist_long_term_memory
+
+Nó do LangGraph responsável por persistir os fatos após a geração e validação da resposta final.
+
+### Estrutura dos arquivos
+
+```text
+libs/
+└── agent_framework/
+ └── src/
+ └── agent_framework/
+ └── memory/
+ ├── __init__.py
+ ├── long_term_extractor.py
+ ├── long_term_memory.py
+ ├── long_term_models.py
+ └── long_term_store.py
+```
+
+### Fluxo de execução
+
+```text
+Mensagem do usuário
+ │
+ ▼
+AgentRuntime.prepare_memory_context()
+ │
+ ├── Conversation Memory
+ ├── Summary Memory
+ └── Long-Term Memory
+ │
+ ▼
+ long_term_memory_context
+ │
+ ▼
+ Prompt do agente
+ │
+ ▼
+ Agente
+ │
+ ▼
+ Guardrails / Judges / Supervisor
+ │
+ ▼
+ persist_long_term_memory
+ │
+ ▼
+ LongTermMemoryExtractor
+ │
+ ▼
+ LongTermMemoryStore
+```
+
+### Configuração do framework
+
+### Novos módulos
+
+Copie os arquivos:
+
+```text
+libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
+libs/agent_framework/src/agent_framework/memory/long_term_memory.py
+libs/agent_framework/src/agent_framework/memory/long_term_models.py
+libs/agent_framework/src/agent_framework/memory/long_term_store.py
+```
+
+### Atualização de memory/__init__.py
+
+Exporte os componentes da Long-Term Memory:
+
+```python
+from agent_framework.memory.long_term_memory import (
+ LongTermMemoryManager,
+ create_long_term_memory_manager,
+)
+from agent_framework.memory.long_term_models import LongTermMemoryItem
+from agent_framework.memory.long_term_store import (
+ InMemoryLongTermMemoryStore,
+ LongTermMemoryStore,
+ SQLiteLongTermMemoryStore,
+ create_long_term_memory_store,
+)
+```
+
+### Atualização de settings.py
+
+Adicione as configurações:
+
+```python
+ENABLE_LONG_TERM_MEMORY: bool = False
+LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
+LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
+LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
+LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
+LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
+LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
+LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
+```
+
+### Integração com AgentRuntime
+
+O runtime deve:
+
+1. verificar se a funcionalidade está habilitada;
+2. criar o manager quando necessário;
+3. recuperar os fatos pela identidade;
+4. preencher o estado;
+5. injetar o contexto no prompt.
+
+Campos adicionados ao estado:
+
+```python
+long_term_memories: list[dict]
+long_term_memory_context: str
+long_term_memory_write_result: dict
+```
+
+### Inicialização no AgentWorkflow
+
+O manager deve ser criado no `AgentWorkflow`:
+
+```python
+self.long_term_memory_manager = create_long_term_memory_manager(
+ settings,
+ telemetry=telemetry,
+)
+```
+
+### Inicialização correta dos agentes
+
+O `long_term_memory_manager` não deve ser passado pelo `agent_kwargs` caso os construtores de `BillingAgent`, `ProductAgent`, `OrdersAgent` e `SupportAgent` não declarem esse parâmetro.
+
+Esta inicialização causa erro:
+
+```python
+agent_kwargs = {
+ "telemetry": telemetry,
+ "settings": settings,
+ "memory": memory,
+ "summary_memory": summary_memory,
+ "long_term_memory_manager": self.long_term_memory_manager,
+}
+
+self.billing = BillingAgent(llm, **agent_kwargs)
+```
+
+Erro resultante:
+
+```text
+TypeError: BillingAgent.__init__() got an unexpected keyword argument
+'long_term_memory_manager'
+```
+
+A forma recomendada é criar os agentes com a assinatura já existente e injetar o manager como atributo após a inicialização:
+
+```python
+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)
+
+for agent in (
+ self.billing,
+ self.product,
+ self.orders,
+ self.support,
+):
+ agent.long_term_memory_manager = self.long_term_memory_manager
+```
+
+Essa abordagem evita alterar os construtores de todos os agentes e mantém a funcionalidade encapsulada no framework.
+
+### Configuração do LangGraph
+
+Registre o nó:
+
+```python
+builder.add_node(
+ "persist_long_term_memory",
+ self._node(
+ "persist_long_term_memory",
+ self.persist_long_term_memory,
+ ),
+)
+```
+
+Altere o fluxo:
+
+```python
+builder.add_edge(
+ "supervisor_review",
+ "persist_long_term_memory",
+)
+builder.add_edge(
+ "persist_long_term_memory",
+ "persist",
+)
+```
+
+Implemente o método:
+
+```python
+async def persist_long_term_memory(
+ self,
+ state: AgentState,
+) -> dict[str, object]:
+ result = await self.long_term_memory_manager.persist_turn(state)
+
+ return {
+ "long_term_memory_write_result": result,
+ }
+```
+
+Fluxo final:
+
+```text
+supervisor_review
+ │
+ ▼
+persist_long_term_memory
+ │
+ ▼
+persist
+```
+
+### Variáveis de ambiente
+
+```env
+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
+
+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
+```
+
+### Caminho do banco SQLite
+
+O caminho relativo é resolvido a partir do diretório em que o backend é iniciado.
+
+Para evitar que bancos diferentes sejam criados acidentalmente, prefira um caminho absoluto em ambientes de desenvolvimento:
+
+```env
+LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
+```
+
+Crie a pasta antes de iniciar:
+
+```bash
+mkdir -p data
+```
+
+### Como testar
+
+### Teste 1 — Gravação
+
+Envie:
+
+```json
+{
+ "session_id": "default:telecom_contas:memory-session-a",
+ "customer_key": "11999999999",
+ "message": "Me chame de Cris. Minha linguagem preferida é Python e meu projeto atual se chama Atlas."
+}
+```
+
+### Teste 2 — Recuperação em outra sessão
+
+Utilize outro `session_id`, mantendo o mesmo `customer_key`:
+
+```json
+{
+ "session_id": "default:telecom_contas:memory-session-b",
+ "customer_key": "11999999999",
+ "message": "O que você lembra sobre mim, minhas preferências e meu projeto?"
+}
+```
+
+Resultado esperado:
+
+```text
+Seu nome preferido é Cris.
+Sua linguagem preferida é Python.
+Seu projeto atual se chama Atlas.
+```
+
+### Teste 3 — Isolamento
+
+Utilize outro cliente:
+
+```json
+{
+ "session_id": "default:telecom_contas:memory-session-c",
+ "customer_key": "outro-cliente",
+ "message": "Qual é meu nome preferido e qual é meu projeto atual?"
+}
+```
+
+Os dados de `11999999999` não devem aparecer.
+
+### Teste 4 — Reinicialização do frontend
+
+Reinicie ou resete o frontend e confirme que ele continua enviando o mesmo `customer_key`.
+
+A memória deve sobreviver à troca do `session_id`. O reset do frontend não apaga o SQLite.
+
+### Teste 5 — Reinicialização do backend
+
+Reinicie o Uvicorn e repita a consulta.
+
+Com:
+
+```env
+LONG_TERM_MEMORY_PROVIDER=sqlite
+```
+
+a memória deve continuar disponível.
+
+Com:
+
+```env
+LONG_TERM_MEMORY_PROVIDER=memory
+```
+
+a memória será perdida quando o processo for encerrado.
+
+### Verificação direta no SQLite
+
+Localize o banco:
+
+```bash
+find . -name "agent_framework.db" -type f
+```
+
+Abra:
+
+```bash
+sqlite3 ./data/agent_framework.db
+```
+
+Consulte:
+
+```sql
+SELECT
+ tenant_id,
+ agent_id,
+ customer_key,
+ memory_type,
+ memory_key,
+ memory_value,
+ confidence,
+ created_at,
+ updated_at
+FROM agentfw_long_term_memory
+ORDER BY updated_at DESC;
+```
+
+### Critérios de sucesso
+
+A implementação está funcionando quando:
+
+- a memória é recuperada com outro `session_id`;
+- o mesmo `customer_key` recupera os fatos anteriores;
+- outro `customer_key` não acessa esses fatos;
+- reiniciar o frontend não apaga a memória;
+- reiniciar o backend não apaga a memória quando o provider é SQLite;
+- o nó `persist_long_term_memory` é executado;
+- o prompt recebe `long_term_memory_context`.
+
+### Boas práticas
+
+- Persistir somente fatos duradouros.
+- Não armazenar a conversa completa como Long-Term Memory.
+- Isolar dados por `tenant_id`, `agent_id` e `customer_key`.
+- Não utilizar `session_id` como identidade permanente do usuário.
+- Persistir somente depois das validações finais.
+- Evitar armazenar resultados temporários de ferramentas.
+- Registrar telemetria de leitura, escrita, atualização e falha.
+- Definir políticas de retenção e exclusão.
+- Usar caminho absoluto para SQLite em ambientes com múltiplos diretórios de execução.
+- Migrar para um banco corporativo em ambientes de produção e alta disponibilidade.
+
+### Limitações da implementação de referência
+
+A implementação atual utiliza extração baseada em regras e SQLite como provider de referência.
+
+Evoluções recomendadas:
+
+- extração de fatos com LLM;
+- memória semântica com vetores;
+- memória episódica;
+- expiração e versionamento;
+- deduplicação semântica;
+- política de consentimento;
+- API de consulta e exclusão;
+- provider Oracle Autonomous Database;
+- criptografia e classificação de dados sensíveis.
diff --git a/Documentacao/README_MCP.md b/Documentacao/README_MCP.md
index 40eeef7..5397002 100644
--- a/Documentacao/README_MCP.md
+++ b/Documentacao/README_MCP.md
@@ -73,3 +73,7 @@ docker compose up --build
```
No compose, o backend usa `config/mcp_servers.docker.yaml` para apontar para `telecom-mcp` e `retail-mcp`.
+
+## Operações read-only e transacionais
+
+Use `config/tool_policies.yaml` no backend para classificar somente as operações que precisam de tratamento adicional. A validação é aplicada no roteador central antes do MCP Gateway/Server. O arquivo é opcional e templates antigos continuam usando as políticas já presentes em `tools.yaml`. A configuração completa e o roteiro de migração estão em [README_TOOL_POLICIES.md](README_TOOL_POLICIES.md).
diff --git a/Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md b/Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md
new file mode 100644
index 0000000..89379c0
--- /dev/null
+++ b/Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md
@@ -0,0 +1,244 @@
+# Route Stickiness Semântica e Controle Global de Sessão no Agent Framework OCI
+
+## Objetivo
+
+A route stickiness semântica evita executar novamente o Enterprise Router quando uma nova mensagem continua claramente sob responsabilidade do agente ativo. A implementação usa um perfil LLM leve e não contém regexes, listas de frases, palavras específicas de idioma ou regras conversacionais por domínio.
+
+A funcionalidade é opcional e preserva integralmente o comportamento anterior quando desabilitada, quando não existe agente ativo, quando a confiança é baixa ou quando ocorre erro na inferência.
+
+## Decisão arquitetural
+
+O classificador possui uma responsabilidade transversal e restrita:
+
+- `CONTINUE`: a mensagem continua com o agente ativo;
+- `ROUTE`: a mensagem deve seguir para o Enterprise Router normal;
+- `HUMAN_HANDOFF`: o usuário solicitou atendimento humano;
+- `END_SESSION`: o usuário solicitou ou confirmou o encerramento do atendimento.
+
+Ele não responde ao usuário, não escolhe outro agente, não executa ferramentas e não interpreta regras de negócio. As duas ações globais são encaminhadas para nós próprios do grafo, evitando que cada agente implemente prompts ou regras de sessão.
+
+Fluxo:
+
+```text
+Todos os turnos com a funcionalidade habilitada
+ -> classificador semântico leve
+ CONTINUE + agente ativo -> agente ativo
+ ROUTE/baixa confiança/erro -> Enterprise Router
+ HUMAN_HANDOFF -> nó global human_handoff
+ END_SESSION -> nó global end_session
+
+No primeiro turno, CONTINUE é normalizado para ROUTE porque ainda não existe agente ativo. Handoff e encerramento podem ser reconhecidos mesmo no primeiro turno.
+```
+
+## Por que não há regras determinísticas
+
+A interpretação de linguagem natural por regex exige manutenção contínua para novas construções, idiomas e domínios. Além disso, transfere aos times dos agentes a responsabilidade de manter flags e padrões de continuidade.
+
+Esta implementação mantém no código apenas decisões técnicas inevitáveis:
+
+- funcionalidade habilitada ou desabilitada;
+- validação de que `CONTINUE` exige agente ativo;
+- threshold de confiança;
+- fallback em timeout, erro ou JSON inválido.
+
+Não existem `DEFAULT_FOLLOWUP_PATTERNS`, regras de repetição, listas de pronomes ou keywords de continuidade.
+
+## Configuração
+
+### `.env`
+
+```dotenv
+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.
+```
+
+- `ENABLE_ROUTE_STICKINESS`: ativa a capacidade.
+- `ROUTE_STICKINESS_LLM_PROFILE`: perfil existente em `llm_profiles.yaml`.
+- `ROUTE_STICKINESS_CONFIDENCE_THRESHOLD`: confiança mínima para bypass.
+- `ROUTE_STICKINESS_HISTORY_TURNS`: quantidade de turnos recentes enviados ao classificador.
+- `ROUTE_STICKINESS_MAX_TOKENS`: limite de saída do classificador.
+- `HUMAN_HANDOFF_MESSAGE`: mensagem devolvida pelo nó global de transferência humana.
+- `END_SESSION_MESSAGE`: mensagem devolvida pelo nó global de encerramento.
+
+### Perfil leve
+
+```yaml
+profiles:
+ route_continuity:
+ provider: oci_openai
+ model: openai.gpt-4.1-mini
+ temperature: 0
+ max_tokens: 80
+ timeout_seconds: 5
+```
+
+O modelo acima é apenas um exemplo. Deve ser substituído pelo menor modelo aprovado e disponível no ambiente OCI. O framework reutiliza o mecanismo já existente de `LLM_PROFILES_PATH`; não há uma segunda configuração de provider/model específica para a funcionalidade.
+
+## Contexto enviado ao modelo
+
+O classificador recebe somente:
+
+- agente ativo;
+- descrições das capacidades dos agentes derivadas das intents já existentes;
+- intent e domínio anteriores;
+- histórico recente limitado;
+- mensagem atual.
+
+Não são enviados RAG completo, resultados MCP integrais, prompt do agente ou regras de negócio.
+
+## Exemplos
+
+### Continuidade
+
+```text
+Usuário: Qual é o meu plano?
+Agente: Seu plano é Controle 50GB.
+Usuário: O que está incluso?
+```
+
+Resultado esperado:
+
+```json
+{
+ "method": "continuity",
+ "route": "product_agent",
+ "route_bypassed": true
+}
+```
+
+### Mudança de domínio
+
+```text
+Usuário: Qual é o meu plano?
+Agente: Seu plano é Controle 50GB.
+Usuário: Agora quero contestar uma cobrança.
+```
+
+O classificador retorna `ROUTE` e o Enterprise Router seleciona o agente apropriado.
+
+### Baixa confiança ou falha
+
+Qualquer resultado abaixo do threshold, timeout ou JSON inválido executa o Enterprise Router. A funcionalidade é fail-safe e nunca força continuidade em caso de dúvida.
+
+## Telemetria
+
+Evento `router.continuity`:
+
+```json
+{
+ "decision": "CONTINUE",
+ "confidence": 0.97,
+ "active_agent": "product_agent",
+ "route_bypassed": true,
+ "profile_name": "route_continuity"
+}
+```
+
+Quando ocorre bypass, `route_decision.method` é `continuity` e o estado final contém:
+
+- `active_agent`;
+- `route_bypassed`;
+- `continuity_signal`.
+
+## Testes
+
+```bash
+pytest -q tests/unit/test_semantic_route_stickiness.py
+```
+
+Os testes validam:
+
+- continuidade com bypass;
+- mudança de assunto com fallback para o router;
+- baixa confiança;
+- saída inválida;
+- primeiro turno sem chamada ao classificador.
+
+## Benchmark recomendado
+
+Executar a mesma conversação com a funcionalidade desabilitada e habilitada, registrando por turno:
+
+- `route_bypassed`;
+- `route_decision.method`;
+- latência do `llm.route_continuity`;
+- chamadas ao `llm.router`;
+- tokens por perfil;
+- latência total p50, p95 e p99.
+
+A redução de tempo total somente deve ser atribuída à stickiness quando houver `route_bypassed=true` e ausência da geração `llm.router` no mesmo turno.
+
+
+## Contratos globais
+
+### Human handoff
+
+Quando a decisão for `HUMAN_HANDOFF`, o router retorna:
+
+```json
+{
+ "route": "human_handoff",
+ "intent": "human_handoff",
+ "method": "continuity",
+ "handoff": true,
+ "metadata": {
+ "session_control": "HUMAN_HANDOFF",
+ "route_bypassed": true
+ }
+}
+```
+
+O nó `human_handoff` produz os campos:
+
+- `session_control=HUMAN_HANDOFF`;
+- `human_handoff_requested=true`;
+- `session_ended=false`;
+- `next_state=HUMAN_HANDOFF_REQUESTED`.
+
+O evento `session.human_handoff.requested` é emitido para que o Channel Gateway ou a integração do cliente encaminhe a conversa à plataforma humana. O framework não presume uma fila, fornecedor ou protocolo específico.
+
+### Encerramento
+
+Quando a decisão for `END_SESSION`, o router retorna:
+
+```json
+{
+ "route": "end_session",
+ "intent": "end_session",
+ "method": "continuity",
+ "metadata": {
+ "session_control": "END_SESSION",
+ "route_bypassed": true
+ }
+}
+```
+
+O nó `end_session` produz:
+
+- `session_control=END_SESSION`;
+- `session_ended=true`;
+- `human_handoff_requested=false`;
+- `next_state=SESSION_ENDED`.
+
+O evento `session.end.requested` é emitido antes da persistência. O backend continua responsável por aplicar a política concreta de expiração, fechamento ou limpeza da sessão em cada canal.
+
+## Exemplos
+
+| Mensagem | Contexto | Decisão esperada | Destino |
+|---|---|---|---|
+| `o que está incluso?` | `product_agent` ativo | `CONTINUE` | `product_agent` |
+| `agora quero contestar uma cobrança` | `product_agent` ativo | `ROUTE` | Enterprise Router |
+| `quero falar com uma pessoa` | com ou sem agente ativo | `HUMAN_HANDOFF` | nó `human_handoff` |
+| `obrigado, pode encerrar` | com ou sem agente ativo | `END_SESSION` | nó `end_session` |
+
+## Segurança e fallback
+
+- Somente decisões acima do threshold são aceitas.
+- `CONTINUE` sem agente ativo vira `ROUTE`.
+- JSON inválido, timeout ou erro usa o Enterprise Router.
+- Handoff e encerramento não executam agentes de domínio nem ferramentas MCP.
+- O classificador não encerra fisicamente conexões nem seleciona filas humanas; ele emite um contrato global para integração.
diff --git a/Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md b/Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md
new file mode 100644
index 0000000..fdfd4d8
--- /dev/null
+++ b/Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md
@@ -0,0 +1,86 @@
+# Semantic Route Stickiness and Global Session Control in Agent Framework OCI
+
+## Purpose
+
+This optional capability uses a lightweight LLM profile and no regex, phrase lists, or domain-specific language rules. It classifies each turn as:
+
+- `CONTINUE`: keep the active agent;
+- `ROUTE`: run the regular Enterprise Router;
+- `HUMAN_HANDOFF`: request human assistance;
+- `END_SESSION`: finish the automated session.
+
+The classifier does not answer the user, execute tools, or implement domain rules. Human handoff and session ending are handled by global graph nodes.
+
+## Flow
+
+```text
+Incoming turn
+ -> lightweight semantic classifier
+ CONTINUE + active agent -> active agent
+ ROUTE / low confidence / error -> Enterprise Router
+ HUMAN_HANDOFF -> human_handoff node
+ END_SESSION -> end_session node
+```
+
+`CONTINUE` is converted to `ROUTE` when there is no active agent. Global session actions can be detected on the first turn.
+
+## Configuration
+
+```dotenv
+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=I will transfer your request to a person.
+END_SESSION_MESSAGE=The session has ended. Thank you for contacting us.
+```
+
+```yaml
+profiles:
+ route_continuity:
+ provider: oci_openai
+ model: openai.gpt-4.1-mini
+ temperature: 0
+ max_tokens: 80
+ timeout_seconds: 5
+```
+
+Use the smallest approved model available in the target OCI environment.
+
+## Human handoff contract
+
+The router returns route `human_handoff`, intent `human_handoff`, `handoff=true`, and metadata `session_control=HUMAN_HANDOFF`. The graph node sets:
+
+- `human_handoff_requested=true`;
+- `session_ended=false`;
+- `next_state=HUMAN_HANDOFF_REQUESTED`.
+
+It emits `session.human_handoff.requested`. The customer integration remains responsible for choosing the human queue and protocol.
+
+## End-session contract
+
+The router returns route `end_session`, intent `end_session`, and metadata `session_control=END_SESSION`. The graph node sets:
+
+- `session_ended=true`;
+- `human_handoff_requested=false`;
+- `next_state=SESSION_ENDED`.
+
+It emits `session.end.requested`. Channel-specific session expiration or connection closing remains an integration responsibility.
+
+## Safety behavior
+
+- Only decisions above the configured confidence threshold are accepted.
+- Invalid JSON, timeout, low confidence, or errors fall back to the Enterprise Router.
+- Human handoff and session ending do not execute domain agents or MCP tools.
+- The classifier never selects a human queue and never physically closes a channel connection.
+
+## Tests
+
+Run:
+
+```bash
+PYTHONPATH=libs/agent_framework/src pytest -q tests/unit/test_semantic_route_stickiness.py
+```
+
+The suite covers CONTINUE, ROUTE, low confidence, invalid output, HUMAN_HANDOFF, END_SESSION, first-turn global actions, and CONTINUE without an active agent.
diff --git a/Documentacao/README_TOOL_POLICIES.md b/Documentacao/README_TOOL_POLICIES.md
new file mode 100644
index 0000000..9dd6c7c
--- /dev/null
+++ b/Documentacao/README_TOOL_POLICIES.md
@@ -0,0 +1,90 @@
+# Políticas mínimas para tools MCP read-only e transacionais
+
+## Objetivo
+
+O framework diferencia operações de consulta (`read_only`) e operações que alteram estado (`transactional`) imediatamente antes da chamada MCP. Essa classificação não substitui autorização, idempotência ou regras de negócio do servidor MCP; ela acrescenta somente a proteção conversacional mínima, especialmente confirmação explícita.
+
+## Onde configurar
+
+A parametrização pertence ao backend da aplicação:
+
+```text
+templates/agent_template_backend/config/tool_policies.yaml
+```
+
+A biblioteca compartilhada contém apenas o loader e a validação. O caminho é opcional:
+
+```dotenv
+TOOL_POLICIES_PATH=./config/tool_policies.yaml
+```
+
+## Exemplo
+
+```yaml
+version: 1
+
+defaults:
+ operation_type: read_only
+ require_confirmation: false
+
+tool_policies:
+ consultar_plano:
+ operation_type: read_only
+
+ alterar_plano:
+ operation_type: transactional
+ require_confirmation: true
+ requires: [new_plan_id]
+```
+
+Para executar `alterar_plano`, os argumentos precisam conter `new_plan_id` e um booleano literal de confirmação:
+
+```json
+{"new_plan_id": "CONTROLE_100", "confirmed": true}
+```
+
+Também é aceito `"confirmation": true`. Strings como `"true"` não são aceitas como confirmação.
+
+## Compatibilidade
+
+- Se `tool_policies.yaml` não existir, o framework continua usando `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
+- Tools antigas sem política continuam executando como antes.
+- Uma política explícita no arquivo novo prevalece para `operation_type` e confirmação daquela tool.
+- O catálogo `tools.yaml` continua sendo a fonte de endpoint, schema, habilitação e cache.
+- O novo arquivo não deve ser colocado em `libs/agent_framework`, pois as decisões variam por aplicação e domínio.
+
+## Fluxo de execução
+
+```text
+agente -> MCPToolRouter -> validação da política -> mapeamento de parâmetros -> MCP Gateway/Server
+```
+
+Uma chamada bloqueada retorna `ok=false`, `metadata.blocked_by_policy=true`, o tipo da operação e a origem da política. O servidor MCP permanece a autoridade final para autenticação, autorização, validação, idempotência e transação de negócio.
+
+## Migração recomendada
+
+1. Atualize a biblioteca sem criar o arquivo: o comportamento permanece legado.
+2. Crie `config/tool_policies.yaml` no backend.
+3. Cadastre primeiro apenas operações transacionais que exigem confirmação.
+4. Teste chamadas sem confirmação, com confirmação booleana e com campos obrigatórios ausentes.
+5. Remova gradualmente duplicações de confirmação de `tools.yaml` quando todos os templates consumidores já usarem a nova configuração.
+
+
+## Runtime transacional mínimo (correção de amarração)
+
+A lista `mcp_tools` do roteamento é uma **allowlist**, não uma ordem para executar todas as ferramentas. O runtime agora:
+
+1. executa automaticamente somente ferramentas `read_only`;
+2. seleciona no máximo uma ação transacional compatível com o pedido do usuário;
+3. quando `require_confirmation: true`, persiste `pending_tool_call` e `transaction_status: AWAITING_CONFIRMATION`;
+4. no turno de confirmação, reutiliza a mesma chamada e executa com `confirmed: true`;
+5. publica no estado `available_mcp_tools`, `selected_tool_call`, `tool_policy_result`, `confirmation_required` e `confirmation_received`.
+
+Para o cenário de exemplo, o pedido `123` (ou `PED-ENTREGUE`) retorna `ENTREGUE` no MCP Retail. Use:
+
+```text
+Quero devolver o pedido 123 porque me arrependi da compra.
+Sim, confirmo a devolução.
+```
+
+O contrato MCP foi padronizado para usar `reason` tanto no catálogo quanto no servidor FastMCP. `tool_policies.yaml` prevalece sobre os campos legados de `tools.yaml`; estes permanecem alinhados nos templates para compatibilidade.
diff --git a/Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md b/Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md
new file mode 100644
index 0000000..b32a123
--- /dev/null
+++ b/Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md
@@ -0,0 +1,25 @@
+# Correção da extração de parâmetros MCP
+
+## Problema corrigido
+
+O bloco `extract` do `mcp_parameter_mapping.yaml` existia na configuração e na
+documentação, mas não era executado pelo runtime. Além disso, valores do
+Business Context podiam sobrescrever argumentos explícitos, fazendo
+`contract_key` substituir o `order_id` informado pelo usuário.
+
+## Correções
+
+- implementação da extração genérica `strategy: llm` após a escolha da tool;
+- suporte preservado para `strategy: month_name_pt`;
+- profile dedicado `mcp_parameter_extraction`;
+- telemetria `llm.mcp_parameter_extraction`;
+- `extract` deixou de ser interpretado como mapeamento simples;
+- argumentos explícitos/extraídos têm precedência sobre Business Context;
+- remoção de `contract_key: order_id` dos templates;
+- `order_id` configurado como `string`;
+- atualização das variantes em `Tuning-Performance`.
+
+## Resultado esperado
+
+Para a mensagem `consultar pedido 123`, a chamada MCP deve receber
+`order_id=123`, mesmo quando o Business Context contém outro `contract_key`.
diff --git a/Documentacao/RELEASE_NOTES_ROUTE_STICKINESS_TRANSACTION_SHIFT.md b/Documentacao/RELEASE_NOTES_ROUTE_STICKINESS_TRANSACTION_SHIFT.md
new file mode 100644
index 0000000..0297c41
--- /dev/null
+++ b/Documentacao/RELEASE_NOTES_ROUTE_STICKINESS_TRANSACTION_SHIFT.md
@@ -0,0 +1,21 @@
+# Correção — mudança de consulta para ação transacional
+
+## Problema
+
+Após `consultar pedido 123`, a mensagem `Quero devolver o pedido 123` podia permanecer no `orders_agent` por route stickiness. Como a intent anterior só expunha tools de consulta, o runtime executava novamente `consultar_pedido` e a resposta direta repetia o status do pedido.
+
+## Correções
+
+- Keywords explícitas configuradas no `routing.yaml` podem preemptar a route stickiness quando apontam para outra intent/agente.
+- `retail_support_exchange_return` passa a ter prioridade maior que `retail_order_tracking` para mensagens de troca/devolução.
+- Tools transacionais declaram `selection_keywords` no `tools.yaml`.
+- A resposta direta read-only é bloqueada quando a mensagem contém uma ação transacional registrada, mesmo que a intent anterior ainda esteja ativa.
+- A seleção da action tool usa configuração, não aliases de domínio fixos no runtime.
+
+## Fluxo esperado
+
+1. `consultar pedido 123` → `orders_agent` → `consultar_pedido` → resposta direta.
+2. `Quero devolver o pedido 123` → preempção da stickiness → `support_agent` / `retail_support_exchange_return`.
+3. `consultar_pedido` valida o pedido.
+4. `solicitar_devolucao` é selecionada e, com confirmação obrigatória, gera `AWAITING_CONFIRMATION`.
+5. `Sim, confirmo` executa a action tool uma única vez.
diff --git a/Documentacao/RELEASE_NOTES_TOOL_POLICIES.md b/Documentacao/RELEASE_NOTES_TOOL_POLICIES.md
new file mode 100644
index 0000000..b9c84ae
--- /dev/null
+++ b/Documentacao/RELEASE_NOTES_TOOL_POLICIES.md
@@ -0,0 +1,36 @@
+# Release notes - políticas read-only/transacionais
+
+## Alterações
+
+- Novo `ToolPolicyRegistry` opcional na biblioteca compartilhada.
+- Validação central no `MCPToolRouter`, inclusive para chamadas diretas.
+- Tipos mínimos `read_only` e `transactional`.
+- Confirmação estrita por `confirmed: true` ou `confirmation: true`.
+- Suporte opcional a campos obrigatórios por política.
+- Fallback automático para `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
+- `config/tool_policies.yaml` e variável `TOOL_POLICIES_PATH` nos templates principais, Day Zero e variantes de `Tuning-Performance/Normal` e `Tuning-Performance/Route_Stickness`.
+- Testes unitários de política e compatibilidade adicionados em `tests/unit/test_tool_policies.py`.
+
+## Verificações executadas
+
+- Compilação de `libs`, `templates`, `Tuning-Performance` e `tests`: aprovada.
+- Validação estrutural dos seis arquivos YAML: aprovada.
+- Casos isolados do loader (política transacional, confirmação, ausência de arquivo e ausência de cadastro): aprovados.
+- Renderização dos dois manuais Word atualizados: aprovada, sem cortes ou sobreposição nas páginas adicionadas.
+
+## Limitação do ambiente de validação
+
+A suíte `pytest` foi preparada, mas não pôde ser executada integralmente neste ambiente porque `pytest` e as dependências de runtime do projeto não estavam instalados e o acesso ao índice de pacotes expirou. Para reproduzir em um ambiente do projeto:
+
+```bash
+PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
+```
+
+## Correção de integração backend/MCP
+- `mcp_tools` passou a ser tratado como allowlist.
+- Ações não são mais executadas automaticamente junto com consultas.
+- Confirmação transacional é persistida e retomada no turno seguinte.
+- Corrigida incompatibilidade `reason`/`motivo` no MCP Retail.
+- Adicionado pedido entregue determinístico para testes (`123`).
+- Removida keyword genérica `produto` da intenção Telecom para evitar colisão com devoluções Retail.
+- Templates Normal e Route_Stickness em `Tuning-Performance` foram sincronizados.
diff --git a/Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx b/Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx
new file mode 100644
index 0000000..de1954f
Binary files /dev/null and b/Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx differ
diff --git a/Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md b/Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md
new file mode 100644
index 0000000..3b7fdaf
--- /dev/null
+++ b/Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md
@@ -0,0 +1,35 @@
+# Test Results - Semantic Route Stickiness and Global Session Control
+
+Date: 2026-07-31
+
+## Command
+
+```bash
+PYTHONPATH=libs/agent_framework/src pytest -q tests/unit/test_semantic_route_stickiness.py
+```
+
+## Result
+
+```text
+9 passed
+```
+
+## Covered scenarios
+
+1. `CONTINUE` bypasses the Enterprise Router.
+2. `ROUTE` falls back to the Enterprise Router.
+3. Low-confidence `CONTINUE` falls back safely.
+4. Invalid model output falls back safely.
+5. With no active agent, the lightweight classifier can still detect global session actions.
+6. `HUMAN_HANDOFF` returns the global `human_handoff` route and session-control metadata.
+7. `END_SESSION` returns the global `end_session` route and session-control metadata.
+8. Global actions work on the first turn.
+9. `CONTINUE` without an active agent is normalized to `ROUTE`.
+
+## Additional validation
+
+```bash
+python -m compileall -q libs/agent_framework/src templates/agent_template_backend/app
+```
+
+Compilation completed successfully.
diff --git a/Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md b/Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md
new file mode 100644
index 0000000..b3b17cf
--- /dev/null
+++ b/Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md
@@ -0,0 +1,27 @@
+# Validação — integração transacional Agent Template Backend / MCP
+
+## Correções implementadas
+
+- `mcp_tools` é tratado como allowlist, não como lista de execução automática.
+- Tools `read_only` continuam disponíveis para enriquecimento de contexto.
+- Somente uma tool transacional compatível com a solicitação é selecionada.
+- `require_confirmation: true` cria `pending_tool_call` e `AWAITING_CONFIRMATION`.
+- O turno de confirmação executa a chamada pendente com `confirmed: true`.
+- O estado expõe `selected_tool_call`, `tool_policy_result`, `confirmation_required`, `confirmation_received` e `transaction_status`.
+- `reason` foi padronizado entre catálogo, mapping e FastMCP Retail.
+- Pedido `123` e `PED-ENTREGUE` retornam status `ENTREGUE` para testes positivos.
+- A keyword genérica `produto` foi removida da intenção Telecom para não capturar devoluções Retail.
+- Templates `Normal` e `Route_Stickness` em `Tuning-Performance` foram atualizados.
+
+## Teste recomendado
+
+1. `Quero devolver o pedido 123 porque me arrependi da compra.`
+2. Esperado: `transaction_status=AWAITING_CONFIRMATION`, sem execução de `solicitar_devolucao`.
+3. `Sim, confirmo a devolução.`
+4. Esperado: `transaction_status=COMPLETED` e execução única de `solicitar_devolucao`.
+
+## Resultado automatizado
+
+```text
+7 passed
+```
diff --git a/README.md b/README.md
index 9a04aae..ba4946e 100644
--- a/README.md
+++ b/README.md
@@ -10889,4 +10889,290 @@ A implementação está arquiteturalmente correta quando:
[ ] 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.
+---
+
+### 33. Tuning-Performance — Extensões padronizadas de desempenho
+
+A pasta `Tuning-Performance` disponibiliza um conjunto adicional de implementações, configurações e exemplos destinados a melhorar o desempenho, a previsibilidade e a eficiência do Agent Framework.
+
+Essas funcionalidades não são ativadas automaticamente apenas pela cópia da pasta. Sua adoção exige a integração dos componentes correspondentes, a revisão das configurações do projeto e a adequação das regras de negócio, ferramentas MCP, prompts, estados conversacionais e políticas de execução.
+
+O objetivo do `Tuning-Performance` é oferecer uma abordagem padronizada para otimizações que normalmente seriam implementadas separadamente em cada agente ou projeto. Com isso, as equipes podem reduzir chamadas desnecessárias a modelos, evitar execuções incorretas de ferramentas, melhorar a continuidade conversacional e estabelecer um comportamento consistente para operações read-only e transacionais.
+
+#### 33.1. Route Stickiness semântica
+
+Mantém o agente atual durante mensagens de continuidade, evitando que o roteador completo seja executado novamente em todos os turnos.
+
+A decisão de continuidade pode classificar a mensagem como:
+
+* `CONTINUE`: mantém o agente e a intenção atuais;
+* `ROUTE`: executa um novo roteamento;
+* `HUMAN_HANDOFF`: encaminha a conversa para atendimento humano;
+* `END_SESSION`: encerra a sessão conversacional.
+
+A continuidade é preemptada quando a nova mensagem contém uma intenção explícita que exige outro agente, domínio ou conjunto de ferramentas. Por exemplo, uma conversa iniciada com consulta de pedido pode ser redirecionada para suporte quando o usuário solicitar uma devolução.
+
+#### 33.2. Preempção por mudança operacional
+
+Detecta mudanças entre operações consultivas e transacionais, mesmo quando elas pertencem ao mesmo domínio.
+
+Exemplo:
+
+```text
+consultar pedido 123
+→ orders_agent
+→ consultar_pedido
+
+quero devolver o pedido 123
+→ support_agent
+→ solicitar_devolucao
+```
+
+Essa proteção impede que a route stickiness mantenha a conversa em uma intenção read-only quando o usuário iniciou uma ação transacional.
+
+#### 33.3. Seleção individual de ferramentas read-only
+
+As ferramentas configuradas na intenção passam a funcionar como uma allowlist, e não como uma lista de execução automática.
+
+O runtime seleciona apenas a ferramenta compatível com a solicitação atual.
+
+Exemplo:
+
+```text
+consultar pedido 123
+→ consultar_pedido
+```
+
+```text
+qual é o rastreamento do pedido 123?
+→ consultar_entrega
+```
+
+Isso evita chamadas redundantes a múltiplas ferramentas MCP e reduz latência, carga e volume de processamento.
+
+#### 33.4. Extração híbrida de parâmetros
+
+Permite extrair parâmetros das mensagens usando uma estratégia determinística com fallback para LLM.
+
+As estratégias suportadas são:
+
+* `regex`: utiliza somente uma expressão regular;
+* `llm`: utiliza sempre o modelo;
+* `hybrid`: tenta primeiro a extração determinística e utiliza a LLM apenas quando necessário.
+
+Exemplo:
+
+```yaml
+extract:
+ order_id:
+ from: message
+ type: string
+ strategy: hybrid
+ pattern: '(?i)\b(?:pedido|order)\s*[:#-]?\s*([A-Z0-9-]+)\b'
+ group: 1
+```
+
+Para uma mensagem como `consultar pedido 123`, o identificador é obtido pelo regex, sem consumo adicional de tokens. A LLM permanece disponível para frases ambíguas ou formatos não reconhecidos.
+
+#### 33.5. Precedência segura de parâmetros MCP
+
+Os parâmetros enviados às ferramentas seguem uma precedência padronizada:
+
+```text
+1. argumento explícito da chamada
+2. valor extraído da mensagem
+3. valor semanticamente compatível do Business Context
+4. valor default
+```
+
+Valores extraídos ou fornecidos explicitamente não são sobrescritos por campos genéricos do contexto.
+
+Essa regra evita, por exemplo, que um `contract_key` seja enviado indevidamente como `order_id`.
+
+#### 33.6. Políticas para ferramentas read-only e transacionais
+
+O arquivo `tool_policies.yaml` permite classificar cada ferramenta e definir seus requisitos operacionais.
+
+Exemplo:
+
+```yaml
+tool_policies:
+ solicitar_devolucao:
+ operation_type: transactional
+ require_confirmation: true
+
+ consultar_pedido:
+ operation_type: read_only
+ require_confirmation: false
+```
+
+Ferramentas read-only podem ser executadas diretamente. Ferramentas transacionais podem exigir confirmação explícita antes da chamada MCP.
+
+#### 33.7. Confirmação transacional persistente
+
+Operações que exigem confirmação são armazenadas como uma chamada pendente.
+
+O fluxo esperado é:
+
+```text
+usuário solicita a operação
+→ parâmetros são preparados
+→ pending_tool_call é persistido
+→ transaction_status = AWAITING_CONFIRMATION
+→ usuário confirma
+→ ferramenta MCP é executada
+```
+
+A confirmação deve retomar exatamente a operação pendente, preservando ferramenta, parâmetros e contexto.
+
+Após a conclusão ou o cancelamento, o estado pendente deve ser limpo para impedir reexecuções acidentais.
+
+#### 33.8. Proteção contra confirmações fora de contexto
+
+Mensagens como `sim`, `confirmo` ou `pode continuar` somente devem executar uma ferramenta quando existir uma operação pendente.
+
+Quando não houver `pending_tool_call`, o framework deve informar que não existe nenhuma operação aguardando confirmação, em vez de reutilizar uma transação anterior ou iniciar uma nova ação com base apenas no histórico.
+
+#### 33.9. Supressão condicional do RAG
+
+O RAG pode ser ignorado quando os resultados MCP já são suficientes para responder à solicitação.
+
+Exemplo:
+
+```text
+consultar pedido 123
+→ resultado autoritativo obtido pelo MCP
+→ RAG não executado
+```
+
+O RAG continua disponível para perguntas relacionadas a políticas, regras, documentação, prazos, procedimentos ou informações não retornadas pelas ferramentas.
+
+Essa separação reduz buscas vetoriais desnecessárias e melhora o tempo de resposta.
+
+#### 33.10. Evidências MCP para o Groundedness Judge
+
+Os resultados das ferramentas MCP são enviados ao judge de groundedness como evidência factual.
+
+Isso evita que respostas baseadas em dados reais de ferramentas sejam classificadas incorretamente como alucinação.
+
+O contexto do judge pode incluir:
+
+* mensagem do usuário;
+* resposta final;
+* resultados MCP;
+* contexto RAG;
+* estado transacional;
+* política aplicada à ferramenta.
+
+#### 33.11. Execução configurável de Judges
+
+A execução dos judges pode ser controlada por amostragem.
+
+Exemplo:
+
+```yaml
+sample_rate: 0.25
+always_run_for_transactional: true
+```
+
+Nesse cenário:
+
+* aproximadamente 25% das interações comuns executam judges;
+* interações transacionais executam judges sempre.
+
+A identificação transacional considera, entre outros sinais:
+
+* `transaction_status`;
+* `tool_policy_result`;
+* `selected_tool_call`;
+* `pending_tool_call`;
+* resultados MCP de ferramentas transacionais.
+
+Essa abordagem reduz custo e latência sem remover avaliação de fluxos críticos.
+
+#### 33.12. Respostas diretas para consultas estruturadas
+
+Consultas simples podem gerar respostas determinísticas a partir do resultado MCP, sem uma nova chamada ao agente LLM.
+
+Exemplo:
+
+```text
+[OrdersAgent] Pedido 123: status ENTREGUE.
+Valor total: R$ 349,90.
+Itens: Livro de Arquitetura de IA; Cabo USB-C.
+```
+
+Essa otimização é indicada quando:
+
+* a ferramenta respondeu com sucesso;
+* os dados são estruturados;
+* não há necessidade de raciocínio adicional;
+* não há combinação complexa de fontes;
+* a mensagem não solicita uma ação transacional.
+
+#### 33.13. Respostas diretas para conclusões transacionais
+
+Resultados estruturados de operações concluídas também podem ser formatados diretamente.
+
+Exemplo:
+
+```text
+A solicitação de devolução do pedido 123 foi registrada.
+
+Protocolo: DEV-2026-001
+Status: ABERTO
+```
+
+Essa opção reduz o uso de LLM apenas para reorganizar informações já fornecidas pela ferramenta.
+
+#### 33.14. Configurações por projeto
+
+As otimizações devem ser ajustadas conforme o comportamento esperado de cada projeto.
+
+Entre os itens que normalmente precisam ser configurados estão:
+
+* keywords e prioridades das intents;
+* allowlist e regras de seleção das ferramentas;
+* expressões regulares de extração;
+* perfis LLM usados como fallback;
+* políticas read-only e transacionais;
+* mensagens de confirmação;
+* critérios de supressão do RAG;
+* amostragem dos judges;
+* respostas diretas;
+* persistência do estado transacional;
+* telemetria e eventos;
+* mecanismos de idempotência no MCP ou no serviço de negócio.
+
+#### 33.15. Considerações sobre idempotência
+
+O Agent Framework controla a experiência conversacional, a confirmação e o estado da operação pendente. Entretanto, a proteção definitiva contra operações duplicadas deve ser implementada no serviço MCP ou no serviço de negócio responsável pela transação.
+
+A necessidade de idempotência deve ser definida por ferramenta. Ela é especialmente importante para ações como:
+
+* devolução;
+* troca;
+* pagamento;
+* cancelamento;
+* criação de protocolo;
+* provisionamento;
+* operações com efeitos financeiros ou externos.
+
+O `Tuning-Performance` pode propagar identificadores e informações de contexto, mas a garantia atômica deve permanecer na camada que controla o dado transacional.
+
+#### 33.16. Benefícios esperados
+
+A adoção das funcionalidades do `Tuning-Performance` pode proporcionar:
+
+* menor latência;
+* menor consumo de tokens;
+* redução de chamadas LLM;
+* redução de chamadas MCP redundantes;
+* menor utilização desnecessária do RAG;
+* melhor continuidade entre turnos;
+* maior segurança em operações transacionais;
+* melhor rastreabilidade;
+* groundedness baseado em evidências reais;
+* comportamento consistente entre diferentes agentes e projetos.
+
+O conteúdo desta pasta deve ser tratado como uma extensão adicional do framework. Sua utilização requer implementação, configuração, testes funcionais e validação das regras de negócio antes da implantação em produção.
diff --git a/README_en.md b/README_en.md
index fadf8e4..d3e29e7 100644
--- a/README_en.md
+++ b/README_en.md
@@ -10795,4 +10795,290 @@ The implementation is architecturally correct when:
[ ] the developer can test the route before testing the actual execution.
```
-With this design, adding a new agent does not require rewriting the frontend or copying logic between backends. The developer creates the specialized backend, registers it in the Agent Gateway, and lets the framework take care of the cross-cutting engines.
+---
+
+### 33. Tuning-Performance — Standardized Performance Extensions
+
+The `Tuning-Performance` folder provides an additional set of implementations, configurations, and examples designed to improve the performance, predictability, and efficiency of the Agent Framework.
+
+These capabilities are not enabled automatically by copying the folder. Adoption requires integrating the corresponding components, reviewing project configuration, and adapting business rules, MCP tools, prompts, conversational states, and execution policies.
+
+The purpose of `Tuning-Performance` is to provide a standardized approach to optimizations that would otherwise be implemented separately in each agent or project. It helps teams reduce unnecessary model calls, prevent incorrect tool execution, improve conversational continuity, and establish consistent behavior for read-only and transactional operations.
+
+#### 33.1. Semantic Route Stickiness
+
+Keeps the current agent active during follow-up messages, avoiding a full routing operation on every conversational turn.
+
+The continuity decision may classify the message as:
+
+* `CONTINUE`: keep the current agent and intent;
+* `ROUTE`: perform a new routing decision;
+* `HUMAN_HANDOFF`: transfer the conversation to a human agent;
+* `END_SESSION`: terminate the conversational session.
+
+Continuity can be preempted when the new message contains an explicit intent that requires a different agent, domain, or tool set. For example, a conversation that starts with an order inquiry can be redirected to support when the user requests a return.
+
+#### 33.2. Operational Shift Preemption
+
+Detects changes between read-only and transactional operations, even when they belong to the same business domain.
+
+Example:
+
+```text
+check order 123
+→ orders_agent
+→ consultar_pedido
+
+I want to return order 123
+→ support_agent
+→ solicitar_devolucao
+```
+
+This prevents route stickiness from keeping the conversation in a read-only intent after the user has initiated a transactional action.
+
+#### 33.3. Individual Read-Only Tool Selection
+
+Tools configured for an intent are treated as an allowlist instead of an automatic execution list.
+
+The runtime selects only the tool that matches the current request.
+
+Example:
+
+```text
+check order 123
+→ consultar_pedido
+```
+
+```text
+where is order 123?
+→ consultar_entrega
+```
+
+This prevents redundant MCP calls and reduces latency, load, and processing volume.
+
+#### 33.4. Hybrid Parameter Extraction
+
+Parameters can be extracted from the user message using deterministic processing with an LLM fallback.
+
+Supported strategies include:
+
+* `regex`: use only a regular expression;
+* `llm`: always use the model;
+* `hybrid`: attempt deterministic extraction first and call the LLM only when necessary.
+
+Example:
+
+```yaml
+extract:
+ order_id:
+ from: message
+ type: string
+ strategy: hybrid
+ pattern: '(?i)\b(?:pedido|order)\s*[:#-]?\s*([A-Z0-9-]+)\b'
+ group: 1
+```
+
+For a message such as `check order 123`, the identifier is extracted by regex without additional token consumption. The LLM remains available for ambiguous phrases or unsupported formats.
+
+#### 33.5. Safe MCP Parameter Precedence
+
+Parameters sent to tools follow a standardized precedence order:
+
+```text
+1. explicit tool-call argument
+2. value extracted from the message
+3. semantically compatible Business Context value
+4. default value
+```
+
+Explicit or extracted values are not overwritten by generic context fields.
+
+This prevents, for example, a `contract_key` from being incorrectly sent as an `order_id`.
+
+#### 33.6. Read-Only and Transactional Tool Policies
+
+The `tool_policies.yaml` file can classify each tool and define its operational requirements.
+
+Example:
+
+```yaml
+tool_policies:
+ solicitar_devolucao:
+ operation_type: transactional
+ require_confirmation: true
+
+ consultar_pedido:
+ operation_type: read_only
+ require_confirmation: false
+```
+
+Read-only tools can be executed directly. Transactional tools can require explicit user confirmation before the MCP call is performed.
+
+#### 33.7. Persistent Transaction Confirmation
+
+Operations that require confirmation are stored as pending tool calls.
+
+The expected flow is:
+
+```text
+user requests an operation
+→ parameters are prepared
+→ pending_tool_call is persisted
+→ transaction_status = AWAITING_CONFIRMATION
+→ user confirms
+→ MCP tool is executed
+```
+
+The confirmation resumes the exact pending operation while preserving its tool, parameters, and context.
+
+After completion or cancellation, the pending state must be cleared to prevent accidental re-execution.
+
+#### 33.8. Protection Against Out-of-Context Confirmations
+
+Messages such as `yes`, `confirm`, or `continue` should execute a tool only when a pending operation exists.
+
+When there is no `pending_tool_call`, the framework should inform the user that no operation is awaiting confirmation instead of reusing a previous transaction or creating a new action from conversation history alone.
+
+#### 33.9. Conditional RAG Suppression
+
+RAG can be skipped when MCP results already provide sufficient information to answer the request.
+
+Example:
+
+```text
+check order 123
+→ authoritative result returned by MCP
+→ RAG is not executed
+```
+
+RAG remains available for questions about policies, rules, documentation, deadlines, procedures, or information not returned by the tools.
+
+This separation reduces unnecessary vector searches and improves response time.
+
+#### 33.10. MCP Evidence for the Groundedness Judge
+
+MCP tool results are passed to the groundedness judge as factual evidence.
+
+This prevents responses based on real tool data from being incorrectly classified as hallucinations.
+
+The judge context may include:
+
+* user message;
+* final answer;
+* MCP results;
+* RAG context;
+* transactional state;
+* tool policy result.
+
+#### 33.11. Configurable Judge Execution
+
+Judge execution can be controlled through sampling.
+
+Example:
+
+```yaml
+sample_rate: 0.25
+always_run_for_transactional: true
+```
+
+In this configuration:
+
+* approximately 25% of regular interactions run judges;
+* transactional interactions always run judges.
+
+Transactional detection may consider:
+
+* `transaction_status`;
+* `tool_policy_result`;
+* `selected_tool_call`;
+* `pending_tool_call`;
+* MCP results from transactional tools.
+
+This reduces cost and latency while retaining evaluation for critical flows.
+
+#### 33.12. Direct Responses for Structured Queries
+
+Simple queries can generate deterministic responses directly from MCP results without an additional agent LLM call.
+
+Example:
+
+```text
+[OrdersAgent] Order 123: status DELIVERED.
+Total amount: $349.90.
+Items: AI Architecture Book; USB-C Cable.
+```
+
+This optimization is appropriate when:
+
+* the tool completed successfully;
+* the result is structured;
+* no additional reasoning is required;
+* there is no complex combination of sources;
+* the message is not requesting a transactional action.
+
+#### 33.13. Direct Responses for Completed Transactions
+
+Structured transactional results can also be formatted directly.
+
+Example:
+
+```text
+The return request for order 123 has been registered.
+
+Protocol: DEV-2026-001
+Status: OPEN
+```
+
+This avoids using an LLM only to reorganize information that was already returned by the tool.
+
+#### 33.14. Project-Specific Configuration
+
+These optimizations must be adapted to the expected behavior of each project.
+
+Typical configuration items include:
+
+* intent keywords and priorities;
+* tool allowlists and selection rules;
+* parameter extraction patterns;
+* fallback LLM profiles;
+* read-only and transactional policies;
+* confirmation messages;
+* RAG suppression criteria;
+* judge sampling;
+* direct-response templates;
+* transactional state persistence;
+* telemetry and events;
+* idempotency mechanisms in the MCP or business service.
+
+#### 33.15. Idempotency Considerations
+
+The Agent Framework controls the conversational experience, confirmation, and pending operation state. However, the definitive protection against duplicate operations should be implemented in the MCP service or in the downstream business service responsible for the transaction.
+
+Idempotency requirements should be defined per tool. They are especially relevant for operations such as:
+
+* returns;
+* exchanges;
+* payments;
+* cancellations;
+* protocol creation;
+* provisioning;
+* operations with financial or external side effects.
+
+`Tuning-Performance` may propagate identifiers and contextual information, but the atomic guarantee should remain in the layer that controls the transactional data.
+
+#### 33.16. Expected Benefits
+
+Adopting the `Tuning-Performance` capabilities can provide:
+
+* lower latency;
+* lower token consumption;
+* fewer LLM calls;
+* fewer redundant MCP calls;
+* reduced unnecessary RAG usage;
+* better continuity across conversational turns;
+* safer transactional operations;
+* improved traceability;
+* groundedness based on real evidence;
+* consistent behavior across agents and projects.
+
+The content of this folder should be treated as an additional framework extension. Its use requires implementation, configuration, functional testing, and business-rule validation before production deployment.
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/.env b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/.env
new file mode 100644
index 0000000..4556734
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/.env
@@ -0,0 +1,207 @@
+###############################################################################
+# 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
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/Dockerfile b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/Dockerfile
new file mode 100644
index 0000000..273fe01
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/README.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/README.md
new file mode 100644
index 0000000..0cf81d7
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/README.md
@@ -0,0 +1,4213 @@
+# 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
+```
+
+
+
+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.
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/README_ENTERPRISE_TEMPLATE.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/README_ENTERPRISE_TEMPLATE.md
new file mode 100644
index 0000000..cae516e
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/__init__.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..884409e
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..7f097b7
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc
new file mode 100644
index 0000000..d662183
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc
new file mode 100644
index 0000000..e5055fe
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/README.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/README.md
new file mode 100644
index 0000000..2917425
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc
new file mode 100644
index 0000000..0e9f1e1
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc
new file mode 100644
index 0000000..f05ef2c
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc
new file mode 100644
index 0000000..2b36337
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc
new file mode 100644
index 0000000..27c5dd6
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc
new file mode 100644
index 0000000..321fc08
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc
new file mode 100644
index 0000000..bcd0559
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/billing_agent.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/billing_agent.py
new file mode 100644
index 0000000..aa60099
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/agents/orders_agent.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/orders_agent.py
new file mode 100644
index 0000000..f557bed
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/agents/product_agent.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/product_agent.py
new file mode 100644
index 0000000..34433f5
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/agents/prompting.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/prompting.py
new file mode 100644
index 0000000..255422b
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/agents/runtime.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/runtime.py
new file mode 100644
index 0000000..e6429c4
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/runtime.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+# Compatibilidade local do template/backend.
+# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
+from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
+
+__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/support_agent.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/agents/support_agent.py
new file mode 100644
index 0000000..b4f0244
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/examples/__init__.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__init__.py
new file mode 100644
index 0000000..3f95e96
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__init__.py
@@ -0,0 +1 @@
+"""Exemplos de uso do template backend enterprise."""
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..e011e2c
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc
new file mode 100644
index 0000000..417fdea
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc
new file mode 100644
index 0000000..43c2841
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc
new file mode 100644
index 0000000..8684f23
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc
new file mode 100644
index 0000000..19cee2f
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc
new file mode 100644
index 0000000..5919f9d
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/grl_examples.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/grl_examples.py
new file mode 100644
index 0000000..8dadac8
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/examples/ic_examples.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/ic_examples.py
new file mode 100644
index 0000000..f6daa57
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/examples/mcp_examples.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/mcp_examples.py
new file mode 100644
index 0000000..613f10c
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/examples/noc_examples.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/noc_examples.py
new file mode 100644
index 0000000..2b38a15
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/examples/observer_examples.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/examples/observer_examples.py
new file mode 100644
index 0000000..926b553
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/main.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/main.py
new file mode 100644
index 0000000..d51bbc2
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/mcp_gateway_client_factory.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/mcp_gateway_client_factory.py
new file mode 100644
index 0000000..5a32d15
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/observability/__init__.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..60f4328
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc
new file mode 100644
index 0000000..a01c7f1
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/telemetry_observer.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/observability/telemetry_observer.py
new file mode 100644
index 0000000..92f07a1
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/state.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/state.py
new file mode 100644
index 0000000..cc19c03
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc
new file mode 100644
index 0000000..17e9714
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/agent_graph.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/agent_graph.py
new file mode 100644
index 0000000..b8ed7bc
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents.yaml
new file mode 100644
index 0000000..7d245a5
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents/retail_orders/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents/retail_orders/judges.yaml
new file mode 100644
index 0000000..62fc7c7
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml
new file mode 100644
index 0000000..f872a2b
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents/telecom_contas/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents/telecom_contas/judges.yaml
new file mode 100644
index 0000000..d488063
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml
new file mode 100644
index 0000000..42732c4
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/guardrails.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/guardrails.yaml
new file mode 100644
index 0000000..44887eb
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/identity.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/identity.yaml
new file mode 100644
index 0000000..5f20147
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/judges.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/judges.yaml
new file mode 100644
index 0000000..c091619
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/mcp_parameter_mapping.yaml
new file mode 100644
index 0000000..5b29ccf
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/mcp_servers.docker.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/mcp_servers.docker.yaml
new file mode 100644
index 0000000..8101130
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/mcp_servers.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/mcp_servers.yaml
new file mode 100644
index 0000000..fe638a2
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/prompt_policy.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/prompt_policy.yaml
new file mode 100644
index 0000000..af4398f
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/routing.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/routing.yaml
new file mode 100644
index 0000000..2dbe95e
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/config/tool_policies.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/Long_Term_Memory/templates/agent_template_backend/config/tools.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/config/tools.yaml
new file mode 100644
index 0000000..d85fae1
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/data/agent_framework.db b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/data/agent_framework.db
new file mode 100644
index 0000000..ddf1883
Binary files /dev/null and b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/data/agent_framework.db differ
diff --git a/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
new file mode 100644
index 0000000..d81efdf
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md
new file mode 100644
index 0000000..83975af
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md
new file mode 100644
index 0000000..3f981ac
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
new file mode 100644
index 0000000..5c41732
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md
new file mode 100644
index 0000000..c7bd3b2
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md
new file mode 100644
index 0000000..849fda1
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md
new file mode 100644
index 0000000..edcd2c7
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md
new file mode 100644
index 0000000..bc2638b
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md
new file mode 100644
index 0000000..cfe5969
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md
new file mode 100644
index 0000000..a9e4458
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt
new file mode 100644
index 0000000..fac4bf4
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/llm_profiles.yaml b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/llm_profiles.yaml
new file mode 100644
index 0000000..908b382
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/requirements.txt b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/requirements.txt
new file mode 100644
index 0000000..71214bd
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Long_Term_Memory/templates/agent_template_backend/scripts/test_long_term_memory.py b/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/scripts/test_long_term_memory.py
new file mode 100644
index 0000000..52e2a8d
--- /dev/null
+++ b/Tuning-Performance/Long_Term_Memory/templates/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/Normal/templates/agent_template_backend/.env b/Tuning-Performance/Normal/templates/agent_template_backend/.env
new file mode 100644
index 0000000..e93ecca
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/.env
@@ -0,0 +1,195 @@
+###############################################################################
+# 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_openai
+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/openai/v1
+OCI_GENAI_MODEL=openai.gpt-4.1
+OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
+OCI_GENAI_PROJECT_OCID=
+
+# OCI SDK / signer / profiles
+OCI_CONFIG_FILE=~/.oci/config
+OCI_PROFILE=DEFAULT
+OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+OCI_REGION=us-chicago-1
+
+###############################################################################
+# Persistência
+###############################################################################
+# Opções: memory, autonomous, mongodb
+SESSION_REPOSITORY_PROVIDER=sqlite
+MEMORY_REPOSITORY_PROVIDER=sqlite
+CHECKPOINT_REPOSITORY_PROVIDER=sqlite
+SQLITE_DB_PATH=./data/agent_framework.db
+
+# Autonomous Database
+ADB_USER=admin
+ADB_PASSWORD=fjhsdf04954hf
+ADB_DSN=oradb23aidev_high
+ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
+ADB_WALLET_PASSWORD=fjhsdf04954hf
+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=sqlite
+GRAPH_STORE_PROVIDER=sqlite
+RAG_TOP_K=5
+EMBEDDING_PROVIDER=mock
+OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
+RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
+
+###############################################################################
+# Observabilidade
+###############################################################################
+ENABLE_LANGFUSE=true
+LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
+LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
+LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
+LANGFUSE_HOST=http://localhost:3005
+ENABLE_OTEL=false
+OTEL_EXPORTER_OTLP_ENDPOINT=
+OTEL_SERVICE_NAME=ai-agent-template
+ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
+
+###############################################################################
+# 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=pubsub
+# 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
+
+###############################################################################
+# MCP / Tools
+###############################################################################
+ENABLE_MCP_TOOLS=true
+MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
+TOOLS_CONFIG_PATH=./config/tools.yaml
+TOOL_POLICIES_PATH=./config/tool_policies.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=sqlite
+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
+
+###############################################################################
+# MCP Gateway
+###############################################################################
+# true = framework routes tool calls to the dedicated MCP Gateway.
+# false = framework calls MCP servers directly from mcp_servers.yaml.
+MCP_GATEWAY_ENABLED=true
+MCP_GATEWAY_URL=http://localhost:8300
+MCP_GATEWAY_TIMEOUT_SECONDS=60
+# MCP_GATEWAY_TOKEN=
+MCP_GATEWAY_AGENT_ID=telecom_contas
+MCP_GATEWAY_TENANT_ID=default
+
+###############################################################################
+# 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
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/Dockerfile b/Tuning-Performance/Normal/templates/agent_template_backend/Dockerfile
new file mode 100644
index 0000000..273fe01
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/README.md b/Tuning-Performance/Normal/templates/agent_template_backend/README.md
new file mode 100644
index 0000000..0cf81d7
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/README.md
@@ -0,0 +1,4213 @@
+# 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
+```
+
+
+
+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.
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/README_ENTERPRISE_TEMPLATE.md b/Tuning-Performance/Normal/templates/agent_template_backend/README_ENTERPRISE_TEMPLATE.md
new file mode 100644
index 0000000..cae516e
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/__init__.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/README.md b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/README.md
new file mode 100644
index 0000000..2917425
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/agents/billing_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/billing_agent.py
new file mode 100644
index 0000000..aa60099
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/agents/orders_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/orders_agent.py
new file mode 100644
index 0000000..f557bed
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/agents/product_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/product_agent.py
new file mode 100644
index 0000000..34433f5
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/agents/prompting.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/prompting.py
new file mode 100644
index 0000000..255422b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/agents/runtime.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/runtime.py
new file mode 100644
index 0000000..e6429c4
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/runtime.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+# Compatibilidade local do template/backend.
+# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
+from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
+
+__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/support_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/agents/support_agent.py
new file mode 100644
index 0000000..b4f0244
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/examples/__init__.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/__init__.py
new file mode 100644
index 0000000..3f95e96
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/__init__.py
@@ -0,0 +1 @@
+"""Exemplos de uso do template backend enterprise."""
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/grl_examples.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/grl_examples.py
new file mode 100644
index 0000000..8dadac8
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/examples/ic_examples.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/ic_examples.py
new file mode 100644
index 0000000..f6daa57
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/examples/mcp_examples.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/mcp_examples.py
new file mode 100644
index 0000000..613f10c
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/examples/noc_examples.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/noc_examples.py
new file mode 100644
index 0000000..2b38a15
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/examples/observer_examples.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/examples/observer_examples.py
new file mode 100644
index 0000000..926b553
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/main.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/main.py
new file mode 100644
index 0000000..06d1bd1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/app/main.py
@@ -0,0 +1,532 @@
+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"],
+ "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"),
+ },
+ )
+ 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,
+ "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/Normal/templates/agent_template_backend/app/mcp_gateway_client_factory.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/mcp_gateway_client_factory.py
new file mode 100644
index 0000000..5a32d15
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/observability/__init__.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/observability/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/app/observability/telemetry_observer.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/observability/telemetry_observer.py
new file mode 100644
index 0000000..92f07a1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/app/state.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/state.py
new file mode 100644
index 0000000..ac673d6
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/app/state.py
@@ -0,0 +1,51 @@
+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]
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py b/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py
new file mode 100644
index 0000000..0a12c4b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py
@@ -0,0 +1,816 @@
+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("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": "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 persist_long_term_memory(self, state):
+ result = await self.long_term_memory_manager.persist_turn(state)
+ return {"long_term_memory_write_result": result}
+
+ 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/Normal/templates/agent_template_backend/config/agents.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents.yaml
new file mode 100644
index 0000000..7d245a5
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents/retail_orders/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents/retail_orders/judges.yaml
new file mode 100644
index 0000000..62fc7c7
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml
new file mode 100644
index 0000000..f872a2b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents/telecom_contas/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents/telecom_contas/judges.yaml
new file mode 100644
index 0000000..d488063
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml
new file mode 100644
index 0000000..42732c4
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/guardrails.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/guardrails.yaml
new file mode 100644
index 0000000..44887eb
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/identity.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/identity.yaml
new file mode 100644
index 0000000..5f20147
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/judges.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/judges.yaml
new file mode 100644
index 0000000..c091619
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/mcp_parameter_mapping.yaml
new file mode 100644
index 0000000..5b29ccf
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/mcp_servers.docker.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/mcp_servers.docker.yaml
new file mode 100644
index 0000000..8101130
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/mcp_servers.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/mcp_servers.yaml
new file mode 100644
index 0000000..fe638a2
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/prompt_policy.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/prompt_policy.yaml
new file mode 100644
index 0000000..af4398f
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/routing.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/routing.yaml
new file mode 100644
index 0000000..2dbe95e
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/config/tool_policies.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/Normal/templates/agent_template_backend/config/tools.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/config/tools.yaml
new file mode 100644
index 0000000..d85fae1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
new file mode 100644
index 0000000..d81efdf
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md
new file mode 100644
index 0000000..83975af
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md
new file mode 100644
index 0000000..3f981ac
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md
new file mode 100644
index 0000000..c7bd3b2
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md
new file mode 100644
index 0000000..849fda1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md
new file mode 100644
index 0000000..edcd2c7
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md
new file mode 100644
index 0000000..bc2638b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md b/Tuning-Performance/Normal/templates/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md
new file mode 100644
index 0000000..a9e4458
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt b/Tuning-Performance/Normal/templates/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt
new file mode 100644
index 0000000..fac4bf4
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/llm_profiles.yaml b/Tuning-Performance/Normal/templates/agent_template_backend/llm_profiles.yaml
new file mode 100644
index 0000000..3992c12
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend/llm_profiles.yaml
@@ -0,0 +1,74 @@
+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
+ 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/Normal/templates/agent_template_backend/requirements.txt b/Tuning-Performance/Normal/templates/agent_template_backend/requirements.txt
new file mode 100644
index 0000000..71214bd
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend/scripts/test_long_term_memory.py b/Tuning-Performance/Normal/templates/agent_template_backend/scripts/test_long_term_memory.py
new file mode 100644
index 0000000..52e2a8d
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/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/Normal/templates/agent_template_backend_day_zero/.env b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/.env
new file mode 100644
index 0000000..34118b9
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/.env
@@ -0,0 +1,192 @@
+###############################################################################
+# 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_openai
+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/openai/v1
+OCI_GENAI_MODEL=openai.gpt-4.1
+OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
+OCI_GENAI_PROJECT_OCID=
+
+# OCI SDK / signer / profiles
+OCI_CONFIG_FILE=~/.oci/config
+OCI_PROFILE=DEFAULT
+OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+OCI_REGION=us-chicago-1
+
+###############################################################################
+# Persistência
+###############################################################################
+# Opções: memory, autonomous, mongodb
+SESSION_REPOSITORY_PROVIDER=sqlite
+MEMORY_REPOSITORY_PROVIDER=sqlite
+CHECKPOINT_REPOSITORY_PROVIDER=sqlite
+SQLITE_DB_PATH=./data/agent_framework.db
+
+# Autonomous Database
+ADB_USER=admin
+ADB_PASSWORD=fjhsdf04954hf
+ADB_DSN=oradb23aidev_high
+ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
+ADB_WALLET_PASSWORD=fjhsdf04954hf
+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=sqlite
+GRAPH_STORE_PROVIDER=sqlite
+RAG_TOP_K=5
+EMBEDDING_PROVIDER=mock
+OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
+RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
+
+###############################################################################
+# Observabilidade
+###############################################################################
+ENABLE_LANGFUSE=true
+LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
+LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
+LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
+LANGFUSE_HOST=http://localhost:3005
+ENABLE_OTEL=false
+OTEL_EXPORTER_OTLP_ENDPOINT=
+OTEL_SERVICE_NAME=ai-agent-template
+ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
+
+###############################################################################
+# 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=pubsub
+# 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
+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
+
+###############################################################################
+# MCP / Tools
+###############################################################################
+ENABLE_MCP_TOOLS=true
+MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
+TOOLS_CONFIG_PATH=./config/tools.yaml
+TOOL_POLICIES_PATH=./config/tool_policies.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=sqlite
+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
+
+###############################################################################
+# MCP Gateway
+###############################################################################
+# true = framework routes tool calls to the dedicated MCP Gateway.
+# false = framework calls MCP servers directly from mcp_servers.yaml.
+MCP_GATEWAY_ENABLED=true
+MCP_GATEWAY_URL=http://localhost:8300
+MCP_GATEWAY_TIMEOUT_SECONDS=60
+# MCP_GATEWAY_TOKEN=
+MCP_GATEWAY_AGENT_ID=telecom_contas
+MCP_GATEWAY_TENANT_ID=default
+
+###############################################################################
+# 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
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/Dockerfile b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/Dockerfile
new file mode 100644
index 0000000..273fe01
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/README_DAY_ZERO.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
new file mode 100644
index 0000000..869ed33
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
@@ -0,0 +1,89 @@
+# agent_template_backend_day_zero
+
+Este folder é uma cópia do `agent_template_backend`, porém transformada em um template **Day Zero**.
+
+A ideia é o desenvolvedor começar um agente novo sem apagar manualmente exemplos de negócio.
+
+## O que foi mantido
+
+Foi mantida a estrutura original do backend:
+
+- `app/main.py`
+- `app/workflows/agent_graph.py`
+- `app/state.py`
+- `app/agents/runtime.py`
+- `app/agents/prompting.py`
+- configurações em `config/`
+- integração com `agent_framework`
+- Analytics / Observer
+- NOC / GRL
+- OutputSupervisor
+- MCP Router
+- RAG
+- cache
+- memória
+- checkpoints
+- Langfuse / OTEL
+
+## O que foi comentado
+
+As implementações de exemplo dos agentes foram comentadas nos arquivos:
+
+- `app/agents/billing_agent.py`
+- `app/agents/product_agent.py`
+- `app/agents/orders_agent.py`
+- `app/agents/support_agent.py`
+
+Cada arquivo contém:
+
+1. um esqueleto funcional mínimo;
+2. comentários `TODO` para o desenvolvedor;
+3. a implementação original comentada no final do arquivo.
+
+## Como desenvolver um novo agente
+
+1. Escolha qual classe vai reutilizar inicialmente, por exemplo `BillingAgent`.
+2. Edite o método `run()`.
+3. Ajuste o prompt em `apply_agent_profile_prompt(...)`.
+4. Descomente MCP se precisar de tools:
+
+```python
+# tool_context = await self._collect_tool_context(state)
+```
+
+5. Descomente RAG se precisar de base de conhecimento:
+
+```python
+# rag_context, rag_metadata = await self._retrieve_rag_context(state)
+```
+
+6. Ajuste o retorno:
+
+```python
+return {
+ "answer": answer,
+ "next_state": "MEU_ESTADO",
+}
+```
+
+## O que o desenvolvedor normalmente altera
+
+- `app/agents/*.py`
+- `config/routing.yaml`
+- `config/agents.yaml`
+- `config/tools.yaml`
+- `config/mcp_servers.yaml`
+- `config/mcp_parameter_mapping.yaml`
+- `.env`
+
+## O que normalmente não deve ser alterado no início
+
+- `app/main.py`
+- `app/workflows/agent_graph.py`
+- `app/state.py`
+- `app/agents/runtime.py`
+
+Esses arquivos são o esqueleto de execução usando o framework.
+# Política opcional de tools
+
+O arquivo `config/tool_policies.yaml` classifica tools como `read_only` ou `transactional`. Para uma transação real, ative `require_confirmation: true`; chamadas sem `confirmed: true` ou `confirmation: true` serão bloqueadas antes do MCP. A ausência do arquivo preserva o comportamento de templates anteriores.
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/__init__.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/README_DESENVOLVEDOR.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/README_DESENVOLVEDOR.md
new file mode 100644
index 0000000..bd311da
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/README_DESENVOLVEDOR.md
@@ -0,0 +1,12 @@
+# Desenvolvimento de agentes
+
+Os arquivos `billing_agent.py`, `product_agent.py`, `orders_agent.py` e `support_agent.py` foram mantidos com os mesmos nomes do template completo para o workflow continuar compatível.
+
+A implementação de negócio original está comentada no final de cada arquivo.
+
+Para criar seu agente:
+
+1. Edite o método `run()` da classe desejada.
+2. Use o bloco comentado como referência.
+3. Depois, ajuste o roteamento em `config/routing.yaml`.
+4. Se quiser renomear classes/arquivos, atualize também os imports em `app/workflows/agent_graph.py`.
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/billing_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/billing_agent.py
new file mode 100644
index 0000000..aa60099
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/agents/orders_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/orders_agent.py
new file mode 100644
index 0000000..f557bed
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/agents/product_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/product_agent.py
new file mode 100644
index 0000000..34433f5
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/agents/prompting.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/prompting.py
new file mode 100644
index 0000000..255422b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/agents/runtime.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/runtime.py
new file mode 100644
index 0000000..e6429c4
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/runtime.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+# Compatibilidade local do template/backend.
+# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
+from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
+
+__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/support_agent.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/agents/support_agent.py
new file mode 100644
index 0000000..b4f0244
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/main.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/main.py
new file mode 100644
index 0000000..06d1bd1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/main.py
@@ -0,0 +1,532 @@
+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"],
+ "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"),
+ },
+ )
+ 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,
+ "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/Normal/templates/agent_template_backend_day_zero/app/mcp_gateway_client_factory.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/mcp_gateway_client_factory.py
new file mode 100644
index 0000000..5a32d15
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/observability/__init__.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/observability/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/observability/telemetry_observer.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/observability/telemetry_observer.py
new file mode 100644
index 0000000..92f07a1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/app/state.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/state.py
new file mode 100644
index 0000000..ac673d6
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/state.py
@@ -0,0 +1,51 @@
+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]
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
new file mode 100644
index 0000000..0a12c4b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
@@ -0,0 +1,816 @@
+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("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": "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 persist_long_term_memory(self, state):
+ result = await self.long_term_memory_manager.persist_turn(state)
+ return {"long_term_memory_write_result": result}
+
+ 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/Normal/templates/agent_template_backend_day_zero/config/agents.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents.yaml
new file mode 100644
index 0000000..1dd4299
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents.yaml
@@ -0,0 +1,38 @@
+# ============================================================================
+# DAY ZERO
+# Este arquivo foi copiado do agent_template_backend original.
+# Ajuste os exemplos abaixo para o domínio do seu novo agente.
+# ============================================================================
+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/Normal/templates/agent_template_backend_day_zero/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents/retail_orders/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents/retail_orders/judges.yaml
new file mode 100644
index 0000000..62fc7c7
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents/retail_orders/prompt_policy.yaml
new file mode 100644
index 0000000..f872a2b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents/telecom_contas/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents/telecom_contas/judges.yaml
new file mode 100644
index 0000000..d488063
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/agents/telecom_contas/prompt_policy.yaml
new file mode 100644
index 0000000..42732c4
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/guardrails.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/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/Normal/templates/agent_template_backend_day_zero/config/identity.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/identity.yaml
new file mode 100644
index 0000000..5f20147
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/judges.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/judges.yaml
new file mode 100644
index 0000000..c091619
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml
new file mode 100644
index 0000000..5b29ccf
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/mcp_servers.docker.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/mcp_servers.docker.yaml
new file mode 100644
index 0000000..8101130
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/mcp_servers.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/mcp_servers.yaml
new file mode 100644
index 0000000..fe638a2
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/prompt_policy.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/prompt_policy.yaml
new file mode 100644
index 0000000..af4398f
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/routing.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/routing.yaml
new file mode 100644
index 0000000..2dbe95e
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/config/tool_policies.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/Normal/templates/agent_template_backend_day_zero/config/tools.yaml b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/config/tools.yaml
new file mode 100644
index 0000000..d85fae1
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
new file mode 100644
index 0000000..d81efdf
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md
new file mode 100644
index 0000000..3f981ac
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/docs/DAY_ZERO_COMO_USAR.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/DAY_ZERO_COMO_USAR.md
new file mode 100644
index 0000000..37ce310
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/DAY_ZERO_COMO_USAR.md
@@ -0,0 +1,60 @@
+# Como usar o `agent_template_backend_day_zero`
+
+Este template é uma cópia do backend completo, mas com a lógica dos agentes de exemplo comentada.
+
+## Fluxo mantido
+
+```text
+Gateway / Canal
+ -> AgentWorkflow
+ -> Input Guardrails
+ -> Router / Supervisor Router
+ -> Agente
+ -> OutputSupervisor
+ -> Output Guardrails
+ -> Judges
+ -> Persistência
+```
+
+## Onde escrever código
+
+O ponto principal é o método `run()` dos agentes em `app/agents/`.
+
+A estrutura esperada pelo workflow é:
+
+```python
+async def run(self, state):
+ ...
+ return {
+ "answer": answer,
+ "next_state": "MEU_ESTADO"
+ }
+```
+
+## Como usar MCP
+
+Dentro de `run()`:
+
+```python
+tool_context = await self._collect_tool_context(state)
+```
+
+## Como usar RAG
+
+Dentro de `run()`:
+
+```python
+rag_context, rag_metadata = await self._retrieve_rag_context(state)
+```
+
+## Como chamar o LLM com cache/telemetria
+
+```python
+answer = await self._invoke_llm_cached(state, "MeuAgente", messages)
+```
+
+## Como ajustar roteamento
+
+Edite `config/routing.yaml`.
+
+O arquivo original foi mantido para servir de referência, mas as intents devem ser adaptadas para o domínio do novo agente.
diff --git a/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md
new file mode 100644
index 0000000..c7bd3b2
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md
new file mode 100644
index 0000000..bc2638b
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/requirements.txt b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/requirements.txt
new file mode 100644
index 0000000..71214bd
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/agent_template_backend_day_zero/scripts/test_long_term_memory.py b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/scripts/test_long_term_memory.py
new file mode 100644
index 0000000..52e2a8d
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/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/Normal/templates/shared/template_retail_orders_support/README.md b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/README.md
new file mode 100644
index 0000000..8f15a43
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/README.md
@@ -0,0 +1,15 @@
+# Template 2 — Retail/E-commerce: Pedidos + Suporte
+
+Este template demonstra outro uso do mesmo framework, com dois agentes diferentes:
+
+- `OrdersAgent`: status de pedido, entrega, troca, devolução e rastreamento.
+- `SupportAgent`: problemas de acesso, cadastro, pagamento, cupom e atendimento geral.
+
+A ideia é mostrar que o framework não é dependente de telecom. O desenvolvedor troca apenas:
+
+- intents em `routing.yaml`;
+- prompts dos agentes;
+- tools de negócio;
+- estados do workflow.
+
+O core de LangGraph, guardrails, judges, supervisor, Langfuse, OCI Generative AI e sessão permanece igual.
diff --git a/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/example_usage.py b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/example_usage.py
new file mode 100644
index 0000000..3ae80c4
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/example_usage.py
@@ -0,0 +1,19 @@
+payload_pedido = {
+ "channel": "web",
+ "payload": {
+ "text": "Meu pedido atrasou e quero rastrear a entrega.",
+ "user_id": "user-002",
+ "channel_id": "browser-002",
+ "context": {"order_id": "ORDER-123"},
+ },
+}
+
+payload_suporte = {
+ "channel": "web",
+ "payload": {
+ "text": "Não consigo fazer login e meu cupom não aplica.",
+ "user_id": "user-002",
+ "channel_id": "browser-002",
+ "context": {"customer_id": "CUST-123"},
+ },
+}
diff --git a/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/orders_agent.py b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/orders_agent.py
new file mode 100644
index 0000000..1c5e60e
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/orders_agent.py
@@ -0,0 +1,17 @@
+class OrdersAgent:
+ name = "orders_agent"
+
+ def __init__(self, llm, telemetry=None):
+ self.llm = llm
+ self.telemetry = telemetry
+
+ async def run(self, state):
+ # EXEMPLO DO TEMPLATE 2: agente de pedidos/e-commerce.
+ # Substitua por tools reais: consultar_pedido, rastrear_entrega,
+ # solicitar_devolucao, consultar_nota_fiscal etc.
+ messages = [
+ {"role": "system", "content": "Você é especialista em pedidos, entrega e devolução."},
+ {"role": "user", "content": state.get("sanitized_input") or state["user_text"]},
+ ]
+ answer = await self.llm.ainvoke(messages)
+ return {"answer": f"[OrdersAgent] {answer}", "next_state": "ORDER_ACTIVE"}
diff --git a/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/routing.yaml b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/routing.yaml
new file mode 100644
index 0000000..d2163ba
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/routing.yaml
@@ -0,0 +1,50 @@
+router:
+ fallback_agent: support_agent
+ confidence_threshold: 0.65
+ allow_handoff: true
+
+state_policies:
+ - state: WAITING_ORDER_CONFIRMATION
+ agent: orders_agent
+ description: Mantém confirmações curtas no fluxo de pedido.
+ - state: WAITING_SUPPORT_CONFIRMATION
+ agent: support_agent
+ description: Mantém confirmações curtas no fluxo de suporte.
+
+intents:
+ - name: order_status_delivery
+ agent: orders_agent
+ description: Status de pedido, entrega, rastreio, troca, devolução e cancelamento de compra.
+ priority: 10
+ keywords:
+ - pedido
+ - entrega
+ - rastreio
+ - rastrear
+ - transportadora
+ - troca
+ - devolução
+ - cancelar compra
+ - nota fiscal
+ examples:
+ - Quero saber onde está meu pedido.
+ - Preciso devolver um produto.
+ - Minha entrega atrasou.
+
+ - name: account_payment_support
+ agent: support_agent
+ description: Problemas de login, cadastro, pagamento, cupom e suporte geral.
+ priority: 20
+ keywords:
+ - login
+ - senha
+ - cadastro
+ - pagamento
+ - cartão
+ - cupom
+ - erro no site
+ - suporte
+ examples:
+ - Não consigo entrar na minha conta.
+ - Meu cupom não funciona.
+ - O pagamento foi recusado.
diff --git a/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/support_agent.py b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/support_agent.py
new file mode 100644
index 0000000..90567ba
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_retail_orders_support/support_agent.py
@@ -0,0 +1,17 @@
+class SupportAgent:
+ name = "support_agent"
+
+ def __init__(self, llm, telemetry=None):
+ self.llm = llm
+ self.telemetry = telemetry
+
+ async def run(self, state):
+ # EXEMPLO DO TEMPLATE 2: agente de suporte geral.
+ # Substitua por tools reais: reset_senha, validar_pagamento,
+ # consultar_cupom, abrir_ticket etc.
+ messages = [
+ {"role": "system", "content": "Você é especialista em suporte de conta, pagamento e uso do site."},
+ {"role": "user", "content": state.get("sanitized_input") or state["user_text"]},
+ ]
+ answer = await self.llm.ainvoke(messages)
+ return {"answer": f"[SupportAgent] {answer}", "next_state": "SUPPORT_ACTIVE"}
diff --git a/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/README.md b/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/README.md
new file mode 100644
index 0000000..d40f4f6
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/README.md
@@ -0,0 +1,15 @@
+# Template 1 — Telecom: Faturas + Produtos
+
+Este template demonstra dois agentes especializados:
+
+- `BillingAgent`: dúvidas de fatura, cobrança, vencimento e segunda via.
+- `ProductAgent`: dúvidas de plano, pacote, VAS, roaming e benefícios.
+
+O roteamento é definido por `config/routing.yaml` e usa:
+
+1. política por estado;
+2. keywords/intents;
+3. LLM router opcional;
+4. fallback.
+
+Use este template quando o atendimento tiver domínios de negócio separados mas precisar manter uma única sessão conversacional.
diff --git a/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/example_usage.py b/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/example_usage.py
new file mode 100644
index 0000000..b37d07c
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/example_usage.py
@@ -0,0 +1,21 @@
+"""Exemplo de payload para testar o template Telecom."""
+
+payload_fatura = {
+ "channel": "web",
+ "payload": {
+ "text": "Minha fatura veio muito alta este mês, pode explicar?",
+ "user_id": "user-001",
+ "channel_id": "browser-001",
+ "context": {"msisdn": "5511999999999", "invoice_id": "INV-123"},
+ },
+}
+
+payload_produto = {
+ "channel": "web",
+ "payload": {
+ "text": "Quais serviços VAS estão ativos no meu plano?",
+ "user_id": "user-001",
+ "channel_id": "browser-001",
+ "context": {"msisdn": "5511999999999", "asset_id": "ASSET-123"},
+ },
+}
diff --git a/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/routing.yaml b/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/routing.yaml
new file mode 100644
index 0000000..66a7a80
--- /dev/null
+++ b/Tuning-Performance/Normal/templates/shared/template_telecom_billing_product/routing.yaml
@@ -0,0 +1,53 @@
+# Roteamento enterprise configurável.
+# Este arquivo permite adicionar intents/agentes sem alterar o core do framework.
+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.
+
+intents:
+ - name: billing_invoice_explanation
+ agent: billing_agent
+ description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores.
+ priority: 10
+ 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
+ agent: product_agent
+ description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios.
+ priority: 20
+ keywords:
+ - plano
+ - produto
+ - 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?
diff --git a/Tuning-Performance/Route_Stickness/ROUTE_STICKINESS_HANDOFF_TRANSACOES_PT.md b/Tuning-Performance/Route_Stickness/ROUTE_STICKINESS_HANDOFF_TRANSACOES_PT.md
new file mode 100644
index 0000000..3caf86b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/ROUTE_STICKINESS_HANDOFF_TRANSACOES_PT.md
@@ -0,0 +1,290 @@
+# Route Stickiness, Handoff, Encerramento e Políticas MCP
+
+## 1. Objetivo
+
+Este material consolida o desenho de continuidade semântica de rota, transferência para atendimento humano, encerramento de sessão e proteção mínima de ferramentas MCP de consulta e transação no Agent Framework OCI.
+
+As capacidades são complementares:
+
+- **Route stickiness** decide se o turno permanece com o agente ativo ou volta ao Enterprise Router.
+- **Handoff e encerramento** tratam ações globais de sessão sem delegá-las a agentes de domínio.
+- **Políticas MCP** decidem se uma ferramenta já selecionada pode executar, com diferenciação entre `read_only` e `transactional`.
+
+Todas são opcionais e preservam o comportamento anterior quando desabilitadas ou não configuradas.
+
+## 2. Visão arquitetural
+
+```text
+Nova mensagem
+ |
+ +-- sessão já encerrada? -- sim --> rejeitar reutilização da sessão
+ |
+ +-- route stickiness habilitada --> classificador semântico leve
+ | |
+ | +-- CONTINUE + agente ativo --> agente ativo
+ | +-- ROUTE/baixa confiança/erro --> Enterprise Router
+ | +-- HUMAN_HANDOFF --> nó global human_handoff
+ | +-- END_SESSION --> nó global end_session
+ |
+ +-- route stickiness desabilitada --> Enterprise Router
+ |
+ v
+ agente de domínio
+ |
+ v
+ ferramenta MCP selecionada
+ |
+ v
+ política read-only/transacional
+ | |
+ permitida bloqueada
+ | |
+ v v
+ MCP Gateway/Server resposta segura
+```
+
+O classificador de continuidade não responde ao usuário, não escolhe outro agente, não executa ferramentas e não interpreta regras de negócio. O MCP Server continua sendo a autoridade final para autenticação, autorização, idempotência, validação e atomicidade.
+
+## 3. Decisões de continuidade e sessão
+
+| Decisão | Condição | Destino | Executa agente/MCP? |
+|---|---|---|---|
+| `CONTINUE` | A mensagem permanece no domínio do agente ativo e supera o threshold | agente ativo | sim, conforme o fluxo do agente |
+| `ROUTE` | Mudança de assunto, dúvida, baixa confiança ou falha | Enterprise Router | somente após nova rota |
+| `HUMAN_HANDOFF` | Solicitação de atendimento humano | nó global `human_handoff` | não |
+| `END_SESSION` | Solicitação ou confirmação de encerramento | nó global `end_session` | não |
+
+No primeiro turno, `CONTINUE` é normalizado para `ROUTE`, pois ainda não existe agente ativo. `HUMAN_HANDOFF` e `END_SESSION` podem ser identificados mesmo no primeiro turno.
+
+### 3.1 Por que a decisão é semântica
+
+Não são usadas regexes, listas de frases, pronomes ou palavras-chave específicas de idioma. O código mantém somente decisões técnicas:
+
+- feature flag;
+- existência de agente ativo;
+- threshold de confiança;
+- validação do contrato de saída;
+- fallback em timeout, erro ou JSON inválido.
+
+Isso evita manutenção de padrões linguísticos por domínio e mantém o comportamento multilíngue no perfil LLM.
+
+## 4. Contratos globais de sessão
+
+### 4.1 Handoff humano
+
+O router produz:
+
+```json
+{
+ "route": "human_handoff",
+ "intent": "human_handoff",
+ "method": "continuity",
+ "handoff": true,
+ "metadata": {
+ "session_control": "HUMAN_HANDOFF",
+ "route_bypassed": true
+ }
+}
+```
+
+O nó global define:
+
+- `session_control=HUMAN_HANDOFF`;
+- `human_handoff_requested=true`;
+- `session_ended=false`;
+- `next_state=HUMAN_HANDOFF_REQUESTED`.
+
+O evento `session.human_handoff.requested` permite que a integração escolha fila, fornecedor e protocolo. O framework não presume uma plataforma humana específica.
+
+### 4.2 Encerramento
+
+O router produz:
+
+```json
+{
+ "route": "end_session",
+ "intent": "end_session",
+ "method": "continuity",
+ "metadata": {
+ "session_control": "END_SESSION",
+ "route_bypassed": true
+ }
+}
+```
+
+O nó global define:
+
+- `session_control=END_SESSION`;
+- `session_ended=true`;
+- `human_handoff_requested=false`;
+- `next_state=SESSION_ENDED`.
+
+O evento `session.end.requested` deve ser emitido antes da persistência. Para encerramento definitivo, `session_ended=true` precisa ser persistido e novas mensagens com a mesma `session_id` devem ser bloqueadas antes de guardrails, roteamento, agentes, RAG, judges ou MCP.
+
+## 5. Políticas MCP read-only e transacionais
+
+### 5.1 Responsabilidade
+
+Depois que a rota e o agente selecionam uma ferramenta, o `MCPToolRouter` aplica a política imediatamente antes da chamada externa:
+
+- `read_only`: consulta sem alteração de estado; por padrão não exige confirmação.
+- `transactional`: operação que altera estado; pode exigir confirmação explícita e campos obrigatórios.
+
+A classificação não cria outro roteador LLM e não substitui a allowlist atual de ferramentas por agente/intenção.
+
+### 5.2 Configuração no backend
+
+```text
+templates/agent_template_backend/config/tool_policies.yaml
+```
+
+```dotenv
+TOOL_POLICIES_PATH=./config/tool_policies.yaml
+```
+
+```yaml
+version: 1
+
+defaults:
+ operation_type: read_only
+ require_confirmation: false
+
+tool_policies:
+ consultar_plano:
+ operation_type: read_only
+
+ alterar_plano:
+ operation_type: transactional
+ require_confirmation: true
+ requires: [new_plan_id]
+
+ cancelar_servico:
+ operation_type: transactional
+ require_confirmation: true
+```
+
+Uma transação confirmada deve receber um booleano literal:
+
+```json
+{
+ "new_plan_id": "CONTROLE_100",
+ "confirmed": true
+}
+```
+
+Também é aceito `"confirmation": true`. Strings como `"true"` não confirmam a operação.
+
+### 5.3 Compatibilidade
+
+- Se `tool_policies.yaml` não existir, o framework preserva `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
+- Tools antigas sem política executam como antes.
+- Uma política explícita no arquivo novo prevalece para tipo e confirmação daquela tool.
+- `tools.yaml` continua sendo a fonte de endpoint, schema, habilitação e cache.
+- A política fica no backend, e não em `libs/agent_framework`, porque varia por aplicação e domínio.
+
+## 6. Interação entre continuidade e transações
+
+Route stickiness não autoriza transações. Mesmo quando `CONTINUE` mantém o agente ativo, toda ferramenta passa novamente pelo controle MCP.
+
+Exemplo:
+
+```text
+Usuário: Quero mudar para o plano Controle 100.
+Agente: Confirma a alteração para o Controle 100?
+Usuário: Sim.
+ -> CONTINUE mantém product_agent
+ -> agente recupera a ação pendente
+ -> MCPToolRouter valida confirmation=true
+ -> alterar_plano executa
+```
+
+Em `HUMAN_HANDOFF` ou `END_SESSION`, nenhum agente de domínio ou MCP deve executar. Uma transação pendente deve ser invalidada ou mantida suspensa conforme política explícita da aplicação; nunca deve executar implicitamente após handoff ou encerramento.
+
+## 7. Configuração da continuidade
+
+```dotenv
+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.
+```
+
+```yaml
+profiles:
+ route_continuity:
+ provider: oci_openai
+ model: openai.gpt-4.1-mini
+ temperature: 0
+ max_tokens: 80
+ timeout_seconds: 5
+```
+
+Use o menor modelo aprovado no ambiente OCI. O classificador recebe apenas agente ativo, capacidades derivadas das intents, intent/domínio anteriores, histórico recente limitado e mensagem atual. Não recebe RAG completo, resultados MCP integrais, prompt do agente ou regras de negócio.
+
+## 8. Segurança e fallback
+
+- Apenas decisões acima do threshold são aceitas.
+- `CONTINUE` sem agente ativo vira `ROUTE`.
+- Baixa confiança, timeout, erro ou JSON inválido voltam ao Enterprise Router.
+- Handoff e encerramento não executam tools.
+- Confirmação transacional exige booleano literal.
+- A validação conversacional não substitui controles do MCP Server.
+- Sessões encerradas devem ser bloqueadas na entrada.
+- Retries de transações devem usar idempotência no serviço de destino.
+
+## 9. Telemetria
+
+Evento de continuidade:
+
+```json
+{
+ "decision": "CONTINUE",
+ "confidence": 0.97,
+ "active_agent": "product_agent",
+ "route_bypassed": true,
+ "profile_name": "route_continuity"
+}
+```
+
+Campos recomendados:
+
+- `route_decision.method`;
+- `active_agent`;
+- `route_bypassed`;
+- `continuity_signal`;
+- `session_control`;
+- `human_handoff_requested`;
+- `session_ended`;
+- `tool_name`;
+- `operation_type`;
+- `policy_source`;
+- `blocked_by_policy`.
+
+## 10. Testes e benchmark
+
+Casos mínimos:
+
+1. continuidade com bypass;
+2. mudança de domínio com retorno ao router;
+3. baixa confiança, timeout e JSON inválido;
+4. `CONTINUE` sem agente ativo;
+5. handoff e encerramento no primeiro turno e em turnos posteriores;
+6. bloqueio de mensagem após sessão encerrada;
+7. consulta sem confirmação;
+8. transação sem confirmação, com string e com booleano válido;
+9. campo obrigatório ausente;
+10. ausência de `tool_policies.yaml` usando comportamento legado.
+
+```bash
+PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
+```
+
+Para benchmark, compare a mesma conversa com a funcionalidade habilitada e desabilitada. Registre `route_bypassed`, chamadas ao `llm.router`, latência de `llm.route_continuity`, tokens por perfil e latência total p50/p95/p99. Só atribua ganho à stickiness quando houver `route_bypassed=true` e nenhuma geração do Enterprise Router no mesmo turno.
+
+## 11. Critério de adoção
+
+Adote route stickiness quando houver conversas multi-turno com retornos frequentes ao mesmo agente. Cadastre políticas apenas para ferramentas que precisem de comportamento adicional, começando pelas transações que exigem confirmação. Mantenha autorização, idempotência e regras de negócio no MCP Server para evitar duas fontes de verdade.
+
diff --git a/Tuning-Performance/Route_Stickness/ROUTE_STICKINESS_HANDOFF_TRANSACTIONS_EN.md b/Tuning-Performance/Route_Stickness/ROUTE_STICKINESS_HANDOFF_TRANSACTIONS_EN.md
new file mode 100644
index 0000000..8f3fbe8
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/ROUTE_STICKINESS_HANDOFF_TRANSACTIONS_EN.md
@@ -0,0 +1,289 @@
+# Route Stickiness, Handoff, Session End, and MCP Policies
+
+## 1. Purpose
+
+This document consolidates semantic route continuity, human handoff, session ending, and minimal protection for read-only and transactional MCP tools in the OCI Agent Framework.
+
+The capabilities are complementary:
+
+- **Route stickiness** decides whether a turn stays with the active agent or returns to the Enterprise Router.
+- **Handoff and session end** handle global session actions outside domain agents.
+- **MCP policies** decide whether an already selected tool may execute, distinguishing `read_only` from `transactional` operations.
+
+All capabilities are optional and preserve previous behavior when disabled or not configured.
+
+## 2. Architecture overview
+
+```text
+Incoming message
+ |
+ +-- session already ended? -- yes --> reject session reuse
+ |
+ +-- route stickiness enabled --> lightweight semantic classifier
+ | |
+ | +-- CONTINUE + active agent --> active agent
+ | +-- ROUTE/low confidence/error --> Enterprise Router
+ | +-- HUMAN_HANDOFF --> global human_handoff node
+ | +-- END_SESSION --> global end_session node
+ |
+ +-- route stickiness disabled --> Enterprise Router
+ |
+ v
+ domain agent
+ |
+ v
+ selected MCP tool
+ |
+ v
+ read-only/transactional policy
+ | |
+ allowed blocked
+ | |
+ v v
+ MCP Gateway/Server safe response
+```
+
+The continuity classifier does not answer users, choose another agent, execute tools, or interpret business rules. The MCP Server remains the final authority for authentication, authorization, idempotency, validation, and atomicity.
+
+## 3. Continuity and session decisions
+
+| Decision | Condition | Destination | Runs agent/MCP? |
+|---|---|---|---|
+| `CONTINUE` | The message remains in the active agent's domain and exceeds the threshold | active agent | yes, according to the agent flow |
+| `ROUTE` | Topic change, uncertainty, low confidence, or failure | Enterprise Router | only after routing |
+| `HUMAN_HANDOFF` | Human assistance requested | global `human_handoff` node | no |
+| `END_SESSION` | Session ending requested or confirmed | global `end_session` node | no |
+
+On the first turn, `CONTINUE` is normalized to `ROUTE` because no active agent exists. `HUMAN_HANDOFF` and `END_SESSION` may be detected on the first turn.
+
+### 3.1 Why the decision is semantic
+
+The implementation uses no regexes, phrase lists, pronoun lists, or language-specific keywords. Code retains only technical decisions:
+
+- feature flag;
+- presence of an active agent;
+- confidence threshold;
+- output-contract validation;
+- fallback on timeout, error, or invalid JSON.
+
+This avoids domain-specific language-pattern maintenance and keeps multilingual behavior in the LLM profile.
+
+## 4. Global session contracts
+
+### 4.1 Human handoff
+
+The router returns:
+
+```json
+{
+ "route": "human_handoff",
+ "intent": "human_handoff",
+ "method": "continuity",
+ "handoff": true,
+ "metadata": {
+ "session_control": "HUMAN_HANDOFF",
+ "route_bypassed": true
+ }
+}
+```
+
+The global node sets:
+
+- `session_control=HUMAN_HANDOFF`;
+- `human_handoff_requested=true`;
+- `session_ended=false`;
+- `next_state=HUMAN_HANDOFF_REQUESTED`.
+
+The `session.human_handoff.requested` event lets the integration select the queue, provider, and protocol. The framework assumes no specific human-service platform.
+
+### 4.2 Session end
+
+The router returns:
+
+```json
+{
+ "route": "end_session",
+ "intent": "end_session",
+ "method": "continuity",
+ "metadata": {
+ "session_control": "END_SESSION",
+ "route_bypassed": true
+ }
+}
+```
+
+The global node sets:
+
+- `session_control=END_SESSION`;
+- `session_ended=true`;
+- `human_handoff_requested=false`;
+- `next_state=SESSION_ENDED`.
+
+The `session.end.requested` event should be emitted before persistence. For definitive closure, `session_ended=true` must be persisted, and new messages using the same `session_id` must be blocked before guardrails, routing, agents, RAG, judges, or MCP.
+
+## 5. Read-only and transactional MCP policies
+
+### 5.1 Responsibility
+
+After routing and agent selection identify a tool, `MCPToolRouter` applies policy immediately before the external call:
+
+- `read_only`: retrieves data without changing state and does not require confirmation by default.
+- `transactional`: changes state and may require explicit confirmation and mandatory fields.
+
+This classification adds no LLM router and does not replace the existing per-agent/per-intent tool allowlist.
+
+### 5.2 Backend configuration
+
+```text
+templates/agent_template_backend/config/tool_policies.yaml
+```
+
+```dotenv
+TOOL_POLICIES_PATH=./config/tool_policies.yaml
+```
+
+```yaml
+version: 1
+
+defaults:
+ operation_type: read_only
+ require_confirmation: false
+
+tool_policies:
+ consultar_plano:
+ operation_type: read_only
+
+ alterar_plano:
+ operation_type: transactional
+ require_confirmation: true
+ requires: [new_plan_id]
+
+ cancelar_servico:
+ operation_type: transactional
+ require_confirmation: true
+```
+
+A confirmed transaction must receive a literal boolean:
+
+```json
+{
+ "new_plan_id": "CONTROLE_100",
+ "confirmed": true
+}
+```
+
+`"confirmation": true` is also accepted. Strings such as `"true"` do not confirm an operation.
+
+### 5.3 Compatibility
+
+- If `tool_policies.yaml` is absent, the framework preserves `tool_type`, `requires`, `confirmation_required`, and `execution_policy` from `tools.yaml`.
+- Legacy tools without policy execute as before.
+- An explicit entry in the new file takes precedence for that tool's type and confirmation behavior.
+- `tools.yaml` remains the source for endpoint, schema, enablement, and cache.
+- Policy belongs in the backend rather than `libs/agent_framework` because it varies by application and domain.
+
+## 6. Interaction between continuity and transactions
+
+Route stickiness does not authorize transactions. Even when `CONTINUE` retains the active agent, every tool passes through MCP policy again.
+
+Example:
+
+```text
+User: I want to switch to the Control 100 plan.
+Agent: Do you confirm the change to Control 100?
+User: Yes.
+ -> CONTINUE retains product_agent
+ -> agent retrieves the pending action
+ -> MCPToolRouter validates confirmation=true
+ -> alterar_plano executes
+```
+
+For `HUMAN_HANDOFF` or `END_SESSION`, no domain agent or MCP tool should execute. A pending transaction must be invalidated or remain suspended according to an explicit application policy; it must never execute implicitly after handoff or session end.
+
+## 7. Continuity configuration
+
+```dotenv
+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=I will transfer your request to a person.
+END_SESSION_MESSAGE=The session has ended. Thank you for contacting us.
+```
+
+```yaml
+profiles:
+ route_continuity:
+ provider: oci_openai
+ model: openai.gpt-4.1-mini
+ temperature: 0
+ max_tokens: 80
+ timeout_seconds: 5
+```
+
+Use the smallest approved model available in the OCI environment. The classifier receives only the active agent, capabilities derived from intents, previous intent/domain, limited recent history, and the current message. It receives no complete RAG context, full MCP results, agent prompt, or business rules.
+
+## 8. Safety and fallback
+
+- Only decisions above the configured threshold are accepted.
+- `CONTINUE` without an active agent becomes `ROUTE`.
+- Low confidence, timeout, error, or invalid JSON falls back to the Enterprise Router.
+- Handoff and session ending execute no tools.
+- Transaction confirmation requires a literal boolean.
+- Conversational validation does not replace MCP Server controls.
+- Ended sessions must be blocked at request entry.
+- Transaction retries require idempotency in the destination service.
+
+## 9. Telemetry
+
+Continuity event:
+
+```json
+{
+ "decision": "CONTINUE",
+ "confidence": 0.97,
+ "active_agent": "product_agent",
+ "route_bypassed": true,
+ "profile_name": "route_continuity"
+}
+```
+
+Recommended fields:
+
+- `route_decision.method`;
+- `active_agent`;
+- `route_bypassed`;
+- `continuity_signal`;
+- `session_control`;
+- `human_handoff_requested`;
+- `session_ended`;
+- `tool_name`;
+- `operation_type`;
+- `policy_source`;
+- `blocked_by_policy`.
+
+## 10. Tests and benchmark
+
+Minimum scenarios:
+
+1. continuity with router bypass;
+2. domain change with router fallback;
+3. low confidence, timeout, and invalid JSON;
+4. `CONTINUE` without an active agent;
+5. handoff and session end on first and subsequent turns;
+6. rejection of messages after session end;
+7. read-only call without confirmation;
+8. transaction without confirmation, with a string, and with a valid boolean;
+9. missing mandatory field;
+10. absent `tool_policies.yaml` using legacy behavior.
+
+```bash
+PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
+```
+
+For benchmarking, compare the same conversation with the feature enabled and disabled. Record `route_bypassed`, calls to `llm.router`, `llm.route_continuity` latency, tokens per profile, and total p50/p95/p99 latency. Attribute gains to stickiness only when `route_bypassed=true` and no Enterprise Router generation occurs in the same turn.
+
+## 11. Adoption criteria
+
+Adopt route stickiness for multi-turn conversations that frequently remain with the same agent. Register policies only for tools requiring additional behavior, starting with transactions that require confirmation. Keep authorization, idempotency, and business rules in the MCP Server to avoid competing sources of truth.
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/.env b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/.env
new file mode 100644
index 0000000..a8a666c
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/.env
@@ -0,0 +1,207 @@
+###############################################################################
+# 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_openai
+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/openai/v1
+OCI_GENAI_MODEL=openai.gpt-4.1
+OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
+OCI_GENAI_PROJECT_OCID=
+
+# OCI SDK / signer / profiles
+OCI_CONFIG_FILE=~/.oci/config
+OCI_PROFILE=DEFAULT
+OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+OCI_REGION=us-chicago-1
+
+###############################################################################
+# Persistência
+###############################################################################
+# Opções: memory, autonomous, mongodb
+SESSION_REPOSITORY_PROVIDER=sqlite
+MEMORY_REPOSITORY_PROVIDER=sqlite
+CHECKPOINT_REPOSITORY_PROVIDER=sqlite
+SQLITE_DB_PATH=./data/agent_framework.db
+
+# Autonomous Database
+ADB_USER=admin
+ADB_PASSWORD=fjhsdf04954hf
+ADB_DSN=oradb23aidev_high
+ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
+ADB_WALLET_PASSWORD=fjhsdf04954hf
+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=sqlite
+GRAPH_STORE_PROVIDER=sqlite
+RAG_TOP_K=5
+EMBEDDING_PROVIDER=mock
+OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
+RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
+
+###############################################################################
+# Observabilidade
+###############################################################################
+ENABLE_LANGFUSE=true
+LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
+LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
+LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
+LANGFUSE_HOST=http://localhost:3005
+ENABLE_OTEL=false
+OTEL_EXPORTER_OTLP_ENDPOINT=
+OTEL_SERVICE_NAME=ai-agent-template
+ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
+
+###############################################################################
+# 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=pubsub
+# 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.
+SESSION_ALREADY_ENDED_MESSAGE=Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.
+
+###############################################################################
+# MCP / Tools
+###############################################################################
+ENABLE_MCP_TOOLS=true
+MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
+TOOLS_CONFIG_PATH=./config/tools.yaml
+TOOL_POLICIES_PATH=./config/tool_policies.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=sqlite
+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
+
+###############################################################################
+# MCP Gateway
+###############################################################################
+# true = framework routes tool calls to the dedicated MCP Gateway.
+# false = framework calls MCP servers directly from mcp_servers.yaml.
+MCP_GATEWAY_ENABLED=true
+MCP_GATEWAY_URL=http://localhost:8300
+MCP_GATEWAY_TIMEOUT_SECONDS=60
+# MCP_GATEWAY_TOKEN=
+MCP_GATEWAY_AGENT_ID=telecom_contas
+MCP_GATEWAY_TENANT_ID=default
+
+###############################################################################
+# 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
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/Dockerfile b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/Dockerfile
new file mode 100644
index 0000000..273fe01
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/README.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/README.md
new file mode 100644
index 0000000..0cf81d7
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/README.md
@@ -0,0 +1,4213 @@
+# 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
+```
+
+
+
+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.
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/README_ENTERPRISE_TEMPLATE.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/README_ENTERPRISE_TEMPLATE.md
new file mode 100644
index 0000000..cae516e
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/__init__.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/README.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/README.md
new file mode 100644
index 0000000..2917425
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/agents/billing_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/billing_agent.py
new file mode 100644
index 0000000..aa60099
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/agents/orders_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/orders_agent.py
new file mode 100644
index 0000000..f557bed
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/agents/product_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/product_agent.py
new file mode 100644
index 0000000..34433f5
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/agents/prompting.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/prompting.py
new file mode 100644
index 0000000..255422b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/agents/runtime.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/runtime.py
new file mode 100644
index 0000000..e6429c4
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/runtime.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+# Compatibilidade local do template/backend.
+# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
+from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
+
+__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/support_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/agents/support_agent.py
new file mode 100644
index 0000000..b4f0244
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/examples/__init__.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/__init__.py
new file mode 100644
index 0000000..3f95e96
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/__init__.py
@@ -0,0 +1 @@
+"""Exemplos de uso do template backend enterprise."""
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/grl_examples.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/grl_examples.py
new file mode 100644
index 0000000..8dadac8
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/examples/ic_examples.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/ic_examples.py
new file mode 100644
index 0000000..f6daa57
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/examples/mcp_examples.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/mcp_examples.py
new file mode 100644
index 0000000..613f10c
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/examples/noc_examples.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/noc_examples.py
new file mode 100644
index 0000000..2b38a15
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/examples/observer_examples.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/examples/observer_examples.py
new file mode 100644
index 0000000..926b553
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/main.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/main.py
new file mode 100644
index 0000000..06d1bd1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/main.py
@@ -0,0 +1,532 @@
+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"],
+ "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"),
+ },
+ )
+ 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,
+ "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/Route_Stickness/templates/agent_template_backend/app/mcp_gateway_client_factory.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/mcp_gateway_client_factory.py
new file mode 100644
index 0000000..5a32d15
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/observability/__init__.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/observability/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/observability/telemetry_observer.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/observability/telemetry_observer.py
new file mode 100644
index 0000000..92f07a1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/app/state.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/state.py
new file mode 100644
index 0000000..ac673d6
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/state.py
@@ -0,0 +1,51 @@
+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]
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py
new file mode 100644
index 0000000..0a12c4b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py
@@ -0,0 +1,816 @@
+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("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": "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 persist_long_term_memory(self, state):
+ result = await self.long_term_memory_manager.persist_turn(state)
+ return {"long_term_memory_write_result": result}
+
+ 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/Route_Stickness/templates/agent_template_backend/config/agents.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents.yaml
new file mode 100644
index 0000000..7d245a5
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents/retail_orders/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents/retail_orders/judges.yaml
new file mode 100644
index 0000000..62fc7c7
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml
new file mode 100644
index 0000000..f872a2b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents/telecom_contas/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents/telecom_contas/judges.yaml
new file mode 100644
index 0000000..d488063
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml
new file mode 100644
index 0000000..42732c4
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/guardrails.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/guardrails.yaml
new file mode 100644
index 0000000..44887eb
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/identity.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/identity.yaml
new file mode 100644
index 0000000..5f20147
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/judges.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/judges.yaml
new file mode 100644
index 0000000..c091619
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/mcp_parameter_mapping.yaml
new file mode 100644
index 0000000..5b29ccf
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/mcp_servers.docker.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/mcp_servers.docker.yaml
new file mode 100644
index 0000000..8101130
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/mcp_servers.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/mcp_servers.yaml
new file mode 100644
index 0000000..fe638a2
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/prompt_policy.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/prompt_policy.yaml
new file mode 100644
index 0000000..af4398f
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/routing.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/routing.yaml
new file mode 100644
index 0000000..2dbe95e
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/config/tool_policies.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/Route_Stickness/templates/agent_template_backend/config/tools.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/config/tools.yaml
new file mode 100644
index 0000000..d85fae1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
new file mode 100644
index 0000000..d81efdf
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md
new file mode 100644
index 0000000..83975af
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md
new file mode 100644
index 0000000..3f981ac
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
new file mode 100644
index 0000000..5c41732
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md
new file mode 100644
index 0000000..c7bd3b2
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md
new file mode 100644
index 0000000..849fda1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md
new file mode 100644
index 0000000..edcd2c7
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md
new file mode 100644
index 0000000..bc2638b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md
new file mode 100644
index 0000000..a9e4458
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt
new file mode 100644
index 0000000..fac4bf4
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/llm_profiles.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/llm_profiles.yaml
new file mode 100644
index 0000000..908b382
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/requirements.txt b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/requirements.txt
new file mode 100644
index 0000000..71214bd
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend/scripts/test_long_term_memory.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend/scripts/test_long_term_memory.py
new file mode 100644
index 0000000..52e2a8d
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/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/Route_Stickness/templates/agent_template_backend_day_zero/.env b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/.env
new file mode 100644
index 0000000..31aa694
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/.env
@@ -0,0 +1,202 @@
+###############################################################################
+# 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_openai
+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/openai/v1
+OCI_GENAI_MODEL=openai.gpt-4.1
+OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
+OCI_GENAI_PROJECT_OCID=
+
+# OCI SDK / signer / profiles
+OCI_CONFIG_FILE=~/.oci/config
+OCI_PROFILE=DEFAULT
+OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+OCI_REGION=us-chicago-1
+
+###############################################################################
+# Persistência
+###############################################################################
+# Opções: memory, autonomous, mongodb
+SESSION_REPOSITORY_PROVIDER=sqlite
+MEMORY_REPOSITORY_PROVIDER=sqlite
+CHECKPOINT_REPOSITORY_PROVIDER=sqlite
+SQLITE_DB_PATH=./data/agent_framework.db
+
+# Autonomous Database
+ADB_USER=admin
+ADB_PASSWORD=fjhsdf04954hf
+ADB_DSN=oradb23aidev_high
+ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
+ADB_WALLET_PASSWORD=fjhsdf04954hf
+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=sqlite
+GRAPH_STORE_PROVIDER=sqlite
+RAG_TOP_K=5
+EMBEDDING_PROVIDER=mock
+OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
+RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
+
+###############################################################################
+# Observabilidade
+###############################################################################
+ENABLE_LANGFUSE=true
+LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
+LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
+LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
+LANGFUSE_HOST=http://localhost:3005
+ENABLE_OTEL=false
+OTEL_EXPORTER_OTLP_ENDPOINT=
+OTEL_SERVICE_NAME=ai-agent-template
+ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
+
+###############################################################################
+# 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=pubsub
+# 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
+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
+
+# Continuidade semântica, handoff humano e encerramento global.
+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.
+SESSION_ALREADY_ENDED_MESSAGE=Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.
+
+###############################################################################
+# MCP / Tools
+###############################################################################
+ENABLE_MCP_TOOLS=true
+MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
+TOOLS_CONFIG_PATH=./config/tools.yaml
+TOOL_POLICIES_PATH=./config/tool_policies.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=sqlite
+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
+
+###############################################################################
+# MCP Gateway
+###############################################################################
+# true = framework routes tool calls to the dedicated MCP Gateway.
+# false = framework calls MCP servers directly from mcp_servers.yaml.
+MCP_GATEWAY_ENABLED=true
+MCP_GATEWAY_URL=http://localhost:8300
+MCP_GATEWAY_TIMEOUT_SECONDS=60
+# MCP_GATEWAY_TOKEN=
+MCP_GATEWAY_AGENT_ID=telecom_contas
+MCP_GATEWAY_TENANT_ID=default
+
+###############################################################################
+# 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
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/Dockerfile b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/Dockerfile
new file mode 100644
index 0000000..e50bea7
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/Dockerfile
@@ -0,0 +1,6 @@
+FROM python:3.12-slim
+WORKDIR /app
+COPY agent_framework /agent_framework
+COPY agent_template_backend_day_zero /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/Route_Stickness/templates/agent_template_backend_day_zero/README_DAY_ZERO.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
new file mode 100644
index 0000000..869ed33
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
@@ -0,0 +1,89 @@
+# agent_template_backend_day_zero
+
+Este folder é uma cópia do `agent_template_backend`, porém transformada em um template **Day Zero**.
+
+A ideia é o desenvolvedor começar um agente novo sem apagar manualmente exemplos de negócio.
+
+## O que foi mantido
+
+Foi mantida a estrutura original do backend:
+
+- `app/main.py`
+- `app/workflows/agent_graph.py`
+- `app/state.py`
+- `app/agents/runtime.py`
+- `app/agents/prompting.py`
+- configurações em `config/`
+- integração com `agent_framework`
+- Analytics / Observer
+- NOC / GRL
+- OutputSupervisor
+- MCP Router
+- RAG
+- cache
+- memória
+- checkpoints
+- Langfuse / OTEL
+
+## O que foi comentado
+
+As implementações de exemplo dos agentes foram comentadas nos arquivos:
+
+- `app/agents/billing_agent.py`
+- `app/agents/product_agent.py`
+- `app/agents/orders_agent.py`
+- `app/agents/support_agent.py`
+
+Cada arquivo contém:
+
+1. um esqueleto funcional mínimo;
+2. comentários `TODO` para o desenvolvedor;
+3. a implementação original comentada no final do arquivo.
+
+## Como desenvolver um novo agente
+
+1. Escolha qual classe vai reutilizar inicialmente, por exemplo `BillingAgent`.
+2. Edite o método `run()`.
+3. Ajuste o prompt em `apply_agent_profile_prompt(...)`.
+4. Descomente MCP se precisar de tools:
+
+```python
+# tool_context = await self._collect_tool_context(state)
+```
+
+5. Descomente RAG se precisar de base de conhecimento:
+
+```python
+# rag_context, rag_metadata = await self._retrieve_rag_context(state)
+```
+
+6. Ajuste o retorno:
+
+```python
+return {
+ "answer": answer,
+ "next_state": "MEU_ESTADO",
+}
+```
+
+## O que o desenvolvedor normalmente altera
+
+- `app/agents/*.py`
+- `config/routing.yaml`
+- `config/agents.yaml`
+- `config/tools.yaml`
+- `config/mcp_servers.yaml`
+- `config/mcp_parameter_mapping.yaml`
+- `.env`
+
+## O que normalmente não deve ser alterado no início
+
+- `app/main.py`
+- `app/workflows/agent_graph.py`
+- `app/state.py`
+- `app/agents/runtime.py`
+
+Esses arquivos são o esqueleto de execução usando o framework.
+# Política opcional de tools
+
+O arquivo `config/tool_policies.yaml` classifica tools como `read_only` ou `transactional`. Para uma transação real, ative `require_confirmation: true`; chamadas sem `confirmed: true` ou `confirmation: true` serão bloqueadas antes do MCP. A ausência do arquivo preserva o comportamento de templates anteriores.
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/__init__.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/README_DESENVOLVEDOR.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/README_DESENVOLVEDOR.md
new file mode 100644
index 0000000..bd311da
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/README_DESENVOLVEDOR.md
@@ -0,0 +1,12 @@
+# Desenvolvimento de agentes
+
+Os arquivos `billing_agent.py`, `product_agent.py`, `orders_agent.py` e `support_agent.py` foram mantidos com os mesmos nomes do template completo para o workflow continuar compatível.
+
+A implementação de negócio original está comentada no final de cada arquivo.
+
+Para criar seu agente:
+
+1. Edite o método `run()` da classe desejada.
+2. Use o bloco comentado como referência.
+3. Depois, ajuste o roteamento em `config/routing.yaml`.
+4. Se quiser renomear classes/arquivos, atualize também os imports em `app/workflows/agent_graph.py`.
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/billing_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/billing_agent.py
new file mode 100644
index 0000000..aa60099
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/orders_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/orders_agent.py
new file mode 100644
index 0000000..f557bed
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/product_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/product_agent.py
new file mode 100644
index 0000000..34433f5
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/prompting.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/prompting.py
new file mode 100644
index 0000000..255422b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/runtime.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/runtime.py
new file mode 100644
index 0000000..e6429c4
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/runtime.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+# Compatibilidade local do template/backend.
+# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
+from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
+
+__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/support_agent.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/agents/support_agent.py
new file mode 100644
index 0000000..b4f0244
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/main.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/main.py
new file mode 100644
index 0000000..06d1bd1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/main.py
@@ -0,0 +1,532 @@
+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"],
+ "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"),
+ },
+ )
+ 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,
+ "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/Route_Stickness/templates/agent_template_backend_day_zero/app/mcp_gateway_client_factory.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/mcp_gateway_client_factory.py
new file mode 100644
index 0000000..5a32d15
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/observability/__init__.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/observability/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/observability/telemetry_observer.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/observability/telemetry_observer.py
new file mode 100644
index 0000000..92f07a1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/app/state.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/state.py
new file mode 100644
index 0000000..ac673d6
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/state.py
@@ -0,0 +1,51 @@
+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]
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
new file mode 100644
index 0000000..0a12c4b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
@@ -0,0 +1,816 @@
+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("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": "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 persist_long_term_memory(self, state):
+ result = await self.long_term_memory_manager.persist_turn(state)
+ return {"long_term_memory_write_result": result}
+
+ 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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents.yaml
new file mode 100644
index 0000000..1dd4299
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents.yaml
@@ -0,0 +1,38 @@
+# ============================================================================
+# DAY ZERO
+# Este arquivo foi copiado do agent_template_backend original.
+# Ajuste os exemplos abaixo para o domínio do seu novo agente.
+# ============================================================================
+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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/retail_orders/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/retail_orders/judges.yaml
new file mode 100644
index 0000000..62fc7c7
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/retail_orders/prompt_policy.yaml
new file mode 100644
index 0000000..f872a2b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/telecom_contas/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/telecom_contas/judges.yaml
new file mode 100644
index 0000000..d488063
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/agents/telecom_contas/prompt_policy.yaml
new file mode 100644
index 0000000..42732c4
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/guardrails.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/guardrails.yaml
new file mode 100644
index 0000000..9fe094a
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/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/Route_Stickness/templates/agent_template_backend_day_zero/config/identity.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/identity.yaml
new file mode 100644
index 0000000..5f20147
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/judges.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/judges.yaml
new file mode 100644
index 0000000..c091619
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml
new file mode 100644
index 0000000..5b29ccf
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/mcp_servers.docker.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/mcp_servers.docker.yaml
new file mode 100644
index 0000000..8101130
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/mcp_servers.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/mcp_servers.yaml
new file mode 100644
index 0000000..fe638a2
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/prompt_policy.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/prompt_policy.yaml
new file mode 100644
index 0000000..af4398f
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/routing.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/routing.yaml
new file mode 100644
index 0000000..2dbe95e
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/config/tool_policies.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/Route_Stickness/templates/agent_template_backend_day_zero/config/tools.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/config/tools.yaml
new file mode 100644
index 0000000..d85fae1
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
new file mode 100644
index 0000000..d81efdf
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md
new file mode 100644
index 0000000..3f981ac
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/docs/DAY_ZERO_COMO_USAR.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/DAY_ZERO_COMO_USAR.md
new file mode 100644
index 0000000..37ce310
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/DAY_ZERO_COMO_USAR.md
@@ -0,0 +1,60 @@
+# Como usar o `agent_template_backend_day_zero`
+
+Este template é uma cópia do backend completo, mas com a lógica dos agentes de exemplo comentada.
+
+## Fluxo mantido
+
+```text
+Gateway / Canal
+ -> AgentWorkflow
+ -> Input Guardrails
+ -> Router / Supervisor Router
+ -> Agente
+ -> OutputSupervisor
+ -> Output Guardrails
+ -> Judges
+ -> Persistência
+```
+
+## Onde escrever código
+
+O ponto principal é o método `run()` dos agentes em `app/agents/`.
+
+A estrutura esperada pelo workflow é:
+
+```python
+async def run(self, state):
+ ...
+ return {
+ "answer": answer,
+ "next_state": "MEU_ESTADO"
+ }
+```
+
+## Como usar MCP
+
+Dentro de `run()`:
+
+```python
+tool_context = await self._collect_tool_context(state)
+```
+
+## Como usar RAG
+
+Dentro de `run()`:
+
+```python
+rag_context, rag_metadata = await self._retrieve_rag_context(state)
+```
+
+## Como chamar o LLM com cache/telemetria
+
+```python
+answer = await self._invoke_llm_cached(state, "MeuAgente", messages)
+```
+
+## Como ajustar roteamento
+
+Edite `config/routing.yaml`.
+
+O arquivo original foi mantido para servir de referência, mas as intents devem ser adaptadas para o domínio do novo agente.
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
new file mode 100644
index 0000000..ba194c7
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
@@ -0,0 +1,13 @@
+# Exemplos implementados no template Day Zero
+
+O Day Zero preserva seu conteúdo simplificado, mas possui o mesmo conjunto transversal do template completo:
+
+- 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. Substitua os agentes e ferramentas de exemplo sem remover os controles transversais.
diff --git a/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md
new file mode 100644
index 0000000..c7bd3b2
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md
new file mode 100644
index 0000000..bc2638b
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/llm_profiles.yaml b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/llm_profiles.yaml
new file mode 100644
index 0000000..908b382
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/requirements.txt b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/requirements.txt
new file mode 100644
index 0000000..71214bd
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/agent_template_backend_day_zero/scripts/test_long_term_memory.py b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/scripts/test_long_term_memory.py
new file mode 100644
index 0000000..52e2a8d
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/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/Route_Stickness/templates/shared/template_retail_orders_support/README.md b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/README.md
new file mode 100644
index 0000000..8f15a43
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/README.md
@@ -0,0 +1,15 @@
+# Template 2 — Retail/E-commerce: Pedidos + Suporte
+
+Este template demonstra outro uso do mesmo framework, com dois agentes diferentes:
+
+- `OrdersAgent`: status de pedido, entrega, troca, devolução e rastreamento.
+- `SupportAgent`: problemas de acesso, cadastro, pagamento, cupom e atendimento geral.
+
+A ideia é mostrar que o framework não é dependente de telecom. O desenvolvedor troca apenas:
+
+- intents em `routing.yaml`;
+- prompts dos agentes;
+- tools de negócio;
+- estados do workflow.
+
+O core de LangGraph, guardrails, judges, supervisor, Langfuse, OCI Generative AI e sessão permanece igual.
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/example_usage.py b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/example_usage.py
new file mode 100644
index 0000000..3ae80c4
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/example_usage.py
@@ -0,0 +1,19 @@
+payload_pedido = {
+ "channel": "web",
+ "payload": {
+ "text": "Meu pedido atrasou e quero rastrear a entrega.",
+ "user_id": "user-002",
+ "channel_id": "browser-002",
+ "context": {"order_id": "ORDER-123"},
+ },
+}
+
+payload_suporte = {
+ "channel": "web",
+ "payload": {
+ "text": "Não consigo fazer login e meu cupom não aplica.",
+ "user_id": "user-002",
+ "channel_id": "browser-002",
+ "context": {"customer_id": "CUST-123"},
+ },
+}
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/orders_agent.py b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/orders_agent.py
new file mode 100644
index 0000000..1c5e60e
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/orders_agent.py
@@ -0,0 +1,17 @@
+class OrdersAgent:
+ name = "orders_agent"
+
+ def __init__(self, llm, telemetry=None):
+ self.llm = llm
+ self.telemetry = telemetry
+
+ async def run(self, state):
+ # EXEMPLO DO TEMPLATE 2: agente de pedidos/e-commerce.
+ # Substitua por tools reais: consultar_pedido, rastrear_entrega,
+ # solicitar_devolucao, consultar_nota_fiscal etc.
+ messages = [
+ {"role": "system", "content": "Você é especialista em pedidos, entrega e devolução."},
+ {"role": "user", "content": state.get("sanitized_input") or state["user_text"]},
+ ]
+ answer = await self.llm.ainvoke(messages)
+ return {"answer": f"[OrdersAgent] {answer}", "next_state": "ORDER_ACTIVE"}
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/routing.yaml b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/routing.yaml
new file mode 100644
index 0000000..d2163ba
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/routing.yaml
@@ -0,0 +1,50 @@
+router:
+ fallback_agent: support_agent
+ confidence_threshold: 0.65
+ allow_handoff: true
+
+state_policies:
+ - state: WAITING_ORDER_CONFIRMATION
+ agent: orders_agent
+ description: Mantém confirmações curtas no fluxo de pedido.
+ - state: WAITING_SUPPORT_CONFIRMATION
+ agent: support_agent
+ description: Mantém confirmações curtas no fluxo de suporte.
+
+intents:
+ - name: order_status_delivery
+ agent: orders_agent
+ description: Status de pedido, entrega, rastreio, troca, devolução e cancelamento de compra.
+ priority: 10
+ keywords:
+ - pedido
+ - entrega
+ - rastreio
+ - rastrear
+ - transportadora
+ - troca
+ - devolução
+ - cancelar compra
+ - nota fiscal
+ examples:
+ - Quero saber onde está meu pedido.
+ - Preciso devolver um produto.
+ - Minha entrega atrasou.
+
+ - name: account_payment_support
+ agent: support_agent
+ description: Problemas de login, cadastro, pagamento, cupom e suporte geral.
+ priority: 20
+ keywords:
+ - login
+ - senha
+ - cadastro
+ - pagamento
+ - cartão
+ - cupom
+ - erro no site
+ - suporte
+ examples:
+ - Não consigo entrar na minha conta.
+ - Meu cupom não funciona.
+ - O pagamento foi recusado.
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/support_agent.py b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/support_agent.py
new file mode 100644
index 0000000..90567ba
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_retail_orders_support/support_agent.py
@@ -0,0 +1,17 @@
+class SupportAgent:
+ name = "support_agent"
+
+ def __init__(self, llm, telemetry=None):
+ self.llm = llm
+ self.telemetry = telemetry
+
+ async def run(self, state):
+ # EXEMPLO DO TEMPLATE 2: agente de suporte geral.
+ # Substitua por tools reais: reset_senha, validar_pagamento,
+ # consultar_cupom, abrir_ticket etc.
+ messages = [
+ {"role": "system", "content": "Você é especialista em suporte de conta, pagamento e uso do site."},
+ {"role": "user", "content": state.get("sanitized_input") or state["user_text"]},
+ ]
+ answer = await self.llm.ainvoke(messages)
+ return {"answer": f"[SupportAgent] {answer}", "next_state": "SUPPORT_ACTIVE"}
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/README.md b/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/README.md
new file mode 100644
index 0000000..d40f4f6
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/README.md
@@ -0,0 +1,15 @@
+# Template 1 — Telecom: Faturas + Produtos
+
+Este template demonstra dois agentes especializados:
+
+- `BillingAgent`: dúvidas de fatura, cobrança, vencimento e segunda via.
+- `ProductAgent`: dúvidas de plano, pacote, VAS, roaming e benefícios.
+
+O roteamento é definido por `config/routing.yaml` e usa:
+
+1. política por estado;
+2. keywords/intents;
+3. LLM router opcional;
+4. fallback.
+
+Use este template quando o atendimento tiver domínios de negócio separados mas precisar manter uma única sessão conversacional.
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/example_usage.py b/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/example_usage.py
new file mode 100644
index 0000000..b37d07c
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/example_usage.py
@@ -0,0 +1,21 @@
+"""Exemplo de payload para testar o template Telecom."""
+
+payload_fatura = {
+ "channel": "web",
+ "payload": {
+ "text": "Minha fatura veio muito alta este mês, pode explicar?",
+ "user_id": "user-001",
+ "channel_id": "browser-001",
+ "context": {"msisdn": "5511999999999", "invoice_id": "INV-123"},
+ },
+}
+
+payload_produto = {
+ "channel": "web",
+ "payload": {
+ "text": "Quais serviços VAS estão ativos no meu plano?",
+ "user_id": "user-001",
+ "channel_id": "browser-001",
+ "context": {"msisdn": "5511999999999", "asset_id": "ASSET-123"},
+ },
+}
diff --git a/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/routing.yaml b/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/routing.yaml
new file mode 100644
index 0000000..66a7a80
--- /dev/null
+++ b/Tuning-Performance/Route_Stickness/templates/shared/template_telecom_billing_product/routing.yaml
@@ -0,0 +1,53 @@
+# Roteamento enterprise configurável.
+# Este arquivo permite adicionar intents/agentes sem alterar o core do framework.
+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.
+
+intents:
+ - name: billing_invoice_explanation
+ agent: billing_agent
+ description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores.
+ priority: 10
+ 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
+ agent: product_agent
+ description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios.
+ priority: 20
+ keywords:
+ - plano
+ - produto
+ - 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?
diff --git a/deploy/oke/.dockerignore b/deploy/oke/.dockerignore
new file mode 100644
index 0000000..cbb77e2
--- /dev/null
+++ b/deploy/oke/.dockerignore
@@ -0,0 +1,10 @@
+.git
+.idea
+__MACOSX
+**/.DS_Store
+**/__pycache__
+**/*.pyc
+.venv
+venv
+.env
+data/*.db
diff --git a/deploy/oke/README_OKE_DEPLOYMENT.md b/deploy/oke/README_OKE_DEPLOYMENT.md
new file mode 100644
index 0000000..40e4c62
--- /dev/null
+++ b/deploy/oke/README_OKE_DEPLOYMENT.md
@@ -0,0 +1,476 @@
+# Deployment do Agent Platform OCI em OCI OKE
+
+Este pacote adiciona os artefatos necessários para publicar o `agent_platform_oci` em um cluster **OCI OKE / Kubernetes**.
+
+O objetivo é atender a três pontos principais:
+
+1. Publicar o `agent_framework` como biblioteca dentro das imagens Python, permitindo imports como:
+
+ ```python
+ from agent_framework import ...
+ ```
+
+2. Implantar o `agent_template_backend` com múltiplos pods, `Service` interno e `HorizontalPodAutoscaler`, permitindo escalabilidade horizontal.
+
+3. Implantar os componentes externos da plataforma:
+
+ - `agent_gateway`
+ - `channel_gateway`
+ - `mcp_gateway`
+ - `agent_frontend`
+
+O desenho recomendado em OKE é:
+
+```text
+Usuário / Canal
+ |
+ | HTTP/S
+ v
+OCI Load Balancer
+ |
+ +--> agent_frontend Serviço LoadBalancer
+ +--> agent_gateway Serviço LoadBalancer
+ +--> channel_gateway Serviço LoadBalancer
+ +--> mcp_gateway Serviço LoadBalancer
+
+Dentro do cluster:
+
+agent_gateway ---> agent_template_backend Service ---> vários pods do agente
+agent_backend ---> mcp_gateway Service
+mcp_gateway ---> MCP servers internos ou externos
+```
+
+> Observação: este pacote deixa os gateways como serviços externos `LoadBalancer`, conforme solicitado. Em produção, é comum expor apenas o `channel_gateway`, `agent_gateway` ou um Ingress/API Gateway corporativo, mantendo `mcp_gateway` interno.
+
+---
+
+## Estrutura criada
+
+```text
+deploy/oke/
+ README_OKE_DEPLOYMENT.md
+ .dockerignore
+ dockerfiles/
+ Dockerfile.agent-template-backend
+ Dockerfile.agent-gateway
+ Dockerfile.channel-gateway
+ Dockerfile.mcp-gateway
+ Dockerfile.agent-frontend
+ nginx/
+ default.conf
+ k8s/base/
+ 00-namespace.yaml
+ 01-configmap.yaml
+ 02-secret-template.yaml
+ 03-agent-template-backend.yaml
+ 04-agent-gateway.yaml
+ 05-channel-gateway.yaml
+ 06-mcp-gateway.yaml
+ 07-frontend.yaml
+ kustomization.yaml
+ scripts/
+ build_images.sh
+ push_images.sh
+ create_runtime_secret.sh
+ deploy_oke.sh
+ status.sh
+ examples/
+ oke.env.example
+```
+
+---
+
+## Por que foram criados novos Dockerfiles
+
+Os Dockerfiles existentes usam caminhos relativos ao diretório da aplicação, por exemplo:
+
+```dockerfile
+COPY agent_framework /agent_framework
+COPY agent_template_backend /app
+```
+
+No repositório atual, o framework está em:
+
+```text
+libs/agent_framework
+```
+
+E o backend está em:
+
+```text
+templates/agent_template_backend
+```
+
+Por isso, os Dockerfiles de OKE usam a **raiz do repositório como build context** e fazem:
+
+```dockerfile
+COPY libs/agent_framework /opt/agent_framework
+RUN pip install -e /opt/agent_framework
+```
+
+Assim, o `agent_framework` fica instalado como biblioteca Python dentro das imagens dos componentes que precisam dele.
+
+---
+
+## Pré-requisitos
+
+Na máquina de build/deploy:
+
+- Docker
+- `kubectl`
+- OCI CLI configurado
+- Acesso ao cluster OKE
+- Acesso ao OCIR
+- Usuário OCI com permissões para push no OCIR
+- Token de autenticação OCI para login no Docker Registry
+
+Login no OCIR:
+
+```bash
+docker login .ocir.io
+```
+
+Exemplo para São Paulo:
+
+```bash
+docker login gru.ocir.io
+```
+
+O usuário normalmente segue o formato:
+
+```text
+/
+```
+
+---
+
+## 1. Configurar o arquivo de ambiente
+
+Copie o exemplo:
+
+```bash
+cp deploy/oke/examples/oke.env.example deploy/oke/oke.env
+```
+
+Edite:
+
+```bash
+vi deploy/oke/oke.env
+```
+
+Campos principais:
+
+```bash
+OCI_REGION=sa-saopaulo-1
+OCI_REGION_KEY=gru
+OCI_TENANCY_NAMESPACE=your_tenancy_namespace
+OCIR_REPOSITORY_PREFIX=agent-platform-oci
+IMAGE_TAG=1.0.0
+K8S_NAMESPACE=agent-platform
+OKE_CLUSTER_OCID=ocid1.cluster.oc1..example
+```
+
+Para usar OCI Generative AI em vez de mock:
+
+```bash
+LLM_PROVIDER=oci_openai
+OCI_GENAI_BASE_URL=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com/openai/v1
+OCI_GENAI_MODEL=
+OCI_GENAI_API_KEY=
+OCI_COMPARTMENT_ID=
+```
+
+Para primeiro teste sem custo de LLM, deixe:
+
+```bash
+LLM_PROVIDER=mock
+```
+
+---
+
+## 2. Build das imagens
+
+Execute a partir da raiz do projeto:
+
+```bash
+./deploy/oke/scripts/build_images.sh deploy/oke/oke.env
+```
+
+Imagens geradas:
+
+```text
+agent-template-backend
+agent-gateway
+channel-gateway
+mcp-gateway
+agent-frontend
+```
+
+Todas serão tagueadas no padrão:
+
+```text
+.ocir.io///:
+```
+
+Exemplo:
+
+```text
+gru.ocir.io/mytenancy/agent-platform-oci/agent-template-backend:1.0.0
+```
+
+---
+
+## 3. Push para OCIR
+
+```bash
+./deploy/oke/scripts/push_images.sh deploy/oke/oke.env
+```
+
+---
+
+## 4. Criar secrets de runtime
+
+O script abaixo cria ou atualiza o secret `agent-platform-secrets` no namespace configurado:
+
+```bash
+./deploy/oke/scripts/create_runtime_secret.sh deploy/oke/oke.env
+```
+
+O arquivo `02-secret-template.yaml` existe apenas como referência. Não coloque credenciais reais no Git.
+
+---
+
+## 5. Fazer deploy no OKE
+
+```bash
+./deploy/oke/scripts/deploy_oke.sh deploy/oke/oke.env
+```
+
+O script:
+
+1. Opcionalmente atualiza o kubeconfig via OCI CLI, se `OKE_CLUSTER_OCID` estiver preenchido.
+2. Cria/atualiza o namespace.
+3. Cria/atualiza os secrets.
+4. Aplica os manifests com Kustomize.
+5. Aguarda o rollout dos deployments.
+6. Lista os serviços e IPs externos.
+
+---
+
+## 6. Verificar status
+
+```bash
+./deploy/oke/scripts/status.sh
+```
+
+Ou manualmente:
+
+```bash
+kubectl -n agent-platform get pods -o wide
+kubectl -n agent-platform get svc
+kubectl -n agent-platform get hpa
+```
+
+Quando o Load Balancer estiver provisionado, os serviços externos aparecerão com `EXTERNAL-IP`:
+
+```bash
+kubectl -n agent-platform get svc agent-gateway
+kubectl -n agent-platform get svc channel-gateway
+kubectl -n agent-platform get svc mcp-gateway
+kubectl -n agent-platform get svc agent-frontend
+```
+
+---
+
+## 7. Testes rápidos
+
+Health do backend interno:
+
+```bash
+kubectl -n agent-platform port-forward svc/agent-template-backend 8000:8000
+curl http://localhost:8000/health
+```
+
+Health do Agent Gateway:
+
+```bash
+kubectl -n agent-platform port-forward svc/agent-gateway 8010:8010
+curl http://localhost:8010/health
+```
+
+Envio de mensagem pelo Agent Gateway:
+
+```bash
+curl -X POST http://localhost:8010/gateway/message \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "channel": "web",
+ "tenant_id": "default",
+ "payload": {
+ "message": "quero consultar minha fatura",
+ "metadata": {
+ "customer_key": "11999999999"
+ }
+ }
+ }'
+```
+
+---
+
+## Escalabilidade
+
+O `agent_template_backend` foi configurado com:
+
+```yaml
+replicas: 3
+```
+
+E com HPA:
+
+```yaml
+minReplicas: 3
+maxReplicas: 10
+averageUtilization: 70
+```
+
+Ajuste em:
+
+```text
+deploy/oke/k8s/base/03-agent-template-backend.yaml
+```
+
+O Load Balancer externo fica nos gateways e no frontend. O backend do agente é `ClusterIP`, porque o acesso deve ocorrer via gateway.
+
+---
+
+## Sobre estado, sessão e persistência
+
+O manifesto usa `emptyDir` para `/data`, suficiente para smoke test e validação inicial.
+
+Para produção, substitua SQLite por um provider externo:
+
+- Autonomous Database
+- MongoDB
+- Redis para cache distribuído
+- Object Storage ou banco para artefatos persistentes
+
+Não use SQLite local com múltiplos pods em produção para sessão, memória, checkpoints e usage, porque cada pod teria seu próprio estado.
+
+Configurações relevantes no `ConfigMap`:
+
+```yaml
+SESSION_REPOSITORY_PROVIDER: "sqlite"
+MEMORY_REPOSITORY_PROVIDER: "sqlite"
+CHECKPOINT_REPOSITORY_PROVIDER: "sqlite"
+USAGE_REPOSITORY_PROVIDER: "sqlite"
+```
+
+Para produção, altere esses providers e injete as credenciais por `Secret`.
+
+---
+
+## Ajuste do Agent Gateway para vários agentes
+
+O arquivo:
+
+```text
+deploy/oke/k8s/base/01-configmap.yaml
+```
+
+cria o `ConfigMap` `agent-gateway-backends` com:
+
+```yaml
+backends:
+ contas:
+ url: http://agent-template-backend.agent-platform.svc.cluster.local:8000
+```
+
+Para adicionar novos agentes, crie novos deployments e serviços, depois adicione novas entradas:
+
+```yaml
+backends:
+ contas:
+ url: http://agent-contas.agent-platform.svc.cluster.local:8000
+ ofertas:
+ url: http://agent-ofertas.agent-platform.svc.cluster.local:8000
+ suporte:
+ url: http://agent-suporte.agent-platform.svc.cluster.local:8000
+```
+
+---
+
+## Ajuste do MCP Gateway
+
+O `mcp_gateway` é implantado com configuração vazia por padrão:
+
+```yaml
+servers: {}
+tools: {}
+```
+
+Edite o `ConfigMap` `mcp-gateway-config` em:
+
+```text
+deploy/oke/k8s/base/01-configmap.yaml
+```
+
+Exemplo:
+
+```yaml
+servers:
+ telecom:
+ enabled: true
+ discover: true
+ protocol: legacy_http
+ transport: http
+ url: http://telecom-mcp.agent-platform.svc.cluster.local:8100/mcp
+ timeout_seconds: 30
+```
+
+---
+
+## Frontend
+
+O frontend foi empacotado em Nginx e exposto com `Service LoadBalancer`.
+
+Como o frontend atual é estático, o endereço do gateway pode ser informado na própria interface, caso ela já tenha campo de backend/gateway. Caso você queira fixar o endpoint em build/runtime, o próximo ajuste recomendado é adicionar um arquivo `/config.js` gerado por `ConfigMap` com a URL pública do `agent_gateway`.
+
+---
+
+## Segurança recomendada para produção
+
+Para produção, recomenda-se:
+
+1. Usar `ClusterIP` para `mcp_gateway` e expor apenas via rede privada.
+2. Usar OCI API Gateway ou Ingress Controller com TLS.
+3. Criar `NetworkPolicy` restringindo tráfego entre namespaces.
+4. Usar OCI Vault/External Secrets para credenciais.
+5. Usar Workload Identity ou Instance Principal quando aplicável.
+6. Usar Autonomous Database ou MongoDB externo para estado.
+7. Configurar observabilidade com OTel/Langfuse.
+8. Separar namespaces por ambiente: `dev`, `test`, `prod`.
+9. Não versionar `.env` nem secrets reais.
+
+---
+
+## Comandos principais
+
+```bash
+cp deploy/oke/examples/oke.env.example deploy/oke/oke.env
+vi deploy/oke/oke.env
+
+./deploy/oke/scripts/build_images.sh deploy/oke/oke.env
+./deploy/oke/scripts/push_images.sh deploy/oke/oke.env
+./deploy/oke/scripts/deploy_oke.sh deploy/oke/oke.env
+./deploy/oke/scripts/status.sh
+```
+
+---
+
+## Próximos passos recomendados
+
+1. Criar manifests separados por ambiente com overlays Kustomize: `dev`, `hml`, `prod`.
+2. Criar pipeline OCI DevOps ou GitHub Actions para build/push/deploy.
+3. Adicionar Ingress/API Gateway com TLS.
+4. Migrar estado de SQLite para Autonomous/MongoDB antes de produção.
+5. Criar manifests específicos para cada agente real derivado do `agent_template_backend`.
diff --git a/deploy/oke/dockerfiles/Dockerfile.agent-frontend b/deploy/oke/dockerfiles/Dockerfile.agent-frontend
new file mode 100644
index 0000000..38e1b15
--- /dev/null
+++ b/deploy/oke/dockerfiles/Dockerfile.agent-frontend
@@ -0,0 +1,4 @@
+FROM nginx:1.27-alpine
+COPY deploy/oke/nginx/default.conf /etc/nginx/conf.d/default.conf
+COPY apps/agent_frontend /usr/share/nginx/html
+EXPOSE 8080
diff --git a/deploy/oke/dockerfiles/Dockerfile.agent-gateway b/deploy/oke/dockerfiles/Dockerfile.agent-gateway
new file mode 100644
index 0000000..80fac31
--- /dev/null
+++ b/deploy/oke/dockerfiles/Dockerfile.agent-gateway
@@ -0,0 +1,23 @@
+FROM python:3.12-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1 \
+ PYTHONPATH=/app
+
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY libs/agent_framework /opt/agent_framework
+COPY apps/agent_gateway/requirements.txt /tmp/requirements.txt
+RUN pip install --upgrade pip \
+ && pip install -e /opt/agent_framework \
+ && pip install -r /tmp/requirements.txt
+
+COPY apps/agent_gateway /app
+
+EXPOSE 8010
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"]
diff --git a/deploy/oke/dockerfiles/Dockerfile.agent-template-backend b/deploy/oke/dockerfiles/Dockerfile.agent-template-backend
new file mode 100644
index 0000000..8932d0d
--- /dev/null
+++ b/deploy/oke/dockerfiles/Dockerfile.agent-template-backend
@@ -0,0 +1,23 @@
+FROM python:3.12-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1 \
+ PYTHONPATH=/app
+
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY libs/agent_framework /opt/agent_framework
+COPY templates/agent_template_backend/requirements.txt /tmp/requirements.txt
+RUN pip install --upgrade pip \
+ && pip install -e /opt/agent_framework \
+ && pip install -r /tmp/requirements.txt
+
+COPY templates/agent_template_backend /app
+
+EXPOSE 8000
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/deploy/oke/dockerfiles/Dockerfile.channel-gateway b/deploy/oke/dockerfiles/Dockerfile.channel-gateway
new file mode 100644
index 0000000..2e6ebcc
--- /dev/null
+++ b/deploy/oke/dockerfiles/Dockerfile.channel-gateway
@@ -0,0 +1,21 @@
+FROM python:3.12-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1 \
+ PYTHONPATH=/app
+
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY apps/channel_gateway/requirements.txt /tmp/requirements.txt
+RUN pip install --upgrade pip \
+ && pip install -r /tmp/requirements.txt
+
+COPY apps/channel_gateway /app
+
+EXPOSE 7000
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7000"]
diff --git a/deploy/oke/dockerfiles/Dockerfile.mcp-gateway b/deploy/oke/dockerfiles/Dockerfile.mcp-gateway
new file mode 100644
index 0000000..b6bfb9c
--- /dev/null
+++ b/deploy/oke/dockerfiles/Dockerfile.mcp-gateway
@@ -0,0 +1,22 @@
+FROM python:3.12-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1 \
+ PYTHONPATH=/app \
+ MCP_GATEWAY_CONFIG_PATH=/app/config/mcp_gateway.yaml
+
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY apps/mcp_gateway/requirements.txt /tmp/requirements.txt
+RUN pip install --upgrade pip \
+ && pip install -r /tmp/requirements.txt
+
+COPY apps/mcp_gateway /app
+
+EXPOSE 8300
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8300"]
diff --git a/deploy/oke/examples/oke.env.example b/deploy/oke/examples/oke.env.example
new file mode 100644
index 0000000..e54d0fe
--- /dev/null
+++ b/deploy/oke/examples/oke.env.example
@@ -0,0 +1,25 @@
+# OCI / OKE / OCIR
+OCI_REGION=sa-saopaulo-1
+OCI_REGION_KEY=gru
+OCI_TENANCY_NAMESPACE=your_tenancy_namespace
+OCIR_REPOSITORY_PREFIX=agent-platform-oci
+IMAGE_TAG=1.0.0
+
+# Kubernetes
+K8S_NAMESPACE=agent-platform
+OKE_CLUSTER_OCID=ocid1.cluster.oc1..example
+
+# Optional: configure kubeconfig automatically with OCI CLI
+OCI_CLI_PROFILE=DEFAULT
+
+# Runtime config
+LLM_PROVIDER=oci_openai
+OCI_GENAI_BASE_URL=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com/openai/v1
+OCI_GENAI_MODEL=cohere.command-r-plus
+OCI_GENAI_API_KEY=replace-me
+OCI_GENAI_PROJECT_OCID=
+OCI_COMPARTMENT_ID=ocid1.compartment.oc1..example
+LANGFUSE_HOST=
+LANGFUSE_PUBLIC_KEY=
+LANGFUSE_SECRET_KEY=
+MCP_GATEWAY_TOKEN=
diff --git a/deploy/oke/k8s/base/00-namespace.yaml b/deploy/oke/k8s/base/00-namespace.yaml
new file mode 100644
index 0000000..af570e6
--- /dev/null
+++ b/deploy/oke/k8s/base/00-namespace.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: agent-platform
diff --git a/deploy/oke/k8s/base/01-configmap.yaml b/deploy/oke/k8s/base/01-configmap.yaml
new file mode 100644
index 0000000..94da75a
--- /dev/null
+++ b/deploy/oke/k8s/base/01-configmap.yaml
@@ -0,0 +1,91 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: agent-platform-config
+ namespace: agent-platform
+data:
+ APP_ENV: "oke"
+ LOG_LEVEL: "INFO"
+ CORS_ORIGINS: "*"
+ LLM_PROVIDER: "mock"
+ LLM_TEMPERATURE: "0.2"
+ LLM_MAX_TOKENS: "2048"
+ SESSION_REPOSITORY_PROVIDER: "sqlite"
+ MEMORY_REPOSITORY_PROVIDER: "sqlite"
+ CHECKPOINT_REPOSITORY_PROVIDER: "sqlite"
+ SQLITE_DB_PATH: "/data/agent_framework.db"
+ USAGE_REPOSITORY_PROVIDER: "sqlite"
+ VECTOR_STORE_PROVIDER: "sqlite"
+ GRAPH_STORE_PROVIDER: "sqlite"
+ EMBEDDING_PROVIDER: "mock"
+ ENABLE_LANGFUSE: "false"
+ ENABLE_OTEL: "false"
+ ENABLE_ANALYTICS: "false"
+ ENABLE_INPUT_GUARDRAILS: "true"
+ ENABLE_OUTPUT_GUARDRAILS: "true"
+ ENABLE_JUDGES: "true"
+ ENABLE_SUPERVISOR: "true"
+ ENABLE_OUTPUT_SUPERVISOR: "true"
+ ENABLE_PARALLEL_GUARDRAILS: "true"
+ FRAMEWORK_CHANNEL_INPUT_MODE: "embedded"
+ ENABLE_MCP_TOOLS: "true"
+ MCP_GATEWAY_ENABLED: "true"
+ MCP_GATEWAY_URL: "http://mcp-gateway.agent-platform.svc.cluster.local:8300"
+ AGENTS_CONFIG_PATH: "./config/agents.yaml"
+ ROUTING_CONFIG_PATH: "./config/routing.yaml"
+ GUARDRAILS_CONFIG_PATH: "./config/guardrails.yaml"
+ JUDGES_CONFIG_PATH: "./config/judges.yaml"
+ PROMPT_POLICY_PATH: "./config/prompt_policy.yaml"
+ IDENTITY_CONFIG_PATH: "./config/identity.yaml"
+ MCP_PARAMETER_MAPPING_PATH: "./config/mcp_parameter_mapping.yaml"
+ BACKENDS_CONFIG_PATH: "/app/config/backends.yaml"
+ CHANNEL_GATEWAY_RUNTIME_MODE: "proxy"
+ DEFAULT_AGENT_BACKEND_URL: "http://agent-template-backend.agent-platform.svc.cluster.local:8000"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: agent-gateway-backends
+ namespace: agent-platform
+data:
+ backends.yaml: |
+ default_backend: contas
+ backends:
+ contas:
+ url: http://agent-template-backend.agent-platform.svc.cluster.local:8000
+ description: Backend principal do template de agentes.
+ 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
+ priority: 10
+ default_agent_id: telecom_contas
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: mcp-gateway-config
+ namespace: agent-platform
+data:
+ mcp_gateway.yaml: |
+ discovery:
+ enabled: true
+ sync_on_startup: true
+ timeout_seconds: 10
+ default_catalog_endpoints: [/.well-known/mcp-server.json, /manifest, /mcp/tools, /tools/list, /tools, /v1/tools]
+ tool_defaults:
+ version: 1.0.0
+ protocol: legacy_http
+ enabled: true
+ idempotent: true
+ cache_ttl_seconds: 300
+ timeout_seconds: 30
+ retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
+ allowed_agents: []
+ allowed_channels: []
+ required_business_keys: []
+ servers: {}
+ tools: {}
+ mcp_servers.yaml: |
+ servers: {}
diff --git a/deploy/oke/k8s/base/02-secret-template.yaml b/deploy/oke/k8s/base/02-secret-template.yaml
new file mode 100644
index 0000000..0b2b01d
--- /dev/null
+++ b/deploy/oke/k8s/base/02-secret-template.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: agent-platform-secrets
+ namespace: agent-platform
+type: Opaque
+stringData:
+ OCI_GENAI_BASE_URL: ""
+ OCI_GENAI_API_KEY: ""
+ OCI_GENAI_MODEL: ""
+ OCI_GENAI_PROJECT_OCID: ""
+ OCI_COMPARTMENT_ID: ""
+ OCI_REGION: ""
+ LANGFUSE_PUBLIC_KEY: ""
+ LANGFUSE_SECRET_KEY: ""
+ LANGFUSE_HOST: ""
+ MCP_GATEWAY_TOKEN: ""
diff --git a/deploy/oke/k8s/base/03-agent-template-backend.yaml b/deploy/oke/k8s/base/03-agent-template-backend.yaml
new file mode 100644
index 0000000..2ecbc2c
--- /dev/null
+++ b/deploy/oke/k8s/base/03-agent-template-backend.yaml
@@ -0,0 +1,72 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: agent-template-backend
+ namespace: agent-platform
+spec:
+ replicas: 3
+ selector:
+ matchLabels: {app: agent-template-backend}
+ template:
+ metadata:
+ labels: {app: agent-template-backend}
+ spec:
+ containers:
+ - name: agent-template-backend
+ image: agent-template-backend:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ envFrom:
+ - configMapRef: {name: agent-platform-config}
+ - secretRef: {name: agent-platform-secrets}
+ readinessProbe:
+ httpGet: {path: /health, port: 8000}
+ initialDelaySeconds: 20
+ periodSeconds: 10
+ livenessProbe:
+ httpGet: {path: /health, port: 8000}
+ initialDelaySeconds: 40
+ periodSeconds: 20
+ resources:
+ requests: {cpu: "250m", memory: "512Mi"}
+ limits: {cpu: "1000m", memory: "1Gi"}
+ volumeMounts:
+ - name: data
+ mountPath: /data
+ volumes:
+ - name: data
+ emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: agent-template-backend
+ namespace: agent-platform
+spec:
+ type: ClusterIP
+ selector: {app: agent-template-backend}
+ ports:
+ - name: http
+ port: 8000
+ targetPort: 8000
+---
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: agent-template-backend
+ namespace: agent-platform
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: agent-template-backend
+ minReplicas: 3
+ maxReplicas: 10
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 70
diff --git a/deploy/oke/k8s/base/04-agent-gateway.yaml b/deploy/oke/k8s/base/04-agent-gateway.yaml
new file mode 100644
index 0000000..99a4c8d
--- /dev/null
+++ b/deploy/oke/k8s/base/04-agent-gateway.yaml
@@ -0,0 +1,57 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: agent-gateway
+ namespace: agent-platform
+spec:
+ replicas: 2
+ selector:
+ matchLabels: {app: agent-gateway}
+ template:
+ metadata:
+ labels: {app: agent-gateway}
+ spec:
+ containers:
+ - name: agent-gateway
+ image: agent-gateway:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8010
+ envFrom:
+ - configMapRef: {name: agent-platform-config}
+ - secretRef: {name: agent-platform-secrets}
+ volumeMounts:
+ - name: backends
+ mountPath: /app/config/backends.yaml
+ subPath: backends.yaml
+ readinessProbe:
+ httpGet: {path: /health, port: 8010}
+ initialDelaySeconds: 15
+ periodSeconds: 10
+ livenessProbe:
+ httpGet: {path: /health, port: 8010}
+ initialDelaySeconds: 30
+ periodSeconds: 20
+ resources:
+ requests: {cpu: "200m", memory: "256Mi"}
+ limits: {cpu: "1000m", memory: "768Mi"}
+ volumes:
+ - name: backends
+ configMap: {name: agent-gateway-backends}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: agent-gateway
+ namespace: agent-platform
+ annotations:
+ service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"
+spec:
+ type: LoadBalancer
+ selector: {app: agent-gateway}
+ ports:
+ - name: http
+ port: 80
+ targetPort: 8010
diff --git a/deploy/oke/k8s/base/05-channel-gateway.yaml b/deploy/oke/k8s/base/05-channel-gateway.yaml
new file mode 100644
index 0000000..95731c8
--- /dev/null
+++ b/deploy/oke/k8s/base/05-channel-gateway.yaml
@@ -0,0 +1,49 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: channel-gateway
+ namespace: agent-platform
+spec:
+ replicas: 2
+ selector:
+ matchLabels: {app: channel-gateway}
+ template:
+ metadata:
+ labels: {app: channel-gateway}
+ spec:
+ containers:
+ - name: channel-gateway
+ image: channel-gateway:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 7000
+ envFrom:
+ - configMapRef: {name: agent-platform-config}
+ readinessProbe:
+ httpGet: {path: /health, port: 7000}
+ initialDelaySeconds: 15
+ periodSeconds: 10
+ livenessProbe:
+ httpGet: {path: /health, port: 7000}
+ initialDelaySeconds: 30
+ periodSeconds: 20
+ resources:
+ requests: {cpu: "100m", memory: "128Mi"}
+ limits: {cpu: "500m", memory: "512Mi"}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: channel-gateway
+ namespace: agent-platform
+ annotations:
+ service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"
+spec:
+ type: LoadBalancer
+ selector: {app: channel-gateway}
+ ports:
+ - name: http
+ port: 80
+ targetPort: 7000
diff --git a/deploy/oke/k8s/base/06-mcp-gateway.yaml b/deploy/oke/k8s/base/06-mcp-gateway.yaml
new file mode 100644
index 0000000..7836432
--- /dev/null
+++ b/deploy/oke/k8s/base/06-mcp-gateway.yaml
@@ -0,0 +1,62 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mcp-gateway
+ namespace: agent-platform
+spec:
+ replicas: 2
+ selector:
+ matchLabels: {app: mcp-gateway}
+ template:
+ metadata:
+ labels: {app: mcp-gateway}
+ spec:
+ containers:
+ - name: mcp-gateway
+ image: mcp-gateway:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8300
+ envFrom:
+ - secretRef: {name: agent-platform-secrets}
+ env:
+ - name: MCP_GATEWAY_CONFIG_PATH
+ value: /app/config/mcp_gateway.yaml
+ volumeMounts:
+ - name: config
+ mountPath: /app/config/mcp_gateway.yaml
+ subPath: mcp_gateway.yaml
+ - name: config
+ mountPath: /app/config/mcp_servers.yaml
+ subPath: mcp_servers.yaml
+ readinessProbe:
+ httpGet: {path: /health, port: 8300}
+ initialDelaySeconds: 15
+ periodSeconds: 10
+ livenessProbe:
+ httpGet: {path: /health, port: 8300}
+ initialDelaySeconds: 30
+ periodSeconds: 20
+ resources:
+ requests: {cpu: "100m", memory: "128Mi"}
+ limits: {cpu: "500m", memory: "512Mi"}
+ volumes:
+ - name: config
+ configMap: {name: mcp-gateway-config}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mcp-gateway
+ namespace: agent-platform
+ annotations:
+ service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"
+spec:
+ type: LoadBalancer
+ selector: {app: mcp-gateway}
+ ports:
+ - name: http
+ port: 80
+ targetPort: 8300
diff --git a/deploy/oke/k8s/base/07-frontend.yaml b/deploy/oke/k8s/base/07-frontend.yaml
new file mode 100644
index 0000000..0885f44
--- /dev/null
+++ b/deploy/oke/k8s/base/07-frontend.yaml
@@ -0,0 +1,47 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: agent-frontend
+ namespace: agent-platform
+spec:
+ replicas: 2
+ selector:
+ matchLabels: {app: agent-frontend}
+ template:
+ metadata:
+ labels: {app: agent-frontend}
+ spec:
+ containers:
+ - name: agent-frontend
+ image: agent-frontend:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8080
+ readinessProbe:
+ httpGet: {path: /health, port: 8080}
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ livenessProbe:
+ httpGet: {path: /health, port: 8080}
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ resources:
+ requests: {cpu: "50m", memory: "64Mi"}
+ limits: {cpu: "250m", memory: "256Mi"}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: agent-frontend
+ namespace: agent-platform
+ annotations:
+ service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
+ service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"
+spec:
+ type: LoadBalancer
+ selector: {app: agent-frontend}
+ ports:
+ - name: http
+ port: 80
+ targetPort: 8080
diff --git a/deploy/oke/k8s/base/kustomization.yaml b/deploy/oke/k8s/base/kustomization.yaml
new file mode 100644
index 0000000..4ec8d0f
--- /dev/null
+++ b/deploy/oke/k8s/base/kustomization.yaml
@@ -0,0 +1,11 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - 00-namespace.yaml
+ - 01-configmap.yaml
+ - 02-secret-template.yaml
+ - 03-agent-template-backend.yaml
+ - 04-agent-gateway.yaml
+ - 05-channel-gateway.yaml
+ - 06-mcp-gateway.yaml
+ - 07-frontend.yaml
diff --git a/deploy/oke/nginx/default.conf b/deploy/oke/nginx/default.conf
new file mode 100644
index 0000000..dfaafe3
--- /dev/null
+++ b/deploy/oke/nginx/default.conf
@@ -0,0 +1,16 @@
+server {
+ listen 8080;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /health {
+ access_log off;
+ return 200 "ok\n";
+ }
+}
diff --git a/deploy/oke/scripts/build_images.sh b/deploy/oke/scripts/build_images.sh
new file mode 100644
index 0000000..8710f1d
--- /dev/null
+++ b/deploy/oke/scripts/build_images.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+ENV_FILE="${1:-$ROOT_DIR/deploy/oke/examples/oke.env.example}"
+
+if [[ -f "$ENV_FILE" ]]; then
+ set -a
+ # shellcheck disable=SC1090
+ source "$ENV_FILE"
+ set +a
+fi
+
+: "${OCI_REGION_KEY:?Set OCI_REGION_KEY, for example gru, iad, phx}"
+: "${OCI_TENANCY_NAMESPACE:?Set OCI_TENANCY_NAMESPACE}"
+: "${OCIR_REPOSITORY_PREFIX:=agent-platform-oci}"
+: "${IMAGE_TAG:=latest}"
+
+REGISTRY="${OCI_REGION_KEY}.ocir.io/${OCI_TENANCY_NAMESPACE}/${OCIR_REPOSITORY_PREFIX}"
+
+echo "Building images with root context: $ROOT_DIR"
+echo "Registry prefix: $REGISTRY"
+
+docker build -f "$ROOT_DIR/deploy/oke/dockerfiles/Dockerfile.agent-template-backend" -t "$REGISTRY/agent-template-backend:$IMAGE_TAG" "$ROOT_DIR"
+docker build -f "$ROOT_DIR/deploy/oke/dockerfiles/Dockerfile.agent-gateway" -t "$REGISTRY/agent-gateway:$IMAGE_TAG" "$ROOT_DIR"
+docker build -f "$ROOT_DIR/deploy/oke/dockerfiles/Dockerfile.channel-gateway" -t "$REGISTRY/channel-gateway:$IMAGE_TAG" "$ROOT_DIR"
+docker build -f "$ROOT_DIR/deploy/oke/dockerfiles/Dockerfile.mcp-gateway" -t "$REGISTRY/mcp-gateway:$IMAGE_TAG" "$ROOT_DIR"
+docker build -f "$ROOT_DIR/deploy/oke/dockerfiles/Dockerfile.agent-frontend" -t "$REGISTRY/agent-frontend:$IMAGE_TAG" "$ROOT_DIR"
+
+echo "Images built:"
+echo "$REGISTRY/agent-template-backend:$IMAGE_TAG"
+echo "$REGISTRY/agent-gateway:$IMAGE_TAG"
+echo "$REGISTRY/channel-gateway:$IMAGE_TAG"
+echo "$REGISTRY/mcp-gateway:$IMAGE_TAG"
+echo "$REGISTRY/agent-frontend:$IMAGE_TAG"
diff --git a/deploy/oke/scripts/create_runtime_secret.sh b/deploy/oke/scripts/create_runtime_secret.sh
new file mode 100644
index 0000000..3a8f401
--- /dev/null
+++ b/deploy/oke/scripts/create_runtime_secret.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+ENV_FILE="${1:-$ROOT_DIR/deploy/oke/examples/oke.env.example}"
+
+if [[ -f "$ENV_FILE" ]]; then
+ set -a
+ # shellcheck disable=SC1090
+ source "$ENV_FILE"
+ set +a
+fi
+
+NS="${K8S_NAMESPACE:-agent-platform}"
+
+kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
+
+kubectl -n "$NS" create secret generic agent-platform-secrets \
+ --from-literal=OCI_GENAI_BASE_URL="${OCI_GENAI_BASE_URL:-}" \
+ --from-literal=OCI_GENAI_API_KEY="${OCI_GENAI_API_KEY:-}" \
+ --from-literal=OCI_GENAI_MODEL="${OCI_GENAI_MODEL:-}" \
+ --from-literal=OCI_GENAI_PROJECT_OCID="${OCI_GENAI_PROJECT_OCID:-}" \
+ --from-literal=OCI_COMPARTMENT_ID="${OCI_COMPARTMENT_ID:-}" \
+ --from-literal=OCI_REGION="${OCI_REGION:-}" \
+ --from-literal=LANGFUSE_PUBLIC_KEY="${LANGFUSE_PUBLIC_KEY:-}" \
+ --from-literal=LANGFUSE_SECRET_KEY="${LANGFUSE_SECRET_KEY:-}" \
+ --from-literal=LANGFUSE_HOST="${LANGFUSE_HOST:-}" \
+ --from-literal=MCP_GATEWAY_TOKEN="${MCP_GATEWAY_TOKEN:-}" \
+ --dry-run=client -o yaml | kubectl apply -f -
diff --git a/deploy/oke/scripts/deploy_oke.sh b/deploy/oke/scripts/deploy_oke.sh
new file mode 100644
index 0000000..f081f5c
--- /dev/null
+++ b/deploy/oke/scripts/deploy_oke.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+ENV_FILE="${1:-$ROOT_DIR/deploy/oke/examples/oke.env.example}"
+
+if [[ -f "$ENV_FILE" ]]; then
+ set -a
+ # shellcheck disable=SC1090
+ source "$ENV_FILE"
+ set +a
+fi
+
+: "${OCI_REGION_KEY:?Set OCI_REGION_KEY}"
+: "${OCI_TENANCY_NAMESPACE:?Set OCI_TENANCY_NAMESPACE}"
+: "${OCIR_REPOSITORY_PREFIX:=agent-platform-oci}"
+: "${IMAGE_TAG:=latest}"
+
+NS="${K8S_NAMESPACE:-agent-platform}"
+REGISTRY="${OCI_REGION_KEY}.ocir.io/${OCI_TENANCY_NAMESPACE}/${OCIR_REPOSITORY_PREFIX}"
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+if [[ -n "${OKE_CLUSTER_OCID:-}" && -n "${OCI_REGION:-}" ]]; then
+ echo "Updating kubeconfig for OKE cluster $OKE_CLUSTER_OCID"
+ oci ce cluster create-kubeconfig \
+ --cluster-id "$OKE_CLUSTER_OCID" \
+ --file "$HOME/.kube/config" \
+ --region "$OCI_REGION" \
+ --token-version 2.0.0 \
+ --kube-endpoint PUBLIC_ENDPOINT \
+ ${OCI_CLI_PROFILE:+--profile "$OCI_CLI_PROFILE"} || true
+fi
+
+"$ROOT_DIR/deploy/oke/scripts/create_runtime_secret.sh" "$ENV_FILE"
+
+cp -R "$ROOT_DIR/deploy/oke/k8s/base"/* "$TMP_DIR/"
+
+cat > "$TMP_DIR/kustomization.yaml" <-py3-none-any.whl
+ └── agent_framework-.tar.gz
+
+Registry privado
+ ├── Azure DevOps Artifacts [recomendado para PyPI privado]
+ └── GitHub Release Assets [alternativa quando o código está no GitHub]
+
+Agentes
+ └── pip install agent-framework==
+```
+
+## Importante sobre GitHub Packages
+
+GitHub Packages é um serviço de packages, mas no momento ele não oferece um registry Python/PyPI compatível como Azure Artifacts, GitLab Package Registry, Nexus, Artifactory ou PyPI. Por isso, para GitHub foram incluídas duas alternativas práticas:
+
+1. publicar o wheel/sdist em **GitHub Releases**;
+2. usar GitHub Actions para publicar em um registry PyPI compatível externo, como PyPI, TestPyPI, Nexus, Artifactory ou outro registry privado.
+
+## Artefatos incluídos
+
+```text
+deploy/package-registry/
+├── README_AGENT_FRAMEWORK_PACKAGE_REGISTRY.md
+├── scripts/
+│ ├── build_agent_framework.sh
+│ ├── publish_azure_artifacts_local.sh
+│ ├── install_from_azure_artifacts.sh
+│ └── create_github_release_package.sh
+
+azure-pipelines-agent-framework-publish.yml
+.github/workflows/
+├── agent-framework-build-release.yml
+└── agent-framework-publish-pypi.yml
+
+templates/agent_template_backend/examples/
+├── requirements.azure-artifacts.example.txt
+├── requirements.github-release.example.txt
+└── Dockerfile.registry-consumer.example
+```
+
+---
+
+# 1. Build local do pacote
+
+Execute a partir da raiz do projeto:
+
+```bash
+./deploy/package-registry/scripts/build_agent_framework.sh
+```
+
+Saída esperada:
+
+```text
+libs/agent_framework/dist/
+├── agent_framework-0.1.0-py3-none-any.whl
+└── agent_framework-0.1.0.tar.gz
+```
+
+A versão vem de:
+
+```text
+libs/agent_framework/pyproject.toml
+```
+
+Exemplo:
+
+```toml
+[project]
+name = "agent-framework"
+version = "0.1.0"
+```
+
+O import Python continua sendo:
+
+```python
+import agent_framework
+```
+
+Mesmo que o nome do pacote publicado seja `agent-framework`.
+
+---
+
+# 2. Publicação no Azure DevOps Artifacts
+
+## 2.1 Criar feed
+
+No Azure DevOps:
+
+```text
+Artifacts > Create Feed
+```
+
+Sugestão:
+
+```text
+agent-framework-feed
+```
+
+Permissões necessárias para a pipeline:
+
+```text
+Feed Publisher / Contributor
+```
+
+## 2.2 Pipeline Azure DevOps
+
+Arquivo incluído na raiz:
+
+```text
+azure-pipelines-agent-framework-publish.yml
+```
+
+O trecho principal usa `TwineAuthenticate@1` e depois publica com `twine`:
+
+```yaml
+- task: TwineAuthenticate@1
+ inputs:
+ artifactFeed: '$(azureFeed)'
+
+- script: |
+ cd $(frameworkDir)
+ python -m twine upload -r agent-framework-feed --config-file "$(PYPIRC_PATH)" dist/*
+```
+
+Ajuste a variável se seu feed tiver outro nome:
+
+```yaml
+variables:
+ azureFeed: '$(System.TeamProject)/agent-framework-feed'
+```
+
+Para feed em escopo de organização, use apenas:
+
+```yaml
+azureFeed: 'agent-framework-feed'
+```
+
+## 2.3 Publicação local no Azure Artifacts
+
+Exemplo:
+
+```bash
+export AZURE_ORG="minha-org"
+export AZURE_PROJECT="AgentPlatform"
+export AZURE_FEED="agent-framework-feed"
+export AZURE_PAT="***"
+
+./deploy/package-registry/scripts/build_agent_framework.sh
+./deploy/package-registry/scripts/publish_azure_artifacts_local.sh
+```
+
+O PAT precisa de permissão:
+
+```text
+Packaging: Read & Write
+```
+
+## 2.4 Consumo pelo agente
+
+Exemplo de instalação local:
+
+```bash
+export AZURE_ORG="minha-org"
+export AZURE_PROJECT="AgentPlatform"
+export AZURE_FEED="agent-framework-feed"
+export AZURE_PAT="***"
+export AGENT_FRAMEWORK_VERSION="0.1.0"
+
+./deploy/package-registry/scripts/install_from_azure_artifacts.sh
+```
+
+No `requirements.txt` do agente, a dependência deve ficar assim:
+
+```text
+agent-framework==0.1.0
+```
+
+A URL e credenciais do feed devem ser passadas no build do Docker, não gravadas no arquivo.
+
+---
+
+# 3. Consumo em Dockerfile do agente
+
+Exemplo incluído:
+
+```text
+templates/agent_template_backend/examples/Dockerfile.registry-consumer.example
+```
+
+Uso com Azure Artifacts:
+
+```bash
+docker build \
+ -f templates/agent_template_backend/examples/Dockerfile.registry-consumer.example \
+ --build-arg PIP_INDEX_URL="https://azdo:${AZURE_PAT}@pkgs.dev.azure.com/${AZURE_ORG}/${AZURE_PROJECT}/_packaging/${AZURE_FEED}/pypi/simple/" \
+ --build-arg PIP_EXTRA_INDEX_URL="https://pypi.org/simple" \
+ --build-arg AGENT_FRAMEWORK_VERSION="0.1.0" \
+ -t agent-template-backend:0.1.0 \
+ .
+```
+
+Recomendação de segurança para pipeline:
+
+- nunca commitar PAT;
+- usar secret variable;
+- usar Docker BuildKit secret quando possível;
+- limitar o PAT a Packaging Read para build de consumidores.
+
+---
+
+# 4. GitHub
+
+## 4.1 GitHub Releases como distribuição de wheel
+
+Arquivo incluído:
+
+```text
+.github/workflows/agent-framework-build-release.yml
+```
+
+Esse workflow é acionado por tags:
+
+```bash
+git tag agent-framework-v0.1.0
+git push origin agent-framework-v0.1.0
+```
+
+Ele gera:
+
+```text
+libs/agent_framework/dist/*.whl
+libs/agent_framework/dist/*.tar.gz
+```
+
+E publica como assets de uma GitHub Release.
+
+Publicação local com GitHub CLI:
+
+```bash
+export GITHUB_REPOSITORY="org/agent_platform_oci"
+export GITHUB_TOKEN="***"
+export PACKAGE_VERSION="0.1.0"
+
+./deploy/package-registry/scripts/build_agent_framework.sh
+./deploy/package-registry/scripts/create_github_release_package.sh
+```
+
+## 4.2 Instalação a partir de GitHub Release
+
+Exemplo:
+
+```text
+agent-framework @ https://github.com///releases/download/agent-framework-v0.1.0/agent_framework-0.1.0-py3-none-any.whl
+```
+
+Para repositório privado, o build precisa de token com permissão de leitura no repositório.
+
+## 4.3 GitHub Actions para PyPI-compatible registry
+
+Arquivo incluído:
+
+```text
+.github/workflows/agent-framework-publish-pypi.yml
+```
+
+Use este workflow para publicar em um registry compatível com PyPI:
+
+- PyPI;
+- TestPyPI;
+- Nexus;
+- Artifactory;
+- outro registry privado compatível.
+
+Secrets esperados:
+
+```text
+PYPI_REPOSITORY_URL
+PYPI_USERNAME
+PYPI_PASSWORD
+```
+
+---
+
+# 5. Ajuste no `agent_template_backend`
+
+O `agent_template_backend` deve parar de instalar o framework por path local em produção.
+
+Uso recomendado:
+
+```text
+agent-framework==0.1.0
+```
+
+Exemplo completo:
+
+```text
+templates/agent_template_backend/examples/requirements.azure-artifacts.example.txt
+```
+
+Em desenvolvimento local, você ainda pode usar modo editável:
+
+```bash
+pip install -e libs/agent_framework
+```
+
+Mas em OKE/produção, use sempre pacote versionado.
+
+---
+
+# 6. Fluxo recomendado de release
+
+```bash
+# 1. Atualizar versão
+vi libs/agent_framework/pyproject.toml
+
+# 2. Build local e validação
+./deploy/package-registry/scripts/build_agent_framework.sh
+
+# 3. Commit
+git add libs/agent_framework/pyproject.toml deploy/package-registry .github/workflows azure-pipelines-agent-framework-publish.yml
+git commit -m "Publish agent-framework package registry artifacts"
+
+# 4. Tag
+git tag agent-framework-v0.1.0
+git push origin main --tags
+```
+
+A partir daí:
+
+- Azure DevOps publica no Azure Artifacts;
+- GitHub Actions publica wheel/sdist em GitHub Release;
+- agentes consomem `agent-framework==0.1.0`.
+
+---
+
+# 7. Relação com OKE
+
+No OKE, os Deployments continuam sendo apenas das aplicações:
+
+```text
+agent_template_backend
+agent_gateway
+channel_gateway
+mcp_gateway
+frontend
+```
+
+O `agent_framework` entra dentro da imagem Docker dessas aplicações durante o build via:
+
+```bash
+pip install agent-framework==0.1.0
+```
+
+Portanto, não existe:
+
+```text
+Deployment agent-framework
+Service agent-framework
+Pod agent-framework
+LoadBalancer agent-framework
+```
+
+Existe apenas uma dependência versionada instalada dentro dos containers.
+
+---
+
+# 8. Estratégia de versionamento
+
+Sugestão SemVer:
+
+```text
+MAJOR.MINOR.PATCH
+```
+
+Exemplos:
+
+```text
+0.1.0 primeira versão empacotada
+0.2.0 nova funcionalidade compatível
+0.2.1 correção sem quebra
+1.0.0 baseline corporativa estável
+```
+
+Para agentes críticos, fixe a versão:
+
+```text
+agent-framework==1.0.0
+```
+
+Evite em produção:
+
+```text
+agent-framework>=1.0.0
+```
+
+---
+
+# 9. Conclusão
+
+A arquitetura correta é tratar o `agent_framework` como biblioteca corporativa versionada, publicada em um registry privado e consumida pelos agentes durante o build.
+
+Para o seu cenário, a recomendação principal é:
+
+```text
+Azure DevOps Artifacts = registry Python privado principal
+GitHub Releases = distribuição alternativa quando o código estiver no GitHub
+OKE = executa somente aplicações consumidoras do framework
+```
diff --git a/deploy/package-registry/scripts/build_agent_framework.sh b/deploy/package-registry/scripts/build_agent_framework.sh
new file mode 100644
index 0000000..02af0b5
--- /dev/null
+++ b/deploy/package-registry/scripts/build_agent_framework.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+FRAMEWORK_DIR="${ROOT_DIR}/libs/agent_framework"
+
+cd "${FRAMEWORK_DIR}"
+python -m pip install --upgrade pip build twine
+rm -rf dist build *.egg-info src/*.egg-info
+python -m build
+python -m twine check dist/*
+
+echo "Build concluído em: ${FRAMEWORK_DIR}/dist"
+ls -lh dist
diff --git a/deploy/package-registry/scripts/create_github_release_package.sh b/deploy/package-registry/scripts/create_github_release_package.sh
new file mode 100644
index 0000000..6ba5b09
--- /dev/null
+++ b/deploy/package-registry/scripts/create_github_release_package.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+: "${GITHUB_REPOSITORY:?Informe GITHUB_REPOSITORY. Ex: org/agent_platform_oci}"
+: "${GITHUB_TOKEN:?Informe GITHUB_TOKEN com permissão contents:write}"
+: "${PACKAGE_VERSION:?Informe PACKAGE_VERSION. Ex: 1.0.0}"
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+DIST_DIR="${ROOT_DIR}/libs/agent_framework/dist"
+TAG="agent-framework-v${PACKAGE_VERSION}"
+
+if ! command -v gh >/dev/null 2>&1; then
+ echo "GitHub CLI não encontrado. Instale gh ou use o workflow GitHub Actions incluído."
+ exit 1
+fi
+
+if [ ! -d "${DIST_DIR}" ] || [ -z "$(ls -A "${DIST_DIR}" 2>/dev/null || true)" ]; then
+ echo "Dist não encontrado. Execute build_agent_framework.sh primeiro."
+ exit 1
+fi
+
+export GH_TOKEN="${GITHUB_TOKEN}"
+gh release create "${TAG}" "${DIST_DIR}"/* \
+ --repo "${GITHUB_REPOSITORY}" \
+ --title "agent-framework ${PACKAGE_VERSION}" \
+ --notes "Wheel/sdist do agent-framework ${PACKAGE_VERSION}."
+
+echo "Release criada: ${TAG}"
diff --git a/deploy/package-registry/scripts/install_from_azure_artifacts.sh b/deploy/package-registry/scripts/install_from_azure_artifacts.sh
new file mode 100644
index 0000000..ae7caeb
--- /dev/null
+++ b/deploy/package-registry/scripts/install_from_azure_artifacts.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+: "${AZURE_ORG:?Informe AZURE_ORG}"
+: "${AZURE_PROJECT:?Informe AZURE_PROJECT}"
+: "${AZURE_FEED:?Informe AZURE_FEED}"
+: "${AZURE_PAT:?Informe AZURE_PAT com permissão Packaging Read}"
+: "${AGENT_FRAMEWORK_VERSION:=0.1.0}"
+
+INDEX_URL="https://azdo:${AZURE_PAT}@pkgs.dev.azure.com/${AZURE_ORG}/${AZURE_PROJECT}/_packaging/${AZURE_FEED}/pypi/simple/"
+python -m pip install --upgrade pip
+python -m pip install --index-url "${INDEX_URL}" --extra-index-url https://pypi.org/simple "agent-framework==${AGENT_FRAMEWORK_VERSION}"
diff --git a/deploy/package-registry/scripts/publish_azure_artifacts_local.sh b/deploy/package-registry/scripts/publish_azure_artifacts_local.sh
new file mode 100644
index 0000000..4f61326
--- /dev/null
+++ b/deploy/package-registry/scripts/publish_azure_artifacts_local.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+: "${AZURE_ORG:?Informe AZURE_ORG. Ex: minha-org}"
+: "${AZURE_PROJECT:?Informe AZURE_PROJECT. Ex: AgentPlatform}"
+: "${AZURE_FEED:?Informe AZURE_FEED. Ex: agent-framework-feed}"
+: "${AZURE_PAT:?Informe AZURE_PAT com permissão Packaging Read/Write}"
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+DIST_DIR="${ROOT_DIR}/libs/agent_framework/dist"
+PYPIRC_FILE="${ROOT_DIR}/deploy/package-registry/.pypirc.azure.generated"
+REPOSITORY_URL="https://pkgs.dev.azure.com/${AZURE_ORG}/${AZURE_PROJECT}/_packaging/${AZURE_FEED}/pypi/upload/"
+
+if [ ! -d "${DIST_DIR}" ] || [ -z "$(ls -A "${DIST_DIR}" 2>/dev/null || true)" ]; then
+ echo "Dist não encontrado. Execute deploy/package-registry/scripts/build_agent_framework.sh primeiro."
+ exit 1
+fi
+
+cat > "${PYPIRC_FILE}" <
- Extraia da mensagem do usuário o valor necessário para o parâmetro
- parametro_externo. Retorne null quando a informação não estiver
- presente no texto.
+ description: 'Extraia da mensagem do usuário o valor necessário para o parâmetro
+ parametro_externo. Retorne null quando a informação não estiver presente
+ no texto.
+ '
consultar_pagamentos:
map:
customer_key: msisdn
@@ -37,24 +36,52 @@ mcp_parameter_mapping:
consultar_pedido:
map:
customer_key: customer_id
- contract_key: order_id
session_key: session_id
+ extract:
+ order_id:
+ from: message
+ type: string
+ strategy: llm
+ description: Extraia somente o identificador do pedido informado explicitamente
+ pelo usuário. Retorne null quando não houver identificador de pedido na
+ mensagem.
consultar_entrega:
map:
- contract_key: order_id
session_key: session_id
+ extract:
+ order_id:
+ from: message
+ type: string
+ strategy: llm
+ description: Extraia somente o identificador do pedido informado explicitamente
+ pelo usuário. Retorne null quando não houver identificador de pedido na
+ mensagem.
solicitar_troca:
map:
- contract_key: order_id
session_key: session_id
defaults:
reason: Solicitação aberta pelo atendimento conversacional.
+ extract:
+ order_id:
+ from: message
+ type: string
+ strategy: llm
+ description: Extraia somente o identificador do pedido informado explicitamente
+ pelo usuário. Retorne null quando não houver identificador de pedido na
+ mensagem.
solicitar_devolucao:
map:
- contract_key: order_id
session_key: session_id
defaults:
reason: Solicitação aberta pelo atendimento conversacional.
+ extract:
+ order_id:
+ from: message
+ type: string
+ strategy: llm
+ description: Extraia somente o identificador do pedido informado explicitamente
+ pelo usuário. Retorne null quando não houver identificador de pedido na
+ mensagem.
consultar_titulo_financeiro:
map:
customer_key: customer_id
diff --git a/libs/agent_framework/config/tools.yaml b/libs/agent_framework/config/tools.yaml
index 4a9408c..49a565a 100644
--- a/libs/agent_framework/config/tools.yaml
+++ b/libs/agent_framework/config/tools.yaml
@@ -68,7 +68,7 @@ tools:
enabled: true
tool_type: action
requires: [order_id, reason]
- confirmation_required: false
+ confirmation_required: true
cache:
enabled: false
args_schema:
@@ -81,7 +81,7 @@ tools:
enabled: true
tool_type: action
requires: [order_id, reason]
- confirmation_required: false
+ confirmation_required: true
cache:
enabled: false
args_schema:
diff --git a/libs/agent_framework/docs/MCP_CACHE.md b/libs/agent_framework/docs/MCP_CACHE.md
index 5048775..b31a3be 100644
--- a/libs/agent_framework/docs/MCP_CACHE.md
+++ b/libs/agent_framework/docs/MCP_CACHE.md
@@ -202,3 +202,6 @@ IC.TOOL_CALLED cached=true
```
Quando houver `IC.MCP_CACHE_HIT`, não deve aparecer `IC.MCP_TOOL_EXECUTING` nem `IC.MCP_TOOL_EXECUTED`, porque o MCP Server não foi chamado.
+# Relação com políticas de execução
+
+Cache e política operacional são independentes. Classifique consultas e transações no arquivo opcional `config/tool_policies.yaml` do backend; mantenha `cache` no catálogo `tools.yaml`. Operações transacionais não devem ser cacheadas. Se o arquivo novo não existir, os campos legados de execução em `tools.yaml` continuam válidos.
diff --git a/libs/agent_framework/docs/MCP_PARAMETER_EXTRACTION.md b/libs/agent_framework/docs/MCP_PARAMETER_EXTRACTION.md
index c8072cc..a8fc4c4 100644
--- a/libs/agent_framework/docs/MCP_PARAMETER_EXTRACTION.md
+++ b/libs/agent_framework/docs/MCP_PARAMETER_EXTRACTION.md
@@ -173,3 +173,39 @@ O framework deve executar extração somente quando:
```
Sem `extract` declarado, nada é extraído.
+
+## Precedência dos valores
+
+A partir desta correção, a precedência efetiva é:
+
+```text
+1. argumento explícito já presente na tool call;
+2. valor extraído da mensagem pelo bloco extract;
+3. valor proveniente do Business Context via map;
+4. defaults globais ou específicos da tool.
+```
+
+O Business Context nunca sobrescreve um argumento explícito ou extraído. Para
+identificadores que obrigatoriamente vêm da mensagem, como `order_id`, não
+configure `contract_key: order_id`.
+
+Exemplo recomendado:
+
+```yaml
+consultar_pedido:
+ map:
+ customer_key: customer_id
+ session_key: session_id
+ extract:
+ order_id:
+ from: message
+ type: string
+ strategy: llm
+ description: >
+ Extraia somente o identificador do pedido informado explicitamente pelo
+ usuário. Retorne null quando não houver identificador.
+```
+
+A extração usa a generation `llm.mcp_parameter_extraction` e o profile
+`mcp_parameter_extraction`. Identificadores devem preferencialmente usar
+`type: string` para preservar zeros à esquerda, hífens e prefixos.
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
new file mode 100644
index 0000000..cea7442
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/__pycache__/gateway_policy_context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/gateway_policy_context.cpython-313.pyc
new file mode 100644
index 0000000..7a1f937
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/__pycache__/gateway_policy_context.cpython-313.pyc differ
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
new file mode 100644
index 0000000..414c521
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc differ
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
new file mode 100644
index 0000000..3c784bf
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/__pycache__/runtime_mcp_gateway_adapter.cpython-313.pyc differ
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
new file mode 100644
index 0000000..4082162
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc
new file mode 100644
index 0000000..d14e34c
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc
new file mode 100644
index 0000000..f566d16
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc differ
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
new file mode 100644
index 0000000..eba3573
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc differ
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
new file mode 100644
index 0000000..6068c50
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc
new file mode 100644
index 0000000..ad4d736
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc
new file mode 100644
index 0000000..7d87060
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d989075
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..90d199f
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc differ
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
new file mode 100644
index 0000000..991504a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc differ
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
new file mode 100644
index 0000000..e4946e4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc differ
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
new file mode 100644
index 0000000..ec83193
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc differ
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
new file mode 100644
index 0000000..a5f45cf
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..58fe8c4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc differ
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
new file mode 100644
index 0000000..78aea25
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d71517e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc differ
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
new file mode 100644
index 0000000..11df462
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c74d169
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc differ
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
new file mode 100644
index 0000000..a2adf2b
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d4157f0
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc differ
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
new file mode 100644
index 0000000..46d96ea
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c61b506
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc differ
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
new file mode 100644
index 0000000..812334a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc differ
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
new file mode 100644
index 0000000..ad52ea8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc
new file mode 100644
index 0000000..893cf5b
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc differ
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
new file mode 100644
index 0000000..9a6ac89
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/config/settings.py b/libs/agent_framework/src/agent_framework/config/settings.py
index 4c8b785..48c9fd7 100644
--- a/libs/agent_framework/src/agent_framework/config/settings.py
+++ b/libs/agent_framework/src/agent_framework/config/settings.py
@@ -102,6 +102,7 @@ class Settings(BaseSettings):
ORACLE_GRAPH_NAME: str = 'AGENTFW_GRAPH'
ORACLE_GRAPH_AUTO_CREATE: bool = False
RAG_TOP_K: int = 5
+ SKIP_RAG_WHEN_MCP_SUFFICIENT: bool = True
ENABLE_RAG_QUERY_REWRITE: bool = False
ENABLE_RAG_CONTEXT_COMPRESSION: bool = False
ENABLE_RAG_GENERATION: bool = False
@@ -173,6 +174,15 @@ class Settings(BaseSettings):
ROUTING_CONFIG_PATH: str = './config/routing.yaml'
ENABLE_LLM_ROUTER: bool = False
ROUTING_MODE: Literal['router','supervisor'] = 'router'
+ # Semantic route stickiness. Uses an LLM profile; no regex or language rules.
+ ENABLE_ROUTE_STICKINESS: bool = False
+ ROUTE_STICKINESS_LLM_PROFILE: str = 'route_continuity'
+ ROUTE_STICKINESS_CONFIDENCE_THRESHOLD: float = 0.90
+ ROUTE_STICKINESS_HISTORY_TURNS: int = 2
+ ROUTE_STICKINESS_MAX_TOKENS: int = 80
+ HUMAN_HANDOFF_MESSAGE: str = 'Vou encaminhar seu atendimento para uma pessoa.'
+ END_SESSION_MESSAGE: str = 'Atendimento encerrado. Obrigado pelo contato.'
+ SESSION_ALREADY_ENDED_MESSAGE: str = 'Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.'
# MCP / Tooling
ENABLE_MCP_TOOLS: bool = True
@@ -180,6 +190,8 @@ class Settings(BaseSettings):
MCP_CACHE_TTL_SECONDS: int = 300
MCP_SERVERS_CONFIG_PATH: str = './config/mcp_servers.yaml'
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'
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
new file mode 100644
index 0000000..06cad37
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/events/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..5abf634
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/events/__pycache__/oci_streaming.cpython-313.pyc differ
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
new file mode 100644
index 0000000..93f4ea3
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..38434ad
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc differ
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
new file mode 100644
index 0000000..4e268ca
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..8b4b609
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/client.cpython-313.pyc differ
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
new file mode 100644
index 0000000..64d7323
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/config.cpython-313.pyc differ
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
new file mode 100644
index 0000000..029d804
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/models.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d7f28a8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/router.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c89ab12
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/global_supervisor/__pycache__/session_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..3140462
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..4df5677
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc differ
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
new file mode 100644
index 0000000..18512dd
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc differ
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
new file mode 100644
index 0000000..92ec351
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc differ
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
new file mode 100644
index 0000000..dc425d0
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/executor.cpython-313.pyc differ
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
new file mode 100644
index 0000000..264dcba
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc differ
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
new file mode 100644
index 0000000..39892d8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/langgraph_adapters.cpython-313.pyc differ
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
new file mode 100644
index 0000000..231c3ca
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc differ
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
new file mode 100644
index 0000000..8a88c26
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c2001d6
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc differ
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
new file mode 100644
index 0000000..740e086
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc differ
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
new file mode 100644
index 0000000..e518fa7
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc differ
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
new file mode 100644
index 0000000..e3a37ca
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2f156c2
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc differ
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
new file mode 100644
index 0000000..6602fd2
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc differ
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
new file mode 100644
index 0000000..6faad3a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc
new file mode 100644
index 0000000..649379c
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc differ
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
new file mode 100644
index 0000000..3a057a3
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/config.cpython-313.pyc differ
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
new file mode 100644
index 0000000..fe027d6
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc differ
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
new file mode 100644
index 0000000..804112d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc differ
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
new file mode 100644
index 0000000..3a6e1eb
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc differ
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
new file mode 100644
index 0000000..e503c92
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc differ
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
new file mode 100644
index 0000000..be63e8f
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f9b6bd9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f7a4ad9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc differ
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
new file mode 100644
index 0000000..5f4e0e1
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc differ
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
new file mode 100644
index 0000000..fefcea1
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..cdeb5fe
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc
new file mode 100644
index 0000000..ed0edf9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc differ
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
new file mode 100644
index 0000000..5fad858
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc differ
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
new file mode 100644
index 0000000..558fed4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc differ
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
new file mode 100644
index 0000000..0bcda8c
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f6b65e9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c84cdee
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc differ
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
new file mode 100644
index 0000000..4dcf36d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc differ
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
new file mode 100644
index 0000000..eab04b9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/safe_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/safe_out.cpython-313.pyc
new file mode 100644
index 0000000..4bfdd2e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/safe_out.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2027ef8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc differ
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
new file mode 100644
index 0000000..1cfdcab
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc differ
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
new file mode 100644
index 0000000..1ee2199
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..263f991
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/supervision_template.cpython-313.pyc differ
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
new file mode 100644
index 0000000..1b509f6
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/shared/__pycache__/tts_rules.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c6f1758
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..bfead35
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc
new file mode 100644
index 0000000..130aedf
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc
new file mode 100644
index 0000000..36e8d33
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc differ
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
new file mode 100644
index 0000000..72a37fb
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc differ
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
new file mode 100644
index 0000000..89fe2f3
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc differ
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
new file mode 100644
index 0000000..a0f1ef8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc differ
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
new file mode 100644
index 0000000..8e00a21
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc differ
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
new file mode 100644
index 0000000..616aadd
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc differ
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
new file mode 100644
index 0000000..7395130
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..44306a6
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/correspondencia_item.cpython-313.pyc differ
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
new file mode 100644
index 0000000..7d06e9d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/groundedness.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/intencao_cancelar.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/intencao_cancelar.cpython-313.pyc
new file mode 100644
index 0000000..3f95302
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/intencao_cancelar.cpython-313.pyc differ
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
new file mode 100644
index 0000000..45ee8e5
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/quantidade_coerente.cpython-313.pyc differ
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
new file mode 100644
index 0000000..a3debc2
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/servico_correto.cpython-313.pyc differ
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
new file mode 100644
index 0000000..0aaff8e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/supervision/__pycache__/verbalizacao_prematura.cpython-313.pyc differ
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
new file mode 100644
index 0000000..7950104
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..3e0aa53
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc differ
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
new file mode 100644
index 0000000..b4c48e4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/oos_blocklist.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d856571
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d57555f
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc differ
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
new file mode 100644
index 0000000..6ccc946
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..b1ad7ac
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc differ
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
new file mode 100644
index 0000000..1875b54
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc differ
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
new file mode 100644
index 0000000..bbdd1ed
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/identity/mcp_mapper.py b/libs/agent_framework/src/agent_framework/identity/mcp_mapper.py
index 92c1122..f12a007 100644
--- a/libs/agent_framework/src/agent_framework/identity/mcp_mapper.py
+++ b/libs/agent_framework/src/agent_framework/identity/mcp_mapper.py
@@ -19,6 +19,16 @@ class MCPParameterMapper:
return cls({})
return cls(yaml.safe_load(p.read_text(encoding="utf-8")) or {})
+ def extract_rules(self, tool_name: str) -> dict[str, dict[str, Any]]:
+ """Retorna as regras declarativas de extração da tool.
+
+ O mapper não executa LLM; ele apenas expõe a configuração para o
+ runtime, que possui acesso ao modelo e à mensagem atual.
+ """
+ rule = self.tools.get(tool_name) or {}
+ raw = rule.get("extract") or {}
+ return {str(k): dict(v or {}) for k, v in raw.items() if isinstance(v, dict)}
+
def map(self, tool_name: str, business_context: BusinessContext | dict[str, Any] | None, *, original_context: dict[str, Any] | None = None, extra_args: dict[str, Any] | None = None) -> dict[str, Any]:
ctx = business_context if isinstance(business_context, BusinessContext) else BusinessContext.from_mapping(business_context or {})
original_context = dict(original_context or {})
@@ -27,13 +37,16 @@ class MCPParameterMapper:
mappings = rule.get("map") or {}
# também aceita formato simples: customer_key: msisdn
for src_key, target in rule.items():
- if src_key in {"map", "defaults", "required"}:
+ if src_key in {"map", "defaults", "required", "extract"}:
continue
mappings.setdefault(src_key, target)
for canonical_key, target_field in mappings.items():
value = getattr(ctx, canonical_key, None)
if value not in (None, ""):
- args[str(target_field)] = value
+ # Argumentos explícitos ou extraídos da mensagem têm precedência
+ # sobre o Business Context. Isso evita, por exemplo, que um
+ # contract_key sobrescreva um order_id informado pelo usuário.
+ args.setdefault(str(target_field), value)
for key, value in {**self.defaults, **(rule.get("defaults") or {})}.items():
args.setdefault(key, value)
# preserva parâmetros específicos já capturados no canal, sem o framework conhecer seus nomes.
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
new file mode 100644
index 0000000..8b627ee
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..1f0cfb9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2a30b77
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..0bb8c02
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/_compat.cpython-313.pyc differ
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
new file mode 100644
index 0000000..0f6d371
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d31b337
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/models.cpython-313.pyc differ
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
new file mode 100644
index 0000000..4c6a376
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..90726b9
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc differ
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
new file mode 100644
index 0000000..95202ba
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc differ
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
new file mode 100644
index 0000000..9687b6a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc differ
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
new file mode 100644
index 0000000..329d5b8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d82b59b
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/judges/judge.py b/libs/agent_framework/src/agent_framework/judges/judge.py
index 35b48cc..c7b76f5 100644
--- a/libs/agent_framework/src/agent_framework/judges/judge.py
+++ b/libs/agent_framework/src/agent_framework/judges/judge.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
+import hashlib
import logging
from pathlib import Path
from typing import Any
@@ -365,6 +366,8 @@ class JudgePipeline:
self.config = _load_judges_config(self.config_path)
self.llm = _ensure_judge_llm(llm, settings=settings) if self.enabled else llm
self.judges = list(judges) if judges is not None else self._build_judges_from_config(self.llm)
+ self.sample_rate = max(0.0, min(1.0, float(self.config.get('sample_rate', 1.0) or 1.0)))
+ self.always_run_for_transactional = _truthy(self.config.get('always_run_for_transactional'), True)
def _build_judges_from_config(self, llm: Any | None) -> list[Any]:
if not self.enabled:
@@ -420,10 +423,70 @@ class JudgePipeline:
return built
+ @staticmethod
+ def _is_transactional_context(ctx: dict[str, Any]) -> bool:
+ """Detect transactional turns from the finalized workflow state.
+
+ The detector intentionally accepts multiple independent signals because
+ confirmation turns may have already cleared ``pending_tool_call`` and
+ may expose the operation only through ``mcp_results`` or policy data.
+ """
+ status = str(ctx.get('transaction_status') or '').strip().upper()
+ if status in {
+ 'AWAITING_CONFIRMATION', 'CONFIRMED', 'EXECUTING',
+ 'COMPLETED', 'FAILED', 'CANCELLED',
+ }:
+ return True
+
+ operation_type = str(ctx.get('operation_type') or '').strip().lower()
+ if operation_type == 'transactional':
+ return True
+
+ policy = ctx.get('tool_policy_result') or {}
+ if isinstance(policy, dict) and str(policy.get('operation_type') or '').lower() == 'transactional':
+ return True
+
+ for key in ('selected_tool_call', 'pending_tool_call'):
+ call = ctx.get(key) or {}
+ if isinstance(call, dict):
+ metadata = call.get('metadata') or {}
+ if str(call.get('operation_type') or metadata.get('operation_type') or '').lower() == 'transactional':
+ return True
+ tool_name = str(call.get('tool_name') or '')
+ if tool_name and tool_name in set(ctx.get('transactional_tools') or []):
+ return True
+
+ for result in ctx.get('mcp_results') or []:
+ if not isinstance(result, dict):
+ continue
+ metadata = result.get('metadata') or {}
+ if str(result.get('operation_type') or metadata.get('operation_type') or '').lower() == 'transactional':
+ return True
+ if result.get('awaiting_confirmation') or result.get('transaction_status'):
+ return True
+ tool_name = str(result.get('tool_name') or '')
+ if tool_name and tool_name in set(ctx.get('transactional_tools') or []):
+ return True
+
+ return False
+
async def evaluate_all(self, question, answer, context):
if not self.enabled or not self.judges:
return []
- return [await j.evaluate(question, answer, context or {}) for j in self.judges]
+ ctx = context or {}
+ transactional = self._is_transactional_context(ctx)
+
+ # Transactional turns take precedence over sampling. Sampling is only
+ # evaluated for ordinary interactions.
+ if not (self.always_run_for_transactional and transactional):
+ if self.sample_rate <= 0.0:
+ return []
+ if self.sample_rate < 1.0:
+ digest = hashlib.sha256(f"{question}|{answer}".encode('utf-8')).hexdigest()
+ bucket = int(digest[:8], 16) / 0xFFFFFFFF
+ if bucket >= self.sample_rate:
+ return []
+ return [await j.evaluate(question, answer, ctx) for j in self.judges]
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
new file mode 100644
index 0000000..01d19a3
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..5a14ed2
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc differ
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
new file mode 100644
index 0000000..453ed4d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc
new file mode 100644
index 0000000..3fff9c6
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/mcp/__init__.py b/libs/agent_framework/src/agent_framework/mcp/__init__.py
index ac8de76..ab442f4 100644
--- a/libs/agent_framework/src/agent_framework/mcp/__init__.py
+++ b/libs/agent_framework/src/agent_framework/mcp/__init__.py
@@ -1,2 +1,3 @@
from .tool_router import MCPToolRouter, create_mcp_tool_router
from .models import MCPServerConfig, MCPToolConfig, MCPToolResult
+from .tool_policy import ToolPolicy, ToolPolicyRegistry
diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..caac53a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2dd559a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2d11ab2
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2af64c1
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc differ
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
new file mode 100644
index 0000000..3af5d3c
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc differ
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
new file mode 100644
index 0000000..014aa9e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/mcp/models.py b/libs/agent_framework/src/agent_framework/mcp/models.py
index 4aa5c95..98f1529 100644
--- a/libs/agent_framework/src/agent_framework/mcp/models.py
+++ b/libs/agent_framework/src/agent_framework/mcp/models.py
@@ -26,6 +26,7 @@ class MCPToolConfig(BaseModel):
requires: list[str] = Field(default_factory=list)
confirmation_required: bool = False
execution_policy: dict[str, Any] = Field(default_factory=dict)
+ selection_keywords: list[str] = Field(default_factory=list)
# Política declarativa de cache da tool, lida diretamente de config/tools.yaml.
# Exemplo:
diff --git a/libs/agent_framework/src/agent_framework/mcp/registry.py b/libs/agent_framework/src/agent_framework/mcp/registry.py
index f41b464..6ac4308 100644
--- a/libs/agent_framework/src/agent_framework/mcp/registry.py
+++ b/libs/agent_framework/src/agent_framework/mcp/registry.py
@@ -69,6 +69,7 @@ class MCPRegistry:
"requires": tool.requires,
"confirmation_required": tool.confirmation_required,
"execution_policy": tool.execution_policy,
+ "selection_keywords": tool.selection_keywords,
"cache": tool.cache,
})
return out
diff --git a/libs/agent_framework/src/agent_framework/mcp/tool_policy.py b/libs/agent_framework/src/agent_framework/mcp/tool_policy.py
new file mode 100644
index 0000000..eb66ac9
--- /dev/null
+++ b/libs/agent_framework/src/agent_framework/mcp/tool_policy.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any, Literal
+
+import yaml
+from pydantic import BaseModel, Field
+
+
+class ToolPolicy(BaseModel):
+ """Política conversacional mínima aplicada antes da chamada MCP."""
+
+ operation_type: Literal["read_only", "transactional"] = "read_only"
+ require_confirmation: bool = False
+ requires: list[str] = Field(default_factory=list)
+
+
+class ToolPolicyRegistry:
+ """Carrega políticas opcionais sem tornar o novo arquivo obrigatório."""
+
+ def __init__(self, path: str | None = None):
+ self.path = path
+ self.defaults = ToolPolicy()
+ self.policies: dict[str, ToolPolicy] = {}
+ self.configured = False
+ if path:
+ self._load(path)
+
+ def _load(self, path: str) -> None:
+ config_path = Path(path)
+ if not config_path.exists():
+ return
+ with config_path.open("r", encoding="utf-8") as stream:
+ raw: dict[str, Any] = yaml.safe_load(stream) or {}
+ defaults = raw.get("defaults") or {}
+ self.defaults = self._parse(defaults, base=ToolPolicy())
+ for name, value in (raw.get("tool_policies") or {}).items():
+ self.policies[name] = self._parse(value or {}, base=self.defaults)
+ self.configured = True
+
+ @staticmethod
+ def _parse(raw: dict[str, Any], *, base: ToolPolicy) -> ToolPolicy:
+ operation_type = raw.get("operation_type", raw.get("type", base.operation_type))
+ confirmation = raw.get(
+ "require_confirmation",
+ raw.get("requires_confirmation", raw.get("confirmation_required", base.require_confirmation)),
+ )
+ return ToolPolicy(
+ operation_type=operation_type,
+ require_confirmation=bool(confirmation),
+ requires=list(raw.get("requires", base.requires) or []),
+ )
+
+ def get(self, tool_name: str) -> ToolPolicy | None:
+ """Retorna somente política explícita; ausência preserva o legado."""
+ return self.policies.get(tool_name)
+
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 d9ec523..b006ff3 100644
--- a/libs/agent_framework/src/agent_framework/mcp/tool_router.py
+++ b/libs/agent_framework/src/agent_framework/mcp/tool_router.py
@@ -8,6 +8,7 @@ from agent_framework.identity import MCPParameterMapper
from .registry import MCPRegistry
from .client import MCPHttpClient
from .models import MCPToolResult
+from .tool_policy import ToolPolicyRegistry
from agent_framework.gateways import MCPGatewayClient
logger = logging.getLogger("agent_framework.mcp.tool_router")
@@ -30,6 +31,9 @@ class MCPToolRouter:
settings.MCP_SERVERS_CONFIG_PATH,
settings.TOOLS_CONFIG_PATH,
)
+ self.tool_policies = ToolPolicyRegistry(
+ getattr(settings, "TOOL_POLICIES_PATH", None)
+ )
self.client = MCPHttpClient(timeout_seconds=settings.MCP_TOOL_TIMEOUT_SECONDS)
self.gateway_enabled = bool(getattr(settings, "MCP_GATEWAY_ENABLED", False))
self.gateway_agent_id = getattr(settings, "MCP_GATEWAY_AGENT_ID", "telecom_contas")
@@ -56,6 +60,59 @@ class MCPToolRouter:
getattr(settings, "MCP_PARAMETER_MAPPING_PATH", None),
)
+ def parameter_extract_rules(self, tool_name: str) -> dict[str, dict[str, Any]]:
+ """Expõe extract do mcp_parameter_mapping.yaml ao runtime."""
+ return self.parameter_mapper.extract_rules(tool_name)
+
+ def resolve_execution_policy(
+ self,
+ tool_name: str,
+ arguments: dict[str, Any] | None = None,
+ ) -> dict[str, Any]:
+ """Retorna a política efetiva sem validar confirmação ou parâmetros."""
+ legacy = self.registry.get_tool(tool_name)
+ explicit = self.tool_policies.get(tool_name)
+ legacy_type = getattr(legacy, "tool_type", None) if legacy else None
+ operation_type = "transactional" if legacy_type in {"action", "transactional"} else "read_only"
+ confirmation_required = bool(getattr(legacy, "confirmation_required", False)) if legacy else False
+ required = list(getattr(legacy, "requires", None) or []) if legacy else []
+ source = "tools.yaml"
+ if explicit is not None:
+ operation_type = explicit.operation_type
+ confirmation_required = explicit.require_confirmation
+ required.extend(explicit.requires)
+ source = "tool_policies.yaml"
+ return {
+ "operation_type": operation_type,
+ "require_confirmation": confirmation_required,
+ "requires": list(dict.fromkeys(required)),
+ "policy_source": source,
+ }
+
+ def validate_execution_policy(
+ self,
+ tool_name: str,
+ arguments: dict[str, Any] | None = None,
+ ) -> tuple[bool, str | None, dict[str, Any]]:
+ """Resolve política nova + campos legados e valida a execução.
+
+ O arquivo novo tem precedência apenas para os campos declarados por
+ ferramenta. Quando ele não existe, o comportamento anterior de
+ ``tools.yaml`` é preservado integralmente.
+ """
+ args = dict(arguments or {})
+ metadata = self.resolve_execution_policy(tool_name, args)
+ operation_type = metadata["operation_type"]
+ confirmation_required = bool(metadata["require_confirmation"])
+ required = list(metadata.get("requires") or [])
+ for field_name in dict.fromkeys(required):
+ if args.get(field_name) in (None, "", [], {}):
+ return False, f"Campo obrigatório ausente para execução da tool: {field_name}", metadata
+ confirmed = args.get("confirmed") is True or args.get("confirmation") is True
+ if confirmation_required and not confirmed:
+ return False, "Tool exige confirmação explícita antes da execução", metadata
+ return True, None, metadata
+
def describe_tools(self, tool_names: list[str] | None = None) -> list[dict[str, Any]]:
return self.registry.describe_tools(tool_names)
@@ -107,6 +164,16 @@ class MCPToolRouter:
if not server:
return None, {}, MCPToolResult(tool_name=tool_name, server_name="unknown", ok=False, error="Tool/server not configured")
+ allowed, reason, policy = self.validate_execution_policy(tool_name, arguments)
+ if not allowed:
+ return None, {}, MCPToolResult(
+ tool_name=tool_name,
+ server_name=server.name,
+ ok=False,
+ error=reason,
+ metadata={"blocked_by_policy": True, **policy},
+ )
+
mapped_arguments = self._mapped_arguments(
tool_name,
arguments,
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
new file mode 100644
index 0000000..c6700de
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..6604383
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc differ
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
new file mode 100644
index 0000000..1dd6188
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c4fc37e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc differ
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
new file mode 100644
index 0000000..9490719
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..39e17a4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc
new file mode 100644
index 0000000..75c26a7
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc differ
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
new file mode 100644
index 0000000..57ecc6e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..72e5800
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc
new file mode 100644
index 0000000..9753641
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc differ
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
new file mode 100644
index 0000000..022d26f
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..fc0eb2a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..da2417e
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc differ
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
new file mode 100644
index 0000000..e4e97c5
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/control_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..dba7960
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/decorators.cpython-313.pyc differ
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
new file mode 100644
index 0000000..fedd876
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc differ
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
new file mode 100644
index 0000000..88712d8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/grl_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..0207188
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc
new file mode 100644
index 0000000..81d1be7
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d3f75e8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/informational_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..8cd2c1a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/langfuse_enterprise.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/langfuse_enterprise.cpython-313.pyc
new file mode 100644
index 0000000..8ba6698
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/langfuse_enterprise.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc
new file mode 100644
index 0000000..26b78f8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc differ
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
new file mode 100644
index 0000000..a4a7127
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc
new file mode 100644
index 0000000..c3fd47d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc differ
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
new file mode 100644
index 0000000..80354a5
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..dffadfd
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc differ
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
new file mode 100644
index 0000000..9624da4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc differ
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
new file mode 100644
index 0000000..48974e1
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc differ
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
new file mode 100644
index 0000000..d7cecc0
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..567abe5
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_exporter.cpython-313.pyc differ
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
new file mode 100644
index 0000000..cc79549
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc differ
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
new file mode 100644
index 0000000..aced2c7
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/tim_backoffice_contract.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f21b465
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc differ
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
new file mode 100644
index 0000000..ef207e3
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..02ab3cd
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2b1c3f3
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f4b9c04
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..aaf4cd4
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/mongodb_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2345191
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..c65d58d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..73cf291
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..168db19
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc differ
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
new file mode 100644
index 0000000..b4a986f
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..86953f0
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc differ
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
new file mode 100644
index 0000000..48a2b9b
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f31138a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc differ
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
new file mode 100644
index 0000000..04c7ea1
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..39e77e8
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc differ
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
new file mode 100644
index 0000000..2ef22cc
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc
new file mode 100644
index 0000000..f196d81
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc differ
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
new file mode 100644
index 0000000..458330d
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc
new file mode 100644
index 0000000..0047345
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc differ
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
new file mode 100644
index 0000000..03ada83
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/routing/continuity.py b/libs/agent_framework/src/agent_framework/routing/continuity.py
new file mode 100644
index 0000000..792cd5e
--- /dev/null
+++ b/libs/agent_framework/src/agent_framework/routing/continuity.py
@@ -0,0 +1,268 @@
+from __future__ import annotations
+
+import json
+import logging
+from dataclasses import dataclass
+from typing import Any
+
+from .models import IntentDefinition, RouteDecision
+
+logger = logging.getLogger("agent_framework.routing.continuity")
+
+
+@dataclass(slots=True)
+class ContinuityEvaluation:
+ decision: str
+ confidence: float
+ reason: str
+ raw: str
+
+
+class SemanticRouteContinuity:
+ """LLM-only semantic turn control and route stickiness.
+
+ This component deliberately contains no linguistic regexes, keyword lists or
+ domain-specific rules. It classifies the turn as CONTINUE, ROUTE,
+ HUMAN_HANDOFF or END_SESSION. Low confidence, timeout and parsing errors fall
+ back to the normal EnterpriseRouter.
+ """
+
+ def __init__(self, settings: Any, llm: Any, telemetry: Any = None):
+ self.settings = settings
+ self.llm = llm
+ self.telemetry = telemetry
+ self.enabled = bool(getattr(settings, "ENABLE_ROUTE_STICKINESS", False))
+ self.profile_name = str(
+ getattr(settings, "ROUTE_STICKINESS_LLM_PROFILE", "route_continuity")
+ )
+ self.confidence_threshold = float(
+ getattr(settings, "ROUTE_STICKINESS_CONFIDENCE_THRESHOLD", 0.90)
+ )
+ self.history_turns = max(
+ 1, int(getattr(settings, "ROUTE_STICKINESS_HISTORY_TURNS", 2))
+ )
+ self.max_tokens = max(
+ 16, int(getattr(settings, "ROUTE_STICKINESS_MAX_TOKENS", 80))
+ )
+
+ async def evaluate(
+ self,
+ state: dict[str, Any],
+ *,
+ intents: list[IntentDefinition],
+ ) -> RouteDecision | None:
+ active_agent = str(state.get("active_agent") or "").strip()
+ if not self.enabled or self.llm is None:
+ return None
+
+ enabled_intents = [intent for intent in intents if intent.enabled]
+ known_agents = {intent.agent for intent in enabled_intents}
+ if active_agent and active_agent not in known_agents:
+ active_agent = ""
+
+ text = str(state.get("sanitized_input") or state.get("user_text") or "").strip()
+ if not text:
+ return None
+
+ try:
+ evaluation = await self._classify(
+ state,
+ text=text,
+ active_agent=active_agent,
+ intents=enabled_intents,
+ )
+ except Exception as exc:
+ logger.warning("Route stickiness LLM failed; using EnterpriseRouter: %s", exc)
+ await self._emit(
+ state,
+ {
+ "decision": "ROUTE",
+ "confidence": 0.0,
+ "reason": f"continuity_error:{type(exc).__name__}",
+ "active_agent": active_agent,
+ "route_bypassed": False,
+ },
+ )
+ return None
+
+ accepted = evaluation.confidence >= self.confidence_threshold
+ bypass = evaluation.decision == "CONTINUE" and accepted and bool(active_agent)
+ await self._emit(
+ state,
+ {
+ "decision": evaluation.decision,
+ "confidence": evaluation.confidence,
+ "reason": evaluation.reason,
+ "active_agent": active_agent,
+ "route_bypassed": bypass,
+ "profile_name": self.profile_name,
+ },
+ )
+ if not accepted:
+ return None
+
+ if evaluation.decision == "HUMAN_HANDOFF":
+ return RouteDecision(
+ route="human_handoff",
+ agent="human_handoff",
+ intent="human_handoff",
+ confidence=evaluation.confidence,
+ reason=evaluation.reason or "O usuário solicitou atendimento humano.",
+ method="continuity",
+ handoff=True,
+ metadata={
+ "route_bypassed": True,
+ "continuity_decision": evaluation.decision,
+ "continuity_profile": self.profile_name,
+ "session_control": "HUMAN_HANDOFF",
+ "raw_llm_answer": evaluation.raw[:1000],
+ },
+ )
+
+ if evaluation.decision == "END_SESSION":
+ return RouteDecision(
+ route="end_session",
+ agent="end_session",
+ intent="end_session",
+ confidence=evaluation.confidence,
+ reason=evaluation.reason or "O usuário solicitou o encerramento do atendimento.",
+ method="continuity",
+ metadata={
+ "route_bypassed": True,
+ "continuity_decision": evaluation.decision,
+ "continuity_profile": self.profile_name,
+ "session_control": "END_SESSION",
+ "raw_llm_answer": evaluation.raw[:1000],
+ },
+ )
+
+ if not bypass:
+ return None
+
+ previous = state.get("route_decision") or {}
+ intent_name = str(previous.get("intent") or state.get("intent") or "continuity")
+ domain = previous.get("domain") or state.get("domain")
+ tools = previous.get("mcp_tools") or state.get("mcp_tools") or []
+ return RouteDecision(
+ route=active_agent,
+ agent=active_agent,
+ intent=intent_name,
+ confidence=evaluation.confidence,
+ reason=evaluation.reason or "Mensagem continua sob responsabilidade do agente ativo.",
+ method="continuity",
+ metadata={
+ "route_bypassed": True,
+ "continuity_decision": evaluation.decision,
+ "continuity_profile": self.profile_name,
+ "raw_llm_answer": evaluation.raw[:1000],
+ },
+ domain=domain,
+ mcp_tools=list(tools),
+ )
+
+ async def _classify(
+ self,
+ state: dict[str, Any],
+ *,
+ text: str,
+ active_agent: str,
+ intents: list[IntentDefinition],
+ ) -> ContinuityEvaluation:
+ agent_capabilities = self._agent_capabilities(intents)
+ history = self._compact_history(state.get("history") or [])
+ previous = state.get("route_decision") or {}
+
+ system = (
+ "Você é um classificador semântico de continuidade de rota. "
+ "Sua única tarefa é classificar o tratamento global da mensagem atual. "
+ "Use CONTINUE somente quando existir agente ativo e ele continuar claramente adequado para "
+ "uma continuação, aprofundamento, resposta, correção ou referência ao contexto anterior. "
+ "Use HUMAN_HANDOFF quando o usuário solicitar explicitamente atendimento por uma pessoa. "
+ "Use END_SESSION quando o usuário indicar claramente que deseja finalizar o atendimento e "
+ "não precisa continuar. Use ROUTE para novo assunto, possível responsabilidade de outro "
+ "agente, ausência de agente ativo, contexto insuficiente ou qualquer dúvida. "
+ "Não responda ao usuário e não selecione um novo agente. Retorne somente JSON válido com "
+ "decision, confidence e reason. decision deve ser CONTINUE, ROUTE, HUMAN_HANDOFF ou END_SESSION."
+ )
+ payload = {
+ "active_agent": active_agent,
+ "active_agent_capabilities": agent_capabilities.get(active_agent, []),
+ "other_agents": {
+ agent: capabilities
+ for agent, capabilities in agent_capabilities.items()
+ if agent != active_agent
+ },
+ "previous_intent": previous.get("intent") or state.get("intent"),
+ "previous_domain": previous.get("domain") or state.get("domain"),
+ "recent_history": history,
+ "current_message": text,
+ }
+ answer = await self.llm.ainvoke(
+ [
+ {"role": "system", "content": system},
+ {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
+ ],
+ temperature=0.0,
+ max_tokens=self.max_tokens,
+ profile_name=self.profile_name,
+ component_name="route_continuity",
+ generation_name="llm.route_continuity",
+ )
+ data = self._parse_json(answer)
+ decision = str(data.get("decision") or "ROUTE").strip().upper()
+ if decision not in {"CONTINUE", "ROUTE", "HUMAN_HANDOFF", "END_SESSION"}:
+ decision = "ROUTE"
+ if decision == "CONTINUE" and not active_agent:
+ decision = "ROUTE"
+ try:
+ confidence = float(data.get("confidence") or 0.0)
+ except (TypeError, ValueError):
+ confidence = 0.0
+ confidence = min(1.0, max(0.0, confidence))
+ return ContinuityEvaluation(
+ decision=decision,
+ confidence=confidence,
+ reason=str(data.get("reason") or ""),
+ raw=str(answer),
+ )
+
+ def _agent_capabilities(self, intents: list[IntentDefinition]) -> dict[str, list[str]]:
+ capabilities: dict[str, list[str]] = {}
+ for intent in intents:
+ description = intent.description or intent.name
+ capabilities.setdefault(intent.agent, []).append(description)
+ return capabilities
+
+ def _compact_history(self, history: list[dict[str, Any]]) -> list[dict[str, str]]:
+ limit = self.history_turns * 2
+ compact: list[dict[str, str]] = []
+ for message in history[-limit:]:
+ role = str(message.get("role") or message.get("type") or "unknown")
+ content = str(message.get("content") or "").strip()
+ if content:
+ compact.append({"role": role, "content": content[:1200]})
+ return compact
+
+ def _parse_json(self, answer: Any) -> dict[str, Any]:
+ text = str(answer).strip()
+ if text.startswith("```"):
+ text = text.strip("`")
+ if text.lower().startswith("json"):
+ text = text[4:].strip()
+ try:
+ return json.loads(text)
+ except json.JSONDecodeError:
+ start, end = text.find("{"), text.rfind("}")
+ if start >= 0 and end > start:
+ return json.loads(text[start : end + 1])
+ raise
+
+ async def _emit(self, state: dict[str, Any], payload: dict[str, Any]) -> None:
+ if self.telemetry:
+ await self.telemetry.event(
+ "router.continuity",
+ {
+ "session_id": state.get("conversation_key") or state.get("session_id"),
+ **payload,
+ },
+ )
diff --git a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py
index 0dfbd46..534269d 100644
--- a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py
+++ b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py
@@ -5,6 +5,7 @@ import logging
from typing import Any
from .config_loader import load_intents, load_router_defaults, load_state_policies
+from .continuity import SemanticRouteContinuity
from .models import IntentDefinition, RouteDecision, RouterStatePolicy
logger = logging.getLogger("agent_framework.routing")
@@ -33,6 +34,7 @@ class EnterpriseRouter:
self.defaults = load_router_defaults(self.config_path)
self.fallback_agent = self.defaults.get("fallback_agent", "billing_agent")
self.enable_llm_router = bool(getattr(settings, "ENABLE_LLM_ROUTER", False))
+ self.continuity = SemanticRouteContinuity(settings, llm, telemetry)
logger.info(
"EnterpriseRouter carregado intents=%s state_policies=%s llm_router=%s fallback=%s",
len(self.intents),
@@ -40,13 +42,51 @@ class EnterpriseRouter:
self.enable_llm_router,
self.fallback_agent,
)
+ logger.info(
+ "Semantic route stickiness enabled=%s profile=%s threshold=%s",
+ self.continuity.enabled,
+ self.continuity.profile_name,
+ self.continuity.confidence_threshold,
+ )
async def route(self, state: dict[str, Any]) -> RouteDecision:
session = (state.get("context") or {}).get("session", {}) or {}
current_state = state.get("next_state") or session.get("metadata", {}).get("workflow_state")
text = state.get("sanitized_input") or state.get("user_text") or ""
- decision = self._route_by_state(current_state)
+ # Estados de coleta/confirmação têm precedência absoluta. Durante esses
+ # estados, palavras como "pedido" são respostas de preenchimento de
+ # parâmetros e não uma nova intenção de tracking.
+ state_decision = self._route_by_state(current_state)
+ if state_decision:
+ await self._emit(state_decision, state)
+ return state_decision
+
+ # Mensagens que expressam de forma explícita uma intenção diferente da
+ # intent/agente ativos devem prevalecer sobre a route stickiness. Isso
+ # evita manter um fluxo read-only (por exemplo, tracking) quando o usuário
+ # muda para uma ação transacional (por exemplo, devolução).
+ keyword_candidate = self._route_by_keyword(text)
+ active_agent = str(state.get("active_agent") or "").strip()
+ previous = state.get("route_decision") or {}
+ previous_intent = str(previous.get("intent") or state.get("intent") or "").strip()
+ if (
+ active_agent
+ and keyword_candidate is not None
+ and keyword_candidate.agent != active_agent
+ and keyword_candidate.intent != previous_intent
+ and self._is_explicit_intent_shift(keyword_candidate)
+ ):
+ keyword_candidate.metadata = {
+ **(keyword_candidate.metadata or {}),
+ "route_stickiness_preempted": True,
+ "previous_agent": active_agent,
+ "previous_intent": previous_intent,
+ }
+ await self._emit(keyword_candidate, state)
+ return keyword_candidate
+
+ decision = await self.continuity.evaluate(state, intents=self.intents)
if decision:
await self._emit(decision, state)
return decision
@@ -75,6 +115,17 @@ class EnterpriseRouter:
await self._emit(decision, state)
return decision
+ @staticmethod
+ def _is_explicit_intent_shift(decision: RouteDecision) -> bool:
+ """Retorna True para matches explícitos que devem vencer a stickiness.
+
+ A regra é configurável porque usa a keyword já declarada no routing.yaml,
+ sem listas linguísticas fixas no código. Keywords com quatro ou mais
+ caracteres são tratadas como sinais explícitos de mudança de intenção.
+ """
+ matched = str((decision.metadata or {}).get("matched_keyword") or "").strip()
+ return len(matched) >= 4
+
def _route_by_state(self, current_state: str | None) -> RouteDecision | None:
if not current_state:
return None
diff --git a/libs/agent_framework/src/agent_framework/routing/models.py b/libs/agent_framework/src/agent_framework/routing/models.py
index e26b2cd..b18c063 100644
--- a/libs/agent_framework/src/agent_framework/routing/models.py
+++ b/libs/agent_framework/src/agent_framework/routing/models.py
@@ -37,7 +37,7 @@ class RouteDecision(BaseModel):
intent: str
confidence: float = 0.0
reason: str = ""
- method: Literal["state", "keyword", "llm", "fallback"] = "fallback"
+ method: Literal["state", "keyword", "llm", "continuity", "fallback"] = "fallback"
next_state: str | None = None
handoff: bool = False
metadata: dict[str, Any] = Field(default_factory=dict)
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
new file mode 100644
index 0000000..29da32f
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc
new file mode 100644
index 0000000..1597535
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py
index c84a457..7080fa7 100644
--- a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py
+++ b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import hashlib
import json
import logging
+import re
from dataclasses import dataclass, field
from typing import Any, Iterable, Mapping
@@ -228,6 +229,13 @@ class AgentRuntimeMixin:
rag_service = getattr(self, "rag_service", None)
if not rag_service:
return "", {"enabled": False}
+ settings = getattr(self, "settings", None)
+ mcp_results = state.get("mcp_results") or []
+ if bool(getattr(settings, "SKIP_RAG_WHEN_MCP_SUFFICIENT", True)) and any(r.get("ok") and r.get("result") for r in mcp_results):
+ text = str(state.get("sanitized_input") or state.get("user_text") or "").lower()
+ policy_terms = ("política", "politica", "regra", "prazo", "como funciona", "por que", "porque")
+ if not any(term in text for term in policy_terms):
+ return "", {"enabled": False, "skipped": True, "reason": "mcp_sufficient"}
runtime = self.get_runtime_context(state)
namespace = (
(state.get("agent_profile") or {}).get("rag_namespace")
@@ -300,6 +308,159 @@ class AgentRuntimeMixin:
args.update({k: v for k, v in (extra_args or {}).items() if v not in _EMPTY_VALUES})
return args
+ @staticmethod
+ def _coerce_extracted_value(value: Any, declared_type: str | None) -> Any:
+ if value in _EMPTY_VALUES:
+ return None
+ kind = str(declared_type or "string").strip().lower()
+ try:
+ if kind in {"int", "integer"}:
+ return int(value)
+ if kind in {"float", "number"}:
+ return float(value)
+ if kind in {"bool", "boolean"}:
+ if isinstance(value, bool):
+ return value
+ normalized = str(value).strip().lower()
+ if normalized in {"true", "1", "yes", "sim"}:
+ return True
+ if normalized in {"false", "0", "no", "não", "nao"}:
+ return False
+ return None
+ return str(value).strip()
+ except (TypeError, ValueError):
+ return None
+
+ @staticmethod
+ def _llm_response_text(response: Any) -> str:
+ if response is None:
+ return ""
+ if isinstance(response, str):
+ return response
+ if isinstance(response, dict):
+ return str(response.get("content") or response.get("text") or response.get("answer") or "")
+ return str(getattr(response, "content", None) or getattr(response, "text", None) or response)
+
+ async def _extract_mcp_parameters(
+ self,
+ tool_name: str,
+ arguments: dict[str, Any],
+ state: dict[str, Any],
+ ) -> dict[str, Any]:
+ """Executa regras ``extract`` declaradas para a tool escolhida.
+
+ Precedência: argumento explícito > valor extraído > Business Context >
+ default. A etapa é genérica: nomes e semântica vêm exclusivamente do
+ mcp_parameter_mapping.yaml.
+ """
+ router = getattr(self, "tool_router", None)
+ if not router or not hasattr(router, "parameter_extract_rules"):
+ return dict(arguments or {})
+ rules = router.parameter_extract_rules(tool_name) or {}
+ if not rules:
+ return dict(arguments or {})
+
+ resolved = dict(arguments or {})
+ runtime = self.get_runtime_context(state)
+ message = runtime.sanitized_input or runtime.original_text or runtime.user_text
+ llm = getattr(self, "llm", None)
+
+ for field_name, rule in rules.items():
+ if resolved.get(field_name) not in _EMPTY_VALUES:
+ continue
+ if str(rule.get("from") or "message").lower() != "message":
+ continue
+ strategy = str(rule.get("strategy") or "llm").lower()
+ value: Any = None
+
+ if strategy in {"regex", "hybrid", "deterministic"}:
+ pattern = str(rule.get("pattern") or "").strip()
+ if pattern and message:
+ try:
+ match = re.search(pattern, str(message), flags=re.IGNORECASE)
+ if match:
+ group = int(rule.get("group", 1) or 1)
+ value = match.group(group)
+ except (re.error, IndexError, ValueError) as exc:
+ logger.warning(
+ "mcp.parameter.regex_extract_failed tool=%s field=%s error=%s",
+ tool_name, field_name, exc,
+ )
+ if value is None and strategy == "hybrid":
+ strategy = "llm"
+ elif value is None:
+ logger.info("mcp.parameter.regex_extracted_null tool=%s field=%s", tool_name, field_name)
+ continue
+
+ if strategy == "month_name_pt":
+ months = {
+ "janeiro": 1, "fevereiro": 2, "março": 3, "marco": 3,
+ "abril": 4, "maio": 5, "junho": 6, "julho": 7,
+ "agosto": 8, "setembro": 9, "outubro": 10,
+ "novembro": 11, "dezembro": 12,
+ }
+ normalized = str(message or "").lower()
+ value = next((number for name, number in months.items() if name in normalized), None)
+ elif strategy == "llm":
+ if llm is None or not message:
+ logger.warning(
+ "mcp.parameter.llm_extract_failed tool=%s field=%s error=llm_or_message_unavailable",
+ tool_name,
+ field_name,
+ )
+ continue
+ description = str(rule.get("description") or f"Extraia o campo {field_name}.").strip()
+ prompt = (
+ "Você é um extrator determinístico de parâmetros para uma tool MCP. "
+ "Responda somente JSON válido, sem markdown.\n"
+ f"Tool: {tool_name}\nCampo: {field_name}\nTipo: {rule.get('type', 'string')}\n"
+ f"Regra: {description}\nMensagem: {message}\n"
+ f"Formato obrigatório: {{\"{field_name}\": valor_ou_null}}"
+ )
+ try:
+ response = await llm.ainvoke(
+ [{"role": "user", "content": prompt}],
+ profile_name="mcp_parameter_extraction",
+ component_name="mcp_parameter_extraction",
+ generation_name="llm.mcp_parameter_extraction",
+ temperature=0.0,
+ max_tokens=80,
+ )
+ raw = self._llm_response_text(response).strip()
+ if raw.startswith("```"):
+ raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.IGNORECASE | re.DOTALL).strip()
+ payload = json.loads(raw)
+ value = payload.get(field_name) if isinstance(payload, dict) else None
+ except Exception as exc:
+ logger.warning(
+ "mcp.parameter.llm_extract_failed tool=%s field=%s error=%s",
+ tool_name,
+ field_name,
+ exc,
+ )
+ continue
+ elif strategy not in {"regex", "hybrid", "deterministic", "month_name_pt"}:
+ logger.warning(
+ "mcp.parameter.extract_strategy_unsupported tool=%s field=%s strategy=%s",
+ tool_name,
+ field_name,
+ strategy,
+ )
+ continue
+
+ coerced = self._coerce_extracted_value(value, rule.get("type"))
+ if coerced is None:
+ logger.info("mcp.parameter.llm_extracted_null tool=%s field=%s", tool_name, field_name)
+ continue
+ resolved[field_name] = coerced
+ logger.info(
+ "mcp.parameter.llm_extracted tool=%s field=%s value=%s",
+ tool_name,
+ field_name,
+ coerced,
+ )
+ return resolved
+
def _tool_config(self, tool_name: str) -> Any:
router = getattr(self, "tool_router", None)
registry = getattr(router, "registry", None)
@@ -307,8 +468,28 @@ class AgentRuntimeMixin:
return registry.get_tool(tool_name)
return None
+ def _resolve_tool_execution_policy(self, tool_name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
+ """Resolve a política efetiva sem executar a tool."""
+ router = getattr(self, "tool_router", None)
+ if router and hasattr(router, "resolve_execution_policy"):
+ return router.resolve_execution_policy(tool_name, arguments)
+ if router and hasattr(router, "validate_execution_policy"):
+ _allowed, _reason, metadata = router.validate_execution_policy(tool_name, arguments or {})
+ return dict(metadata or {})
+ cfg = self._tool_config(tool_name)
+ tool_type = getattr(cfg, "tool_type", None) if cfg is not None else None
+ return {
+ "operation_type": "transactional" if tool_type in {"action", "transactional"} else "read_only",
+ "require_confirmation": bool(getattr(cfg, "confirmation_required", False)) if cfg is not None else False,
+ "policy_source": "tools.yaml",
+ }
+
def _validate_tool_execution_policy(self, tool_name: str, arguments: dict[str, Any]) -> tuple[bool, str | None]:
- """Aplica política genérica de execução declarada em tools.yaml."""
+ """Aplica a mesma política central usada pelo MCPToolRouter."""
+ router = getattr(self, "tool_router", None)
+ if router and hasattr(router, "validate_execution_policy"):
+ allowed, reason, _metadata = router.validate_execution_policy(tool_name, arguments)
+ return allowed, reason
cfg = self._tool_config(tool_name)
required: list[str] = []
tool_type = None
@@ -599,7 +780,7 @@ class AgentRuntimeMixin:
return result
async def _call_mcp_tool(self, tool_name: str, arguments: dict[str, Any] | None, state: dict[str, Any]) -> dict[str, Any]:
- args = arguments or {}
+ args = await self._extract_mcp_parameters(tool_name, dict(arguments or {}), state)
telemetry = getattr(self, "telemetry", None)
prepared_server, effective_args, prepare_error = self._prepare_mcp_call(tool_name, args, state)
@@ -716,6 +897,195 @@ class AgentRuntimeMixin:
)
return result
+ @staticmethod
+ def _confirmation_decision(text: str) -> str | None:
+ normalized = " ".join((text or "").strip().lower().split())
+ normalized = re.sub(r"[.!?]+$", "", normalized).strip()
+ if normalized in {"sim", "confirmo", "sim, confirmo", "pode fazer", "pode prosseguir", "sim, desejo", "sim, desejo trocar", "sim, confirmo a devolução", "sim, confirmo a troca"}:
+ return "confirm"
+ if normalized in {"não", "nao", "cancelar", "cancele", "não confirmo", "nao confirmo"}:
+ return "reject"
+ return None
+
+ @staticmethod
+ def _extract_action_arguments(text: str) -> dict[str, Any]:
+ """Extrai apenas entidades explicitamente informadas na mensagem.
+
+ Não usa a mensagem inteira como ``reason``: frases como "quero devolver
+ uma compra" expressam a ação, mas não necessariamente o motivo. Defaults
+ declarados no mapper continuam sendo aplicados por ``build_tool_arguments``.
+ """
+ raw = text or ""
+ args: dict[str, Any] = {}
+ match = re.search(
+ r"(?:pedido|ordem)\s*(?:n[ºo°.]?\s*)?(?:é\s*(?:o\s*)?|[:#=-]\s*)?([A-Za-z0-9_-]+)",
+ raw,
+ flags=re.IGNORECASE,
+ )
+ if match:
+ args["order_id"] = match.group(1)
+
+ reason_match = re.search(
+ r"(?:porque|pois|motivo\s*[:=-]?|por\s+(?:arrependimento|defeito|erro|atraso)|me\s+arrependi(?:\s+da\s+compra)?|arrependimento)\s*(.*)",
+ raw,
+ flags=re.IGNORECASE,
+ )
+ if reason_match:
+ reason = reason_match.group(1).strip(" .,:;-")
+ if not reason:
+ matched_phrase = reason_match.group(0).strip(" .,:;-")
+ if re.search(r"me\s+arrependi|arrependimento", matched_phrase, flags=re.IGNORECASE):
+ reason = "Arrependimento da compra"
+ if reason:
+ args["reason"] = reason
+ return args
+
+ def _transactional_action_match(self, text: str, tools: list[str] | None = None) -> str | None:
+ """Detecta solicitação transacional usando metadados de tools.yaml.
+
+ Quando ``tools`` é None, examina todas as tools registradas. Isso permite
+ bloquear uma resposta direta read-only mesmo quando a intent atual ainda
+ não expôs a action tool correta.
+ """
+ normalized = (text or "").lower()
+ router = getattr(self, "tool_router", None)
+ registry = getattr(router, "registry", None)
+ names = list(tools or (list(getattr(registry, "tools", {}).keys()) if registry else []))
+ for tool in names:
+ if self._resolve_tool_execution_policy(tool).get("operation_type") != "transactional":
+ continue
+ cfg = registry.get_tool(tool) if registry else None
+ keywords = list(getattr(cfg, "selection_keywords", None) or [])
+ if any(str(token).lower() in normalized for token in keywords):
+ return tool
+ return None
+
+ def _select_transactional_tool(self, tools: list[str], text: str) -> str | None:
+ return self._transactional_action_match(text, tools)
+
+ def transaction_state_patch(self, state: dict[str, Any]) -> dict[str, Any]:
+ keys = (
+ "available_mcp_tools", "selected_tool_call", "pending_tool_call",
+ "transaction_status", "confirmation_required", "confirmation_received",
+ "tool_policy_result", "missing_parameters", "next_state",
+ )
+ return {key: state.get(key) for key in keys if key in state}
+
+
+ def transaction_clarification_message(self, state: dict[str, Any]) -> str | None:
+ """Retorna pergunta determinística para parâmetros obrigatórios ausentes."""
+ if state.get("transaction_status") != "COLLECTING_PARAMETERS":
+ return None
+ missing = list(state.get("missing_parameters") or [])
+ if not missing:
+ return None
+ labels = {
+ "order_id": "o número do pedido",
+ "reason": "o motivo da solicitação",
+ "customer_id": "a identificação do cliente",
+ }
+ friendly = [labels.get(name, str(name).replace("_", " ")) for name in missing]
+ if len(friendly) == 1:
+ detail = friendly[0]
+ else:
+ detail = ", ".join(friendly[:-1]) + " e " + friendly[-1]
+ return f"Para prosseguir, informe {detail}."
+
+ @staticmethod
+ def _missing_required_arguments(policy: dict[str, Any], arguments: dict[str, Any]) -> list[str]:
+ return [
+ str(name) for name in (policy.get("requires") or [])
+ if arguments.get(str(name)) in (None, "", [], {})
+ ]
+
+ def _set_collecting_parameters(
+ self,
+ state: dict[str, Any],
+ *,
+ tool_name: str,
+ arguments: dict[str, Any],
+ policy: dict[str, Any],
+ missing: list[str],
+ ) -> None:
+ current_agent = state.get("route") or state.get("active_agent") or "support_agent"
+ collecting_state = {
+ "billing_agent": "COLLECTING_BILLING_PARAMETERS",
+ "product_agent": "COLLECTING_PRODUCT_PARAMETERS",
+ "orders_agent": "COLLECTING_ORDER_PARAMETERS",
+ "support_agent": "COLLECTING_SUPPORT_PARAMETERS",
+ }.get(current_agent, "COLLECTING_SUPPORT_PARAMETERS")
+ state.update({
+ "selected_tool_call": {"tool_name": tool_name, "arguments": arguments},
+ "pending_tool_call": {},
+ "transaction_status": "COLLECTING_PARAMETERS",
+ "confirmation_required": False,
+ "confirmation_received": False,
+ "missing_parameters": missing,
+ "next_state": collecting_state,
+ "tool_policy_result": {**policy, "tool_name": tool_name, "action": "collecting_parameters"},
+ })
+
+ def transaction_confirmation_message(self, state: dict[str, Any]) -> str | None:
+ if state.get("transaction_status") != "AWAITING_CONFIRMATION":
+ return None
+ pending = state.get("pending_tool_call") or {}
+ tool_name = pending.get("tool_name") or "a operação solicitada"
+ args = pending.get("arguments") or {}
+ order_id = args.get("order_id")
+ target = f" para o pedido {order_id}" if order_id else ""
+ labels = {
+ "solicitar_devolucao": "a solicitação de devolução",
+ "solicitar_troca": "a solicitação de troca",
+ }
+ action = labels.get(tool_name, tool_name.replace("_", " "))
+ return f"Você confirma {action}{target}? Responda 'sim' para executar ou 'não' para cancelar."
+
+ def _select_read_only_tools(self, available_tools: list[str], text: str) -> list[str]:
+ """Seleciona somente as consultas necessárias entre as tools permitidas.
+
+ `selection_keywords` vem de tools.yaml. Se nenhuma tool casar, usa a
+ primeira read-only para preservar compatibilidade sem executar todas.
+ """
+ if len(available_tools) <= 1:
+ return list(available_tools)
+ normalized = str(text or "").lower()
+ matches: list[str] = []
+ router = getattr(self, "tool_router", None)
+ registry = getattr(router, "registry", None)
+ for name in available_tools:
+ cfg = registry.get_tool(name) if registry else None
+ keywords = list(getattr(cfg, "selection_keywords", None) or [])
+ if keywords and any(str(k).lower() in normalized for k in keywords):
+ matches.append(name)
+ return matches or available_tools[:1]
+
+ def build_direct_mcp_answer(self, state: dict[str, Any], mcp_results: list[dict[str, Any]], *, agent_label: str) -> str | None:
+ """Resposta determinística para consultas estruturadas simples."""
+ ok = [r for r in mcp_results if r.get("ok") and isinstance(r.get("result"), dict)]
+ text = state.get("sanitized_input") or state.get("user_text") or ""
+ if (
+ len(ok) != 1
+ or state.get("transaction_status")
+ or self._transactional_action_match(str(text)) is not None
+ ):
+ return None
+ tool = ok[0].get("tool_name")
+ data = ok[0]["result"]
+ if tool == "consultar_pedido":
+ oid=data.get("order_id"); status=data.get("status"); total=data.get("valor_total")
+ lines=[f"[{agent_label}] Pedido {oid}: status {status}."]
+ if total is not None: lines.append(f"Valor total: R$ {float(total):.2f}.".replace('.', ','))
+ items=data.get("itens") or []
+ if items: lines.append("Itens: " + "; ".join(str(i.get("descricao") or i.get("nome") or i.get("sku")) for i in items) + ".")
+ return " ".join(lines)
+ if tool == "consultar_entrega":
+ return f"[{agent_label}] Entrega do pedido {data.get('order_id')}: transportadora {data.get('transportadora')}, rastreio {data.get('codigo_rastreio')}, previsão {data.get('previsao_entrega')}."
+ if tool == "consultar_plano":
+ return f"[{agent_label}] Seu plano é {data.get('plano')}, com {data.get('internet_gb')} GB e status {data.get('status')}."
+ if tool == "consultar_fatura":
+ return f"[{agent_label}] Fatura consultada: {data}."
+ return None
+
async def execute_tools_for_intent(
self,
state: dict[str, Any],
@@ -724,36 +1094,206 @@ class AgentRuntimeMixin:
aliases: dict[str, Iterable[str]] | None = None,
emit_events: bool = True,
) -> list[dict[str, Any]]:
+ """Executa consultas e controla ações transacionais.
+
+ ``mcp_tools`` é uma allowlist. Tools read-only podem enriquecer o contexto;
+ uma tool transacional só é selecionada quando a mensagem expressa a ação.
+ Quando a política exige confirmação, a chamada é persistida no state e só
+ executada em um turno posterior confirmado.
+ """
results: list[dict[str, Any]] = []
- selected_tools = list(tools if tools is not None else (state.get("mcp_tools") or []))
- for tool in selected_tools:
- args = self.build_tool_arguments(state, tool_name=tool, intent=state.get("intent"), aliases=aliases)
- allowed, reason = self._validate_tool_execution_policy(tool, args)
- if not allowed:
- result = {"ok": False, "tool_name": tool, "skipped": True, "reason": reason}
+ available_tools = list(tools if tools is not None else (state.get("mcp_tools") or []))
+ state["available_mcp_tools"] = available_tools
+ text = state.get("sanitized_input") or state.get("user_text") or ""
+
+ # Antes de confirmar, complete os parâmetros obrigatórios da ação.
+ if state.get("transaction_status") == "COLLECTING_PARAMETERS":
+ selected = dict(state.get("selected_tool_call") or {})
+ tool_name = selected.get("tool_name")
+ if tool_name:
+ previous_args = dict(selected.get("arguments") or {})
+ new_args = self.build_tool_arguments(
+ state,
+ tool_name=tool_name,
+ intent=state.get("intent"),
+ aliases=aliases,
+ extra_args=self._extract_action_arguments(text),
+ )
+ arguments = {
+ **previous_args,
+ **{k: v for k, v in new_args.items() if v not in (None, "", [], {})},
+ }
+ policy = self._resolve_tool_execution_policy(tool_name, arguments)
+ missing = self._missing_required_arguments(policy, arguments)
+ if missing:
+ self._set_collecting_parameters(
+ state, tool_name=tool_name, arguments=arguments, policy=policy, missing=missing
+ )
+ return [{
+ "ok": True,
+ "executed": False,
+ "tool_name": tool_name,
+ "collecting_parameters": True,
+ "transaction_status": "COLLECTING_PARAMETERS",
+ "missing_parameters": missing,
+ "metadata": policy,
+ }]
+
+ selected = {"tool_name": tool_name, "arguments": arguments}
+ state["selected_tool_call"] = selected
+ state["missing_parameters"] = []
+ if policy.get("require_confirmation"):
+ current_agent = state.get("route") or state.get("active_agent") or "support_agent"
+ waiting_state = {
+ "billing_agent": "WAITING_BILLING_CONFIRMATION",
+ "product_agent": "WAITING_PRODUCT_CONFIRMATION",
+ "orders_agent": "WAITING_ORDER_CONFIRMATION",
+ "support_agent": "WAITING_SUPPORT_CONFIRMATION",
+ }.get(current_agent, "WAITING_SUPPORT_CONFIRMATION")
+ state.update({
+ "pending_tool_call": selected,
+ "transaction_status": "AWAITING_CONFIRMATION",
+ "confirmation_required": True,
+ "confirmation_received": False,
+ "next_state": waiting_state,
+ "tool_policy_result": {**policy, "tool_name": tool_name},
+ })
+ return [{
+ "ok": True,
+ "executed": False,
+ "tool_name": tool_name,
+ "awaiting_confirmation": True,
+ "transaction_status": "AWAITING_CONFIRMATION",
+ "metadata": policy,
+ }]
+
+ arguments["confirmed"] = True
+ result = await self._call_mcp_tool(tool_name, arguments, state)
+ state.update({
+ "transaction_status": "COMPLETED" if result.get("ok") else "FAILED",
+ "confirmation_required": False,
+ "confirmation_received": True,
+ "pending_tool_call": {},
+ "missing_parameters": [],
+ })
+ return [result]
+
+ pending = state.get("pending_tool_call") or {}
+ if pending:
+ decision = self._confirmation_decision(text)
+ if decision == "reject":
+ state.update({
+ "transaction_status": "CANCELLED",
+ "confirmation_received": False,
+ "confirmation_required": False,
+ "selected_tool_call": pending,
+ "pending_tool_call": {},
+ "tool_policy_result": {"action": "cancelled", "tool_name": pending.get("tool_name")},
+ })
+ return [{"ok": True, "tool_name": pending.get("tool_name"), "transaction_status": "CANCELLED", "cancelled": True}]
+ if decision == "confirm":
+ tool_name = pending.get("tool_name")
+ arguments = dict(pending.get("arguments") or {})
+ arguments["confirmed"] = True
+ state["confirmation_received"] = True
+ result = await self._call_mcp_tool(tool_name, arguments, state)
+ state.update({
+ "transaction_status": "COMPLETED" if result.get("ok") else "FAILED",
+ "confirmation_required": False,
+ "selected_tool_call": pending,
+ "pending_tool_call": {},
+ "tool_policy_result": {"action": "executed_after_confirmation", "tool_name": tool_name},
+ })
results.append(result)
- if emit_events:
- await self._emit_ic("IC.TOOL_SKIPPED_BY_POLICY", state, {"tool_name": tool, "reason": reason}, component="agent_runtime.tool_policy")
- continue
+ return results
+ state["transaction_status"] = "AWAITING_CONFIRMATION"
+ state["confirmation_required"] = True
+ return [{"ok": False, "tool_name": pending.get("tool_name"), "awaiting_confirmation": True, "transaction_status": "AWAITING_CONFIRMATION"}]
+
+ read_only_tools = [
+ tool for tool in available_tools
+ if self._resolve_tool_execution_policy(tool).get("operation_type") != "transactional"
+ ]
+ read_only_tools = self._select_read_only_tools(read_only_tools, text)
+ state["selected_read_only_tools"] = read_only_tools
+ for tool in read_only_tools:
+ args = self.build_tool_arguments(state, tool_name=tool, intent=state.get("intent"), aliases=aliases)
if emit_events:
- await self._emit_ic("IC.MCP_TOOL_REQUESTED", state, {"tool_name": tool}, component="agent_runtime")
+ await self._emit_ic("IC.MCP_TOOL_REQUESTED", state, {"tool_name": tool, "operation_type": "read_only"}, component="agent_runtime")
result = await self._call_mcp_tool(tool, args, state)
results.append(result)
+
+ selected_action = self._select_transactional_tool(available_tools, text)
+ if not selected_action:
+ return results
+
+ action_args = self.build_tool_arguments(
+ state,
+ tool_name=selected_action,
+ intent=state.get("intent"),
+ aliases=aliases,
+ extra_args=self._extract_action_arguments(text),
+ )
+ policy = self._resolve_tool_execution_policy(selected_action, action_args)
+ selected = {"tool_name": selected_action, "arguments": action_args}
+ state["selected_tool_call"] = selected
+ state["tool_policy_result"] = {**policy, "tool_name": selected_action}
+
+ missing = self._missing_required_arguments(policy, action_args)
+ if missing:
+ self._set_collecting_parameters(
+ state,
+ tool_name=selected_action,
+ arguments=action_args,
+ policy=policy,
+ missing=missing,
+ )
if emit_events:
await self._emit_ic(
- "IC.TOOL_CALLED",
+ "IC.TRANSACTION_PARAMETERS_REQUIRED",
state,
- {
- "tool_name": tool,
- "ok": result.get("ok"),
- "server_name": result.get("server_name"),
- "error": result.get("error"),
- "cached": bool(result.get("cached")),
- },
- component="agent_runtime",
+ {"tool_name": selected_action, "missing_parameters": missing, **policy},
+ component="agent_runtime.tool_policy",
)
- if not result.get("ok"):
- await self._emit_noc("NOC.MCP_TOOL_FAILED", state, {"tool_name": tool, "error": result.get("error")}, component="agent_runtime")
+ results.append({
+ "ok": True,
+ "executed": False,
+ "tool_name": selected_action,
+ "collecting_parameters": True,
+ "transaction_status": "COLLECTING_PARAMETERS",
+ "missing_parameters": missing,
+ "metadata": policy,
+ })
+ return results
+
+ if policy.get("require_confirmation"):
+ state.update({
+ "pending_tool_call": selected,
+ "transaction_status": "AWAITING_CONFIRMATION",
+ "confirmation_required": True,
+ "confirmation_received": False,
+ })
+ current_agent = state.get("route") or state.get("active_agent") or "support_agent"
+ state["next_state"] = {
+ "billing_agent": "WAITING_BILLING_CONFIRMATION",
+ "product_agent": "WAITING_PRODUCT_CONFIRMATION",
+ "orders_agent": "WAITING_ORDER_CONFIRMATION",
+ "support_agent": "WAITING_SUPPORT_CONFIRMATION",
+ }.get(current_agent, "WAITING_SUPPORT_CONFIRMATION")
+ if emit_events:
+ await self._emit_ic("IC.TRANSACTION_CONFIRMATION_REQUIRED", state, {"tool_name": selected_action, **policy}, component="agent_runtime.tool_policy")
+ results.append({"ok": False, "tool_name": selected_action, "awaiting_confirmation": True, "transaction_status": "AWAITING_CONFIRMATION", "metadata": policy})
+ return results
+
+ action_args["confirmed"] = True
+ result = await self._call_mcp_tool(selected_action, action_args, state)
+ state.update({
+ "transaction_status": "COMPLETED" if result.get("ok") else "FAILED",
+ "confirmation_required": False,
+ "confirmation_received": True,
+ "pending_tool_call": {},
+ })
+ results.append(result)
return results
async def _collect_mcp_context(self, state: dict[str, Any]) -> list[dict[str, Any]]:
diff --git a/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..628d64a
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc differ
diff --git a/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc
new file mode 100644
index 0000000..4f6c70c
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc differ
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
new file mode 100644
index 0000000..32f0587
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc differ
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
new file mode 100644
index 0000000..ab3c206
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/router_supervisor.cpython-313.pyc differ
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
new file mode 100644
index 0000000..f272d36
Binary files /dev/null and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc differ
diff --git a/mcp/servers/retail_mcp_server/main.py b/mcp/servers/retail_mcp_server/main.py
index e0c75a1..6f4e3e9 100644
--- a/mcp/servers/retail_mcp_server/main.py
+++ b/mcp/servers/retail_mcp_server/main.py
@@ -59,7 +59,7 @@ async def call_tool(call: ToolCall):
result = {
"order_id": args.get("order_id") or "PED-1001",
"customer_id": args.get("customer_id") or "CLIENTE-001",
- "status": "EM_TRANSPORTE",
+ "status": "ENTREGUE" if str(args.get("order_id") or "").upper() in {"123", "PED-ENTREGUE"} else "EM_TRANSPORTE",
"valor_total": 349.90,
"itens": [
{"sku": "LIV-001", "descricao": "Livro de Arquitetura de IA", "quantidade": 1, "valor": 199.90},
diff --git a/mcp/servers/retail_mcp_server/main_fastmcp.py b/mcp/servers/retail_mcp_server/main_fastmcp.py
index 0399794..5abf44d 100644
--- a/mcp/servers/retail_mcp_server/main_fastmcp.py
+++ b/mcp/servers/retail_mcp_server/main_fastmcp.py
@@ -13,7 +13,7 @@ def consultar_pedido(customer_id: str | None = None, order_id: str | None = None
return {
"customer_id": customer_id or "CUST-001",
"order_id": order_id or "ORD-001",
- "status": "EM_TRANSPORTE",
+ "status": "ENTREGUE" if str(order_id or "").upper() in {"123", "PED-ENTREGUE"} else "EM_TRANSPORTE",
"valor_total": 399.90,
"itens": [{"sku": "SKU-001", "nome": "Produto exemplo", "quantidade": 1}],
}
@@ -31,23 +31,23 @@ def consultar_entrega(order_id: str | None = None) -> dict[str, Any]:
@mcp.tool()
-def solicitar_troca(order_id: str | None = None, motivo: str | None = None) -> dict[str, Any]:
+def solicitar_troca(order_id: str | None = None, reason: str | None = None) -> dict[str, Any]:
"""Abre solicitação de troca para um pedido."""
return {
"order_id": order_id or "ORD-001",
"protocolo": "TROCA-123456",
- "motivo": motivo or "Não informado",
+ "reason": reason or "Não informado",
"status": "ABERTA",
}
@mcp.tool()
-def solicitar_devolucao(order_id: str | None = None, motivo: str | None = None) -> dict[str, Any]:
+def solicitar_devolucao(order_id: str | None = None, reason: str | None = None) -> dict[str, Any]:
"""Abre solicitação de devolução para um pedido."""
return {
"order_id": order_id or "ORD-001",
"protocolo": "DEV-123456",
- "motivo": motivo or "Não informado",
+ "reason": reason or "Não informado",
"status": "ABERTA",
}
diff --git a/specs/SPEC-004-MCP-Gateway.md b/specs/SPEC-004-MCP-Gateway.md
index c6dc6c1..600cf77 100644
--- a/specs/SPEC-004-MCP-Gateway.md
+++ b/specs/SPEC-004-MCP-Gateway.md
@@ -225,3 +225,7 @@ execution:
| MCP Gateway | Aplicação de governança e roteamento de tools MCP. |
| Evaluator | Camada de avaliação online/offline, regressão e certificação. |
| Business Context | Conjunto de chaves canônicas de negócio: customer_key, contract_key, interaction_key, account_key, resource_key e session_key. |
+
+## Política mínima de operação
+
+Antes de encaminhar uma tool, o runtime deve aplicar a política opcional do backend em `config/tool_policies.yaml`. Os tipos canônicos são `read_only` e `transactional`; esta última pode exigir confirmação booleana explícita e campos obrigatórios. A ausência do arquivo não é erro e preserva os campos legados de `tools.yaml`. A política conversacional não substitui autenticação, autorização, idempotência nem atomicidade no MCP Server.
diff --git a/templates/agent_template_backend/.env b/templates/agent_template_backend/.env
index 0ac21b5..4556734 100644
--- a/templates/agent_template_backend/.env
+++ b/templates/agent_template_backend/.env
@@ -14,38 +14,45 @@ 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_openai
+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/openai/v1
+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-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
+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=DEFAULT
-OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+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=sqlite
-MEMORY_REPOSITORY_PROVIDER=sqlite
-CHECKPOINT_REPOSITORY_PROVIDER=sqlite
-SQLITE_DB_PATH=./data/agent_framework.db
+SESSION_REPOSITORY_PROVIDER=autonomous
+MEMORY_REPOSITORY_PROVIDER=autonomous
+CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
-ADB_PASSWORD=fjhsdf04954hf
-ADB_DSN=oradb23aidev_high
-ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
-ADB_WALLET_PASSWORD=fjhsdf04954hf
+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
@@ -59,10 +66,10 @@ ENABLE_REDIS_CACHE=false
###############################################################################
# RAG / Vector / Graph
###############################################################################
-VECTOR_STORE_PROVIDER=sqlite
-GRAPH_STORE_PROVIDER=sqlite
+VECTOR_STORE_PROVIDER=autonomous
+GRAPH_STORE_PROVIDER=autonomous
RAG_TOP_K=5
-EMBEDDING_PROVIDER=mock
+EMBEDDING_PROVIDER=oci
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
@@ -70,14 +77,21 @@ RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
# Observabilidade
###############################################################################
ENABLE_LANGFUSE=true
-LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
-LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
-LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
+ # 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
@@ -85,7 +99,7 @@ ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
ENABLE_ANALYTICS=false
# Providers aceitos: oci_streaming,pubsub,noop
-ANALYTICS_PROVIDERS=pubsub
+ANALYTICS_PROVIDERS=oci_streaming
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC=
GCP_PUBSUB_TOPIC_PATH=
@@ -138,6 +152,17 @@ ROUTING_CONFIG_PATH=./config/routing.yaml
# 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
###############################################################################
@@ -150,7 +175,7 @@ MCP_TOOL_TIMEOUT_SECONDS=30
ROUTING_MODE=router
# Usage/cost accounting
-USAGE_REPOSITORY_PROVIDER=sqlite
+USAGE_REPOSITORY_PROVIDER=autonomous
IDENTITY_CONFIG_PATH=./config/identity.yaml
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
@@ -167,18 +192,6 @@ MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
-###############################################################################
-# MCP Gateway
-###############################################################################
-# true = framework routes tool calls to the dedicated MCP Gateway.
-# false = framework calls MCP servers directly from mcp_servers.yaml.
-MCP_GATEWAY_ENABLED=true
-MCP_GATEWAY_URL=http://localhost:8300
-MCP_GATEWAY_TIMEOUT_SECONDS=60
-# MCP_GATEWAY_TOKEN=
-MCP_GATEWAY_AGENT_ID=telecom_contas
-MCP_GATEWAY_TENANT_ID=default
-
###############################################################################
# LONG-TERM MEMORY
###############################################################################
diff --git a/templates/agent_template_backend/README.md b/templates/agent_template_backend/README.md
index 1eec12c..0cf81d7 100644
--- a/templates/agent_template_backend/README.md
+++ b/templates/agent_template_backend/README.md
@@ -4207,3 +4207,7 @@ A implementação está arquiteturalmente correta quando:
```
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.
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 81e4d15..884409e 100644
Binary files a/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc and b/templates/agent_template_backend/app/__pycache__/__init__.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc b/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc
index c407e91..7f097b7 100644
Binary files a/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc and b/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc differ
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 f2aaf83..d662183 100644
Binary files a/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc and b/templates/agent_template_backend/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc b/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc
index f83d3c1..e5055fe 100644
Binary files a/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc and b/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc differ
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 55af51d..0e9f1e1 100644
Binary files a/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc and b/templates/agent_template_backend/app/agents/__pycache__/billing_agent.cpython-313.pyc differ
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 d5454cc..f05ef2c 100644
Binary files a/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc and b/templates/agent_template_backend/app/agents/__pycache__/orders_agent.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc b/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc
index c959856..2b36337 100644
Binary files a/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc and b/templates/agent_template_backend/app/agents/__pycache__/product_agent.cpython-313.pyc differ
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 b5bdcfb..27c5dd6 100644
Binary files a/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc and b/templates/agent_template_backend/app/agents/__pycache__/prompting.cpython-313.pyc differ
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 b1847a2..321fc08 100644
Binary files a/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc and b/templates/agent_template_backend/app/agents/__pycache__/runtime.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc b/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc
index 3797425..bcd0559 100644
Binary files a/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc and b/templates/agent_template_backend/app/agents/__pycache__/support_agent.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/agents/billing_agent.py b/templates/agent_template_backend/app/agents/billing_agent.py
index e941eb6..aa60099 100644
--- a/templates/agent_template_backend/app/agents/billing_agent.py
+++ b/templates/agent_template_backend/app/agents/billing_agent.py
@@ -44,6 +44,36 @@ class BillingAgent(AgentRuntimeMixin):
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(
@@ -79,6 +109,7 @@ class BillingAgent(AgentRuntimeMixin):
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
+ **self.transaction_state_patch(state),
}
await self._emit_ic(
diff --git a/templates/agent_template_backend/app/agents/orders_agent.py b/templates/agent_template_backend/app/agents/orders_agent.py
index 665afaa..f557bed 100644
--- a/templates/agent_template_backend/app/agents/orders_agent.py
+++ b/templates/agent_template_backend/app/agents/orders_agent.py
@@ -44,6 +44,36 @@ class OrdersAgent(AgentRuntimeMixin):
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(
@@ -79,6 +109,7 @@ class OrdersAgent(AgentRuntimeMixin):
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
+ **self.transaction_state_patch(state),
}
await self._emit_ic(
diff --git a/templates/agent_template_backend/app/agents/product_agent.py b/templates/agent_template_backend/app/agents/product_agent.py
index 5c8ebb4..34433f5 100644
--- a/templates/agent_template_backend/app/agents/product_agent.py
+++ b/templates/agent_template_backend/app/agents/product_agent.py
@@ -44,6 +44,36 @@ class ProductAgent(AgentRuntimeMixin):
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(
@@ -79,6 +109,7 @@ class ProductAgent(AgentRuntimeMixin):
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
+ **self.transaction_state_patch(state),
}
await self._emit_ic(
diff --git a/templates/agent_template_backend/app/agents/support_agent.py b/templates/agent_template_backend/app/agents/support_agent.py
index 1613997..b4f0244 100644
--- a/templates/agent_template_backend/app/agents/support_agent.py
+++ b/templates/agent_template_backend/app/agents/support_agent.py
@@ -44,6 +44,36 @@ class SupportAgent(AgentRuntimeMixin):
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(
@@ -79,6 +109,7 @@ class SupportAgent(AgentRuntimeMixin):
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
+ **self.transaction_state_patch(state),
}
await self._emit_ic(
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 95ea2b3..e011e2c 100644
Binary files a/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc and b/templates/agent_template_backend/app/examples/__pycache__/__init__.cpython-313.pyc differ
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 8e4bd21..417fdea 100644
Binary files a/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc and b/templates/agent_template_backend/app/examples/__pycache__/grl_examples.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc b/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc
index 5ee30f0..43c2841 100644
Binary files a/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc and b/templates/agent_template_backend/app/examples/__pycache__/ic_examples.cpython-313.pyc differ
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 223d915..8684f23 100644
Binary files a/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc and b/templates/agent_template_backend/app/examples/__pycache__/mcp_examples.cpython-313.pyc differ
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 683dcc4..19cee2f 100644
Binary files a/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc and b/templates/agent_template_backend/app/examples/__pycache__/noc_examples.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc b/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc
index f59adf5..5919f9d 100644
Binary files a/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc and b/templates/agent_template_backend/app/examples/__pycache__/observer_examples.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/main.py b/templates/agent_template_backend/app/main.py
index 06d1bd1..d51bbc2 100644
--- a/templates/agent_template_backend/app/main.py
+++ b/templates/agent_template_backend/app/main.py
@@ -291,6 +291,11 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"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": {
@@ -365,6 +370,13 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"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)
@@ -387,6 +399,14 @@ async def health():
"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,
diff --git a/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc b/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc
index f8eaf36..60f4328 100644
Binary files a/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc and b/templates/agent_template_backend/app/observability/__pycache__/__init__.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc b/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc
index 2b6330f..a01c7f1 100644
Binary files a/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc and b/templates/agent_template_backend/app/observability/__pycache__/telemetry_observer.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/state.py b/templates/agent_template_backend/app/state.py
index 754c71c..cc19c03 100644
--- a/templates/agent_template_backend/app/state.py
+++ b/templates/agent_template_backend/app/state.py
@@ -23,9 +23,22 @@ class AgentState(TypedDict, total=False):
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
@@ -36,3 +49,5 @@ class AgentState(TypedDict, total=False):
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/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 9ba65e9..17e9714 100644
Binary files a/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc and b/templates/agent_template_backend/app/workflows/__pycache__/agent_graph.cpython-313.pyc differ
diff --git a/templates/agent_template_backend/app/workflows/agent_graph.py b/templates/agent_template_backend/app/workflows/agent_graph.py
index bdc8e13..b8ed7bc 100644
--- a/templates/agent_template_backend/app/workflows/agent_graph.py
+++ b/templates/agent_template_backend/app/workflows/agent_graph.py
@@ -139,12 +139,15 @@ class AgentWorkflow:
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))
@@ -157,8 +160,9 @@ class AgentWorkflow:
builder.add_conditional_edges(
"input_guardrails",
self._after_input_guardrails,
- {"blocked": "persist", "continue": "routing_decision"},
+ {"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"),
@@ -168,6 +172,8 @@ class AgentWorkflow:
"orders_agent": "orders_agent",
"support_agent": "support_agent",
"handoff": "handoff",
+ "human_handoff": "human_handoff",
+ "end_session": "end_session",
"supervisor_agent": "supervisor_agent",
},
)
@@ -176,6 +182,8 @@ class AgentWorkflow:
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")
@@ -190,6 +198,24 @@ class AgentWorkflow:
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"),
@@ -326,6 +352,17 @@ class AgentWorkflow:
"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):
@@ -415,6 +452,48 @@ class AgentWorkflow:
)
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.
@@ -576,8 +655,34 @@ class AgentWorkflow:
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"], state.get("context", {})
+ state["user_text"], state["final_answer"], judge_context
)
for _result in results:
await self.judge_telemetry.evaluated(_result)
@@ -607,9 +712,78 @@ class AgentWorkflow:
)
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):
- result = await self.long_term_memory_manager.persist_turn(state)
- return {"long_term_memory_write_result": result}
+ 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(
diff --git a/templates/agent_template_backend/config/judges.yaml b/templates/agent_template_backend/config/judges.yaml
index d488063..c091619 100644
--- a/templates/agent_template_backend/config/judges.yaml
+++ b/templates/agent_template_backend/config/judges.yaml
@@ -1,20 +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
\ No newline at end of file
+- 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/templates/agent_template_backend/config/mcp_parameter_mapping.yaml b/templates/agent_template_backend/config/mcp_parameter_mapping.yaml
index ba3bdaf..5b29ccf 100644
--- a/templates/agent_template_backend/config/mcp_parameter_mapping.yaml
+++ b/templates/agent_template_backend/config/mcp_parameter_mapping.yaml
@@ -8,18 +8,16 @@ mcp_parameter_mapping:
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.
+ 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
@@ -38,21 +36,57 @@ mcp_parameter_mapping:
consultar_pedido:
map:
customer_key: customer_id
- contract_key: order_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:
- contract_key: order_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
solicitar_troca:
map:
- contract_key: order_id
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:
- contract_key: order_id
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/templates/agent_template_backend/config/routing.yaml b/templates/agent_template_backend/config/routing.yaml
index ce9a5f2..2dbe95e 100644
--- a/templates/agent_template_backend/config/routing.yaml
+++ b/templates/agent_template_backend/config/routing.yaml
@@ -20,6 +20,18 @@ state_policies:
- 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
@@ -55,7 +67,6 @@ intents:
- listar_servicos
keywords:
- plano
- - produto
- serviço
- pacote
- internet
@@ -94,12 +105,15 @@ intents:
domain: retail
agent: support_agent
description: Suporte, troca, devolução, garantia e problema com produto.
- priority: 40
+ priority: 25
mcp_tools:
- consultar_pedido
- solicitar_troca
- solicitar_devolucao
keywords:
+ - solicitar devolução
+ - devolver pedido
+ - solicitar troca
- troca
- devolução
- devolver
diff --git a/templates/agent_template_backend/config/tool_policies.yaml b/templates/agent_template_backend/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/templates/agent_template_backend/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/templates/agent_template_backend/config/tools.yaml b/templates/agent_template_backend/config/tools.yaml
index 02b83dc..d85fae1 100644
--- a/templates/agent_template_backend/config/tools.yaml
+++ b/templates/agent_template_backend/config/tools.yaml
@@ -6,14 +6,19 @@ tools:
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
@@ -21,14 +26,18 @@ tools:
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
@@ -36,33 +45,57 @@ tools:
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: false
+ 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: false
+ 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/templates/agent_template_backend/data/agent_framework.db b/templates/agent_template_backend/data/agent_framework.db
index d1d18fd..ddf1883 100644
Binary files a/templates/agent_template_backend/data/agent_framework.db and b/templates/agent_template_backend/data/agent_framework.db differ
diff --git a/templates/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/templates/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
new file mode 100644
index 0000000..5c41732
--- /dev/null
+++ b/templates/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/templates/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md b/templates/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md
new file mode 100644
index 0000000..cfe5969
--- /dev/null
+++ b/templates/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/templates/agent_template_backend/llm_profiles.yaml b/templates/agent_template_backend/llm_profiles.yaml
index 15a1cd0..908b382 100644
--- a/templates/agent_template_backend/llm_profiles.yaml
+++ b/templates/agent_template_backend/llm_profiles.yaml
@@ -1,88 +1,80 @@
-# Optional file. If this file is absent, the backend keeps using .env exactly as before.
-# If present, each inference point can override provider/model/params.
profiles:
default:
provider: oci_openai
model: openai.gpt-4.1
temperature: 0.2
max_tokens: 2048
-
- # Workflow/routing
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
-
- # Safety / evaluation
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
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: xopenai.gpt-4.1
+ model: openai.gpt-4.1
temperature: 0.1
max_tokens: 1800
-
- # Memory / operations
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
-
- # Agent-specific overrides
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/templates/agent_template_backend_day_zero/.env b/templates/agent_template_backend_day_zero/.env
index ffeb4f9..31aa694 100644
--- a/templates/agent_template_backend_day_zero/.env
+++ b/templates/agent_template_backend_day_zero/.env
@@ -135,12 +135,23 @@ ROUTING_CONFIG_PATH=./config/routing.yaml
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
ENABLE_LLM_ROUTER=true
+# Continuidade semântica, handoff humano e encerramento global.
+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.
+SESSION_ALREADY_ENDED_MESSAGE=Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.
+
###############################################################################
# MCP / Tools
###############################################################################
ENABLE_MCP_TOOLS=true
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
TOOLS_CONFIG_PATH=./config/tools.yaml
+TOOL_POLICIES_PATH=./config/tool_policies.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
diff --git a/templates/agent_template_backend_day_zero/Dockerfile b/templates/agent_template_backend_day_zero/Dockerfile
index 273fe01..e50bea7 100644
--- a/templates/agent_template_backend_day_zero/Dockerfile
+++ b/templates/agent_template_backend_day_zero/Dockerfile
@@ -1,6 +1,6 @@
FROM python:3.12-slim
WORKDIR /app
COPY agent_framework /agent_framework
-COPY agent_template_backend /app
+COPY agent_template_backend_day_zero /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/templates/agent_template_backend_day_zero/README_DAY_ZERO.md b/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
index 5df0468..869ed33 100644
--- a/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
+++ b/templates/agent_template_backend_day_zero/README_DAY_ZERO.md
@@ -84,3 +84,6 @@ return {
- `app/agents/runtime.py`
Esses arquivos são o esqueleto de execução usando o framework.
+# Política opcional de tools
+
+O arquivo `config/tool_policies.yaml` classifica tools como `read_only` ou `transactional`. Para uma transação real, ative `require_confirmation: true`; chamadas sem `confirmed: true` ou `confirmation: true` serão bloqueadas antes do MCP. A ausência do arquivo preserva o comportamento de templates anteriores.
diff --git a/templates/agent_template_backend_day_zero/app/agents/billing_agent.py b/templates/agent_template_backend_day_zero/app/agents/billing_agent.py
index 2c65234..aa60099 100644
--- a/templates/agent_template_backend_day_zero/app/agents/billing_agent.py
+++ b/templates/agent_template_backend_day_zero/app/agents/billing_agent.py
@@ -1,10 +1,3 @@
-"""
-DAY ZERO TEMPLATE - BillingAgent
-
-Esqueleto mínimo já compatível com ConversationSummaryMemory.
-Substitua o prompt e a regra de negócio conforme o seu agente.
-"""
-
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
@@ -35,12 +28,67 @@ class BillingAgent(AgentRuntimeMixin):
self.summary_memory = summary_memory
async def run(self, state):
- # OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
- tool_context = []
- rag_context = None
- rag_metadata = {}
+ await self._emit_ic(
+ "IC.BILLING_AGENT_STARTED",
+ state,
+ {"business_component": "faturas"},
+ component="agent.billing.start",
+ )
- # Prepara a memória resumida antes do prompt.
+ 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(
@@ -55,13 +103,27 @@ class BillingAgent(AgentRuntimeMixin):
)
answer = await self._invoke_llm_cached(state, "BillingAgent", messages)
- return {
- "answer": answer,
- "next_state": "DAY_ZERO_ACTIVE",
+ 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/templates/agent_template_backend_day_zero/app/agents/orders_agent.py b/templates/agent_template_backend_day_zero/app/agents/orders_agent.py
index ae14a4e..f557bed 100644
--- a/templates/agent_template_backend_day_zero/app/agents/orders_agent.py
+++ b/templates/agent_template_backend_day_zero/app/agents/orders_agent.py
@@ -1,10 +1,3 @@
-"""
-DAY ZERO TEMPLATE - OrdersAgent
-
-Esqueleto mínimo já compatível com ConversationSummaryMemory.
-Substitua o prompt e a regra de negócio conforme o seu agente.
-"""
-
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
@@ -35,12 +28,67 @@ class OrdersAgent(AgentRuntimeMixin):
self.summary_memory = summary_memory
async def run(self, state):
- # OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
- tool_context = []
- rag_context = None
- rag_metadata = {}
+ await self._emit_ic(
+ "IC.ORDERS_AGENT_STARTED",
+ state,
+ {"business_component": "pedidos"},
+ component="agent.orders.start",
+ )
- # Prepara a memória resumida antes do prompt.
+ 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(
@@ -55,13 +103,27 @@ class OrdersAgent(AgentRuntimeMixin):
)
answer = await self._invoke_llm_cached(state, "OrdersAgent", messages)
- return {
- "answer": answer,
- "next_state": "DAY_ZERO_ACTIVE",
+ 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/templates/agent_template_backend_day_zero/app/agents/product_agent.py b/templates/agent_template_backend_day_zero/app/agents/product_agent.py
index b4f0232..34433f5 100644
--- a/templates/agent_template_backend_day_zero/app/agents/product_agent.py
+++ b/templates/agent_template_backend_day_zero/app/agents/product_agent.py
@@ -1,10 +1,3 @@
-"""
-DAY ZERO TEMPLATE - ProductAgent
-
-Esqueleto mínimo já compatível com ConversationSummaryMemory.
-Substitua o prompt e a regra de negócio conforme o seu agente.
-"""
-
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
@@ -35,12 +28,67 @@ class ProductAgent(AgentRuntimeMixin):
self.summary_memory = summary_memory
async def run(self, state):
- # OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
- tool_context = []
- rag_context = None
- rag_metadata = {}
+ await self._emit_ic(
+ "IC.PRODUCT_AGENT_STARTED",
+ state,
+ {"business_component": "produtos"},
+ component="agent.product.start",
+ )
- # Prepara a memória resumida antes do prompt.
+ 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(
@@ -55,13 +103,27 @@ class ProductAgent(AgentRuntimeMixin):
)
answer = await self._invoke_llm_cached(state, "ProductAgent", messages)
- return {
- "answer": answer,
- "next_state": "DAY_ZERO_ACTIVE",
+ 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/templates/agent_template_backend_day_zero/app/agents/support_agent.py b/templates/agent_template_backend_day_zero/app/agents/support_agent.py
index 86c3c97..b4f0244 100644
--- a/templates/agent_template_backend_day_zero/app/agents/support_agent.py
+++ b/templates/agent_template_backend_day_zero/app/agents/support_agent.py
@@ -1,10 +1,3 @@
-"""
-DAY ZERO TEMPLATE - SupportAgent
-
-Esqueleto mínimo já compatível com ConversationSummaryMemory.
-Substitua o prompt e a regra de negócio conforme o seu agente.
-"""
-
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
@@ -35,12 +28,67 @@ class SupportAgent(AgentRuntimeMixin):
self.summary_memory = summary_memory
async def run(self, state):
- # OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
- tool_context = []
- rag_context = None
- rag_metadata = {}
+ await self._emit_ic(
+ "IC.SUPPORT_AGENT_STARTED",
+ state,
+ {"business_component": "suporte"},
+ component="agent.support.start",
+ )
- # Prepara a memória resumida antes do prompt.
+ 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(
@@ -55,13 +103,27 @@ class SupportAgent(AgentRuntimeMixin):
)
answer = await self._invoke_llm_cached(state, "SupportAgent", messages)
- return {
- "answer": answer,
- "next_state": "DAY_ZERO_ACTIVE",
+ 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/templates/agent_template_backend_day_zero/app/state.py b/templates/agent_template_backend_day_zero/app/state.py
index 754c71c..ac673d6 100644
--- a/templates/agent_template_backend_day_zero/app/state.py
+++ b/templates/agent_template_backend_day_zero/app/state.py
@@ -23,9 +23,22 @@ class AgentState(TypedDict, total=False):
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
diff --git a/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py b/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
index bdc8e13..0a12c4b 100644
--- a/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
+++ b/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py
@@ -145,6 +145,8 @@ class AgentWorkflow:
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))
@@ -168,6 +170,8 @@ class AgentWorkflow:
"orders_agent": "orders_agent",
"support_agent": "support_agent",
"handoff": "handoff",
+ "human_handoff": "human_handoff",
+ "end_session": "end_session",
"supervisor_agent": "supervisor_agent",
},
)
@@ -176,6 +180,8 @@ class AgentWorkflow:
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")
@@ -190,6 +196,24 @@ class AgentWorkflow:
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"),
@@ -326,6 +350,17 @@ class AgentWorkflow:
"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):
@@ -415,6 +450,48 @@ class AgentWorkflow:
)
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.
@@ -576,8 +653,34 @@ class AgentWorkflow:
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"], state.get("context", {})
+ state["user_text"], state["final_answer"], judge_context
)
for _result in results:
await self.judge_telemetry.evaluated(_result)
diff --git a/templates/agent_template_backend_day_zero/config/judges.yaml b/templates/agent_template_backend_day_zero/config/judges.yaml
index d488063..c091619 100644
--- a/templates/agent_template_backend_day_zero/config/judges.yaml
+++ b/templates/agent_template_backend_day_zero/config/judges.yaml
@@ -1,20 +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
\ No newline at end of file
+- 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/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml b/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml
index e4e3e30..5b29ccf 100644
--- a/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml
+++ b/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml
@@ -1,8 +1,3 @@
-# ============================================================================
-# DAY ZERO
-# Este arquivo foi copiado do agent_template_backend original.
-# Ajuste os exemplos abaixo para o domínio do seu novo agente.
-# ============================================================================
mcp_parameter_mapping:
defaults:
use_mock: true
@@ -13,6 +8,16 @@ mcp_parameter_mapping:
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
@@ -31,21 +36,57 @@ mcp_parameter_mapping:
consultar_pedido:
map:
customer_key: customer_id
- contract_key: order_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:
- contract_key: order_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
solicitar_troca:
map:
- contract_key: order_id
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:
- contract_key: order_id
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/templates/agent_template_backend_day_zero/config/routing.yaml b/templates/agent_template_backend_day_zero/config/routing.yaml
index 25ad1c4..34c9781 100644
--- a/templates/agent_template_backend_day_zero/config/routing.yaml
+++ b/templates/agent_template_backend_day_zero/config/routing.yaml
@@ -25,6 +25,18 @@ state_policies:
- 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
@@ -60,7 +72,6 @@ intents:
- listar_servicos
keywords:
- plano
- - produto
- serviço
- pacote
- internet
@@ -99,12 +110,15 @@ intents:
domain: retail
agent: support_agent
description: Suporte, troca, devolução, garantia e problema com produto.
- priority: 40
+ priority: 25
mcp_tools:
- consultar_pedido
- solicitar_troca
- solicitar_devolucao
keywords:
+ - solicitar devolução
+ - devolver pedido
+ - solicitar troca
- troca
- devolução
- devolver
diff --git a/templates/agent_template_backend_day_zero/config/tool_policies.yaml b/templates/agent_template_backend_day_zero/config/tool_policies.yaml
new file mode 100644
index 0000000..66c9854
--- /dev/null
+++ b/templates/agent_template_backend_day_zero/config/tool_policies.yaml
@@ -0,0 +1,21 @@
+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
+
+# 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/templates/agent_template_backend_day_zero/config/tools.yaml b/templates/agent_template_backend_day_zero/config/tools.yaml
index 5b9fc20..d85fae1 100644
--- a/templates/agent_template_backend_day_zero/config/tools.yaml
+++ b/templates/agent_template_backend_day_zero/config/tools.yaml
@@ -1,8 +1,3 @@
-# ============================================================================
-# DAY ZERO
-# Este arquivo foi copiado do agent_template_backend original.
-# Ajuste os exemplos abaixo para o domínio do seu novo agente.
-# ============================================================================
tools:
consultar_fatura:
description: Consulta dados resumidos de fatura por msisdn/invoice_id.
@@ -11,14 +6,19 @@ tools:
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
@@ -26,14 +26,18 @@ tools:
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
@@ -41,32 +45,57 @@ tools:
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: false
+ 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: false
+ 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/templates/agent_template_backend_day_zero/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/templates/agent_template_backend_day_zero/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
new file mode 100644
index 0000000..ba194c7
--- /dev/null
+++ b/templates/agent_template_backend_day_zero/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md
@@ -0,0 +1,13 @@
+# Exemplos implementados no template Day Zero
+
+O Day Zero preserva seu conteúdo simplificado, mas possui o mesmo conjunto transversal do template completo:
+
+- 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. Substitua os agentes e ferramentas de exemplo sem remover os controles transversais.
diff --git a/templates/agent_template_backend_day_zero/llm_profiles.yaml b/templates/agent_template_backend_day_zero/llm_profiles.yaml
new file mode 100644
index 0000000..908b382
--- /dev/null
+++ b/templates/agent_template_backend_day_zero/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/tests/test_judge_transaction_sampling.py b/tests/test_judge_transaction_sampling.py
new file mode 100644
index 0000000..3a4419f
--- /dev/null
+++ b/tests/test_judge_transaction_sampling.py
@@ -0,0 +1,60 @@
+import asyncio
+from types import SimpleNamespace
+
+from agent_framework.judges.judge import JudgePipeline, JudgeResult
+
+
+class DummyJudge:
+ async def evaluate(self, question, answer, context):
+ return JudgeResult(name="dummy", score=1.0, passed=True, reason="ran")
+
+
+def pipeline(*, sample_rate=0.0, always=True):
+ obj = object.__new__(JudgePipeline)
+ obj.enabled = True
+ obj.judges = [DummyJudge()]
+ obj.sample_rate = sample_rate
+ obj.always_run_for_transactional = always
+ return obj
+
+
+def test_awaiting_confirmation_bypasses_sampling():
+ p = pipeline(sample_rate=0.0, always=True)
+ results = asyncio.run(p.evaluate_all("devolver", "confirma?", {
+ "transaction_status": "AWAITING_CONFIRMATION",
+ "mcp_results": [{
+ "tool_name": "solicitar_devolucao",
+ "awaiting_confirmation": True,
+ "transaction_status": "AWAITING_CONFIRMATION",
+ "metadata": {"operation_type": "transactional"},
+ }],
+ }))
+ assert len(results) == 1
+
+
+def test_completed_transaction_bypasses_sampling_from_mcp_result():
+ p = pipeline(sample_rate=0.0, always=True)
+ results = asyncio.run(p.evaluate_all("sim", "protocolo DEV-1", {
+ "mcp_results": [{
+ "tool_name": "solicitar_devolucao",
+ "ok": True,
+ "metadata": {"operation_type": "transactional"},
+ }],
+ }))
+ assert len(results) == 1
+
+
+def test_non_transactional_turn_respects_zero_sample_rate():
+ p = pipeline(sample_rate=0.0, always=True)
+ results = asyncio.run(p.evaluate_all("pedido 123", "entregue", {
+ "mcp_results": [{"tool_name": "consultar_pedido", "ok": True}],
+ }))
+ assert results == []
+
+
+def test_transactional_detection_from_tool_policy():
+ p = pipeline(sample_rate=0.0, always=True)
+ results = asyncio.run(p.evaluate_all("sim", "feito", {
+ "tool_policy_result": {"operation_type": "transactional"},
+ }))
+ assert len(results) == 1
diff --git a/tests/test_mcp_parameter_extraction_runtime.py b/tests/test_mcp_parameter_extraction_runtime.py
new file mode 100644
index 0000000..a70c57e
--- /dev/null
+++ b/tests/test_mcp_parameter_extraction_runtime.py
@@ -0,0 +1,61 @@
+import pytest
+from agent_framework.identity.mcp_mapper import MCPParameterMapper
+from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
+
+
+def test_explicit_order_id_has_precedence_over_contract_key():
+ mapper = MCPParameterMapper({
+ "mcp_parameter_mapping": {
+ "tools": {
+ "consultar_pedido": {
+ "map": {"contract_key": "order_id", "customer_key": "customer_id"},
+ "extract": {"order_id": {"from": "message", "strategy": "llm", "type": "string"}},
+ }
+ }
+ }
+ })
+ mapped = mapper.map(
+ "consultar_pedido",
+ {"contract_key": "3000131180", "customer_key": "11999999999"},
+ extra_args={"order_id": "123"},
+ )
+ assert mapped["order_id"] == "123"
+ assert mapped["customer_id"] == "11999999999"
+ assert "extract" not in mapped
+
+
+class _FakeLLM:
+ async def ainvoke(self, messages, **kwargs):
+ assert "consultar pedido 123" in messages[0]["content"]
+ assert kwargs["generation_name"] == "llm.mcp_parameter_extraction"
+ return {"content": '{"order_id": "123"}'}
+
+
+class _FakeRouter:
+ def parameter_extract_rules(self, tool_name):
+ return {
+ "order_id": {
+ "from": "message",
+ "strategy": "llm",
+ "type": "string",
+ "description": "Extraia o identificador do pedido.",
+ }
+ }
+
+
+class _Runtime(AgentRuntimeMixin):
+ def __init__(self):
+ self.tool_router = _FakeRouter()
+ self.llm = _FakeLLM()
+
+
+@pytest.mark.asyncio
+async def test_runtime_extracts_order_id_from_current_message():
+ runtime = _Runtime()
+ result = await runtime._extract_mcp_parameters(
+ "consultar_pedido",
+ {"contract_key": "3000131180"},
+ {"user_text": "consultar pedido 123", "sanitized_input": "consultar pedido 123"},
+ )
+ assert result["order_id"] == "123"
+ assert result["contract_key"] == "3000131180"
diff --git a/tests/test_performance_optimizations.py b/tests/test_performance_optimizations.py
new file mode 100644
index 0000000..ba9adec
--- /dev/null
+++ b/tests/test_performance_optimizations.py
@@ -0,0 +1,50 @@
+import asyncio
+from types import SimpleNamespace
+from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
+
+class Registry:
+ def __init__(self):
+ self.items={
+ 'consultar_pedido': SimpleNamespace(selection_keywords=['pedido','status do pedido']),
+ 'consultar_entrega': SimpleNamespace(selection_keywords=['entrega','rastreio']),
+ }
+ def get_tool(self,name): return self.items.get(name)
+
+class Router:
+ registry=Registry()
+ def parameter_extract_rules(self, tool):
+ return {'order_id': {'from':'message','type':'string','strategy':'hybrid','pattern':r'(?i)\bpedido\s+([A-Z0-9-]+)\b','group':1}}
+
+class Runtime(AgentRuntimeMixin):
+ tool_router=Router()
+ llm=None
+ settings=SimpleNamespace(SKIP_RAG_WHEN_MCP_SUFFICIENT=True)
+
+
+def test_selects_only_relevant_read_only_tool():
+ r=Runtime()
+ assert r._select_read_only_tools(['consultar_pedido','consultar_entrega'],'consultar pedido 123') == ['consultar_pedido']
+ assert r._select_read_only_tools(['consultar_pedido','consultar_entrega'],'rastreio da entrega 123') == ['consultar_entrega']
+
+
+def test_hybrid_regex_does_not_require_llm():
+ r=Runtime()
+ state={'user_text':'consultar pedido 123','sanitized_input':'consultar pedido 123','context':{},'business_context':{}}
+ out=asyncio.run(r._extract_mcp_parameters('consultar_pedido',{},state))
+ assert out['order_id']=='123'
+
+
+def test_direct_answer_is_blocked_for_transactional_request():
+ runtime = object.__new__(AgentRuntimeMixin)
+ registry = SimpleNamespace(
+ tools={"consultar_pedido": object(), "solicitar_devolucao": object()},
+ get_tool=lambda name: {
+ "consultar_pedido": SimpleNamespace(selection_keywords=["pedido"]),
+ "solicitar_devolucao": SimpleNamespace(selection_keywords=["devolver pedido", "devolver", "devolução"]),
+ }.get(name),
+ )
+ runtime.tool_router = SimpleNamespace(registry=registry)
+ runtime._resolve_tool_execution_policy = lambda name, args=None: {"operation_type": "transactional" if name == "solicitar_devolucao" else "read_only"}
+ state = {"user_text": "Quero devolver o pedido 123"}
+ results = [{"ok": True, "tool_name": "consultar_pedido", "result": {"order_id": "123", "status": "ENTREGUE"}}]
+ assert runtime.build_direct_mcp_answer(state, results, agent_label="OrdersAgent") is None
diff --git a/tests/test_route_stickiness_transaction_shift.py b/tests/test_route_stickiness_transaction_shift.py
new file mode 100644
index 0000000..7a7b93e
--- /dev/null
+++ b/tests/test_route_stickiness_transaction_shift.py
@@ -0,0 +1,14 @@
+from types import SimpleNamespace
+
+from agent_framework.routing.enterprise_router import EnterpriseRouter
+from agent_framework.routing.models import RouteDecision
+
+
+def test_explicit_keyword_shift_preempts_stickiness():
+ d = RouteDecision(route="support_agent", agent="support_agent", intent="retail_support_exchange_return", method="keyword", metadata={"matched_keyword": "devolver pedido"})
+ assert EnterpriseRouter._is_explicit_intent_shift(d) is True
+
+
+def test_short_generic_keyword_does_not_preempt():
+ d = RouteDecision(route="x", agent="x", intent="x", method="keyword", metadata={"matched_keyword": "id"})
+ assert EnterpriseRouter._is_explicit_intent_shift(d) is False
diff --git a/tests/test_transactional_tool_flow.py b/tests/test_transactional_tool_flow.py
new file mode 100644
index 0000000..f292bec
--- /dev/null
+++ b/tests/test_transactional_tool_flow.py
@@ -0,0 +1,90 @@
+from pathlib import Path
+
+from agent_framework.mcp.tool_policy import ToolPolicyRegistry
+
+
+def test_tool_policy_registry_reads_transactional_confirmation(tmp_path: Path):
+ config = tmp_path / "tool_policies.yaml"
+ config.write_text("""version: 1
+defaults:
+ operation_type: read_only
+ require_confirmation: false
+tool_policies:
+ solicitar_devolucao:
+ operation_type: transactional
+ require_confirmation: true
+""", encoding="utf-8")
+ policy = ToolPolicyRegistry(str(config)).get("solicitar_devolucao")
+ assert policy is not None
+ assert policy.operation_type == "transactional"
+ assert policy.require_confirmation is True
+
+
+def test_runtime_source_contains_persisted_confirmation_contract():
+ source = Path("libs/agent_framework/src/agent_framework/runtime/agent_runtime.py").read_text(encoding="utf-8")
+ assert "pending_tool_call" in source
+ assert "AWAITING_CONFIRMATION" in source
+ assert "executed_after_confirmation" in source
+
+import pytest
+from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
+
+
+class _PolicyRouter:
+ def __init__(self):
+ from types import SimpleNamespace
+ self.registry = SimpleNamespace(
+ tools={"consultar_pedido": object(), "solicitar_devolucao": object()},
+ get_tool=lambda name: {
+ "consultar_pedido": SimpleNamespace(selection_keywords=["consultar pedido", "pedido"]),
+ "solicitar_devolucao": SimpleNamespace(selection_keywords=["devolver pedido", "devolver", "devolução", "arrependimento"]),
+ }.get(name),
+ )
+
+ def resolve_execution_policy(self, tool_name, arguments=None):
+ if tool_name == "solicitar_devolucao":
+ return {"operation_type": "transactional", "require_confirmation": True, "policy_source": "test"}
+ return {"operation_type": "read_only", "require_confirmation": False, "policy_source": "test"}
+
+ def validate_execution_policy(self, tool_name, arguments=None):
+ policy = self.resolve_execution_policy(tool_name, arguments)
+ if policy["require_confirmation"] and not (arguments or {}).get("confirmed"):
+ return False, "Tool exige confirmação explícita antes da execução", policy
+ return True, None, policy
+
+
+class _Runtime(AgentRuntimeMixin):
+ def __init__(self):
+ self.tool_router = _PolicyRouter()
+ self.calls = []
+
+ async def _call_mcp_tool(self, tool_name, arguments, state):
+ self.calls.append((tool_name, dict(arguments)))
+ return {"ok": True, "tool_name": tool_name, "result": {"status": "ABERTO"}}
+
+
+@pytest.mark.asyncio
+async def test_transaction_waits_then_executes_after_confirmation():
+ runtime = _Runtime()
+ state = {
+ "user_text": "Quero devolver o pedido 123 porque me arrependi",
+ "sanitized_input": "Quero devolver o pedido 123 porque me arrependi",
+ "mcp_tools": ["consultar_pedido", "solicitar_devolucao"],
+ "route": "support_agent",
+ "intent": "retail_support_exchange_return",
+ }
+ first = await runtime.execute_tools_for_intent(state)
+ assert state["transaction_status"] == "AWAITING_CONFIRMATION"
+ assert state["pending_tool_call"]["tool_name"] == "solicitar_devolucao"
+ assert state["pending_tool_call"]["arguments"]["order_id"] == "123"
+ assert not any(name == "solicitar_devolucao" for name, _ in runtime.calls)
+ assert first[-1]["awaiting_confirmation"] is True
+
+ state["user_text"] = "Sim, confirmo a devolução."
+ state["sanitized_input"] = state["user_text"]
+ second = await runtime.execute_tools_for_intent(state)
+ assert state["transaction_status"] == "COMPLETED"
+ assert state["pending_tool_call"] == {}
+ assert runtime.calls[-1][0] == "solicitar_devolucao"
+ assert runtime.calls[-1][1]["confirmed"] is True
+ assert second[-1]["ok"] is True
diff --git a/tests/unit/test_semantic_route_stickiness.py b/tests/unit/test_semantic_route_stickiness.py
new file mode 100644
index 0000000..2613583
--- /dev/null
+++ b/tests/unit/test_semantic_route_stickiness.py
@@ -0,0 +1,204 @@
+from __future__ import annotations
+
+import json
+from types import SimpleNamespace
+
+import pytest
+
+from agent_framework.routing.continuity import SemanticRouteContinuity
+from agent_framework.routing.models import IntentDefinition
+
+
+class FakeLLM:
+ def __init__(self, response: dict | str):
+ self.response = response
+ self.calls = []
+
+ async def ainvoke(self, messages, **kwargs):
+ self.calls.append((messages, kwargs))
+ if isinstance(self.response, str):
+ return self.response
+ return json.dumps(self.response)
+
+
+class FakeTelemetry:
+ def __init__(self):
+ self.events = []
+
+ async def event(self, name, payload):
+ self.events.append((name, payload))
+
+
+def settings(**overrides):
+ values = {
+ "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,
+ }
+ values.update(overrides)
+ return SimpleNamespace(**values)
+
+
+def intents():
+ return [
+ IntentDefinition(
+ name="product_services_information",
+ agent="product_agent",
+ description="Planos, serviços, benefícios e mudança de plano.",
+ ),
+ IntentDefinition(
+ name="billing_invoice_explanation",
+ agent="billing_agent",
+ description="Faturas, pagamentos, cobranças e contestação.",
+ ),
+ ]
+
+
+def state(message="o que está incluso?"):
+ return {
+ "session_id": "s1",
+ "active_agent": "product_agent",
+ "intent": "product_services_information",
+ "domain": "telecom",
+ "route_decision": {
+ "intent": "product_services_information",
+ "domain": "telecom",
+ "mcp_tools": ["consultar_plano"],
+ },
+ "history": [
+ {"role": "user", "content": "qual é o meu plano?"},
+ {"role": "assistant", "content": "Seu plano atual é Controle 50GB."},
+ ],
+ "user_text": message,
+ "sanitized_input": message,
+ }
+
+
+@pytest.mark.asyncio
+async def test_continue_bypasses_router_without_regex_rules():
+ llm = FakeLLM({"decision": "CONTINUE", "confidence": 0.97, "reason": "Continua o assunto do plano."})
+ telemetry = FakeTelemetry()
+ policy = SemanticRouteContinuity(settings(), llm, telemetry)
+
+ decision = await policy.evaluate(state(), intents=intents())
+
+ assert decision is not None
+ assert decision.agent == "product_agent"
+ assert decision.method == "continuity"
+ assert decision.metadata["route_bypassed"] is True
+ assert llm.calls[0][1]["profile_name"] == "route_continuity"
+ prompt = json.loads(llm.calls[0][0][1]["content"])
+ assert prompt["current_message"] == "o que está incluso?"
+ assert "product_agent" not in prompt["other_agents"]
+ assert telemetry.events[-1][1]["route_bypassed"] is True
+
+
+@pytest.mark.asyncio
+async def test_route_result_falls_back_to_enterprise_router():
+ llm = FakeLLM({"decision": "ROUTE", "confidence": 0.98, "reason": "Novo assunto de cobrança."})
+ policy = SemanticRouteContinuity(settings(), llm)
+
+ decision = await policy.evaluate(
+ state("agora quero contestar uma cobrança"), intents=intents()
+ )
+
+ assert decision is None
+
+
+@pytest.mark.asyncio
+async def test_low_confidence_continue_falls_back_safely():
+ llm = FakeLLM({"decision": "CONTINUE", "confidence": 0.70, "reason": "Possível continuidade."})
+ policy = SemanticRouteContinuity(settings(), llm)
+
+ assert await policy.evaluate(state(), intents=intents()) is None
+
+
+@pytest.mark.asyncio
+async def test_invalid_output_falls_back_safely():
+ llm = FakeLLM("not-json")
+ policy = SemanticRouteContinuity(settings(), llm)
+
+ assert await policy.evaluate(state(), intents=intents()) is None
+
+
+@pytest.mark.asyncio
+async def test_no_active_agent_still_classifies_global_session_actions():
+ llm = FakeLLM({"decision": "ROUTE", "confidence": 1.0})
+ policy = SemanticRouteContinuity(settings(), llm)
+ current = state()
+ current.pop("active_agent")
+
+ assert await policy.evaluate(current, intents=intents()) is None
+ assert len(llm.calls) == 1
+
+@pytest.mark.asyncio
+async def test_human_handoff_is_returned_as_global_route():
+ llm = FakeLLM({
+ "decision": "HUMAN_HANDOFF",
+ "confidence": 0.99,
+ "reason": "O usuário pediu atendimento humano.",
+ })
+ policy = SemanticRouteContinuity(settings(), llm)
+
+ decision = await policy.evaluate(
+ state("quero falar com um atendente"), intents=intents()
+ )
+
+ assert decision is not None
+ assert decision.route == "human_handoff"
+ assert decision.agent == "human_handoff"
+ assert decision.intent == "human_handoff"
+ assert decision.handoff is True
+ assert decision.metadata["session_control"] == "HUMAN_HANDOFF"
+ assert decision.metadata["route_bypassed"] is True
+
+
+@pytest.mark.asyncio
+async def test_end_session_is_returned_as_global_route():
+ llm = FakeLLM({
+ "decision": "END_SESSION",
+ "confidence": 0.98,
+ "reason": "O usuário informou que não precisa continuar.",
+ })
+ policy = SemanticRouteContinuity(settings(), llm)
+
+ decision = await policy.evaluate(state("obrigado, era só isso"), intents=intents())
+
+ assert decision is not None
+ assert decision.route == "end_session"
+ assert decision.agent == "end_session"
+ assert decision.intent == "end_session"
+ assert decision.handoff is False
+ assert decision.metadata["session_control"] == "END_SESSION"
+ assert decision.metadata["route_bypassed"] is True
+
+
+@pytest.mark.asyncio
+async def test_global_session_actions_work_without_active_agent():
+ llm = FakeLLM({
+ "decision": "HUMAN_HANDOFF",
+ "confidence": 0.97,
+ "reason": "Solicitação explícita de pessoa.",
+ })
+ policy = SemanticRouteContinuity(settings(), llm)
+ current = state("quero uma pessoa")
+ current.pop("active_agent")
+
+ decision = await policy.evaluate(current, intents=intents())
+
+ assert decision is not None
+ assert decision.route == "human_handoff"
+ assert len(llm.calls) == 1
+
+
+@pytest.mark.asyncio
+async def test_continue_without_active_agent_falls_back_to_router():
+ llm = FakeLLM({"decision": "CONTINUE", "confidence": 0.99})
+ policy = SemanticRouteContinuity(settings(), llm)
+ current = state()
+ current.pop("active_agent")
+
+ assert await policy.evaluate(current, intents=intents()) is None
+ assert len(llm.calls) == 1
diff --git a/tests/unit/test_tool_policies.py b/tests/unit/test_tool_policies.py
new file mode 100644
index 0000000..8080cdf
--- /dev/null
+++ b/tests/unit/test_tool_policies.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+from agent_framework.mcp.tool_policy import ToolPolicyRegistry
+from agent_framework.mcp.tool_router import MCPToolRouter
+
+
+class _Registry:
+ def __init__(self, tool=None):
+ self.tool = tool
+
+ def get_tool(self, _name):
+ return self.tool
+
+
+def _router(policy_registry, legacy=None):
+ router = MCPToolRouter.__new__(MCPToolRouter)
+ router.tool_policies = policy_registry
+ router.registry = _Registry(legacy)
+ return router
+
+
+def test_missing_policy_file_preserves_legacy_behavior(tmp_path):
+ policies = ToolPolicyRegistry(str(tmp_path / "missing.yaml"))
+ legacy = SimpleNamespace(
+ tool_type="action",
+ requires=["order_id"],
+ confirmation_required=True,
+ execution_policy={},
+ )
+ router = _router(policies, legacy)
+
+ allowed, reason, metadata = router.validate_execution_policy("alterar", {"order_id": "42"})
+
+ assert allowed is False
+ assert "confirmação" in reason
+ assert metadata["operation_type"] == "transactional"
+ assert metadata["policy_source"] == "tools.yaml"
+
+
+def test_read_only_policy_executes_without_confirmation(tmp_path):
+ path = tmp_path / "tool_policies.yaml"
+ path.write_text(
+ "tool_policies:\n consultar:\n operation_type: read_only\n",
+ encoding="utf-8",
+ )
+ router = _router(ToolPolicyRegistry(str(path)))
+
+ allowed, reason, metadata = router.validate_execution_policy("consultar", {})
+
+ assert allowed is True
+ assert reason is None
+ assert metadata["operation_type"] == "read_only"
+
+
+def test_transactional_policy_requires_literal_boolean_confirmation(tmp_path):
+ path = tmp_path / "tool_policies.yaml"
+ path.write_text(
+ "tool_policies:\n cancelar:\n operation_type: transactional\n require_confirmation: true\n",
+ encoding="utf-8",
+ )
+ router = _router(ToolPolicyRegistry(str(path)))
+
+ denied, _, _ = router.validate_execution_policy("cancelar", {"confirmed": "true"})
+ allowed, reason, metadata = router.validate_execution_policy("cancelar", {"confirmed": True})
+
+ assert denied is False
+ assert allowed is True
+ assert reason is None
+ assert metadata["policy_source"] == "tool_policies.yaml"
+
+
+def test_requires_confirmation_alias_is_supported(tmp_path):
+ path = tmp_path / "tool_policies.yaml"
+ path.write_text(
+ "tool_policies:\n alterar:\n type: transactional\n requires_confirmation: true\n",
+ encoding="utf-8",
+ )
+
+ policy = ToolPolicyRegistry(str(path)).get("alterar")
+
+ assert policy.operation_type == "transactional"
+ assert policy.require_confirmation is True