bugfix: Propagação de sessionId no langfuse

This commit is contained in:
2026-08-14 17:22:36 -03:00
parent 2ed66ed6fe
commit eaf6ea85b1
3 changed files with 163 additions and 9 deletions

View File

@@ -0,0 +1,62 @@
# Langfuse native `sessionId` fix
## Problema
O Agent Framework já carregava `session_id` no contexto e no metadata de spans,
porém traces criados com Langfuse Python SDK v4 podiam aparecer com
`trace.sessionId = null`. Como consequência, `/api/public/sessions` não retornava
as conversas, embora `metadata.session_id` estivesse presente nas observations.
## Causa
No Langfuse Python SDK v4, atributos correlacionais como `session_id`, `user_id`,
tags e metadata devem ser aplicados por `propagate_attributes`, que é uma função
**de nível de módulo** (`from langfuse import propagate_attributes`).
O framework tinha duas tentativas:
1. `observation.update_trace(session_id=...)` — mantido por compatibilidade, mas
descontinuado no SDK v4;
2. `self.langfuse.propagate_attributes(...)` — formato incompatível com o SDK v4,
pois `propagate_attributes` não é método do client.
## Correção
`Telemetry` agora importa e mantém o callable de módulo do Langfuse v4 e o usa
imediatamente dentro do root span:
```text
agent.gateway_message root span
-> propagate_attributes(
session_id=<agent_session_id>,
user_id=<user_id>,
metadata=<correlation metadata>,
tags=<root tags>,
trace_name=<root span name>
)
-> workflow / child observations
```
O fallback por método do client permanece para compatibilidade com SDKs ou
wrappers anteriores.
## Resultado esperado
Para uma sessão de negócio:
```text
default:telecom_contas:f2a6e957-2c74-49ba-882e-ad14131cf1cc
```
o trace retornado pelo Langfuse deve apresentar:
```json
{
"sessionId": "default:telecom_contas:f2a6e957-2c74-49ba-882e-ad14131cf1cc"
}
```
e `/api/public/sessions` deve materializar/agrupar a sessão.
O `session_id` continua também no metadata do framework para diagnóstico e
retrocompatibilidade.

View File

@@ -318,6 +318,11 @@ class Telemetry:
def __init__(self, settings):
self.settings = settings
self.langfuse = None
# Langfuse SDK v4 exposes propagate_attributes as a module-level
# context manager (from langfuse import propagate_attributes), not as
# a Langfuse client method. Keep the callable on the Telemetry instance
# so the framework can support v4 while preserving legacy fallbacks.
self._langfuse_propagate_attributes = None
self.enabled = bool(getattr(settings, "ENABLE_LANGFUSE", False))
self.event_bus = TelemetryEventBus()
self.otel = OpenTelemetryProvider(settings)
@@ -342,7 +347,12 @@ class Telemetry:
return
try:
from langfuse import Langfuse
try:
from langfuse import propagate_attributes as langfuse_propagate_attributes
except ImportError:
langfuse_propagate_attributes = None
self.langfuse = Langfuse(public_key=public_key, secret_key=secret_key, host=host)
self._langfuse_propagate_attributes = langfuse_propagate_attributes
logger.info("Langfuse habilitado host=%s", host)
except Exception as exc:
logger.exception("Falha ao inicializar Langfuse: %s", exc)
@@ -884,25 +894,47 @@ class Telemetry:
except Exception: logger.debug("Trace input/output update não suportado", exc_info=True)
def _start_trace_attribute_propagation(self, name: str, attrs: dict[str, Any]):
if not self.is_enabled() or not hasattr(self.langfuse, "propagate_attributes"):
"""Propagate native Langfuse trace attributes, including session_id.
Langfuse Python SDK v4 moved ``propagate_attributes`` to a module-level
context manager. Calling ``observation.update_trace(session_id=...)`` is
not sufficient/recommended in v4 and, in practice, left ``sessionId``
unset on traces even though the framework metadata contained
``session_id``.
Prefer the v4 module-level callable imported during initialization. A
client-method fallback is retained for older/custom SDK versions.
"""
if not self.is_enabled():
return None
metadata = {
k: attrs.get(k)
for k in ("request_id", "trace_id", "agent_id", "tenant_id", "channel", "message_id", "ura_call_id", "workflow_id")
if attrs.get(k)
}
tags = attrs.get("tags") if isinstance(attrs.get("tags"), list) else None
kwargs = {
"user_id": str(attrs["user_id"]) if attrs.get("user_id") is not None else None,
"session_id": str(attrs["session_id"]) if attrs.get("session_id") is not None else None,
"metadata": metadata or None,
"tags": [str(tag) for tag in tags] if tags else None,
"trace_name": name,
}
try:
return self.langfuse.propagate_attributes(
user_id=str(attrs["user_id"]) if attrs.get("user_id") is not None else None,
session_id=str(attrs["session_id"]) if attrs.get("session_id") is not None else None,
metadata=metadata or None,
tags=[str(tag) for tag in tags] if tags else None,
trace_name=name,
)
# Langfuse SDK v4: ``from langfuse import propagate_attributes``.
if callable(self._langfuse_propagate_attributes):
return self._langfuse_propagate_attributes(**kwargs)
# Backward compatibility for SDK builds/wrappers that exposed the
# propagation context manager on the client instance.
legacy_propagate = getattr(self.langfuse, "propagate_attributes", None)
if callable(legacy_propagate):
return legacy_propagate(**kwargs)
except Exception:
logger.debug("Trace attribute propagation não suportada", exc_info=True)
return None
return None
class _LegacyObservationContext:
def __init__(self, observation): self.observation = observation