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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
137
templates/agent_template_backend/app/agents/billing_agent.py
Normal file
137
templates/agent_template_backend/app/agents/billing_agent.py
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
"rag_context": rag_context,
|
||||
"rag_results": [{
|
||||
"intent": state.get("intent"),
|
||||
"agent": self.name,
|
||||
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||
"metadata": rag_metadata,
|
||||
**({"context": rag_context} if rag_context else {}),
|
||||
}],
|
||||
"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)
|
||||
137
templates/agent_template_backend/app/agents/orders_agent.py
Normal file
137
templates/agent_template_backend/app/agents/orders_agent.py
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
"rag_context": rag_context,
|
||||
"rag_results": [{
|
||||
"intent": state.get("intent"),
|
||||
"agent": self.name,
|
||||
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||
"metadata": rag_metadata,
|
||||
**({"context": rag_context} if rag_context else {}),
|
||||
}],
|
||||
"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)
|
||||
152
templates/agent_template_backend/app/agents/product_agent.py
Normal file
152
templates/agent_template_backend/app/agents/product_agent.py
Normal file
@@ -0,0 +1,152 @@
|
||||
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)
|
||||
rag_event_payload = {
|
||||
"provider": rag_metadata.get("provider"),
|
||||
"status": rag_metadata.get("status"),
|
||||
"attempted": rag_metadata.get("attempted"),
|
||||
"enabled": rag_metadata.get("enabled"),
|
||||
"document_count": rag_metadata.get("document_count"),
|
||||
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
||||
"latency_ms": rag_metadata.get("latency_ms"),
|
||||
"reason": rag_metadata.get("reason"),
|
||||
"error": rag_metadata.get("error"),
|
||||
"query": rag_metadata.get("query"),
|
||||
"namespace": rag_metadata.get("namespace"),
|
||||
}
|
||||
await self._emit_ic(
|
||||
"IC.PRODUCT_RAG_CONTEXT_EVALUATED",
|
||||
state,
|
||||
rag_event_payload,
|
||||
component="agent.product.rag",
|
||||
)
|
||||
if rag_metadata.get("enabled") and rag_metadata.get("status") == "executed":
|
||||
await self._emit_ic(
|
||||
"IC.PRODUCT_RAG_CONTEXT_RETRIEVED",
|
||||
state,
|
||||
rag_event_payload,
|
||||
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,
|
||||
"rag_context": rag_context,
|
||||
"rag_results": [{
|
||||
"intent": state.get("intent"),
|
||||
"agent": self.name,
|
||||
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||
"metadata": rag_metadata,
|
||||
**({"context": rag_context} if rag_context else {}),
|
||||
}],
|
||||
"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)
|
||||
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}"
|
||||
10
templates/agent_template_backend/app/agents/runtime.py
Normal file
10
templates/agent_template_backend/app/agents/runtime.py
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
from app.presentation import register_tool_renderers
|
||||
|
||||
register_tool_renderers()
|
||||
|
||||
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||
137
templates/agent_template_backend/app/agents/support_agent.py
Normal file
137
templates/agent_template_backend/app/agents/support_agent.py
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
"rag_context": rag_context,
|
||||
"rag_results": [{
|
||||
"intent": state.get("intent"),
|
||||
"agent": self.name,
|
||||
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||
"metadata": rag_metadata,
|
||||
**({"context": rag_context} if rag_context else {}),
|
||||
}],
|
||||
"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)
|
||||
@@ -0,0 +1 @@
|
||||
"""Exemplos de uso do template backend enterprise."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
)
|
||||
672
templates/agent_template_backend/app/main.py
Normal file
672
templates/agent_template_backend/app/main.py
Normal file
@@ -0,0 +1,672 @@
|
||||
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.channels.interruption import classify_processing_interruption, evaluate_interruption
|
||||
from agent_framework.channels.transcription import fix_whole_utterance_transcription
|
||||
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 _emit_gateway_event(session_id, event, payload, emit_sse, progress):
|
||||
if emit_sse:
|
||||
await sse_hub.emit(session_id, event, payload)
|
||||
if progress is not None:
|
||||
await progress(event, payload)
|
||||
|
||||
|
||||
async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False, progress=None) -> 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 _emit_gateway_event(agent_session_id, "flow.start", {"session_id": agent_session_id, "message_id": message_id, "agent_id": identity.agent_id}, emit_sse, progress)
|
||||
|
||||
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 sessions.upsert(session)
|
||||
|
||||
fixed_message_text = fix_whole_utterance_transcription(msg.text)
|
||||
if fixed_message_text != msg.text:
|
||||
await telemetry.event(
|
||||
"channel.transcription.fixed",
|
||||
{
|
||||
"session_id": agent_session_id,
|
||||
"original_text": msg.text,
|
||||
"fixed_text": fixed_message_text,
|
||||
},
|
||||
)
|
||||
|
||||
interruption = evaluate_interruption(
|
||||
payload=payload,
|
||||
message_text=fixed_message_text,
|
||||
session_metadata=session.metadata,
|
||||
terminal_fallback_text=getattr(settings, "POST_FINALIZE_REPLAY_MESSAGE", ""),
|
||||
)
|
||||
if interruption.action == "classify":
|
||||
prior_history = await memory.list(agent_session_id)
|
||||
prior_user_text = ""
|
||||
for prior in reversed(prior_history):
|
||||
role = getattr(prior, "role", None)
|
||||
content = getattr(prior, "content", "")
|
||||
if str(role or "") == "user" and str(content or "").strip():
|
||||
prior_user_text = str(content).strip()
|
||||
break
|
||||
regenerate = await classify_processing_interruption(
|
||||
llm,
|
||||
original_agent=interruption.replay_text,
|
||||
original_client=prior_user_text,
|
||||
supplement_client=interruption.text,
|
||||
)
|
||||
await telemetry.event(
|
||||
"channel.processing_interruption.classified",
|
||||
{
|
||||
"session_id": agent_session_id,
|
||||
"regenerate": regenerate,
|
||||
"profile_name": "processing_interruption_classifier",
|
||||
},
|
||||
)
|
||||
if regenerate:
|
||||
interruption.action = "process"
|
||||
interruption.reason = "classifier_result_1"
|
||||
else:
|
||||
interruption.action = "replay"
|
||||
interruption.reason = "classifier_result_0"
|
||||
|
||||
if interruption.action == "replay":
|
||||
response = ChannelResponse(
|
||||
channel=msg.channel,
|
||||
session_id=agent_session_id,
|
||||
text=interruption.replay_text,
|
||||
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,
|
||||
"replay": True,
|
||||
"replay_reason": interruption.reason,
|
||||
"is_interruptible": interruption.is_interruptible,
|
||||
"framework_short_circuit": True,
|
||||
"terminal_status": interruption.terminal_status,
|
||||
"llm_called": False,
|
||||
"tools_called": False,
|
||||
"guardrails_called": False,
|
||||
},
|
||||
)
|
||||
rendered = await gateway.render(response)
|
||||
await telemetry.event("gateway.message.replayed", {"session_id": agent_session_id, "reason": interruption.reason})
|
||||
await _emit_gateway_event(agent_session_id, "message.responded", rendered, emit_sse, progress)
|
||||
return rendered
|
||||
|
||||
effective_text = interruption.text
|
||||
await _emit_gateway_event(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}, emit_sse, progress)
|
||||
|
||||
await memory.append(
|
||||
agent_session_id,
|
||||
ChatMessage(
|
||||
role="user",
|
||||
content=effective_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 _emit_gateway_event(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}, emit_sse, progress)
|
||||
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": effective_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 _emit_gateway_event(agent_session_id, "workflow.started", trace_context, emit_sse, progress)
|
||||
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": effective_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 _emit_gateway_event(agent_session_id, "workflow.completed", {"session_id": agent_session_id, "route": result.get("route"), "intent": result.get("intent")}, emit_sse, progress)
|
||||
|
||||
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"),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
terminal_status = str(result.get("terminal_status") or "").strip()
|
||||
session_ended = bool(result.get("session_ended")) or bool(terminal_status)
|
||||
session.metadata = {
|
||||
**(session.metadata or {}),
|
||||
"last_assistant_text": answer,
|
||||
"last_assistant_is_interruptible": bool(result.get("is_interruptible", True)),
|
||||
"last_route": result.get("route"),
|
||||
"last_intent": result.get("intent"),
|
||||
"conversation_closed": session_ended,
|
||||
"terminal_status": terminal_status or ("resolvido" if session_ended else ""),
|
||||
"terminal_replay_text": answer if session_ended else "",
|
||||
}
|
||||
await sessions.upsert(session)
|
||||
|
||||
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"),
|
||||
"supervisor_results": result.get("supervisor_results") or [],
|
||||
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
|
||||
"domain": result.get("domain"),
|
||||
"mcp_tools": result.get("mcp_tools"),
|
||||
"mcp_results": result.get("mcp_results"),
|
||||
"rag_results": result.get("rag_results"),
|
||||
"transaction_evidence": result.get("relevant_transaction_evidence", []),
|
||||
"transaction_pre_validation": result.get("transaction_pre_validation"),
|
||||
"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 _emit_gateway_event(agent_session_id, "message.responded", rendered, emit_sse, progress)
|
||||
await _emit_gateway_event(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}, emit_sse, progress)
|
||||
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": effective_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()
|
||||
|
||||
|
||||
# Optional direct A2A endpoint for agents built with the framework template.
|
||||
if settings.ENABLE_A2A:
|
||||
from agent_framework.a2a.progress import report_progress
|
||||
from agent_framework.a2a.server import install_backend_a2a
|
||||
from agent_framework.security import install_authentication
|
||||
|
||||
install_authentication(app, prefix="AGENT_AUTH")
|
||||
|
||||
async def _a2a_backend_handler(data: dict):
|
||||
return await _process_gateway_message(GatewayRequest(**data), progress=report_progress)
|
||||
|
||||
install_backend_a2a(app, _a2a_backend_handler, agent_profiles, settings)
|
||||
@@ -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")),
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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})
|
||||
@@ -0,0 +1,3 @@
|
||||
from .tool_renderers import register_tool_renderers
|
||||
|
||||
__all__ = ["register_tool_renderers"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent_framework.presentation import register_tool_response_renderer
|
||||
|
||||
|
||||
def _money_brl(value: Any) -> str:
|
||||
try:
|
||||
return f"{float(value):.2f}".replace(".", ",")
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
|
||||
|
||||
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
return f"[{agent_label}] Fatura consultada: {result}."
|
||||
|
||||
|
||||
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
plano = result.get("plano")
|
||||
if plano is None:
|
||||
return None
|
||||
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||
internet_gb = result.get("internet_gb")
|
||||
status = result.get("status")
|
||||
if internet_gb is not None:
|
||||
parts.append(f"com {internet_gb} GB")
|
||||
if status is not None:
|
||||
parts.append(f"status {status}")
|
||||
return ", ".join(parts) + "."
|
||||
|
||||
|
||||
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
order_id = result.get("order_id")
|
||||
status = result.get("status")
|
||||
if order_id is None or status is None:
|
||||
return None
|
||||
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||
total = result.get("valor_total")
|
||||
if total is not None:
|
||||
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||
items = result.get("itens") or []
|
||||
rendered_items: list[str] = []
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||
else:
|
||||
value = item
|
||||
if value not in (None, ""):
|
||||
rendered_items.append(str(value))
|
||||
if rendered_items:
|
||||
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||
return " ".join(lines)
|
||||
|
||||
|
||||
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
order_id = result.get("order_id")
|
||||
transportadora = result.get("transportadora")
|
||||
codigo = result.get("codigo_rastreio")
|
||||
previsao = result.get("previsao_entrega")
|
||||
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||
return None
|
||||
return (
|
||||
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||
f"rastreio {codigo}, previsão {previsao}."
|
||||
)
|
||||
|
||||
|
||||
def register_tool_renderers() -> None:
|
||||
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||
register_tool_response_renderer("retail.order", render_retail_order)
|
||||
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||
65
templates/agent_template_backend/app/state.py
Normal file
65
templates/agent_template_backend/app/state.py
Normal file
@@ -0,0 +1,65 @@
|
||||
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]]
|
||||
rag_results: list[dict[str, Any]]
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
transaction_pre_validation: dict[str, Any]
|
||||
transaction_evidence: list[dict[str, Any]]
|
||||
last_transaction_evidence: dict[str, Any]
|
||||
relevant_transaction_evidence: list[dict[str, Any]]
|
||||
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]]
|
||||
agent_responses: list[dict[str, Any]]
|
||||
pending_topics: list[dict[str, Any]]
|
||||
handled_topics: list[dict[str, Any]]
|
||||
operation_results: dict[str, dict[str, Any]]
|
||||
multi_intent_plan: 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
|
||||
@@ -0,0 +1 @@
|
||||
from . import devolucao # noqa: F401
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
"""Actions de domínio permanecem no agente; o runtime está no framework."""
|
||||
from agent_framework.workflows import workflow_action
|
||||
|
||||
|
||||
@workflow_action("validar_pedido")
|
||||
async def validar_pedido(params: dict, state: dict) -> dict:
|
||||
return {"valid": bool(params.get("order_id"))}
|
||||
|
||||
|
||||
@workflow_action("registrar_devolucao")
|
||||
async def registrar_devolucao(params: dict, state: dict) -> dict:
|
||||
# Substitua pela chamada real ao serviço/MCP e use chave idempotente.
|
||||
return {"protocol": f"DEV-{params['order_id']}", "status": "REQUESTED"}
|
||||
Binary file not shown.
Binary file not shown.
1017
templates/agent_template_backend/app/workflows/agent_graph.py
Normal file
1017
templates/agent_template_backend/app/workflows/agent_graph.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user