bugfix: max_tokens hardcoded

This commit is contained in:
2026-09-19 00:30:53 -03:00
parent 2ad9afd974
commit e7b0ce4912
45 changed files with 464 additions and 284 deletions

View File

@@ -0,0 +1,29 @@
# Multi-item result preservation fix
## Objective
Preserve the terminal outcome of each item executed by the primary transactional tool. A failure in a later auxiliary/composite step must not erase successful item outcomes or turn an already-completed multi-item operation into a total failure.
## Framework-only change
No agent/template business logic is required for this fix. The runtime now recognizes an explicit structural contract on the requested tool: a `results` array with two or more entries carrying boolean `success` or `ok` fields.
For workflow-backed tools the authoritative source is preferentially:
`workflow.output[tool_name].results`
The framework derives one of:
- `SUCCESS`
- `PARTIAL_SUCCESS`
- `FAILED`
and publishes a generic `metadata.multi_item` summary with item counts.
When one or more primary items completed successfully but the outer wrapper is `ok=false` because a later auxiliary/post-processing step failed, the normalized result preserves the original failure as `original_ok=false` / `secondary_error` while exposing the requested operation as operationally successful (`ok=true`).
The LLM context also receives an explicit generic instruction that successful per-item terminal evidence cannot be overwritten by a secondary error and that mixed outcomes must be reported individually.
## Regression tests
Added `tests/test_multi_item_result_normalization.py` covering:
- 2 success + 1 failure -> `PARTIAL_SUCCESS`;
- all primary items successful + later secondary failure -> primary `SUCCESS` preserved;
- single-item behavior unchanged;
- unrelated nested tool results cannot override failure of the requested primary tool.

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -9,11 +9,18 @@ profiles:
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -9,11 +9,18 @@ profiles:
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -0,0 +1,58 @@
# Correção — `max_tokens` por `llm_profiles.yaml` / `.env`
## Objetivo
Remover `max_tokens` efetivos fixados nos pontos de inferência do framework sem perder compatibilidade com os budgets anteriores.
A precedência usada pelos pontos corrigidos passa a ser:
1. `max_tokens` do profile selecionado em `llm_profiles.yaml`;
2. `max_tokens` do profile `default` do mesmo YAML, quando aplicável ao resolver;
3. `LLM_MAX_TOKENS` configurado no ambiente / `.env`;
4. valor legado do ponto de inferência, usado somente como fallback de compatibilidade.
O parâmetro interno `fallback_max_tokens` **não é uma nova configuração do usuário**. Ele apenas preserva o valor histórico quando não existe configuração externa.
## Pontos corrigidos
- Enterprise Router:
- transaction parameter relevance: fallback legado `256`;
- transaction intent shift: fallback legado `512`;
- multi-intent: fallback legado `768`;
- routing normal: fallback legado `512`.
- MCP parameter extraction: fallback `80`.
- Processing interruption classifier: fallback `8`.
- LLM guardrail: fallback `600`.
- RAG query rewrite: fallback `300`.
- RAG context compression: mantém a fórmula histórica `max(512, max_chars // 3)` apenas como fallback.
- Conversation summary memory: mantém a fórmula histórica `max(256, max_summary_chars // 4)` apenas como fallback.
## Atualização de `llm_profiles.yaml`
Como os quatro pontos do Enterprise Router usam o mesmo profile `router`, o profile foi ajustado para o **maior budget histórico**, conforme solicitado:
```yaml
router:
max_tokens: 768
```
Também foram garantidos os profiles explícitos:
```yaml
processing_interruption_classifier:
max_tokens: 8
mcp_parameter_extraction:
max_tokens: 80
```
Os profiles já existentes foram mantidos:
- `guardrail: 600`
- `rag_rewriter: 300`
- `rag_compressor: 1200`
- `summary_memory: 1200`
## Compatibilidade
Se `llm_profiles.yaml` não estiver disponível e `LLM_MAX_TOKENS` não estiver definido, cada ponto corrigido continua usando seu budget histórico. Portanto a correção não transforma a ausência de configuração em `2048` apenas por causa do default global de `Settings`.

