First commit

This commit is contained in:
2026-06-19 22:17:09 -03:00
commit 239203ee2a
533 changed files with 75195 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
# ConversationSummaryMemory
Este módulo adiciona compressão de contexto conversacional ao framework sem substituir a memória bruta existente.
## Objetivo
O framework passa a trabalhar com dois níveis de memória:
1. **Histórico bruto**: mensagens completas persistidas por `ConversationMemory`.
2. **Resumo incremental**: contexto antigo compactado por `ConversationSummaryMemory`.
O prompt final do agente pode receber:
```text
Resumo da conversa até agora:
{summary}
Últimas mensagens completas da conversa:
{recent_messages}
Mensagem do usuário:
{current_user_message}
```
## 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
```
Estratégias disponíveis:
- `none`: não injeta memória conversacional no prompt.
- `window`: injeta apenas as últimas mensagens.
- `summary`: mantém resumo acumulado das mensagens antigas e últimas mensagens completas.
## Pontos implementados
Arquivos adicionados:
```text
src/agent_framework/memory/summary_memory.py
src/agent_framework/memory/summary_store.py
```
Arquivos alterados:
```text
src/agent_framework/memory/__init__.py
src/agent_framework/config/settings.py
src/agent_framework/runtime/agent_runtime.py
src/agent_framework/persistence/sqlite_store.py
src/agent_framework/persistence/oracle_store.py
```
## Como usar no agente
Antes de chamar `build_messages()`, prepare a memória:
```python
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=system_prompt,
user_text=state.get("sanitized_input"),
)
```
O método `prepare_memory_context()` salva o resultado em:
```python
state["memory_context"]
state["memory_context_metadata"]
```
O método `build_messages()` injeta automaticamente esse contexto quando ele existe.
## Eventos de observabilidade
O runtime emite eventos IC quando a memória é carregada ou comprimida:
```text
IC.MEMORY_CONTEXT_LOADED
IC.MEMORY_COMPRESSION_TRIGGERED
IC.MEMORY_SUMMARY_UPDATED
```
## Persistência
SQLite:
```text
agent_memory_summaries
```
Oracle:
```text
<ADB_TABLE_PREFIX>_MEMORY_SUMMARY
```
MongoDB:
```text
memory_summaries
```
## Observação importante
`ConversationSummaryMemory` não é o mesmo que `Checkpoint Compaction`.
- Checkpoint compaction reduz checkpoints técnicos do LangGraph.
- ConversationSummaryMemory reduz o contexto semântico da conversa para o LLM.

View File

@@ -0,0 +1,54 @@
# Dynamic LLM Profiles
`llm_profiles.yaml` is optional.
If the file does not exist, the backend keeps the current behavior and uses `.env`:
```env
LLM_PROVIDER=oci_openai
OCI_GENAI_MODEL=openai.gpt-4.1
LLM_TEMPERATURE=0.2
LLM_MAX_TOKENS=2048
```
If `llm_profiles.yaml` exists, each inference point resolves parameters in this order:
```text
specific profile -> default profile -> .env
```
Supported inference points:
| Profile | Used by |
|---|---|
| `default` | global fallback when YAML exists |
| `supervisor` | global supervisor / LLM supervisor |
| `router` | EnterpriseRouter LLM classification |
| `guardrail` | optional LLM guardrail rail |
| `grl` | optional output supervisor / GRL LLM rail and GRL advisor |
| `judge` | LLM judge when enabled in `config/judges.yaml` |
| `rag_rewriter` | RAG query rewriting |
| `rag_compressor` | RAG context compression |
| `rag_generation` | direct RAG answer generation |
| `summary_memory` | ConversationSummaryMemory |
| `noc` | optional NOC reasoning advisor |
| `<agent_name>` | agent runtime, for example `billing_agent` |
Optional LLM inference points are disabled by default to preserve current behavior:
```env
ENABLE_LLM_GUARDRAIL=false
ENABLE_LLM_GRL=false
ENABLE_RAG_QUERY_REWRITE=false
ENABLE_RAG_CONTEXT_COMPRESSION=false
ENABLE_RAG_GENERATION=false
```
To enable guardrails/GRL, inject the same backend LLM object into the corresponding component and set the flags above as needed. For judges, do not use an extra LLM flag: enable or disable the LLM judge in `config/judges.yaml`.
## Provider per profile
Recommendation: declare `provider` explicitly in every profile. The resolver can inherit it from `default`, but explicit provider avoids ambiguity and makes tests with invalid models/providers deterministic.
Judge activation is controlled by `config/judges.yaml`; `llm_profiles.yaml` only chooses the `judge` model/provider/params.

View File

@@ -0,0 +1,84 @@
# Guardrails calibrados adaptados ao framework
## Objetivo
Este pacote mantém a arquitetura atual do `agent_framework` e substitui a calibração interna dos rails pela lógica do pacote `guardrails.zip` anexado.
Foram preservados:
- `GuardrailPipeline`
- execução paralela/fail-fast via `ParallelRailExecutor`
- emissão de eventos GRL e eventos nomeados por rail
- `OutputSupervisor`
- perfis dinâmicos de LLM (`guardrail` e `grl`)
- gravação do modelo no Langfuse pelo provider do próprio framework
## O que mudou
A lógica calibrada foi adicionada em:
```text
src/agent_framework/guardrails/calibrated/
```
A ponte com o LLM do framework foi adicionada em:
```text
src/agent_framework/guardrails/framework_llm_client.py
```
As classes públicas foram preservadas em:
```text
src/agent_framework/guardrails/rails.py
```
## Rails calibrados integrados
Input:
- `INPUT_SIZE`
- `MSK`
- `TOX`
- `PINJ`
- `VLOOP`
- `DLEX_IN`
- `OOS` opcional via `GUARDRAIL_OOS_ENABLED=true`
Output:
- `MSK`
- `TOXOUT`
- `CMP`
- `AOFERTA`
- `REVPREC`
- `DLEX_OUT`
- `GND`
- `ALUC_RISK`
Retrieval:
- `RET_REL`
- `RAGSEC`
- `MSK`
## LLM e Langfuse
Os rails LLM não criam outro cliente fora do framework. Eles usam o `llm` passado ao `GuardrailPipeline`, chamando:
```python
llm.ainvoke(..., profile_name="guardrail" ou "grl", component_name="guardrail.<code>")
```
Assim, o modelo usado por `PINJ`, `OOS`, `AOFERTA`, `REVPREC`, `RAGSEC`, etc. continua aparecendo corretamente no Langfuse conforme a instrumentação atual do framework.
## Modo mock
Quando `USE_MOCK_LLM=true`, os rails LLM usam heurísticas locais calibradas para desenvolvimento/teste rápido.
Para validar com LLM real:
```bash
export USE_MOCK_LLM=false
```

View File

@@ -0,0 +1,43 @@
# Guardrails and llm_profiles.yaml enforcement
This fix ensures calibrated guardrails respect `llm_profiles.yaml` for the `guardrail` and `grl` profiles.
## Problem
Some boot paths instantiated `GuardrailPipeline` without an explicit framework LLM. In that case the adapter treated `llm is None` as local mock mode and returned local fallback decisions. Also, `USE_MOCK_LLM=true` could hide the model configured in `profiles.guardrail` or `profiles.grl`.
That meant intentionally invalid models such as `xopenai.gpt-4.1` did not fail, because the guardrail never reached the configured provider.
## Fix
`framework_llm_client.py` now:
- resolves the selected profile before deciding mock vs real;
- creates the framework LLM from `Settings` when the pipeline did not receive one;
- gives precedence to an explicit non-mock `guardrail`/`grl` profile over `USE_MOCK_LLM`;
- no longer overrides `temperature` and `max_tokens` at call time, so YAML profile values are honored.
`custom_rails.py` now allows passing `llm` and `observer` into the generated `GuardrailPipeline`.
## Expected validation
With this YAML:
```yaml
profiles:
default:
provider: oci_openai
model: openai.gpt-4.1
guardrail:
model: xopenai.gpt-4.1
temperature: 0
max_tokens: 600
grl:
model: xopenai.gpt-4.1
temperature: 0
max_tokens: 700
```
LLM-based guardrails such as `PINJ` fallback, `AOFERTA`, `REVPREC`, `DLEX_OUT`, `RAGSEC`, and enabled LLM checks must attempt to use `xopenai.gpt-4.1` and surface the provider/model error instead of silently returning local mock results.
Deterministic short-circuit rails may still block before calling an LLM. To validate profile usage, use a case that reaches the LLM rail or inspect Langfuse generation metadata for `profile_name`, `model`, and `profile_source`.

View File

@@ -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`.

