mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-07-09 14:04:19 +00:00
first commit
This commit is contained in:
0
templates/agent_template_backend/app/__init__.py
Normal file
0
templates/agent_template_backend/app/__init__.py
Normal file
15
templates/agent_template_backend/app/agents/README.md
Normal file
15
templates/agent_template_backend/app/agents/README.md
Normal file
@@ -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.
|
||||
98
templates/agent_template_backend/app/agents/billing_agent.py
Normal file
98
templates/agent_template_backend/app/agents/billing_agent.py
Normal file
@@ -0,0 +1,98 @@
|
||||
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",
|
||||
)
|
||||
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
98
templates/agent_template_backend/app/agents/orders_agent.py
Normal file
98
templates/agent_template_backend/app/agents/orders_agent.py
Normal file
@@ -0,0 +1,98 @@
|
||||
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",
|
||||
)
|
||||
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
98
templates/agent_template_backend/app/agents/product_agent.py
Normal file
98
templates/agent_template_backend/app/agents/product_agent.py
Normal file
@@ -0,0 +1,98 @@
|
||||
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",
|
||||
)
|
||||
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
15
templates/agent_template_backend/app/agents/prompting.py
Normal file
15
templates/agent_template_backend/app/agents/prompting.py
Normal file
@@ -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}"
|
||||
7
templates/agent_template_backend/app/agents/runtime.py
Normal file
7
templates/agent_template_backend/app/agents/runtime.py
Normal file
@@ -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"]
|
||||
98
templates/agent_template_backend/app/agents/support_agent.py
Normal file
98
templates/agent_template_backend/app/agents/support_agent.py
Normal file
@@ -0,0 +1,98 @@
|
||||
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",
|
||||
)
|
||||
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
"""Exemplos de uso do template backend enterprise."""
|
||||
@@ -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",
|
||||
)
|
||||
34
templates/agent_template_backend/app/examples/ic_examples.py
Normal file
34
templates/agent_template_backend/app/examples/ic_examples.py
Normal file
@@ -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",
|
||||
)
|
||||
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
476
templates/agent_template_backend/app/main.py
Normal file
476
templates/agent_template_backend/app/main.py
Normal file
@@ -0,0 +1,476 @@
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
class GatewayRequest(BaseModel):
|
||||
channel: str = "web"
|
||||
payload: dict
|
||||
agent_id: str | None = None
|
||||
tenant_id: str | None = None
|
||||
|
||||
|
||||
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
|
||||
identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg)
|
||||
agent_session_id = identity.conversation_key()
|
||||
message_id = (req.payload or {}).get("message_id") or str(uuid4())
|
||||
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,
|
||||
ura_call_id=(req.payload or {}).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)]
|
||||
|
||||
trace_input = {
|
||||
"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,
|
||||
"message_id": message_id,
|
||||
"business_context": business_context.model_dump(),
|
||||
"identity_missing": missing_identity_keys,
|
||||
}
|
||||
|
||||
async with telemetry.span(
|
||||
"agent.gateway_message",
|
||||
session_id=agent_session_id,
|
||||
user_id=session.user_id,
|
||||
channel=msg.channel,
|
||||
input=trace_input,
|
||||
tags=["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"],
|
||||
):
|
||||
await telemetry.event("gateway.message.received", trace_input)
|
||||
await sse_hub.emit(agent_session_id, "workflow.started", trace_input) 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,
|
||||
"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,
|
||||
"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,
|
||||
"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)
|
||||
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()
|
||||
@@ -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})
|
||||
34
templates/agent_template_backend/app/state.py
Normal file
34
templates/agent_template_backend/app/state.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from typing import Any, TypedDict
|
||||
|
||||
|
||||
class AgentState(TypedDict, total=False):
|
||||
tenant_id: str
|
||||
agent_id: str
|
||||
session_id: str
|
||||
conversation_key: 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]]
|
||||
supervisor_plan: dict[str, Any]
|
||||
supervisor_results: list[dict[str, Any]]
|
||||
active_agent: str
|
||||
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
|
||||
705
templates/agent_template_backend/app/workflows/agent_graph.py
Normal file
705
templates/agent_template_backend/app/workflows/agent_graph.py
Normal file
@@ -0,0 +1,705 @@
|
||||
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
|
||||
|
||||
|
||||
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.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)
|
||||
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("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", 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",
|
||||
"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("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")
|
||||
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):
|
||||
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,
|
||||
}
|
||||
|
||||
async def billing_agent(self, state):
|
||||
async with self.langgraph_telemetry.node("billing_agent", 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.langgraph_telemetry.node("product_agent", 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.langgraph_telemetry.node("orders_agent", 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.langgraph_telemetry.node("support_agent", 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 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")},
|
||||
):
|
||||
results = await self.judges.evaluate_all(
|
||||
state["user_text"], state["final_answer"], state.get("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(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
|
||||
Reference in New Issue
Block a user