View File

@@ -9,3 +9,11 @@ profiles:
model: mock-judge model: mock-judge
temperature: 0 temperature: 0
max_tokens: 300 max_tokens: 300
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5

View File

@@ -58,7 +58,7 @@ async def classify_processing_interruption(
response = await llm.ainvoke( response = await llm.ainvoke(
[{"role": "system", "content": prompt}], [{"role": "system", "content": prompt}],
temperature=0, temperature=0,
max_tokens=8, fallback_max_tokens=8,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -47,7 +47,7 @@ class LLMGuardrailRail(Guardrail):
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
temperature=0, temperature=0,
max_tokens=600, fallback_max_tokens=600,
profile_name=self.profile_name, profile_name=self.profile_name,
component_name=self.profile_name, component_name=self.profile_name,
generation_name=f"llm.{self.profile_name}", generation_name=f"llm.{self.profile_name}",

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import copy import copy
import logging import logging
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -164,6 +165,66 @@ class LLMProfileResolver:
return effective return effective
def resolve_configured_value(
self,
profile_name: str | None,
key: str,
*,
env_var: str | None = None,
fallback: Any = None,
) -> Any:
"""Resolve one configuration value with an explicit compatibility fallback.
Precedence is intentionally aligned with the framework configuration model:
1. specific ``llm_profiles.yaml`` profile;
2. YAML ``default`` profile;
3. explicitly configured general environment variable;
4. the caller-provided legacy fallback.
The last step is important for old inference points that historically had a
local constant. Pydantic's Settings object always has framework defaults, so
checking only ``settings.LLM_MAX_TOKENS`` would make an *unset* environment
indistinguishable from the global default.
"""
selected_name = self.normalize_profile_name(profile_name)
if self.enabled:
specific_profile = self._profiles.get(selected_name) or {}
default_profile = self._profiles.get("default") or {}
if key in specific_profile and specific_profile.get(key) is not None:
return specific_profile.get(key)
if key in default_profile and default_profile.get(key) is not None:
return default_profile.get(key)
if env_var:
raw = os.getenv(env_var)
if raw is not None and str(raw).strip() != "":
raw = str(raw).strip()
try:
if isinstance(fallback, bool):
return raw.lower() in {"1", "true", "yes", "on"}
if isinstance(fallback, int) and not isinstance(fallback, bool):
return int(raw)
if isinstance(fallback, float):
return float(raw)
except (TypeError, ValueError):
logger.warning(
"Invalid %s=%r for profile=%s key=%s; using legacy fallback=%r",
env_var, raw, selected_name, key, fallback,
)
return fallback
return raw
# Also honor a value explicitly supplied when Settings was instantiated
# programmatically, without treating the class default as configuration.
field_name = env_var or ""
fields_set = getattr(self.settings, "model_fields_set", set()) or set()
if field_name and field_name in fields_set:
value = getattr(self.settings, field_name, None)
if value is not None:
return value
return fallback
def normalize_profile_name(self, profile_name: str | None) -> str: def normalize_profile_name(self, profile_name: str | None) -> str:
return _canonical_profile_name(profile_name) return _canonical_profile_name(profile_name)

View File

@@ -370,6 +370,14 @@ class OCICompatibleOpenAIProvider(LLMProvider):
profile_name = kwargs.pop("profile_name", None) profile_name = kwargs.pop("profile_name", None)
component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default" component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default"
generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}" generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}"
fallback_max_tokens = kwargs.pop("fallback_max_tokens", None)
if fallback_max_tokens is not None:
kwargs["max_tokens"] = self.profile_resolver.resolve_configured_value(
profile_name,
"max_tokens",
env_var="LLM_MAX_TOKENS",
fallback=fallback_max_tokens,
)
generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name) generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name)
effective = self.profile_resolver.resolve(profile_name, **kwargs) effective = self.profile_resolver.resolve(profile_name, **kwargs)
provider = str(effective.get("provider") or self.provider_name) provider = str(effective.get("provider") or self.provider_name)