View File

@@ -0,0 +1,22 @@
# Correção: `reason` real nos guardrails calibrados
Esta versão corrige o fallback local dos guardrails calibrados para não emitir razões genéricas como `mock PINJ calibrado`.
Mesmo quando `USE_MOCK_LLM=true`, os rails agora retornam uma razão operacional baseada no marcador ou padrão que disparou a decisão.
Exemplos:
- `PINJ`: informa o padrão determinístico de prompt injection/jailbreak detectado.
- `REVPREC`: informa o marcador de verbalização prematura encontrado.
- `AOFERTA`: informa o marcador de oferta proativa detectado.
- `TOX`: informa o padrão determinístico de toxicidade detectado.
- `OOS`: informa o marcador fora de escopo encontrado.
- `RAGSEC`, `DLEX_IN` e `DLEX_OUT`: informam o padrão local de risco quando o fallback local estiver ativo.
A arquitetura atual foi preservada:
- `GuardrailPipeline`
- `ParallelRailExecutor`
- emissão GRL
- execução paralela/fail-fast
- uso de `llm_profiles.yaml` via LLM do framework quando `USE_MOCK_LLM=false`

View File

@@ -0,0 +1,72 @@
# guardrails.yaml como fonte da verdade
## Problema corrigido
O framework estava instanciando o bundle default de guardrails diretamente dentro de `GuardrailPipeline` e `CustomRails`.
Na prática, isso fazia com que todos os guardrails disponíveis fossem executados mesmo quando `config/guardrails.yaml` declarava apenas alguns rails habilitados.
## Regra atual
Agora a regra é:
```text
Se config/guardrails.yaml existir:
somente os rails listados e enabled=true serão executados.
Se config/guardrails.yaml não existir:
o framework mantém o comportamento legado e carrega o bundle default.
```
## Exemplo
```yaml
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true
```
Com esse arquivo, o input executa apenas `MSK` e `VLOOP`, e o output executa apenas `REVPREC`.
Guardrails como `PINJ`, `TOX`, `DLEX_IN`, `AOFERTA`, `DLEX_OUT`, `CMP` e `RAGSEC` não são instanciados se não estiverem no YAML.
## Guardrail LLM
Quando um rail LLM está habilitado no YAML, ele usa o profile adequado do `llm_profiles.yaml`:
```text
PINJ, TOX, OOS, DLEX_IN, RAGSEC -> profile guardrail
REVPREC, AOFERTA, DLEX_OUT -> profile grl
```
Se o modelo do profile estiver errado, o erro não deve ser escondido por fallback silencioso.
## Rails conhecidos
Principais códigos aceitos:
```text
INPUT_SIZE
MSK
TOX
PINJ
VLOOP
DLEX_IN
OOS
TOXOUT
CMP
AOFERTA
REVPREC
DLEX_OUT
GND
ALUC_RISK
RET_REL
RAGSEC
TOOL_VAL
```
Também existem aliases de compatibilidade, como `JAILBREAK`, `GROUNDEDNESS`, `HALLUCINATION_RISK`, `TOX_OUT`, `MSK_OUT` e `TOOL_VALIDATION`.

View File

