mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 22:04:20 +00:00
first commit
This commit is contained in:
15
app/agents/README.md
Normal file
15
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.
|
||||
BIN
app/agents/__pycache__/backoffice_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/backoffice_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/billing_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/billing_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/orders_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/orders_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/product_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/product_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/prompting.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/prompting.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/runtime.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/runtime.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/support_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/support_agent.cpython-313.pyc
Normal file
Binary file not shown.
421
app/agents/backoffice_agent.py
Normal file
421
app/agents/backoffice_agent.py
Normal file
@@ -0,0 +1,421 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.prompting import apply_agent_profile_prompt
|
||||
from app.identity_extraction import extract_identity_from_text
|
||||
from app.agents.runtime import AgentRuntimeMixin
|
||||
|
||||
|
||||
class BackofficeAgent(AgentRuntimeMixin):
|
||||
"""Agente Backoffice/ANATEL usando somente contratos nativos do framework.
|
||||
|
||||
Este agente não contém grafo próprio, node legado ou orquestração paralela ao
|
||||
framework. A execução segue o padrão uniforme:
|
||||
|
||||
1. LangGraph do framework faz guardrails de entrada, roteamento e checkpoint.
|
||||
2. EnterpriseRouter injeta intent, route e mcp_tools a partir de YAML.
|
||||
3. AgentRuntimeMixin executa MCP Tool Router, RAG, cache, IC/NOC/GRL.
|
||||
4. LLM recebe contexto já normalizado e gera a resposta.
|
||||
5. Workflow do framework aplica OutputSupervisor, output guardrails, judges e persistência.
|
||||
|
||||
Regras específicas do domínio ficam em prompt_policy/routing/tools/mcp mapping,
|
||||
não em um fluxo particular dentro do agente.
|
||||
"""
|
||||
|
||||
name = "backoffice_agent"
|
||||
_domain_graph_cache: dict[str, Any] = {}
|
||||
|
||||
|
||||
DEFAULT_TOOLS_BY_INTENT: dict[str, list[str]] = {
|
||||
"backoffice_anatel_triage": [
|
||||
"consultar_reclamacao",
|
||||
"consultar_cliente_backoffice",
|
||||
"consultar_siebel_caso",
|
||||
"consultar_imdb_cliente",
|
||||
"consultar_speech_analytics",
|
||||
"consultar_tais_kb",
|
||||
"consultar_abrt",
|
||||
"consultar_portabilidade",
|
||||
],
|
||||
"backoffice_customer_lookup": [
|
||||
"consultar_cliente_backoffice",
|
||||
"consultar_imdb_cliente",
|
||||
"consultar_portabilidade",
|
||||
],
|
||||
"backoffice_action_register": [
|
||||
"consultar_reclamacao",
|
||||
"consultar_siebel_caso",
|
||||
"registrar_acao_backoffice",
|
||||
"registrar_acao_siebel",
|
||||
],
|
||||
"backoffice_response_emulator": [
|
||||
"consultar_reclamacao",
|
||||
"buscar_templates_emulador",
|
||||
"gerar_rascunho_emulador",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None):
|
||||
self.llm = llm
|
||||
self.telemetry = telemetry
|
||||
self.tool_router = tool_router
|
||||
self.rag_service = rag_service
|
||||
self.cache = cache
|
||||
self.settings = settings
|
||||
self.observer = observer
|
||||
|
||||
async def run(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
await self._emit_ic(
|
||||
"AGA.001",
|
||||
state,
|
||||
{
|
||||
"status": "Entrada recebida pelo agente nativo do framework",
|
||||
"framework_native": True,
|
||||
"business_component": "backoffice_anatel",
|
||||
},
|
||||
component="agent.backoffice.native.start",
|
||||
)
|
||||
|
||||
normalized_state = self._normalize_state_tools(state)
|
||||
await self._emit_ic(
|
||||
"AGA.018",
|
||||
normalized_state,
|
||||
{
|
||||
"status": "Contexto canônico validado pelo framework",
|
||||
"missing_fields": self._missing_required_context(normalized_state),
|
||||
"framework_native": True,
|
||||
},
|
||||
component="agent.backoffice.native.context",
|
||||
)
|
||||
|
||||
if self._has_original_ticket_context(normalized_state):
|
||||
await self._emit_ic(
|
||||
"BACKOFFICE_TICKET_CONTEXT_DETECTED",
|
||||
normalized_state,
|
||||
{
|
||||
"status": "Contexto de ticket detectado; execução checklist deve ocorrer pelo BackofficeNativeRuntime nas rotas /agent/*",
|
||||
"framework_native": True,
|
||||
"reason": "o agente conversacional não compila nem executa grafos de domínio",
|
||||
},
|
||||
component="agent.backoffice.native.ticket_context",
|
||||
)
|
||||
|
||||
rag_context, rag_metadata = await self._retrieve_rag_context(normalized_state)
|
||||
if rag_metadata.get("enabled"):
|
||||
await self._emit_ic(
|
||||
"AGA.012",
|
||||
normalized_state,
|
||||
{
|
||||
"status": "RAG/TAIS KB consultado pelo serviço nativo do framework",
|
||||
"document_count": rag_metadata.get("document_count", 0),
|
||||
"top_document_ids": rag_metadata.get("top_document_ids", []),
|
||||
},
|
||||
component="agent.backoffice.native.rag",
|
||||
)
|
||||
|
||||
mcp_results = await self._execute_tools_by_intent(normalized_state)
|
||||
await self._emit_ic(
|
||||
"AGA.014",
|
||||
normalized_state,
|
||||
{
|
||||
"status": "Ferramentas MCP selecionadas pelo roteador do framework",
|
||||
"intent": normalized_state.get("intent"),
|
||||
"tool_count": len(mcp_results),
|
||||
"tools": [r.get("tool_name") or r.get("tool") for r in mcp_results],
|
||||
},
|
||||
component="agent.backoffice.native.tools",
|
||||
)
|
||||
|
||||
answer = await self._generate_answer(normalized_state, rag_context, rag_metadata, mcp_results)
|
||||
|
||||
await self._emit_ic(
|
||||
"AGA.043",
|
||||
normalized_state,
|
||||
{
|
||||
"status": "Resposta produzida pelo agente nativo e entregue ao workflow do framework",
|
||||
"answer_chars": len(answer or ""),
|
||||
"framework_native": True,
|
||||
},
|
||||
component="agent.backoffice.native.completed",
|
||||
)
|
||||
await self._emit_noc(
|
||||
"001",
|
||||
normalized_state,
|
||||
{"message": "Backoffice native agent completed", "type": "INFO"},
|
||||
component="agent.backoffice.native.completed",
|
||||
)
|
||||
|
||||
return {
|
||||
"answer": answer,
|
||||
"next_state": self._next_state(normalized_state, mcp_results),
|
||||
"mcp_results": mcp_results,
|
||||
"rag_metadata": rag_metadata,
|
||||
"framework_native": {
|
||||
"agent": self.name,
|
||||
"intent": normalized_state.get("intent"),
|
||||
"route": normalized_state.get("route"),
|
||||
"tools": normalized_state.get("mcp_tools") or [],
|
||||
},
|
||||
}
|
||||
|
||||
def _normalize_state_tools(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
intent = state.get("intent") or "backoffice_anatel_triage"
|
||||
configured_tools = list(state.get("mcp_tools") or [])
|
||||
default_tools = self.DEFAULT_TOOLS_BY_INTENT.get(intent, self.DEFAULT_TOOLS_BY_INTENT["backoffice_anatel_triage"])
|
||||
tools = configured_tools or default_tools
|
||||
|
||||
# Garante ordem estável e remove duplicidade sem perder a configuração do roteador.
|
||||
seen: set[str] = set()
|
||||
deduped = []
|
||||
for tool in tools:
|
||||
if tool and tool not in seen:
|
||||
seen.add(tool)
|
||||
deduped.append(tool)
|
||||
|
||||
return {
|
||||
**state,
|
||||
"route": state.get("route") or self.name,
|
||||
"active_agent": self.name,
|
||||
"intent": intent,
|
||||
"mcp_tools": deduped,
|
||||
}
|
||||
|
||||
def _missing_required_context(self, state: dict[str, Any]) -> list[str]:
|
||||
ctx = state.get("context") or {}
|
||||
bc = ctx.get("business_context") or state.get("business_context") or {}
|
||||
interaction = (bc.get("interaction_key") if isinstance(bc, dict) else None) or ctx.get("interaction_key")
|
||||
if not interaction:
|
||||
interaction = (
|
||||
ctx.get("protocol_id")
|
||||
or ctx.get("protocolo")
|
||||
or ctx.get("complaint_id")
|
||||
or ctx.get("complaintProtocol")
|
||||
or ctx.get("transactionId")
|
||||
or ctx.get("transaction_id")
|
||||
or ctx.get("ticket_id")
|
||||
)
|
||||
return [] if interaction else ["interaction_key"]
|
||||
|
||||
async def _execute_tools_by_intent(self, state: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
intent = state.get("intent") or "backoffice_anatel_triage"
|
||||
tools = list(state.get("mcp_tools") or [])
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
for tool in tools:
|
||||
arguments = self._tool_arguments(tool, intent, state)
|
||||
if tool.startswith("registrar_") and not arguments.get("action_text"):
|
||||
# O framework não deve registrar ação operacional sem texto explícito.
|
||||
results.append({
|
||||
"ok": False,
|
||||
"tool_name": tool,
|
||||
"skipped": True,
|
||||
"reason": "action_text ausente; registro não executado por segurança operacional",
|
||||
})
|
||||
await self._emit_ic(
|
||||
"AGA.008",
|
||||
state,
|
||||
{"status": "Registro operacional não executado sem action_text", "tool_name": tool},
|
||||
component="agent.backoffice.native.tool.skip",
|
||||
)
|
||||
continue
|
||||
|
||||
result = await self._call_mcp_tool(tool, arguments, state)
|
||||
results.append(result)
|
||||
if tool in {"consultar_speech_analytics"}:
|
||||
await self._emit_ic("AGA.010", state, {"status": "Speech Analytics consultado", "tool_ok": result.get("ok")}, component="agent.backoffice.native.speech")
|
||||
elif tool in {"consultar_imdb_cliente", "consultar_cliente_backoffice"}:
|
||||
await self._emit_ic("AGA.011", state, {"status": "Cliente/IMDB consultado", "tool_ok": result.get("ok")}, component="agent.backoffice.native.customer")
|
||||
elif tool in {"consultar_tais_kb", "buscar_templates_emulador"}:
|
||||
await self._emit_ic("AGA.020", state, {"status": "Templates/TAIS consultados", "tool_ok": result.get("ok")}, component="agent.backoffice.native.templates")
|
||||
elif tool in {"registrar_acao_backoffice", "registrar_acao_siebel"}:
|
||||
await self._emit_ic("AGA.006", state, {"status": "Ação operacional solicitada", "tool_ok": result.get("ok")}, component="agent.backoffice.native.action")
|
||||
|
||||
return results
|
||||
|
||||
def _tool_arguments(self, tool: str, intent: str, state: dict[str, Any]) -> dict[str, Any]:
|
||||
ctx = state.get("context") or {}
|
||||
# Para query/prompt usamos o texto sanitizado; para extração de CPF/CNPJ
|
||||
# precisamos do texto original, pois o guardrail MSK mascara o documento.
|
||||
text = state.get("sanitized_input") or state.get("user_text") or ""
|
||||
original_text = (
|
||||
ctx.get("message")
|
||||
or ctx.get("text")
|
||||
or ctx.get("query")
|
||||
or state.get("user_text")
|
||||
or text
|
||||
or ""
|
||||
)
|
||||
bc = ctx.get("business_context") or state.get("business_context") or {}
|
||||
explicit = ctx.get("tool_arguments") or state.get("tool_arguments") or {}
|
||||
|
||||
def pick(*names: str) -> Any:
|
||||
for name in names:
|
||||
cur: Any = None
|
||||
if name in explicit:
|
||||
cur = explicit.get(name)
|
||||
elif isinstance(bc, dict) and name in bc:
|
||||
cur = bc.get(name)
|
||||
elif name in ctx:
|
||||
cur = ctx.get(name)
|
||||
elif name in state:
|
||||
cur = state.get(name)
|
||||
if cur not in (None, "", {}, []):
|
||||
return cur
|
||||
return None
|
||||
|
||||
protocol_id = pick(
|
||||
"protocol_id",
|
||||
"protocolo",
|
||||
"interaction_key",
|
||||
"complaint_id",
|
||||
"complaintProtocol",
|
||||
"transactionId",
|
||||
"transaction_id",
|
||||
"ticket_id",
|
||||
)
|
||||
extracted_identity = extract_identity_from_text(str(original_text))
|
||||
# Identificador explicitamente digitado na mensagem atual prevalece sobre
|
||||
# defaults da sessão/front (ex.: user_id/msisdn 11999999999).
|
||||
customer_key = extracted_identity.get("customer_key") or pick("customer_key", "cpf", "cnpj", "document", "msisdn", "customer_id", "cpf_hash", "document_hash", "user_id")
|
||||
if not protocol_id:
|
||||
protocol_id = extracted_identity.get("protocol_id") or extracted_identity.get("interaction_key")
|
||||
contract_key = pick("contract_key", "contract_id", "account_id", "asset_id")
|
||||
session_key = pick("session_key", "conversation_key", "session_id")
|
||||
|
||||
# Extra args são passados ao MCPToolRouter. O mapper do framework ainda
|
||||
# aplica mcp_parameter_mapping.yaml, mas estes aliases tornam a chamada
|
||||
# resiliente para tools que exigem protocol_id diretamente.
|
||||
common = {
|
||||
"query": text,
|
||||
"operator_instructions": text,
|
||||
"selected_actions": explicit.get("selected_actions") or [],
|
||||
}
|
||||
if protocol_id:
|
||||
common["protocol_id"] = str(protocol_id)
|
||||
common["interaction_key"] = str(protocol_id)
|
||||
if customer_key:
|
||||
common["customer_key"] = str(customer_key)
|
||||
# Mantém aliases documentais para tools TIM que aceitam CPF/CNPJ/document.
|
||||
for identity_key in ("cpf", "cnpj", "document", "document_type"):
|
||||
if extracted_identity.get(identity_key):
|
||||
common[identity_key] = str(extracted_identity[identity_key])
|
||||
if contract_key:
|
||||
common["contract_key"] = str(contract_key)
|
||||
if session_key:
|
||||
common["session_key"] = str(session_key)
|
||||
common["operator_session"] = str(session_key)
|
||||
|
||||
if tool.startswith("registrar_"):
|
||||
action_text = explicit.get("action_text")
|
||||
if not action_text and intent == "backoffice_action_register":
|
||||
action_text = text
|
||||
common["action_text"] = action_text
|
||||
return common
|
||||
|
||||
async def _generate_answer(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
rag_context: str,
|
||||
rag_metadata: dict[str, Any],
|
||||
mcp_results: list[dict[str, Any]],
|
||||
) -> str:
|
||||
missing = self._missing_required_context(state)
|
||||
if missing:
|
||||
return (
|
||||
"[BackofficeAgent] Para seguir no padrão do framework, preciso do identificador canônico "
|
||||
f"{', '.join(missing)}. Informe o protocolo/reclamação/chamado ou configure o mapping em identity.yaml."
|
||||
)
|
||||
|
||||
system_prompt = apply_agent_profile_prompt(state, self._system_prompt())
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Mensagem do usuário:\n"
|
||||
f"{state.get('sanitized_input') or state.get('user_text') or ''}\n\n"
|
||||
"Intent/rota escolhidos pelo framework:\n"
|
||||
f"intent={state.get('intent')} route={state.get('route')}\n\n"
|
||||
"Resultados MCP normalizados pelo framework:\n"
|
||||
f"{mcp_results}\n\n"
|
||||
"Contexto RAG nativo do framework:\n"
|
||||
f"{rag_context or '[sem contexto RAG]'}\n\n"
|
||||
"Metadados RAG:\n"
|
||||
f"{rag_metadata}"
|
||||
),
|
||||
},
|
||||
]
|
||||
try:
|
||||
generated = await self._invoke_llm_cached(state, "BackofficeNativeAgent", messages)
|
||||
return f"[BackofficeAgent] {generated}"
|
||||
except Exception as exc:
|
||||
await self._emit_noc(
|
||||
"005",
|
||||
state,
|
||||
{"message": f"LLM failed in native backoffice agent: {type(exc).__name__}: {exc}", "type": "FAILURE"},
|
||||
component="agent.backoffice.native.llm",
|
||||
)
|
||||
return self._fallback_answer(state, mcp_results)
|
||||
|
||||
def _has_original_ticket_context(self, state: dict[str, Any]) -> bool:
|
||||
ctx = state.get("context") or {}
|
||||
rc = ctx.get("request_context") or ctx
|
||||
return bool(
|
||||
rc.get("transactionId")
|
||||
and isinstance(rc.get("complaint") or {}, dict)
|
||||
and isinstance(rc.get("customer") or {}, dict)
|
||||
)
|
||||
|
||||
async def _run_original_checklist_workflow(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Deprecated compatibility stub.
|
||||
|
||||
A migração framework-native não permite que o agente conversacional
|
||||
compile/execute ``src.agent.graphs`` diretamente. Os fluxos checklist e
|
||||
response emulator são executados pelo BackofficeNativeRuntime chamado
|
||||
pelas rotas/adapters do backend.
|
||||
"""
|
||||
return {
|
||||
"executed": False,
|
||||
"error": {
|
||||
"type": "DeprecatedDirectGraphExecution",
|
||||
"message": "Use BackofficeNativeRuntime.execute_workflow('backoffice_checklist')",
|
||||
},
|
||||
}
|
||||
|
||||
def _system_prompt(self) -> str:
|
||||
return """
|
||||
Você é o agente Backoffice/ANATEL executando exclusivamente pelo framework corporativo.
|
||||
|
||||
Use apenas estes insumos:
|
||||
- BusinessContext canônico do framework.
|
||||
- Ferramentas MCP selecionadas pelo EnterpriseRouter.
|
||||
- Contexto RAG retornado pelo RagService do framework.
|
||||
- Memória/checkpoint já carregados pelo workflow.
|
||||
|
||||
Regras obrigatórias:
|
||||
1. Não invente protocolo, cliente, contrato, status, SLA, parecer ou ação Siebel.
|
||||
2. Se uma ferramenta MCP retornar erro ou ausência de dados, diga exatamente que a evidência não foi encontrada.
|
||||
3. Não confirme registro de ação sem retorno ok/registered da tool correspondente.
|
||||
4. Se faltar protocolo/chamado/reclamação, peça somente esse identificador.
|
||||
5. Responda de forma operacional, curta e rastreável.
|
||||
6. A resposta será validada por OutputSupervisor, guardrails de saída e judges do framework.
|
||||
""".strip()
|
||||
|
||||
def _fallback_answer(self, state: dict[str, Any], mcp_results: list[dict[str, Any]]) -> str:
|
||||
ok_tools = [r.get("tool_name") or r.get("tool") for r in mcp_results if r.get("ok")]
|
||||
failed_tools = [r.get("tool_name") or r.get("tool") for r in mcp_results if not r.get("ok")]
|
||||
return (
|
||||
"[BackofficeAgent] Fluxo nativo executado pelo framework. "
|
||||
f"Intent: {state.get('intent')}. "
|
||||
f"Tools com sucesso: {ok_tools or 'nenhuma'}. "
|
||||
f"Tools pendentes/erro: {failed_tools or 'nenhuma'}. "
|
||||
"A resposta final não foi enriquecida pelo LLM porque houve falha controlada nessa etapa."
|
||||
)
|
||||
|
||||
def _next_state(self, state: dict[str, Any], mcp_results: list[dict[str, Any]]) -> str:
|
||||
if self._missing_required_context(state):
|
||||
return "WAITING_BACKOFFICE_IDENTIFIER"
|
||||
if any(r.get("skipped") for r in mcp_results):
|
||||
return "BACKOFFICE_WAITING_ACTION_TEXT"
|
||||
return "BACKOFFICE_ACTIVE"
|
||||
70
app/agents/billing_agent.py
Normal file
70
app/agents/billing_agent.py
Normal file
@@ -0,0 +1,70 @@
|
||||
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):
|
||||
self.llm = llm
|
||||
self.telemetry = telemetry
|
||||
self.tool_router = tool_router
|
||||
self.rag_service = rag_service
|
||||
self.cache = cache
|
||||
self.settings = settings
|
||||
self.observer = observer
|
||||
|
||||
async def run(self, state):
|
||||
await self._emit_ic(
|
||||
"IC.BILLING_AGENT_STARTED",
|
||||
state,
|
||||
{"business_component": "faturas"},
|
||||
component="agent.billing.start",
|
||||
)
|
||||
session = (state.get("context") or {}).get("session", {})
|
||||
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",
|
||||
)
|
||||
messages = [
|
||||
{"role": "system", "content": 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.")},
|
||||
{"role": "user", "content": (
|
||||
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
|
||||
f"Contexto de sessão: {session}\n"
|
||||
f"Intent: {state.get('intent')}\n"
|
||||
f"Dados MCP: {tool_context}\n"
|
||||
f"Contexto RAG: {rag_context}"
|
||||
)},
|
||||
]
|
||||
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}
|
||||
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")),
|
||||
},
|
||||
component="agent.billing.completed",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
async def _collect_tool_context(self, state):
|
||||
return await self._collect_mcp_context(state)
|
||||
69
app/agents/orders_agent.py
Normal file
69
app/agents/orders_agent.py
Normal file
@@ -0,0 +1,69 @@
|
||||
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):
|
||||
self.llm = llm
|
||||
self.telemetry = telemetry
|
||||
self.tool_router = tool_router
|
||||
self.rag_service = rag_service
|
||||
self.cache = cache
|
||||
self.settings = settings
|
||||
self.observer = observer
|
||||
|
||||
async def run(self, state):
|
||||
await self._emit_ic(
|
||||
"IC.ORDERS_AGENT_STARTED",
|
||||
state,
|
||||
{"business_component": "pedidos"},
|
||||
component="agent.orders.start",
|
||||
)
|
||||
session = (state.get("context") or {}).get("session", {})
|
||||
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",
|
||||
)
|
||||
messages = [
|
||||
{"role": "system", "content": apply_agent_profile_prompt(state, "Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.")},
|
||||
{"role": "user", "content": (
|
||||
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
|
||||
f"Sessão: {session}\n"
|
||||
f"Intent: {state.get('intent')}\n"
|
||||
f"Dados MCP: {tool_context}\n"
|
||||
f"Contexto RAG: {rag_context}"
|
||||
)},
|
||||
]
|
||||
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}
|
||||
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")),
|
||||
},
|
||||
component="agent.orders.completed",
|
||||
)
|
||||
return result
|
||||
|
||||
async def _collect_tool_context(self, state):
|
||||
return await self._collect_mcp_context(state)
|
||||
70
app/agents/product_agent.py
Normal file
70
app/agents/product_agent.py
Normal file
@@ -0,0 +1,70 @@
|
||||
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):
|
||||
self.llm = llm
|
||||
self.telemetry = telemetry
|
||||
self.tool_router = tool_router
|
||||
self.rag_service = rag_service
|
||||
self.cache = cache
|
||||
self.settings = settings
|
||||
self.observer = observer
|
||||
|
||||
async def run(self, state):
|
||||
await self._emit_ic(
|
||||
"IC.PRODUCT_AGENT_STARTED",
|
||||
state,
|
||||
{"business_component": "produtos"},
|
||||
component="agent.product.start",
|
||||
)
|
||||
session = (state.get("context") or {}).get("session", {})
|
||||
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",
|
||||
)
|
||||
messages = [
|
||||
{"role": "system", "content": 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.")},
|
||||
{"role": "user", "content": (
|
||||
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
|
||||
f"Contexto de sessão: {session}\n"
|
||||
f"Intent: {state.get('intent')}\n"
|
||||
f"Dados MCP: {tool_context}\n"
|
||||
f"Contexto RAG: {rag_context}"
|
||||
)},
|
||||
]
|
||||
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}
|
||||
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")),
|
||||
},
|
||||
component="agent.product.completed",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
async def _collect_tool_context(self, state):
|
||||
return await self._collect_mcp_context(state)
|
||||
15
app/agents/prompting.py
Normal file
15
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}"
|
||||
267
app/agents/runtime.py
Normal file
267
app/agents/runtime.py
Normal file
@@ -0,0 +1,267 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AgentRuntimeMixin:
|
||||
"""Mixin operacional para agentes.
|
||||
|
||||
Integra RAG, cache, telemetria e chamadas MCP usando BusinessContext.
|
||||
Os agentes não precisam conhecer nomes reais de parâmetros do domínio
|
||||
(msisdn, invoice_id, order_id etc.); eles repassam as chaves canônicas e
|
||||
o MCPParameterMapper converte para cada tool configurada.
|
||||
"""
|
||||
|
||||
|
||||
async def _emit_ic(self, code: str, state: dict[str, Any], payload: dict[str, Any] | None = None, component: str | None = None) -> None:
|
||||
"""Emite Item de Controle (IC) sem impactar a execução do agente.
|
||||
|
||||
Este helper é intencionalmente fail-open: erro de observabilidade não
|
||||
pode quebrar a jornada de negócio do agente. O desenvolvedor pode usar
|
||||
o mesmo padrão para ICs específicos da sua squad.
|
||||
"""
|
||||
observer = getattr(self, "observer", None)
|
||||
if not observer:
|
||||
return
|
||||
ctx = state.get("context") or {}
|
||||
base = {
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"route": state.get("route"),
|
||||
"intent": state.get("intent"),
|
||||
"message_id": ctx.get("message_id"),
|
||||
"channel_id": ctx.get("channel"),
|
||||
}
|
||||
base.update(payload or {})
|
||||
try:
|
||||
await observer.emit_ic(code, base, component=component or f"agent.{getattr(self, 'name', 'unknown')}")
|
||||
except Exception:
|
||||
return
|
||||
|
||||
async def _emit_noc(self, code: str, state: dict[str, Any], payload: dict[str, Any] | None = None, component: str | None = None) -> None:
|
||||
"""Emite evento NOC sem acoplar a lógica de negócio à observabilidade."""
|
||||
observer = getattr(self, "observer", None)
|
||||
if not observer:
|
||||
return
|
||||
ctx = state.get("context") or {}
|
||||
base = {
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"route": state.get("route"),
|
||||
"intent": state.get("intent"),
|
||||
"message_id": ctx.get("message_id"),
|
||||
"channel_id": ctx.get("channel"),
|
||||
}
|
||||
base.update(payload or {})
|
||||
try:
|
||||
await observer.emit_noc(code, base, component=component or f"agent.{getattr(self, 'name', 'unknown')}")
|
||||
except Exception:
|
||||
return
|
||||
|
||||
async def _emit_grl(self, code: str, state: dict[str, Any], payload: dict[str, Any] | None = None, component: str | None = None) -> None:
|
||||
"""Emite evento GRL opcional para custom rails implementados no backend."""
|
||||
observer = getattr(self, "observer", None)
|
||||
if not observer:
|
||||
return
|
||||
ctx = state.get("context") or {}
|
||||
base = {
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"route": state.get("route"),
|
||||
"intent": state.get("intent"),
|
||||
"message_id": ctx.get("message_id"),
|
||||
"channel_id": ctx.get("channel"),
|
||||
}
|
||||
base.update(payload or {})
|
||||
try:
|
||||
await observer.emit_grl(code, base, component=component or f"agent.{getattr(self, 'name', 'unknown')}")
|
||||
except Exception:
|
||||
return
|
||||
|
||||
async def _retrieve_rag_context(self, state: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
||||
rag_service = getattr(self, "rag_service", None)
|
||||
if not rag_service:
|
||||
return "", {"enabled": False}
|
||||
text = state.get("sanitized_input") or state.get("user_text") or ""
|
||||
namespace = (
|
||||
(state.get("agent_profile") or {}).get("rag_namespace")
|
||||
or state.get("agent_id")
|
||||
or state.get("route")
|
||||
or "default"
|
||||
)
|
||||
ctx = state.get("context") or {}
|
||||
business_context = ctx.get("business_context") or {}
|
||||
graph_node = (
|
||||
ctx.get("graph_node")
|
||||
or business_context.get("customer_key")
|
||||
or business_context.get("contract_key")
|
||||
or ctx.get("customer_id")
|
||||
)
|
||||
result = await rag_service.retrieve(text, namespace=namespace, graph_node=graph_node)
|
||||
context = result.as_prompt_context()
|
||||
return context, {
|
||||
"enabled": True,
|
||||
"namespace": namespace,
|
||||
"latency_ms": result.latency_ms,
|
||||
"document_count": len(result.documents),
|
||||
"graph_neighbors": len(result.graph_neighbors),
|
||||
"top_document_ids": [d.id for d in result.documents[:5]],
|
||||
"top_scores": [d.score for d in result.documents[:5]],
|
||||
}
|
||||
|
||||
async def _call_mcp_tool(self, tool: str, arguments: dict[str, Any] | None, state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Chama uma ferramenta via MCPToolRouter usando o contrato canônico do framework.
|
||||
|
||||
Use este helper quando o agente precisa passar argumentos específicos
|
||||
além do BusinessContext mapeado em mcp_parameter_mapping.yaml.
|
||||
Observabilidade IC.MCP_TOOL_CALLED/IC.TOOL_CALLED permanece uniforme.
|
||||
"""
|
||||
if not getattr(self, "tool_router", None):
|
||||
return {"ok": False, "tool_name": tool, "error": "tool_router não configurado"}
|
||||
ctx = state.get("context") or {}
|
||||
business_context = ctx.get("business_context") or state.get("business_context") or {}
|
||||
original_context = {
|
||||
**ctx,
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"conversation_key": state.get("conversation_key") or state.get("session_id"),
|
||||
}
|
||||
observer = getattr(self, "observer", None)
|
||||
if observer:
|
||||
await observer.emit_ic(
|
||||
"IC.MCP_TOOL_CALLED",
|
||||
{
|
||||
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
|
||||
"tenant_id": original_context.get("tenant_id"),
|
||||
"agent_id": original_context.get("agent_id"),
|
||||
"tool_name": tool,
|
||||
"framework_native": True,
|
||||
},
|
||||
component="agent_runtime.native_mcp",
|
||||
)
|
||||
try:
|
||||
res = await self.tool_router.call(
|
||||
tool,
|
||||
arguments or {},
|
||||
business_context=business_context,
|
||||
original_context=original_context,
|
||||
)
|
||||
result_payload = res.model_dump(mode="json") if hasattr(res, "model_dump") else dict(res)
|
||||
except Exception as exc:
|
||||
result_payload = {"ok": False, "tool_name": tool, "error": str(exc), "error_type": type(exc).__name__}
|
||||
result_payload.setdefault("tool_name", tool)
|
||||
if observer:
|
||||
await observer.emit_ic(
|
||||
"IC.TOOL_CALLED",
|
||||
{
|
||||
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
|
||||
"tenant_id": original_context.get("tenant_id"),
|
||||
"agent_id": original_context.get("agent_id"),
|
||||
"tool_name": tool,
|
||||
"ok": result_payload.get("ok"),
|
||||
"server_name": result_payload.get("server_name"),
|
||||
"error": result_payload.get("error"),
|
||||
"framework_native": True,
|
||||
},
|
||||
component="agent_runtime.native_mcp",
|
||||
)
|
||||
return result_payload
|
||||
|
||||
async def _collect_mcp_context(self, state: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
if not getattr(self, "tool_router", None):
|
||||
return results
|
||||
tools = state.get("mcp_tools") or []
|
||||
ctx = state.get("context") or {}
|
||||
business_context = ctx.get("business_context") or state.get("business_context") or {}
|
||||
original_context = {
|
||||
**ctx,
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"conversation_key": state.get("conversation_key") or state.get("session_id"),
|
||||
}
|
||||
for tool in tools:
|
||||
observer = getattr(self, "observer", None)
|
||||
if observer:
|
||||
await observer.emit_ic(
|
||||
"IC.MCP_TOOL_CALLED",
|
||||
{
|
||||
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
|
||||
"tenant_id": original_context.get("tenant_id"),
|
||||
"agent_id": original_context.get("agent_id"),
|
||||
"tool_name": tool,
|
||||
},
|
||||
component="agent_runtime",
|
||||
)
|
||||
res = await self.tool_router.call(
|
||||
tool,
|
||||
{},
|
||||
business_context=business_context,
|
||||
original_context=original_context,
|
||||
)
|
||||
result_payload = res.model_dump(mode="json")
|
||||
if observer:
|
||||
await observer.emit_ic(
|
||||
"IC.TOOL_CALLED",
|
||||
{
|
||||
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
|
||||
"tenant_id": original_context.get("tenant_id"),
|
||||
"agent_id": original_context.get("agent_id"),
|
||||
"tool_name": tool,
|
||||
"ok": result_payload.get("ok"),
|
||||
"server_name": result_payload.get("server_name"),
|
||||
"error": result_payload.get("error"),
|
||||
},
|
||||
component="agent_runtime",
|
||||
)
|
||||
results.append(result_payload)
|
||||
return results
|
||||
|
||||
async def _cache_get(self, key: str):
|
||||
cache = getattr(self, "cache", None)
|
||||
if not cache:
|
||||
return None
|
||||
return await cache.get(key)
|
||||
|
||||
async def _cache_set(self, key: str, value: Any, ttl_seconds: int | None = None):
|
||||
cache = getattr(self, "cache", None)
|
||||
if not cache:
|
||||
return
|
||||
await cache.set(key, value, ttl_seconds)
|
||||
|
||||
def _llm_cache_key(self, state: dict[str, Any], agent_name: str, prompt_parts: list[Any]) -> str:
|
||||
business_context = (state.get("context") or {}).get("business_context") or {}
|
||||
raw = "|".join([
|
||||
agent_name,
|
||||
state.get("tenant_id") or "",
|
||||
state.get("agent_id") or "",
|
||||
state.get("intent") or "",
|
||||
business_context.get("customer_key") or "",
|
||||
business_context.get("contract_key") or "",
|
||||
business_context.get("interaction_key") or "",
|
||||
state.get("sanitized_input") or state.get("user_text") or "",
|
||||
repr(prompt_parts),
|
||||
])
|
||||
return "llm:" + hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
async def _invoke_llm_cached(self, state: dict[str, Any], agent_name: str, messages: list[dict[str, str]]):
|
||||
ttl = int(getattr(getattr(self, "settings", None), "CACHE_TTL_SECONDS", 300) or 300)
|
||||
key = self._llm_cache_key(state, agent_name, messages)
|
||||
cached = await self._cache_get(key)
|
||||
if cached is not None:
|
||||
telemetry = getattr(self, "telemetry", None)
|
||||
if telemetry:
|
||||
await telemetry.event("cache.llm.hit", {"agent": agent_name, "key": key}, kind="cache")
|
||||
return cached
|
||||
telemetry = getattr(self, "telemetry", None)
|
||||
if telemetry:
|
||||
await telemetry.event("cache.llm.miss", {"agent": agent_name, "key": key}, kind="cache")
|
||||
answer = await self.llm.ainvoke(messages)
|
||||
await self._cache_set(key, answer, ttl)
|
||||
return answer
|
||||
67
app/agents/support_agent.py
Normal file
67
app/agents/support_agent.py
Normal file
@@ -0,0 +1,67 @@
|
||||
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):
|
||||
self.llm = llm
|
||||
self.telemetry = telemetry
|
||||
self.tool_router = tool_router
|
||||
self.rag_service = rag_service
|
||||
self.cache = cache
|
||||
self.settings = settings
|
||||
self.observer = observer
|
||||
|
||||
async def run(self, state):
|
||||
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",
|
||||
)
|
||||
messages = [
|
||||
{"role": "system", "content": apply_agent_profile_prompt(state, "Você é um agente de suporte de varejo para troca, devolução e garantia.")},
|
||||
{"role": "user", "content": (
|
||||
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
|
||||
f"Intent: {state.get('intent')}\n"
|
||||
f"Dados MCP: {tool_context}\n"
|
||||
f"Contexto RAG: {rag_context}"
|
||||
)},
|
||||
]
|
||||
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}
|
||||
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")),
|
||||
},
|
||||
component="agent.support.completed",
|
||||
)
|
||||
return result
|
||||
|
||||
async def _collect_tool_context(self, state):
|
||||
return await self._collect_mcp_context(state)
|
||||
Reference in New Issue
Block a user