verbose live UI + tool-level SSE events + Groq default + regression tests
This commit is contained in:
87
api/langfuse_client.py
Normal file
87
api/langfuse_client.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Langfuse SDK wrapper — single source of truth for the client and spans.
|
||||
|
||||
Used by every layer (tools, analyses, plan, runtime, evals), which is why it
|
||||
lives at the api/ root rather than under any one layer.
|
||||
|
||||
Gracefully returns no-op spans when Langfuse keys aren't configured, so local
|
||||
dev without Langfuse still works.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Iterator
|
||||
|
||||
from api.config import get_settings
|
||||
|
||||
logger = logging.getLogger("nvi.langfuse")
|
||||
|
||||
_client: Any = None
|
||||
|
||||
|
||||
def get_client() -> Any | None:
|
||||
global _client
|
||||
if _client is not None:
|
||||
return _client
|
||||
s = get_settings()
|
||||
if not s.langfuse_public_key or not s.langfuse_secret_key:
|
||||
return None
|
||||
try:
|
||||
from langfuse import Langfuse
|
||||
_client = Langfuse(
|
||||
public_key=s.langfuse_public_key,
|
||||
secret_key=s.langfuse_secret_key,
|
||||
host=s.langfuse_host,
|
||||
)
|
||||
return _client
|
||||
except Exception as e:
|
||||
logger.warning("langfuse init failed: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
class _NullSpan:
|
||||
"""No-op stand-in when Langfuse isn't configured."""
|
||||
def update(self, **_: Any) -> None: ...
|
||||
def end(self, **_: Any) -> None: ...
|
||||
def __enter__(self) -> "_NullSpan": return self
|
||||
def __exit__(self, *_: Any) -> None: ...
|
||||
|
||||
|
||||
@contextmanager
|
||||
def span(
|
||||
name: str,
|
||||
*,
|
||||
as_type: str = "span",
|
||||
input: Any = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> Iterator[Any]:
|
||||
"""Open a Langfuse observation; yields the span object or a no-op.
|
||||
|
||||
Exceptions raised by the *body* of the `with` block propagate up
|
||||
untouched. Only Langfuse's own setup failures fall through to a no-op
|
||||
span — we never want to mask a real error.
|
||||
"""
|
||||
lf = get_client()
|
||||
if lf is None:
|
||||
yield _NullSpan()
|
||||
return
|
||||
try:
|
||||
cm = lf.start_as_current_observation(
|
||||
name=name, as_type=as_type, input=input, metadata=metadata or {}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("langfuse setup for %s failed: %s", name, e)
|
||||
yield _NullSpan()
|
||||
return
|
||||
with cm as s:
|
||||
yield s
|
||||
|
||||
|
||||
def flush() -> None:
|
||||
lf = get_client()
|
||||
if lf is not None:
|
||||
try:
|
||||
lf.flush()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user