Files
compass_backoffice/src/providers/langfuse_provider.py
2026-06-13 08:23:21 -03:00

113 lines
4.0 KiB
Python

"""
Local Langfuse client wrapper with enriched error handling.
The get_prompt_from_langfuse() method from agent_framework.prompts silently
captures all exceptions without indicating the root cause of the failure.
This module replaces that function with a wrapper that:
- Distinguishes between authentication errors, prompt not found, and network errors.
- Logs the root cause in a structured way.
- Supports retrieval by label (e.g. "production") instead of version "latest".
"""
import langfuse
from typing import Optional, Tuple
from src.core.logging import get_logger
from src.core.config import settings
logger = get_logger(__name__)
def _build_httpx_client():
"""Mirror setup_observer's behavior: honor settings.OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE."""
cert_path = settings.OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE
if not cert_path:
return None
try:
import httpx
return httpx.Client(verify=cert_path)
except Exception as exc:
logger.warning("Failed to build httpx client for Langfuse provider: %s", exc)
return None
def get_prompt_with_config_from_langfuse(
prompt_name: str,
label: str = "production",
) -> Tuple[Optional[str], dict]:
"""
Fetches a text prompt + its `config` dict from Langfuse.
Langfuse permite anexar um objeto JSON `config` ao lado do corpo do
prompt (editável pela mesma UI). Usamos isso para guardar tuning
parameters que o squad de prompt-engineering pode ajustar sem deploy
(ex.: tamanho de janelas de histórico, thresholds, flags).
Args:
prompt_name: Nome do prompt registrado no Langfuse.
label: Label a recuperar (default: "production").
Returns:
Tupla `(content, config)`. `content=None` quando o prompt não foi
encontrado ou houve erro de rede/auth — nesses casos o caller deve
cair em fallback local. `config` é `{}` quando o prompt não tem
config setado.
"""
try:
client_kwargs = {
"public_key": settings.LANGFUSE_PUBLIC_KEY,
"secret_key": settings.LANGFUSE_SECRET_KEY,
"host": settings.LANGFUSE_BASE_URL,
}
httpx_client = _build_httpx_client()
if httpx_client is not None:
client_kwargs["httpx_client"] = httpx_client
client = langfuse.Langfuse(**client_kwargs)
except Exception as e:
logger.error(
"Failed to initialize the Langfuse client. Check LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY and LANGFUSE_BASE_URL. "
f"Error: {e}"
)
return None, {}
try:
prompt_obj = client.get_prompt(name=prompt_name, label=label)
except Exception as e:
logger.error(
f"Unexpected error while fetching prompt '{prompt_name}' from Langfuse: "
f"{type(e).__name__}: {e}"
)
return None, {}
# The Langfuse v3 SDK returns different types depending on the prompt type.
# For text prompts (type="text"), the content is in the .prompt attribute.
if prompt_obj is None:
logger.warning(f"Langfuse returned None for prompt '{prompt_name}'.")
return None, {}
# Try both known SDK attributes to ensure version compatibility.
content = getattr(prompt_obj, "prompt", None) or getattr(prompt_obj, "content", None)
if not content:
logger.warning(
f"Prompt '{prompt_name}' found but has no content. "
f"Available attributes: {[a for a in dir(prompt_obj) if not a.startswith('_')]}"
)
return None, {}
# `config` é opcional no Langfuse. Normalizamos para dict para que o
# caller possa sempre fazer `config.get(...)` sem checar tipo.
raw_config = getattr(prompt_obj, "config", None)
config = raw_config if isinstance(raw_config, dict) else {}
return content, config
def get_prompt_from_langfuse(
prompt_name: str,
label: str = "production",
) -> Optional[str]:
"""Backwards-compatible wrapper que ignora o `config` do prompt."""
content, _config = get_prompt_with_config_from_langfuse(prompt_name, label)
return content