@@ -0,0 +1,67 @@
# IC/NOC/GRL nativo no Langfuse
Esta versão do framework remove a necessidade de um `ics_collector.py` dentro de cada agente.
Agora o próprio framework publica eventos `IC.*`, `AGA.*`, `NOC.*` e `GRL.*` no Langfuse por meio do `AgentObserver` e do `LangfuseAnalyticsPublisher`.
## Configuração
Para publicar IC/NOC/GRL no Langfuse:
```env
ENABLE_LANGFUSE=true
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=http://localhost:3005
# Opcional. Se não informar, ENABLE_LANGFUSE=true já inclui langfuse no observer.
ENABLE_ANALYTICS=true
ANALYTICS_PROVIDERS=langfuse,oci_streaming
```
Para manter compatibilidade com projetos antigos:
```python
from agent_framework.observer import configure
configure({"publisher": {"type": "langfuse"}})
```
## Emissão em agentes nativos
```python
from agent_framework.observer import event, ic, noc, grl
# Mantém o código exatamente como o backoffice original mostrava no Langfuse.
event("AGA.001", data={"sessionId": session_id, "agentId": "backoffice"})
# Também pode usar os atalhos.
ic("AGA.018", data={"missingFields": ["gsm"]})
noc("NOC.001", data={"sessionId": session_id})
grl("GRL.004", data={"rail": "PINJ", "blocked": True})
```
## Comportamento esperado no Langfuse
Cada evento vira uma observation/span com `name` igual ao código:
- `AGA.001`
- `AGA.018`
- `NOC.001`
- `GRL.004`
A metadata recebe automaticamente:
- `tag`
- `ic=true` para `IC.*` e `AGA.*`
- `noc=true` para `NOC.*`
- `grl=true` para `GRL.*`
- `sessionId`, `messageId`, `agentId`, `channelId` quando existirem no payload
## Compatibilidade TIM/FIRST
`ic("AGA.001")` não vira `IC.AGA.001`. O framework preserva `AGA.001`, porque no backoffice original esse era o contrato exibido no Langfuse.
`noc("001")` vira `NOC.001`.
`grl("004")` vira `GRL.004`.

View File

@@ -0,0 +1,90 @@
# Calibrated Judges Adaptation
This project now carries the calibrated judge package inside the framework while preserving the existing architecture.
## What changed
The calibrated judge prompts were added under:
```text
src/agent_framework/judges/calibrated/
```
The main integration point remains:
```text
src/agent_framework/judges/judge.py
```
The framework still uses:
- `JudgePipeline`
- `config/judges.yaml` as the source of truth for which judges run
- `llm_profiles.yaml` profile `judge` for provider/model/temperature/max tokens
- the existing framework LLM provider, Langfuse instrumentation and token accounting
- `.env` fallback when `llm_profiles.yaml` is absent
There is intentionally no `ENABLE_LLM_JUDGE` gate.
## Current mapping
With this YAML:
```yaml
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6
```
The framework runs:
| YAML name | Calibrated task | Purpose |
| --- | --- | --- |
| `response_quality` | `RQLT` | response quality |
| `groundedness` | `ALUC` | hallucination / unsupported factual claims |
Both use the `judge` LLM profile unless another profile is set on the YAML item.
## Testing model enforcement
If this is configured:
```yaml
profiles:
judge:
provider: oci_openai
model: xopenai.gpt-4.1
temperature: 0
max_tokens: 800
```
Then `response_quality` and `groundedness` will try to use `xopenai.gpt-4.1`. If that model does not exist, the calibrated judge call should fail according to the entry/global `fail_closed` behavior.
## Keeping the old heuristic judges
To force the old deterministic behavior for a specific judge:
```yaml
judges:
- name: response_quality
type: deterministic
enabled: true
threshold: 0.7
```
## Optional calibrated judges
```yaml
judges:
- name: tone
enabled: true
- name: sentiment
enabled: true
fail_on_negative: false
```
These map to the calibrated `VCTN` and `CSI` prompts.

View File

@@ -0,0 +1,33 @@
# judges.yaml simple schema
The framework accepts the simple judge configuration format:
```yaml
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6
```
In this format, `type` is optional. The framework infers deterministic judges from `name`:
- `response_quality` -> deterministic response quality judge
- `groundedness` -> deterministic groundedness judge
The `threshold` field is now applied to the deterministic judge pass/fail calculation and is also emitted in the judge result metadata.
No LLM is called by this YAML. The `llm_profiles.yaml` profile named `judge` is only used if a LLM judge is explicitly declared, for example:
```yaml
judges:
- name: llm_judge
type: llm
enabled: true
profile: judge
fail_closed: true
```
There is no `ENABLE_LLM_JUDGE` gate. The YAML is the source of truth.

View File

@@ -0,0 +1,40 @@
# Judges YAML as the source of truth
This version removes the extra `ENABLE_LLM_JUDGE` activation gate.
The judge stage now follows this rule:
1. `ENABLE_JUDGES=false` disables the whole judge stage.
2. `config/judges.yaml` decides which judges are active.
3. If a judge has `type: llm` and `enabled: true`, the framework calls the LLM.
4. `llm_profiles.yaml` decides which model/provider that LLM judge uses through the configured profile, usually `judge`.
5. If `llm_profiles.yaml` is absent, the LLM judge falls back to the global `.env` LLM configuration.
Example:
```yaml
judges:
- code: llm_judge
type: llm
enabled: true
profile: judge
fail_closed: true
```
And in `llm_profiles.yaml`:
```yaml
profiles:
judge:
provider: oci_openai
model: openai.gpt-4.1
temperature: 0
max_tokens: 800
```
If you intentionally configure a nonexistent model for `judge`, the LLM judge will try to use it. The final behavior depends on `fail_closed`:
- `fail_closed: true` blocks/fails the judge result.
- `fail_closed: false` reports the LLM judge as unavailable and follows fail-open.
`ENABLE_LLM_JUDGE` is intentionally not used anymore.

View File

@@ -0,0 +1,60 @@
# Judge model/profile error handling
The calibrated judges (`response_quality`, `groundedness`, `sentiment`, `tone`, `llm_judge`) are LLM-based unless an entry explicitly declares `type: deterministic`.
Because they depend on the model configured in `llm_profiles.yaml`, the default behavior is now **fail-closed**:
```yaml
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6
```
With this configuration, if the profile `judge` points to an invalid model, for example:
```yaml
profiles:
judge:
provider: oci_openai
model: xopenai.gpt-4.1
```
then the judge result is returned as `passed=false`, with `score=0.0`, the exception metadata, and a reason similar to:
```text
Falha no judge calibrado RQLT: ...
```
To intentionally keep the old fail-open behavior, configure it explicitly in `judges.yaml`:
```yaml
fail_closed: false
judges:
- name: response_quality
enabled: true
threshold: 0.7
```
Or per judge:
```yaml
judges:
- name: response_quality
enabled: true
threshold: 0.7
fail_closed: false
```
Deterministic judges do not call the LLM profile:
```yaml
judges:
- name: response_quality
type: deterministic
enabled: true
threshold: 0.7
```

View File

