"""Langfuse trace-attribute metadata builders. The Langfuse v4 ``langchain.CallbackHandler`` lifts a fixed set of reserved keys from ``RunnableConfig.metadata`` onto the root trace: - ``langfuse_session_id`` → groups traces (LangGraph thread → Langfuse Session) - ``langfuse_user_id`` → trace user_id (powers the Users page) - ``langfuse_trace_name`` → human-readable trace name - ``langfuse_tags`` → trace tags See ``langfuse/langchain/CallbackHandler.py::_parse_langfuse_trace_attributes`` and https://langfuse.com/docs/observability/features/sessions for the contract. Builders here exist so the gateway/run worker can inject the right metadata without leaking Langfuse internals into the call sites. """ from __future__ import annotations from typing import Any from deerflow.config import get_enabled_tracing_providers # Lazy-imported below to avoid a circular import: ``deerflow.runtime`` eagerly # imports the run worker, which in turn needs ``deerflow.tracing``. _DEFAULT_TRACE_NAME = "lead-agent" def build_langfuse_trace_metadata( *, thread_id: str | None, user_id: str | None = None, assistant_id: str | None = None, model_name: str | None = None, environment: str | None = None, ) -> dict[str, Any]: """Return Langfuse trace-attribute metadata for ``RunnableConfig.metadata``. Returns ``{}`` when Langfuse is not in the enabled tracing providers so callers can unconditionally merge the result without affecting LangSmith or other tracers. Args: thread_id: LangGraph thread id; mapped to ``langfuse_session_id``. user_id: Effective user id; falls back to ``DEFAULT_USER_ID`` when ``None`` so the Langfuse Users page works in no-auth mode. assistant_id: Optional agent identifier; defaults to ``"lead-agent"``. model_name: Model name; emitted as ``model:`` in ``langfuse_tags``. environment: Deployment env (e.g. ``"production"``); emitted as ``env:`` in ``langfuse_tags``. """ if "langfuse" not in get_enabled_tracing_providers(): return {} from deerflow.runtime.user_context import DEFAULT_USER_ID metadata: dict[str, Any] = { "langfuse_session_id": thread_id, "langfuse_user_id": user_id or DEFAULT_USER_ID, "langfuse_trace_name": assistant_id or _DEFAULT_TRACE_NAME, } tags: list[str] = [] if environment: tags.append(f"env:{environment}") if model_name: tags.append(f"model:{model_name}") if tags: metadata["langfuse_tags"] = tags return metadata def inject_langfuse_metadata( config: dict, *, thread_id: str | None, user_id: str | None = None, assistant_id: str | None = None, model_name: str | None = None, environment: str | None = None, ) -> None: """Merge Langfuse trace-attribute metadata into ``config["metadata"]``. Shared by the gateway worker (``runtime/runs/worker.py``) and the embedded client (``client.py``) so the two paths cannot drift apart. Caller-supplied metadata wins via ``setdefault`` — an upstream value for e.g. ``langfuse_session_id`` set by the frontend stays untouched. The ``config`` dict is mutated in place; the call is a no-op when Langfuse is not in the enabled tracing providers. """ langfuse_metadata = build_langfuse_trace_metadata( thread_id=thread_id, user_id=user_id, assistant_id=assistant_id, model_name=model_name, environment=environment, ) if not langfuse_metadata: return merged_metadata = dict(config.get("metadata") or {}) for key, value in langfuse_metadata.items(): merged_metadata.setdefault(key, value) config["metadata"] = merged_metadata