View File

@@ -167,7 +167,7 @@ class ConversationSummaryMemory:
summary = await self.llm.ainvoke([ summary = await self.llm.ainvoke([
{"role": "system", "content": "Você resume memória conversacional para agentes corporativos."}, {"role": "system", "content": "Você resume memória conversacional para agentes corporativos."},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory") ], fallback_max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory")
summary = (summary or "").strip() summary = (summary or "").strip()
if not summary: if not summary:
return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars) return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars)

View File

@@ -84,7 +84,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=300, fallback_max_tokens=300,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",
@@ -112,7 +112,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=max(512, max_chars // 3), fallback_max_tokens=max(512, max_chars // 3),
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -1045,7 +1045,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=256, fallback_max_tokens=256,
profile_name="router", profile_name="router",
component_name="router.transaction_parameter_relevance", component_name="router.transaction_parameter_relevance",
generation_name="llm.transaction_parameter_relevance", generation_name="llm.transaction_parameter_relevance",
@@ -1328,7 +1328,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.transaction_intent_shift", generation_name="llm.transaction_intent_shift",
@@ -1674,7 +1674,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=768, fallback_max_tokens=768,
profile_name="router", profile_name="router",
component_name="router.multi_intent", component_name="router.multi_intent",
generation_name="llm.router.multi_intent", generation_name="llm.router.multi_intent",
@@ -1730,7 +1730,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.router", generation_name="llm.router",

View File

@@ -849,7 +849,7 @@ class AgentRuntimeMixin:
component_name="mcp_parameter_extraction", component_name="mcp_parameter_extraction",
generation_name="llm.mcp_parameter_extraction", generation_name="llm.mcp_parameter_extraction",
temperature=0.0, temperature=0.0,
max_tokens=80, fallback_max_tokens=80,
) )
raw = self._llm_response_text(response).strip() raw = self._llm_response_text(response).strip()
payload = parse_json_object(raw) payload = parse_json_object(raw)

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
@@ -72,6 +78,13 @@ profiles:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0.2 temperature: 0.2
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5
transaction_parameter_extraction: transaction_parameter_extraction:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1-mini model: openai.gpt-4.1-mini

View File

@@ -15,11 +15,18 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
# Safety / evaluation # Safety / evaluation
guardrail: guardrail:
@@ -87,3 +94,11 @@ profiles:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0.2 temperature: 0.2
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5

View File