@@ -0,0 +1,44 @@
# Langfuse analytics context correlation fix
This patch fixes a remaining trace-splitting issue in the Langfuse analytics publisher.
## Problem
After the framework trace-id normalization fix, the main HTTP/workflow trace was being created correctly, but some IC/NOC/GRL events could still appear as separate root traces in Langfuse, especially events such as:
- `IC.BACKOFFICE_WORKFLOW_COMPLETED`
- `IC.BACKOFFICE_NODE_COMPLETED`
- `NOC.*`
This happened when those analytics events carried only business identifiers such as `transaction_id` or `sessionId`, while the HTTP trace was correlated by `request_id`.
## Fix
`src/agent_framework/analytics/providers/langfuse.py` now merges the current `ObservabilityContext` into analytics event payloads before computing the Langfuse trace context.
Correlation priority is now:
1. Current `trace_id` / `request_id` from `ObservabilityContext`
2. Payload `trace_id` / `request_id`
3. Business fallback: `transaction_id`, `session_id`, `sessionId`
This keeps business IDs in metadata, but ensures Langfuse observations are attached to the active request trace whenever a workflow is running.
## Expected result
In Langfuse `Tracing > Traces`, a single backoffice request should appear as one main trace, such as:
- `http.request.completed`
- or the configured request/workflow root name
Inside that trace, the internal observations should include:
- `backoffice.channel.normalized`
- `backoffice.workflow.dispatch`
- `langgraph.node.*`
- `mcp.tool_call.*`
- `IC.BACKOFFICE_*`
- `NOC.*`
- guardrails and judges
IC/NOC/GRL events should no longer create a separate trace just because they only carried `transaction_id` or `sessionId`.

View File

@@ -0,0 +1,81 @@
# Langfuse Internal Event Root Trace Fix
## Problema
Alguns eventos internos IC/NOC/GRL estavam aparecendo na tela **Tracing → Traces** como traces raiz separados, mesmo quando pertenciam à mesma execução REST/workflow.
Exemplo observado:
```text
Name: http.request.completed
Input: {"eventType": "NOC.006", ...}
Output: {"published": true}
```
Esse registro não representa a execução real do agente. Ele representa apenas a publicação de um evento interno via analytics, e por isso não deve aparecer como trace raiz.
## Correção
O arquivo abaixo foi ajustado:
```text
src/agent_framework/analytics/providers/langfuse.py
```
A nova regra é:
```text
1 request/workflow = 1 trace raiz
IC/NOC/GRL = observations/spans dentro do trace corrente
Eventos internos embrulhados em http.request/gateway/telemetry não criam trace raiz
```
## Regras aplicadas
O publisher agora:
1. Detecta envelopes internos como `IC.*`, `NOC.*`, `GRL.*` e `AGA.*`.
2. Suprime eventos técnicos do tipo `http.request.completed` cujo input real é um envelope interno como `NOC.006`.
3. Prioriza correlação por `ObservabilityContext`:
```text
trace_id/request_id do contexto atual
> trace_id/request_id do payload
> transaction_id/session_id apenas como fallback
```
4. Evita fallback para `langfuse.trace(...)` ou `langfuse.span(...)` para eventos internos/técnicos quando a observation correlacionada falha.
5. Mantém a flag abaixo para debug isolado:
```bash
export LANGFUSE_ALLOW_STANDALONE_INTERNAL_EVENTS=true
```
Por padrão, essa flag deve ficar desligada.
## Resultado esperado
Na tela **Tracing → Traces**, uma execução nova deve aparecer como uma linha principal, por exemplo:
```text
http.request.completed
```
ou:
```text
backoffice.process-and-stream
```
Ao abrir o trace, devem aparecer internamente:
```text
IC.BACKOFFICE_WORKFLOW_COMPLETED
NOC.006
langgraph.node.*
mcp.tool_call.*
guardrail.*
judge.*
```
O trace solto com `Input: {"eventType": "NOC.006"}` e `Output: {"published": true}` deve desaparecer.

View File

@@ -0,0 +1,56 @@
# Langfuse Span Hierarchy Fix
## Problem
After trace correlation was fixed, a full request no longer exploded into many independent Langfuse traces. However, observations inside the trace could appear flattened at the same level.
This happened because the framework was propagating only the Langfuse `trace_id`, but not the current parent observation/span id.
In Langfuse, a tree needs both:
- `trace_id`: identifies the root execution trace;
- `parent_span_id`: identifies which observation/span should be the parent of the new observation.
Without `parent_span_id`, all observations are correlated to the same trace but may appear as direct children of the trace root.
## Fix
The framework now keeps the current Langfuse observation id in the async observability context.
Updated files:
- `src/agent_framework/observability/context.py`
- `src/agent_framework/observability/telemetry.py`
- `src/agent_framework/analytics/providers/langfuse.py`
## Behavior
When a span starts:
1. The framework creates the Langfuse observation.
2. It extracts the observation id from the SDK object.
3. It stores that id in a ContextVar as the current parent observation.
4. Nested spans, generations and analytics events pass it as `trace_context.parent_span_id`.
5. When the span exits, the previous parent observation id is restored.
## Expected Langfuse Structure
A backoffice request should appear as one trace, with nested observations such as:
```text
http.request / backoffice.process-and-stream
└── backoffice.workflow.dispatch
├── langgraph.node.framework_input_guardrails
├── langgraph.node.fetch_ticket
├── langgraph.node.validation
├── langgraph.node.imdb_enrichment
│ └── mcp.tool_call.consultar_imdb_cliente
├── langgraph.node.knowledge_base_enrichment
│ └── mcp.tool_call.consultar_tais_kb
├── langgraph.node.treatment_decision
└── langgraph.node.siebel_sr_opening
```
## Notes
This fix complements the previous trace correlation fixes. Those fixes solved root trace duplication. This fix solves parent-child hierarchy inside the trace.

View File

