mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 10:13:46 +00:00
New features: Route Stickness, Handoff, Clarification, Read-Only/Transactional, Long Term Memory
This commit is contained in:
69
.env
69
.env
@@ -22,30 +22,31 @@ LLM_TIMEOUT_SECONDS=120
|
|||||||
# OCI OpenAI-compatible endpoint
|
# OCI OpenAI-compatible endpoint
|
||||||
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
||||||
OCI_GENAI_MODEL=openai.gpt-4.1
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
|
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
||||||
OCI_GENAI_PROJECT_OCID=
|
OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
|
||||||
|
OCI_AUTH_MODE=config_file
|
||||||
# OCI SDK / signer / profiles
|
# OCI SDK / signer / profiles
|
||||||
OCI_CONFIG_FILE=~/.oci/config
|
OCI_CONFIG_FILE=~/.oci/config
|
||||||
OCI_PROFILE=LATINOAMERICA-Chicago
|
OCI_PROFILE=DEFAULT
|
||||||
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
|
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
OCI_REGION=us-chicago-1
|
OCI_REGION=us-chicago-1
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Persistência
|
# Persistência
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Opções: memory, autonomous, mongodb
|
# Opções: memory, autonomous, mongodb
|
||||||
SESSION_REPOSITORY_PROVIDER=sqlite
|
SESSION_REPOSITORY_PROVIDER=autonomous
|
||||||
MEMORY_REPOSITORY_PROVIDER=sqlite
|
MEMORY_REPOSITORY_PROVIDER=autonomous
|
||||||
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
SQLITE_DB_PATH=./data/agent_framework.db
|
|
||||||
|
|
||||||
# Autonomous Database
|
# Autonomous Database
|
||||||
ADB_USER=admin
|
ADB_USER=admin
|
||||||
ADB_PASSWORD=Moniquinha1972
|
ADB_PASSWORD=fjhsdf04954hf
|
||||||
ADB_DSN=oradb23ai_high
|
ADB_DSN=oradb23aidev_high
|
||||||
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
|
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
||||||
ADB_WALLET_PASSWORD=Moniquinha1972
|
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
||||||
ADB_TABLE_PREFIX=AGENTFW
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
|
||||||
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||||
@@ -70,13 +71,16 @@ RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
|||||||
# Observabilidade
|
# Observabilidade
|
||||||
###############################################################################
|
###############################################################################
|
||||||
ENABLE_LANGFUSE=true
|
ENABLE_LANGFUSE=true
|
||||||
LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
|
LANGFUSE_TRACE_MODE=verbose # Opcional: verbose, compact
|
||||||
LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
|
LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
|
||||||
LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
|
LANGFUSE_LEGACY_IO_FALLBACK=true
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
|
||||||
LANGFUSE_HOST=http://localhost:3005
|
LANGFUSE_HOST=http://localhost:3005
|
||||||
ENABLE_OTEL=false
|
ENABLE_OTEL=false
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
OTEL_SERVICE_NAME=ai-agent-template
|
OTEL_SERVICE_NAME=ai-agent-template
|
||||||
|
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Analytics / Observer corporativo
|
# Analytics / Observer corporativo
|
||||||
@@ -121,6 +125,9 @@ PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
|||||||
# Gateway de canais
|
# Gateway de canais
|
||||||
###############################################################################
|
###############################################################################
|
||||||
DEFAULT_CHANNEL=web
|
DEFAULT_CHANNEL=web
|
||||||
|
# embedded = backend may parse simple/native channel payloads.
|
||||||
|
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
ENABLE_VOICE_ADAPTER=true
|
ENABLE_VOICE_ADAPTER=true
|
||||||
ENABLE_WHATSAPP_ADAPTER=true
|
ENABLE_WHATSAPP_ADAPTER=true
|
||||||
ENABLE_TEXT_ADAPTER=true
|
ENABLE_TEXT_ADAPTER=true
|
||||||
@@ -134,21 +141,33 @@ ROUTING_CONFIG_PATH=./config/routing.yaml
|
|||||||
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||||
ENABLE_LLM_ROUTER=true
|
ENABLE_LLM_ROUTER=true
|
||||||
|
|
||||||
|
# Semantic route stickiness (optional).
|
||||||
|
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
||||||
|
# There are no regexes or deterministic language rules.
|
||||||
|
ENABLE_ROUTE_STICKINESS=false
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||||
|
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# MCP / Tools
|
# MCP / Tools
|
||||||
###############################################################################
|
###############################################################################
|
||||||
ENABLE_MCP_TOOLS=true
|
ENABLE_MCP_TOOLS=true
|
||||||
MCP_SERVERS_CONFIG_PATH=./agent_template_backend/config/mcp_servers.yaml
|
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||||
TOOLS_CONFIG_PATH=./agent_template_backend/config/tools.yaml
|
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||||
|
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||||
MCP_TOOL_TIMEOUT_SECONDS=30
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||||
ROUTING_MODE=router
|
ROUTING_MODE=router
|
||||||
|
|
||||||
# Usage/cost accounting
|
# Usage/cost accounting
|
||||||
USAGE_REPOSITORY_PROVIDER=sqlite
|
USAGE_REPOSITORY_PROVIDER=autonomous
|
||||||
IDENTITY_CONFIG_PATH=./agent_template_backend/config/identity.yaml
|
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||||
MCP_PARAMETER_MAPPING_PATH=./agent_template_backend/config/mcp_parameter_mapping.yaml
|
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# ConversationSummaryMemory / compressão de contexto conversacional
|
# ConversationSummaryMemory / compressão de contexto conversacional
|
||||||
@@ -163,6 +182,18 @@ MEMORY_SUMMARY_USE_LLM=true
|
|||||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||||
MEMORY_INJECT_SUMMARY=true
|
MEMORY_INJECT_SUMMARY=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# MCP Gateway
|
||||||
|
###############################################################################
|
||||||
|
# true = framework routes tool calls to the dedicated MCP Gateway.
|
||||||
|
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
# MCP_GATEWAY_TOKEN=
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# LONG-TERM MEMORY
|
# LONG-TERM MEMORY
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
14
.env.example
14
.env.example
@@ -141,12 +141,24 @@ ROUTING_CONFIG_PATH=./config/routing.yaml
|
|||||||
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||||
ENABLE_LLM_ROUTER=true
|
ENABLE_LLM_ROUTER=true
|
||||||
|
|
||||||
|
# Semantic route stickiness (optional).
|
||||||
|
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
||||||
|
# There are no regexes or deterministic language rules.
|
||||||
|
ENABLE_ROUTE_STICKINESS=false
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||||
|
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# MCP / Tools
|
# MCP / Tools
|
||||||
###############################################################################
|
###############################################################################
|
||||||
ENABLE_MCP_TOOLS=true
|
ENABLE_MCP_TOOLS=true
|
||||||
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||||
TOOLS_CONFIG_PATH=./config/tools.yaml
|
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||||
|
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||||
MCP_TOOL_TIMEOUT_SECONDS=30
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||||
@@ -194,4 +206,4 @@ LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
|||||||
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
|||||||
163
.idea/workspace.xml
generated
163
.idea/workspace.xml
generated
@@ -4,9 +4,7 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="bugfix Alex 2026-06-30">
|
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
@@ -47,28 +45,31 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "main",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"node.js.detected.package.eslint": "true",
|
"git-widget-placeholder": "master",
|
||||||
"node.js.detected.package.tslint": "true",
|
"kotlin-language-version-configured": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"last_opened_file_path": "D:/Dropbox/ORACLE/TIM/FY27/Wave_2/Ajuste para manter o agente atual ou voltar ao roteamento/agent_framework_oci",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.detected.package.eslint": "true",
|
||||||
"nodejs_package_manager_path": "npm",
|
"node.js.detected.package.tslint": "true",
|
||||||
"settings.editor.selected.configurable": "configurable.group.appearance",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "configurable.group.appearance",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.23774.435" />
|
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.29188.36" />
|
||||||
<option value="bundled-js-predefined-d6986cc7102b-f27c65a3e318-JavaScript-IU-251.23774.435" />
|
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-IU-251.29188.36" />
|
||||||
</set>
|
</set>
|
||||||
</attachedChunks>
|
</attachedChunks>
|
||||||
</component>
|
</component>
|
||||||
@@ -83,8 +84,14 @@
|
|||||||
<workItem from="1782038604623" duration="6486000" />
|
<workItem from="1782038604623" duration="6486000" />
|
||||||
<workItem from="1782047166074" duration="27000" />
|
<workItem from="1782047166074" duration="27000" />
|
||||||
<workItem from="1782047194363" duration="961000" />
|
<workItem from="1782047194363" duration="961000" />
|
||||||
<workItem from="1782048494672" duration="2790000" />
|
<workItem from="1782048494672" duration="230000" />
|
||||||
<workItem from="1782900499027" duration="911000" />
|
<workItem from="1782592218589" duration="28000" />
|
||||||
|
<workItem from="1785245181314" duration="79000" />
|
||||||
|
<workItem from="1785245608367" duration="670000" />
|
||||||
|
<workItem from="1785326458404" duration="86000" />
|
||||||
|
<workItem from="1785414225783" duration="148000" />
|
||||||
|
<workItem from="1785414447653" duration="704000" />
|
||||||
|
<workItem from="1785630146329" duration="316000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
|
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -110,111 +117,7 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1782048304579</updated>
|
<updated>1782048304579</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00004" summary="Ajustes na documentação e remanejamento dos folders">
|
<option name="localTasksCounter" value="4" />
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782048747421</created>
|
|
||||||
<option name="number" value="00004" />
|
|
||||||
<option name="presentableId" value="LOCAL-00004" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782048747421</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00005" summary="bugfix Alex 2026-06-24">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782321346355</created>
|
|
||||||
<option name="number" value="00005" />
|
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782321346355</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00006" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900906490</created>
|
|
||||||
<option name="number" value="00006" />
|
|
||||||
<option name="presentableId" value="LOCAL-00006" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900906490</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00007" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900910793</created>
|
|
||||||
<option name="number" value="00007" />
|
|
||||||
<option name="presentableId" value="LOCAL-00007" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900910793</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00008" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900913579</created>
|
|
||||||
<option name="number" value="00008" />
|
|
||||||
<option name="presentableId" value="LOCAL-00008" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900913579</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00009" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900914906</created>
|
|
||||||
<option name="number" value="00009" />
|
|
||||||
<option name="presentableId" value="LOCAL-00009" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900914906</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00010" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900923834</created>
|
|
||||||
<option name="number" value="00010" />
|
|
||||||
<option name="presentableId" value="LOCAL-00010" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900923834</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00011" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900925229</created>
|
|
||||||
<option name="number" value="00011" />
|
|
||||||
<option name="presentableId" value="LOCAL-00011" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900925229</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00012" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900926577</created>
|
|
||||||
<option name="number" value="00012" />
|
|
||||||
<option name="presentableId" value="LOCAL-00012" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900926577</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00013" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900927457</created>
|
|
||||||
<option name="number" value="00013" />
|
|
||||||
<option name="presentableId" value="LOCAL-00013" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900927457</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00014" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900928320</created>
|
|
||||||
<option name="number" value="00014" />
|
|
||||||
<option name="presentableId" value="LOCAL-00014" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900928320</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00015" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900928490</created>
|
|
||||||
<option name="number" value="00015" />
|
|
||||||
<option name="presentableId" value="LOCAL-00015" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900928490</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00016" summary="bugfix Alex 2026-06-30">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1782900939364</created>
|
|
||||||
<option name="number" value="00016" />
|
|
||||||
<option name="presentableId" value="LOCAL-00016" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1782900939364</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="17" />
|
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -222,8 +125,6 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="Ajustes na documentação e remanejamento dos folders" />
|
<MESSAGE value="Ajustes na documentação e remanejamento dos folders" />
|
||||||
<MESSAGE value="bugfix Alex 2026-06-24" />
|
<option name="LAST_COMMIT_MESSAGE" value="Ajustes na documentação e remanejamento dos folders" />
|
||||||
<MESSAGE value="bugfix Alex 2026-06-30" />
|
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="bugfix Alex 2026-06-30" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
520
Documentacao/Long_Term_Memory_Implementation_Guide_EN.md
Normal file
520
Documentacao/Long_Term_Memory_Implementation_Guide_EN.md
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
### Long-Term Memory Implementation Guide
|
||||||
|
|
||||||
|
### Concept
|
||||||
|
|
||||||
|
Long-Term Memory (LTM) is the `agent_framework` capability that stores and retrieves durable facts beyond the lifetime of a conversation session.
|
||||||
|
|
||||||
|
Unlike message history, which is normally associated with a `session_id`, Long-Term Memory is associated with the business identity of the user or customer. In the current implementation, this identity consists of:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id
|
||||||
|
agent_id
|
||||||
|
customer_key
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows an agent to retrieve preferences, identity information, projects and constraints even when a new session is created.
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Long-Term Memory is used to:
|
||||||
|
|
||||||
|
- maintain continuity across sessions;
|
||||||
|
- personalize responses;
|
||||||
|
- prevent users from repeating previously supplied information;
|
||||||
|
- reduce the need to send the full conversation history to the model;
|
||||||
|
- store preferences, current projects, preferred names and constraints;
|
||||||
|
- isolate memory across tenants, agents and customers.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Session A:
|
||||||
|
"Call me Cris. My preferred language is Python."
|
||||||
|
|
||||||
|
Session B, with another session_id and the same customer_key:
|
||||||
|
"What do you remember about me?"
|
||||||
|
|
||||||
|
Expected response:
|
||||||
|
"Your preferred name is Cris and your preferred language is Python."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory type differences
|
||||||
|
|
||||||
|
#### Conversation Memory
|
||||||
|
|
||||||
|
Stores messages from the current conversation and is normally associated with the `session_id`.
|
||||||
|
|
||||||
|
#### Summary Memory
|
||||||
|
|
||||||
|
Stores a summary of the conversation to reduce the context size sent to the model.
|
||||||
|
|
||||||
|
#### Long-Term Memory
|
||||||
|
|
||||||
|
Stores durable facts across sessions and is associated with the business identity, primarily the `customer_key`.
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
#### LongTermMemoryManager
|
||||||
|
|
||||||
|
Coordinates:
|
||||||
|
|
||||||
|
- memory loading;
|
||||||
|
- identity-based retrieval;
|
||||||
|
- context rendering;
|
||||||
|
- durable fact extraction;
|
||||||
|
- fact persistence;
|
||||||
|
- deduplication and updates.
|
||||||
|
|
||||||
|
#### LongTermMemoryStore
|
||||||
|
|
||||||
|
Persistence interface used by the manager.
|
||||||
|
|
||||||
|
#### SQLiteLongTermMemoryStore
|
||||||
|
|
||||||
|
Reference implementation based on SQLite.
|
||||||
|
|
||||||
|
It is suitable for:
|
||||||
|
|
||||||
|
- local development;
|
||||||
|
- testing;
|
||||||
|
- demonstrations;
|
||||||
|
- low-scale environments.
|
||||||
|
|
||||||
|
#### InMemoryLongTermMemoryStore
|
||||||
|
|
||||||
|
In-memory implementation used for quick tests.
|
||||||
|
|
||||||
|
Its content is lost when the backend process stops.
|
||||||
|
|
||||||
|
#### LongTermMemoryExtractor
|
||||||
|
|
||||||
|
Identifies durable facts in messages.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preferred_name = Cris
|
||||||
|
preferred_language = Python
|
||||||
|
current_project = Atlas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LongTermMemoryItem
|
||||||
|
|
||||||
|
Data model representing a persisted item, including identity, key, value, category, confidence and metadata.
|
||||||
|
|
||||||
|
#### AgentRuntime
|
||||||
|
|
||||||
|
Loads memory before agent execution and injects the rendered context into the prompt.
|
||||||
|
|
||||||
|
#### persist_long_term_memory node
|
||||||
|
|
||||||
|
LangGraph node responsible for persisting facts after the final response has been generated and validated.
|
||||||
|
|
||||||
|
### File structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/
|
||||||
|
└── agent_framework/
|
||||||
|
└── src/
|
||||||
|
└── agent_framework/
|
||||||
|
└── memory/
|
||||||
|
├── __init__.py
|
||||||
|
├── long_term_extractor.py
|
||||||
|
├── long_term_memory.py
|
||||||
|
├── long_term_models.py
|
||||||
|
└── long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
User message
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AgentRuntime.prepare_memory_context()
|
||||||
|
│
|
||||||
|
├── Conversation Memory
|
||||||
|
├── Summary Memory
|
||||||
|
└── Long-Term Memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
long_term_memory_context
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent prompt
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Guardrails / Judges / Supervisor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryExtractor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryStore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Framework configuration
|
||||||
|
|
||||||
|
### New modules
|
||||||
|
|
||||||
|
Copy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_memory.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_models.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update memory/__init__.py
|
||||||
|
|
||||||
|
Export the Long-Term Memory components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.memory.long_term_memory import (
|
||||||
|
LongTermMemoryManager,
|
||||||
|
create_long_term_memory_manager,
|
||||||
|
)
|
||||||
|
from agent_framework.memory.long_term_models import LongTermMemoryItem
|
||||||
|
from agent_framework.memory.long_term_store import (
|
||||||
|
InMemoryLongTermMemoryStore,
|
||||||
|
LongTermMemoryStore,
|
||||||
|
SQLiteLongTermMemoryStore,
|
||||||
|
create_long_term_memory_store,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update settings.py
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_LONG_TERM_MEMORY: bool = False
|
||||||
|
LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
|
||||||
|
LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentRuntime integration
|
||||||
|
|
||||||
|
The runtime must:
|
||||||
|
|
||||||
|
1. verify that the feature is enabled;
|
||||||
|
2. create the manager when needed;
|
||||||
|
3. retrieve facts using the identity;
|
||||||
|
4. populate the workflow state;
|
||||||
|
5. inject the rendered context into the prompt.
|
||||||
|
|
||||||
|
State fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
long_term_memories: list[dict]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentWorkflow initialization
|
||||||
|
|
||||||
|
Create the manager in `AgentWorkflow`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(
|
||||||
|
settings,
|
||||||
|
telemetry=telemetry,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Correct agent initialization
|
||||||
|
|
||||||
|
Do not pass `long_term_memory_manager` through `agent_kwargs` when the constructors of `BillingAgent`, `ProductAgent`, `OrdersAgent` and `SupportAgent` do not declare that parameter.
|
||||||
|
|
||||||
|
This initialization causes an error:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"settings": settings,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
"long_term_memory_manager": self.long_term_memory_manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting error:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: BillingAgent.__init__() got an unexpected keyword argument
|
||||||
|
'long_term_memory_manager'
|
||||||
|
```
|
||||||
|
|
||||||
|
The recommended approach is to create agents using their existing signatures and inject the manager as an attribute after initialization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"tool_router": getattr(self, "tool_router", None),
|
||||||
|
"rag_service": self.rag_service,
|
||||||
|
"cache": self.cache,
|
||||||
|
"settings": settings,
|
||||||
|
"observer": self.observer,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
self.product = ProductAgent(llm, **agent_kwargs)
|
||||||
|
self.orders = OrdersAgent(llm, **agent_kwargs)
|
||||||
|
self.support = SupportAgent(llm, **agent_kwargs)
|
||||||
|
|
||||||
|
for agent in (
|
||||||
|
self.billing,
|
||||||
|
self.product,
|
||||||
|
self.orders,
|
||||||
|
self.support,
|
||||||
|
):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach avoids changing every agent constructor and keeps the feature encapsulated in the framework.
|
||||||
|
|
||||||
|
### LangGraph configuration
|
||||||
|
|
||||||
|
Register the node:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self._node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self.persist_long_term_memory,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the edges:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_edge(
|
||||||
|
"supervisor_review",
|
||||||
|
"persist_long_term_memory",
|
||||||
|
)
|
||||||
|
builder.add_edge(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
"persist",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def persist_long_term_memory(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memory_write_result": result,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Final flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_review
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite database path
|
||||||
|
|
||||||
|
A relative path is resolved from the directory in which the backend is started.
|
||||||
|
|
||||||
|
To prevent different databases from being created accidentally, prefer an absolute path in development environments:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the directory before starting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
### Test 1 — Persistence
|
||||||
|
|
||||||
|
Send:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-a",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "Call me Cris. My preferred language is Python and my current project is Atlas."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2 — Retrieval in another session
|
||||||
|
|
||||||
|
Use another `session_id` while keeping the same `customer_key`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-b",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "What do you remember about me, my preferences and my project?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Your preferred name is Cris.
|
||||||
|
Your preferred language is Python.
|
||||||
|
Your current project is Atlas.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3 — Isolation
|
||||||
|
|
||||||
|
Use another customer:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-c",
|
||||||
|
"customer_key": "another-customer",
|
||||||
|
"message": "What is my preferred name and current project?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The data associated with `11999999999` must not be returned.
|
||||||
|
|
||||||
|
### Test 4 — Frontend reset
|
||||||
|
|
||||||
|
Restart or reset the frontend and verify that it still sends the same `customer_key`.
|
||||||
|
|
||||||
|
Memory must survive a `session_id` change. Resetting the frontend does not delete the SQLite database.
|
||||||
|
|
||||||
|
### Test 5 — Backend restart
|
||||||
|
|
||||||
|
Restart Uvicorn and repeat the query.
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
memory must remain available.
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
memory is lost when the process stops.
|
||||||
|
|
||||||
|
### Direct SQLite verification
|
||||||
|
|
||||||
|
Find the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -name "agent_framework.db" -type f
|
||||||
|
```
|
||||||
|
|
||||||
|
Open it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 ./data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Query:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
agent_id,
|
||||||
|
customer_key,
|
||||||
|
memory_type,
|
||||||
|
memory_key,
|
||||||
|
memory_value,
|
||||||
|
confidence,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM agentfw_long_term_memory
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success criteria
|
||||||
|
|
||||||
|
The implementation is working when:
|
||||||
|
|
||||||
|
- memory is retrieved with another `session_id`;
|
||||||
|
- the same `customer_key` retrieves previous facts;
|
||||||
|
- another `customer_key` cannot access those facts;
|
||||||
|
- restarting the frontend does not erase memory;
|
||||||
|
- restarting the backend does not erase memory when using SQLite;
|
||||||
|
- the `persist_long_term_memory` node runs;
|
||||||
|
- the prompt receives `long_term_memory_context`.
|
||||||
|
|
||||||
|
### Best practices
|
||||||
|
|
||||||
|
- Persist only durable facts.
|
||||||
|
- Do not store the complete conversation as Long-Term Memory.
|
||||||
|
- Isolate data by `tenant_id`, `agent_id` and `customer_key`.
|
||||||
|
- Do not use `session_id` as the permanent user identity.
|
||||||
|
- Persist only after final validations.
|
||||||
|
- Avoid persisting temporary tool results.
|
||||||
|
- Record telemetry for reads, writes, updates and failures.
|
||||||
|
- Define retention and deletion policies.
|
||||||
|
- Use an absolute SQLite path in environments with multiple working directories.
|
||||||
|
- Move to an enterprise database for production and high-availability environments.
|
||||||
|
|
||||||
|
### Reference implementation limitations
|
||||||
|
|
||||||
|
The current implementation uses rule-based extraction and SQLite as the reference provider.
|
||||||
|
|
||||||
|
Recommended future enhancements:
|
||||||
|
|
||||||
|
- LLM-based fact extraction;
|
||||||
|
- vector-based semantic memory;
|
||||||
|
- episodic memory;
|
||||||
|
- expiration and versioning;
|
||||||
|
- semantic deduplication;
|
||||||
|
- consent policies;
|
||||||
|
- query and deletion APIs;
|
||||||
|
- Oracle Autonomous Database provider;
|
||||||
|
- encryption and sensitive-data classification.
|
||||||
272
Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
272
Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Agent Platform OCI — Manual Oficial de Agent Gateway e MCP Gateway
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Este documento consolida:
|
||||||
|
- Arquitetura oficial
|
||||||
|
- Inventário dos componentes
|
||||||
|
- Procedimento completo de execução local
|
||||||
|
- MCP Gateway
|
||||||
|
- Agent Gateway
|
||||||
|
- Backend Runtime
|
||||||
|
- Frontend
|
||||||
|
- Testes E2E
|
||||||
|
- Troubleshooting
|
||||||
|
- Decisões arquiteturais
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arquitetura Oficial
|
||||||
|
|
||||||
|
Frontend (5173)
|
||||||
|
↓
|
||||||
|
Agent Gateway (9000)
|
||||||
|
↓
|
||||||
|
Agent Template Backend / Runtime (8000)
|
||||||
|
↓
|
||||||
|
MCP Gateway (8300)
|
||||||
|
↓
|
||||||
|
Telecom MCP Server (8100)
|
||||||
|
Retail MCP Server (8200)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Portas Oficiais
|
||||||
|
|
||||||
|
| Componente | Porta |
|
||||||
|
|------------|--------|
|
||||||
|
| Frontend | 5173 |
|
||||||
|
| Agent Gateway | 9000 |
|
||||||
|
| Backend Runtime | 8000 |
|
||||||
|
| MCP Gateway | 8300 |
|
||||||
|
| Telecom MCP Server | 8100 |
|
||||||
|
| Retail MCP Server | 8200 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Variáveis Oficiais
|
||||||
|
|
||||||
|
## Agent Template Backend
|
||||||
|
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ordem de Inicialização
|
||||||
|
|
||||||
|
1. Telecom MCP Server
|
||||||
|
2. Retail MCP Server
|
||||||
|
3. MCP Gateway
|
||||||
|
4. Agent Template Backend
|
||||||
|
5. Agent Gateway
|
||||||
|
6. Frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 1 — Telecom MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/telecom_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 2 — Retail MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/retail_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 3 — MCP Gateway
|
||||||
|
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/ready
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 4 — Agent Template Backend
|
||||||
|
|
||||||
|
cd templates/agent_template_backend
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 5 — Agent Gateway
|
||||||
|
|
||||||
|
cd apps/agent_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:9000/gateway/message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 6 — Frontend
|
||||||
|
|
||||||
|
cd agent_frontend
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
|
||||||
|
Abrir:
|
||||||
|
|
||||||
|
http://localhost:5173
|
||||||
|
|
||||||
|
Backend URL:
|
||||||
|
|
||||||
|
http://localhost:9000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fluxo de Tools
|
||||||
|
|
||||||
|
Agent
|
||||||
|
↓
|
||||||
|
MCPToolRouter
|
||||||
|
↓
|
||||||
|
MCPGatewayClient
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
MCP Server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Teste Integrado E2E
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
↓
|
||||||
|
Agent Gateway
|
||||||
|
↓
|
||||||
|
Backend Runtime
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
Telecom MCP Server
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
- Agent Gateway recebe requisição
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- MCP Gateway resolve tool
|
||||||
|
- MCP Server responde
|
||||||
|
- Usuário recebe resposta
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Backend chamando MCP Server direto
|
||||||
|
|
||||||
|
Confirmar:
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
|
||||||
|
## Porta incorreta
|
||||||
|
|
||||||
|
A porta oficial do MCP Gateway é:
|
||||||
|
|
||||||
|
8300
|
||||||
|
|
||||||
|
## Agent Gateway não encontra Backend
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
## MCP Gateway não encontra MCP Server
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Decisões Arquiteturais Oficiais
|
||||||
|
|
||||||
|
- Agent Gateway centraliza governança
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- Runtime executa LLM
|
||||||
|
- MCP Gateway centraliza tools
|
||||||
|
- MCP Servers executam tools
|
||||||
|
- Backend usa MCP Gateway
|
||||||
|
- gateway_runtime.env.example foi removido
|
||||||
|
- MCP_GATEWAY_* fica no .env do backend
|
||||||
|
- Porta oficial MCP Gateway = 8300
|
||||||
Binary file not shown.
Binary file not shown.
520
Documentacao/Manual_Long_Term_Memory_PT.md
Normal file
520
Documentacao/Manual_Long_Term_Memory_PT.md
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
### Manual de Implementação — Long-Term Memory
|
||||||
|
|
||||||
|
### Conceito
|
||||||
|
|
||||||
|
A Long-Term Memory (LTM) é a capacidade do `agent_framework` de armazenar e recuperar fatos duradouros além da duração de uma sessão de conversa.
|
||||||
|
|
||||||
|
Diferentemente do histórico de mensagens, que normalmente está associado a um `session_id`, a memória de longo prazo é associada à identidade de negócio do usuário ou cliente. Na implementação atual, essa identidade é composta por:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id
|
||||||
|
agent_id
|
||||||
|
customer_key
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso permite que um agente recupere preferências, informações de identidade, projetos e restrições mesmo quando uma nova sessão é criada.
|
||||||
|
|
||||||
|
### Para que serve
|
||||||
|
|
||||||
|
A Long-Term Memory serve para:
|
||||||
|
|
||||||
|
- manter continuidade entre sessões;
|
||||||
|
- personalizar respostas;
|
||||||
|
- evitar que o usuário repita informações já fornecidas;
|
||||||
|
- reduzir a necessidade de enviar todo o histórico ao modelo;
|
||||||
|
- armazenar preferências, projetos atuais, nomes preferidos e restrições;
|
||||||
|
- isolar a memória entre tenants, agentes e clientes.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Sessão A:
|
||||||
|
"Me chame de Cris. Minha linguagem preferida é Python."
|
||||||
|
|
||||||
|
Sessão B, com outro session_id e o mesmo customer_key:
|
||||||
|
"O que você lembra sobre mim?"
|
||||||
|
|
||||||
|
Resposta esperada:
|
||||||
|
"Seu nome preferido é Cris e sua linguagem preferida é Python."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diferença entre os tipos de memória
|
||||||
|
|
||||||
|
#### Conversation Memory
|
||||||
|
|
||||||
|
Mantém as mensagens da conversa atual e normalmente está associada ao `session_id`.
|
||||||
|
|
||||||
|
#### Summary Memory
|
||||||
|
|
||||||
|
Mantém um resumo da conversa para reduzir o tamanho do contexto enviado ao modelo.
|
||||||
|
|
||||||
|
#### Long-Term Memory
|
||||||
|
|
||||||
|
Mantém fatos duradouros entre sessões e é associada à identidade de negócio, principalmente ao `customer_key`.
|
||||||
|
|
||||||
|
### Componentes da funcionalidade
|
||||||
|
|
||||||
|
#### LongTermMemoryManager
|
||||||
|
|
||||||
|
Responsável por coordenar:
|
||||||
|
|
||||||
|
- carregamento das memórias;
|
||||||
|
- recuperação por identidade;
|
||||||
|
- renderização do contexto;
|
||||||
|
- extração de novos fatos;
|
||||||
|
- persistência dos fatos;
|
||||||
|
- deduplicação e atualização.
|
||||||
|
|
||||||
|
#### LongTermMemoryStore
|
||||||
|
|
||||||
|
Interface de persistência utilizada pelo manager.
|
||||||
|
|
||||||
|
#### SQLiteLongTermMemoryStore
|
||||||
|
|
||||||
|
Implementação de referência baseada em SQLite.
|
||||||
|
|
||||||
|
É apropriada para:
|
||||||
|
|
||||||
|
- desenvolvimento local;
|
||||||
|
- testes;
|
||||||
|
- demonstrações;
|
||||||
|
- ambientes de baixa escala.
|
||||||
|
|
||||||
|
#### InMemoryLongTermMemoryStore
|
||||||
|
|
||||||
|
Implementação em memória utilizada para testes rápidos.
|
||||||
|
|
||||||
|
O conteúdo é perdido quando o processo do backend é encerrado.
|
||||||
|
|
||||||
|
#### LongTermMemoryExtractor
|
||||||
|
|
||||||
|
Responsável por identificar fatos duradouros nas mensagens.
|
||||||
|
|
||||||
|
Exemplos de fatos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preferred_name = Cris
|
||||||
|
preferred_language = Python
|
||||||
|
current_project = Atlas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LongTermMemoryItem
|
||||||
|
|
||||||
|
Modelo que representa um item persistido, incluindo identidade, chave, valor, categoria, confiança e metadados.
|
||||||
|
|
||||||
|
#### AgentRuntime
|
||||||
|
|
||||||
|
Carrega a memória antes da execução do agente e injeta o contexto no prompt.
|
||||||
|
|
||||||
|
#### Nó persist_long_term_memory
|
||||||
|
|
||||||
|
Nó do LangGraph responsável por persistir os fatos após a geração e validação da resposta final.
|
||||||
|
|
||||||
|
### Estrutura dos arquivos
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/
|
||||||
|
└── agent_framework/
|
||||||
|
└── src/
|
||||||
|
└── agent_framework/
|
||||||
|
└── memory/
|
||||||
|
├── __init__.py
|
||||||
|
├── long_term_extractor.py
|
||||||
|
├── long_term_memory.py
|
||||||
|
├── long_term_models.py
|
||||||
|
└── long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fluxo de execução
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mensagem do usuário
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AgentRuntime.prepare_memory_context()
|
||||||
|
│
|
||||||
|
├── Conversation Memory
|
||||||
|
├── Summary Memory
|
||||||
|
└── Long-Term Memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
long_term_memory_context
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Prompt do agente
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agente
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Guardrails / Judges / Supervisor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryExtractor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryStore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuração do framework
|
||||||
|
|
||||||
|
### Novos módulos
|
||||||
|
|
||||||
|
Copie os arquivos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_memory.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_models.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualização de memory/__init__.py
|
||||||
|
|
||||||
|
Exporte os componentes da Long-Term Memory:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.memory.long_term_memory import (
|
||||||
|
LongTermMemoryManager,
|
||||||
|
create_long_term_memory_manager,
|
||||||
|
)
|
||||||
|
from agent_framework.memory.long_term_models import LongTermMemoryItem
|
||||||
|
from agent_framework.memory.long_term_store import (
|
||||||
|
InMemoryLongTermMemoryStore,
|
||||||
|
LongTermMemoryStore,
|
||||||
|
SQLiteLongTermMemoryStore,
|
||||||
|
create_long_term_memory_store,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualização de settings.py
|
||||||
|
|
||||||
|
Adicione as configurações:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_LONG_TERM_MEMORY: bool = False
|
||||||
|
LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
|
||||||
|
LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integração com AgentRuntime
|
||||||
|
|
||||||
|
O runtime deve:
|
||||||
|
|
||||||
|
1. verificar se a funcionalidade está habilitada;
|
||||||
|
2. criar o manager quando necessário;
|
||||||
|
3. recuperar os fatos pela identidade;
|
||||||
|
4. preencher o estado;
|
||||||
|
5. injetar o contexto no prompt.
|
||||||
|
|
||||||
|
Campos adicionados ao estado:
|
||||||
|
|
||||||
|
```python
|
||||||
|
long_term_memories: list[dict]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inicialização no AgentWorkflow
|
||||||
|
|
||||||
|
O manager deve ser criado no `AgentWorkflow`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(
|
||||||
|
settings,
|
||||||
|
telemetry=telemetry,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inicialização correta dos agentes
|
||||||
|
|
||||||
|
O `long_term_memory_manager` não deve ser passado pelo `agent_kwargs` caso os construtores de `BillingAgent`, `ProductAgent`, `OrdersAgent` e `SupportAgent` não declarem esse parâmetro.
|
||||||
|
|
||||||
|
Esta inicialização causa erro:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"settings": settings,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
"long_term_memory_manager": self.long_term_memory_manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Erro resultante:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: BillingAgent.__init__() got an unexpected keyword argument
|
||||||
|
'long_term_memory_manager'
|
||||||
|
```
|
||||||
|
|
||||||
|
A forma recomendada é criar os agentes com a assinatura já existente e injetar o manager como atributo após a inicialização:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"tool_router": getattr(self, "tool_router", None),
|
||||||
|
"rag_service": self.rag_service,
|
||||||
|
"cache": self.cache,
|
||||||
|
"settings": settings,
|
||||||
|
"observer": self.observer,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
self.product = ProductAgent(llm, **agent_kwargs)
|
||||||
|
self.orders = OrdersAgent(llm, **agent_kwargs)
|
||||||
|
self.support = SupportAgent(llm, **agent_kwargs)
|
||||||
|
|
||||||
|
for agent in (
|
||||||
|
self.billing,
|
||||||
|
self.product,
|
||||||
|
self.orders,
|
||||||
|
self.support,
|
||||||
|
):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa abordagem evita alterar os construtores de todos os agentes e mantém a funcionalidade encapsulada no framework.
|
||||||
|
|
||||||
|
### Configuração do LangGraph
|
||||||
|
|
||||||
|
Registre o nó:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self._node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self.persist_long_term_memory,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere o fluxo:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_edge(
|
||||||
|
"supervisor_review",
|
||||||
|
"persist_long_term_memory",
|
||||||
|
)
|
||||||
|
builder.add_edge(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
"persist",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemente o método:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def persist_long_term_memory(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memory_write_result": result,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fluxo final:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_review
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variáveis de ambiente
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caminho do banco SQLite
|
||||||
|
|
||||||
|
O caminho relativo é resolvido a partir do diretório em que o backend é iniciado.
|
||||||
|
|
||||||
|
Para evitar que bancos diferentes sejam criados acidentalmente, prefira um caminho absoluto em ambientes de desenvolvimento:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie a pasta antes de iniciar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Como testar
|
||||||
|
|
||||||
|
### Teste 1 — Gravação
|
||||||
|
|
||||||
|
Envie:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-a",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "Me chame de Cris. Minha linguagem preferida é Python e meu projeto atual se chama Atlas."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste 2 — Recuperação em outra sessão
|
||||||
|
|
||||||
|
Utilize outro `session_id`, mantendo o mesmo `customer_key`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-b",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "O que você lembra sobre mim, minhas preferências e meu projeto?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Seu nome preferido é Cris.
|
||||||
|
Sua linguagem preferida é Python.
|
||||||
|
Seu projeto atual se chama Atlas.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste 3 — Isolamento
|
||||||
|
|
||||||
|
Utilize outro cliente:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-c",
|
||||||
|
"customer_key": "outro-cliente",
|
||||||
|
"message": "Qual é meu nome preferido e qual é meu projeto atual?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Os dados de `11999999999` não devem aparecer.
|
||||||
|
|
||||||
|
### Teste 4 — Reinicialização do frontend
|
||||||
|
|
||||||
|
Reinicie ou resete o frontend e confirme que ele continua enviando o mesmo `customer_key`.
|
||||||
|
|
||||||
|
A memória deve sobreviver à troca do `session_id`. O reset do frontend não apaga o SQLite.
|
||||||
|
|
||||||
|
### Teste 5 — Reinicialização do backend
|
||||||
|
|
||||||
|
Reinicie o Uvicorn e repita a consulta.
|
||||||
|
|
||||||
|
Com:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
a memória deve continuar disponível.
|
||||||
|
|
||||||
|
Com:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
a memória será perdida quando o processo for encerrado.
|
||||||
|
|
||||||
|
### Verificação direta no SQLite
|
||||||
|
|
||||||
|
Localize o banco:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -name "agent_framework.db" -type f
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 ./data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Consulte:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
agent_id,
|
||||||
|
customer_key,
|
||||||
|
memory_type,
|
||||||
|
memory_key,
|
||||||
|
memory_value,
|
||||||
|
confidence,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM agentfw_long_term_memory
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critérios de sucesso
|
||||||
|
|
||||||
|
A implementação está funcionando quando:
|
||||||
|
|
||||||
|
- a memória é recuperada com outro `session_id`;
|
||||||
|
- o mesmo `customer_key` recupera os fatos anteriores;
|
||||||
|
- outro `customer_key` não acessa esses fatos;
|
||||||
|
- reiniciar o frontend não apaga a memória;
|
||||||
|
- reiniciar o backend não apaga a memória quando o provider é SQLite;
|
||||||
|
- o nó `persist_long_term_memory` é executado;
|
||||||
|
- o prompt recebe `long_term_memory_context`.
|
||||||
|
|
||||||
|
### Boas práticas
|
||||||
|
|
||||||
|
- Persistir somente fatos duradouros.
|
||||||
|
- Não armazenar a conversa completa como Long-Term Memory.
|
||||||
|
- Isolar dados por `tenant_id`, `agent_id` e `customer_key`.
|
||||||
|
- Não utilizar `session_id` como identidade permanente do usuário.
|
||||||
|
- Persistir somente depois das validações finais.
|
||||||
|
- Evitar armazenar resultados temporários de ferramentas.
|
||||||
|
- Registrar telemetria de leitura, escrita, atualização e falha.
|
||||||
|
- Definir políticas de retenção e exclusão.
|
||||||
|
- Usar caminho absoluto para SQLite em ambientes com múltiplos diretórios de execução.
|
||||||
|
- Migrar para um banco corporativo em ambientes de produção e alta disponibilidade.
|
||||||
|
|
||||||
|
### Limitações da implementação de referência
|
||||||
|
|
||||||
|
A implementação atual utiliza extração baseada em regras e SQLite como provider de referência.
|
||||||
|
|
||||||
|
Evoluções recomendadas:
|
||||||
|
|
||||||
|
- extração de fatos com LLM;
|
||||||
|
- memória semântica com vetores;
|
||||||
|
- memória episódica;
|
||||||
|
- expiração e versionamento;
|
||||||
|
- deduplicação semântica;
|
||||||
|
- política de consentimento;
|
||||||
|
- API de consulta e exclusão;
|
||||||
|
- provider Oracle Autonomous Database;
|
||||||
|
- criptografia e classificação de dados sensíveis.
|
||||||
@@ -73,3 +73,7 @@ docker compose up --build
|
|||||||
```
|
```
|
||||||
|
|
||||||
No compose, o backend usa `config/mcp_servers.docker.yaml` para apontar para `telecom-mcp` e `retail-mcp`.
|
No compose, o backend usa `config/mcp_servers.docker.yaml` para apontar para `telecom-mcp` e `retail-mcp`.
|
||||||
|
|
||||||
|
## Operações read-only e transacionais
|
||||||
|
|
||||||
|
Use `config/tool_policies.yaml` no backend para classificar somente as operações que precisam de tratamento adicional. A validação é aplicada no roteador central antes do MCP Gateway/Server. O arquivo é opcional e templates antigos continuam usando as políticas já presentes em `tools.yaml`. A configuração completa e o roteiro de migração estão em [README_TOOL_POLICIES.md](README_TOOL_POLICIES.md).
|
||||||
|
|||||||
244
Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md
Normal file
244
Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Route Stickiness Semântica e Controle Global de Sessão no Agent Framework OCI
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
A route stickiness semântica evita executar novamente o Enterprise Router quando uma nova mensagem continua claramente sob responsabilidade do agente ativo. A implementação usa um perfil LLM leve e não contém regexes, listas de frases, palavras específicas de idioma ou regras conversacionais por domínio.
|
||||||
|
|
||||||
|
A funcionalidade é opcional e preserva integralmente o comportamento anterior quando desabilitada, quando não existe agente ativo, quando a confiança é baixa ou quando ocorre erro na inferência.
|
||||||
|
|
||||||
|
## Decisão arquitetural
|
||||||
|
|
||||||
|
O classificador possui uma responsabilidade transversal e restrita:
|
||||||
|
|
||||||
|
- `CONTINUE`: a mensagem continua com o agente ativo;
|
||||||
|
- `ROUTE`: a mensagem deve seguir para o Enterprise Router normal;
|
||||||
|
- `HUMAN_HANDOFF`: o usuário solicitou atendimento humano;
|
||||||
|
- `END_SESSION`: o usuário solicitou ou confirmou o encerramento do atendimento.
|
||||||
|
|
||||||
|
Ele não responde ao usuário, não escolhe outro agente, não executa ferramentas e não interpreta regras de negócio. As duas ações globais são encaminhadas para nós próprios do grafo, evitando que cada agente implemente prompts ou regras de sessão.
|
||||||
|
|
||||||
|
Fluxo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Todos os turnos com a funcionalidade habilitada
|
||||||
|
-> classificador semântico leve
|
||||||
|
CONTINUE + agente ativo -> agente ativo
|
||||||
|
ROUTE/baixa confiança/erro -> Enterprise Router
|
||||||
|
HUMAN_HANDOFF -> nó global human_handoff
|
||||||
|
END_SESSION -> nó global end_session
|
||||||
|
|
||||||
|
No primeiro turno, CONTINUE é normalizado para ROUTE porque ainda não existe agente ativo. Handoff e encerramento podem ser reconhecidos mesmo no primeiro turno.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Por que não há regras determinísticas
|
||||||
|
|
||||||
|
A interpretação de linguagem natural por regex exige manutenção contínua para novas construções, idiomas e domínios. Além disso, transfere aos times dos agentes a responsabilidade de manter flags e padrões de continuidade.
|
||||||
|
|
||||||
|
Esta implementação mantém no código apenas decisões técnicas inevitáveis:
|
||||||
|
|
||||||
|
- funcionalidade habilitada ou desabilitada;
|
||||||
|
- validação de que `CONTINUE` exige agente ativo;
|
||||||
|
- threshold de confiança;
|
||||||
|
- fallback em timeout, erro ou JSON inválido.
|
||||||
|
|
||||||
|
Não existem `DEFAULT_FOLLOWUP_PATTERNS`, regras de repetição, listas de pronomes ou keywords de continuidade.
|
||||||
|
|
||||||
|
## Configuração
|
||||||
|
|
||||||
|
### `.env`
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||||
|
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ENABLE_ROUTE_STICKINESS`: ativa a capacidade.
|
||||||
|
- `ROUTE_STICKINESS_LLM_PROFILE`: perfil existente em `llm_profiles.yaml`.
|
||||||
|
- `ROUTE_STICKINESS_CONFIDENCE_THRESHOLD`: confiança mínima para bypass.
|
||||||
|
- `ROUTE_STICKINESS_HISTORY_TURNS`: quantidade de turnos recentes enviados ao classificador.
|
||||||
|
- `ROUTE_STICKINESS_MAX_TOKENS`: limite de saída do classificador.
|
||||||
|
- `HUMAN_HANDOFF_MESSAGE`: mensagem devolvida pelo nó global de transferência humana.
|
||||||
|
- `END_SESSION_MESSAGE`: mensagem devolvida pelo nó global de encerramento.
|
||||||
|
|
||||||
|
### Perfil leve
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
profiles:
|
||||||
|
route_continuity:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 80
|
||||||
|
timeout_seconds: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
O modelo acima é apenas um exemplo. Deve ser substituído pelo menor modelo aprovado e disponível no ambiente OCI. O framework reutiliza o mecanismo já existente de `LLM_PROFILES_PATH`; não há uma segunda configuração de provider/model específica para a funcionalidade.
|
||||||
|
|
||||||
|
## Contexto enviado ao modelo
|
||||||
|
|
||||||
|
O classificador recebe somente:
|
||||||
|
|
||||||
|
- agente ativo;
|
||||||
|
- descrições das capacidades dos agentes derivadas das intents já existentes;
|
||||||
|
- intent e domínio anteriores;
|
||||||
|
- histórico recente limitado;
|
||||||
|
- mensagem atual.
|
||||||
|
|
||||||
|
Não são enviados RAG completo, resultados MCP integrais, prompt do agente ou regras de negócio.
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
### Continuidade
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário: Qual é o meu plano?
|
||||||
|
Agente: Seu plano é Controle 50GB.
|
||||||
|
Usuário: O que está incluso?
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"method": "continuity",
|
||||||
|
"route": "product_agent",
|
||||||
|
"route_bypassed": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mudança de domínio
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário: Qual é o meu plano?
|
||||||
|
Agente: Seu plano é Controle 50GB.
|
||||||
|
Usuário: Agora quero contestar uma cobrança.
|
||||||
|
```
|
||||||
|
|
||||||
|
O classificador retorna `ROUTE` e o Enterprise Router seleciona o agente apropriado.
|
||||||
|
|
||||||
|
### Baixa confiança ou falha
|
||||||
|
|
||||||
|
Qualquer resultado abaixo do threshold, timeout ou JSON inválido executa o Enterprise Router. A funcionalidade é fail-safe e nunca força continuidade em caso de dúvida.
|
||||||
|
|
||||||
|
## Telemetria
|
||||||
|
|
||||||
|
Evento `router.continuity`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"decision": "CONTINUE",
|
||||||
|
"confidence": 0.97,
|
||||||
|
"active_agent": "product_agent",
|
||||||
|
"route_bypassed": true,
|
||||||
|
"profile_name": "route_continuity"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando ocorre bypass, `route_decision.method` é `continuity` e o estado final contém:
|
||||||
|
|
||||||
|
- `active_agent`;
|
||||||
|
- `route_bypassed`;
|
||||||
|
- `continuity_signal`.
|
||||||
|
|
||||||
|
## Testes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest -q tests/unit/test_semantic_route_stickiness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Os testes validam:
|
||||||
|
|
||||||
|
- continuidade com bypass;
|
||||||
|
- mudança de assunto com fallback para o router;
|
||||||
|
- baixa confiança;
|
||||||
|
- saída inválida;
|
||||||
|
- primeiro turno sem chamada ao classificador.
|
||||||
|
|
||||||
|
## Benchmark recomendado
|
||||||
|
|
||||||
|
Executar a mesma conversação com a funcionalidade desabilitada e habilitada, registrando por turno:
|
||||||
|
|
||||||
|
- `route_bypassed`;
|
||||||
|
- `route_decision.method`;
|
||||||
|
- latência do `llm.route_continuity`;
|
||||||
|
- chamadas ao `llm.router`;
|
||||||
|
- tokens por perfil;
|
||||||
|
- latência total p50, p95 e p99.
|
||||||
|
|
||||||
|
A redução de tempo total somente deve ser atribuída à stickiness quando houver `route_bypassed=true` e ausência da geração `llm.router` no mesmo turno.
|
||||||
|
|
||||||
|
|
||||||
|
## Contratos globais
|
||||||
|
|
||||||
|
### Human handoff
|
||||||
|
|
||||||
|
Quando a decisão for `HUMAN_HANDOFF`, o router retorna:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "human_handoff",
|
||||||
|
"intent": "human_handoff",
|
||||||
|
"method": "continuity",
|
||||||
|
"handoff": true,
|
||||||
|
"metadata": {
|
||||||
|
"session_control": "HUMAN_HANDOFF",
|
||||||
|
"route_bypassed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O nó `human_handoff` produz os campos:
|
||||||
|
|
||||||
|
- `session_control=HUMAN_HANDOFF`;
|
||||||
|
- `human_handoff_requested=true`;
|
||||||
|
- `session_ended=false`;
|
||||||
|
- `next_state=HUMAN_HANDOFF_REQUESTED`.
|
||||||
|
|
||||||
|
O evento `session.human_handoff.requested` é emitido para que o Channel Gateway ou a integração do cliente encaminhe a conversa à plataforma humana. O framework não presume uma fila, fornecedor ou protocolo específico.
|
||||||
|
|
||||||
|
### Encerramento
|
||||||
|
|
||||||
|
Quando a decisão for `END_SESSION`, o router retorna:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "end_session",
|
||||||
|
"intent": "end_session",
|
||||||
|
"method": "continuity",
|
||||||
|
"metadata": {
|
||||||
|
"session_control": "END_SESSION",
|
||||||
|
"route_bypassed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O nó `end_session` produz:
|
||||||
|
|
||||||
|
- `session_control=END_SESSION`;
|
||||||
|
- `session_ended=true`;
|
||||||
|
- `human_handoff_requested=false`;
|
||||||
|
- `next_state=SESSION_ENDED`.
|
||||||
|
|
||||||
|
O evento `session.end.requested` é emitido antes da persistência. O backend continua responsável por aplicar a política concreta de expiração, fechamento ou limpeza da sessão em cada canal.
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
| Mensagem | Contexto | Decisão esperada | Destino |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `o que está incluso?` | `product_agent` ativo | `CONTINUE` | `product_agent` |
|
||||||
|
| `agora quero contestar uma cobrança` | `product_agent` ativo | `ROUTE` | Enterprise Router |
|
||||||
|
| `quero falar com uma pessoa` | com ou sem agente ativo | `HUMAN_HANDOFF` | nó `human_handoff` |
|
||||||
|
| `obrigado, pode encerrar` | com ou sem agente ativo | `END_SESSION` | nó `end_session` |
|
||||||
|
|
||||||
|
## Segurança e fallback
|
||||||
|
|
||||||
|
- Somente decisões acima do threshold são aceitas.
|
||||||
|
- `CONTINUE` sem agente ativo vira `ROUTE`.
|
||||||
|
- JSON inválido, timeout ou erro usa o Enterprise Router.
|
||||||
|
- Handoff e encerramento não executam agentes de domínio nem ferramentas MCP.
|
||||||
|
- O classificador não encerra fisicamente conexões nem seleciona filas humanas; ele emite um contrato global para integração.
|
||||||
86
Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md
Normal file
86
Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Semantic Route Stickiness and Global Session Control in Agent Framework OCI
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This optional capability uses a lightweight LLM profile and no regex, phrase lists, or domain-specific language rules. It classifies each turn as:
|
||||||
|
|
||||||
|
- `CONTINUE`: keep the active agent;
|
||||||
|
- `ROUTE`: run the regular Enterprise Router;
|
||||||
|
- `HUMAN_HANDOFF`: request human assistance;
|
||||||
|
- `END_SESSION`: finish the automated session.
|
||||||
|
|
||||||
|
The classifier does not answer the user, execute tools, or implement domain rules. Human handoff and session ending are handled by global graph nodes.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Incoming turn
|
||||||
|
-> lightweight semantic classifier
|
||||||
|
CONTINUE + active agent -> active agent
|
||||||
|
ROUTE / low confidence / error -> Enterprise Router
|
||||||
|
HUMAN_HANDOFF -> human_handoff node
|
||||||
|
END_SESSION -> end_session node
|
||||||
|
```
|
||||||
|
|
||||||
|
`CONTINUE` is converted to `ROUTE` when there is no active agent. Global session actions can be detected on the first turn.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=I will transfer your request to a person.
|
||||||
|
END_SESSION_MESSAGE=The session has ended. Thank you for contacting us.
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
profiles:
|
||||||
|
route_continuity:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 80
|
||||||
|
timeout_seconds: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the smallest approved model available in the target OCI environment.
|
||||||
|
|
||||||
|
## Human handoff contract
|
||||||
|
|
||||||
|
The router returns route `human_handoff`, intent `human_handoff`, `handoff=true`, and metadata `session_control=HUMAN_HANDOFF`. The graph node sets:
|
||||||
|
|
||||||
|
- `human_handoff_requested=true`;
|
||||||
|
- `session_ended=false`;
|
||||||
|
- `next_state=HUMAN_HANDOFF_REQUESTED`.
|
||||||
|
|
||||||
|
It emits `session.human_handoff.requested`. The customer integration remains responsible for choosing the human queue and protocol.
|
||||||
|
|
||||||
|
## End-session contract
|
||||||
|
|
||||||
|
The router returns route `end_session`, intent `end_session`, and metadata `session_control=END_SESSION`. The graph node sets:
|
||||||
|
|
||||||
|
- `session_ended=true`;
|
||||||
|
- `human_handoff_requested=false`;
|
||||||
|
- `next_state=SESSION_ENDED`.
|
||||||
|
|
||||||
|
It emits `session.end.requested`. Channel-specific session expiration or connection closing remains an integration responsibility.
|
||||||
|
|
||||||
|
## Safety behavior
|
||||||
|
|
||||||
|
- Only decisions above the configured confidence threshold are accepted.
|
||||||
|
- Invalid JSON, timeout, low confidence, or errors fall back to the Enterprise Router.
|
||||||
|
- Human handoff and session ending do not execute domain agents or MCP tools.
|
||||||
|
- The classifier never selects a human queue and never physically closes a channel connection.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src pytest -q tests/unit/test_semantic_route_stickiness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite covers CONTINUE, ROUTE, low confidence, invalid output, HUMAN_HANDOFF, END_SESSION, first-turn global actions, and CONTINUE without an active agent.
|
||||||
90
Documentacao/README_TOOL_POLICIES.md
Normal file
90
Documentacao/README_TOOL_POLICIES.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Políticas mínimas para tools MCP read-only e transacionais
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
O framework diferencia operações de consulta (`read_only`) e operações que alteram estado (`transactional`) imediatamente antes da chamada MCP. Essa classificação não substitui autorização, idempotência ou regras de negócio do servidor MCP; ela acrescenta somente a proteção conversacional mínima, especialmente confirmação explícita.
|
||||||
|
|
||||||
|
## Onde configurar
|
||||||
|
|
||||||
|
A parametrização pertence ao backend da aplicação:
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/agent_template_backend/config/tool_policies.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
A biblioteca compartilhada contém apenas o loader e a validação. O caminho é opcional:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exemplo
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
operation_type: read_only
|
||||||
|
require_confirmation: false
|
||||||
|
|
||||||
|
tool_policies:
|
||||||
|
consultar_plano:
|
||||||
|
operation_type: read_only
|
||||||
|
|
||||||
|
alterar_plano:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
requires: [new_plan_id]
|
||||||
|
```
|
||||||
|
|
||||||
|
Para executar `alterar_plano`, os argumentos precisam conter `new_plan_id` e um booleano literal de confirmação:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"new_plan_id": "CONTROLE_100", "confirmed": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
Também é aceito `"confirmation": true`. Strings como `"true"` não são aceitas como confirmação.
|
||||||
|
|
||||||
|
## Compatibilidade
|
||||||
|
|
||||||
|
- Se `tool_policies.yaml` não existir, o framework continua usando `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
|
||||||
|
- Tools antigas sem política continuam executando como antes.
|
||||||
|
- Uma política explícita no arquivo novo prevalece para `operation_type` e confirmação daquela tool.
|
||||||
|
- O catálogo `tools.yaml` continua sendo a fonte de endpoint, schema, habilitação e cache.
|
||||||
|
- O novo arquivo não deve ser colocado em `libs/agent_framework`, pois as decisões variam por aplicação e domínio.
|
||||||
|
|
||||||
|
## Fluxo de execução
|
||||||
|
|
||||||
|
```text
|
||||||
|
agente -> MCPToolRouter -> validação da política -> mapeamento de parâmetros -> MCP Gateway/Server
|
||||||
|
```
|
||||||
|
|
||||||
|
Uma chamada bloqueada retorna `ok=false`, `metadata.blocked_by_policy=true`, o tipo da operação e a origem da política. O servidor MCP permanece a autoridade final para autenticação, autorização, validação, idempotência e transação de negócio.
|
||||||
|
|
||||||
|
## Migração recomendada
|
||||||
|
|
||||||
|
1. Atualize a biblioteca sem criar o arquivo: o comportamento permanece legado.
|
||||||
|
2. Crie `config/tool_policies.yaml` no backend.
|
||||||
|
3. Cadastre primeiro apenas operações transacionais que exigem confirmação.
|
||||||
|
4. Teste chamadas sem confirmação, com confirmação booleana e com campos obrigatórios ausentes.
|
||||||
|
5. Remova gradualmente duplicações de confirmação de `tools.yaml` quando todos os templates consumidores já usarem a nova configuração.
|
||||||
|
|
||||||
|
|
||||||
|
## Runtime transacional mínimo (correção de amarração)
|
||||||
|
|
||||||
|
A lista `mcp_tools` do roteamento é uma **allowlist**, não uma ordem para executar todas as ferramentas. O runtime agora:
|
||||||
|
|
||||||
|
1. executa automaticamente somente ferramentas `read_only`;
|
||||||
|
2. seleciona no máximo uma ação transacional compatível com o pedido do usuário;
|
||||||
|
3. quando `require_confirmation: true`, persiste `pending_tool_call` e `transaction_status: AWAITING_CONFIRMATION`;
|
||||||
|
4. no turno de confirmação, reutiliza a mesma chamada e executa com `confirmed: true`;
|
||||||
|
5. publica no estado `available_mcp_tools`, `selected_tool_call`, `tool_policy_result`, `confirmation_required` e `confirmation_received`.
|
||||||
|
|
||||||
|
Para o cenário de exemplo, o pedido `123` (ou `PED-ENTREGUE`) retorna `ENTREGUE` no MCP Retail. Use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Quero devolver o pedido 123 porque me arrependi da compra.
|
||||||
|
Sim, confirmo a devolução.
|
||||||
|
```
|
||||||
|
|
||||||
|
O contrato MCP foi padronizado para usar `reason` tanto no catálogo quanto no servidor FastMCP. `tool_policies.yaml` prevalece sobre os campos legados de `tools.yaml`; estes permanecem alinhados nos templates para compatibilidade.
|
||||||
25
Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md
Normal file
25
Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Correção da extração de parâmetros MCP
|
||||||
|
|
||||||
|
## Problema corrigido
|
||||||
|
|
||||||
|
O bloco `extract` do `mcp_parameter_mapping.yaml` existia na configuração e na
|
||||||
|
documentação, mas não era executado pelo runtime. Além disso, valores do
|
||||||
|
Business Context podiam sobrescrever argumentos explícitos, fazendo
|
||||||
|
`contract_key` substituir o `order_id` informado pelo usuário.
|
||||||
|
|
||||||
|
## Correções
|
||||||
|
|
||||||
|
- implementação da extração genérica `strategy: llm` após a escolha da tool;
|
||||||
|
- suporte preservado para `strategy: month_name_pt`;
|
||||||
|
- profile dedicado `mcp_parameter_extraction`;
|
||||||
|
- telemetria `llm.mcp_parameter_extraction`;
|
||||||
|
- `extract` deixou de ser interpretado como mapeamento simples;
|
||||||
|
- argumentos explícitos/extraídos têm precedência sobre Business Context;
|
||||||
|
- remoção de `contract_key: order_id` dos templates;
|
||||||
|
- `order_id` configurado como `string`;
|
||||||
|
- atualização das variantes em `Tuning-Performance`.
|
||||||
|
|
||||||
|
## Resultado esperado
|
||||||
|
|
||||||
|
Para a mensagem `consultar pedido 123`, a chamada MCP deve receber
|
||||||
|
`order_id=123`, mesmo quando o Business Context contém outro `contract_key`.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Correção — mudança de consulta para ação transacional
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
Após `consultar pedido 123`, a mensagem `Quero devolver o pedido 123` podia permanecer no `orders_agent` por route stickiness. Como a intent anterior só expunha tools de consulta, o runtime executava novamente `consultar_pedido` e a resposta direta repetia o status do pedido.
|
||||||
|
|
||||||
|
## Correções
|
||||||
|
|
||||||
|
- Keywords explícitas configuradas no `routing.yaml` podem preemptar a route stickiness quando apontam para outra intent/agente.
|
||||||
|
- `retail_support_exchange_return` passa a ter prioridade maior que `retail_order_tracking` para mensagens de troca/devolução.
|
||||||
|
- Tools transacionais declaram `selection_keywords` no `tools.yaml`.
|
||||||
|
- A resposta direta read-only é bloqueada quando a mensagem contém uma ação transacional registrada, mesmo que a intent anterior ainda esteja ativa.
|
||||||
|
- A seleção da action tool usa configuração, não aliases de domínio fixos no runtime.
|
||||||
|
|
||||||
|
## Fluxo esperado
|
||||||
|
|
||||||
|
1. `consultar pedido 123` → `orders_agent` → `consultar_pedido` → resposta direta.
|
||||||
|
2. `Quero devolver o pedido 123` → preempção da stickiness → `support_agent` / `retail_support_exchange_return`.
|
||||||
|
3. `consultar_pedido` valida o pedido.
|
||||||
|
4. `solicitar_devolucao` é selecionada e, com confirmação obrigatória, gera `AWAITING_CONFIRMATION`.
|
||||||
|
5. `Sim, confirmo` executa a action tool uma única vez.
|
||||||
36
Documentacao/RELEASE_NOTES_TOOL_POLICIES.md
Normal file
36
Documentacao/RELEASE_NOTES_TOOL_POLICIES.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Release notes - políticas read-only/transacionais
|
||||||
|
|
||||||
|
## Alterações
|
||||||
|
|
||||||
|
- Novo `ToolPolicyRegistry` opcional na biblioteca compartilhada.
|
||||||
|
- Validação central no `MCPToolRouter`, inclusive para chamadas diretas.
|
||||||
|
- Tipos mínimos `read_only` e `transactional`.
|
||||||
|
- Confirmação estrita por `confirmed: true` ou `confirmation: true`.
|
||||||
|
- Suporte opcional a campos obrigatórios por política.
|
||||||
|
- Fallback automático para `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
|
||||||
|
- `config/tool_policies.yaml` e variável `TOOL_POLICIES_PATH` nos templates principais, Day Zero e variantes de `Tuning-Performance/Normal` e `Tuning-Performance/Route_Stickness`.
|
||||||
|
- Testes unitários de política e compatibilidade adicionados em `tests/unit/test_tool_policies.py`.
|
||||||
|
|
||||||
|
## Verificações executadas
|
||||||
|
|
||||||
|
- Compilação de `libs`, `templates`, `Tuning-Performance` e `tests`: aprovada.
|
||||||
|
- Validação estrutural dos seis arquivos YAML: aprovada.
|
||||||
|
- Casos isolados do loader (política transacional, confirmação, ausência de arquivo e ausência de cadastro): aprovados.
|
||||||
|
- Renderização dos dois manuais Word atualizados: aprovada, sem cortes ou sobreposição nas páginas adicionadas.
|
||||||
|
|
||||||
|
## Limitação do ambiente de validação
|
||||||
|
|
||||||
|
A suíte `pytest` foi preparada, mas não pôde ser executada integralmente neste ambiente porque `pytest` e as dependências de runtime do projeto não estavam instalados e o acesso ao índice de pacotes expirou. Para reproduzir em um ambiente do projeto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Correção de integração backend/MCP
|
||||||
|
- `mcp_tools` passou a ser tratado como allowlist.
|
||||||
|
- Ações não são mais executadas automaticamente junto com consultas.
|
||||||
|
- Confirmação transacional é persistida e retomada no turno seguinte.
|
||||||
|
- Corrigida incompatibilidade `reason`/`motivo` no MCP Retail.
|
||||||
|
- Adicionado pedido entregue determinístico para testes (`123`).
|
||||||
|
- Removida keyword genérica `produto` da intenção Telecom para evitar colisão com devoluções Retail.
|
||||||
|
- Templates Normal e Route_Stickness em `Tuning-Performance` foram sincronizados.
|
||||||
BIN
Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx
Normal file
BIN
Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx
Normal file
Binary file not shown.
35
Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md
Normal file
35
Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Test Results - Semantic Route Stickiness and Global Session Control
|
||||||
|
|
||||||
|
Date: 2026-07-31
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src pytest -q tests/unit/test_semantic_route_stickiness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
```text
|
||||||
|
9 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Covered scenarios
|
||||||
|
|
||||||
|
1. `CONTINUE` bypasses the Enterprise Router.
|
||||||
|
2. `ROUTE` falls back to the Enterprise Router.
|
||||||
|
3. Low-confidence `CONTINUE` falls back safely.
|
||||||
|
4. Invalid model output falls back safely.
|
||||||
|
5. With no active agent, the lightweight classifier can still detect global session actions.
|
||||||
|
6. `HUMAN_HANDOFF` returns the global `human_handoff` route and session-control metadata.
|
||||||
|
7. `END_SESSION` returns the global `end_session` route and session-control metadata.
|
||||||
|
8. Global actions work on the first turn.
|
||||||
|
9. `CONTINUE` without an active agent is normalized to `ROUTE`.
|
||||||
|
|
||||||
|
## Additional validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m compileall -q libs/agent_framework/src templates/agent_template_backend/app
|
||||||
|
```
|
||||||
|
|
||||||
|
Compilation completed successfully.
|
||||||
27
Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md
Normal file
27
Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Validação — integração transacional Agent Template Backend / MCP
|
||||||
|
|
||||||
|
## Correções implementadas
|
||||||
|
|
||||||
|
- `mcp_tools` é tratado como allowlist, não como lista de execução automática.
|
||||||
|
- Tools `read_only` continuam disponíveis para enriquecimento de contexto.
|
||||||
|
- Somente uma tool transacional compatível com a solicitação é selecionada.
|
||||||
|
- `require_confirmation: true` cria `pending_tool_call` e `AWAITING_CONFIRMATION`.
|
||||||
|
- O turno de confirmação executa a chamada pendente com `confirmed: true`.
|
||||||
|
- O estado expõe `selected_tool_call`, `tool_policy_result`, `confirmation_required`, `confirmation_received` e `transaction_status`.
|
||||||
|
- `reason` foi padronizado entre catálogo, mapping e FastMCP Retail.
|
||||||
|
- Pedido `123` e `PED-ENTREGUE` retornam status `ENTREGUE` para testes positivos.
|
||||||
|
- A keyword genérica `produto` foi removida da intenção Telecom para não capturar devoluções Retail.
|
||||||
|
- Templates `Normal` e `Route_Stickness` em `Tuning-Performance` foram atualizados.
|
||||||
|
|
||||||
|
## Teste recomendado
|
||||||
|
|
||||||
|
1. `Quero devolver o pedido 123 porque me arrependi da compra.`
|
||||||
|
2. Esperado: `transaction_status=AWAITING_CONFIRMATION`, sem execução de `solicitar_devolucao`.
|
||||||
|
3. `Sim, confirmo a devolução.`
|
||||||
|
4. Esperado: `transaction_status=COMPLETED` e execução única de `solicitar_devolucao`.
|
||||||
|
|
||||||
|
## Resultado automatizado
|
||||||
|
|
||||||
|
```text
|
||||||
|
7 passed
|
||||||
|
```
|
||||||
288
README.md
288
README.md
@@ -10889,4 +10889,290 @@ A implementação está arquiteturalmente correta quando:
|
|||||||
[ ] o desenvolvedor consegue testar rota antes de testar execução real.
|
[ ] o desenvolvedor consegue testar rota antes de testar execução real.
|
||||||
```
|
```
|
||||||
|
|
||||||
Com esse desenho, adicionar um novo agente não exige reescrever o frontend nem copiar lógica entre backends. O desenvolvedor cria o backend especializado, registra no Agent Gateway e deixa o framework cuidar dos motores transversais.
|
---
|
||||||
|
|
||||||
|
### 33. Tuning-Performance — Extensões padronizadas de desempenho
|
||||||
|
|
||||||
|
A pasta `Tuning-Performance` disponibiliza um conjunto adicional de implementações, configurações e exemplos destinados a melhorar o desempenho, a previsibilidade e a eficiência do Agent Framework.
|
||||||
|
|
||||||
|
Essas funcionalidades não são ativadas automaticamente apenas pela cópia da pasta. Sua adoção exige a integração dos componentes correspondentes, a revisão das configurações do projeto e a adequação das regras de negócio, ferramentas MCP, prompts, estados conversacionais e políticas de execução.
|
||||||
|
|
||||||
|
O objetivo do `Tuning-Performance` é oferecer uma abordagem padronizada para otimizações que normalmente seriam implementadas separadamente em cada agente ou projeto. Com isso, as equipes podem reduzir chamadas desnecessárias a modelos, evitar execuções incorretas de ferramentas, melhorar a continuidade conversacional e estabelecer um comportamento consistente para operações read-only e transacionais.
|
||||||
|
|
||||||
|
#### 33.1. Route Stickiness semântica
|
||||||
|
|
||||||
|
Mantém o agente atual durante mensagens de continuidade, evitando que o roteador completo seja executado novamente em todos os turnos.
|
||||||
|
|
||||||
|
A decisão de continuidade pode classificar a mensagem como:
|
||||||
|
|
||||||
|
* `CONTINUE`: mantém o agente e a intenção atuais;
|
||||||
|
* `ROUTE`: executa um novo roteamento;
|
||||||
|
* `HUMAN_HANDOFF`: encaminha a conversa para atendimento humano;
|
||||||
|
* `END_SESSION`: encerra a sessão conversacional.
|
||||||
|
|
||||||
|
A continuidade é preemptada quando a nova mensagem contém uma intenção explícita que exige outro agente, domínio ou conjunto de ferramentas. Por exemplo, uma conversa iniciada com consulta de pedido pode ser redirecionada para suporte quando o usuário solicitar uma devolução.
|
||||||
|
|
||||||
|
#### 33.2. Preempção por mudança operacional
|
||||||
|
|
||||||
|
Detecta mudanças entre operações consultivas e transacionais, mesmo quando elas pertencem ao mesmo domínio.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
consultar pedido 123
|
||||||
|
→ orders_agent
|
||||||
|
→ consultar_pedido
|
||||||
|
|
||||||
|
quero devolver o pedido 123
|
||||||
|
→ support_agent
|
||||||
|
→ solicitar_devolucao
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa proteção impede que a route stickiness mantenha a conversa em uma intenção read-only quando o usuário iniciou uma ação transacional.
|
||||||
|
|
||||||
|
#### 33.3. Seleção individual de ferramentas read-only
|
||||||
|
|
||||||
|
As ferramentas configuradas na intenção passam a funcionar como uma allowlist, e não como uma lista de execução automática.
|
||||||
|
|
||||||
|
O runtime seleciona apenas a ferramenta compatível com a solicitação atual.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
consultar pedido 123
|
||||||
|
→ consultar_pedido
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
qual é o rastreamento do pedido 123?
|
||||||
|
→ consultar_entrega
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso evita chamadas redundantes a múltiplas ferramentas MCP e reduz latência, carga e volume de processamento.
|
||||||
|
|
||||||
|
#### 33.4. Extração híbrida de parâmetros
|
||||||
|
|
||||||
|
Permite extrair parâmetros das mensagens usando uma estratégia determinística com fallback para LLM.
|
||||||
|
|
||||||
|
As estratégias suportadas são:
|
||||||
|
|
||||||
|
* `regex`: utiliza somente uma expressão regular;
|
||||||
|
* `llm`: utiliza sempre o modelo;
|
||||||
|
* `hybrid`: tenta primeiro a extração determinística e utiliza a LLM apenas quando necessário.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
pattern: '(?i)\b(?:pedido|order)\s*[:#-]?\s*([A-Z0-9-]+)\b'
|
||||||
|
group: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Para uma mensagem como `consultar pedido 123`, o identificador é obtido pelo regex, sem consumo adicional de tokens. A LLM permanece disponível para frases ambíguas ou formatos não reconhecidos.
|
||||||
|
|
||||||
|
#### 33.5. Precedência segura de parâmetros MCP
|
||||||
|
|
||||||
|
Os parâmetros enviados às ferramentas seguem uma precedência padronizada:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. argumento explícito da chamada
|
||||||
|
2. valor extraído da mensagem
|
||||||
|
3. valor semanticamente compatível do Business Context
|
||||||
|
4. valor default
|
||||||
|
```
|
||||||
|
|
||||||
|
Valores extraídos ou fornecidos explicitamente não são sobrescritos por campos genéricos do contexto.
|
||||||
|
|
||||||
|
Essa regra evita, por exemplo, que um `contract_key` seja enviado indevidamente como `order_id`.
|
||||||
|
|
||||||
|
#### 33.6. Políticas para ferramentas read-only e transacionais
|
||||||
|
|
||||||
|
O arquivo `tool_policies.yaml` permite classificar cada ferramenta e definir seus requisitos operacionais.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tool_policies:
|
||||||
|
solicitar_devolucao:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
|
||||||
|
consultar_pedido:
|
||||||
|
operation_type: read_only
|
||||||
|
require_confirmation: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Ferramentas read-only podem ser executadas diretamente. Ferramentas transacionais podem exigir confirmação explícita antes da chamada MCP.
|
||||||
|
|
||||||
|
#### 33.7. Confirmação transacional persistente
|
||||||
|
|
||||||
|
Operações que exigem confirmação são armazenadas como uma chamada pendente.
|
||||||
|
|
||||||
|
O fluxo esperado é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
usuário solicita a operação
|
||||||
|
→ parâmetros são preparados
|
||||||
|
→ pending_tool_call é persistido
|
||||||
|
→ transaction_status = AWAITING_CONFIRMATION
|
||||||
|
→ usuário confirma
|
||||||
|
→ ferramenta MCP é executada
|
||||||
|
```
|
||||||
|
|
||||||
|
A confirmação deve retomar exatamente a operação pendente, preservando ferramenta, parâmetros e contexto.
|
||||||
|
|
||||||
|
Após a conclusão ou o cancelamento, o estado pendente deve ser limpo para impedir reexecuções acidentais.
|
||||||
|
|
||||||
|
#### 33.8. Proteção contra confirmações fora de contexto
|
||||||
|
|
||||||
|
Mensagens como `sim`, `confirmo` ou `pode continuar` somente devem executar uma ferramenta quando existir uma operação pendente.
|
||||||
|
|
||||||
|
Quando não houver `pending_tool_call`, o framework deve informar que não existe nenhuma operação aguardando confirmação, em vez de reutilizar uma transação anterior ou iniciar uma nova ação com base apenas no histórico.
|
||||||
|
|
||||||
|
#### 33.9. Supressão condicional do RAG
|
||||||
|
|
||||||
|
O RAG pode ser ignorado quando os resultados MCP já são suficientes para responder à solicitação.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
consultar pedido 123
|
||||||
|
→ resultado autoritativo obtido pelo MCP
|
||||||
|
→ RAG não executado
|
||||||
|
```
|
||||||
|
|
||||||
|
O RAG continua disponível para perguntas relacionadas a políticas, regras, documentação, prazos, procedimentos ou informações não retornadas pelas ferramentas.
|
||||||
|
|
||||||
|
Essa separação reduz buscas vetoriais desnecessárias e melhora o tempo de resposta.
|
||||||
|
|
||||||
|
#### 33.10. Evidências MCP para o Groundedness Judge
|
||||||
|
|
||||||
|
Os resultados das ferramentas MCP são enviados ao judge de groundedness como evidência factual.
|
||||||
|
|
||||||
|
Isso evita que respostas baseadas em dados reais de ferramentas sejam classificadas incorretamente como alucinação.
|
||||||
|
|
||||||
|
O contexto do judge pode incluir:
|
||||||
|
|
||||||
|
* mensagem do usuário;
|
||||||
|
* resposta final;
|
||||||
|
* resultados MCP;
|
||||||
|
* contexto RAG;
|
||||||
|
* estado transacional;
|
||||||
|
* política aplicada à ferramenta.
|
||||||
|
|
||||||
|
#### 33.11. Execução configurável de Judges
|
||||||
|
|
||||||
|
A execução dos judges pode ser controlada por amostragem.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sample_rate: 0.25
|
||||||
|
always_run_for_transactional: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Nesse cenário:
|
||||||
|
|
||||||
|
* aproximadamente 25% das interações comuns executam judges;
|
||||||
|
* interações transacionais executam judges sempre.
|
||||||
|
|
||||||
|
A identificação transacional considera, entre outros sinais:
|
||||||
|
|
||||||
|
* `transaction_status`;
|
||||||
|
* `tool_policy_result`;
|
||||||
|
* `selected_tool_call`;
|
||||||
|
* `pending_tool_call`;
|
||||||
|
* resultados MCP de ferramentas transacionais.
|
||||||
|
|
||||||
|
Essa abordagem reduz custo e latência sem remover avaliação de fluxos críticos.
|
||||||
|
|
||||||
|
#### 33.12. Respostas diretas para consultas estruturadas
|
||||||
|
|
||||||
|
Consultas simples podem gerar respostas determinísticas a partir do resultado MCP, sem uma nova chamada ao agente LLM.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[OrdersAgent] Pedido 123: status ENTREGUE.
|
||||||
|
Valor total: R$ 349,90.
|
||||||
|
Itens: Livro de Arquitetura de IA; Cabo USB-C.
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa otimização é indicada quando:
|
||||||
|
|
||||||
|
* a ferramenta respondeu com sucesso;
|
||||||
|
* os dados são estruturados;
|
||||||
|
* não há necessidade de raciocínio adicional;
|
||||||
|
* não há combinação complexa de fontes;
|
||||||
|
* a mensagem não solicita uma ação transacional.
|
||||||
|
|
||||||
|
#### 33.13. Respostas diretas para conclusões transacionais
|
||||||
|
|
||||||
|
Resultados estruturados de operações concluídas também podem ser formatados diretamente.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
A solicitação de devolução do pedido 123 foi registrada.
|
||||||
|
|
||||||
|
Protocolo: DEV-2026-001
|
||||||
|
Status: ABERTO
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa opção reduz o uso de LLM apenas para reorganizar informações já fornecidas pela ferramenta.
|
||||||
|
|
||||||
|
#### 33.14. Configurações por projeto
|
||||||
|
|
||||||
|
As otimizações devem ser ajustadas conforme o comportamento esperado de cada projeto.
|
||||||
|
|
||||||
|
Entre os itens que normalmente precisam ser configurados estão:
|
||||||
|
|
||||||
|
* keywords e prioridades das intents;
|
||||||
|
* allowlist e regras de seleção das ferramentas;
|
||||||
|
* expressões regulares de extração;
|
||||||
|
* perfis LLM usados como fallback;
|
||||||
|
* políticas read-only e transacionais;
|
||||||
|
* mensagens de confirmação;
|
||||||
|
* critérios de supressão do RAG;
|
||||||
|
* amostragem dos judges;
|
||||||
|
* respostas diretas;
|
||||||
|
* persistência do estado transacional;
|
||||||
|
* telemetria e eventos;
|
||||||
|
* mecanismos de idempotência no MCP ou no serviço de negócio.
|
||||||
|
|
||||||
|
#### 33.15. Considerações sobre idempotência
|
||||||
|
|
||||||
|
O Agent Framework controla a experiência conversacional, a confirmação e o estado da operação pendente. Entretanto, a proteção definitiva contra operações duplicadas deve ser implementada no serviço MCP ou no serviço de negócio responsável pela transação.
|
||||||
|
|
||||||
|
A necessidade de idempotência deve ser definida por ferramenta. Ela é especialmente importante para ações como:
|
||||||
|
|
||||||
|
* devolução;
|
||||||
|
* troca;
|
||||||
|
* pagamento;
|
||||||
|
* cancelamento;
|
||||||
|
* criação de protocolo;
|
||||||
|
* provisionamento;
|
||||||
|
* operações com efeitos financeiros ou externos.
|
||||||
|
|
||||||
|
O `Tuning-Performance` pode propagar identificadores e informações de contexto, mas a garantia atômica deve permanecer na camada que controla o dado transacional.
|
||||||
|
|
||||||
|
#### 33.16. Benefícios esperados
|
||||||
|
|
||||||
|
A adoção das funcionalidades do `Tuning-Performance` pode proporcionar:
|
||||||
|
|
||||||
|
* menor latência;
|
||||||
|
* menor consumo de tokens;
|
||||||
|
* redução de chamadas LLM;
|
||||||
|
* redução de chamadas MCP redundantes;
|
||||||
|
* menor utilização desnecessária do RAG;
|
||||||
|
* melhor continuidade entre turnos;
|
||||||
|
* maior segurança em operações transacionais;
|
||||||
|
* melhor rastreabilidade;
|
||||||
|
* groundedness baseado em evidências reais;
|
||||||
|
* comportamento consistente entre diferentes agentes e projetos.
|
||||||
|
|
||||||
|
O conteúdo desta pasta deve ser tratado como uma extensão adicional do framework. Sua utilização requer implementação, configuração, testes funcionais e validação das regras de negócio antes da implantação em produção.
|
||||||
|
|||||||
288
README_en.md
288
README_en.md
@@ -10795,4 +10795,290 @@ The implementation is architecturally correct when:
|
|||||||
[ ] the developer can test the route before testing the actual execution.
|
[ ] the developer can test the route before testing the actual execution.
|
||||||
```
|
```
|
||||||
|
|
||||||
With this design, adding a new agent does not require rewriting the frontend or copying logic between backends. The developer creates the specialized backend, registers it in the Agent Gateway, and lets the framework take care of the cross-cutting engines.
|
---
|
||||||
|
|
||||||
|
### 33. Tuning-Performance — Standardized Performance Extensions
|
||||||
|
|
||||||
|
The `Tuning-Performance` folder provides an additional set of implementations, configurations, and examples designed to improve the performance, predictability, and efficiency of the Agent Framework.
|
||||||
|
|
||||||
|
These capabilities are not enabled automatically by copying the folder. Adoption requires integrating the corresponding components, reviewing project configuration, and adapting business rules, MCP tools, prompts, conversational states, and execution policies.
|
||||||
|
|
||||||
|
The purpose of `Tuning-Performance` is to provide a standardized approach to optimizations that would otherwise be implemented separately in each agent or project. It helps teams reduce unnecessary model calls, prevent incorrect tool execution, improve conversational continuity, and establish consistent behavior for read-only and transactional operations.
|
||||||
|
|
||||||
|
#### 33.1. Semantic Route Stickiness
|
||||||
|
|
||||||
|
Keeps the current agent active during follow-up messages, avoiding a full routing operation on every conversational turn.
|
||||||
|
|
||||||
|
The continuity decision may classify the message as:
|
||||||
|
|
||||||
|
* `CONTINUE`: keep the current agent and intent;
|
||||||
|
* `ROUTE`: perform a new routing decision;
|
||||||
|
* `HUMAN_HANDOFF`: transfer the conversation to a human agent;
|
||||||
|
* `END_SESSION`: terminate the conversational session.
|
||||||
|
|
||||||
|
Continuity can be preempted when the new message contains an explicit intent that requires a different agent, domain, or tool set. For example, a conversation that starts with an order inquiry can be redirected to support when the user requests a return.
|
||||||
|
|
||||||
|
#### 33.2. Operational Shift Preemption
|
||||||
|
|
||||||
|
Detects changes between read-only and transactional operations, even when they belong to the same business domain.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
check order 123
|
||||||
|
→ orders_agent
|
||||||
|
→ consultar_pedido
|
||||||
|
|
||||||
|
I want to return order 123
|
||||||
|
→ support_agent
|
||||||
|
→ solicitar_devolucao
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents route stickiness from keeping the conversation in a read-only intent after the user has initiated a transactional action.
|
||||||
|
|
||||||
|
#### 33.3. Individual Read-Only Tool Selection
|
||||||
|
|
||||||
|
Tools configured for an intent are treated as an allowlist instead of an automatic execution list.
|
||||||
|
|
||||||
|
The runtime selects only the tool that matches the current request.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
check order 123
|
||||||
|
→ consultar_pedido
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
where is order 123?
|
||||||
|
→ consultar_entrega
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents redundant MCP calls and reduces latency, load, and processing volume.
|
||||||
|
|
||||||
|
#### 33.4. Hybrid Parameter Extraction
|
||||||
|
|
||||||
|
Parameters can be extracted from the user message using deterministic processing with an LLM fallback.
|
||||||
|
|
||||||
|
Supported strategies include:
|
||||||
|
|
||||||
|
* `regex`: use only a regular expression;
|
||||||
|
* `llm`: always use the model;
|
||||||
|
* `hybrid`: attempt deterministic extraction first and call the LLM only when necessary.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
pattern: '(?i)\b(?:pedido|order)\s*[:#-]?\s*([A-Z0-9-]+)\b'
|
||||||
|
group: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
For a message such as `check order 123`, the identifier is extracted by regex without additional token consumption. The LLM remains available for ambiguous phrases or unsupported formats.
|
||||||
|
|
||||||
|
#### 33.5. Safe MCP Parameter Precedence
|
||||||
|
|
||||||
|
Parameters sent to tools follow a standardized precedence order:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. explicit tool-call argument
|
||||||
|
2. value extracted from the message
|
||||||
|
3. semantically compatible Business Context value
|
||||||
|
4. default value
|
||||||
|
```
|
||||||
|
|
||||||
|
Explicit or extracted values are not overwritten by generic context fields.
|
||||||
|
|
||||||
|
This prevents, for example, a `contract_key` from being incorrectly sent as an `order_id`.
|
||||||
|
|
||||||
|
#### 33.6. Read-Only and Transactional Tool Policies
|
||||||
|
|
||||||
|
The `tool_policies.yaml` file can classify each tool and define its operational requirements.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tool_policies:
|
||||||
|
solicitar_devolucao:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
|
||||||
|
consultar_pedido:
|
||||||
|
operation_type: read_only
|
||||||
|
require_confirmation: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Read-only tools can be executed directly. Transactional tools can require explicit user confirmation before the MCP call is performed.
|
||||||
|
|
||||||
|
#### 33.7. Persistent Transaction Confirmation
|
||||||
|
|
||||||
|
Operations that require confirmation are stored as pending tool calls.
|
||||||
|
|
||||||
|
The expected flow is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
user requests an operation
|
||||||
|
→ parameters are prepared
|
||||||
|
→ pending_tool_call is persisted
|
||||||
|
→ transaction_status = AWAITING_CONFIRMATION
|
||||||
|
→ user confirms
|
||||||
|
→ MCP tool is executed
|
||||||
|
```
|
||||||
|
|
||||||
|
The confirmation resumes the exact pending operation while preserving its tool, parameters, and context.
|
||||||
|
|
||||||
|
After completion or cancellation, the pending state must be cleared to prevent accidental re-execution.
|
||||||
|
|
||||||
|
#### 33.8. Protection Against Out-of-Context Confirmations
|
||||||
|
|
||||||
|
Messages such as `yes`, `confirm`, or `continue` should execute a tool only when a pending operation exists.
|
||||||
|
|
||||||
|
When there is no `pending_tool_call`, the framework should inform the user that no operation is awaiting confirmation instead of reusing a previous transaction or creating a new action from conversation history alone.
|
||||||
|
|
||||||
|
#### 33.9. Conditional RAG Suppression
|
||||||
|
|
||||||
|
RAG can be skipped when MCP results already provide sufficient information to answer the request.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
check order 123
|
||||||
|
→ authoritative result returned by MCP
|
||||||
|
→ RAG is not executed
|
||||||
|
```
|
||||||
|
|
||||||
|
RAG remains available for questions about policies, rules, documentation, deadlines, procedures, or information not returned by the tools.
|
||||||
|
|
||||||
|
This separation reduces unnecessary vector searches and improves response time.
|
||||||
|
|
||||||
|
#### 33.10. MCP Evidence for the Groundedness Judge
|
||||||
|
|
||||||
|
MCP tool results are passed to the groundedness judge as factual evidence.
|
||||||
|
|
||||||
|
This prevents responses based on real tool data from being incorrectly classified as hallucinations.
|
||||||
|
|
||||||
|
The judge context may include:
|
||||||
|
|
||||||
|
* user message;
|
||||||
|
* final answer;
|
||||||
|
* MCP results;
|
||||||
|
* RAG context;
|
||||||
|
* transactional state;
|
||||||
|
* tool policy result.
|
||||||
|
|
||||||
|
#### 33.11. Configurable Judge Execution
|
||||||
|
|
||||||
|
Judge execution can be controlled through sampling.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sample_rate: 0.25
|
||||||
|
always_run_for_transactional: true
|
||||||
|
```
|
||||||
|
|
||||||
|
In this configuration:
|
||||||
|
|
||||||
|
* approximately 25% of regular interactions run judges;
|
||||||
|
* transactional interactions always run judges.
|
||||||
|
|
||||||
|
Transactional detection may consider:
|
||||||
|
|
||||||
|
* `transaction_status`;
|
||||||
|
* `tool_policy_result`;
|
||||||
|
* `selected_tool_call`;
|
||||||
|
* `pending_tool_call`;
|
||||||
|
* MCP results from transactional tools.
|
||||||
|
|
||||||
|
This reduces cost and latency while retaining evaluation for critical flows.
|
||||||
|
|
||||||
|
#### 33.12. Direct Responses for Structured Queries
|
||||||
|
|
||||||
|
Simple queries can generate deterministic responses directly from MCP results without an additional agent LLM call.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[OrdersAgent] Order 123: status DELIVERED.
|
||||||
|
Total amount: $349.90.
|
||||||
|
Items: AI Architecture Book; USB-C Cable.
|
||||||
|
```
|
||||||
|
|
||||||
|
This optimization is appropriate when:
|
||||||
|
|
||||||
|
* the tool completed successfully;
|
||||||
|
* the result is structured;
|
||||||
|
* no additional reasoning is required;
|
||||||
|
* there is no complex combination of sources;
|
||||||
|
* the message is not requesting a transactional action.
|
||||||
|
|
||||||
|
#### 33.13. Direct Responses for Completed Transactions
|
||||||
|
|
||||||
|
Structured transactional results can also be formatted directly.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
The return request for order 123 has been registered.
|
||||||
|
|
||||||
|
Protocol: DEV-2026-001
|
||||||
|
Status: OPEN
|
||||||
|
```
|
||||||
|
|
||||||
|
This avoids using an LLM only to reorganize information that was already returned by the tool.
|
||||||
|
|
||||||
|
#### 33.14. Project-Specific Configuration
|
||||||
|
|
||||||
|
These optimizations must be adapted to the expected behavior of each project.
|
||||||
|
|
||||||
|
Typical configuration items include:
|
||||||
|
|
||||||
|
* intent keywords and priorities;
|
||||||
|
* tool allowlists and selection rules;
|
||||||
|
* parameter extraction patterns;
|
||||||
|
* fallback LLM profiles;
|
||||||
|
* read-only and transactional policies;
|
||||||
|
* confirmation messages;
|
||||||
|
* RAG suppression criteria;
|
||||||
|
* judge sampling;
|
||||||
|
* direct-response templates;
|
||||||
|
* transactional state persistence;
|
||||||
|
* telemetry and events;
|
||||||
|
* idempotency mechanisms in the MCP or business service.
|
||||||
|
|
||||||
|
#### 33.15. Idempotency Considerations
|
||||||
|
|
||||||
|
The Agent Framework controls the conversational experience, confirmation, and pending operation state. However, the definitive protection against duplicate operations should be implemented in the MCP service or in the downstream business service responsible for the transaction.
|
||||||
|
|
||||||
|
Idempotency requirements should be defined per tool. They are especially relevant for operations such as:
|
||||||
|
|
||||||
|
* returns;
|
||||||
|
* exchanges;
|
||||||
|
* payments;
|
||||||
|
* cancellations;
|
||||||
|
* protocol creation;
|
||||||
|
* provisioning;
|
||||||
|
* operations with financial or external side effects.
|
||||||
|
|
||||||
|
`Tuning-Performance` may propagate identifiers and contextual information, but the atomic guarantee should remain in the layer that controls the transactional data.
|
||||||
|
|
||||||
|
#### 33.16. Expected Benefits
|
||||||
|
|
||||||
|
Adopting the `Tuning-Performance` capabilities can provide:
|
||||||
|
|
||||||
|
* lower latency;
|
||||||
|
* lower token consumption;
|
||||||
|
* fewer LLM calls;
|
||||||
|
* fewer redundant MCP calls;
|
||||||
|
* reduced unnecessary RAG usage;
|
||||||
|
* better continuity across conversational turns;
|
||||||
|
* safer transactional operations;
|
||||||
|
* improved traceability;
|
||||||
|
* groundedness based on real evidence;
|
||||||
|
* consistent behavior across agents and projects.
|
||||||
|
|
||||||
|
The content of this folder should be treated as an additional framework extension. Its use requires implementation, configuration, functional testing, and business-rule validation before production deployment.
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
###############################################################################
|
||||||
|
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
|
||||||
|
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
APP_NAME=ai-agent-template
|
||||||
|
APP_ENV=local
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LLM - OCI Generative AI como provider principal
|
||||||
|
###############################################################################
|
||||||
|
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
||||||
|
LLM_PROVIDER=oci_sdk
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_MAX_TOKENS=2048
|
||||||
|
LLM_TIMEOUT_SECONDS=120
|
||||||
|
|
||||||
|
# OCI OpenAI-compatible endpoint
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
|
||||||
|
OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
|
||||||
|
#OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
#OCI_GENAI_API_KEY=
|
||||||
|
#OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
|
||||||
|
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
|
||||||
|
OCI_AUTH_MODE=config_file
|
||||||
|
# OCI SDK / signer / profiles
|
||||||
|
OCI_CONFIG_FILE=~/.oci/config
|
||||||
|
OCI_PROFILE=LATINOAMERICA-Chicago
|
||||||
|
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
|
||||||
|
OCI_REGION=us-chicago-1
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Persistência
|
||||||
|
###############################################################################
|
||||||
|
# Opções: memory, autonomous, mongodb
|
||||||
|
SESSION_REPOSITORY_PROVIDER=autonomous
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=autonomous
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
|
|
||||||
|
# Autonomous Database
|
||||||
|
ADB_USER=admin
|
||||||
|
ADB_PASSWORD=Moniquinha19721972
|
||||||
|
ADB_DSN=oradb23ai_high
|
||||||
|
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
|
||||||
|
ADB_WALLET_PASSWORD=Moniquinha1972
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
|
||||||
|
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||||
|
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
||||||
|
MONGODB_DATABASE=agent_platform
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
ENABLE_REDIS_CACHE=false
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RAG / Vector / Graph
|
||||||
|
###############################################################################
|
||||||
|
VECTOR_STORE_PROVIDER=autonomous
|
||||||
|
GRAPH_STORE_PROVIDER=autonomous
|
||||||
|
RAG_TOP_K=5
|
||||||
|
EMBEDDING_PROVIDER=oci
|
||||||
|
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||||
|
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Observabilidade
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
# Opcional: verbose, compact
|
||||||
|
LANGFUSE_TRACE_MODE=compact
|
||||||
|
# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow
|
||||||
|
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC.
|
||||||
|
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
|
||||||
|
LANGFUSE_IGNORE_HEALTHCHECKS=true
|
||||||
|
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
|
||||||
|
LANGFUSE_HOST=http://localhost:3005
|
||||||
|
ENABLE_OTEL=false
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
|
OTEL_SERVICE_NAME=ai-agent-template
|
||||||
|
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||||
|
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Analytics / Observer corporativo
|
||||||
|
###############################################################################
|
||||||
|
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
# Providers aceitos: oci_streaming,pubsub,noop
|
||||||
|
ANALYTICS_PROVIDERS=oci_streaming
|
||||||
|
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||||
|
AGENT_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=
|
||||||
|
GCP_PROJECT_ID=
|
||||||
|
GCP_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||||
|
# Credencial GCP segue padrão Google:
|
||||||
|
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OCI Streaming
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_OCI_STREAMING=false
|
||||||
|
OCI_STREAM_ENDPOINT=
|
||||||
|
OCI_STREAM_OCID=
|
||||||
|
OCI_STREAM_PARTITION_KEY=agent-events
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Guardrails, Judges, Supervisor
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_INPUT_GUARDRAILS=true
|
||||||
|
ENABLE_OUTPUT_GUARDRAILS=true
|
||||||
|
ENABLE_JUDGES=true
|
||||||
|
ENABLE_SUPERVISOR=true
|
||||||
|
ENABLE_OUTPUT_SUPERVISOR=true
|
||||||
|
ENABLE_PARALLEL_GUARDRAILS=true
|
||||||
|
GUARDRAILS_FAIL_FAST=true
|
||||||
|
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||||
|
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||||
|
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||||
|
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Gateway de canais
|
||||||
|
###############################################################################
|
||||||
|
DEFAULT_CHANNEL=web
|
||||||
|
# embedded = backend may parse simple/native channel payloads.
|
||||||
|
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
|
ENABLE_VOICE_ADAPTER=true
|
||||||
|
ENABLE_WHATSAPP_ADAPTER=true
|
||||||
|
ENABLE_TEXT_ADAPTER=true
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
# ENTERPRISE ROUTING
|
||||||
|
#################################################
|
||||||
|
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
||||||
|
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||||
|
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
||||||
|
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||||
|
ENABLE_LLM_ROUTER=true
|
||||||
|
|
||||||
|
# Semantic route stickiness (optional).
|
||||||
|
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
||||||
|
# There are no regexes or deterministic language rules.
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||||
|
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# MCP / Tools
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||||
|
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||||
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
|
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||||
|
ROUTING_MODE=router
|
||||||
|
|
||||||
|
# Usage/cost accounting
|
||||||
|
USAGE_REPOSITORY_PROVIDER=autonomous
|
||||||
|
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||||
|
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ConversationSummaryMemory / compressão de contexto conversacional
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||||
|
MEMORY_CONTEXT_STRATEGY=summary
|
||||||
|
MEMORY_HISTORY_LIMIT=80
|
||||||
|
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||||
|
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||||
|
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||||
|
MEMORY_SUMMARY_USE_LLM=true
|
||||||
|
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||||
|
MEMORY_INJECT_SUMMARY=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LONG-TERM MEMORY
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
||||||
|
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY agent_framework /agent_framework
|
||||||
|
COPY agent_template_backend /app
|
||||||
|
RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
# Agent Template Backend Enterprise
|
||||||
|
|
||||||
|
Este folder é uma cópia completa do `agent_template_backend`, sem cortes de
|
||||||
|
arquitetura. Ele mantém workflow, router, output supervisor, guardrails,
|
||||||
|
analytics, observer, MCP, memória, checkpoints e configurações.
|
||||||
|
|
||||||
|
A diferença é que a lógica de negócio dos agentes de exemplo foi removida da
|
||||||
|
execução e preservada comentada nos próprios arquivos:
|
||||||
|
|
||||||
|
- `app/agents/billing_agent.py`
|
||||||
|
- `app/agents/product_agent.py`
|
||||||
|
- `app/agents/orders_agent.py`
|
||||||
|
- `app/agents/support_agent.py`
|
||||||
|
|
||||||
|
## O que o desenvolvedor deve alterar
|
||||||
|
|
||||||
|
1. Escolher ou criar um agente em `app/agents/`.
|
||||||
|
2. Implementar o método `run()`.
|
||||||
|
3. Ajustar prompts e tools, se necessário.
|
||||||
|
4. Emitir ICs de negócio relevantes para a jornada.
|
||||||
|
5. Manter NOC/GRL nos pontos operacionais e de guardrails.
|
||||||
|
|
||||||
|
## O que já está integrado
|
||||||
|
|
||||||
|
- `AgentObserver`
|
||||||
|
- `observer.emit_ic()`
|
||||||
|
- `observer.emit_noc()`
|
||||||
|
- `observer.emit_grl()`
|
||||||
|
- `AnalyticsPublisher`
|
||||||
|
- OCI Streaming
|
||||||
|
- GCP Pub/Sub
|
||||||
|
- OutputSupervisor
|
||||||
|
- GuardrailPipeline com suporte a execução paralela/fail-fast no framework
|
||||||
|
- MCP Tool Router
|
||||||
|
- LangGraph
|
||||||
|
- Memory
|
||||||
|
- Checkpoint
|
||||||
|
- Langfuse / OpenTelemetry
|
||||||
|
|
||||||
|
## Exemplos adicionados
|
||||||
|
|
||||||
|
Veja `app/examples/`:
|
||||||
|
|
||||||
|
- `ic_examples.py`
|
||||||
|
- `noc_examples.py`
|
||||||
|
- `grl_examples.py`
|
||||||
|
- `mcp_examples.py`
|
||||||
|
- `observer_examples.py`
|
||||||
|
|
||||||
|
## Convenção rápida
|
||||||
|
|
||||||
|
- IC = evento de negócio / curadoria / informacional.
|
||||||
|
- NOC = evento operacional / saúde técnica.
|
||||||
|
- GRL = evento de guardrail / segurança / validação.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -0,0 +1,129 @@
|
|||||||
|
from app.agents.prompting import apply_agent_profile_prompt
|
||||||
|
from app.agents.runtime import AgentRuntimeMixin
|
||||||
|
|
||||||
|
|
||||||
|
class BillingAgent(AgentRuntimeMixin):
|
||||||
|
name = "billingAgent"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm,
|
||||||
|
telemetry=None,
|
||||||
|
tool_router=None,
|
||||||
|
rag_service=None,
|
||||||
|
cache=None,
|
||||||
|
settings=None,
|
||||||
|
observer=None,
|
||||||
|
memory=None,
|
||||||
|
summary_memory=None,
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.tool_router = tool_router
|
||||||
|
self.rag_service = rag_service
|
||||||
|
self.cache = cache
|
||||||
|
self.settings = settings
|
||||||
|
self.observer = observer
|
||||||
|
self.memory = memory
|
||||||
|
self.summary_memory = summary_memory
|
||||||
|
|
||||||
|
async def run(self, state):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.BILLING_AGENT_STARTED",
|
||||||
|
state,
|
||||||
|
{"business_component": "faturas"},
|
||||||
|
component="agent.billing.start",
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_context = await self._collect_tool_context(state)
|
||||||
|
if tool_context:
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.BILLING_MCP_CONTEXT_COLLECTED",
|
||||||
|
state,
|
||||||
|
{"tool_result_count": len(tool_context)},
|
||||||
|
component="agent.billing.mcp",
|
||||||
|
)
|
||||||
|
|
||||||
|
state["mcp_results"] = tool_context
|
||||||
|
clarification_message = self.transaction_clarification_message(state)
|
||||||
|
if clarification_message:
|
||||||
|
return {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {clarification_message}",
|
||||||
|
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmation_message = self.transaction_confirmation_message(state)
|
||||||
|
if confirmation_message:
|
||||||
|
result = {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
|
||||||
|
"next_state": state.get("next_state"),
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="BillingAgent")
|
||||||
|
if direct_answer:
|
||||||
|
return {
|
||||||
|
"answer": direct_answer,
|
||||||
|
"next_state": state.get("next_state") or "ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||||
|
if rag_metadata.get("enabled"):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.BILLING_RAG_CONTEXT_RETRIEVED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"document_count": rag_metadata.get("document_count"),
|
||||||
|
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
||||||
|
"latency_ms": rag_metadata.get("latency_ms"),
|
||||||
|
},
|
||||||
|
component="agent.billing.rag",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepara ConversationSummaryMemory antes de montar o prompt.
|
||||||
|
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
|
||||||
|
await self.prepare_memory_context(state)
|
||||||
|
|
||||||
|
messages = self.build_messages(
|
||||||
|
state,
|
||||||
|
system_prompt=apply_agent_profile_prompt(
|
||||||
|
state,
|
||||||
|
"Você é um agente especialista em faturas. Responda com clareza, objetividade e sem sugerir ações não solicitadas. Use dados MCP quando disponíveis.",
|
||||||
|
),
|
||||||
|
mcp_results=tool_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
rag_metadata=rag_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = await self._invoke_llm_cached(state, "BillingAgent", messages)
|
||||||
|
result = {
|
||||||
|
"answer": f"[BillingAgent] {answer}",
|
||||||
|
"next_state": "BILLING_ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": rag_metadata,
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.BILLING_AGENT_COMPLETED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"answer_chars": len(result.get("answer") or ""),
|
||||||
|
"has_mcp_results": bool(tool_context),
|
||||||
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
||||||
|
"memory_context": state.get("memory_context_metadata"),
|
||||||
|
},
|
||||||
|
component="agent.billing.completed",
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _collect_tool_context(self, state):
|
||||||
|
return await self._collect_mcp_context(state)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from app.agents.prompting import apply_agent_profile_prompt
|
||||||
|
from app.agents.runtime import AgentRuntimeMixin
|
||||||
|
|
||||||
|
|
||||||
|
class OrdersAgent(AgentRuntimeMixin):
|
||||||
|
name = "orders_agent"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm,
|
||||||
|
telemetry=None,
|
||||||
|
tool_router=None,
|
||||||
|
rag_service=None,
|
||||||
|
cache=None,
|
||||||
|
settings=None,
|
||||||
|
observer=None,
|
||||||
|
memory=None,
|
||||||
|
summary_memory=None,
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.tool_router = tool_router
|
||||||
|
self.rag_service = rag_service
|
||||||
|
self.cache = cache
|
||||||
|
self.settings = settings
|
||||||
|
self.observer = observer
|
||||||
|
self.memory = memory
|
||||||
|
self.summary_memory = summary_memory
|
||||||
|
|
||||||
|
async def run(self, state):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.ORDERS_AGENT_STARTED",
|
||||||
|
state,
|
||||||
|
{"business_component": "pedidos"},
|
||||||
|
component="agent.orders.start",
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_context = await self._collect_tool_context(state)
|
||||||
|
if tool_context:
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.ORDERS_MCP_CONTEXT_COLLECTED",
|
||||||
|
state,
|
||||||
|
{"tool_result_count": len(tool_context)},
|
||||||
|
component="agent.orders.mcp",
|
||||||
|
)
|
||||||
|
|
||||||
|
state["mcp_results"] = tool_context
|
||||||
|
clarification_message = self.transaction_clarification_message(state)
|
||||||
|
if clarification_message:
|
||||||
|
return {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {clarification_message}",
|
||||||
|
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmation_message = self.transaction_confirmation_message(state)
|
||||||
|
if confirmation_message:
|
||||||
|
result = {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
|
||||||
|
"next_state": state.get("next_state"),
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="OrdersAgent")
|
||||||
|
if direct_answer:
|
||||||
|
return {
|
||||||
|
"answer": direct_answer,
|
||||||
|
"next_state": state.get("next_state") or "ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||||
|
if rag_metadata.get("enabled"):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.ORDERS_RAG_CONTEXT_RETRIEVED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"document_count": rag_metadata.get("document_count"),
|
||||||
|
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
||||||
|
"latency_ms": rag_metadata.get("latency_ms"),
|
||||||
|
},
|
||||||
|
component="agent.orders.rag",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepara ConversationSummaryMemory antes de montar o prompt.
|
||||||
|
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
|
||||||
|
await self.prepare_memory_context(state)
|
||||||
|
|
||||||
|
messages = self.build_messages(
|
||||||
|
state,
|
||||||
|
system_prompt=apply_agent_profile_prompt(
|
||||||
|
state,
|
||||||
|
"Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.",
|
||||||
|
),
|
||||||
|
mcp_results=tool_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
rag_metadata=rag_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = await self._invoke_llm_cached(state, "OrdersAgent", messages)
|
||||||
|
result = {
|
||||||
|
"answer": f"[OrdersAgent] {answer}",
|
||||||
|
"next_state": "ORDER_ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": rag_metadata,
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.ORDERS_AGENT_COMPLETED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"answer_chars": len(result.get("answer") or ""),
|
||||||
|
"has_mcp_results": bool(tool_context),
|
||||||
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
||||||
|
"memory_context": state.get("memory_context_metadata"),
|
||||||
|
},
|
||||||
|
component="agent.orders.completed",
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _collect_tool_context(self, state):
|
||||||
|
return await self._collect_mcp_context(state)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from app.agents.prompting import apply_agent_profile_prompt
|
||||||
|
from app.agents.runtime import AgentRuntimeMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ProductAgent(AgentRuntimeMixin):
|
||||||
|
name = "productAgent"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm,
|
||||||
|
telemetry=None,
|
||||||
|
tool_router=None,
|
||||||
|
rag_service=None,
|
||||||
|
cache=None,
|
||||||
|
settings=None,
|
||||||
|
observer=None,
|
||||||
|
memory=None,
|
||||||
|
summary_memory=None,
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.tool_router = tool_router
|
||||||
|
self.rag_service = rag_service
|
||||||
|
self.cache = cache
|
||||||
|
self.settings = settings
|
||||||
|
self.observer = observer
|
||||||
|
self.memory = memory
|
||||||
|
self.summary_memory = summary_memory
|
||||||
|
|
||||||
|
async def run(self, state):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.PRODUCT_AGENT_STARTED",
|
||||||
|
state,
|
||||||
|
{"business_component": "produtos"},
|
||||||
|
component="agent.product.start",
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_context = await self._collect_tool_context(state)
|
||||||
|
if tool_context:
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.PRODUCT_MCP_CONTEXT_COLLECTED",
|
||||||
|
state,
|
||||||
|
{"tool_result_count": len(tool_context)},
|
||||||
|
component="agent.product.mcp",
|
||||||
|
)
|
||||||
|
|
||||||
|
state["mcp_results"] = tool_context
|
||||||
|
clarification_message = self.transaction_clarification_message(state)
|
||||||
|
if clarification_message:
|
||||||
|
return {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {clarification_message}",
|
||||||
|
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmation_message = self.transaction_confirmation_message(state)
|
||||||
|
if confirmation_message:
|
||||||
|
result = {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
|
||||||
|
"next_state": state.get("next_state"),
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="ProductAgent")
|
||||||
|
if direct_answer:
|
||||||
|
return {
|
||||||
|
"answer": direct_answer,
|
||||||
|
"next_state": state.get("next_state") or "ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||||
|
if rag_metadata.get("enabled"):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.PRODUCT_RAG_CONTEXT_RETRIEVED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"document_count": rag_metadata.get("document_count"),
|
||||||
|
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
||||||
|
"latency_ms": rag_metadata.get("latency_ms"),
|
||||||
|
},
|
||||||
|
component="agent.product.rag",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepara ConversationSummaryMemory antes de montar o prompt.
|
||||||
|
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
|
||||||
|
await self.prepare_memory_context(state)
|
||||||
|
|
||||||
|
messages = self.build_messages(
|
||||||
|
state,
|
||||||
|
system_prompt=apply_agent_profile_prompt(
|
||||||
|
state,
|
||||||
|
"Você é um agente especialista em produtos, planos e serviços. Explique sem fazer oferta proativa e sem executar ações sem confirmação. Use dados MCP quando disponíveis.",
|
||||||
|
),
|
||||||
|
mcp_results=tool_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
rag_metadata=rag_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = await self._invoke_llm_cached(state, "ProductAgent", messages)
|
||||||
|
result = {
|
||||||
|
"answer": f"[ProductAgent] {answer}",
|
||||||
|
"next_state": "PRODUCT_ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": rag_metadata,
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.PRODUCT_AGENT_COMPLETED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"answer_chars": len(result.get("answer") or ""),
|
||||||
|
"has_mcp_results": bool(tool_context),
|
||||||
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
||||||
|
"memory_context": state.get("memory_context_metadata"),
|
||||||
|
},
|
||||||
|
component="agent.product.completed",
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _collect_tool_context(self, state):
|
||||||
|
return await self._collect_mcp_context(state)
|
||||||
@@ -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}"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Compatibilidade local do template/backend.
|
||||||
|
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||||
|
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||||
|
|
||||||
|
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from app.agents.prompting import apply_agent_profile_prompt
|
||||||
|
from app.agents.runtime import AgentRuntimeMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SupportAgent(AgentRuntimeMixin):
|
||||||
|
name = "support_agent"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm,
|
||||||
|
telemetry=None,
|
||||||
|
tool_router=None,
|
||||||
|
rag_service=None,
|
||||||
|
cache=None,
|
||||||
|
settings=None,
|
||||||
|
observer=None,
|
||||||
|
memory=None,
|
||||||
|
summary_memory=None,
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.tool_router = tool_router
|
||||||
|
self.rag_service = rag_service
|
||||||
|
self.cache = cache
|
||||||
|
self.settings = settings
|
||||||
|
self.observer = observer
|
||||||
|
self.memory = memory
|
||||||
|
self.summary_memory = summary_memory
|
||||||
|
|
||||||
|
async def run(self, state):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_AGENT_STARTED",
|
||||||
|
state,
|
||||||
|
{"business_component": "suporte"},
|
||||||
|
component="agent.support.start",
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_context = await self._collect_tool_context(state)
|
||||||
|
if tool_context:
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_MCP_CONTEXT_COLLECTED",
|
||||||
|
state,
|
||||||
|
{"tool_result_count": len(tool_context)},
|
||||||
|
component="agent.support.mcp",
|
||||||
|
)
|
||||||
|
|
||||||
|
state["mcp_results"] = tool_context
|
||||||
|
clarification_message = self.transaction_clarification_message(state)
|
||||||
|
if clarification_message:
|
||||||
|
return {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {clarification_message}",
|
||||||
|
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmation_message = self.transaction_confirmation_message(state)
|
||||||
|
if confirmation_message:
|
||||||
|
result = {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
|
||||||
|
"next_state": state.get("next_state"),
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="SupportAgent")
|
||||||
|
if direct_answer:
|
||||||
|
return {
|
||||||
|
"answer": direct_answer,
|
||||||
|
"next_state": state.get("next_state") or "ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||||
|
if rag_metadata.get("enabled"):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_RAG_CONTEXT_RETRIEVED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"document_count": rag_metadata.get("document_count"),
|
||||||
|
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
||||||
|
"latency_ms": rag_metadata.get("latency_ms"),
|
||||||
|
},
|
||||||
|
component="agent.support.rag",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepara ConversationSummaryMemory antes de montar o prompt.
|
||||||
|
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
|
||||||
|
await self.prepare_memory_context(state)
|
||||||
|
|
||||||
|
messages = self.build_messages(
|
||||||
|
state,
|
||||||
|
system_prompt=apply_agent_profile_prompt(
|
||||||
|
state,
|
||||||
|
"Você é um agente de suporte de varejo para troca, devolução e garantia.",
|
||||||
|
),
|
||||||
|
mcp_results=tool_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
rag_metadata=rag_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = await self._invoke_llm_cached(state, "SupportAgent", messages)
|
||||||
|
result = {
|
||||||
|
"answer": f"[SupportAgent] {answer}",
|
||||||
|
"next_state": "SUPPORT_ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": rag_metadata,
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_AGENT_COMPLETED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"answer_chars": len(result.get("answer") or ""),
|
||||||
|
"has_mcp_results": bool(tool_context),
|
||||||
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
||||||
|
"memory_context": state.get("memory_context_metadata"),
|
||||||
|
},
|
||||||
|
component="agent.support.completed",
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _collect_tool_context(self, state):
|
||||||
|
return await self._collect_mcp_context(state)
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from uuid import uuid4
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from agent_framework.channels.base import ChannelResponse
|
||||||
|
from agent_framework.channels.gateway import ChannelGateway
|
||||||
|
from agent_framework.config.agent_registry import AgentProfileRegistry
|
||||||
|
from agent_framework.config.settings import settings
|
||||||
|
from agent_framework.analytics.factory import create_analytics_publisher
|
||||||
|
from agent_framework.observer import configure as configure_global_observer
|
||||||
|
from agent_framework.llm.providers import create_llm
|
||||||
|
from agent_framework.memory.message_history import create_memory
|
||||||
|
from agent_framework.memory.summary_memory import create_conversation_summary_memory
|
||||||
|
from agent_framework.mcp.tool_router import create_mcp_tool_router
|
||||||
|
from agent_framework.models.identity import AgentIdentity
|
||||||
|
from agent_framework.identity import IdentityResolver, BusinessContext
|
||||||
|
from agent_framework.models.session import ChatMessage, SessionContext
|
||||||
|
from agent_framework.observability.telemetry import Telemetry
|
||||||
|
from agent_framework.observability.context import set_observability_context, clear_observability_context
|
||||||
|
from agent_framework.repositories.session_repository import create_session_repository
|
||||||
|
from agent_framework.checkpoints.checkpoint_repository import create_checkpoint_repository
|
||||||
|
from agent_framework.cache.cache import create_cache
|
||||||
|
from agent_framework.billing.usage_repository import create_usage_repository
|
||||||
|
from agent_framework.sse.events import SSEHub
|
||||||
|
from app.workflows.agent_graph import AgentWorkflow
|
||||||
|
from app.observability.telemetry_observer import TelemetryBackedAgentObserver
|
||||||
|
|
||||||
|
logging.basicConfig(level=settings.LOG_LEVEL)
|
||||||
|
logger = logging.getLogger("agent_template_backend")
|
||||||
|
|
||||||
|
app = FastAPI(title="Agent Template Backend FIRST-ready")
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
telemetry = Telemetry(settings)
|
||||||
|
usage_repository = create_usage_repository(settings)
|
||||||
|
llm = create_llm(settings, telemetry=telemetry, usage_repository=usage_repository)
|
||||||
|
memory = create_memory(settings)
|
||||||
|
summary_memory = create_conversation_summary_memory(settings, message_history=memory, llm=llm, telemetry=telemetry)
|
||||||
|
sessions = create_session_repository(settings)
|
||||||
|
checkpoints = create_checkpoint_repository(settings)
|
||||||
|
cache = create_cache(settings, telemetry=telemetry)
|
||||||
|
gateway = ChannelGateway(input_mode=settings.FRAMEWORK_CHANNEL_INPUT_MODE)
|
||||||
|
analytics = create_analytics_publisher(settings)
|
||||||
|
observer = TelemetryBackedAgentObserver(telemetry=telemetry)
|
||||||
|
configure_global_observer({
|
||||||
|
"enabled": getattr(settings, "ENABLE_ANALYTICS", False),
|
||||||
|
"providers": getattr(settings, "ANALYTICS_PROVIDERS", "oci_streaming"),
|
||||||
|
"topic_path": getattr(settings, "GCP_PUBSUB_TOPIC_PATH", None) or getattr(settings, "AGENT_PUBSUB_TOPIC", None),
|
||||||
|
})
|
||||||
|
tool_router = create_mcp_tool_router(settings, telemetry=telemetry)
|
||||||
|
identity_resolver = IdentityResolver.from_yaml(settings.IDENTITY_CONFIG_PATH)
|
||||||
|
agent_profiles = AgentProfileRegistry(settings)
|
||||||
|
sse_hub = SSEHub(settings, telemetry=telemetry)
|
||||||
|
workflow = AgentWorkflow(llm, memory, telemetry, analytics, settings, observer=observer, tool_router=tool_router, summary_memory=summary_memory)
|
||||||
|
|
||||||
|
logger.info("LLM provider carregado: %s", llm.__class__.__name__)
|
||||||
|
logger.info("Langfuse habilitado: %s host=%s", telemetry.is_enabled(), settings.LANGFUSE_HOST)
|
||||||
|
logger.info("Analytics habilitado: %s providers=%s", getattr(settings, "ENABLE_ANALYTICS", False), getattr(settings, "ANALYTICS_PROVIDERS", ""))
|
||||||
|
logger.info("Agentes disponíveis: %s", [p.agent_id for p in agent_profiles.list_profiles()])
|
||||||
|
logger.info("Framework channel input mode: %s", gateway.input_mode)
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def observability_context_middleware(request: Request, call_next):
|
||||||
|
clear_observability_context()
|
||||||
|
request_id = request.headers.get("x-request-id") or str(uuid4())
|
||||||
|
set_observability_context(
|
||||||
|
request_id=request_id,
|
||||||
|
channel=request.headers.get("x-channel") or "http",
|
||||||
|
ura_call_id=request.headers.get("x-ura-call-id"),
|
||||||
|
)
|
||||||
|
started = time.time()
|
||||||
|
try:
|
||||||
|
response = await call_next(request)
|
||||||
|
response.headers["x-request-id"] = request_id
|
||||||
|
await telemetry.event("http.request.completed", {
|
||||||
|
"method": request.method,
|
||||||
|
"path": request.url.path,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"duration_ms": int((time.time() - started) * 1000),
|
||||||
|
}, kind="http")
|
||||||
|
return response
|
||||||
|
except Exception as exc:
|
||||||
|
await telemetry.event("http.request.failed", {
|
||||||
|
"method": request.method,
|
||||||
|
"path": request.url.path,
|
||||||
|
"error": str(exc),
|
||||||
|
"duration_ms": int((time.time() - started) * 1000),
|
||||||
|
}, kind="http")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
clear_observability_context()
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayRequest(BaseModel):
|
||||||
|
channel: str = "web"
|
||||||
|
payload: dict
|
||||||
|
agent_id: str | None = None
|
||||||
|
tenant_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_value(payload: dict, key: str):
|
||||||
|
metadata = payload.get("metadata")
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
return metadata.get(key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_workflow_id(payload: dict) -> str | None:
|
||||||
|
return (
|
||||||
|
payload.get("workflow_id")
|
||||||
|
or payload.get("workflowId")
|
||||||
|
or _metadata_value(payload, "workflow_id")
|
||||||
|
or _metadata_value(payload, "workflowId")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_root_span_name(template: str | None, values: dict) -> str:
|
||||||
|
template = template or "agent.gateway_message"
|
||||||
|
try:
|
||||||
|
return template.format(**{k: v or "unknown" for k, v in values.items()})
|
||||||
|
except Exception:
|
||||||
|
logger.warning("LANGFUSE_ROOT_SPAN_NAME inválido: %s", template)
|
||||||
|
return "agent.gateway_message"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_identity(req: GatewayRequest, msg) -> tuple[AgentIdentity, dict, BusinessContext, list[str]]:
|
||||||
|
payload = req.payload or {}
|
||||||
|
context = dict(msg.context or {})
|
||||||
|
tenant_id = req.tenant_id or payload.get("tenant_id") or context.get("tenant_id") or "default"
|
||||||
|
agent_id = req.agent_id or payload.get("agent_id") or context.get("agent_id") or agent_profiles.default_agent_id
|
||||||
|
profile = agent_profiles.get(agent_id)
|
||||||
|
|
||||||
|
# 1) Identidade técnica do framework: isola tenant/agente/sessão.
|
||||||
|
context.update({"tenant_id": tenant_id, "agent_id": profile.agent_id, "agent_profile": profile.__dict__})
|
||||||
|
identity = AgentIdentity.from_context(context, session_id=msg.session_id)
|
||||||
|
|
||||||
|
# 2) Identidade de negócio: chaves canônicas vindas do front/canal.
|
||||||
|
# Estas chaves são estáveis na sessão e seguem até agentes e MCP Router.
|
||||||
|
previous_business_context = context.get("business_context") or context.get("identity") or {}
|
||||||
|
business_context = identity_resolver.resolve(
|
||||||
|
{**payload, **context},
|
||||||
|
session_id=identity.conversation_key(),
|
||||||
|
previous=previous_business_context,
|
||||||
|
)
|
||||||
|
missing_identity_keys = identity_resolver.validate(business_context)
|
||||||
|
context.update({
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"business_keys": business_context.to_context_dict(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
"conversation_key": identity.conversation_key(),
|
||||||
|
"original_session_id": msg.session_id,
|
||||||
|
})
|
||||||
|
return identity, context, business_context, missing_identity_keys
|
||||||
|
|
||||||
|
|
||||||
|
async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False) -> dict:
|
||||||
|
try:
|
||||||
|
msg = await gateway.normalize(req.channel, req.payload)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||||
|
payload = req.payload or {}
|
||||||
|
identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg)
|
||||||
|
agent_session_id = identity.conversation_key()
|
||||||
|
message_id = payload.get("message_id") or str(uuid4())
|
||||||
|
workflow_id = _extract_workflow_id(payload)
|
||||||
|
set_observability_context(
|
||||||
|
session_id=agent_session_id,
|
||||||
|
user_id=msg.user_id,
|
||||||
|
tenant_id=identity.tenant_id,
|
||||||
|
agent_id=identity.agent_id,
|
||||||
|
channel=msg.channel,
|
||||||
|
message_id=message_id,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
ura_call_id=payload.get("ura_call_id") or normalized_context.get("ura_call_id") or business_context.interaction_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
stream = sse_hub.stream_for(agent_session_id)
|
||||||
|
async with stream.lock:
|
||||||
|
await sse_hub.emit(agent_session_id, "flow.start", {"session_id": agent_session_id, "message_id": message_id, "agent_id": identity.agent_id}) if emit_sse else None
|
||||||
|
|
||||||
|
session = await sessions.get(agent_session_id)
|
||||||
|
if not session:
|
||||||
|
context_fields = {
|
||||||
|
k: v
|
||||||
|
for k, v in normalized_context.items()
|
||||||
|
if k in SessionContext.model_fields
|
||||||
|
and k not in {"tenant_id", "agent_id", "session_id", "user_id", "channel", "channel_id"}
|
||||||
|
}
|
||||||
|
session = SessionContext(
|
||||||
|
tenant_id=identity.tenant_id,
|
||||||
|
agent_id=identity.agent_id,
|
||||||
|
session_id=agent_session_id,
|
||||||
|
user_id=msg.user_id,
|
||||||
|
channel=msg.channel,
|
||||||
|
channel_id=msg.channel_id,
|
||||||
|
**context_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.tenant_id = identity.tenant_id
|
||||||
|
session.agent_id = identity.agent_id
|
||||||
|
session.channel = msg.channel
|
||||||
|
session.channel_id = msg.channel_id or session.channel_id
|
||||||
|
await sessions.upsert(session)
|
||||||
|
session.metadata = {
|
||||||
|
**(session.metadata or {}),
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
"original_context": normalized_context,
|
||||||
|
}
|
||||||
|
await sse_hub.emit(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}) if emit_sse else None
|
||||||
|
|
||||||
|
await memory.append(
|
||||||
|
agent_session_id,
|
||||||
|
ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=msg.text,
|
||||||
|
metadata={
|
||||||
|
**normalized_context,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await sse_hub.emit(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}) if emit_sse else None
|
||||||
|
history = [m.model_dump(mode="json") for m in await memory.list(agent_session_id)]
|
||||||
|
|
||||||
|
cms_input = {
|
||||||
|
"channel": req.channel,
|
||||||
|
"tenant_id": req.tenant_id,
|
||||||
|
"agent_id": req.agent_id,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
trace_context = {
|
||||||
|
"text": msg.text,
|
||||||
|
"channel": msg.channel,
|
||||||
|
"channel_id": msg.channel_id,
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"conversation_key": agent_session_id,
|
||||||
|
"workflow_id": workflow_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
}
|
||||||
|
root_span_name = _format_root_span_name(
|
||||||
|
getattr(settings, "LANGFUSE_ROOT_SPAN_NAME", "agent.gateway_message"),
|
||||||
|
{
|
||||||
|
"workflow_id": workflow_id,
|
||||||
|
"channel": msg.channel,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
root_tags = ["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"]
|
||||||
|
if workflow_id:
|
||||||
|
root_tags.append(f"workflow:{workflow_id}")
|
||||||
|
|
||||||
|
async with telemetry.span(
|
||||||
|
root_span_name,
|
||||||
|
session_id=agent_session_id,
|
||||||
|
user_id=session.user_id,
|
||||||
|
channel=msg.channel,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
input=cms_input,
|
||||||
|
tags=root_tags,
|
||||||
|
_root_span=True,
|
||||||
|
) as root_span:
|
||||||
|
await telemetry.event("gateway.message.received", trace_context)
|
||||||
|
await sse_hub.emit(agent_session_id, "workflow.started", trace_context) if emit_sse else None
|
||||||
|
result = await workflow.ainvoke(
|
||||||
|
{
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"session_id": agent_session_id,
|
||||||
|
"conversation_key": agent_session_id,
|
||||||
|
"workflow_id": workflow_id,
|
||||||
|
"agent_profile": normalized_context["agent_profile"],
|
||||||
|
# Chave estável de LTM. Nunca use session_id como identidade de longo prazo.
|
||||||
|
"long_term_memory_subject_key": business_context.customer_key or session.user_id,
|
||||||
|
"customer_key": business_context.customer_key,
|
||||||
|
"user_id": session.user_id,
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"user_text": msg.text,
|
||||||
|
"history": history,
|
||||||
|
"context": {
|
||||||
|
**normalized_context,
|
||||||
|
"session": session.model_dump(mode="json"),
|
||||||
|
"original_session_id": msg.session_id,
|
||||||
|
"session_id": agent_session_id,
|
||||||
|
"conversation_key": agent_session_id,
|
||||||
|
"workflow_id": workflow_id,
|
||||||
|
"user_id": session.user_id,
|
||||||
|
"channel": msg.channel,
|
||||||
|
"message_id": message_id,
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"business_keys": business_context.to_context_dict(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await checkpoints.put(agent_session_id, {"state": result, "message_id": message_id})
|
||||||
|
await sse_hub.emit(agent_session_id, "workflow.completed", {"session_id": agent_session_id, "route": result.get("route"), "intent": result.get("intent")}) if emit_sse else None
|
||||||
|
|
||||||
|
answer = result.get("final_answer") or result.get("answer") or ""
|
||||||
|
await memory.append(
|
||||||
|
agent_session_id,
|
||||||
|
ChatMessage(
|
||||||
|
role="assistant",
|
||||||
|
content=answer,
|
||||||
|
metadata={
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"message_id": f"assistant-{message_id}",
|
||||||
|
"route": result.get("route"),
|
||||||
|
"intent": result.get("intent"),
|
||||||
|
"route_decision": result.get("route_decision"),
|
||||||
|
"judges": result.get("judge_results"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await telemetry.event(
|
||||||
|
"gateway.message.responded",
|
||||||
|
{
|
||||||
|
"session_id": agent_session_id,
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"route": result.get("route"),
|
||||||
|
"intent": result.get("intent"),
|
||||||
|
"answer_chars": len(answer),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = ChannelResponse(
|
||||||
|
channel=msg.channel,
|
||||||
|
session_id=agent_session_id,
|
||||||
|
text=answer,
|
||||||
|
metadata={
|
||||||
|
"channel_id": msg.channel_id,
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"original_session_id": msg.session_id,
|
||||||
|
"conversation_key": agent_session_id,
|
||||||
|
"workflow_id": workflow_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
"route": result.get("route"),
|
||||||
|
"intent": result.get("intent"),
|
||||||
|
"route_decision": result.get("route_decision"),
|
||||||
|
"domain": result.get("domain"),
|
||||||
|
"mcp_tools": result.get("mcp_tools"),
|
||||||
|
"mcp_results": result.get("mcp_results"),
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
"judges": result.get("judge_results"),
|
||||||
|
"guardrails": result.get("guardrail_decisions"),
|
||||||
|
"long_term_memory": {
|
||||||
|
"subject_key": business_context.customer_key or session.user_id,
|
||||||
|
"loaded": result.get("long_term_memories", []),
|
||||||
|
"context": result.get("long_term_memory_context", ""),
|
||||||
|
"load_error": result.get("long_term_memory_load_error"),
|
||||||
|
"write_result": result.get("long_term_memory_write_result", {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rendered = await gateway.render(response)
|
||||||
|
root_span.set_output(rendered)
|
||||||
|
await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None
|
||||||
|
await sse_hub.emit(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}) if emit_sse else None
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"llm_provider": settings.LLM_PROVIDER,
|
||||||
|
"llm_class": llm.__class__.__name__,
|
||||||
|
"langfuse_enabled": telemetry.is_enabled(),
|
||||||
|
"agents": [p.agent_id for p in agent_profiles.list_profiles()],
|
||||||
|
"default_agent_id": agent_profiles.default_agent_id,
|
||||||
|
"routing_mode": settings.ROUTING_MODE,
|
||||||
|
"sse_enabled": settings.ENABLE_SSE,
|
||||||
|
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
|
||||||
|
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
|
||||||
|
"long_term_memory": {
|
||||||
|
"enabled": getattr(settings, "ENABLE_LONG_TERM_MEMORY", False),
|
||||||
|
"provider": getattr(settings, "LONG_TERM_MEMORY_PROVIDER", None),
|
||||||
|
"sqlite_path": getattr(settings, "LONG_TERM_MEMORY_SQLITE_PATH", None),
|
||||||
|
"table": getattr(settings, "LONG_TERM_MEMORY_TABLE", None),
|
||||||
|
"auto_extract": getattr(settings, "LONG_TERM_MEMORY_AUTO_EXTRACT", None),
|
||||||
|
"inject_context": getattr(settings, "LONG_TERM_MEMORY_INJECT_CONTEXT", None),
|
||||||
|
},
|
||||||
|
"checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER,
|
||||||
|
"usage_repository": settings.USAGE_REPOSITORY_PROVIDER,
|
||||||
|
"identity_config_path": settings.IDENTITY_CONFIG_PATH,
|
||||||
|
"mcp_parameter_mapping_path": settings.MCP_PARAMETER_MAPPING_PATH,
|
||||||
|
"framework_channel_input_mode": settings.FRAMEWORK_CHANNEL_INPUT_MODE,
|
||||||
|
"legacy_channel_gateway_mode": settings.CHANNEL_GATEWAY_MODE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/agents")
|
||||||
|
async def list_agents():
|
||||||
|
return {"default_agent_id": agent_profiles.default_agent_id, "agents": [p.__dict__ for p in agent_profiles.list_profiles()]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/debug/env")
|
||||||
|
async def debug_env():
|
||||||
|
return {
|
||||||
|
"APP_ENV": settings.APP_ENV,
|
||||||
|
"LLM_PROVIDER": settings.LLM_PROVIDER,
|
||||||
|
"ENABLE_LANGFUSE": settings.ENABLE_LANGFUSE,
|
||||||
|
"LANGFUSE_HOST": settings.LANGFUSE_HOST,
|
||||||
|
"TELEMETRY_ENABLED": telemetry.is_enabled(),
|
||||||
|
"SQLITE_DB_PATH": settings.SQLITE_DB_PATH,
|
||||||
|
"SESSION_REPOSITORY_PROVIDER": settings.SESSION_REPOSITORY_PROVIDER,
|
||||||
|
"MEMORY_REPOSITORY_PROVIDER": settings.MEMORY_REPOSITORY_PROVIDER,
|
||||||
|
"CHECKPOINT_REPOSITORY_PROVIDER": settings.CHECKPOINT_REPOSITORY_PROVIDER,
|
||||||
|
"AGENTS_CONFIG_PATH": settings.AGENTS_CONFIG_PATH,
|
||||||
|
"ROUTING_CONFIG_PATH": settings.ROUTING_CONFIG_PATH,
|
||||||
|
"ROUTING_MODE": settings.ROUTING_MODE,
|
||||||
|
"FRAMEWORK_CHANNEL_INPUT_MODE": settings.FRAMEWORK_CHANNEL_INPUT_MODE,
|
||||||
|
"CHANNEL_GATEWAY_MODE": settings.CHANNEL_GATEWAY_MODE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test-llm")
|
||||||
|
async def test_llm():
|
||||||
|
async with telemetry.span("debug.test_llm", input={"message": "Diga apenas OK"}):
|
||||||
|
answer = await llm.ainvoke([
|
||||||
|
{"role": "system", "content": "Responda de forma curta."},
|
||||||
|
{"role": "user", "content": "Diga apenas OK"},
|
||||||
|
])
|
||||||
|
telemetry.flush()
|
||||||
|
return {"provider": llm.__class__.__name__, "answer": answer}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/debug/route")
|
||||||
|
async def debug_route(req: GatewayRequest):
|
||||||
|
msg = await gateway.normalize(req.channel, req.payload)
|
||||||
|
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
|
||||||
|
state = {
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"session_id": msg.session_id or "debug-session",
|
||||||
|
"conversation_key": identity.conversation_key(),
|
||||||
|
"agent_profile": context["agent_profile"],
|
||||||
|
"user_text": msg.text,
|
||||||
|
"sanitized_input": msg.text,
|
||||||
|
"history": [],
|
||||||
|
"context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()},
|
||||||
|
}
|
||||||
|
if settings.ROUTING_MODE == "supervisor":
|
||||||
|
plan = await workflow.supervisor.route_plan(state)
|
||||||
|
return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata}
|
||||||
|
decision = await workflow.router.route(state)
|
||||||
|
data = decision.model_dump(mode="json")
|
||||||
|
data["mode"] = "router"
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/debug/identity")
|
||||||
|
async def debug_identity(req: GatewayRequest):
|
||||||
|
msg = await gateway.normalize(req.channel, req.payload)
|
||||||
|
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
|
||||||
|
return {
|
||||||
|
"technical_identity": {
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"conversation_key": identity.conversation_key(),
|
||||||
|
"original_session_id": msg.session_id,
|
||||||
|
},
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
"context_keys": sorted(context.keys()),
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/debug/usage")
|
||||||
|
async def debug_usage(tenant_id: str | None = None, session_id: str | None = None):
|
||||||
|
return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/debug/mcp/tools")
|
||||||
|
async def debug_mcp_tools():
|
||||||
|
return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/debug/mcp/call/{tool_name}")
|
||||||
|
async def debug_mcp_call(tool_name: str, arguments: dict | None = None):
|
||||||
|
arguments = arguments or {}
|
||||||
|
ctx = arguments.get("business_context") or arguments.get("identity") or {}
|
||||||
|
result = await tool_router.call(
|
||||||
|
tool_name,
|
||||||
|
arguments,
|
||||||
|
business_context=ctx,
|
||||||
|
original_context=arguments,
|
||||||
|
)
|
||||||
|
return result.model_dump(mode="json")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/gateway/message")
|
||||||
|
async def gateway_message(req: GatewayRequest):
|
||||||
|
return await _process_gateway_message(req, emit_sse=False)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/gateway/message/sse")
|
||||||
|
async def gateway_message_sse(req: GatewayRequest):
|
||||||
|
return await _process_gateway_message(req, emit_sse=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gateway/events/{session_id}")
|
||||||
|
async def gateway_events(session_id: str, request: Request):
|
||||||
|
last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0"
|
||||||
|
return StreamingResponse(
|
||||||
|
sse_hub.subscribe(session_id, int(last)),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sessions/{session_id}/messages")
|
||||||
|
async def get_session_messages(session_id: str, limit: int = 50):
|
||||||
|
return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sessions/{session_id}/checkpoint")
|
||||||
|
async def get_session_checkpoint(session_id: str):
|
||||||
|
return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
telemetry.shutdown()
|
||||||
@@ -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.
@@ -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,53 @@
|
|||||||
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class AgentState(TypedDict, total=False):
|
||||||
|
tenant_id: str
|
||||||
|
agent_id: str
|
||||||
|
session_id: str
|
||||||
|
conversation_key: str
|
||||||
|
workflow_id: str
|
||||||
|
agent_profile: dict[str, Any]
|
||||||
|
user_text: str
|
||||||
|
sanitized_input: str
|
||||||
|
route: str
|
||||||
|
intent: str
|
||||||
|
route_decision: dict[str, Any]
|
||||||
|
answer: str
|
||||||
|
final_answer: str
|
||||||
|
history: list[dict[str, Any]]
|
||||||
|
context: dict[str, Any]
|
||||||
|
guardrail_decisions: list[dict[str, Any]]
|
||||||
|
judge_results: list[dict[str, Any]]
|
||||||
|
next_state: str
|
||||||
|
domain: str
|
||||||
|
mcp_tools: list[str]
|
||||||
|
mcp_results: list[dict[str, Any]]
|
||||||
|
available_mcp_tools: list[str]
|
||||||
|
selected_tool_call: dict[str, Any]
|
||||||
|
pending_tool_call: dict[str, Any]
|
||||||
|
transaction_status: str
|
||||||
|
confirmation_required: bool
|
||||||
|
confirmation_received: bool
|
||||||
|
tool_policy_result: dict[str, Any]
|
||||||
|
missing_parameters: list[str]
|
||||||
|
supervisor_plan: dict[str, Any]
|
||||||
|
supervisor_results: list[dict[str, Any]]
|
||||||
|
active_agent: str
|
||||||
|
route_bypassed: bool
|
||||||
|
continuity_signal: dict[str, Any]
|
||||||
|
session_control: str
|
||||||
|
session_ended: bool
|
||||||
|
human_handoff_requested: bool
|
||||||
|
blocked: bool
|
||||||
|
supervisor_action: str
|
||||||
|
supervisor_guidance: str
|
||||||
|
supervisor_attempt: int
|
||||||
|
supervisor_handover_reason: str
|
||||||
|
output_supervisor_results: list[dict[str, Any]]
|
||||||
|
output_guardrails_already_applied: bool
|
||||||
|
long_term_memories: list[dict[str, Any]]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict[str, Any]
|
||||||
|
long_term_memory_subject_key: str
|
||||||
|
long_term_memory_load_error: str
|
||||||
Binary file not shown.
@@ -0,0 +1,887 @@
|
|||||||
|
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
|
||||||
|
from langgraph.graph import END, START, StateGraph
|
||||||
|
|
||||||
|
from agent_framework.guardrails.pipeline import GuardrailPipeline
|
||||||
|
from agent_framework.guardrails.output_supervisor import OutputSupervisor
|
||||||
|
from agent_framework.guardrails.rail_action import RailAction
|
||||||
|
from agent_framework.guardrails.rail_result import RailResult
|
||||||
|
from agent_framework.judges.judge import JudgePipeline
|
||||||
|
from agent_framework.routing.enterprise_router import EnterpriseRouter
|
||||||
|
from agent_framework.supervisor.supervisor import Supervisor
|
||||||
|
from agent_framework.observability.workflow_events import WorkflowTelemetry
|
||||||
|
from agent_framework.observability.guardrail_events import GuardrailTelemetry
|
||||||
|
from agent_framework.observability.judge_events import JudgeTelemetry
|
||||||
|
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
|
||||||
|
from agent_framework.observability.observer import AgentObserver
|
||||||
|
from app.agents.billing_agent import BillingAgent
|
||||||
|
from app.agents.product_agent import ProductAgent
|
||||||
|
from app.agents.orders_agent import OrdersAgent
|
||||||
|
from app.agents.support_agent import SupportAgent
|
||||||
|
from app.state import AgentState
|
||||||
|
from agent_framework.rag.rag_service import RagService
|
||||||
|
from agent_framework.rag.embedding_provider import create_embedding_provider
|
||||||
|
from agent_framework.cache.cache import create_cache
|
||||||
|
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyOutputGuardrailRail:
|
||||||
|
"""Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo.
|
||||||
|
|
||||||
|
O framework antigo retornava decisões allowed=True/False. O OutputSupervisor
|
||||||
|
corporativo trabalha com RailAction (allow/sanitize/retry/block/handover).
|
||||||
|
Este adapter evita reescrever todos os rails agora e mantém compatibilidade.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = "LEGACY_OUTPUT_GUARDRAILS"
|
||||||
|
|
||||||
|
def __init__(self, pipeline: GuardrailPipeline):
|
||||||
|
self.pipeline = pipeline
|
||||||
|
|
||||||
|
async def evaluate(self, candidate: str, context: dict):
|
||||||
|
final, decisions = await self.pipeline.run_output(candidate, context)
|
||||||
|
serialized = [d.model_dump() for d in decisions]
|
||||||
|
|
||||||
|
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
|
||||||
|
if blocked:
|
||||||
|
first = blocked[0]
|
||||||
|
code = (getattr(first, "code", "") or "").upper()
|
||||||
|
action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK
|
||||||
|
return RailResult(
|
||||||
|
code=code or self.code,
|
||||||
|
action=action,
|
||||||
|
reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"),
|
||||||
|
guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."),
|
||||||
|
sanitized_text=final,
|
||||||
|
metadata={"legacy_decisions": serialized},
|
||||||
|
)
|
||||||
|
|
||||||
|
if final != candidate:
|
||||||
|
return RailResult(
|
||||||
|
code=self.code,
|
||||||
|
action=RailAction.SANITIZE,
|
||||||
|
reason="Resposta sanitizada por guardrail de saída legado.",
|
||||||
|
sanitized_text=final,
|
||||||
|
metadata={"legacy_decisions": serialized},
|
||||||
|
)
|
||||||
|
|
||||||
|
return RailResult(
|
||||||
|
code=self.code,
|
||||||
|
action=RailAction.ALLOW,
|
||||||
|
reason="Resposta aprovada pelos guardrails de saída legados.",
|
||||||
|
sanitized_text=final,
|
||||||
|
metadata={"legacy_decisions": serialized},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentWorkflow:
|
||||||
|
"""Workflow principal com dois modos de roteamento.
|
||||||
|
|
||||||
|
Modos suportados por configuração:
|
||||||
|
ROUTING_MODE=router
|
||||||
|
input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails
|
||||||
|
|
||||||
|
ROUTING_MODE=supervisor
|
||||||
|
input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação
|
||||||
|
|
||||||
|
Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None, summary_memory=None):
|
||||||
|
self.llm = llm
|
||||||
|
self.memory = memory
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.analytics = analytics
|
||||||
|
self.observer = observer or AgentObserver(analytics=analytics)
|
||||||
|
self.settings = settings
|
||||||
|
self.tool_router = tool_router
|
||||||
|
self.summary_memory = summary_memory
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry)
|
||||||
|
self.guardrails = GuardrailPipeline(
|
||||||
|
observer=self.observer,
|
||||||
|
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
|
||||||
|
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
||||||
|
)
|
||||||
|
self.output_supervisor_engine = OutputSupervisor(
|
||||||
|
rails=[LegacyOutputGuardrailRail(self.guardrails)],
|
||||||
|
observer=self.observer,
|
||||||
|
max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)),
|
||||||
|
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
|
||||||
|
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
||||||
|
)
|
||||||
|
self.judges = JudgePipeline()
|
||||||
|
self.supervisor = Supervisor()
|
||||||
|
self.workflow_telemetry = WorkflowTelemetry(telemetry)
|
||||||
|
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
|
||||||
|
self.judge_telemetry = JudgeTelemetry(telemetry)
|
||||||
|
self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry)
|
||||||
|
self.cache = create_cache(settings)
|
||||||
|
self.embedding_provider = create_embedding_provider(settings)
|
||||||
|
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
|
||||||
|
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
|
||||||
|
agent_kwargs = {"telemetry": telemetry, "tool_router": getattr(self, "tool_router", None), "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer, "memory": memory, "summary_memory": summary_memory}
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
self.product = ProductAgent(llm, **agent_kwargs)
|
||||||
|
self.orders = OrdersAgent(llm, **agent_kwargs)
|
||||||
|
self.support = SupportAgent(llm, **agent_kwargs)
|
||||||
|
|
||||||
|
# The existing agent constructors intentionally keep their stable API.
|
||||||
|
# Long-term memory is injected as a runtime capability after creation.
|
||||||
|
for agent in (self.billing, self.product, self.orders, self.support):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
self.graph = self._build_graph()
|
||||||
|
|
||||||
|
def _node(self, name, fn):
|
||||||
|
async def _wrapped(state):
|
||||||
|
async with self.langgraph_telemetry.node(name, state):
|
||||||
|
return await fn(state)
|
||||||
|
return _wrapped
|
||||||
|
|
||||||
|
def _build_graph(self):
|
||||||
|
builder = StateGraph(AgentState)
|
||||||
|
builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails))
|
||||||
|
builder.add_node("load_long_term_memory", self._node("load_long_term_memory", self.load_long_term_memory))
|
||||||
|
builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision))
|
||||||
|
builder.add_node("billing_agent", self._node("billing_agent", self.billing_agent))
|
||||||
|
builder.add_node("product_agent", self._node("product_agent", self.product_agent))
|
||||||
|
builder.add_node("orders_agent", self._node("orders_agent", self.orders_agent))
|
||||||
|
builder.add_node("support_agent", self._node("support_agent", self.support_agent))
|
||||||
|
builder.add_node("handoff", self._node("handoff", self.handoff))
|
||||||
|
builder.add_node("human_handoff", self._node("human_handoff", self.human_handoff))
|
||||||
|
builder.add_node("end_session", self._node("end_session", self.end_session))
|
||||||
|
builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent))
|
||||||
|
builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor))
|
||||||
|
builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
|
||||||
|
builder.add_node("judge", self._node("judge", self.judge))
|
||||||
|
builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
|
||||||
|
builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory))
|
||||||
|
builder.add_node("persist", self._node("persist", self.persist))
|
||||||
|
|
||||||
|
builder.add_edge(START, "input_guardrails")
|
||||||
|
builder.add_conditional_edges(
|
||||||
|
"input_guardrails",
|
||||||
|
self._after_input_guardrails,
|
||||||
|
{"blocked": "persist", "continue": "load_long_term_memory"},
|
||||||
|
)
|
||||||
|
builder.add_edge("load_long_term_memory", "routing_decision")
|
||||||
|
builder.add_conditional_edges(
|
||||||
|
"routing_decision",
|
||||||
|
lambda s: s.get("route", "billing_agent"),
|
||||||
|
{
|
||||||
|
"billing_agent": "billing_agent",
|
||||||
|
"product_agent": "product_agent",
|
||||||
|
"orders_agent": "orders_agent",
|
||||||
|
"support_agent": "support_agent",
|
||||||
|
"handoff": "handoff",
|
||||||
|
"human_handoff": "human_handoff",
|
||||||
|
"end_session": "end_session",
|
||||||
|
"supervisor_agent": "supervisor_agent",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
builder.add_edge("billing_agent", "output_supervisor")
|
||||||
|
builder.add_edge("product_agent", "output_supervisor")
|
||||||
|
builder.add_edge("orders_agent", "output_supervisor")
|
||||||
|
builder.add_edge("support_agent", "output_supervisor")
|
||||||
|
builder.add_edge("handoff", "output_supervisor")
|
||||||
|
builder.add_edge("human_handoff", "output_supervisor")
|
||||||
|
builder.add_edge("end_session", "output_supervisor")
|
||||||
|
builder.add_edge("supervisor_agent", "output_supervisor")
|
||||||
|
builder.add_edge("output_supervisor", "output_guardrails")
|
||||||
|
builder.add_edge("output_guardrails", "judge")
|
||||||
|
builder.add_edge("judge", "supervisor_review")
|
||||||
|
builder.add_edge("supervisor_review", "persist_long_term_memory")
|
||||||
|
builder.add_edge("persist_long_term_memory", "persist")
|
||||||
|
builder.add_edge("persist", END)
|
||||||
|
|
||||||
|
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
|
||||||
|
|
||||||
|
def _after_input_guardrails(self, state):
|
||||||
|
return "blocked" if state.get("blocked") else "continue"
|
||||||
|
|
||||||
|
async def input_guardrails(self, state):
|
||||||
|
if state.get("session_ended") is True:
|
||||||
|
answer = str(getattr(
|
||||||
|
self.settings,
|
||||||
|
"SESSION_ALREADY_ENDED_MESSAGE",
|
||||||
|
"Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.",
|
||||||
|
))
|
||||||
|
await self.telemetry.event(
|
||||||
|
"session.message.rejected_after_end",
|
||||||
|
{"session_id": state.get("conversation_key") or state.get("session_id")},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"answer": answer,
|
||||||
|
"final_answer": answer,
|
||||||
|
"blocked": True,
|
||||||
|
"session_control": "END_SESSION",
|
||||||
|
"session_ended": True,
|
||||||
|
"next_state": "SESSION_ENDED",
|
||||||
|
}
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.input_guardrails",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input=state.get("user_text"),
|
||||||
|
):
|
||||||
|
history_texts = [m.get("content", "") for m in state.get("history", [])]
|
||||||
|
await self.observer.emit_grl(
|
||||||
|
"001",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"phase": "input",
|
||||||
|
},
|
||||||
|
component="workflow.input_guardrails.start",
|
||||||
|
)
|
||||||
|
sanitized, decisions = await self.guardrails.run_input(
|
||||||
|
state["user_text"],
|
||||||
|
{
|
||||||
|
**(state.get("context") or {}),
|
||||||
|
"history_texts": history_texts,
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"agent_profile": state.get("agent_profile") or {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for _decision in decisions:
|
||||||
|
await self.guardrail_telemetry.evaluated("input", _decision)
|
||||||
|
await self.observer.emit_grl(
|
||||||
|
"002" if _decision.allowed else "004",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"phase": "input",
|
||||||
|
"rail_code": getattr(_decision, "code", None),
|
||||||
|
"allowed": bool(_decision.allowed),
|
||||||
|
"reason": getattr(_decision, "reason", None),
|
||||||
|
},
|
||||||
|
component="workflow.input_guardrails.decision",
|
||||||
|
)
|
||||||
|
if not _decision.allowed:
|
||||||
|
await self.guardrail_telemetry.blocked("input", _decision)
|
||||||
|
await self.telemetry.event(
|
||||||
|
"guardrails.input.completed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"decisions": [d.model_dump() for d in decisions],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self.observer.emit_grl(
|
||||||
|
"009",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"phase": "input",
|
||||||
|
"blocked": any(not d.allowed for d in decisions),
|
||||||
|
"decision_count": len(decisions),
|
||||||
|
},
|
||||||
|
component="workflow.input_guardrails.final",
|
||||||
|
)
|
||||||
|
if any(not d.allowed for d in decisions):
|
||||||
|
return {
|
||||||
|
"sanitized_input": sanitized,
|
||||||
|
"answer": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||||
|
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||||
|
"guardrail_decisions": [d.model_dump() for d in decisions],
|
||||||
|
"route": "blocked",
|
||||||
|
"blocked": True,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"sanitized_input": sanitized,
|
||||||
|
"guardrail_decisions": [d.model_dump() for d in decisions],
|
||||||
|
"blocked": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def routing_decision(self, state):
|
||||||
|
mode = getattr(self.settings, "ROUTING_MODE", "router")
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.routing_decision",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={
|
||||||
|
"mode": mode,
|
||||||
|
"text": state.get("sanitized_input") or state.get("user_text"),
|
||||||
|
"previous_state": state.get("next_state"),
|
||||||
|
},
|
||||||
|
):
|
||||||
|
if mode == "supervisor":
|
||||||
|
plan = await self.supervisor.route_plan(state)
|
||||||
|
await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence})
|
||||||
|
return {
|
||||||
|
"route": "supervisor_agent",
|
||||||
|
"intent": plan.intent,
|
||||||
|
"supervisor_plan": {
|
||||||
|
"agents": plan.agents,
|
||||||
|
"intent": plan.intent,
|
||||||
|
"confidence": plan.confidence,
|
||||||
|
"reason": plan.reason,
|
||||||
|
"metadata": plan.metadata,
|
||||||
|
},
|
||||||
|
"route_decision": {
|
||||||
|
"route": "supervisor_agent",
|
||||||
|
"agent": "supervisor",
|
||||||
|
"intent": plan.intent,
|
||||||
|
"confidence": plan.confidence,
|
||||||
|
"reason": plan.reason,
|
||||||
|
"method": "supervisor",
|
||||||
|
"metadata": plan.metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
decision = await self.router.route(state)
|
||||||
|
await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence})
|
||||||
|
await self.observer.emit_ic(
|
||||||
|
"ROUTE_SELECTED",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"route": decision.route,
|
||||||
|
"intent": decision.intent,
|
||||||
|
"confidence": decision.confidence,
|
||||||
|
"method": getattr(decision, "method", None),
|
||||||
|
},
|
||||||
|
component="workflow.routing_decision",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"route": decision.route,
|
||||||
|
"intent": decision.intent,
|
||||||
|
"route_decision": decision.model_dump(mode="json"),
|
||||||
|
"domain": decision.domain,
|
||||||
|
"mcp_tools": decision.mcp_tools,
|
||||||
|
"next_state": decision.next_state,
|
||||||
|
"active_agent": decision.agent,
|
||||||
|
"route_bypassed": decision.method == "continuity",
|
||||||
|
"session_control": (decision.metadata or {}).get("session_control", ""),
|
||||||
|
"human_handoff_requested": (decision.metadata or {}).get("session_control") == "HUMAN_HANDOFF",
|
||||||
|
"session_ended": (decision.metadata or {}).get("session_control") == "END_SESSION",
|
||||||
|
"continuity_signal": {
|
||||||
|
"decision": (decision.metadata or {}).get("continuity_decision"),
|
||||||
|
"confidence": decision.confidence if decision.method == "continuity" else None,
|
||||||
|
"reason": decision.reason if decision.method == "continuity" else None,
|
||||||
|
"profile": (decision.metadata or {}).get("continuity_profile"),
|
||||||
|
} if decision.method == "continuity" else {},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def billing_agent(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.agent.billing",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"intent": state.get("intent")},
|
||||||
|
):
|
||||||
|
return await self.billing.run(state)
|
||||||
|
|
||||||
|
async def product_agent(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.agent.product",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"intent": state.get("intent")},
|
||||||
|
):
|
||||||
|
return await self.product.run(state)
|
||||||
|
|
||||||
|
async def orders_agent(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.agent.orders",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"intent": state.get("intent")},
|
||||||
|
):
|
||||||
|
return await self.orders.run(state)
|
||||||
|
|
||||||
|
async def support_agent(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.agent.support",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"intent": state.get("intent")},
|
||||||
|
):
|
||||||
|
return await self.support.run(state)
|
||||||
|
|
||||||
|
async def supervisor_agent(self, state):
|
||||||
|
"""Executa um ou mais agentes no modo supervisor e consolida a resposta.
|
||||||
|
|
||||||
|
Este nó mantém o desenho de supervisor sem obrigar o restante do workflow
|
||||||
|
a conhecer quantos agentes foram acionados. Cada execução especializada
|
||||||
|
recebe o mesmo estado, mas com route/active_agent atualizados.
|
||||||
|
"""
|
||||||
|
plan = state.get("supervisor_plan") or {}
|
||||||
|
agents = plan.get("agents") or ["billing_agent"]
|
||||||
|
handlers = {
|
||||||
|
"billing_agent": self.billing.run,
|
||||||
|
"product_agent": self.product.run,
|
||||||
|
"orders_agent": self.orders.run,
|
||||||
|
"support_agent": self.support.run,
|
||||||
|
}
|
||||||
|
partials = []
|
||||||
|
mcp_results = []
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.supervisor_agent",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"agents": agents, "intent": state.get("intent")},
|
||||||
|
):
|
||||||
|
for agent_name in agents:
|
||||||
|
handler = handlers.get(agent_name)
|
||||||
|
if handler is None:
|
||||||
|
continue
|
||||||
|
child_state = {**state, "route": agent_name, "active_agent": agent_name}
|
||||||
|
result = await handler(child_state)
|
||||||
|
partials.append({"agent": agent_name, "answer": result.get("answer", "")})
|
||||||
|
mcp_results.extend(result.get("mcp_results") or [])
|
||||||
|
|
||||||
|
if len(partials) == 1:
|
||||||
|
answer = partials[0]["answer"]
|
||||||
|
else:
|
||||||
|
joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials)
|
||||||
|
answer = (
|
||||||
|
"[Supervisor] Consolidação de múltiplos agentes acionados.\n"
|
||||||
|
f"{joined}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"answer": answer,
|
||||||
|
"supervisor_results": partials,
|
||||||
|
"mcp_results": mcp_results,
|
||||||
|
"next_state": "SUPERVISOR_ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handoff(self, state):
|
||||||
|
async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")):
|
||||||
|
target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent")
|
||||||
|
answer = (
|
||||||
|
"Vou redirecionar sua solicitação para o especialista correto. "
|
||||||
|
f"Destino sugerido: {target or 'agente especializado'}."
|
||||||
|
)
|
||||||
|
return {"answer": answer}
|
||||||
|
|
||||||
|
async def human_handoff(self, state):
|
||||||
|
session_id = state.get("conversation_key") or state.get("session_id")
|
||||||
|
async with self.telemetry.span("workflow.human_handoff", session_id=session_id):
|
||||||
|
answer = str(getattr(self.settings, "HUMAN_HANDOFF_MESSAGE", "Vou encaminhar seu atendimento para uma pessoa."))
|
||||||
|
await self.telemetry.event(
|
||||||
|
"session.human_handoff.requested",
|
||||||
|
{
|
||||||
|
"session_id": session_id,
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"reason": (state.get("route_decision") or {}).get("reason"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"answer": answer,
|
||||||
|
"session_control": "HUMAN_HANDOFF",
|
||||||
|
"human_handoff_requested": True,
|
||||||
|
"session_ended": False,
|
||||||
|
"next_state": "HUMAN_HANDOFF_REQUESTED",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def end_session(self, state):
|
||||||
|
session_id = state.get("conversation_key") or state.get("session_id")
|
||||||
|
async with self.telemetry.span("workflow.end_session", session_id=session_id):
|
||||||
|
answer = str(getattr(self.settings, "END_SESSION_MESSAGE", "Atendimento encerrado. Obrigado pelo contato."))
|
||||||
|
await self.telemetry.event(
|
||||||
|
"session.end.requested",
|
||||||
|
{
|
||||||
|
"session_id": session_id,
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"reason": (state.get("route_decision") or {}).get("reason"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"answer": answer,
|
||||||
|
"session_control": "END_SESSION",
|
||||||
|
"session_ended": True,
|
||||||
|
"human_handoff_requested": False,
|
||||||
|
"next_state": "SESSION_ENDED",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def output_supervisor(self, state):
|
||||||
|
"""Valida a resposta candidata com o OutputSupervisor corporativo.
|
||||||
|
|
||||||
|
Este nó não substitui o roteador/supervisor multiagente. Ele roda após o
|
||||||
|
agente gerar `answer` e antes dos judges/persistência, produzindo campos
|
||||||
|
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
|
||||||
|
"""
|
||||||
|
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
|
||||||
|
return {
|
||||||
|
"output_guardrails_already_applied": False,
|
||||||
|
"supervisor_action": "disabled",
|
||||||
|
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate = state.get("answer") or ""
|
||||||
|
context = {
|
||||||
|
**(state.get("context") or {}),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"route": state.get("route"),
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
|
||||||
|
}
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.output_supervisor",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input=candidate,
|
||||||
|
):
|
||||||
|
decision = await self.output_supervisor_engine.evaluate(candidate, context)
|
||||||
|
action = decision.action.value
|
||||||
|
await self.telemetry.event(
|
||||||
|
"output_supervisor.completed",
|
||||||
|
{
|
||||||
|
"session_id": context["session_id"],
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"action": action,
|
||||||
|
"approved": decision.approved,
|
||||||
|
"guidance": decision.guidance,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.observer.emit_ic(
|
||||||
|
"IC.OUTPUT_SUPERVISOR_COMPLETED",
|
||||||
|
{
|
||||||
|
"session_id": context["session_id"],
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"route": state.get("route"),
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"action": action,
|
||||||
|
"approved": decision.approved,
|
||||||
|
"result_count": len(decision.results),
|
||||||
|
},
|
||||||
|
component="workflow.output_supervisor",
|
||||||
|
)
|
||||||
|
|
||||||
|
if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
|
||||||
|
final_answer = decision.candidate
|
||||||
|
elif decision.action == RailAction.HANDOVER:
|
||||||
|
final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista."
|
||||||
|
else:
|
||||||
|
final_answer = decision.fallback_message
|
||||||
|
|
||||||
|
return {
|
||||||
|
"answer": final_answer,
|
||||||
|
"final_answer": final_answer,
|
||||||
|
"supervisor_action": action,
|
||||||
|
"supervisor_guidance": decision.guidance,
|
||||||
|
"supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0),
|
||||||
|
"supervisor_handover_reason": decision.handover_reason,
|
||||||
|
"output_supervisor_results": [
|
||||||
|
{
|
||||||
|
"code": r.code,
|
||||||
|
"action": r.action.value,
|
||||||
|
"reason": r.reason,
|
||||||
|
"guidance": r.guidance,
|
||||||
|
"metadata": r.metadata,
|
||||||
|
}
|
||||||
|
for r in decision.results
|
||||||
|
],
|
||||||
|
"output_guardrails_already_applied": True,
|
||||||
|
"guardrail_decisions": state.get("guardrail_decisions", [])
|
||||||
|
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def output_guardrails(self, state):
|
||||||
|
if state.get("output_guardrails_already_applied"):
|
||||||
|
return {"final_answer": state.get("final_answer") or state.get("answer") or ""}
|
||||||
|
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.output_guardrails",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input=state.get("answer"),
|
||||||
|
):
|
||||||
|
await self.observer.emit_grl(
|
||||||
|
"001",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"phase": "output",
|
||||||
|
"route": state.get("route"),
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
},
|
||||||
|
component="workflow.output_guardrails.start",
|
||||||
|
)
|
||||||
|
final, decisions = await self.guardrails.run_output(
|
||||||
|
state["answer"], state.get("context", {})
|
||||||
|
)
|
||||||
|
for _decision in decisions:
|
||||||
|
await self.guardrail_telemetry.evaluated("output", _decision)
|
||||||
|
await self.observer.emit_grl(
|
||||||
|
"002" if _decision.allowed else "004",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"phase": "output",
|
||||||
|
"rail_code": getattr(_decision, "code", None),
|
||||||
|
"allowed": bool(_decision.allowed),
|
||||||
|
"reason": getattr(_decision, "reason", None),
|
||||||
|
},
|
||||||
|
component="workflow.output_guardrails.decision",
|
||||||
|
)
|
||||||
|
if not _decision.allowed:
|
||||||
|
await self.guardrail_telemetry.blocked("output", _decision)
|
||||||
|
await self.telemetry.event(
|
||||||
|
"guardrails.output.completed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"decisions": [d.model_dump() for d in decisions],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self.observer.emit_grl(
|
||||||
|
"009",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"phase": "output",
|
||||||
|
"blocked": any(not d.allowed for d in decisions),
|
||||||
|
"decision_count": len(decisions),
|
||||||
|
},
|
||||||
|
component="workflow.output_guardrails.final",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"final_answer": final,
|
||||||
|
"guardrail_decisions": state.get("guardrail_decisions", [])
|
||||||
|
+ [d.model_dump() for d in decisions],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def judge(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.judge",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"question": state.get("user_text"), "answer": state.get("final_answer")},
|
||||||
|
):
|
||||||
|
judge_context = dict(state.get("context", {}) or {})
|
||||||
|
judge_context["mcp_results"] = state.get("mcp_results", [])
|
||||||
|
judge_context["evidence"] = state.get("mcp_results", []) or judge_context.get("evidence")
|
||||||
|
judge_context["route"] = state.get("route")
|
||||||
|
judge_context["intent"] = state.get("intent")
|
||||||
|
# Judge sampling must see the finalized transaction state. These
|
||||||
|
# fields are populated by the agent/tool runtime before this node.
|
||||||
|
for key in (
|
||||||
|
"transaction_status",
|
||||||
|
"confirmation_required",
|
||||||
|
"confirmation_received",
|
||||||
|
"tool_policy_result",
|
||||||
|
"selected_tool_call",
|
||||||
|
"pending_tool_call",
|
||||||
|
):
|
||||||
|
judge_context[key] = state.get(key)
|
||||||
|
judge_context["transactional_tools"] = [
|
||||||
|
result.get("tool_name")
|
||||||
|
for result in state.get("mcp_results", [])
|
||||||
|
if isinstance(result, dict)
|
||||||
|
and (
|
||||||
|
(result.get("metadata") or {}).get("operation_type") == "transactional"
|
||||||
|
or result.get("awaiting_confirmation")
|
||||||
|
or result.get("transaction_status")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
results = await self.judges.evaluate_all(
|
||||||
|
state["user_text"], state["final_answer"], judge_context
|
||||||
|
)
|
||||||
|
for _result in results:
|
||||||
|
await self.judge_telemetry.evaluated(_result)
|
||||||
|
await self.telemetry.event(
|
||||||
|
"judges.completed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"results": [r.model_dump() for r in results],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"judge_results": [r.model_dump() for r in results]}
|
||||||
|
|
||||||
|
async def supervisor_review(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.supervisor_review",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input=state.get("final_answer"),
|
||||||
|
):
|
||||||
|
ok, answer = await self.supervisor.review(
|
||||||
|
state["final_answer"], state.get("context", {})
|
||||||
|
)
|
||||||
|
await self.telemetry.event(
|
||||||
|
"supervisor.review.completed",
|
||||||
|
{"session_id": state.get("session_id"), "approved": ok},
|
||||||
|
)
|
||||||
|
return {"final_answer": answer if ok else answer}
|
||||||
|
|
||||||
|
async def load_long_term_memory(self, state):
|
||||||
|
"""Carrega LTM antes do roteamento e mantém o resultado no estado.
|
||||||
|
|
||||||
|
A carga explícita evita depender apenas do agente selecionado para realizar
|
||||||
|
a recuperação e facilita o diagnóstico de identidade/namespace.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
memories = await self.long_term_memory_manager.load(state)
|
||||||
|
serialized = []
|
||||||
|
context_lines = []
|
||||||
|
for item in memories or []:
|
||||||
|
if hasattr(item, "model_dump"):
|
||||||
|
data = item.model_dump(mode="json")
|
||||||
|
elif hasattr(item, "__dict__"):
|
||||||
|
data = dict(item.__dict__)
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
data = dict(item)
|
||||||
|
else:
|
||||||
|
data = {"value": str(item)}
|
||||||
|
serialized.append(data)
|
||||||
|
key = data.get("key") or data.get("memory_key") or data.get("category") or "memory"
|
||||||
|
value = data.get("value") or data.get("memory_value")
|
||||||
|
if value not in (None, ""):
|
||||||
|
context_lines.append(f"- {key}: {value}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memories": serialized,
|
||||||
|
"long_term_memory_context": "\n".join(context_lines),
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
await self.telemetry.event(
|
||||||
|
"long_term_memory.load.failed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"subject_key": state.get("long_term_memory_subject_key"),
|
||||||
|
"error": str(exc),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"long_term_memories": [],
|
||||||
|
"long_term_memory_context": "",
|
||||||
|
"long_term_memory_load_error": str(exc),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def persist_long_term_memory(self, state):
|
||||||
|
try:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
await self.telemetry.event(
|
||||||
|
"long_term_memory.persist.completed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"subject_key": state.get("long_term_memory_subject_key"),
|
||||||
|
"result": result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"long_term_memory_write_result": result}
|
||||||
|
except Exception as exc:
|
||||||
|
await self.telemetry.event(
|
||||||
|
"long_term_memory.persist.failed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"subject_key": state.get("long_term_memory_subject_key"),
|
||||||
|
"error": str(exc),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"long_term_memory_write_result": {"saved": 0, "error": str(exc)}}
|
||||||
|
|
||||||
|
async def persist(self, state):
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.persist",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
input={"route": state.get("route"), "intent": state.get("intent")},
|
||||||
|
):
|
||||||
|
await self.observer.emit_ic(
|
||||||
|
"AGENT_COMPLETED",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state["session_id"],
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"route": state.get("route"),
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"route_decision": state.get("route_decision"),
|
||||||
|
"judges": state.get("judge_results", []),
|
||||||
|
"mcp_tools": state.get("mcp_tools", []),
|
||||||
|
"mcp_results": state.get("mcp_results", []),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.observer.emit_noc(
|
||||||
|
"006",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state["session_id"],
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"route": state.get("route"),
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"answer_chars": len(state.get("final_answer") or ""),
|
||||||
|
},
|
||||||
|
component="workflow.persist",
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.telemetry.event(
|
||||||
|
"agent.completed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state["session_id"],
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"route": state.get("route"),
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"answer_chars": len(state.get("final_answer") or ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
async def ainvoke(self, state):
|
||||||
|
thread_id = state.get("conversation_key") or state["session_id"]
|
||||||
|
config = {"configurable": {"thread_id": thread_id}}
|
||||||
|
async with self.telemetry.span(
|
||||||
|
"workflow.langgraph.ainvoke",
|
||||||
|
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||||
|
user_id=state.get("context", {}).get("user_id"),
|
||||||
|
input={"user_text": state.get("user_text")},
|
||||||
|
tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",],
|
||||||
|
):
|
||||||
|
await self.workflow_telemetry.started("agent_workflow", state)
|
||||||
|
await self.observer.emit_noc(
|
||||||
|
"001",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"channel_id": (state.get("context") or {}).get("channel"),
|
||||||
|
"message_id": (state.get("context") or {}).get("message_id"),
|
||||||
|
"ura_call_id": (state.get("context") or {}).get("ura_call_id"),
|
||||||
|
},
|
||||||
|
component="workflow.ainvoke",
|
||||||
|
)
|
||||||
|
await self.observer.emit_ic(
|
||||||
|
"AGENT_STARTED",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"channel_id": (state.get("context") or {}).get("channel"),
|
||||||
|
"message_id": (state.get("context") or {}).get("message_id"),
|
||||||
|
"user_text_chars": len(state.get("user_text") or ""),
|
||||||
|
},
|
||||||
|
component="workflow.ainvoke",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = await self.graph.ainvoke(state, config=config)
|
||||||
|
await self.workflow_telemetry.completed("agent_workflow", result)
|
||||||
|
return result
|
||||||
|
except Exception as exc:
|
||||||
|
await self.workflow_telemetry.failed("agent_workflow", exc)
|
||||||
|
await self.observer.emit_noc(
|
||||||
|
"005",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"error": str(exc),
|
||||||
|
"exception_type": exc.__class__.__name__,
|
||||||
|
},
|
||||||
|
component="workflow.ainvoke",
|
||||||
|
)
|
||||||
|
raise
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
default_agent_id: telecom_contas
|
||||||
|
agents:
|
||||||
|
- agent_id: telecom_contas
|
||||||
|
name: Agente Telecom Contas
|
||||||
|
description: Template de atendimento para faturas, produtos e suporte de telecom.
|
||||||
|
prompt_policy_path: ./config/agents/telecom_contas/prompt_policy.yaml
|
||||||
|
routing_config_path: ./config/routing.yaml
|
||||||
|
guardrails_config_path: ./config/agents/telecom_contas/guardrails.yaml
|
||||||
|
judges_config_path: ./config/agents/telecom_contas/judges.yaml
|
||||||
|
mcp_servers_config_path: ./config/mcp_servers.yaml
|
||||||
|
tools_config_path: ./config/tools.yaml
|
||||||
|
metadata:
|
||||||
|
domain: telecom
|
||||||
|
system_prefix: |
|
||||||
|
Você está executando o agent_template telecom_contas.
|
||||||
|
Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id.
|
||||||
|
Não misture histórico ou decisões de outros agentes.
|
||||||
|
|
||||||
|
- agent_id: retail_orders
|
||||||
|
name: Agente Retail Pedidos
|
||||||
|
description: Template de varejo para pedidos, produtos, troca/devolução e garantia.
|
||||||
|
prompt_policy_path: ./config/agents/retail_orders/prompt_policy.yaml
|
||||||
|
routing_config_path: ./config/routing.yaml
|
||||||
|
guardrails_config_path: ./config/agents/retail_orders/guardrails.yaml
|
||||||
|
judges_config_path: ./config/agents/retail_orders/judges.yaml
|
||||||
|
mcp_servers_config_path: ./config/mcp_servers.yaml
|
||||||
|
tools_config_path: ./config/tools.yaml
|
||||||
|
metadata:
|
||||||
|
domain: retail
|
||||||
|
system_prefix: |
|
||||||
|
Você está executando o agent_template retail_orders.
|
||||||
|
Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id.
|
||||||
|
Não misture histórico ou decisões de outros agentes.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
input:
|
||||||
|
- code: MSK
|
||||||
|
enabled: true
|
||||||
|
- code: VLOOP
|
||||||
|
enabled: true
|
||||||
|
output:
|
||||||
|
- code: REVPREC
|
||||||
|
enabled: true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
judges:
|
||||||
|
- name: response_quality
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.7
|
||||||
|
- name: groundedness
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.6
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
id: retail_orders_prompt_policy
|
||||||
|
version: 1
|
||||||
|
description: Prompt base isolado do agente de varejo/pedidos.
|
||||||
|
system_prefix: |
|
||||||
|
Você é um agente corporativo de varejo especializado em pedidos, entrega, troca, devolução e garantia.
|
||||||
|
Seja claro, objetivo e não use regras de negócio de telecom neste agente.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
input:
|
||||||
|
- code: MSK
|
||||||
|
enabled: true
|
||||||
|
- code: VLOOP
|
||||||
|
enabled: true
|
||||||
|
output:
|
||||||
|
- code: REVPREC
|
||||||
|
enabled: true
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
enabled: true
|
||||||
|
fail_closed: true
|
||||||
|
profile: judge
|
||||||
|
|
||||||
|
judges:
|
||||||
|
- name: response_quality
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.7
|
||||||
|
|
||||||
|
- name: groundedness
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.6
|
||||||
|
|
||||||
|
- name: sentiment
|
||||||
|
enabled: true
|
||||||
|
fail_on_negative: false
|
||||||
|
|
||||||
|
- name: tone
|
||||||
|
enabled: true
|
||||||
|
fail_closed: true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
id: telecom_contas_prompt_policy
|
||||||
|
version: 1
|
||||||
|
description: Prompt base isolado do agente de telecom/contas.
|
||||||
|
system_prefix: |
|
||||||
|
Você é um agente corporativo de atendimento telecom especializado em faturas, produtos, VAS e suporte.
|
||||||
|
Seja claro, objetivo e não prometa execução operacional sem ferramenta ou confirmação válida.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
input:
|
||||||
|
- code: MSK
|
||||||
|
enabled: true
|
||||||
|
- code: VLOOP
|
||||||
|
enabled: true
|
||||||
|
output:
|
||||||
|
- code: REVPREC
|
||||||
|
enabled: true
|
||||||
|
- code: PINJ
|
||||||
|
enabled: true
|
||||||
|
- code: DLEX_OUT
|
||||||
|
enabled: true
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
identity:
|
||||||
|
version: "2"
|
||||||
|
required:
|
||||||
|
- session_key
|
||||||
|
keys:
|
||||||
|
customer_key:
|
||||||
|
description: Cliente/assinante/consumidor canônico.
|
||||||
|
sources:
|
||||||
|
- business_context.customer_key
|
||||||
|
- customer_key
|
||||||
|
- msisdn
|
||||||
|
- customer_id
|
||||||
|
- user_id
|
||||||
|
- ani
|
||||||
|
- from
|
||||||
|
contract_key:
|
||||||
|
description: Contrato, conta, fatura, pedido ou asset principal.
|
||||||
|
sources:
|
||||||
|
- business_context.contract_key
|
||||||
|
- contract_key
|
||||||
|
- invoice_id
|
||||||
|
- current_invoice_number
|
||||||
|
- order_id
|
||||||
|
- pedido_id
|
||||||
|
- asset_id
|
||||||
|
interaction_key:
|
||||||
|
description: Chave externa da interação/call/chat vinda do canal.
|
||||||
|
sources:
|
||||||
|
- business_context.interaction_key
|
||||||
|
- interaction_key
|
||||||
|
- ura_call_id
|
||||||
|
- call_id
|
||||||
|
- message_id
|
||||||
|
account_key:
|
||||||
|
description: Conta de cobrança/conta comercial.
|
||||||
|
sources:
|
||||||
|
- business_context.account_key
|
||||||
|
- account_key
|
||||||
|
- account_id
|
||||||
|
- billing_account_id
|
||||||
|
resource_key:
|
||||||
|
description: Recurso/linha/produto/asset específico.
|
||||||
|
sources:
|
||||||
|
- business_context.resource_key
|
||||||
|
- resource_key
|
||||||
|
- asset_id
|
||||||
|
- product_id
|
||||||
|
- sku
|
||||||
|
session_key:
|
||||||
|
description: Sessão técnica estável já escopada por tenant e agente.
|
||||||
|
sources:
|
||||||
|
- business_context.session_key
|
||||||
|
- session_key
|
||||||
|
- conversation_key
|
||||||
|
- session_id
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
enabled: true
|
||||||
|
fail_closed: true
|
||||||
|
profile: judge
|
||||||
|
judges:
|
||||||
|
- name: response_quality
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.7
|
||||||
|
- name: groundedness
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.6
|
||||||
|
- name: sentiment
|
||||||
|
enabled: true
|
||||||
|
fail_on_negative: false
|
||||||
|
- name: tone
|
||||||
|
enabled: true
|
||||||
|
fail_closed: true
|
||||||
|
sample_rate: 0.25
|
||||||
|
always_run_for_transactional: true
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
mcp_parameter_mapping:
|
||||||
|
defaults:
|
||||||
|
use_mock: true
|
||||||
|
tools:
|
||||||
|
consultar_fatura:
|
||||||
|
map:
|
||||||
|
customer_key: msisdn
|
||||||
|
contract_key: invoice_id
|
||||||
|
interaction_key: ura_call_id
|
||||||
|
session_key: session_id
|
||||||
|
extract:
|
||||||
|
mes_referencia:
|
||||||
|
from: message
|
||||||
|
type: int
|
||||||
|
strategy: month_name_pt
|
||||||
|
description: 'Extrair mês citado na mensagem. janeiro=1, fevereiro=2, março=3,
|
||||||
|
abril=4, maio=5, junho=6, julho=7, agosto=8, setembro=9, outubro=10, novembro=11,
|
||||||
|
dezembro=12.
|
||||||
|
|
||||||
|
'
|
||||||
|
consultar_pagamentos:
|
||||||
|
map:
|
||||||
|
customer_key: msisdn
|
||||||
|
interaction_key: ura_call_id
|
||||||
|
session_key: session_id
|
||||||
|
consultar_plano:
|
||||||
|
map:
|
||||||
|
customer_key: msisdn
|
||||||
|
resource_key: asset_id
|
||||||
|
contract_key: asset_id
|
||||||
|
session_key: session_id
|
||||||
|
listar_servicos:
|
||||||
|
map:
|
||||||
|
customer_key: msisdn
|
||||||
|
session_key: session_id
|
||||||
|
consultar_pedido:
|
||||||
|
map:
|
||||||
|
customer_key: customer_id
|
||||||
|
session_key: session_id
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
description: Extraia somente o identificador do pedido informado explicitamente
|
||||||
|
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||||
|
mensagem.
|
||||||
|
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||||
|
group: 1
|
||||||
|
consultar_entrega:
|
||||||
|
map:
|
||||||
|
session_key: session_id
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
description: Extraia somente o identificador do pedido informado explicitamente
|
||||||
|
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||||
|
mensagem.
|
||||||
|
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||||
|
group: 1
|
||||||
|
solicitar_troca:
|
||||||
|
map:
|
||||||
|
session_key: session_id
|
||||||
|
defaults:
|
||||||
|
reason: Solicitação aberta pelo atendimento conversacional.
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
description: Extraia somente o identificador do pedido informado explicitamente
|
||||||
|
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||||
|
mensagem.
|
||||||
|
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||||
|
group: 1
|
||||||
|
solicitar_devolucao:
|
||||||
|
map:
|
||||||
|
session_key: session_id
|
||||||
|
defaults:
|
||||||
|
reason: Solicitação aberta pelo atendimento conversacional.
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
description: Extraia somente o identificador do pedido informado explicitamente
|
||||||
|
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||||
|
mensagem.
|
||||||
|
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||||
|
group: 1
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
servers:
|
||||||
|
telecom:
|
||||||
|
transport: http
|
||||||
|
endpoint: http://telecom-mcp:8100/mcp
|
||||||
|
enabled: true
|
||||||
|
description: MCP Server Telecom via docker-compose.
|
||||||
|
|
||||||
|
retail:
|
||||||
|
transport: http
|
||||||
|
endpoint: http://retail-mcp:8200/mcp
|
||||||
|
enabled: true
|
||||||
|
description: MCP Server Retail via docker-compose.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# MCP servers registry.
|
||||||
|
# transport=http keeps the legacy framework mock contract:
|
||||||
|
# GET <endpoint>/tools/list
|
||||||
|
# POST <endpoint>/tools/call
|
||||||
|
# transport=fastmcp uses official MCP Streamable HTTP, typically endpoint http://host:port/mcp
|
||||||
|
# transport=sse uses official MCP SSE, typically endpoint http://host:port/sse
|
||||||
|
servers:
|
||||||
|
# telecom:
|
||||||
|
# enabled: true
|
||||||
|
# transport: fastmcp
|
||||||
|
# endpoint: http://localhost:8001/mcp
|
||||||
|
# description: Telecom FastMCP server using official MCP protocol
|
||||||
|
#
|
||||||
|
# retail:
|
||||||
|
# enabled: true
|
||||||
|
# transport: fastmcp
|
||||||
|
# endpoint: http://localhost:8002/mcp
|
||||||
|
# description: Retail FastMCP server using official MCP protocol
|
||||||
|
|
||||||
|
telecom:
|
||||||
|
enabled: true
|
||||||
|
transport: http
|
||||||
|
endpoint: http://localhost:8100/mcp
|
||||||
|
description: Telecom legacy HTTP mock MCP server
|
||||||
|
|
||||||
|
retail:
|
||||||
|
enabled: true
|
||||||
|
transport: http
|
||||||
|
endpoint: http://localhost:8200/mcp
|
||||||
|
description: Retail legacy HTTP mock MCP server
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
tone:
|
||||||
|
style: "claro, objetivo, empático"
|
||||||
|
forbidden_phrases:
|
||||||
|
- "procure atendimento humano"
|
||||||
|
vocabulary:
|
||||||
|
preferred:
|
||||||
|
fatura: "fatura"
|
||||||
|
contestacao: "contestação"
|
||||||
|
intents:
|
||||||
|
billing_agent:
|
||||||
|
- fatura
|
||||||
|
- boleto
|
||||||
|
- cobrança
|
||||||
|
- segunda via
|
||||||
|
product_agent:
|
||||||
|
- plano
|
||||||
|
- produto
|
||||||
|
- oferta
|
||||||
|
- serviço
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Roteamento enterprise configurável com MCP-aware intents.
|
||||||
|
router:
|
||||||
|
# mode também pode ser definido por variável de ambiente ROUTING_MODE.
|
||||||
|
# Valores: router | supervisor
|
||||||
|
mode: router
|
||||||
|
fallback_agent: billing_agent
|
||||||
|
confidence_threshold: 0.65
|
||||||
|
allow_handoff: true
|
||||||
|
|
||||||
|
state_policies:
|
||||||
|
- state: WAITING_BILLING_CONFIRMATION
|
||||||
|
agent: billing_agent
|
||||||
|
description: Mantém mensagens curtas como "sim" ou "não" no fluxo de fatura.
|
||||||
|
- state: WAITING_PRODUCT_CONFIRMATION
|
||||||
|
agent: product_agent
|
||||||
|
description: Mantém confirmações no fluxo de produtos/serviços.
|
||||||
|
- state: WAITING_ORDER_CONFIRMATION
|
||||||
|
agent: orders_agent
|
||||||
|
description: Mantém confirmações no fluxo de pedidos.
|
||||||
|
- state: WAITING_SUPPORT_CONFIRMATION
|
||||||
|
agent: support_agent
|
||||||
|
description: Mantém confirmações no fluxo de suporte retail.
|
||||||
|
- state: COLLECTING_BILLING_PARAMETERS
|
||||||
|
agent: billing_agent
|
||||||
|
description: Mantém a coleta de parâmetros no fluxo de faturamento.
|
||||||
|
- state: COLLECTING_PRODUCT_PARAMETERS
|
||||||
|
agent: product_agent
|
||||||
|
description: Mantém a coleta de parâmetros no fluxo de produtos e serviços.
|
||||||
|
- state: COLLECTING_ORDER_PARAMETERS
|
||||||
|
agent: orders_agent
|
||||||
|
description: Mantém a coleta de parâmetros no fluxo de pedidos.
|
||||||
|
- state: COLLECTING_SUPPORT_PARAMETERS
|
||||||
|
agent: support_agent
|
||||||
|
description: Mantém a coleta de parâmetros no fluxo transacional de suporte retail.
|
||||||
|
|
||||||
|
intents:
|
||||||
|
- name: billing_invoice_explanation
|
||||||
|
domain: telecom
|
||||||
|
agent: billing_agent
|
||||||
|
description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores.
|
||||||
|
priority: 10
|
||||||
|
mcp_tools:
|
||||||
|
- consultar_fatura
|
||||||
|
- consultar_pagamentos
|
||||||
|
keywords:
|
||||||
|
- fatura
|
||||||
|
- conta
|
||||||
|
- cobrança
|
||||||
|
- boleto
|
||||||
|
- vencimento
|
||||||
|
- segunda via
|
||||||
|
- contestar
|
||||||
|
- valor alto
|
||||||
|
- invoice
|
||||||
|
examples:
|
||||||
|
- Minha fatura veio alta.
|
||||||
|
- Quero entender uma cobrança.
|
||||||
|
- Preciso da segunda via da conta.
|
||||||
|
|
||||||
|
- name: product_services_information
|
||||||
|
domain: telecom
|
||||||
|
agent: product_agent
|
||||||
|
description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios.
|
||||||
|
priority: 20
|
||||||
|
mcp_tools:
|
||||||
|
- consultar_plano
|
||||||
|
- listar_servicos
|
||||||
|
keywords:
|
||||||
|
- plano
|
||||||
|
- serviço
|
||||||
|
- pacote
|
||||||
|
- internet
|
||||||
|
- roaming
|
||||||
|
- vas
|
||||||
|
- benefício
|
||||||
|
- assinatura
|
||||||
|
examples:
|
||||||
|
- Quais serviços estão ativos no meu plano?
|
||||||
|
- Quero saber sobre meu pacote de internet.
|
||||||
|
- Tenho roaming internacional?
|
||||||
|
|
||||||
|
- name: retail_order_tracking
|
||||||
|
domain: retail
|
||||||
|
agent: orders_agent
|
||||||
|
description: Consulta de pedido, entrega, rastreamento, atraso e status de compra.
|
||||||
|
priority: 30
|
||||||
|
mcp_tools:
|
||||||
|
- consultar_pedido
|
||||||
|
- consultar_entrega
|
||||||
|
keywords:
|
||||||
|
- pedido
|
||||||
|
- entrega
|
||||||
|
- rastreio
|
||||||
|
- rastreamento
|
||||||
|
- encomenda
|
||||||
|
- compra
|
||||||
|
- atraso
|
||||||
|
- correios
|
||||||
|
examples:
|
||||||
|
- Meu pedido não chegou.
|
||||||
|
- Quero rastrear minha entrega.
|
||||||
|
- Qual é o status da minha compra?
|
||||||
|
|
||||||
|
- name: retail_support_exchange_return
|
||||||
|
domain: retail
|
||||||
|
agent: support_agent
|
||||||
|
description: Suporte, troca, devolução, garantia e problema com produto.
|
||||||
|
priority: 25
|
||||||
|
mcp_tools:
|
||||||
|
- consultar_pedido
|
||||||
|
- solicitar_troca
|
||||||
|
- solicitar_devolucao
|
||||||
|
keywords:
|
||||||
|
- solicitar devolução
|
||||||
|
- devolver pedido
|
||||||
|
- solicitar troca
|
||||||
|
- troca
|
||||||
|
- devolução
|
||||||
|
- devolver
|
||||||
|
- garantia
|
||||||
|
- defeito
|
||||||
|
- produto quebrado
|
||||||
|
- suporte
|
||||||
|
- arrependimento
|
||||||
|
examples:
|
||||||
|
- Quero trocar um produto.
|
||||||
|
- Meu produto veio com defeito.
|
||||||
|
- Como faço uma devolução?
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
version: 1
|
||||||
|
|
||||||
|
# Arquivo opcional da aplicação. A ausência mantém o comportamento dos
|
||||||
|
# templates anteriores e as políticas legadas declaradas em tools.yaml.
|
||||||
|
defaults:
|
||||||
|
operation_type: read_only
|
||||||
|
require_confirmation: false
|
||||||
|
|
||||||
|
tool_policies:
|
||||||
|
solicitar_troca:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
|
||||||
|
solicitar_devolucao:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
|
||||||
|
# Exemplo para uma operação real que só pode executar após confirmação:
|
||||||
|
# cancelar_servico:
|
||||||
|
# operation_type: transactional
|
||||||
|
# require_confirmation: true
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
tools:
|
||||||
|
consultar_fatura:
|
||||||
|
description: Consulta dados resumidos de fatura por msisdn/invoice_id.
|
||||||
|
mcp_server: telecom
|
||||||
|
enabled: true
|
||||||
|
args_schema:
|
||||||
|
msisdn: string
|
||||||
|
invoice_id: string
|
||||||
|
selection_keywords:
|
||||||
|
- fatura
|
||||||
|
- conta
|
||||||
|
- boleto
|
||||||
|
consultar_pagamentos:
|
||||||
|
description: Consulta histórico de pagamentos do cliente.
|
||||||
|
mcp_server: telecom
|
||||||
|
enabled: true
|
||||||
|
args_schema:
|
||||||
|
msisdn: string
|
||||||
|
selection_keywords:
|
||||||
|
- pagamento
|
||||||
|
- pagamentos
|
||||||
|
consultar_plano:
|
||||||
|
description: Consulta plano ativo e atributos comerciais.
|
||||||
|
mcp_server: telecom
|
||||||
|
enabled: true
|
||||||
|
args_schema:
|
||||||
|
msisdn: string
|
||||||
|
asset_id: string
|
||||||
|
selection_keywords:
|
||||||
|
- plano
|
||||||
|
listar_servicos:
|
||||||
|
description: Lista serviços ativos e adicionais VAS.
|
||||||
|
mcp_server: telecom
|
||||||
|
enabled: true
|
||||||
|
args_schema:
|
||||||
|
msisdn: string
|
||||||
|
selection_keywords:
|
||||||
|
- serviços
|
||||||
|
- servicos
|
||||||
|
- vas
|
||||||
|
consultar_pedido:
|
||||||
|
description: Consulta pedido de varejo por order_id/customer_id.
|
||||||
|
mcp_server: retail
|
||||||
|
enabled: true
|
||||||
|
args_schema:
|
||||||
|
order_id: string
|
||||||
|
customer_id: string
|
||||||
|
selection_keywords:
|
||||||
|
- consultar pedido
|
||||||
|
- status do pedido
|
||||||
|
- pedido
|
||||||
|
consultar_entrega:
|
||||||
|
description: Consulta entrega e rastreamento do pedido.
|
||||||
|
mcp_server: retail
|
||||||
|
enabled: true
|
||||||
|
args_schema:
|
||||||
|
order_id: string
|
||||||
|
selection_keywords:
|
||||||
|
- entrega
|
||||||
|
- rastreio
|
||||||
|
- rastreamento
|
||||||
|
- transportadora
|
||||||
|
- previsão
|
||||||
|
solicitar_troca:
|
||||||
|
description: Simula abertura de solicitação de troca.
|
||||||
|
mcp_server: retail
|
||||||
|
enabled: true
|
||||||
|
tool_type: action
|
||||||
|
requires:
|
||||||
|
- order_id
|
||||||
|
- reason
|
||||||
|
confirmation_required: true
|
||||||
|
args_schema:
|
||||||
|
order_id: string
|
||||||
|
reason: string
|
||||||
|
selection_keywords:
|
||||||
|
- solicitar troca
|
||||||
|
- trocar
|
||||||
|
- troca
|
||||||
|
- defeito
|
||||||
|
- quebrado
|
||||||
|
solicitar_devolucao:
|
||||||
|
description: Simula abertura de solicitação de devolução.
|
||||||
|
mcp_server: retail
|
||||||
|
enabled: true
|
||||||
|
tool_type: action
|
||||||
|
requires:
|
||||||
|
- order_id
|
||||||
|
- reason
|
||||||
|
confirmation_required: true
|
||||||
|
args_schema:
|
||||||
|
order_id: string
|
||||||
|
reason: string
|
||||||
|
selection_keywords:
|
||||||
|
- solicitar devolução
|
||||||
|
- solicitar devolucao
|
||||||
|
- devolver pedido
|
||||||
|
- devolver
|
||||||
|
- devolução
|
||||||
|
- devolucao
|
||||||
|
- arrependimento
|
||||||
Binary file not shown.
@@ -0,0 +1,95 @@
|
|||||||
|
# Atualização do Template Backend — Analytics, Observer, NOC/GRL e OutputSupervisor
|
||||||
|
|
||||||
|
Esta versão do `agent_template_backend` foi atualizada para consumir as novidades transportadas para o `agent_framework`.
|
||||||
|
|
||||||
|
## 1. Analytics e Pub/Sub
|
||||||
|
|
||||||
|
O backend não chama mais diretamente apenas o publisher antigo de eventos. Agora ele cria um `AnalyticsPublisher`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.analytics.factory import create_analytics_publisher
|
||||||
|
from agent_framework.observability.observer import AgentObserver
|
||||||
|
|
||||||
|
analytics = create_analytics_publisher(settings)
|
||||||
|
observer = AgentObserver(analytics=analytics)
|
||||||
|
```
|
||||||
|
|
||||||
|
Com isso, o mesmo backend pode publicar em:
|
||||||
|
|
||||||
|
- OCI Streaming
|
||||||
|
- GCP Pub/Sub
|
||||||
|
- CompositePublisher, quando `ANALYTICS_PROVIDERS=oci_streaming,pubsub`
|
||||||
|
- Noop, quando analytics estiver desligado
|
||||||
|
|
||||||
|
## 2. Configuração mínima
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_ANALYTICS=true
|
||||||
|
ANALYTICS_PROVIDERS=pubsub
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Para publicar simultaneamente em OCI Streaming e GCP Pub/Sub:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_ANALYTICS=true
|
||||||
|
ANALYTICS_PROVIDERS=oci_streaming,pubsub
|
||||||
|
ENABLE_OCI_STREAMING=true
|
||||||
|
OCI_STREAM_ENDPOINT=<endpoint>
|
||||||
|
OCI_STREAM_OCID=<stream-ocid>
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Observer corporativo
|
||||||
|
|
||||||
|
O workflow recebeu emissão automática dos principais eventos corporativos:
|
||||||
|
|
||||||
|
- `NOC.001`: início do workflow
|
||||||
|
- `NOC.005`: exceção fatal no workflow
|
||||||
|
- `NOC.006`: fim do workflow antes da resposta final
|
||||||
|
- `IC.AGENT_COMPLETED`: evento informacional de conclusão
|
||||||
|
- `GRL.001` a `GRL.009`: emitidos pelo `OutputSupervisor`
|
||||||
|
|
||||||
|
## 4. OutputSupervisor
|
||||||
|
|
||||||
|
Foi inserido um novo nó LangGraph:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent -> output_supervisor -> output_guardrails -> judge -> supervisor_review -> persist
|
||||||
|
```
|
||||||
|
|
||||||
|
O `OutputSupervisor` não substitui o supervisor de roteamento. Ele valida a saída candidata do agente usando o contrato corporativo:
|
||||||
|
|
||||||
|
- `allow`
|
||||||
|
- `sanitize`
|
||||||
|
- `retry`
|
||||||
|
- `block`
|
||||||
|
- `handover`
|
||||||
|
- `observe`
|
||||||
|
|
||||||
|
Para compatibilidade com os guardrails já existentes, o template inclui o adapter `LegacyOutputGuardrailRail`, que converte decisões antigas `allowed=True/False` para `RailAction`.
|
||||||
|
|
||||||
|
## 5. Campos adicionados ao AgentState
|
||||||
|
|
||||||
|
```python
|
||||||
|
supervisor_action: str
|
||||||
|
supervisor_guidance: str
|
||||||
|
supervisor_attempt: int
|
||||||
|
supervisor_handover_reason: str
|
||||||
|
output_supervisor_results: list[dict]
|
||||||
|
output_guardrails_already_applied: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Arquivos alterados
|
||||||
|
|
||||||
|
- `agent_template_backend/app/main.py`
|
||||||
|
- `agent_template_backend/app/workflows/agent_graph.py`
|
||||||
|
- `agent_template_backend/app/state.py`
|
||||||
|
- `agent_template_backend/.env`
|
||||||
|
- `agent_template_backend/requirements.txt`
|
||||||
|
- `agent_framework/src/agent_framework/config/settings.py`
|
||||||
|
|
||||||
|
## 7. Observação importante
|
||||||
|
|
||||||
|
O `OutputSupervisor` roda os guardrails de saída por meio do adapter legado e marca `output_guardrails_already_applied=True`. Assim o nó `output_guardrails` permanece no grafo para compatibilidade, mas evita reexecutar a mesma validação quando o supervisor já aplicou os rails.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Como usar IC, NOC e GRL no Template Backend
|
||||||
|
|
||||||
|
## IC — Item de Controle
|
||||||
|
|
||||||
|
Use IC para registrar eventos de negócio relevantes.
|
||||||
|
|
||||||
|
```python
|
||||||
|
await observer.emit_ic(
|
||||||
|
"IC.FATURA_CONSULTADA",
|
||||||
|
{"session_id": session_id, "invoice_id": invoice_id},
|
||||||
|
component="billing_agent",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## NOC — Evento operacional
|
||||||
|
|
||||||
|
Use NOC para saúde técnica, latência, erros e checkpoints operacionais.
|
||||||
|
|
||||||
|
```python
|
||||||
|
await observer.emit_noc(
|
||||||
|
"003",
|
||||||
|
{"session_id": session_id, "resourceName": "ADB", "latencyMs": 120},
|
||||||
|
component="repository",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## GRL — Evento de guardrail
|
||||||
|
|
||||||
|
Normalmente o framework emite GRL automaticamente. Use manualmente apenas para
|
||||||
|
rails customizados dentro do agente.
|
||||||
|
|
||||||
|
```python
|
||||||
|
await observer.emit_grl(
|
||||||
|
"OBSERVE",
|
||||||
|
{"session_id": session_id, "rail_code": "CUSTOM_POLICY"},
|
||||||
|
component="custom_rail",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Onde já existe no template
|
||||||
|
|
||||||
|
- `app/workflows/agent_graph.py` emite IC/NOC no ciclo do workflow.
|
||||||
|
- `app/agents/runtime.py` emite IC para MCP/tools.
|
||||||
|
- `app/agents/*_agent.py` contém exemplos dentro do método `run()`.
|
||||||
|
- `app/examples/` contém exemplos isolados.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Backends atualizados para ConversationSummaryMemory
|
||||||
|
|
||||||
|
Esta versão dos backends foi compatibilizada com a versão do framework que adiciona `ConversationSummaryMemory`.
|
||||||
|
|
||||||
|
## O que mudou
|
||||||
|
|
||||||
|
- `app/main.py` agora inicializa `create_conversation_summary_memory(...)` junto com `create_memory(...)`.
|
||||||
|
- `AgentWorkflow` recebe `summary_memory` e repassa para os agentes.
|
||||||
|
- Os agentes não montam mais prompts manuais para o LLM; agora usam `build_messages()` do framework.
|
||||||
|
- Antes da chamada ao LLM, os agentes executam `await self.prepare_memory_context(state)`.
|
||||||
|
- Quando habilitado por `.env`, o prompt passa a receber:
|
||||||
|
- resumo acumulado da conversa;
|
||||||
|
- últimas mensagens completas;
|
||||||
|
- mensagem atual;
|
||||||
|
- BusinessContext;
|
||||||
|
- MCP results;
|
||||||
|
- RAG context e metadata.
|
||||||
|
|
||||||
|
## Configuração
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||||
|
MEMORY_CONTEXT_STRATEGY=summary
|
||||||
|
MEMORY_HISTORY_LIMIT=80
|
||||||
|
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||||
|
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||||
|
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||||
|
MEMORY_SUMMARY_USE_LLM=true
|
||||||
|
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||||
|
MEMORY_INJECT_SUMMARY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backends alterados
|
||||||
|
|
||||||
|
- `backoffice_convertido_framework`
|
||||||
|
- `agent_template_backend`
|
||||||
|
- `agent_template_backend_day_zero`
|
||||||
|
|
||||||
|
## Observação importante
|
||||||
|
|
||||||
|
Estes backends esperam que o pacote `agent_framework` instalado/conectado seja a versão com os módulos:
|
||||||
|
|
||||||
|
- `agent_framework.memory.summary_memory`
|
||||||
|
- `agent_framework.memory.summary_store`
|
||||||
|
- `AgentRuntimeMixin.prepare_memory_context()`
|
||||||
|
- `AgentRuntimeMixin.build_messages()` com injeção de memória
|
||||||
|
|
||||||
|
Use junto com o ZIP `agent_framework_conversation_summary_memory.zip`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Exemplos implementados no template
|
||||||
|
|
||||||
|
Este projeto entrega as capacidades transversais habilitadas como referência:
|
||||||
|
|
||||||
|
- route stickiness semântica com o perfil `route_continuity`;
|
||||||
|
- decisões `CONTINUE`, `ROUTE`, `HUMAN_HANDOFF` e `END_SESSION`;
|
||||||
|
- nós globais `human_handoff` e `end_session`;
|
||||||
|
- persistência de `active_agent`, `route_bypassed`, `continuity_signal` e controle de sessão;
|
||||||
|
- rejeição de novas mensagens depois de `session_ended=true`;
|
||||||
|
- políticas MCP `read_only` e `transactional` no backend;
|
||||||
|
- exemplo `solicitar_devolucao` com `require_confirmation: true`.
|
||||||
|
|
||||||
|
Para confirmar a transação, envie `confirmed: true` ou `confirmation: true` como booleano. Handoff e encerramento não chamam agentes de domínio nem MCP.
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# FRAMEWORK_CHANNEL_INPUT_MODE
|
||||||
|
|
||||||
|
This backend setting controls what kind of channel input the Agent Framework backend accepts.
|
||||||
|
|
||||||
|
It replaces the ambiguous use of `CHANNEL_GATEWAY_MODE` inside the backend.
|
||||||
|
|
||||||
|
## Values
|
||||||
|
|
||||||
|
```env
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend may use internal channel adapters to interpret simple/native channel payloads. This is useful for demos, labs, local frontend, curl tests, and simple environments.
|
||||||
|
|
||||||
|
```env
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend accepts only a normalized `GatewayRequest` produced by an external Channel Gateway. It does not parse native WhatsApp, Voice, Teams, or other channel payloads.
|
||||||
|
|
||||||
|
## Recommended enterprise setup
|
||||||
|
|
||||||
|
In the external channel gateway service:
|
||||||
|
|
||||||
|
```env
|
||||||
|
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
In this backend:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||||
|
```
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
External channel / browser / customer adapter
|
||||||
|
↓
|
||||||
|
channel_gateway:7000
|
||||||
|
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
|
||||||
|
↓ GatewayRequest
|
||||||
|
agent_template_backend:8000
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||||
|
↓
|
||||||
|
LangGraph / Agents / MCP / Guardrails
|
||||||
|
```
|
||||||
|
|
||||||
|
## Valid direct request to backend in external mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "http://localhost:8000/gateway/message" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"payload": {
|
||||||
|
"message": "Quero consultar minha fatura",
|
||||||
|
"session_id": "backend-external-ok-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invalid direct request to backend in external mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -s -X POST "http://localhost:8000/gateway/message" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"message": "Quero consultar minha fatura",
|
||||||
|
"session_id": "raw-payload-error-001"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: HTTP 422.
|
||||||
|
|
||||||
|
## Legacy compatibility
|
||||||
|
|
||||||
|
`CHANNEL_GATEWAY_MODE` is still present as a legacy alias for older environments, but new deployments should use:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded|external
|
||||||
|
```
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Guardrails paralelos fail-fast e Observer IC
|
||||||
|
|
||||||
|
## O que foi implementado
|
||||||
|
|
||||||
|
### 1. ParallelRailExecutor
|
||||||
|
|
||||||
|
Arquivo principal:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/guardrails/parallel_executor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Também foi criado um alias de compatibilidade:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/guardrails/executor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse alias evita erro quando algum código antigo importar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.guardrails.executor import ParallelRailExecutor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Execução paralela no GuardrailPipeline
|
||||||
|
|
||||||
|
Arquivo alterado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/guardrails/pipeline.py
|
||||||
|
```
|
||||||
|
|
||||||
|
O pipeline continua retornando o contrato antigo:
|
||||||
|
|
||||||
|
```python
|
||||||
|
(texto_final, list[RailDecision])
|
||||||
|
```
|
||||||
|
|
||||||
|
mas internamente pode executar rails em paralelo com fail-fast.
|
||||||
|
|
||||||
|
### 3. Execução paralela no OutputSupervisor
|
||||||
|
|
||||||
|
Arquivo alterado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/guardrails/output_supervisor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
O `OutputSupervisor` agora usa `ParallelRailExecutor` quando habilitado.
|
||||||
|
|
||||||
|
### 4. Configuração
|
||||||
|
|
||||||
|
Novas configurações:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_PARALLEL_GUARDRAILS=true
|
||||||
|
GUARDRAILS_FAIL_FAST=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Também foram adicionadas em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/config/settings.py
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
agent_template_backend/.env
|
||||||
|
agent_template_backend_day_zero/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Observer IC
|
||||||
|
|
||||||
|
O `AgentObserver` já tinha `emit_ic()`.
|
||||||
|
|
||||||
|
Foi complementada a API global compatível com FIRST/TIM:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.observer import ic, aic, noc, anoc, grl, agrl
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplos:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ic("AGENT_COMPLETED", data={"session_id": "..."})
|
||||||
|
await aic("MCP_TOOL_CALLED", data={"tool_name": "consultar_fatura"})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. ICs automáticos no template backend
|
||||||
|
|
||||||
|
O backend emite agora:
|
||||||
|
|
||||||
|
```text
|
||||||
|
IC.AGENT_STARTED
|
||||||
|
IC.ROUTE_SELECTED
|
||||||
|
IC.MCP_TOOL_CALLED
|
||||||
|
IC.TOOL_CALLED
|
||||||
|
IC.AGENT_COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
Além dos eventos já existentes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NOC.001
|
||||||
|
NOC.005
|
||||||
|
NOC.006
|
||||||
|
GRL.001 ... GRL.009
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validações executadas
|
||||||
|
|
||||||
|
Foram executadas validações locais com `PYTHONPATH=agent_framework/src`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m compileall -q agent_framework/src/agent_framework agent_template_backend/app agent_template_backend_day_zero/app
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke tests executados:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. Import de ParallelRailExecutor via agent_framework.guardrails
|
||||||
|
2. Import de ParallelRailExecutor via agent_framework.guardrails.executor
|
||||||
|
3. Execução fail-fast: FastBlock cancela SlowAllow
|
||||||
|
4. GuardrailPipeline paralelo retorna RailDecision legado
|
||||||
|
5. OutputSupervisor paralelo retorna RailAction.BLOCK
|
||||||
|
6. API global observer.ic/noc/grl/aic/anoc/agrl
|
||||||
|
```
|
||||||
|
|
||||||
|
Observação: o import completo do `agent_template_backend.app.workflows.agent_graph` depende de `langgraph`, que não está instalado no sandbox de validação. O arquivo foi validado por `compileall`, e a dependência já consta em `agent_template_backend/requirements.txt`.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Implementação IC/NOC/GRL preservando lógica existente
|
||||||
|
|
||||||
|
Esta versão mantém a lógica original dos agentes do `agent_template_backend` e adiciona observabilidade corporativa.
|
||||||
|
|
||||||
|
## IC adicionados nos agentes
|
||||||
|
|
||||||
|
Cada agente agora emite eventos de negócio sem alterar a resposta final:
|
||||||
|
|
||||||
|
- `IC.BILLING_AGENT_STARTED` / `IC.BILLING_AGENT_COMPLETED`
|
||||||
|
- `IC.ORDERS_AGENT_STARTED` / `IC.ORDERS_AGENT_COMPLETED`
|
||||||
|
- `IC.PRODUCT_AGENT_STARTED` / `IC.PRODUCT_AGENT_COMPLETED`
|
||||||
|
- `IC.SUPPORT_AGENT_STARTED` / `IC.SUPPORT_AGENT_COMPLETED`
|
||||||
|
- `IC.<AGENT>_MCP_CONTEXT_COLLECTED` quando houver dados MCP
|
||||||
|
- `IC.<AGENT>_RAG_CONTEXT_RETRIEVED` quando RAG estiver habilitado
|
||||||
|
|
||||||
|
O mixin `AgentRuntimeMixin` também emite:
|
||||||
|
|
||||||
|
- `IC.MCP_TOOL_CALLED` antes da chamada MCP
|
||||||
|
- `IC.TOOL_CALLED` após a chamada MCP
|
||||||
|
|
||||||
|
## NOC
|
||||||
|
|
||||||
|
O workflow já emite eventos operacionais principais:
|
||||||
|
|
||||||
|
- `NOC.001` no início da execução
|
||||||
|
- `NOC.005` em exceção fatal
|
||||||
|
- `NOC.006` na persistência/finalização
|
||||||
|
|
||||||
|
## GRL
|
||||||
|
|
||||||
|
O backend agora também exemplifica emissão GRL no workflow:
|
||||||
|
|
||||||
|
- `GRL.001` início do pipeline de guardrails
|
||||||
|
- `GRL.002` decisão allow
|
||||||
|
- `GRL.004` decisão block
|
||||||
|
- `GRL.009` decisão final agregada
|
||||||
|
|
||||||
|
Quando `OutputSupervisor` está habilitado, ele continua sendo o principal mecanismo corporativo de supervisão de saída.
|
||||||
|
|
||||||
|
## Garantia
|
||||||
|
|
||||||
|
A lógica original dos agentes não foi substituída por stubs. As chamadas LLM, MCP, RAG, cache e os retornos originais foram preservados.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Langfuse single trace observer fix
|
||||||
|
|
||||||
|
This backend now uses `TelemetryBackedAgentObserver` instead of publishing IC/NOC/GRL through `AgentObserver(analytics=...)`.
|
||||||
|
|
||||||
|
Why: when analytics includes the Langfuse provider, observer events such as `IC.AGENT_COMPLETED` and `NOC.006` may create a second root trace with little detail. Emitting those events through `Telemetry.event(...)` keeps them inside the active request/workflow trace.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Teste e diagnóstico de Long-Term Memory
|
||||||
|
|
||||||
|
## O que foi corrigido
|
||||||
|
|
||||||
|
1. A LTM agora é carregada explicitamente antes do roteamento.
|
||||||
|
2. O estado recebe uma chave estável em `long_term_memory_subject_key`, baseada em `business_context.customer_key` e, como fallback, `user_id`.
|
||||||
|
3. O resultado de carga e persistência aparece em `metadata.long_term_memory` da resposta.
|
||||||
|
4. `/health` informa a configuração efetiva de LTM carregada pelo processo.
|
||||||
|
5. Falhas de leitura e gravação geram eventos `long_term_memory.load.failed` e `long_term_memory.persist.failed`.
|
||||||
|
|
||||||
|
## Teste
|
||||||
|
|
||||||
|
Primeira sessão:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel":"web",
|
||||||
|
"payload":{
|
||||||
|
"text":"Meu nome preferido é Cris e minha linguagem preferida é Python.",
|
||||||
|
"session_id":"ltm-session-001",
|
||||||
|
"user_id":"ltm-user-001",
|
||||||
|
"customer_id":"ltm-customer-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifique na resposta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"long_term_memory": {
|
||||||
|
"subject_key": "ltm-customer-001",
|
||||||
|
"write_result": {
|
||||||
|
"saved": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nova sessão, mesma identidade:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel":"web",
|
||||||
|
"payload":{
|
||||||
|
"text":"Qual é meu nome preferido e qual linguagem eu prefiro?",
|
||||||
|
"session_id":"ltm-session-002",
|
||||||
|
"user_id":"ltm-user-001",
|
||||||
|
"customer_id":"ltm-customer-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Na segunda resposta, confira:
|
||||||
|
|
||||||
|
- `metadata.long_term_memory.subject_key` igual à primeira chamada;
|
||||||
|
- `metadata.long_term_memory.loaded` com registros;
|
||||||
|
- `metadata.long_term_memory.context` preenchido;
|
||||||
|
- ausência de `load_error`.
|
||||||
|
|
||||||
|
## Diagnóstico rápido
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
A seção `long_term_memory` deve mostrar:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "sqlite",
|
||||||
|
"sqlite_path": "./data/agent_framework.db",
|
||||||
|
"table": "agentfw_long_term_memory",
|
||||||
|
"auto_extract": true,
|
||||||
|
"inject_context": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute o backend com o diretório do projeto como diretório de trabalho. Como o caminho SQLite é relativo, iniciar a aplicação em outro diretório pode criar ou consultar outro arquivo `./data/agent_framework.db`.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Validação da versão com IC/NOC/GRL
|
||||||
|
|
||||||
|
Validações executadas nesta geração:
|
||||||
|
|
||||||
|
1. `python -m compileall -q agent_template_backend/app`
|
||||||
|
- Resultado: OK.
|
||||||
|
|
||||||
|
2. Smoke test dos agentes com LLM fake e Observer fake:
|
||||||
|
- `BillingAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||||
|
- `OrdersAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||||
|
- `ProductAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||||
|
- `SupportAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||||
|
|
||||||
|
3. Verificação de regressão:
|
||||||
|
- Nenhum agente retorna `Template Enterprise ativo`.
|
||||||
|
- A lógica LLM/MCP/RAG/cache existente foi preservada.
|
||||||
|
|
||||||
|
## Eventos adicionados
|
||||||
|
|
||||||
|
### IC
|
||||||
|
|
||||||
|
Nos agentes:
|
||||||
|
|
||||||
|
- `IC.BILLING_AGENT_STARTED`
|
||||||
|
- `IC.BILLING_MCP_CONTEXT_COLLECTED`
|
||||||
|
- `IC.BILLING_RAG_CONTEXT_RETRIEVED`
|
||||||
|
- `IC.BILLING_AGENT_COMPLETED`
|
||||||
|
- `IC.ORDERS_AGENT_STARTED`
|
||||||
|
- `IC.ORDERS_MCP_CONTEXT_COLLECTED`
|
||||||
|
- `IC.ORDERS_RAG_CONTEXT_RETRIEVED`
|
||||||
|
- `IC.ORDERS_AGENT_COMPLETED`
|
||||||
|
- `IC.PRODUCT_AGENT_STARTED`
|
||||||
|
- `IC.PRODUCT_MCP_CONTEXT_COLLECTED`
|
||||||
|
- `IC.PRODUCT_RAG_CONTEXT_RETRIEVED`
|
||||||
|
- `IC.PRODUCT_AGENT_COMPLETED`
|
||||||
|
- `IC.SUPPORT_AGENT_STARTED`
|
||||||
|
- `IC.SUPPORT_MCP_CONTEXT_COLLECTED`
|
||||||
|
- `IC.SUPPORT_RAG_CONTEXT_RETRIEVED`
|
||||||
|
- `IC.SUPPORT_AGENT_COMPLETED`
|
||||||
|
|
||||||
|
No runtime MCP:
|
||||||
|
|
||||||
|
- `IC.MCP_TOOL_CALLED`
|
||||||
|
- `IC.TOOL_CALLED`
|
||||||
|
|
||||||
|
### NOC
|
||||||
|
|
||||||
|
Já integrados no workflow:
|
||||||
|
|
||||||
|
- `NOC.001` início da execução
|
||||||
|
- `NOC.005` erro fatal
|
||||||
|
- `NOC.006` finalização/persistência
|
||||||
|
|
||||||
|
### GRL
|
||||||
|
|
||||||
|
No workflow de guardrails:
|
||||||
|
|
||||||
|
- `GRL.001` início da avaliação
|
||||||
|
- `GRL.002` allow
|
||||||
|
- `GRL.004` block
|
||||||
|
- `GRL.009` decisão final
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
compileall app: OK
|
||||||
|
Arquivos de exemplos IC/NOC/GRL adicionados.
|
||||||
|
Agentes preservam implementação original comentada.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
profiles:
|
||||||
|
default:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.2
|
||||||
|
max_tokens: 2048
|
||||||
|
supervisor:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 700
|
||||||
|
route_continuity:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 80
|
||||||
|
timeout_seconds: 5
|
||||||
|
router:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 500
|
||||||
|
guardrail:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 600
|
||||||
|
grl:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 700
|
||||||
|
judge:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 800
|
||||||
|
rag_rewriter:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 300
|
||||||
|
rag_compressor:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 1200
|
||||||
|
rag_generation:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.1
|
||||||
|
max_tokens: 1800
|
||||||
|
summary_memory:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.1
|
||||||
|
max_tokens: 1200
|
||||||
|
noc:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 700
|
||||||
|
billing_agent:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.2
|
||||||
|
product_agent:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.2
|
||||||
|
backoffice_agent:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.2
|
||||||
|
mcp_parameter_extraction:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 80
|
||||||
|
timeout_seconds: 5
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
fastapi>=0.115.0
|
||||||
|
uvicorn[standard]>=0.30.0
|
||||||
|
pydantic>=2.8.0
|
||||||
|
pydantic-settings>=2.4.0
|
||||||
|
python-dotenv>=1.0.1
|
||||||
|
langgraph>=0.2.60
|
||||||
|
langchain-core>=0.3.0
|
||||||
|
openai>=1.60.0
|
||||||
|
oci>=2.130.0
|
||||||
|
oracledb>=2.4.0
|
||||||
|
pymongo>=4.8.0
|
||||||
|
redis>=5.0.0
|
||||||
|
PyYAML>=6.0.2
|
||||||
|
|
||||||
|
langfuse>=3.0.0
|
||||||
|
httpx>=0.27.0
|
||||||
|
opentelemetry-api>=1.27.0
|
||||||
|
opentelemetry-sdk>=1.27.0
|
||||||
|
opentelemetry-exporter-otlp-proto-http>=1.27.0
|
||||||
|
|
||||||
|
pytest>=8.0.0
|
||||||
|
pytest-asyncio>=0.23.0
|
||||||
|
google-cloud-pubsub>=2.28.0
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import asyncio
|
||||||
|
import tempfile
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
settings = SimpleNamespace(
|
||||||
|
ENABLE_LONG_TERM_MEMORY=True,
|
||||||
|
LONG_TERM_MEMORY_PROVIDER='sqlite',
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db',
|
||||||
|
LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory',
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20,
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70,
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=True,
|
||||||
|
)
|
||||||
|
manager = create_long_term_memory_manager(settings)
|
||||||
|
first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}}
|
||||||
|
assert (await manager.persist_turn(first))['saved'] >= 3
|
||||||
|
second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}}
|
||||||
|
values = {item.key:item.value for item in await manager.load(second)}
|
||||||
|
assert values['preferred_name'].lower() == 'cris'
|
||||||
|
assert values['preferred_language'].lower() == 'python'
|
||||||
|
assert values['current_project'].lower() == 'atlas'
|
||||||
|
isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}}
|
||||||
|
assert await manager.load(isolated) == []
|
||||||
|
print('OK: persistência, recuperação entre sessões e isolamento validados')
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
195
Tuning-Performance/Normal/templates/agent_template_backend/.env
Normal file
195
Tuning-Performance/Normal/templates/agent_template_backend/.env
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
###############################################################################
|
||||||
|
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
|
||||||
|
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
APP_NAME=ai-agent-template
|
||||||
|
APP_ENV=local
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LLM - OCI Generative AI como provider principal
|
||||||
|
###############################################################################
|
||||||
|
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
||||||
|
LLM_PROVIDER=oci_openai
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_MAX_TOKENS=2048
|
||||||
|
LLM_TIMEOUT_SECONDS=120
|
||||||
|
|
||||||
|
# OCI OpenAI-compatible endpoint
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
||||||
|
OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
# OCI SDK / signer / profiles
|
||||||
|
OCI_CONFIG_FILE=~/.oci/config
|
||||||
|
OCI_PROFILE=DEFAULT
|
||||||
|
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
OCI_REGION=us-chicago-1
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Persistência
|
||||||
|
###############################################################################
|
||||||
|
# Opções: memory, autonomous, mongodb
|
||||||
|
SESSION_REPOSITORY_PROVIDER=sqlite
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=sqlite
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
||||||
|
SQLITE_DB_PATH=./data/agent_framework.db
|
||||||
|
|
||||||
|
# Autonomous Database
|
||||||
|
ADB_USER=admin
|
||||||
|
ADB_PASSWORD=fjhsdf04954hf
|
||||||
|
ADB_DSN=oradb23aidev_high
|
||||||
|
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
||||||
|
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
|
||||||
|
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||||
|
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
||||||
|
MONGODB_DATABASE=agent_platform
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
ENABLE_REDIS_CACHE=false
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RAG / Vector / Graph
|
||||||
|
###############################################################################
|
||||||
|
VECTOR_STORE_PROVIDER=sqlite
|
||||||
|
GRAPH_STORE_PROVIDER=sqlite
|
||||||
|
RAG_TOP_K=5
|
||||||
|
EMBEDDING_PROVIDER=mock
|
||||||
|
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||||
|
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Observabilidade
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
|
||||||
|
LANGFUSE_HOST=http://localhost:3005
|
||||||
|
ENABLE_OTEL=false
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
|
OTEL_SERVICE_NAME=ai-agent-template
|
||||||
|
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Analytics / Observer corporativo
|
||||||
|
###############################################################################
|
||||||
|
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
# Providers aceitos: oci_streaming,pubsub,noop
|
||||||
|
ANALYTICS_PROVIDERS=pubsub
|
||||||
|
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||||
|
AGENT_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=
|
||||||
|
GCP_PROJECT_ID=
|
||||||
|
GCP_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||||
|
# Credencial GCP segue padrão Google:
|
||||||
|
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OCI Streaming
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_OCI_STREAMING=false
|
||||||
|
OCI_STREAM_ENDPOINT=
|
||||||
|
OCI_STREAM_OCID=
|
||||||
|
OCI_STREAM_PARTITION_KEY=agent-events
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Guardrails, Judges, Supervisor
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_INPUT_GUARDRAILS=true
|
||||||
|
ENABLE_OUTPUT_GUARDRAILS=true
|
||||||
|
ENABLE_JUDGES=true
|
||||||
|
ENABLE_SUPERVISOR=true
|
||||||
|
ENABLE_OUTPUT_SUPERVISOR=true
|
||||||
|
ENABLE_PARALLEL_GUARDRAILS=true
|
||||||
|
GUARDRAILS_FAIL_FAST=true
|
||||||
|
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||||
|
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||||
|
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||||
|
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Gateway de canais
|
||||||
|
###############################################################################
|
||||||
|
DEFAULT_CHANNEL=web
|
||||||
|
# embedded = backend may parse simple/native channel payloads.
|
||||||
|
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
|
ENABLE_VOICE_ADAPTER=true
|
||||||
|
ENABLE_WHATSAPP_ADAPTER=true
|
||||||
|
ENABLE_TEXT_ADAPTER=true
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
# ENTERPRISE ROUTING
|
||||||
|
#################################################
|
||||||
|
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
||||||
|
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||||
|
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
||||||
|
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||||
|
ENABLE_LLM_ROUTER=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# MCP / Tools
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||||
|
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||||
|
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||||
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
|
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||||
|
ROUTING_MODE=router
|
||||||
|
|
||||||
|
# Usage/cost accounting
|
||||||
|
USAGE_REPOSITORY_PROVIDER=sqlite
|
||||||
|
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||||
|
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ConversationSummaryMemory / compressão de contexto conversacional
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||||
|
MEMORY_CONTEXT_STRATEGY=summary
|
||||||
|
MEMORY_HISTORY_LIMIT=80
|
||||||
|
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||||
|
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||||
|
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||||
|
MEMORY_SUMMARY_USE_LLM=true
|
||||||
|
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||||
|
MEMORY_INJECT_SUMMARY=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# MCP Gateway
|
||||||
|
###############################################################################
|
||||||
|
# true = framework routes tool calls to the dedicated MCP Gateway.
|
||||||
|
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
# MCP_GATEWAY_TOKEN=
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LONG-TERM MEMORY
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
||||||
|
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY agent_framework /agent_framework
|
||||||
|
COPY agent_template_backend /app
|
||||||
|
RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
4213
Tuning-Performance/Normal/templates/agent_template_backend/README.md
Normal file
4213
Tuning-Performance/Normal/templates/agent_template_backend/README.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
# Agent Template Backend Enterprise
|
||||||
|
|
||||||
|
Este folder é uma cópia completa do `agent_template_backend`, sem cortes de
|
||||||
|
arquitetura. Ele mantém workflow, router, output supervisor, guardrails,
|
||||||
|
analytics, observer, MCP, memória, checkpoints e configurações.
|
||||||
|
|
||||||
|
A diferença é que a lógica de negócio dos agentes de exemplo foi removida da
|
||||||
|
execução e preservada comentada nos próprios arquivos:
|
||||||
|
|
||||||
|
- `app/agents/billing_agent.py`
|
||||||
|
- `app/agents/product_agent.py`
|
||||||
|
- `app/agents/orders_agent.py`
|
||||||
|
- `app/agents/support_agent.py`
|
||||||
|
|
||||||
|
## O que o desenvolvedor deve alterar
|
||||||
|
|
||||||
|
1. Escolher ou criar um agente em `app/agents/`.
|
||||||
|
2. Implementar o método `run()`.
|
||||||
|
3. Ajustar prompts e tools, se necessário.
|
||||||
|
4. Emitir ICs de negócio relevantes para a jornada.
|
||||||
|
5. Manter NOC/GRL nos pontos operacionais e de guardrails.
|
||||||
|
|
||||||
|
## O que já está integrado
|
||||||
|
|
||||||
|
- `AgentObserver`
|
||||||
|
- `observer.emit_ic()`
|
||||||
|
- `observer.emit_noc()`
|
||||||
|
- `observer.emit_grl()`
|
||||||
|
- `AnalyticsPublisher`
|
||||||
|
- OCI Streaming
|
||||||
|
- GCP Pub/Sub
|
||||||
|
- OutputSupervisor
|
||||||
|
- GuardrailPipeline com suporte a execução paralela/fail-fast no framework
|
||||||
|
- MCP Tool Router
|
||||||
|
- LangGraph
|
||||||
|
- Memory
|
||||||
|
- Checkpoint
|
||||||
|
- Langfuse / OpenTelemetry
|
||||||
|
|
||||||
|
## Exemplos adicionados
|
||||||
|
|
||||||
|
Veja `app/examples/`:
|
||||||
|
|
||||||
|
- `ic_examples.py`
|
||||||
|
- `noc_examples.py`
|
||||||
|
- `grl_examples.py`
|
||||||
|
- `mcp_examples.py`
|
||||||
|
- `observer_examples.py`
|
||||||
|
|
||||||
|
## Convenção rápida
|
||||||
|
|
||||||
|
- IC = evento de negócio / curadoria / informacional.
|
||||||
|
- NOC = evento operacional / saúde técnica.
|
||||||
|
- GRL = evento de guardrail / segurança / validação.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user