@@ -58,7 +58,7 @@ async def classify_processing_interruption(
response = await llm.ainvoke( response = await llm.ainvoke(
[{"role": "system", "content": prompt}], [{"role": "system", "content": prompt}],
temperature=0, temperature=0,
max_tokens=8, fallback_max_tokens=8,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -49,7 +49,7 @@ class LLMGuardrailRail(Guardrail):
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
temperature=0, temperature=0,
max_tokens=600, fallback_max_tokens=600,
profile_name=self.profile_name, profile_name=self.profile_name,
component_name=self.profile_name, component_name=self.profile_name,
generation_name=f"llm.{self.profile_name}", generation_name=f"llm.{self.profile_name}",

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import copy import copy
import logging import logging
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -164,6 +165,66 @@ class LLMProfileResolver:
return effective return effective
def resolve_configured_value(
self,
profile_name: str | None,
key: str,
*,
env_var: str | None = None,
fallback: Any = None,
) -> Any:
"""Resolve one configuration value with an explicit compatibility fallback.
Precedence is intentionally aligned with the framework configuration model:
1. specific ``llm_profiles.yaml`` profile;
2. YAML ``default`` profile;
3. explicitly configured general environment variable;
4. the caller-provided legacy fallback.
The last step is important for old inference points that historically had a
local constant. Pydantic's Settings object always has framework defaults, so
checking only ``settings.LLM_MAX_TOKENS`` would make an *unset* environment
indistinguishable from the global default.
"""
selected_name = self.normalize_profile_name(profile_name)
if self.enabled:
specific_profile = self._profiles.get(selected_name) or {}
default_profile = self._profiles.get("default") or {}
if key in specific_profile and specific_profile.get(key) is not None:
return specific_profile.get(key)
if key in default_profile and default_profile.get(key) is not None:
return default_profile.get(key)
if env_var:
raw = os.getenv(env_var)
if raw is not None and str(raw).strip() != "":
raw = str(raw).strip()
try:
if isinstance(fallback, bool):
return raw.lower() in {"1", "true", "yes", "on"}
if isinstance(fallback, int) and not isinstance(fallback, bool):
return int(raw)
if isinstance(fallback, float):
return float(raw)
except (TypeError, ValueError):
logger.warning(
"Invalid %s=%r for profile=%s key=%s; using legacy fallback=%r",
env_var, raw, selected_name, key, fallback,
)
return fallback
return raw
# Also honor a value explicitly supplied when Settings was instantiated
# programmatically, without treating the class default as configuration.
field_name = env_var or ""
fields_set = getattr(self.settings, "model_fields_set", set()) or set()
if field_name and field_name in fields_set:
value = getattr(self.settings, field_name, None)
if value is not None:
return value
return fallback
def normalize_profile_name(self, profile_name: str | None) -> str: def normalize_profile_name(self, profile_name: str | None) -> str:
return _canonical_profile_name(profile_name) return _canonical_profile_name(profile_name)

View File

@@ -370,6 +370,14 @@ class OCICompatibleOpenAIProvider(LLMProvider):
profile_name = kwargs.pop("profile_name", None) profile_name = kwargs.pop("profile_name", None)
component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default" component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default"
generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}" generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}"
fallback_max_tokens = kwargs.pop("fallback_max_tokens", None)
if fallback_max_tokens is not None:
kwargs["max_tokens"] = self.profile_resolver.resolve_configured_value(
profile_name,
"max_tokens",
env_var="LLM_MAX_TOKENS",
fallback=fallback_max_tokens,
)
generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name) generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name)
effective = self.profile_resolver.resolve(profile_name, **kwargs) effective = self.profile_resolver.resolve(profile_name, **kwargs)
provider = str(effective.get("provider") or self.provider_name) provider = str(effective.get("provider") or self.provider_name)

View File

@@ -167,7 +167,7 @@ class ConversationSummaryMemory:
summary = await self.llm.ainvoke([ summary = await self.llm.ainvoke([
{"role": "system", "content": "Você resume memória conversacional para agentes corporativos."}, {"role": "system", "content": "Você resume memória conversacional para agentes corporativos."},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory") ], fallback_max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory")
summary = (summary or "").strip() summary = (summary or "").strip()
if not summary: if not summary:
return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars) return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars)

View File

@@ -98,7 +98,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=300, fallback_max_tokens=300,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",
@@ -126,7 +126,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=max(512, max_chars // 3), fallback_max_tokens=max(512, max_chars // 3),
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -1045,7 +1045,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=256, fallback_max_tokens=256,
profile_name="router", profile_name="router",
component_name="router.transaction_parameter_relevance", component_name="router.transaction_parameter_relevance",
generation_name="llm.transaction_parameter_relevance", generation_name="llm.transaction_parameter_relevance",
@@ -1328,7 +1328,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.transaction_intent_shift", generation_name="llm.transaction_intent_shift",
@@ -1674,7 +1674,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=768, fallback_max_tokens=768,
profile_name="router", profile_name="router",
component_name="router.multi_intent", component_name="router.multi_intent",
generation_name="llm.router.multi_intent", generation_name="llm.router.multi_intent",
@@ -1730,7 +1730,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.router", generation_name="llm.router",

View File

@@ -849,7 +849,7 @@ class AgentRuntimeMixin:
component_name="mcp_parameter_extraction", component_name="mcp_parameter_extraction",
generation_name="llm.mcp_parameter_extraction", generation_name="llm.mcp_parameter_extraction",
temperature=0.0, temperature=0.0,
max_tokens=80, fallback_max_tokens=80,
) )
raw = self._llm_response_text(response).strip() raw = self._llm_response_text(response).strip()
payload = parse_json_object(raw) payload = parse_json_object(raw)