@@ -0,0 +1,107 @@
# Langfuse trace correlation fix
## Problem
The Langfuse **Tracing > Traces** list was showing one row per internal framework event, for example:
- `langgraph.node.started`
- `langgraph.node.fetch_ticket`
- `OpenAI-generation`
- `TaisKbClient.search_documents`
- `NOC.001`
That is not the intended observability model.
The intended model is:
```text
1 REST/SSE/workflow request = 1 Langfuse trace
internal steps = observations/spans/generations inside that trace
```
## Root causes
1. `Telemetry.event(...)` used raw `langfuse.event(...)` when available. Depending on the SDK/context, this creates a new top-level trace for every event.
2. `Telemetry.generation(...)` preferred raw `langfuse.generation(...)`, which can also create top-level traces when no current Langfuse observation is active.
3. `LangfuseAnalyticsPublisher` for IC/NOC/GRL also created standalone observations without a deterministic trace context.
4. The LLM provider used `langfuse.openai.AsyncOpenAI` whenever Langfuse was enabled. This auto-instrumentation can create separate `OpenAI-generation` traces. The framework already emits correlated LLM generations through `Telemetry.generation(...)`, so the wrapper caused noisy duplicate top-level traces.
## What was changed
### `agent_framework/observability/telemetry.py`
- Added deterministic Langfuse trace correlation using `trace_id` / `request_id` / `session_id`.
- Injects `trace_context={"trace_id": ...}` when starting Langfuse observations/generations, with backward-compatible TypeError fallback.
- `Telemetry.event(...)` no longer calls raw `langfuse.event(...)` first.
- `Telemetry.generation(...)` no longer prefers raw `langfuse.generation(...)`; it prefers correlated current generation/observation APIs.
- When a span has no explicit `trace_id`, it uses the request id as the trace id and stores it in the observability context.
### `agent_framework/analytics/providers/langfuse.py`
- Added deterministic trace correlation for IC/NOC/GRL analytics events.
- Injects `trace_context` into `start_as_current_observation(...)`.
- Avoids raw `langfuse.event(...)` fallback.
- For legacy SDKs, attempts to create/reuse a deterministic trace with `langfuse.trace(id=...)` and attach spans to it.
### `agent_framework/llm/providers.py`
- Langfuse OpenAI auto-instrumentation is now opt-in.
- Default behavior uses the standard `openai.AsyncOpenAI` client and relies on the framework's own `Telemetry.generation(...)` to create correlated Langfuse generations.
- To re-enable wrapper-based auto-instrumentation, set:
```env
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
```
For this framework, the recommended default is to keep it disabled.
## Expected result
In Langfuse **Tracing > Traces**, you should see one row per business execution, for example:
```text
POST /agent/process-and-stream | man-da8657ac
POST /agent/process-ticket | man-fec67d60
```
When opening a trace, you should see internal observations such as:
```text
http.request
channel_gateway
backoffice.workflow.dispatch
langgraph.node.framework_input_guardrails
langgraph.node.fetch_ticket
langgraph.node.validation
langgraph.node.imdb_enrichment
mcp.tool_call.consultar_imdb_cliente
langgraph.node.treatment_decision
langgraph.node.siebel_sr_opening
framework_output_guardrails
framework_judges
```
## Validation
Run the backend and execute one request. Then verify:
1. The `Traces` screen has one trace row for the request, not one row per node.
2. `OpenAI-generation` no longer appears as a separate top-level trace unless `ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true`.
3. LangGraph node events and IC/NOC/GRL events appear under the same request trace.
## Fix adicional: formato do trace_id no Langfuse SDK v3
O Langfuse SDK v3 exige que `trace_context.trace_id` seja exatamente um valor hexadecimal minúsculo com 32 caracteres.
Como o framework usa IDs de negócio como UUID com hífens (`d411b925-a096-...`) ou session ids (`man-bcbe3e05`), esses valores agora são normalizados antes de serem enviados ao Langfuse:
- UUID com hífens: remove hífens e reaproveita o hexadecimal de 32 caracteres;
- qualquer outro identificador: gera um hash MD5 determinístico de 32 caracteres;
- o valor original continua preservado em metadata como `framework_trace_id`;
- o valor aceito pelo Langfuse fica em `langfuse_trace_id`.
Isso evita erros como:
```text
ValueError: invalid literal for int() with base 16: 'd411b925-a096-465c-adf2-186623b82c19'
```

View File

@@ -0,0 +1,16 @@
# Remoção do LEGACY_OUTPUT_GUARDRAIL
`LEGACY_OUTPUT_GUARDRAIL` era um sinal de compatibilidade associado ao guardrail LLM genérico/catch-all do pipeline antigo.
Na arquitetura atual, os rails calibrados já executam suas próprias decisões e emitem GRL com códigos de negócio específicos, por exemplo `PINJ`, `TOX`, `REVPREC`, `AOFERTA`, `DLEX_OUT` e `RAGSEC`.
Por isso, o emit legado foi removido/suprimido para evitar ruído e duplicidade no Langfuse.
## Regra atual
- Mantém `GRL.001` a `GRL.009` para ciclo e resultado do pipeline.
- Mantém eventos nomeados dos rails calibrados, como `GRL.REVPREC` e `guardrail.output.REVPREC.completed`.
- Suprime eventos genéricos/legados: `LEGACY_OUTPUT_GUARDRAIL`, `LLM_GUARDRAIL` e `LLM_GRL`.
- Remove o auto-append do guardrail LLM genérico controlado por `ENABLE_LLM_GUARDRAIL`.
O uso de LLM permanece nos rails calibrados que precisam dele, usando `llm_profiles.yaml` com os profiles `guardrail` e `grl`.

View File

@@ -0,0 +1,43 @@
# Explicit provider in llm_profiles.yaml
Each LLM profile should declare `provider` explicitly. The resolver can still inherit
missing keys from `profiles.default` and then from `.env`, but explicit `provider`
per profile is safer because each inference point clearly states which client must
be used.
Rules:
- If `llm_profiles.yaml` exists, the selected profile overrides `.env`.
- Missing keys in a selected profile fall back to `profiles.default`.
- Missing keys in `profiles.default` fall back to `.env`.
- `judges.yaml` decides whether an LLM judge exists. There is no
`ENABLE_LLM_JUDGE` gate and no `LLM_JUDGE_FAIL_CLOSED` setting. Judge
fail-open/fail-closed behavior belongs in `judges.yaml`.
- `llm_profiles.yaml` only chooses provider/model/params for the judge profile.
Example test:
```yaml
profiles:
judge:
provider: oci_openai
model: xopenai.gpt-4.1
temperature: 0
max_tokens: 800
```
And enable the LLM judge in `config/judges.yaml`:
```yaml
enabled: true
fail_closed: true
judges:
- code: llm_judge
type: llm
enabled: true
profile: judge
fail_closed: true
```
With that setup, the invalid model must be used by the judge profile and the
judge must fail closed.

View File

