""" 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