View File

@@ -1,250 +0,0 @@
###############################################################################
# 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://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com/openai/v1
#OCI_GENAI_MODEL=ocid1.generativeaiendpoint.oc1.eu-frankfurt-1.amaaaaaad6nji3aa5fmicyft7qci6snu5idhwoomviehnsv4lrostzbmenkq
#OCI_GENAI_API_KEY=sk-TB0OzfrhPLXbQTqZ4aKlrLpcgjHAcn11O1o8C4I4aPvWF0Vx
#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=true
# Providers aceitos: oci_streaming,pubsub,noop
ANALYTICS_PROVIDERS=pubsub
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC=teste_pubsub
PUBSUB_TOPIC_PATH=projects/project-7f829b68-bf43-442c-ad3/topics/teste_pubsub
PROJECT_ID=project-7f829b68-bf43-442c-ad3
GCP_PUBSUB_TOPIC_PATH=projects/project-7f829b68-bf43-442c-ad3/topics/teste_pubsub
GCP_PROJECT_ID=project-7f829b68-bf43-442c-ad3
GCP_PUBSUB_TOPIC=teste_pubsub
GCP_PUBSUB_TIMEOUT_SECONDS=30
# Credencial GCP segue padrão Google:
GOOGLE_APPLICATION_CREDENTIALS=/mnt/d/Dropbox/ORACLE/TIM/FY27/Testes/sa-hoshikawa.json
PUBSUB_SEQUENCE_ENABLED=true
PUBSUB_SEQUENCE_PROVIDER=mongodb
#PUBSUB_SEQUENCE_MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
#PUBSUB_SEQUENCE_MONGODB_DATABASE=agent_platform
PUBSUB_SEQUENCE_MONGODB_URI=mongodb://ADMIN:Moniquinha1972@srlrz1eg.adb.sa-saopaulo-1.oraclecloudapps.com:37017/ADMIN?authMechanism=PLAIN&authSource=$external&ssl=true&retryWrites=false&loadBalanced=true
PUBSUB_SEQUENCE_MONGODB_DATABASE=ADMIN
PUBSUB_SEQUENCE_MONGODB_COLLECTION=observer_event_counters
PUBSUB_SEQUENCE_TTL_SECONDS=86400
PUBSUB_SEQUENCE_KEY_PREFIX=observer:sequence
PUBSUB_SEQUENCE_MEMORY_FALLBACK=false
SIMULATE_AGA_MISSING_TRANSACTION_ID=true
###############################################################################
# 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
###############################################################################
# RAG provider selection (mutually exclusive at runtime)
###############################################################################
# standard = RAG original do agent_framework_oci (default, backward compatible)
# kbdb = KBDB enterprise via PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE
RAG_PROVIDER=kbdb
# Somente usados quando RAG_PROVIDER=kbdb. Se vazios, credenciais caem para ADB_*.
KBDB_DB_USER=KB_USER
KBDB_DB_PASSWORD=SuperStr0ngPa##w0rd
KBDB_DB_DSN=kbdb_public_high
KBDB_DB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/TIM/FY27/kb/Wallet_kbdb
KBDB_DB_WALLET_PASSWORD=SuperStr0ngPa##w0rd
KBDB_SEARCH_TYPE=hybrid
KBDB_NODE_EXPANSION=true
KBDB_NODE_MAX_RELATED=8
KBDB_GRAPH_CROSS_REF=false
KBDB_MAX_CROSS_REF_HOPS=1
KBDB_DOCUMENT_TYPE=customer_safe
KBDB_METADATA_JSON=
KBDB_MIN_SCORE=0.5
KBDB_IDENTIFY_DOCUMENT=true
KBDB_STORE_QUERY=true
KBDB_IDENTIFY_TOP_N=3

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
from types import SimpleNamespace
import yaml
ROOT = Path(__file__).resolve().parents[1]
LIB = ROOT / "libs" / "agent_framework" / "src"
if str(LIB) not in sys.path:
sys.path.insert(0, str(LIB))
from agent_framework.llm.profile_resolver import LLMProfileResolver
def _settings(path: Path):
return SimpleNamespace(
LLM_PROFILES_PATH=str(path),
LLM_PROVIDER="oci_openai",
OCI_GENAI_MODEL="openai.gpt-4.1",
LLM_TEMPERATURE=0.2,
LLM_MAX_TOKENS=2048,
LLM_TIMEOUT_SECONDS=120,
OCI_GENAI_BASE_URL="",
OCI_GENAI_API_KEY=None,
OCI_GENAI_PROJECT_OCID=None,
OCI_AUTH_MODE="config_file",
OCI_GENAI_ENDPOINT=None,
OCI_REGION="",
model_fields_set=set(),
)
def test_profile_max_tokens_wins_over_env_and_legacy_fallback(tmp_path, monkeypatch):
profile = tmp_path / "llm_profiles.yaml"
profile.write_text(
"profiles:\n"
" default:\n"
" provider: oci_openai\n"
" router:\n"
" max_tokens: 768\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_MAX_TOKENS", "999")
resolver = LLMProfileResolver.from_settings(_settings(profile))
assert resolver.resolve_configured_value(
"router", "max_tokens", env_var="LLM_MAX_TOKENS", fallback=512
) == 768
def test_env_max_tokens_wins_when_profiles_file_is_absent(tmp_path, monkeypatch):
missing = tmp_path / "missing.yaml"
monkeypatch.setenv("LLM_MAX_TOKENS", "901")
resolver = LLMProfileResolver.from_settings(_settings(missing))
assert resolver.resolve_configured_value(
"router", "max_tokens", env_var="LLM_MAX_TOKENS", fallback=512
) == 901
def test_legacy_fallback_is_kept_when_yaml_and_env_are_absent(tmp_path, monkeypatch):
missing = tmp_path / "missing.yaml"
monkeypatch.delenv("LLM_MAX_TOKENS", raising=False)
resolver = LLMProfileResolver.from_settings(_settings(missing))
assert resolver.resolve_configured_value(
"router", "max_tokens", env_var="LLM_MAX_TOKENS", fallback=512
) == 512
def test_canonical_profiles_keep_existing_budgets_and_router_uses_largest_value():
profile_path = ROOT / "libs" / "agent_framework" / "config" / "llm_profiles.yaml"
profiles = yaml.safe_load(profile_path.read_text(encoding="utf-8"))["profiles"]
assert profiles["router"]["max_tokens"] == 768
assert profiles["processing_interruption_classifier"]["max_tokens"] == 8
assert profiles["mcp_parameter_extraction"]["max_tokens"] == 80
assert profiles["guardrail"]["max_tokens"] == 600
assert profiles["rag_rewriter"]["max_tokens"] == 300
assert profiles["rag_compressor"]["max_tokens"] == 1200
assert profiles["summary_memory"]["max_tokens"] == 1200
def test_inference_points_use_fallback_max_tokens_instead_of_direct_max_tokens():
checks = {
"libs/agent_framework/src/agent_framework/routing/enterprise_router.py": [256, 512, 768],
"libs/agent_framework/src/agent_framework/runtime/agent_runtime.py": [80],
"libs/agent_framework/src/agent_framework/channels/interruption.py": [8],
"libs/agent_framework/src/agent_framework/guardrails/llm_rails.py": [600],
"libs/agent_framework/src/agent_framework/rag/rag_service.py": [300],
}
for relative, values in checks.items():
text = (ROOT / relative).read_text(encoding="utf-8")
for value in values:
assert f"fallback_max_tokens={value}" in text
assert f"max_tokens={value}," not in text.replace(f"fallback_max_tokens={value},", "")