@@ -0,0 +1,204 @@
# MCP Cache
O cache MCP é configurado diretamente no `config/tools.yaml`, dentro da própria tool.
Não existe regra chumbada por nome, idioma ou prefixo. Uma tool só usa cache quando declarar explicitamente:
```yaml
tools:
consultar_fatura:
description: Consulta dados resumidos de fatura por msisdn/invoice_id.
mcp_server: telecom
enabled: true
cache:
enabled: true
ttl_seconds: 600
args_schema:
msisdn: string
invoice_id: string
```
Tools sem bloco `cache` não usam cache por padrão:
```yaml
tools:
consultar_titulo_financeiro:
description: Consulta um título financeiro por cliente e contrato.
mcp_server: telecom
enabled: true
args_schema:
customer_id: string
contract_id: string
```
Tools de ação devem ficar sem cache ou com cache explicitamente desabilitado:
```yaml
tools:
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail
enabled: true
tool_type: action
requires: [order_id, reason]
confirmation_required: false
cache:
enabled: false
args_schema:
order_id: string
reason: string
```
## Como a `cache_key` é montada
A chave de cache MCP precisa ser determinística. Ela não pode depender de valores que mudam a cada turno, como `session_id`, `request_id`, `trace_id`, `timestamp`, `intent`, `agent_id` ou `business_context` completo.
A regra implementada é:
```text
mesma tool + mesmos campos declarados no args_schema + mesmos valores = mesma cache_key
```
Exemplo:
```yaml
tools:
consultar_fatura:
cache:
enabled: true
ttl_seconds: 600
```
Com isso, estas duas chamadas geram a mesma chave:
```json
{
"msisdn": "11999999999",
"invoice_id": "12345"
}
```
```json
{
"invoice_id": "12345",
"msisdn": "11999999999",
"session_id": "valor-que-muda",
"trace_id": "valor-que-muda"
}
```
A chave considera automaticamente apenas `msisdn` e `invoice_id` porque eles estão declarados em `args_schema`. Atributos auxiliares fora do contrato da tool são ignorados.
Não é necessário declarar `key_fields` no YAML. A fonte da verdade para a chave é o próprio `args_schema` da tool.
## Configurações globais
```env
ENABLE_MCP_CACHE=true
MCP_CACHE_TTL_SECONDS=300
TOOLS_CONFIG_PATH=./config/tools.yaml
```
`MCP_CACHE_TTL_SECONDS` é apenas fallback. O TTL preferencial vem de cada tool.
## Fluxo
```text
AgentRuntimeMixin._call_mcp_tool()
Lê política cache da tool em tools.yaml
Monta cache_key por tool_name + campos declarados no args_schema
cache ausente/false → IC.MCP_CACHE_BYPASS → chama MCP normalmente
cache.enabled=true → tenta cache
cache hit → IC.MCP_CACHE_HIT → retorna resultado salvo
cache miss → IC.MCP_CACHE_MISS → chama MCP Router
ok=true → IC.MCP_CACHE_SET → salva no cache com TTL da tool
ok=false → IC.MCP_CACHE_NOT_STORED → não salva
```
## Evidências operacionais
O runtime grava logs:
```text
MCP cache bypass
MCP cache hit
MCP cache miss
MCP cache set
MCP cache not stored
```
O runtime também emite eventos IC. Nos eventos `HIT`, `MISS`, `SET` e `NOT_STORED`, o payload inclui `cache_key` e `cache_key_payload` para auditoria:
```text
IC.MCP_CACHE_BYPASS
IC.MCP_CACHE_HIT
IC.MCP_CACHE_MISS
IC.MCP_CACHE_SET
IC.MCP_CACHE_NOT_STORED
```
E eventos de telemetria:
```text
cache.mcp.hit
cache.mcp.miss
cache.mcp.set
```
## Onde está implementado
- `src/agent_framework/runtime/agent_runtime.py`
- `src/agent_framework/mcp/models.py`
- `src/agent_framework/mcp/registry.py`
- `config/tools.yaml`
## Regra de segurança
O default é `cache.enabled=false`. Isso evita cache acidental em tools mutáveis, como abertura de chamado, troca, devolução, cancelamento ou alteração cadastral.
## Exemplo de evidência esperada
Primeira chamada:
```text
IC.MCP_CACHE_MISS tool=consultar_fatura
IC.MCP_CACHE_SET tool=consultar_fatura ttl_seconds=600
```
Segunda chamada com os mesmos `msisdn` e `invoice_id`:
```text
IC.MCP_CACHE_HIT tool=consultar_fatura
```
Se a segunda chamada gerar `MISS`, confira o `cache_key_payload`. Ele deve conter os mesmos argumentos efetivos enviados ao MCP.
## Ordem correta dos eventos
Na primeira chamada cacheável:
```text
IC.MCP_TOOL_REQUESTED
IC.MCP_CACHE_MISS
IC.MCP_TOOL_EXECUTING
IC.MCP_TOOL_EXECUTED
IC.MCP_CACHE_SET
IC.TOOL_CALLED cached=false
```
Na segunda chamada com os mesmos campos de `args_schema`:
```text
IC.MCP_TOOL_REQUESTED
IC.MCP_CACHE_HIT
IC.TOOL_CALLED cached=true
```
Quando houver `IC.MCP_CACHE_HIT`, não deve aparecer `IC.MCP_TOOL_EXECUTING` nem `IC.MCP_TOOL_EXECUTED`, porque o MCP Server não foi chamado.

View File

@@ -0,0 +1,175 @@
# MCP Parameter Extraction (`extract`)
## Objetivo
O recurso `extract` permite que o framework extraia parâmetros adicionais da mensagem do usuário antes da chamada do MCP Server.
Esses parâmetros não fazem parte do Business Context (`customer_key`, `contract_key`, `interaction_key`, `account_key`, `resource_key`, `session_key`). Eles representam dados específicos de uma tool MCP.
Exemplos genéricos:
- período solicitado;
- quantidade solicitada;
- código citado pelo usuário;
- identificador informado na mensagem;
- data textual mencionada;
- qualquer entidade de negócio necessária para a tool.
## Princípio arquitetural
O mecanismo é declarativo e genérico.
O framework não deve conhecer nomes de campos específicos, regras de domínio ou valores possíveis. A semântica de cada campo vem exclusivamente da configuração da tool no `mcp_parameter_mapping.yaml`.
```text
identity.yaml
→ resolve identidade e chaves canônicas
mcp_parameter_mapping.yaml
→ mapeia parâmetros MCP e declara extrações específicas de tool
```
## Quando usar
Use `extract` quando:
1. a informação está presente na mensagem em linguagem natural;
2. a informação não é uma chave canônica do Business Context;
3. a informação só é necessária para uma tool específica;
4. o MCP Server deve receber o valor já estruturado em `args`.
## Quando não usar
Não use `extract` para resolver:
- `customer_key`;
- `contract_key`;
- `interaction_key`;
- `account_key`;
- `resource_key`;
- `session_key`.
Essas chaves pertencem ao mecanismo de identidade e devem ser resolvidas por `identity.yaml`.
## Exemplo de configuração
```yaml
mcp_parameter_mapping:
tools:
minha_tool:
map:
customer_key: customer_id
contract_key: contract_id
session_key: session_id
extract:
parametro_externo:
from: message
type: string
strategy: llm
description: >
Extraia da mensagem do usuário o valor necessário para preencher
parametro_externo. Retorne null quando a informação não estiver
presente no texto.
```
## Fluxo de execução
```text
Mensagem do usuário
Router identifica intent
Framework escolhe a tool MCP
MCPParameterMapper aplica map/defaults
MCPToolRouter verifica extract da tool escolhida
Executor genérico chama LLM para cada extractor declarado
Campos extraídos são injetados em args
MCP Server é chamado
```
## Exemplo conceitual
Mensagem do usuário:
```text
Texto em linguagem natural contendo uma informação necessária para a tool.
```
Resultado esperado da extração:
```json
{
"parametro_externo": "valor_extraido"
}
```
Payload enviado ao MCP:
```json
{
"customer_id": "123",
"contract_id": "ABC",
"session_id": "S1",
"parametro_externo": "valor_extraido"
}
```
No MCP Server:
```python
valor = args.get("parametro_externo")
```
## Logs esperados
Quando a extração é executada com sucesso:
```text
mcp.parameter.llm_extracted tool=minha_tool field=parametro_externo value=valor_extraido
```
Quando o valor não é encontrado:
```text
mcp.parameter.llm_extracted_null tool=minha_tool field=parametro_externo
```
Quando o LLM não está disponível ou ocorre erro:
```text
mcp.parameter.llm_extract_failed tool=minha_tool field=parametro_externo error=...
```
## Boas práticas
- Mantenha `identity.yaml` apenas para identidade e chaves canônicas.
- Declare parâmetros adicionais no `mcp_parameter_mapping.yaml`.
- Não coloque regras de domínio hardcoded no framework.
- Não coloque parsing de linguagem natural dentro do MCP Server.
- Nomeie os parâmetros extraídos de forma estável.
- Use `description` para orientar o LLM sobre o que deve ser extraído.
## Regra principal
O framework deve executar extração somente quando:
```text
1. a tool MCP já foi escolhida;
2. essa tool possui extract configurado;
3. o extractor usa uma estratégia suportada, como strategy: llm.
```
Sem `extract` declarado, nada é extraído.

View File

@@ -0,0 +1,75 @@
# OCI Instance Principal and FastMCP support
This version adds two framework capabilities:
1. OCI SDK authentication with `OCI_AUTH_MODE=instance_principal`.
2. Official MCP/FastMCP client transport in addition to the legacy HTTP mock contract.
## OCI authentication
Supported values:
```env
OCI_AUTH_MODE=config_file # local ~/.oci/config profile, default
OCI_AUTH_MODE=instance_principal # OCI Compute / OKE workload identity via instance principal
OCI_AUTH_MODE=resource_principal # OCI Functions / resource principal contexts
```
For local development, keep:
```env
LLM_PROVIDER=oci_openai
OCI_GENAI_API_KEY=...
```
For OCI runtimes without API keys, use the SDK provider:
```env
LLM_PROVIDER=oci_sdk
OCI_AUTH_MODE=instance_principal
OCI_COMPARTMENT_ID=ocid1.compartment.oc1...
OCI_REGION=sa-saopaulo-1
OCI_GENAI_MODEL=cohere.command-r-plus
```
The same `OCI_AUTH_MODE` is also used by the OCI embedding provider:
```env
EMBEDDING_PROVIDER=oci
OCI_AUTH_MODE=instance_principal
OCI_COMPARTMENT_ID=ocid1.compartment.oc1...
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
```
> Note: `oci_openai` continues to use the OpenAI-compatible endpoint and API key. Instance principal is implemented through `oci_sdk` because it needs OCI request signing.
## FastMCP transport
The previous framework MCP client remains available:
```yaml
servers:
telecom:
transport: http
endpoint: http://localhost:8001/mcp
```
For FastMCP / official MCP Streamable HTTP:
```yaml
servers:
telecom:
transport: fastmcp
endpoint: http://localhost:8001/mcp
```
For MCP SSE:
```yaml
servers:
telecom:
transport: sse
endpoint: http://localhost:8001/sse
```
Tools still use `tools.yaml` and `mcp_parameter_mapping.yaml`. Only the server transport changes.

View File

@@ -0,0 +1,179 @@
# Correção TIM Observer Payload / NOC OTel
Esta versão corrige dois gaps da migração do `agent_framework_oci`:
1. **Pub/Sub flat**: eventos IC/GRL/analytics passam a ser publicados no contrato flat combinado com Data/TIM, sem envelope `{type, payload}` e sem `payload.payload`.
2. **NOC em OpenTelemetry Logs**: eventos NOC passam a ter caminho dedicado para OTel Logs, separado de traces/spans.
3. **Sequence automático**: eventos Pub/Sub flat passam a receber `sequence` incremental por `agentId/sessionId`, preservando valor explícito quando já vier no evento.
## Arquivos alterados/adicionados
- `src/agent_framework/analytics/tim_payload_mapper.py`
- Novo mapper canônico para converter o envelope interno do framework para o payload TIM flat.
- Mantém campos canônicos na raiz.
- Mantém apenas `agentSpecificData` como objeto aninhado.
- `src/agent_framework/analytics/providers/pubsub.py`
- Publica flat por padrão.
- Mantém modo legado por configuração.
- Exclui `NOC.*` do Pub/Sub por padrão, seguindo a lib antiga.
- Injeta `sequence` automaticamente no payload flat antes do publish.
- `src/agent_framework/analytics/tim_sequence.py`
- Novo gerador de sequence por `agentId/sessionId`.
- Suporta Redis `INCR` como contador atômico cross-worker/cross-pod.
- Suporta MongoDB com `find_one_and_update` + `$inc`, mantendo paridade com a lib antiga.
- Usa fallback em memória quando o backend compartilhado não estiver disponível, sem quebrar o fluxo de observabilidade.
- Preserva `sequence` explícito quando o chamador já informou o campo.
- `src/agent_framework/observability/noc_otel.py`
- Novo exportador dedicado de NOC para OpenTelemetry Logs.
- Usa `OTLPLogExporter` e `LoggingHandler`.
- Aplica DE/PARA flat com `keep_none=True`.
- Achata dict/list para string JSON antes de enviar ao OTel.
- `src/agent_framework/observability/observer.py`
- `emit_noc()` agora dispara o canal dedicado de OTel Logs antes da publicação analytics.
- `src/agent_framework/config/settings.py`
- Novas variáveis de configuração.
## Novas variáveis
```env
# Pub/Sub: padrão corrigido para TIM/Data
PUBSUB_PAYLOAD_MODE=flat
PUBSUB_EXCLUDE_NOC=true
# Sequence automático por sessão no payload Pub/Sub flat
PUBSUB_SEQUENCE_ENABLED=true
# auto = Redis se configurado; senão MongoDB se configurado; senão fallback em memória.
# Para o BO sem OCI Cache, usar mongodb para manter paridade com a lib antiga.
PUBSUB_SEQUENCE_PROVIDER=mongodb
# Opção Redis, quando existir cache disponível
# PUBSUB_SEQUENCE_REDIS_URL=redis://localhost:6379/0
# Opção MongoDB, equivalente ao comportamento antigo via find_one_and_update + $inc
PUBSUB_SEQUENCE_MONGODB_URI=mongodb://localhost:27017
PUBSUB_SEQUENCE_MONGODB_DATABASE=agent_platform
PUBSUB_SEQUENCE_MONGODB_COLLECTION=${AGENT_NAME}_event_counters
PUBSUB_SEQUENCE_TTL_SECONDS=86400
PUBSUB_SEQUENCE_MEMORY_FALLBACK=true
PUBSUB_SEQUENCE_KEY_PREFIX=observer:sequence
# Para voltar temporariamente ao formato antigo envelopado
# PUBSUB_PAYLOAD_MODE=legacy
# NOC via OpenTelemetry Logs
ENABLE_NOC_OTEL_LOGS=true
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://10.153.35.23/v1/logs
OTEL_EXPORTER_OTLP_HOST_HEADER=tim-ai-atend-agnt-opentelemetry
OTEL_SERVICE_NAME=ai-agent-template
```
## Exemplo de Pub/Sub corrigido
```json
{
"eventType": "IC.FATURA_CONSULTADA",
"version": "1.0",
"eventDate": "2026-06-19T12:00:00+00:00",
"sessionId": "sess-789",
"channelId": "whatsapp",
"agentId": "billing-agent",
"tag": "IC.FATURA_CONSULTADA",
"sequence": 12,
"agentSpecificData": {
"invoiceId": "INV-001"
}
}
```
## Observação
O envelope interno retornado pelo `observer.emit(...)` foi mantido para não quebrar EventBus, Langfuse ou consumidores internos. A correção ocorre no provider Pub/Sub e no novo canal NOC OTel.
## Sequence automático
No modo flat, o provider Pub/Sub chama `ensure_sequence(message)` antes de publicar.
Com `sessionId` presente e sem `sequence` explícito, o framework gera:
```text
observer:sequence:<agentId>:<sessionId> -> INCR
```
Exemplo:
```json
{ "eventType": "IC.001", "sessionId": "sess-1", "agentId": "billing", "sequence": 1 }
{ "eventType": "IC.002", "sessionId": "sess-1", "agentId": "billing", "sequence": 2 }
{ "eventType": "IC.003", "sessionId": "sess-1", "agentId": "billing", "sequence": 3 }
```
Regras:
- Se `sequence` já vier no metadata/payload, ele é preservado.
- Se `sessionId` não existir, o campo não é gerado.
- MongoDB é suportado para cenários sem OCI Cache/Redis e usa operação atômica `find_one_and_update` com `$inc`, como na lib antiga.
- Redis continua suportado quando houver cache disponível, pois `INCR` é atômico entre workers/pods.
- O fallback em memória é apenas best-effort local para ambientes de desenvolvimento ou contingência.
### Sequence com MongoDB
Para ambientes do BO onde não existe OCI Cache/Redis dimensionado, configure:
```env
PUBSUB_SEQUENCE_ENABLED=true
PUBSUB_SEQUENCE_PROVIDER=mongodb
PUBSUB_SEQUENCE_MONGODB_URI=mongodb://<host>:27017
PUBSUB_SEQUENCE_MONGODB_DATABASE=agent_platform
PUBSUB_SEQUENCE_MONGODB_COLLECTION=${AGENT_NAME}_event_counters
PUBSUB_SEQUENCE_TTL_SECONDS=86400
PUBSUB_SEQUENCE_MEMORY_FALLBACK=true
```
O documento no Mongo usa `_id` igual à chave lógica:
```text
observer:sequence:<agentId>:<sessionId>
```
A atualização é atômica:
```python
find_one_and_update(
{"_id": key},
{"$inc": {"sequence": 1}},
upsert=True,
return_document=AFTER,
)
```
Também é criado, em best-effort, um índice TTL sobre `expiresAt`. Se o usuário Mongo não tiver permissão para criar índice, a geração de sequence continua funcionando; apenas a limpeza automática pode depender de rotina externa.
### Collection Mongo compatível com legado
Quando `PUBSUB_SEQUENCE_PROVIDER=mongodb`, a collection dos contadores pode ser informada explicitamente:
```env
PUBSUB_SEQUENCE_MONGODB_COLLECTION=telecom_contas_event_counters
```
Se essa variável não for definida, o framework usa o padrão legado:
```text
{AGENT_NAME}_event_counters
```
Também são aceitos, por compatibilidade operacional:
```env
MONGODB_EVENT_COUNTERS_COLLECTION=telecom_contas_event_counters
EVENT_COUNTERS_COLLECTION=telecom_contas_event_counters
```