langfuse
Langfuse Python SDK — observability, evaluation, and prompt management for LLM applications.
Capabilities:
- Tracing / observability:
@observedecorator,Langfuse.start_observation/start_as_current_observationcontext managers, OpenTelemetry-based; integrations for OpenAI (langfuse.openai) and LangChain (langfuse.langchain.CallbackHandler). - Trace attributes:
propagate_attributes(top-level function) sets user_id, session_id, tags, and metadata on all spans in a context. - Datasets & experiments:
Langfuse.get_dataset,Langfuse.run_experimentfor offline evaluation and regression testing of prompt/model changes (CI support via https://github.com/langfuse/experiment-action andRegressionError). - Evaluation / LLM-as-a-judge:
Evaluationresults from custom or model-based evaluators; scores viaLangfuse.create_score/span.score. - Prompt management:
Langfuse.get_prompt,Langfuse.create_promptwith client-side caching and version/label control. - Full REST API:
Langfuse.api(sync) /Langfuse.async_api(async) clients.
Quickstart:
# env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
from langfuse import get_client
langfuse = get_client()
# Create a span using a context manager
with langfuse.start_as_current_observation(as_type="span", name="process-request") as span:
# Your processing logic here
span.update(output="Processing complete")
# Create a nested generation for an LLM call
with langfuse.start_as_current_observation(as_type="generation", name="llm-response", model="gpt-3.5-turbo") as generation:
# Your LLM call logic here
generation.update(output="Generated response")
# All spans are automatically closed when exiting their context blocks
# Flush events in short-lived applications
langfuse.flush()
Configuration is via constructor args or environment variables: LANGFUSE_PUBLIC_KEY,
LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL (defaults to https://cloud.langfuse.com). See langfuse._client.environment_variables
for the full list.
Docs: https://langfuse.com/docs — machine-readable index: https://langfuse.com/llms.txt
1"""Langfuse Python SDK — observability, evaluation, and prompt management for LLM applications. 2 3Capabilities: 4 5- **Tracing / observability**: `@observe` decorator, `Langfuse.start_observation` / 6 `start_as_current_observation` context managers, OpenTelemetry-based; integrations 7 for OpenAI (`langfuse.openai`) and LangChain (`langfuse.langchain.CallbackHandler`). 8- **Trace attributes**: `propagate_attributes` (top-level function) sets user_id, 9 session_id, tags, and metadata on all spans in a context. 10- **Datasets & experiments**: `Langfuse.get_dataset`, `Langfuse.run_experiment` for 11 offline evaluation and regression testing of prompt/model changes (CI support via 12 https://github.com/langfuse/experiment-action and `RegressionError`). 13- **Evaluation / LLM-as-a-judge**: `Evaluation` results from custom or model-based 14 evaluators; scores via `Langfuse.create_score` / `span.score`. 15- **Prompt management**: `Langfuse.get_prompt`, `Langfuse.create_prompt` with 16 client-side caching and version/label control. 17- **Full REST API**: `Langfuse.api` (sync) / `Langfuse.async_api` (async) clients. 18 19Quickstart: 20 21```python 22# env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL 23from langfuse import get_client 24 25langfuse = get_client() 26 27# Create a span using a context manager 28with langfuse.start_as_current_observation(as_type="span", name="process-request") as span: 29 # Your processing logic here 30 span.update(output="Processing complete") 31 32 # Create a nested generation for an LLM call 33 with langfuse.start_as_current_observation(as_type="generation", name="llm-response", model="gpt-3.5-turbo") as generation: 34 # Your LLM call logic here 35 generation.update(output="Generated response") 36 37# All spans are automatically closed when exiting their context blocks 38 39# Flush events in short-lived applications 40langfuse.flush() 41``` 42 43Configuration is via constructor args or environment variables: `LANGFUSE_PUBLIC_KEY`, 44`LANGFUSE_SECRET_KEY`, `LANGFUSE_BASE_URL` (defaults to https://cloud.langfuse.com). See `langfuse._client.environment_variables` 45for the full list. 46 47Docs: https://langfuse.com/docs — machine-readable index: https://langfuse.com/llms.txt 48 49.. include:: ../README.md 50""" 51 52from langfuse.batch_evaluation import ( 53 BatchEvaluationResult, 54 BatchEvaluationResumeToken, 55 CompositeEvaluatorFunction, 56 EvaluatorInputs, 57 EvaluatorStats, 58 MapperFunction, 59) 60from langfuse.experiment import Evaluation, RegressionError, RunnerContext 61 62from ._client import client as _client_module 63from ._client.attributes import LangfuseOtelSpanAttributes 64from ._client.constants import ObservationTypeLiteral 65from ._client.get_client import get_client 66from ._client.observe import observe 67from ._client.propagation import propagate_attributes 68from ._client.span import ( 69 LangfuseAgent, 70 LangfuseChain, 71 LangfuseEmbedding, 72 LangfuseEvaluator, 73 LangfuseEvent, 74 LangfuseGeneration, 75 LangfuseGuardrail, 76 LangfuseRetriever, 77 LangfuseSpan, 78 LangfuseTool, 79) 80from ._version import __version__ 81from .media import LangfuseMedia, LangfuseMediaReference 82from .span_filter import ( 83 KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES, 84 is_default_export_span, 85 is_genai_span, 86 is_known_llm_instrumentor, 87 is_langfuse_span, 88) 89from .types import ( 90 MaskOtelSpansFunction, 91 MaskOtelSpansParams, 92 MaskOtelSpansResult, 93 OtelSpanData, 94 OtelSpanIdentifier, 95 OtelSpanPatch, 96) 97 98Langfuse = _client_module.Langfuse 99 100__all__ = [ 101 "Langfuse", 102 "LangfuseMedia", 103 "LangfuseMediaReference", 104 "get_client", 105 "observe", 106 "propagate_attributes", 107 "ObservationTypeLiteral", 108 "LangfuseSpan", 109 "LangfuseGeneration", 110 "LangfuseEvent", 111 "LangfuseOtelSpanAttributes", 112 "LangfuseAgent", 113 "LangfuseTool", 114 "LangfuseChain", 115 "LangfuseEmbedding", 116 "LangfuseEvaluator", 117 "LangfuseRetriever", 118 "LangfuseGuardrail", 119 "Evaluation", 120 "EvaluatorInputs", 121 "MapperFunction", 122 "CompositeEvaluatorFunction", 123 "EvaluatorStats", 124 "BatchEvaluationResumeToken", 125 "BatchEvaluationResult", 126 "RunnerContext", 127 "RegressionError", 128 "__version__", 129 "is_default_export_span", 130 "is_langfuse_span", 131 "is_genai_span", 132 "is_known_llm_instrumentor", 133 "KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES", 134 "MaskOtelSpansFunction", 135 "MaskOtelSpansParams", 136 "MaskOtelSpansResult", 137 "OtelSpanData", 138 "OtelSpanIdentifier", 139 "OtelSpanPatch", 140 "experiment", 141 "api", 142]
180class Langfuse: 181 """Main client for Langfuse tracing and platform features. 182 183 This class provides an interface for creating and managing traces, spans, 184 and generations in Langfuse as well as interacting with the Langfuse API. 185 186 The client features a thread-safe singleton pattern for each unique public API key, 187 ensuring consistent trace context propagation across your application. It implements 188 efficient batching of spans with configurable flush settings and includes background 189 thread management for media uploads and score ingestion. 190 191 Configuration is flexible through either direct parameters or environment variables, 192 with graceful fallbacks and runtime configuration updates. 193 194 Attributes: 195 api: Synchronous API client for Langfuse backend communication 196 async_api: Asynchronous API client for Langfuse backend communication 197 _otel_tracer: Internal LangfuseTracer instance managing OpenTelemetry components 198 199 Parameters: 200 public_key (Optional[str]): Your Langfuse public API key. Can also be set via LANGFUSE_PUBLIC_KEY environment variable. 201 secret_key (Optional[str]): Your Langfuse secret API key. Can also be set via LANGFUSE_SECRET_KEY environment variable. 202 base_url (Optional[str]): The Langfuse API base URL. Defaults to "https://cloud.langfuse.com". Can also be set via LANGFUSE_BASE_URL environment variable. 203 host (Optional[str]): Deprecated. Use base_url instead. The Langfuse API host URL. Defaults to "https://cloud.langfuse.com". 204 timeout (Optional[int]): Timeout in seconds for API requests. Defaults to 5 seconds. 205 httpx_client (Optional[httpx.Client]): Custom httpx client for making non-tracing HTTP requests. If not provided, a default client will be created. 206 **Fork safety**: ``httpx.Client`` is thread-safe but not process-safe. When using 207 ``fork()``-based servers (e.g. Gunicorn with ``--preload``), the SDK automatically 208 recreates its internally-managed HTTP client in child processes after fork. A custom 209 ``httpx_client`` is intentionally left as-is (the fork-inherited copy is reused), so 210 you retain the opportunity to handle process-safety yourself — for example by 211 registering your own ``os.register_at_fork(after_in_child=...)`` handler to close and 212 reopen connections on the custom client. 213 debug (bool): Enable debug logging. Defaults to False. Can also be set via LANGFUSE_DEBUG environment variable. 214 tracing_enabled (Optional[bool]): Enable or disable tracing. Defaults to True. Can also be set via LANGFUSE_TRACING_ENABLED environment variable. 215 flush_at (Optional[int]): Number of spans to batch before sending to the API. Defaults to 512. Can also be set via LANGFUSE_FLUSH_AT environment variable. 216 flush_interval (Optional[float]): Time in seconds between batch flushes. Defaults to 5 seconds. Can also be set via LANGFUSE_FLUSH_INTERVAL environment variable. 217 environment (Optional[str]): Environment name for tracing. Default is 'default'. Can also be set via LANGFUSE_TRACING_ENVIRONMENT environment variable. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. 218 release (Optional[str]): Release version/hash of your application. Used for grouping analytics by release. 219 media_upload_thread_count (Optional[int]): Number of background threads for handling media uploads. Defaults to 1. Can also be set via LANGFUSE_MEDIA_UPLOAD_THREAD_COUNT environment variable. 220 sample_rate (Optional[float]): Sampling rate for traces (0.0 to 1.0). Defaults to 1.0 (100% of traces are sampled). Can also be set via LANGFUSE_SAMPLE_RATE environment variable. 221 mask (Optional[MaskFunction]): Function to mask sensitive data synchronously when Langfuse SDK attributes are created. This applies only to data set through Langfuse SDK APIs such as `start_observation()`, `update()`, and `set_trace_io()`. 222 mask_otel_spans (Optional[MaskOtelSpansFunction]): Synchronous export-stage hook for masking raw OpenTelemetry span attributes before this Langfuse client sends them to Langfuse. Use this for spans created by third-party OpenTelemetry instrumentations, or when you need to inspect final span attributes after export filtering and Langfuse media handling. It does not modify spans already exported through other OpenTelemetry exporters. 223 224 The hook receives one OpenTelemetry export batch. A batch is not guaranteed to contain a complete trace, request, or Langfuse observation tree. The hook usually runs on the OpenTelemetry batch span processor worker thread; during `flush()` and shutdown it may run on the caller thread. Keep it synchronous, deterministic, and fast. 225 226 Return `None` to leave the batch unchanged. Return `MaskOtelSpansResult` with `OtelSpanPatch` values to delete or replace attributes on selected spans. If a batch contains duplicate trace and span identifiers, Langfuse keeps only the last matching span. If the hook raises or returns an invalid batch result, Langfuse drops the whole export batch. If one returned span patch is invalid, Langfuse drops only that span from the Langfuse export. 227 228 Example: 229 ```python 230 from typing import Optional 231 232 from langfuse import Langfuse 233 from langfuse.types import ( 234 MaskOtelSpansParams, 235 MaskOtelSpansResult, 236 OtelSpanPatch, 237 ) 238 239 def mask_otel_spans( 240 *, params: MaskOtelSpansParams 241 ) -> Optional[MaskOtelSpansResult]: 242 patches = {} 243 244 for identifier, span in params.spans.items(): 245 if "gen_ai.prompt.0.content" in span.attributes: 246 patches[identifier] = OtelSpanPatch( 247 delete_attributes=("gen_ai.prompt.0.content",), 248 set_attributes={"masking.applied": True}, 249 ) 250 251 return MaskOtelSpansResult(span_patches=patches) 252 253 langfuse = Langfuse(mask_otel_spans=mask_otel_spans) 254 ``` 255 blocked_instrumentation_scopes (Optional[List[str]]): Deprecated. Use `should_export_span` instead. Equivalent behavior: 256 ```python 257 from langfuse.span_filter import is_default_export_span 258 blocked = {"sqlite", "requests"} 259 260 should_export_span = lambda span: ( 261 is_default_export_span(span) 262 and ( 263 span.instrumentation_scope is None 264 or span.instrumentation_scope.name not in blocked 265 ) 266 ) 267 ``` 268 should_export_span (Optional[Callable[[ReadableSpan], bool]]): Callback to decide whether to export a span. If omitted, Langfuse uses the default filter (Langfuse SDK spans, spans with `gen_ai.*` attributes, and known LLM instrumentation scopes). 269 additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and in the default OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. If `span_exporter` is provided, these headers are not wired into that exporter and must be configured on the exporter instance directly. 270 tracer_provider(Optional[TracerProvider]): OpenTelemetry TracerProvider to use for Langfuse. This can be useful to set to have disconnected tracing between Langfuse and other OpenTelemetry-span emitting libraries. Note: To track active spans, the context is still shared between TracerProviders. This may lead to broken trace trees. 271 id_generator (Optional[IdGenerator]): OpenTelemetry ID generator to use when Langfuse creates its own TracerProvider. If omitted, the OpenTelemetry SDK default is used. If `tracer_provider` is provided, or an OpenTelemetry TracerProvider is already registered globally, configure the ID generator on that provider instead. 272 span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire `base_url`, exporter headers, exporter auth, or exporter timeout into it. Configure endpoint, headers, and timeout on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, include `x-langfuse-ingestion-version=4` on the exporter to enable real time processing of exported spans. 273 274 Example: 275 ```python 276 from langfuse import Langfuse 277 278 # Initialize the client (reads from env vars if not provided) 279 langfuse = Langfuse( 280 public_key="your-public-key", 281 secret_key="your-secret-key", 282 base_url="https://cloud.langfuse.com", # Optional, default shown 283 ) 284 285 # Create a trace span 286 with langfuse.start_as_current_observation(name="process-query") as span: 287 # Your application code here 288 289 # Create a nested generation span for an LLM call 290 with span.start_as_current_generation( 291 name="generate-response", 292 model="gpt-4", 293 input={"query": "Tell me about AI"}, 294 model_parameters={"temperature": 0.7, "max_tokens": 500} 295 ) as generation: 296 # Generate response here 297 response = "AI is a field of computer science..." 298 299 generation.update( 300 output=response, 301 usage_details={"prompt_tokens": 10, "completion_tokens": 50}, 302 cost_details={"total_cost": 0.0023} 303 ) 304 305 # Score the generation (supports NUMERIC, BOOLEAN, CATEGORICAL) 306 generation.score(name="relevance", value=0.95, data_type="NUMERIC") 307 ``` 308 """ 309 310 _resources: Optional[LangfuseResourceManager] = None 311 _mask: Optional[MaskFunction] = None 312 _otel_tracer: otel_trace_api.Tracer 313 314 def __init__( 315 self, 316 *, 317 public_key: Optional[str] = None, 318 secret_key: Optional[str] = None, 319 base_url: Optional[str] = None, 320 host: Optional[str] = None, 321 timeout: Optional[int] = None, 322 httpx_client: Optional[httpx.Client] = None, 323 debug: bool = False, 324 tracing_enabled: Optional[bool] = True, 325 flush_at: Optional[int] = None, 326 flush_interval: Optional[float] = None, 327 environment: Optional[str] = None, 328 release: Optional[str] = None, 329 media_upload_thread_count: Optional[int] = None, 330 sample_rate: Optional[float] = None, 331 mask: Optional[MaskFunction] = None, 332 mask_otel_spans: Optional[MaskOtelSpansFunction] = None, 333 blocked_instrumentation_scopes: Optional[List[str]] = None, 334 should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, 335 additional_headers: Optional[Dict[str, str]] = None, 336 tracer_provider: Optional[TracerProvider] = None, 337 id_generator: Optional[IdGenerator] = None, 338 span_exporter: Optional[SpanExporter] = None, 339 ): 340 self._base_url = ( 341 base_url 342 or os.environ.get(LANGFUSE_BASE_URL) 343 or host 344 or os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") 345 ) 346 self._environment = environment or cast( 347 str, os.environ.get(LANGFUSE_TRACING_ENVIRONMENT) 348 ) 349 self._release = ( 350 release 351 or os.environ.get(LANGFUSE_RELEASE, None) 352 or get_common_release_envs() 353 ) 354 self._project_id: Optional[str] = None 355 if sample_rate is None: 356 sample_rate = float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0)) 357 if not 0.0 <= sample_rate <= 1.0: 358 raise ValueError( 359 f"Sample rate must be between 0.0 and 1.0, got {sample_rate}" 360 ) 361 362 timeout = timeout or int(os.environ.get(LANGFUSE_TIMEOUT, 5)) 363 364 self._tracing_enabled = ( 365 tracing_enabled 366 and os.environ.get(LANGFUSE_TRACING_ENABLED, "true").lower() != "false" 367 ) 368 if not self._tracing_enabled: 369 langfuse_logger.info( 370 "Configuration: Langfuse tracing is explicitly disabled. No data will be sent to the Langfuse API." 371 ) 372 373 debug = ( 374 debug if debug else (os.getenv(LANGFUSE_DEBUG, "false").lower() == "true") 375 ) 376 if debug: 377 logging.basicConfig( 378 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 379 ) 380 langfuse_logger.setLevel(logging.DEBUG) 381 382 public_key = public_key or os.environ.get(LANGFUSE_PUBLIC_KEY) 383 if public_key is None: 384 langfuse_logger.warning( 385 "Authentication error: Langfuse client initialized without public_key. Client will be disabled. " 386 "Provide a public_key parameter or set LANGFUSE_PUBLIC_KEY environment variable. " 387 ) 388 self._otel_tracer = otel_trace_api.NoOpTracer() 389 return 390 391 secret_key = secret_key or os.environ.get(LANGFUSE_SECRET_KEY) 392 if secret_key is None: 393 langfuse_logger.warning( 394 "Authentication error: Langfuse client initialized without secret_key. Client will be disabled. " 395 "Provide a secret_key parameter or set LANGFUSE_SECRET_KEY environment variable. " 396 ) 397 self._otel_tracer = otel_trace_api.NoOpTracer() 398 return 399 400 if os.environ.get("OTEL_SDK_DISABLED", "false").lower() == "true": 401 langfuse_logger.warning( 402 "OTEL_SDK_DISABLED is set. Langfuse tracing will be disabled and no traces will appear in the UI." 403 ) 404 405 if blocked_instrumentation_scopes is not None: 406 warnings.warn( 407 "`blocked_instrumentation_scopes` is deprecated and will be removed in a future release. " 408 "Use `should_export_span` instead. Example: " 409 "from langfuse.span_filter import is_default_export_span; " 410 'blocked={"scope"}; should_export_span=lambda span: ' 411 "is_default_export_span(span) and (span.instrumentation_scope is None or " 412 "span.instrumentation_scope.name not in blocked).", 413 DeprecationWarning, 414 stacklevel=2, 415 ) 416 417 # Initialize api and tracer if requirements are met 418 self._resources = LangfuseResourceManager( 419 public_key=public_key, 420 secret_key=secret_key, 421 base_url=self._base_url, 422 timeout=timeout, 423 environment=self._environment, 424 release=release, 425 flush_at=flush_at, 426 flush_interval=flush_interval, 427 httpx_client=httpx_client, 428 media_upload_thread_count=media_upload_thread_count, 429 sample_rate=sample_rate, 430 mask=mask, 431 mask_otel_spans=mask_otel_spans, 432 tracing_enabled=self._tracing_enabled, 433 blocked_instrumentation_scopes=blocked_instrumentation_scopes, 434 should_export_span=should_export_span, 435 additional_headers=additional_headers, 436 tracer_provider=tracer_provider, 437 id_generator=id_generator, 438 span_exporter=span_exporter, 439 ) 440 self._mask = self._resources.mask 441 442 self._otel_tracer = ( 443 self._resources.tracer 444 if self._tracing_enabled and self._resources.tracer is not None 445 else otel_trace_api.NoOpTracer() 446 ) 447 448 @property 449 def api(self) -> LangfuseAPI: 450 """Synchronous client for the full Langfuse REST API (traces, observations, scores, datasets, prompts, ...). 451 452 Use this to read or manage data on the Langfuse server; use the tracing methods 453 (`start_observation`, `@observe`) to create traces. Use `async_api` for the 454 asyncio variant. 455 456 Semantics that are easy to miss: 457 458 - **Ingestion is asynchronous.** `langfuse.flush()` only guarantees delivery to 459 the API, not read visibility: reads such as `api.trace.get(trace_id)` may 460 raise `langfuse.api.NotFoundError` until processing completes (typically 461 within 15-30 seconds; longer under load). The same applies to scores and 462 dataset run reads. Instead of a fixed sleep, retry with a deadline: 463 464 - **List endpoints return lightweight views.** `api.trace.list(...)` returns 465 `TraceWithDetails`, where `observations` and `scores` are lists of ID strings. 466 Fetch the full objects with `api.trace.get(trace_id)` (`TraceWithFullDetails`), 467 or prefer `api.observations.get_many(trace_id=...)` for row-level observation 468 queries. The same list-view vs. get-detail pattern applies to other resources. 469 470 - **Prefer the v2 data APIs — they are the defaults since SDK v4.** 471 `api.observations` and `api.metrics` map to the high-performance 472 `/api/public/v2/...` endpoints and are the recommended read path. Their v1 473 equivalents remain available under `api.legacy.observations_v1` / 474 `api.legacy.metrics_v1` but are less performant at scale, not recommended 475 for new workflows, and will be deprecated. 476 477 - For large-scale aggregation (usage/cost by model, user, etc.), prefer the 478 v2 Metrics API (`api.metrics.metrics(...)`) over paginating row-level data. 479 480 481 See also: `async_api`, 482 https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk 483 (ingestion lag: #ingestion-lag, list vs. get: #traces-list-vs-get), 484 https://langfuse.com/docs/api-and-data-platform/features/observations-api, 485 https://langfuse.com/docs/metrics/features/metrics-api 486 """ 487 if self._resources is None: 488 raise AttributeError("Langfuse client is not initialized") 489 490 return self._resources.api 491 492 @api.setter 493 def api(self, value: LangfuseAPI) -> None: 494 if self._resources is None: 495 raise AttributeError("Langfuse client is not initialized") 496 497 self._resources.api = value 498 499 @property 500 def async_api(self) -> AsyncLangfuseAPI: 501 if self._resources is None: 502 raise AttributeError("Langfuse client is not initialized") 503 504 return self._resources.async_api 505 506 @async_api.setter 507 def async_api(self, value: AsyncLangfuseAPI) -> None: 508 if self._resources is None: 509 raise AttributeError("Langfuse client is not initialized") 510 511 self._resources.async_api = value 512 513 @overload 514 def start_observation( 515 self, 516 *, 517 trace_context: Optional[TraceContext] = None, 518 name: str, 519 as_type: Literal["generation"], 520 input: Optional[Any] = None, 521 output: Optional[Any] = None, 522 metadata: Optional[Any] = None, 523 version: Optional[str] = None, 524 level: Optional[SpanLevel] = None, 525 status_message: Optional[str] = None, 526 completion_start_time: Optional[datetime] = None, 527 model: Optional[str] = None, 528 model_parameters: Optional[Dict[str, MapValue]] = None, 529 usage_details: Optional[Dict[str, int]] = None, 530 cost_details: Optional[Dict[str, float]] = None, 531 prompt: Optional[PromptClient] = None, 532 ) -> LangfuseGeneration: ... 533 534 @overload 535 def start_observation( 536 self, 537 *, 538 trace_context: Optional[TraceContext] = None, 539 name: str, 540 as_type: Literal["span"] = "span", 541 input: Optional[Any] = None, 542 output: Optional[Any] = None, 543 metadata: Optional[Any] = None, 544 version: Optional[str] = None, 545 level: Optional[SpanLevel] = None, 546 status_message: Optional[str] = None, 547 ) -> LangfuseSpan: ... 548 549 @overload 550 def start_observation( 551 self, 552 *, 553 trace_context: Optional[TraceContext] = None, 554 name: str, 555 as_type: Literal["agent"], 556 input: Optional[Any] = None, 557 output: Optional[Any] = None, 558 metadata: Optional[Any] = None, 559 version: Optional[str] = None, 560 level: Optional[SpanLevel] = None, 561 status_message: Optional[str] = None, 562 ) -> LangfuseAgent: ... 563 564 @overload 565 def start_observation( 566 self, 567 *, 568 trace_context: Optional[TraceContext] = None, 569 name: str, 570 as_type: Literal["tool"], 571 input: Optional[Any] = None, 572 output: Optional[Any] = None, 573 metadata: Optional[Any] = None, 574 version: Optional[str] = None, 575 level: Optional[SpanLevel] = None, 576 status_message: Optional[str] = None, 577 ) -> LangfuseTool: ... 578 579 @overload 580 def start_observation( 581 self, 582 *, 583 trace_context: Optional[TraceContext] = None, 584 name: str, 585 as_type: Literal["chain"], 586 input: Optional[Any] = None, 587 output: Optional[Any] = None, 588 metadata: Optional[Any] = None, 589 version: Optional[str] = None, 590 level: Optional[SpanLevel] = None, 591 status_message: Optional[str] = None, 592 ) -> LangfuseChain: ... 593 594 @overload 595 def start_observation( 596 self, 597 *, 598 trace_context: Optional[TraceContext] = None, 599 name: str, 600 as_type: Literal["retriever"], 601 input: Optional[Any] = None, 602 output: Optional[Any] = None, 603 metadata: Optional[Any] = None, 604 version: Optional[str] = None, 605 level: Optional[SpanLevel] = None, 606 status_message: Optional[str] = None, 607 ) -> LangfuseRetriever: ... 608 609 @overload 610 def start_observation( 611 self, 612 *, 613 trace_context: Optional[TraceContext] = None, 614 name: str, 615 as_type: Literal["evaluator"], 616 input: Optional[Any] = None, 617 output: Optional[Any] = None, 618 metadata: Optional[Any] = None, 619 version: Optional[str] = None, 620 level: Optional[SpanLevel] = None, 621 status_message: Optional[str] = None, 622 ) -> LangfuseEvaluator: ... 623 624 @overload 625 def start_observation( 626 self, 627 *, 628 trace_context: Optional[TraceContext] = None, 629 name: str, 630 as_type: Literal["embedding"], 631 input: Optional[Any] = None, 632 output: Optional[Any] = None, 633 metadata: Optional[Any] = None, 634 version: Optional[str] = None, 635 level: Optional[SpanLevel] = None, 636 status_message: Optional[str] = None, 637 completion_start_time: Optional[datetime] = None, 638 model: Optional[str] = None, 639 model_parameters: Optional[Dict[str, MapValue]] = None, 640 usage_details: Optional[Dict[str, int]] = None, 641 cost_details: Optional[Dict[str, float]] = None, 642 prompt: Optional[PromptClient] = None, 643 ) -> LangfuseEmbedding: ... 644 645 @overload 646 def start_observation( 647 self, 648 *, 649 trace_context: Optional[TraceContext] = None, 650 name: str, 651 as_type: Literal["guardrail"], 652 input: Optional[Any] = None, 653 output: Optional[Any] = None, 654 metadata: Optional[Any] = None, 655 version: Optional[str] = None, 656 level: Optional[SpanLevel] = None, 657 status_message: Optional[str] = None, 658 ) -> LangfuseGuardrail: ... 659 660 def start_observation( 661 self, 662 *, 663 trace_context: Optional[TraceContext] = None, 664 name: str, 665 as_type: ObservationTypeLiteralNoEvent = "span", 666 input: Optional[Any] = None, 667 output: Optional[Any] = None, 668 metadata: Optional[Any] = None, 669 version: Optional[str] = None, 670 level: Optional[SpanLevel] = None, 671 status_message: Optional[str] = None, 672 completion_start_time: Optional[datetime] = None, 673 model: Optional[str] = None, 674 model_parameters: Optional[Dict[str, MapValue]] = None, 675 usage_details: Optional[Dict[str, int]] = None, 676 cost_details: Optional[Dict[str, float]] = None, 677 prompt: Optional[PromptClient] = None, 678 ) -> Union[ 679 LangfuseSpan, 680 LangfuseGeneration, 681 LangfuseAgent, 682 LangfuseTool, 683 LangfuseChain, 684 LangfuseRetriever, 685 LangfuseEvaluator, 686 LangfuseEmbedding, 687 LangfuseGuardrail, 688 ]: 689 """Create a new observation of the specified type. 690 691 This method creates a new observation but does not set it as the current span in the 692 context. To create and use an observation within a context, use start_as_current_observation(). 693 694 Args: 695 trace_context: Optional context for connecting to an existing trace 696 name: Name of the observation 697 as_type: Type of observation to create (defaults to "span") 698 input: Input data for the operation 699 output: Output data from the operation 700 metadata: Additional metadata to associate with the observation 701 version: Version identifier for the code or component 702 level: Importance level of the observation 703 status_message: Optional status message for the observation 704 completion_start_time: When the model started generating (for generation types) 705 model: Name/identifier of the AI model used (for generation types) 706 model_parameters: Parameters used for the model (for generation types) 707 usage_details: Token usage information (for generation types) 708 cost_details: Cost information (for generation types) 709 prompt: Associated prompt template (for generation types) 710 711 Returns: 712 An observation object of the appropriate type that must be ended with .end() 713 """ 714 if trace_context: 715 trace_id = trace_context.get("trace_id", None) 716 parent_span_id = trace_context.get("parent_span_id", None) 717 718 if trace_id: 719 remote_parent_span = self._create_remote_parent_span( 720 trace_id=trace_id, parent_span_id=parent_span_id 721 ) 722 723 with otel_trace_api.use_span( 724 cast(otel_trace_api.Span, remote_parent_span) 725 ): 726 otel_span = self._otel_tracer.start_span(name=name) 727 otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) 728 729 return self._create_observation_from_otel_span( 730 otel_span=otel_span, 731 as_type=as_type, 732 input=input, 733 output=output, 734 metadata=metadata, 735 version=version, 736 level=level, 737 status_message=status_message, 738 completion_start_time=completion_start_time, 739 model=model, 740 model_parameters=model_parameters, 741 usage_details=usage_details, 742 cost_details=cost_details, 743 prompt=prompt, 744 ) 745 746 otel_span = self._otel_tracer.start_span(name=name) 747 748 return self._create_observation_from_otel_span( 749 otel_span=otel_span, 750 as_type=as_type, 751 input=input, 752 output=output, 753 metadata=metadata, 754 version=version, 755 level=level, 756 status_message=status_message, 757 completion_start_time=completion_start_time, 758 model=model, 759 model_parameters=model_parameters, 760 usage_details=usage_details, 761 cost_details=cost_details, 762 prompt=prompt, 763 ) 764 765 def _create_observation_from_otel_span( 766 self, 767 *, 768 otel_span: otel_trace_api.Span, 769 as_type: ObservationTypeLiteralNoEvent, 770 input: Optional[Any] = None, 771 output: Optional[Any] = None, 772 metadata: Optional[Any] = None, 773 version: Optional[str] = None, 774 level: Optional[SpanLevel] = None, 775 status_message: Optional[str] = None, 776 completion_start_time: Optional[datetime] = None, 777 model: Optional[str] = None, 778 model_parameters: Optional[Dict[str, MapValue]] = None, 779 usage_details: Optional[Dict[str, int]] = None, 780 cost_details: Optional[Dict[str, float]] = None, 781 prompt: Optional[PromptClient] = None, 782 ) -> Union[ 783 LangfuseSpan, 784 LangfuseGeneration, 785 LangfuseAgent, 786 LangfuseTool, 787 LangfuseChain, 788 LangfuseRetriever, 789 LangfuseEvaluator, 790 LangfuseEmbedding, 791 LangfuseGuardrail, 792 ]: 793 """Create the appropriate observation type from an OTEL span.""" 794 if as_type in get_observation_types_list(ObservationTypeGenerationLike): 795 observation_class = self._get_span_class(as_type) 796 # Type ignore to prevent overloads of internal _get_span_class function, 797 # issue is that LangfuseEvent could be returned and that classes have diff. args 798 return observation_class( # type: ignore[return-value,call-arg] 799 otel_span=otel_span, 800 langfuse_client=self, 801 environment=self._environment, 802 release=self._release, 803 input=input, 804 output=output, 805 metadata=metadata, 806 version=version, 807 level=level, 808 status_message=status_message, 809 completion_start_time=completion_start_time, 810 model=model, 811 model_parameters=model_parameters, 812 usage_details=usage_details, 813 cost_details=cost_details, 814 prompt=prompt, 815 ) 816 else: 817 # For other types (e.g. span, guardrail), create appropriate class without generation properties 818 observation_class = self._get_span_class(as_type) 819 # Type ignore to prevent overloads of internal _get_span_class function, 820 # issue is that LangfuseEvent could be returned and that classes have diff. args 821 return observation_class( # type: ignore[return-value,call-arg] 822 otel_span=otel_span, 823 langfuse_client=self, 824 environment=self._environment, 825 release=self._release, 826 input=input, 827 output=output, 828 metadata=metadata, 829 version=version, 830 level=level, 831 status_message=status_message, 832 ) 833 # span._observation_type = as_type 834 # span._otel_span.set_attribute("langfuse.observation.type", as_type) 835 # return span 836 837 @overload 838 def start_as_current_observation( 839 self, 840 *, 841 trace_context: Optional[TraceContext] = None, 842 name: str, 843 as_type: Literal["generation"], 844 input: Optional[Any] = None, 845 output: Optional[Any] = None, 846 metadata: Optional[Any] = None, 847 version: Optional[str] = None, 848 level: Optional[SpanLevel] = None, 849 status_message: Optional[str] = None, 850 completion_start_time: Optional[datetime] = None, 851 model: Optional[str] = None, 852 model_parameters: Optional[Dict[str, MapValue]] = None, 853 usage_details: Optional[Dict[str, int]] = None, 854 cost_details: Optional[Dict[str, float]] = None, 855 prompt: Optional[PromptClient] = None, 856 end_on_exit: Optional[bool] = None, 857 ) -> _AgnosticContextManager[LangfuseGeneration]: ... 858 859 @overload 860 def start_as_current_observation( 861 self, 862 *, 863 trace_context: Optional[TraceContext] = None, 864 name: str, 865 as_type: Literal["span"] = "span", 866 input: Optional[Any] = None, 867 output: Optional[Any] = None, 868 metadata: Optional[Any] = None, 869 version: Optional[str] = None, 870 level: Optional[SpanLevel] = None, 871 status_message: Optional[str] = None, 872 end_on_exit: Optional[bool] = None, 873 ) -> _AgnosticContextManager[LangfuseSpan]: ... 874 875 @overload 876 def start_as_current_observation( 877 self, 878 *, 879 trace_context: Optional[TraceContext] = None, 880 name: str, 881 as_type: Literal["agent"], 882 input: Optional[Any] = None, 883 output: Optional[Any] = None, 884 metadata: Optional[Any] = None, 885 version: Optional[str] = None, 886 level: Optional[SpanLevel] = None, 887 status_message: Optional[str] = None, 888 end_on_exit: Optional[bool] = None, 889 ) -> _AgnosticContextManager[LangfuseAgent]: ... 890 891 @overload 892 def start_as_current_observation( 893 self, 894 *, 895 trace_context: Optional[TraceContext] = None, 896 name: str, 897 as_type: Literal["tool"], 898 input: Optional[Any] = None, 899 output: Optional[Any] = None, 900 metadata: Optional[Any] = None, 901 version: Optional[str] = None, 902 level: Optional[SpanLevel] = None, 903 status_message: Optional[str] = None, 904 end_on_exit: Optional[bool] = None, 905 ) -> _AgnosticContextManager[LangfuseTool]: ... 906 907 @overload 908 def start_as_current_observation( 909 self, 910 *, 911 trace_context: Optional[TraceContext] = None, 912 name: str, 913 as_type: Literal["chain"], 914 input: Optional[Any] = None, 915 output: Optional[Any] = None, 916 metadata: Optional[Any] = None, 917 version: Optional[str] = None, 918 level: Optional[SpanLevel] = None, 919 status_message: Optional[str] = None, 920 end_on_exit: Optional[bool] = None, 921 ) -> _AgnosticContextManager[LangfuseChain]: ... 922 923 @overload 924 def start_as_current_observation( 925 self, 926 *, 927 trace_context: Optional[TraceContext] = None, 928 name: str, 929 as_type: Literal["retriever"], 930 input: Optional[Any] = None, 931 output: Optional[Any] = None, 932 metadata: Optional[Any] = None, 933 version: Optional[str] = None, 934 level: Optional[SpanLevel] = None, 935 status_message: Optional[str] = None, 936 end_on_exit: Optional[bool] = None, 937 ) -> _AgnosticContextManager[LangfuseRetriever]: ... 938 939 @overload 940 def start_as_current_observation( 941 self, 942 *, 943 trace_context: Optional[TraceContext] = None, 944 name: str, 945 as_type: Literal["evaluator"], 946 input: Optional[Any] = None, 947 output: Optional[Any] = None, 948 metadata: Optional[Any] = None, 949 version: Optional[str] = None, 950 level: Optional[SpanLevel] = None, 951 status_message: Optional[str] = None, 952 end_on_exit: Optional[bool] = None, 953 ) -> _AgnosticContextManager[LangfuseEvaluator]: ... 954 955 @overload 956 def start_as_current_observation( 957 self, 958 *, 959 trace_context: Optional[TraceContext] = None, 960 name: str, 961 as_type: Literal["embedding"], 962 input: Optional[Any] = None, 963 output: Optional[Any] = None, 964 metadata: Optional[Any] = None, 965 version: Optional[str] = None, 966 level: Optional[SpanLevel] = None, 967 status_message: Optional[str] = None, 968 completion_start_time: Optional[datetime] = None, 969 model: Optional[str] = None, 970 model_parameters: Optional[Dict[str, MapValue]] = None, 971 usage_details: Optional[Dict[str, int]] = None, 972 cost_details: Optional[Dict[str, float]] = None, 973 prompt: Optional[PromptClient] = None, 974 end_on_exit: Optional[bool] = None, 975 ) -> _AgnosticContextManager[LangfuseEmbedding]: ... 976 977 @overload 978 def start_as_current_observation( 979 self, 980 *, 981 trace_context: Optional[TraceContext] = None, 982 name: str, 983 as_type: Literal["guardrail"], 984 input: Optional[Any] = None, 985 output: Optional[Any] = None, 986 metadata: Optional[Any] = None, 987 version: Optional[str] = None, 988 level: Optional[SpanLevel] = None, 989 status_message: Optional[str] = None, 990 end_on_exit: Optional[bool] = None, 991 ) -> _AgnosticContextManager[LangfuseGuardrail]: ... 992 993 def start_as_current_observation( 994 self, 995 *, 996 trace_context: Optional[TraceContext] = None, 997 name: str, 998 as_type: ObservationTypeLiteralNoEvent = "span", 999 input: Optional[Any] = None, 1000 output: Optional[Any] = None, 1001 metadata: Optional[Any] = None, 1002 version: Optional[str] = None, 1003 level: Optional[SpanLevel] = None, 1004 status_message: Optional[str] = None, 1005 completion_start_time: Optional[datetime] = None, 1006 model: Optional[str] = None, 1007 model_parameters: Optional[Dict[str, MapValue]] = None, 1008 usage_details: Optional[Dict[str, int]] = None, 1009 cost_details: Optional[Dict[str, float]] = None, 1010 prompt: Optional[PromptClient] = None, 1011 end_on_exit: Optional[bool] = None, 1012 ) -> Union[ 1013 _AgnosticContextManager[LangfuseGeneration], 1014 _AgnosticContextManager[LangfuseSpan], 1015 _AgnosticContextManager[LangfuseAgent], 1016 _AgnosticContextManager[LangfuseTool], 1017 _AgnosticContextManager[LangfuseChain], 1018 _AgnosticContextManager[LangfuseRetriever], 1019 _AgnosticContextManager[LangfuseEvaluator], 1020 _AgnosticContextManager[LangfuseEmbedding], 1021 _AgnosticContextManager[LangfuseGuardrail], 1022 ]: 1023 """Create a new observation and set it as the current span in a context manager. 1024 1025 This method creates a new observation of the specified type and sets it as the 1026 current span within a context manager. Use this method with a 'with' statement to 1027 automatically handle the observation lifecycle within a code block. 1028 1029 The created observation will be the child of the current span in the context. 1030 1031 Args: 1032 trace_context: Optional context for connecting to an existing trace 1033 name: Name of the observation (e.g., function or operation name) 1034 as_type: Type of observation to create (defaults to "span") 1035 input: Input data for the operation (can be any JSON-serializable object) 1036 output: Output data from the operation (can be any JSON-serializable object) 1037 metadata: Additional metadata to associate with the observation 1038 version: Version identifier for the code or component 1039 level: Importance level of the observation (info, warning, error) 1040 status_message: Optional status message for the observation 1041 end_on_exit (default: True): Whether to end the span automatically when leaving the context manager. If False, the span must be manually ended to avoid memory leaks. 1042 1043 The following parameters are available when as_type is: "generation" or "embedding". 1044 completion_start_time: When the model started generating the response 1045 model: Name/identifier of the AI model used (e.g., "gpt-4") 1046 model_parameters: Parameters used for the model (e.g., temperature, max_tokens) 1047 usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) 1048 cost_details: Cost information for the model call 1049 prompt: Associated prompt template from Langfuse prompt management 1050 1051 Returns: 1052 A context manager that yields the appropriate observation type based on as_type 1053 1054 Example: 1055 ```python 1056 # Create a span 1057 with langfuse.start_as_current_observation(name="process-query", as_type="span") as span: 1058 # Do work 1059 result = process_data() 1060 span.update(output=result) 1061 1062 # Create a child span automatically 1063 with span.start_as_current_observation(name="sub-operation") as child_span: 1064 # Do sub-operation work 1065 child_span.update(output="sub-result") 1066 1067 # Create a tool observation 1068 with langfuse.start_as_current_observation(name="web-search", as_type="tool") as tool: 1069 # Do tool work 1070 results = search_web(query) 1071 tool.update(output=results) 1072 1073 # Create a generation observation 1074 with langfuse.start_as_current_observation( 1075 name="answer-generation", 1076 as_type="generation", 1077 model="gpt-4" 1078 ) as generation: 1079 # Generate answer 1080 response = llm.generate(...) 1081 generation.update(output=response) 1082 ``` 1083 """ 1084 if as_type in get_observation_types_list(ObservationTypeGenerationLike): 1085 if trace_context: 1086 trace_id = trace_context.get("trace_id", None) 1087 parent_span_id = trace_context.get("parent_span_id", None) 1088 1089 if trace_id: 1090 remote_parent_span = self._create_remote_parent_span( 1091 trace_id=trace_id, parent_span_id=parent_span_id 1092 ) 1093 1094 return cast( 1095 Union[ 1096 _AgnosticContextManager[LangfuseGeneration], 1097 _AgnosticContextManager[LangfuseEmbedding], 1098 ], 1099 self._create_span_with_parent_context( 1100 as_type=as_type, 1101 name=name, 1102 remote_parent_span=remote_parent_span, 1103 parent=None, 1104 end_on_exit=end_on_exit, 1105 input=input, 1106 output=output, 1107 metadata=metadata, 1108 version=version, 1109 level=level, 1110 status_message=status_message, 1111 completion_start_time=completion_start_time, 1112 model=model, 1113 model_parameters=model_parameters, 1114 usage_details=usage_details, 1115 cost_details=cost_details, 1116 prompt=prompt, 1117 ), 1118 ) 1119 1120 return cast( 1121 Union[ 1122 _AgnosticContextManager[LangfuseGeneration], 1123 _AgnosticContextManager[LangfuseEmbedding], 1124 ], 1125 self._start_as_current_otel_span_with_processed_media( 1126 as_type=as_type, 1127 name=name, 1128 end_on_exit=end_on_exit, 1129 input=input, 1130 output=output, 1131 metadata=metadata, 1132 version=version, 1133 level=level, 1134 status_message=status_message, 1135 completion_start_time=completion_start_time, 1136 model=model, 1137 model_parameters=model_parameters, 1138 usage_details=usage_details, 1139 cost_details=cost_details, 1140 prompt=prompt, 1141 ), 1142 ) 1143 1144 if as_type in get_observation_types_list(ObservationTypeSpanLike): 1145 if trace_context: 1146 trace_id = trace_context.get("trace_id", None) 1147 parent_span_id = trace_context.get("parent_span_id", None) 1148 1149 if trace_id: 1150 remote_parent_span = self._create_remote_parent_span( 1151 trace_id=trace_id, parent_span_id=parent_span_id 1152 ) 1153 1154 return cast( 1155 Union[ 1156 _AgnosticContextManager[LangfuseSpan], 1157 _AgnosticContextManager[LangfuseAgent], 1158 _AgnosticContextManager[LangfuseTool], 1159 _AgnosticContextManager[LangfuseChain], 1160 _AgnosticContextManager[LangfuseRetriever], 1161 _AgnosticContextManager[LangfuseEvaluator], 1162 _AgnosticContextManager[LangfuseGuardrail], 1163 ], 1164 self._create_span_with_parent_context( 1165 as_type=as_type, 1166 name=name, 1167 remote_parent_span=remote_parent_span, 1168 parent=None, 1169 end_on_exit=end_on_exit, 1170 input=input, 1171 output=output, 1172 metadata=metadata, 1173 version=version, 1174 level=level, 1175 status_message=status_message, 1176 ), 1177 ) 1178 1179 return cast( 1180 Union[ 1181 _AgnosticContextManager[LangfuseSpan], 1182 _AgnosticContextManager[LangfuseAgent], 1183 _AgnosticContextManager[LangfuseTool], 1184 _AgnosticContextManager[LangfuseChain], 1185 _AgnosticContextManager[LangfuseRetriever], 1186 _AgnosticContextManager[LangfuseEvaluator], 1187 _AgnosticContextManager[LangfuseGuardrail], 1188 ], 1189 self._start_as_current_otel_span_with_processed_media( 1190 as_type=as_type, 1191 name=name, 1192 end_on_exit=end_on_exit, 1193 input=input, 1194 output=output, 1195 metadata=metadata, 1196 version=version, 1197 level=level, 1198 status_message=status_message, 1199 ), 1200 ) 1201 1202 # This should never be reached since all valid types are handled above 1203 langfuse_logger.warning( 1204 "Unknown observation type: %s, falling back to span", as_type 1205 ) 1206 return self._start_as_current_otel_span_with_processed_media( 1207 as_type="span", 1208 name=name, 1209 end_on_exit=end_on_exit, 1210 input=input, 1211 output=output, 1212 metadata=metadata, 1213 version=version, 1214 level=level, 1215 status_message=status_message, 1216 ) 1217 1218 def _get_span_class( 1219 self, 1220 as_type: str, 1221 ) -> Union[ 1222 Type[LangfuseAgent], 1223 Type[LangfuseTool], 1224 Type[LangfuseChain], 1225 Type[LangfuseRetriever], 1226 Type[LangfuseEvaluator], 1227 Type[LangfuseEmbedding], 1228 Type[LangfuseGuardrail], 1229 Type[LangfuseGeneration], 1230 Type[LangfuseEvent], 1231 Type[LangfuseSpan], 1232 ]: 1233 """Get the appropriate span class based on as_type.""" 1234 normalized_type = as_type.lower() 1235 1236 if normalized_type == "agent": 1237 return LangfuseAgent 1238 elif normalized_type == "tool": 1239 return LangfuseTool 1240 elif normalized_type == "chain": 1241 return LangfuseChain 1242 elif normalized_type == "retriever": 1243 return LangfuseRetriever 1244 elif normalized_type == "evaluator": 1245 return LangfuseEvaluator 1246 elif normalized_type == "embedding": 1247 return LangfuseEmbedding 1248 elif normalized_type == "guardrail": 1249 return LangfuseGuardrail 1250 elif normalized_type == "generation": 1251 return LangfuseGeneration 1252 elif normalized_type == "event": 1253 return LangfuseEvent 1254 elif normalized_type == "span": 1255 return LangfuseSpan 1256 else: 1257 return LangfuseSpan 1258 1259 @staticmethod 1260 def _get_observation_type_from_otel_span(otel_span: otel_trace_api.Span) -> str: 1261 if not otel_span.is_recording(): 1262 return "span" 1263 1264 attributes = getattr(otel_span, "attributes", None) 1265 if attributes is None or not hasattr(attributes, "get"): 1266 return "span" 1267 1268 observation_type = attributes.get( 1269 LangfuseOtelSpanAttributes.OBSERVATION_TYPE, "span" 1270 ) 1271 1272 return observation_type if isinstance(observation_type, str) else "span" 1273 1274 @_agnosticcontextmanager 1275 def _create_span_with_parent_context( 1276 self, 1277 *, 1278 name: str, 1279 parent: Optional[otel_trace_api.Span] = None, 1280 remote_parent_span: Optional[otel_trace_api.Span] = None, 1281 as_type: ObservationTypeLiteralNoEvent, 1282 end_on_exit: Optional[bool] = None, 1283 input: Optional[Any] = None, 1284 output: Optional[Any] = None, 1285 metadata: Optional[Any] = None, 1286 version: Optional[str] = None, 1287 level: Optional[SpanLevel] = None, 1288 status_message: Optional[str] = None, 1289 completion_start_time: Optional[datetime] = None, 1290 model: Optional[str] = None, 1291 model_parameters: Optional[Dict[str, MapValue]] = None, 1292 usage_details: Optional[Dict[str, int]] = None, 1293 cost_details: Optional[Dict[str, float]] = None, 1294 prompt: Optional[PromptClient] = None, 1295 ) -> Any: 1296 parent_span = parent or cast(otel_trace_api.Span, remote_parent_span) 1297 1298 with otel_trace_api.use_span(parent_span): 1299 with self._start_as_current_otel_span_with_processed_media( 1300 name=name, 1301 as_type=as_type, 1302 end_on_exit=end_on_exit, 1303 input=input, 1304 output=output, 1305 metadata=metadata, 1306 version=version, 1307 level=level, 1308 status_message=status_message, 1309 completion_start_time=completion_start_time, 1310 model=model, 1311 model_parameters=model_parameters, 1312 usage_details=usage_details, 1313 cost_details=cost_details, 1314 prompt=prompt, 1315 ) as langfuse_span: 1316 if remote_parent_span is not None: 1317 langfuse_span._otel_span.set_attribute( 1318 LangfuseOtelSpanAttributes.AS_ROOT, True 1319 ) 1320 1321 yield langfuse_span 1322 1323 @_agnosticcontextmanager 1324 def _start_as_current_otel_span_with_processed_media( 1325 self, 1326 *, 1327 name: str, 1328 as_type: Optional[ObservationTypeLiteralNoEvent] = None, 1329 end_on_exit: Optional[bool] = None, 1330 input: Optional[Any] = None, 1331 output: Optional[Any] = None, 1332 metadata: Optional[Any] = None, 1333 version: Optional[str] = None, 1334 level: Optional[SpanLevel] = None, 1335 status_message: Optional[str] = None, 1336 completion_start_time: Optional[datetime] = None, 1337 model: Optional[str] = None, 1338 model_parameters: Optional[Dict[str, MapValue]] = None, 1339 usage_details: Optional[Dict[str, int]] = None, 1340 cost_details: Optional[Dict[str, float]] = None, 1341 prompt: Optional[PromptClient] = None, 1342 ) -> Any: 1343 with self._otel_tracer.start_as_current_span( 1344 name=name, 1345 end_on_exit=end_on_exit if end_on_exit is not None else True, 1346 ) as otel_span: 1347 baggage_token = None 1348 1349 if otel_span.is_recording(): 1350 context_with_app_root_claim = _set_langfuse_trace_id_in_baggage( 1351 trace_id=self._get_otel_trace_id(otel_span), 1352 context=otel_context_api.get_current(), 1353 ) 1354 baggage_token = otel_context_api.attach(context_with_app_root_claim) 1355 1356 span_class = self._get_span_class( 1357 as_type or "generation" 1358 ) # default was "generation" 1359 1360 try: 1361 common_args = { 1362 "otel_span": otel_span, 1363 "langfuse_client": self, 1364 "environment": self._environment, 1365 "release": self._release, 1366 "input": input, 1367 "output": output, 1368 "metadata": metadata, 1369 "version": version, 1370 "level": level, 1371 "status_message": status_message, 1372 } 1373 1374 if span_class in [ 1375 LangfuseGeneration, 1376 LangfuseEmbedding, 1377 ]: 1378 common_args.update( 1379 { 1380 "completion_start_time": completion_start_time, 1381 "model": model, 1382 "model_parameters": model_parameters, 1383 "usage_details": usage_details, 1384 "cost_details": cost_details, 1385 "prompt": prompt, 1386 } 1387 ) 1388 # For span-like types (span, agent, tool, chain, retriever, evaluator, guardrail), no generation properties needed 1389 1390 yield span_class(**common_args) # type: ignore[arg-type] 1391 1392 finally: 1393 if baggage_token is not None: 1394 _detach_context_token_safely(baggage_token) 1395 1396 def _get_current_otel_span(self) -> Optional[otel_trace_api.Span]: 1397 current_span = otel_trace_api.get_current_span() 1398 1399 if current_span is otel_trace_api.INVALID_SPAN: 1400 langfuse_logger.warning( 1401 "Context error: No active span in current context. Operations that depend on an active span will be skipped. " 1402 "Ensure spans are created with start_as_current_observation() or that you're operating within an active span context." 1403 ) 1404 return None 1405 1406 return current_span 1407 1408 def update_current_generation( 1409 self, 1410 *, 1411 name: Optional[str] = None, 1412 input: Optional[Any] = None, 1413 output: Optional[Any] = None, 1414 metadata: Optional[Any] = None, 1415 version: Optional[str] = None, 1416 level: Optional[SpanLevel] = None, 1417 status_message: Optional[str] = None, 1418 completion_start_time: Optional[datetime] = None, 1419 model: Optional[str] = None, 1420 model_parameters: Optional[Dict[str, MapValue]] = None, 1421 usage_details: Optional[Dict[str, int]] = None, 1422 cost_details: Optional[Dict[str, float]] = None, 1423 prompt: Optional[PromptClient] = None, 1424 ) -> None: 1425 """Update the current active generation span with new information. 1426 1427 This method updates the current generation span in the active context with 1428 additional information. It's useful for adding output, usage stats, or other 1429 details that become available during or after model generation. 1430 1431 Args: 1432 name: The generation name 1433 input: Updated input data for the model 1434 output: Output from the model (e.g., completions) 1435 metadata: Additional metadata to associate with the generation 1436 version: Version identifier for the model or component 1437 level: Importance level of the generation (info, warning, error) 1438 status_message: Optional status message for the generation 1439 completion_start_time: When the model started generating the response 1440 model: Name/identifier of the AI model used (e.g., "gpt-4") 1441 model_parameters: Parameters used for the model (e.g., temperature, max_tokens) 1442 usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) 1443 cost_details: Cost information for the model call 1444 prompt: Associated prompt template from Langfuse prompt management 1445 1446 Example: 1447 ```python 1448 with langfuse.start_as_current_generation(name="answer-query") as generation: 1449 # Initial setup and API call 1450 response = llm.generate(...) 1451 1452 # Update with results that weren't available at creation time 1453 langfuse.update_current_generation( 1454 output=response.text, 1455 usage_details={ 1456 "prompt_tokens": response.usage.prompt_tokens, 1457 "completion_tokens": response.usage.completion_tokens 1458 } 1459 ) 1460 ``` 1461 """ 1462 if not self._tracing_enabled: 1463 langfuse_logger.debug( 1464 "Operation skipped: update_current_generation - Tracing is disabled or client is in no-op mode." 1465 ) 1466 return 1467 1468 current_otel_span = self._get_current_otel_span() 1469 1470 if current_otel_span is not None: 1471 generation = LangfuseGeneration( 1472 otel_span=current_otel_span, langfuse_client=self 1473 ) 1474 1475 if name: 1476 current_otel_span.update_name(name) 1477 1478 generation.update( 1479 input=input, 1480 output=output, 1481 metadata=metadata, 1482 version=version, 1483 level=level, 1484 status_message=status_message, 1485 completion_start_time=completion_start_time, 1486 model=model, 1487 model_parameters=model_parameters, 1488 usage_details=usage_details, 1489 cost_details=cost_details, 1490 prompt=prompt, 1491 ) 1492 1493 def update_current_span( 1494 self, 1495 *, 1496 name: Optional[str] = None, 1497 input: Optional[Any] = None, 1498 output: Optional[Any] = None, 1499 metadata: Optional[Any] = None, 1500 version: Optional[str] = None, 1501 level: Optional[SpanLevel] = None, 1502 status_message: Optional[str] = None, 1503 ) -> None: 1504 """Update the current active span with new information. 1505 1506 This method updates the current span in the active context with 1507 additional information. It's useful for adding outputs or metadata 1508 that become available during execution. 1509 1510 Args: 1511 name: The span name 1512 input: Updated input data for the operation 1513 output: Output data from the operation 1514 metadata: Additional metadata to associate with the span 1515 version: Version identifier for the code or component 1516 level: Importance level of the span (info, warning, error) 1517 status_message: Optional status message for the span 1518 1519 Example: 1520 ```python 1521 with langfuse.start_as_current_observation(name="process-data") as span: 1522 # Initial processing 1523 result = process_first_part() 1524 1525 # Update with intermediate results 1526 langfuse.update_current_span(metadata={"intermediate_result": result}) 1527 1528 # Continue processing 1529 final_result = process_second_part(result) 1530 1531 # Final update 1532 langfuse.update_current_span(output=final_result) 1533 ``` 1534 """ 1535 if not self._tracing_enabled: 1536 langfuse_logger.debug( 1537 "Operation skipped: update_current_span - Tracing is disabled or client is in no-op mode." 1538 ) 1539 return 1540 1541 current_otel_span = self._get_current_otel_span() 1542 1543 if current_otel_span is not None: 1544 span_class = self._get_span_class( 1545 self._get_observation_type_from_otel_span(current_otel_span) 1546 ) 1547 span = span_class( 1548 otel_span=current_otel_span, 1549 langfuse_client=self, 1550 environment=self._environment, 1551 release=self._release, 1552 ) 1553 1554 if name: 1555 current_otel_span.update_name(name) 1556 1557 span.update( 1558 input=input, 1559 output=output, 1560 metadata=metadata, 1561 version=version, 1562 level=level, 1563 status_message=status_message, 1564 ) 1565 1566 @deprecated( 1567 "Trace-level input/output is deprecated. " 1568 "For trace attributes (user_id, session_id, tags, etc.), use propagate_attributes() instead. " 1569 "This method will be removed in a future major version." 1570 ) 1571 def set_current_trace_io( 1572 self, 1573 *, 1574 input: Optional[Any] = None, 1575 output: Optional[Any] = None, 1576 ) -> None: 1577 """Set trace-level input and output for the current span's trace. 1578 1579 .. deprecated:: 1580 This is a legacy method for backward compatibility with Langfuse platform 1581 features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge 1582 evaluators). It will be removed in a future major version. 1583 1584 For setting other trace attributes (user_id, session_id, metadata, tags, version), 1585 use :func:`langfuse.propagate_attributes` (top-level import) instead. 1586 1587 Args: 1588 input: Input data to associate with the trace. 1589 output: Output data to associate with the trace. 1590 """ 1591 if not self._tracing_enabled: 1592 langfuse_logger.debug( 1593 "Operation skipped: set_current_trace_io - Tracing is disabled or client is in no-op mode." 1594 ) 1595 return 1596 1597 current_otel_span = self._get_current_otel_span() 1598 1599 if current_otel_span is not None and current_otel_span.is_recording(): 1600 span_class = self._get_span_class( 1601 self._get_observation_type_from_otel_span(current_otel_span) 1602 ) 1603 span = span_class( 1604 otel_span=current_otel_span, 1605 langfuse_client=self, 1606 environment=self._environment, 1607 release=self._release, 1608 ) 1609 1610 span.set_trace_io( 1611 input=input, 1612 output=output, 1613 ) 1614 1615 def set_current_trace_as_public(self) -> None: 1616 """Make the current trace publicly accessible via its URL. 1617 1618 When a trace is published, anyone with the trace link can view the full trace 1619 without needing to be logged in to Langfuse. This action cannot be undone 1620 programmatically - once published, the entire trace becomes public. 1621 1622 This is a convenience method that publishes the trace from the currently 1623 active span context. Use this when you want to make a trace public from 1624 within a traced function without needing direct access to the span object. 1625 """ 1626 if not self._tracing_enabled: 1627 langfuse_logger.debug( 1628 "Operation skipped: set_current_trace_as_public - Tracing is disabled or client is in no-op mode." 1629 ) 1630 return 1631 1632 current_otel_span = self._get_current_otel_span() 1633 1634 if current_otel_span is not None and current_otel_span.is_recording(): 1635 span_class = self._get_span_class( 1636 self._get_observation_type_from_otel_span(current_otel_span) 1637 ) 1638 span = span_class( 1639 otel_span=current_otel_span, 1640 langfuse_client=self, 1641 environment=self._environment, 1642 ) 1643 1644 span.set_trace_as_public() 1645 1646 def create_event( 1647 self, 1648 *, 1649 trace_context: Optional[TraceContext] = None, 1650 name: str, 1651 input: Optional[Any] = None, 1652 output: Optional[Any] = None, 1653 metadata: Optional[Any] = None, 1654 version: Optional[str] = None, 1655 level: Optional[SpanLevel] = None, 1656 status_message: Optional[str] = None, 1657 ) -> LangfuseEvent: 1658 """Create a new Langfuse observation of type 'EVENT'. 1659 1660 The created Langfuse Event observation will be the child of the current span in the context. 1661 1662 Args: 1663 trace_context: Optional context for connecting to an existing trace 1664 name: Name of the span (e.g., function or operation name) 1665 input: Input data for the operation (can be any JSON-serializable object) 1666 output: Output data from the operation (can be any JSON-serializable object) 1667 metadata: Additional metadata to associate with the span 1668 version: Version identifier for the code or component 1669 level: Importance level of the span (info, warning, error) 1670 status_message: Optional status message for the span 1671 1672 Returns: 1673 The Langfuse Event object 1674 1675 Example: 1676 ```python 1677 event = langfuse.create_event(name="process-event") 1678 ``` 1679 """ 1680 timestamp = time_ns() 1681 1682 if trace_context: 1683 trace_id = trace_context.get("trace_id", None) 1684 parent_span_id = trace_context.get("parent_span_id", None) 1685 1686 if trace_id: 1687 remote_parent_span = self._create_remote_parent_span( 1688 trace_id=trace_id, parent_span_id=parent_span_id 1689 ) 1690 1691 with otel_trace_api.use_span( 1692 cast(otel_trace_api.Span, remote_parent_span) 1693 ): 1694 otel_span = self._otel_tracer.start_span( 1695 name=name, start_time=timestamp 1696 ) 1697 otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) 1698 1699 return cast( 1700 LangfuseEvent, 1701 LangfuseEvent( 1702 otel_span=otel_span, 1703 langfuse_client=self, 1704 environment=self._environment, 1705 release=self._release, 1706 input=input, 1707 output=output, 1708 metadata=metadata, 1709 version=version, 1710 level=level, 1711 status_message=status_message, 1712 ).end(end_time=timestamp), 1713 ) 1714 1715 otel_span = self._otel_tracer.start_span(name=name, start_time=timestamp) 1716 1717 return cast( 1718 LangfuseEvent, 1719 LangfuseEvent( 1720 otel_span=otel_span, 1721 langfuse_client=self, 1722 environment=self._environment, 1723 release=self._release, 1724 input=input, 1725 output=output, 1726 metadata=metadata, 1727 version=version, 1728 level=level, 1729 status_message=status_message, 1730 ).end(end_time=timestamp), 1731 ) 1732 1733 def _create_remote_parent_span( 1734 self, *, trace_id: str, parent_span_id: Optional[str] 1735 ) -> Any: 1736 if not self._is_valid_trace_id(trace_id): 1737 langfuse_logger.warning( 1738 "Passed trace ID '%s' is not a valid 32 lowercase hex char Langfuse trace " 1739 "id. Ignoring trace ID.", 1740 trace_id, 1741 ) 1742 1743 if parent_span_id and not self._is_valid_span_id(parent_span_id): 1744 langfuse_logger.warning( 1745 "Passed span ID '%s' is not a valid 16 lowercase hex char Langfuse span " 1746 "id. Ignoring parent span ID.", 1747 parent_span_id, 1748 ) 1749 1750 int_trace_id = int(trace_id, 16) 1751 int_parent_span_id = ( 1752 int(parent_span_id, 16) 1753 if parent_span_id 1754 else RandomIdGenerator().generate_span_id() 1755 ) 1756 1757 span_context = otel_trace_api.SpanContext( 1758 trace_id=int_trace_id, 1759 span_id=int_parent_span_id, 1760 trace_flags=otel_trace_api.TraceFlags(0x01), # mark span as sampled 1761 is_remote=False, 1762 ) 1763 1764 return otel_trace_api.NonRecordingSpan(span_context) 1765 1766 def _is_valid_trace_id(self, trace_id: str) -> bool: 1767 pattern = r"^[0-9a-f]{32}$" 1768 1769 return bool(re.match(pattern, trace_id)) 1770 1771 def _is_valid_span_id(self, span_id: str) -> bool: 1772 pattern = r"^[0-9a-f]{16}$" 1773 1774 return bool(re.match(pattern, span_id)) 1775 1776 def _create_observation_id(self, *, seed: Optional[str] = None) -> str: 1777 """Create a unique observation ID for use with Langfuse. 1778 1779 This method generates a unique observation ID (span ID in OpenTelemetry terms) 1780 for use with various Langfuse APIs. It can either generate a random ID or 1781 create a deterministic ID based on a seed string. 1782 1783 Observation IDs must be 16 lowercase hexadecimal characters, representing 8 bytes. 1784 This method ensures the generated ID meets this requirement. If you need to 1785 correlate an external ID with a Langfuse observation ID, use the external ID as 1786 the seed to get a valid, deterministic observation ID. 1787 1788 Args: 1789 seed: Optional string to use as a seed for deterministic ID generation. 1790 If provided, the same seed will always produce the same ID. 1791 If not provided, a random ID will be generated. 1792 1793 Returns: 1794 A 16-character lowercase hexadecimal string representing the observation ID. 1795 1796 Example: 1797 ```python 1798 # Generate a random observation ID 1799 obs_id = langfuse.create_observation_id() 1800 1801 # Generate a deterministic ID based on a seed 1802 user_obs_id = langfuse.create_observation_id(seed="user-123-feedback") 1803 1804 # Correlate an external item ID with a Langfuse observation ID 1805 item_id = "item-789012" 1806 correlated_obs_id = langfuse.create_observation_id(seed=item_id) 1807 1808 # Use the ID with Langfuse APIs 1809 langfuse.create_score( 1810 name="relevance", 1811 value=0.95, 1812 trace_id=trace_id, 1813 observation_id=obs_id 1814 ) 1815 ``` 1816 """ 1817 if not seed: 1818 span_id_int = RandomIdGenerator().generate_span_id() 1819 1820 return self._format_otel_span_id(span_id_int) 1821 1822 return sha256(seed.encode("utf-8")).digest()[:8].hex() 1823 1824 @staticmethod 1825 def create_trace_id(*, seed: Optional[str] = None) -> str: 1826 """Create a unique trace ID for use with Langfuse. 1827 1828 This method generates a unique trace ID for use with various Langfuse APIs. 1829 It can either generate a random ID or create a deterministic ID based on 1830 a seed string. 1831 1832 Trace IDs must be 32 lowercase hexadecimal characters, representing 16 bytes. 1833 This method ensures the generated ID meets this requirement. If you need to 1834 correlate an external ID with a Langfuse trace ID, use the external ID as the 1835 seed to get a valid, deterministic Langfuse trace ID. 1836 1837 Args: 1838 seed: Optional string to use as a seed for deterministic ID generation. 1839 If provided, the same seed will always produce the same ID. 1840 If not provided, a random ID will be generated. 1841 1842 Returns: 1843 A 32-character lowercase hexadecimal string representing the Langfuse trace ID. 1844 1845 Example: 1846 ```python 1847 # Generate a random trace ID 1848 trace_id = langfuse.create_trace_id() 1849 1850 # Generate a deterministic ID based on a seed 1851 session_trace_id = langfuse.create_trace_id(seed="session-456") 1852 1853 # Correlate an external ID with a Langfuse trace ID 1854 external_id = "external-system-123456" 1855 correlated_trace_id = langfuse.create_trace_id(seed=external_id) 1856 1857 # Use the ID with trace context 1858 with langfuse.start_as_current_observation( 1859 name="process-request", 1860 trace_context={"trace_id": trace_id} 1861 ) as span: 1862 # Operation will be part of the specific trace 1863 pass 1864 ``` 1865 """ 1866 if not seed: 1867 trace_id_int = RandomIdGenerator().generate_trace_id() 1868 1869 return Langfuse._format_otel_trace_id(trace_id_int) 1870 1871 return sha256(seed.encode("utf-8")).digest()[:16].hex() 1872 1873 def _get_otel_trace_id(self, otel_span: otel_trace_api.Span) -> str: 1874 span_context = otel_span.get_span_context() 1875 1876 return self._format_otel_trace_id(span_context.trace_id) 1877 1878 def _get_otel_span_id(self, otel_span: otel_trace_api.Span) -> str: 1879 span_context = otel_span.get_span_context() 1880 1881 return self._format_otel_span_id(span_context.span_id) 1882 1883 @staticmethod 1884 def _format_otel_span_id(span_id_int: int) -> str: 1885 """Format an integer span ID to a 16-character lowercase hex string. 1886 1887 Internal method to convert an OpenTelemetry integer span ID to the standard 1888 W3C Trace Context format (16-character lowercase hex string). 1889 1890 Args: 1891 span_id_int: 64-bit integer representing a span ID 1892 1893 Returns: 1894 A 16-character lowercase hexadecimal string 1895 """ 1896 return format(span_id_int, "016x") 1897 1898 @staticmethod 1899 def _format_otel_trace_id(trace_id_int: int) -> str: 1900 """Format an integer trace ID to a 32-character lowercase hex string. 1901 1902 Internal method to convert an OpenTelemetry integer trace ID to the standard 1903 W3C Trace Context format (32-character lowercase hex string). 1904 1905 Args: 1906 trace_id_int: 128-bit integer representing a trace ID 1907 1908 Returns: 1909 A 32-character lowercase hexadecimal string 1910 """ 1911 return format(trace_id_int, "032x") 1912 1913 @overload 1914 def create_score( 1915 self, 1916 *, 1917 name: str, 1918 value: float, 1919 session_id: Optional[str] = None, 1920 dataset_run_id: Optional[str] = None, 1921 trace_id: Optional[str] = None, 1922 observation_id: Optional[str] = None, 1923 score_id: Optional[str] = None, 1924 data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, 1925 comment: Optional[str] = None, 1926 config_id: Optional[str] = None, 1927 metadata: Optional[Any] = None, 1928 timestamp: Optional[datetime] = None, 1929 environment: Optional[str] = None, 1930 ) -> None: ... 1931 1932 @overload 1933 def create_score( 1934 self, 1935 *, 1936 name: str, 1937 value: str, 1938 session_id: Optional[str] = None, 1939 dataset_run_id: Optional[str] = None, 1940 trace_id: Optional[str] = None, 1941 score_id: Optional[str] = None, 1942 observation_id: Optional[str] = None, 1943 data_type: Optional[ 1944 Literal["CATEGORICAL", "TEXT", "CORRECTION"] 1945 ] = "CATEGORICAL", 1946 comment: Optional[str] = None, 1947 config_id: Optional[str] = None, 1948 metadata: Optional[Any] = None, 1949 timestamp: Optional[datetime] = None, 1950 environment: Optional[str] = None, 1951 ) -> None: ... 1952 1953 def create_score( 1954 self, 1955 *, 1956 name: str, 1957 value: Union[float, str], 1958 session_id: Optional[str] = None, 1959 dataset_run_id: Optional[str] = None, 1960 trace_id: Optional[str] = None, 1961 observation_id: Optional[str] = None, 1962 score_id: Optional[str] = None, 1963 data_type: Optional[ScoreDataType] = None, 1964 comment: Optional[str] = None, 1965 config_id: Optional[str] = None, 1966 metadata: Optional[Any] = None, 1967 timestamp: Optional[datetime] = None, 1968 environment: Optional[str] = None, 1969 ) -> None: 1970 """Create a score for a specific trace or observation. 1971 1972 This method creates a score for evaluating a Langfuse trace or observation. Scores can be 1973 used to track quality metrics, user feedback, or automated evaluations. 1974 1975 Args: 1976 name: Name of the score (e.g., "relevance", "accuracy") 1977 value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION) 1978 session_id: ID of the Langfuse session to associate the score with 1979 dataset_run_id: ID of the Langfuse dataset run to associate the score with 1980 trace_id: ID of the Langfuse trace to associate the score with 1981 observation_id: Optional ID of the specific observation to score. Trace ID must be provided too. 1982 score_id: Optional custom ID for the score (auto-generated if not provided) 1983 data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION) 1984 comment: Optional comment or explanation for the score 1985 config_id: Optional ID of a score config defined in Langfuse 1986 metadata: Optional metadata to be attached to the score 1987 timestamp: Optional timestamp for the score (defaults to current UTC time) 1988 environment: Optional environment override for this score. If omitted, 1989 the score uses the client-level environment from 1990 `Langfuse(environment=...)` or `LANGFUSE_TRACING_ENVIRONMENT`. 1991 Langfuse observation wrapper methods pass their resolved span 1992 environment here so scores created via `span.score()` or 1993 `span.score_trace()` stay grouped with the scored observation or 1994 trace, including request-scoped environments propagated with 1995 `propagate_attributes(environment=...)`. 1996 1997 Example: 1998 ```python 1999 # Create a numeric score for accuracy 2000 langfuse.create_score( 2001 name="accuracy", 2002 value=0.92, 2003 trace_id="abcdef1234567890abcdef1234567890", 2004 data_type="NUMERIC", 2005 comment="High accuracy with minor irrelevant details" 2006 ) 2007 2008 # Create a categorical score for sentiment 2009 langfuse.create_score( 2010 name="sentiment", 2011 value="positive", 2012 trace_id="abcdef1234567890abcdef1234567890", 2013 observation_id="abcdef1234567890", 2014 data_type="CATEGORICAL" 2015 ) 2016 ``` 2017 """ 2018 if not self._tracing_enabled: 2019 return 2020 2021 score_id = score_id or self._create_observation_id() 2022 2023 try: 2024 new_body = ScoreBody( 2025 id=score_id, 2026 sessionId=session_id, 2027 datasetRunId=dataset_run_id, 2028 traceId=trace_id, 2029 observationId=observation_id, 2030 name=name, 2031 value=value, 2032 dataType=data_type, # type: ignore 2033 comment=comment, 2034 configId=config_id, 2035 environment=environment or self._environment, 2036 metadata=metadata, 2037 ) 2038 2039 event = { 2040 "id": self.create_trace_id(), 2041 "type": "score-create", 2042 "timestamp": timestamp or _get_timestamp(), 2043 "body": new_body, 2044 } 2045 2046 if self._resources is not None: 2047 # Force the score to be in sample if it was for a legacy trace ID, i.e. non-32 hexchar 2048 force_sample = ( 2049 not self._is_valid_trace_id(trace_id) if trace_id else True 2050 ) 2051 2052 self._resources.add_score_task( 2053 event, 2054 force_sample=force_sample, 2055 ) 2056 2057 except Exception as e: 2058 langfuse_logger.exception( 2059 "Error creating score: Failed to process score event for trace_id=%s, " 2060 "name=%s. Error: %s", 2061 trace_id, 2062 name, 2063 e, 2064 ) 2065 2066 def _create_trace_tags_via_ingestion( 2067 self, 2068 *, 2069 trace_id: str, 2070 tags: List[str], 2071 ) -> None: 2072 """Private helper to enqueue trace tag updates via ingestion API events.""" 2073 if not self._tracing_enabled: 2074 return 2075 2076 if len(tags) == 0: 2077 return 2078 2079 try: 2080 new_body = TraceBody( 2081 id=trace_id, 2082 tags=tags, 2083 ) 2084 2085 event = { 2086 "id": self.create_trace_id(), 2087 "type": "trace-create", 2088 "timestamp": _get_timestamp(), 2089 "body": new_body, 2090 } 2091 2092 if self._resources is not None: 2093 self._resources.add_trace_task(event) 2094 except Exception as e: 2095 langfuse_logger.exception( 2096 "Error updating trace tags: Failed to process trace update event for " 2097 "trace_id=%s. Error: %s", 2098 trace_id, 2099 e, 2100 ) 2101 2102 @overload 2103 def score_current_span( 2104 self, 2105 *, 2106 name: str, 2107 value: float, 2108 score_id: Optional[str] = None, 2109 data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, 2110 comment: Optional[str] = None, 2111 config_id: Optional[str] = None, 2112 metadata: Optional[Any] = None, 2113 ) -> None: ... 2114 2115 @overload 2116 def score_current_span( 2117 self, 2118 *, 2119 name: str, 2120 value: str, 2121 score_id: Optional[str] = None, 2122 data_type: Optional[ 2123 Literal["CATEGORICAL", "TEXT", "CORRECTION"] 2124 ] = "CATEGORICAL", 2125 comment: Optional[str] = None, 2126 config_id: Optional[str] = None, 2127 metadata: Optional[Any] = None, 2128 ) -> None: ... 2129 2130 def score_current_span( 2131 self, 2132 *, 2133 name: str, 2134 value: Union[float, str], 2135 score_id: Optional[str] = None, 2136 data_type: Optional[ScoreDataType] = None, 2137 comment: Optional[str] = None, 2138 config_id: Optional[str] = None, 2139 metadata: Optional[Any] = None, 2140 ) -> None: 2141 """Create a score for the current active span. 2142 2143 This method scores the currently active span in the context. It's a convenient 2144 way to score the current operation without needing to know its trace and span IDs. 2145 If the active span has a `langfuse.environment` attribute, including one 2146 set by `propagate_attributes(environment=...)`, the score uses that 2147 environment. Otherwise it uses the client-level environment. 2148 2149 Args: 2150 name: Name of the score (e.g., "relevance", "accuracy") 2151 value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION) 2152 score_id: Optional custom ID for the score (auto-generated if not provided) 2153 data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION) 2154 comment: Optional comment or explanation for the score 2155 config_id: Optional ID of a score config defined in Langfuse 2156 metadata: Optional metadata to be attached to the score 2157 2158 Example: 2159 ```python 2160 with langfuse.start_as_current_generation(name="answer-query") as generation: 2161 # Generate answer 2162 response = generate_answer(...) 2163 generation.update(output=response) 2164 2165 # Score the generation 2166 langfuse.score_current_span( 2167 name="relevance", 2168 value=0.85, 2169 data_type="NUMERIC", 2170 comment="Mostly relevant but contains some tangential information", 2171 metadata={"model": "gpt-4", "prompt_version": "v2"} 2172 ) 2173 ``` 2174 """ 2175 current_span = self._get_current_otel_span() 2176 2177 if current_span is not None: 2178 trace_id = self._get_otel_trace_id(current_span) 2179 observation_id = self._get_otel_span_id(current_span) 2180 2181 langfuse_logger.info( 2182 "Score: Creating score name='%s' value=%s for current span (%s) in trace " 2183 "%s", 2184 name, 2185 value, 2186 observation_id, 2187 trace_id, 2188 ) 2189 2190 self.create_score( 2191 trace_id=trace_id, 2192 observation_id=observation_id, 2193 name=name, 2194 value=cast(str, value), 2195 score_id=score_id, 2196 data_type=cast(Literal["CATEGORICAL", "TEXT", "CORRECTION"], data_type), 2197 comment=comment, 2198 config_id=config_id, 2199 metadata=metadata, 2200 environment=get_string_span_attribute( 2201 current_span, LangfuseOtelSpanAttributes.ENVIRONMENT 2202 ), 2203 ) 2204 2205 @overload 2206 def score_current_trace( 2207 self, 2208 *, 2209 name: str, 2210 value: float, 2211 score_id: Optional[str] = None, 2212 data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, 2213 comment: Optional[str] = None, 2214 config_id: Optional[str] = None, 2215 metadata: Optional[Any] = None, 2216 ) -> None: ... 2217 2218 @overload 2219 def score_current_trace( 2220 self, 2221 *, 2222 name: str, 2223 value: str, 2224 score_id: Optional[str] = None, 2225 data_type: Optional[ 2226 Literal["CATEGORICAL", "TEXT", "CORRECTION"] 2227 ] = "CATEGORICAL", 2228 comment: Optional[str] = None, 2229 config_id: Optional[str] = None, 2230 metadata: Optional[Any] = None, 2231 ) -> None: ... 2232 2233 def score_current_trace( 2234 self, 2235 *, 2236 name: str, 2237 value: Union[float, str], 2238 score_id: Optional[str] = None, 2239 data_type: Optional[ScoreDataType] = None, 2240 comment: Optional[str] = None, 2241 config_id: Optional[str] = None, 2242 metadata: Optional[Any] = None, 2243 ) -> None: 2244 """Create a score for the current trace. 2245 2246 This method scores the trace of the currently active span. Unlike score_current_span, 2247 this method associates the score with the entire trace rather than a specific span. 2248 It's useful for scoring overall performance or quality of the entire operation. 2249 If the active span has a `langfuse.environment` attribute, including one 2250 set by `propagate_attributes(environment=...)`, the score uses that 2251 environment. Otherwise it uses the client-level environment. 2252 2253 Args: 2254 name: Name of the score (e.g., "user_satisfaction", "overall_quality") 2255 value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION) 2256 score_id: Optional custom ID for the score (auto-generated if not provided) 2257 data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION) 2258 comment: Optional comment or explanation for the score 2259 config_id: Optional ID of a score config defined in Langfuse 2260 metadata: Optional metadata to be attached to the score 2261 2262 Example: 2263 ```python 2264 with langfuse.start_as_current_observation(name="process-user-request") as span: 2265 # Process request 2266 result = process_complete_request() 2267 span.update(output=result) 2268 2269 # Score the overall trace 2270 langfuse.score_current_trace( 2271 name="overall_quality", 2272 value=0.95, 2273 data_type="NUMERIC", 2274 comment="High quality end-to-end response", 2275 metadata={"evaluator": "gpt-4", "criteria": "comprehensive"} 2276 ) 2277 ``` 2278 """ 2279 current_span = self._get_current_otel_span() 2280 2281 if current_span is not None: 2282 trace_id = self._get_otel_trace_id(current_span) 2283 2284 langfuse_logger.info( 2285 "Score: Creating score name='%s' value=%s for entire trace %s", 2286 name, 2287 value, 2288 trace_id, 2289 ) 2290 2291 self.create_score( 2292 trace_id=trace_id, 2293 name=name, 2294 value=cast(str, value), 2295 score_id=score_id, 2296 data_type=cast(Literal["CATEGORICAL", "TEXT", "CORRECTION"], data_type), 2297 comment=comment, 2298 config_id=config_id, 2299 metadata=metadata, 2300 environment=get_string_span_attribute( 2301 current_span, LangfuseOtelSpanAttributes.ENVIRONMENT 2302 ), 2303 ) 2304 2305 def flush(self) -> None: 2306 """Force flush all pending spans and events to the Langfuse API. 2307 2308 This method manually flushes any pending spans, scores, and other events to the 2309 Langfuse API. It's useful in scenarios where you want to ensure all data is sent 2310 before proceeding, without waiting for the automatic flush interval. 2311 2312 Example: 2313 ```python 2314 # Record some spans and scores 2315 with langfuse.start_as_current_observation(name="operation") as span: 2316 # Do work... 2317 pass 2318 2319 # Ensure all data is sent to Langfuse before proceeding 2320 langfuse.flush() 2321 2322 # Continue with other work 2323 ``` 2324 2325 Note: 2326 `flush()` guarantees data was *delivered* to the API, not that it is 2327 *readable* yet: server-side ingestion is asynchronous, so flushed data 2328 may not be queryable for 15-30 seconds — 2329 `api.observations.get_many(trace_id=...)` may return empty results and 2330 `api.trace.get()` may raise `langfuse.api.NotFoundError` right after a 2331 successful flush. See the `api` property docs for a bounded retry 2332 pattern, or 2333 https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk#ingestion-lag 2334 """ 2335 if self._resources is not None: 2336 self._resources.flush() 2337 2338 def shutdown(self) -> None: 2339 """Shut down the Langfuse client and flush all pending data. 2340 2341 This method cleanly shuts down the Langfuse client, ensuring all pending data 2342 is flushed to the API and all background threads are properly terminated. 2343 2344 It's important to call this method when your application is shutting down to 2345 prevent data loss and resource leaks. For most applications, using the client 2346 as a context manager or relying on the automatic shutdown via atexit is sufficient. 2347 2348 Example: 2349 ```python 2350 # Initialize Langfuse 2351 langfuse = Langfuse(public_key="...", secret_key="...") 2352 2353 # Use Langfuse throughout your application 2354 # ... 2355 2356 # When application is shutting down 2357 langfuse.shutdown() 2358 ``` 2359 """ 2360 if self._resources is not None: 2361 self._resources.shutdown() 2362 2363 def get_current_trace_id(self) -> Optional[str]: 2364 """Get the trace ID of the current active span. 2365 2366 This method retrieves the trace ID from the currently active span in the context. 2367 It can be used to get the trace ID for referencing in logs, external systems, 2368 or for creating related operations. 2369 2370 Returns: 2371 The current trace ID as a 32-character lowercase hexadecimal string, 2372 or None if there is no active span. 2373 2374 Example: 2375 ```python 2376 with langfuse.start_as_current_observation(name="process-request") as span: 2377 # Get the current trace ID for reference 2378 trace_id = langfuse.get_current_trace_id() 2379 2380 # Use it for external correlation 2381 log.info(f"Processing request with trace_id: {trace_id}") 2382 2383 # Or pass to another system 2384 external_system.process(data, trace_id=trace_id) 2385 ``` 2386 """ 2387 if not self._tracing_enabled: 2388 langfuse_logger.debug( 2389 "Operation skipped: get_current_trace_id - Tracing is disabled or client is in no-op mode." 2390 ) 2391 return None 2392 2393 current_otel_span = self._get_current_otel_span() 2394 2395 return self._get_otel_trace_id(current_otel_span) if current_otel_span else None 2396 2397 def get_current_observation_id(self) -> Optional[str]: 2398 """Get the observation ID (span ID) of the current active span. 2399 2400 This method retrieves the observation ID from the currently active span in the context. 2401 It can be used to get the observation ID for referencing in logs, external systems, 2402 or for creating scores or other related operations. 2403 2404 Returns: 2405 The current observation ID as a 16-character lowercase hexadecimal string, 2406 or None if there is no active span. 2407 2408 Example: 2409 ```python 2410 with langfuse.start_as_current_observation(name="process-user-query") as span: 2411 # Get the current observation ID 2412 observation_id = langfuse.get_current_observation_id() 2413 2414 # Store it for later reference 2415 cache.set(f"query_{query_id}_observation", observation_id) 2416 2417 # Process the query... 2418 ``` 2419 """ 2420 if not self._tracing_enabled: 2421 langfuse_logger.debug( 2422 "Operation skipped: get_current_observation_id - Tracing is disabled or client is in no-op mode." 2423 ) 2424 return None 2425 2426 current_otel_span = self._get_current_otel_span() 2427 2428 return self._get_otel_span_id(current_otel_span) if current_otel_span else None 2429 2430 def _get_project_id(self) -> Optional[str]: 2431 """Fetch and return the current project id. Persisted across requests. Returns None if no project id is found for api keys.""" 2432 if not self._project_id: 2433 proj = self.api.projects.get() 2434 if not proj.data or not proj.data[0].id: 2435 return None 2436 2437 self._project_id = proj.data[0].id 2438 2439 return self._project_id 2440 2441 def get_trace_url(self, *, trace_id: Optional[str] = None) -> Optional[str]: 2442 """Get the URL to view a trace in the Langfuse UI. 2443 2444 This method generates a URL that links directly to a trace in the Langfuse UI. 2445 It's useful for providing links in logs, notifications, or debugging tools. 2446 2447 Args: 2448 trace_id: Optional trace ID to generate a URL for. If not provided, 2449 the trace ID of the current active span will be used. 2450 2451 Returns: 2452 A URL string pointing to the trace in the Langfuse UI, 2453 or None if the project ID couldn't be retrieved or no trace ID is available. 2454 2455 Example: 2456 ```python 2457 # Get URL for the current trace 2458 with langfuse.start_as_current_observation(name="process-request") as span: 2459 trace_url = langfuse.get_trace_url() 2460 log.info(f"Processing trace: {trace_url}") 2461 2462 # Get URL for a specific trace 2463 specific_trace_url = langfuse.get_trace_url(trace_id="1234567890abcdef1234567890abcdef") 2464 send_notification(f"Review needed for trace: {specific_trace_url}") 2465 ``` 2466 """ 2467 final_trace_id = trace_id or self.get_current_trace_id() 2468 if not final_trace_id: 2469 return None 2470 2471 project_id = self._get_project_id() 2472 2473 return ( 2474 f"{self._base_url}/project/{project_id}/traces/{final_trace_id}" 2475 if project_id and final_trace_id 2476 else None 2477 ) 2478 2479 def get_dataset( 2480 self, 2481 name: str, 2482 *, 2483 fetch_items_page_size: Optional[int] = 50, 2484 version: Optional[datetime] = None, 2485 ) -> "DatasetClient": 2486 """Fetch a dataset by its name. 2487 2488 Args: 2489 name: The name of the dataset to fetch. 2490 fetch_items_page_size: All items of the dataset will be fetched in chunks of this size. Defaults to 50. 2491 version: Retrieve dataset items as they existed at this specific point in time (UTC). 2492 If provided, returns the state of items at the specified UTC timestamp. 2493 If not provided, returns the latest version. Must be a timezone-aware datetime object in UTC. 2494 2495 Returns: 2496 DatasetClient: The dataset with the given name. 2497 """ 2498 try: 2499 langfuse_logger.debug("Getting datasets %s", name) 2500 dataset = self.api.datasets.get(dataset_name=self._url_encode(name)) 2501 2502 dataset_items: List[DatasetItem] = [] 2503 page = 1 2504 2505 while True: 2506 new_items = self.api.dataset_items.list( 2507 dataset_name=self._url_encode(name, is_url_param=True), 2508 page=page, 2509 limit=fetch_items_page_size, 2510 version=version, 2511 ) 2512 dataset_items.extend( 2513 self._hydrate_dataset_item_media_references(item) 2514 for item in new_items.data 2515 ) 2516 2517 if new_items.meta.total_pages <= page: 2518 break 2519 2520 page += 1 2521 2522 return DatasetClient( 2523 dataset=dataset, 2524 items=dataset_items, 2525 version=version, 2526 langfuse_client=self, 2527 ) 2528 2529 except Error as e: 2530 handle_fern_exception(e) 2531 raise e 2532 2533 def get_dataset_run( 2534 self, *, dataset_name: str, run_name: str 2535 ) -> DatasetRunWithItems: 2536 """Fetch a dataset run by dataset name and run name. 2537 2538 Args: 2539 dataset_name (str): The name of the dataset. 2540 run_name (str): The name of the run. 2541 2542 Returns: 2543 DatasetRunWithItems: The dataset run with its items. 2544 """ 2545 try: 2546 return cast( 2547 DatasetRunWithItems, 2548 self.api.datasets.get_run( 2549 dataset_name=self._url_encode(dataset_name), 2550 run_name=self._url_encode(run_name), 2551 request_options=None, 2552 ), 2553 ) 2554 except Error as e: 2555 handle_fern_exception(e) 2556 raise e 2557 2558 def get_dataset_runs( 2559 self, 2560 *, 2561 dataset_name: str, 2562 page: Optional[int] = None, 2563 limit: Optional[int] = None, 2564 ) -> PaginatedDatasetRuns: 2565 """Fetch all runs for a dataset. 2566 2567 Args: 2568 dataset_name (str): The name of the dataset. 2569 page (Optional[int]): Page number, starts at 1. 2570 limit (Optional[int]): Limit of items per page. 2571 2572 Returns: 2573 PaginatedDatasetRuns: Paginated list of dataset runs. 2574 """ 2575 try: 2576 return cast( 2577 PaginatedDatasetRuns, 2578 self.api.datasets.get_runs( 2579 dataset_name=self._url_encode(dataset_name), 2580 page=page, 2581 limit=limit, 2582 request_options=None, 2583 ), 2584 ) 2585 except Error as e: 2586 handle_fern_exception(e) 2587 raise e 2588 2589 def delete_dataset_run( 2590 self, *, dataset_name: str, run_name: str 2591 ) -> DeleteDatasetRunResponse: 2592 """Delete a dataset run and all its run items. This action is irreversible. 2593 2594 Args: 2595 dataset_name (str): The name of the dataset. 2596 run_name (str): The name of the run. 2597 2598 Returns: 2599 DeleteDatasetRunResponse: Confirmation of deletion. 2600 """ 2601 try: 2602 return cast( 2603 DeleteDatasetRunResponse, 2604 self.api.datasets.delete_run( 2605 dataset_name=self._url_encode(dataset_name), 2606 run_name=self._url_encode(run_name), 2607 request_options=None, 2608 ), 2609 ) 2610 except Error as e: 2611 handle_fern_exception(e) 2612 raise e 2613 2614 def run_experiment( 2615 self, 2616 *, 2617 name: str, 2618 run_name: Optional[str] = None, 2619 description: Optional[str] = None, 2620 data: ExperimentData, 2621 task: TaskFunction, 2622 evaluators: List[EvaluatorFunction] = [], 2623 composite_evaluator: Optional[CompositeEvaluatorFunction] = None, 2624 run_evaluators: List[RunEvaluatorFunction] = [], 2625 max_concurrency: int = 50, 2626 metadata: Optional[Dict[str, str]] = None, 2627 _dataset_version: Optional[datetime] = None, 2628 ) -> ExperimentResult: 2629 """Run an experiment on a dataset with automatic tracing and evaluation. 2630 2631 This method executes a task function on each item in the provided dataset, 2632 automatically traces all executions with Langfuse for observability, runs 2633 item-level and run-level evaluators on the outputs, and returns comprehensive 2634 results with evaluation metrics. 2635 2636 The experiment system provides: 2637 - Automatic tracing of all task executions 2638 - Concurrent processing with configurable limits 2639 - Comprehensive error handling that isolates failures 2640 - Integration with Langfuse datasets for experiment tracking 2641 - Flexible evaluation framework supporting both sync and async evaluators 2642 2643 Args: 2644 name: Human-readable name for the experiment. Used for identification 2645 in the Langfuse UI. 2646 run_name: Optional exact name for the experiment run. If provided, this will be 2647 used as the exact dataset run name if the `data` contains Langfuse dataset items. 2648 If not provided, this will default to the experiment name appended with an ISO timestamp. 2649 description: Optional description explaining the experiment's purpose, 2650 methodology, or expected outcomes. 2651 data: Array of data items to process. Can be either: 2652 - List of dict-like items with 'input', 'expected_output', 'metadata' keys 2653 - List of Langfuse DatasetItem objects from dataset.items 2654 task: Function that processes each data item and returns output. 2655 Must accept 'item' as keyword argument and can return sync or async results. 2656 The task function signature should be: task(*, item, **kwargs) -> Any 2657 evaluators: List of functions to evaluate each item's output individually. 2658 Each evaluator receives input, output, expected_output, and metadata. 2659 Can return single Evaluation dict or list of Evaluation dicts. 2660 composite_evaluator: Optional function that creates composite scores from item-level evaluations. 2661 Receives the same inputs as item-level evaluators (input, output, expected_output, metadata) 2662 plus the list of evaluations from item-level evaluators. Useful for weighted averages, 2663 pass/fail decisions based on multiple criteria, or custom scoring logic combining multiple metrics. 2664 run_evaluators: List of functions to evaluate the entire experiment run. 2665 Each run evaluator receives all item_results and can compute aggregate metrics. 2666 Useful for calculating averages, distributions, or cross-item comparisons. 2667 max_concurrency: Maximum number of concurrent task executions (default: 50). 2668 Controls the number of items processed simultaneously. Adjust based on 2669 API rate limits and system resources. 2670 metadata: Optional metadata dictionary to attach to all experiment traces. 2671 This metadata will be included in every trace created during the experiment. 2672 If `data` are Langfuse dataset items, the metadata will be attached to the dataset run, too. 2673 2674 Returns: 2675 ExperimentResult containing: 2676 - run_name: The experiment run name. This is equal to the dataset run name if experiment was on Langfuse dataset. 2677 - item_results: List of results for each processed item with outputs and evaluations 2678 - run_evaluations: List of aggregate evaluation results for the entire run 2679 - experiment_id: Stable identifier for the experiment run across all items 2680 - dataset_run_id: ID of the dataset run (if using Langfuse datasets) 2681 - dataset_run_url: Direct URL to view results in Langfuse UI (if applicable) 2682 2683 Raises: 2684 ValueError: If required parameters are missing or invalid 2685 Exception: If experiment setup fails (individual item failures are handled gracefully) 2686 2687 Examples: 2688 Basic experiment with local data: 2689 ```python 2690 def summarize_text(*, item, **kwargs): 2691 return f"Summary: {item['input'][:50]}..." 2692 2693 def length_evaluator(*, input, output, expected_output=None, **kwargs): 2694 return { 2695 "name": "output_length", 2696 "value": len(output), 2697 "comment": f"Output contains {len(output)} characters" 2698 } 2699 2700 result = langfuse.run_experiment( 2701 name="Text Summarization Test", 2702 description="Evaluate summarization quality and length", 2703 data=[ 2704 {"input": "Long article text...", "expected_output": "Expected summary"}, 2705 {"input": "Another article...", "expected_output": "Another summary"} 2706 ], 2707 task=summarize_text, 2708 evaluators=[length_evaluator] 2709 ) 2710 2711 print(f"Processed {len(result.item_results)} items") 2712 for item_result in result.item_results: 2713 print(f"Input: {item_result.item['input']}") 2714 print(f"Output: {item_result.output}") 2715 print(f"Evaluations: {item_result.evaluations}") 2716 ``` 2717 2718 Advanced experiment with async task and multiple evaluators: 2719 ```python 2720 async def llm_task(*, item, **kwargs): 2721 # Simulate async LLM call 2722 response = await openai_client.chat.completions.create( 2723 model="gpt-4", 2724 messages=[{"role": "user", "content": item["input"]}] 2725 ) 2726 return response.choices[0].message.content 2727 2728 def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): 2729 if expected_output and expected_output.lower() in output.lower(): 2730 return {"name": "accuracy", "value": 1.0, "comment": "Correct answer"} 2731 return {"name": "accuracy", "value": 0.0, "comment": "Incorrect answer"} 2732 2733 def toxicity_evaluator(*, input, output, expected_output=None, **kwargs): 2734 # Simulate toxicity check 2735 toxicity_score = check_toxicity(output) # Your toxicity checker 2736 return { 2737 "name": "toxicity", 2738 "value": toxicity_score, 2739 "comment": f"Toxicity level: {'high' if toxicity_score > 0.7 else 'low'}" 2740 } 2741 2742 def average_accuracy(*, item_results, **kwargs): 2743 accuracies = [ 2744 eval.value for result in item_results 2745 for eval in result.evaluations 2746 if eval.name == "accuracy" 2747 ] 2748 return { 2749 "name": "average_accuracy", 2750 "value": sum(accuracies) / len(accuracies) if accuracies else 0, 2751 "comment": f"Average accuracy across {len(accuracies)} items" 2752 } 2753 2754 result = langfuse.run_experiment( 2755 name="LLM Safety and Accuracy Test", 2756 description="Evaluate model accuracy and safety across diverse prompts", 2757 data=test_dataset, # Your dataset items 2758 task=llm_task, 2759 evaluators=[accuracy_evaluator, toxicity_evaluator], 2760 run_evaluators=[average_accuracy], 2761 max_concurrency=5, # Limit concurrent API calls 2762 metadata={"model": "gpt-4", "temperature": 0.7} 2763 ) 2764 ``` 2765 2766 Using with Langfuse datasets: 2767 ```python 2768 # Get dataset from Langfuse 2769 dataset = langfuse.get_dataset("my-eval-dataset") 2770 2771 result = dataset.run_experiment( 2772 name="Production Model Evaluation", 2773 description="Monthly evaluation of production model performance", 2774 task=my_production_task, 2775 evaluators=[accuracy_evaluator, latency_evaluator] 2776 ) 2777 2778 # Results automatically linked to dataset in Langfuse UI 2779 print(f"View results: {result['dataset_run_url']}") 2780 ``` 2781 2782 Note: 2783 - Task and evaluator functions can be either synchronous or asynchronous 2784 - Individual item failures are logged but don't stop the experiment 2785 - All executions are automatically traced and visible in Langfuse UI 2786 - When using Langfuse datasets, results are automatically linked for easy comparison 2787 - This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.) 2788 - Async execution is handled automatically with smart event loop detection 2789 """ 2790 return cast( 2791 ExperimentResult, 2792 run_async_safely( 2793 self._run_experiment_async( 2794 name=name, 2795 run_name=self._create_experiment_run_name( 2796 name=name, run_name=run_name 2797 ), 2798 description=description, 2799 data=data, 2800 task=task, 2801 evaluators=evaluators or [], 2802 composite_evaluator=composite_evaluator, 2803 run_evaluators=run_evaluators or [], 2804 max_concurrency=max_concurrency, 2805 metadata=metadata, 2806 dataset_version=_dataset_version, 2807 ), 2808 ), 2809 ) 2810 2811 async def _run_experiment_async( 2812 self, 2813 *, 2814 name: str, 2815 run_name: str, 2816 description: Optional[str], 2817 data: ExperimentData, 2818 task: TaskFunction, 2819 evaluators: List[EvaluatorFunction], 2820 composite_evaluator: Optional[CompositeEvaluatorFunction], 2821 run_evaluators: List[RunEvaluatorFunction], 2822 max_concurrency: int, 2823 metadata: Optional[Dict[str, Any]] = None, 2824 dataset_version: Optional[datetime] = None, 2825 ) -> ExperimentResult: 2826 langfuse_logger.debug( 2827 "Starting experiment '%s' run '%s' with %s items", name, run_name, len(data) 2828 ) 2829 2830 shared_fallback_experiment_id = self._create_observation_id() 2831 2832 # Set up concurrency control 2833 semaphore = asyncio.Semaphore(max_concurrency) 2834 2835 # Process all items 2836 async def process_item(item: ExperimentItem) -> ExperimentItemResult: 2837 async with semaphore: 2838 return await self._process_experiment_item( 2839 item, 2840 task, 2841 evaluators, 2842 composite_evaluator, 2843 shared_fallback_experiment_id, 2844 name, 2845 run_name, 2846 description, 2847 metadata, 2848 dataset_version, 2849 ) 2850 2851 # Run all items concurrently 2852 tasks = [process_item(item) for item in data] 2853 item_results = await asyncio.gather(*tasks, return_exceptions=True) 2854 2855 # Filter out any exceptions and log errors 2856 valid_results: List[ExperimentItemResult] = [] 2857 for i, result in enumerate(item_results): 2858 if isinstance(result, Exception): 2859 langfuse_logger.error("Item %s failed: %s", i, result) 2860 elif isinstance(result, ExperimentItemResult): 2861 valid_results.append(result) # type: ignore 2862 2863 # Run experiment-level evaluators 2864 run_evaluations: List[Evaluation] = [] 2865 for run_evaluator in run_evaluators: 2866 try: 2867 evaluations = await _run_evaluator( 2868 run_evaluator, item_results=valid_results 2869 ) 2870 run_evaluations.extend(evaluations) 2871 except Exception as e: 2872 langfuse_logger.error("Run evaluator failed: %s", e) 2873 2874 # Generate dataset run URL if applicable 2875 dataset_run_id = next( 2876 ( 2877 result.dataset_run_id 2878 for result in valid_results 2879 if result.dataset_run_id 2880 ), 2881 None, 2882 ) 2883 dataset_run_url = None 2884 if dataset_run_id and data: 2885 try: 2886 # Check if the first item has dataset_id (for DatasetItem objects) 2887 first_item = data[0] 2888 dataset_id = None 2889 2890 if hasattr(first_item, "dataset_id"): 2891 dataset_id = getattr(first_item, "dataset_id", None) 2892 2893 if dataset_id: 2894 project_id = self._get_project_id() 2895 2896 if project_id: 2897 dataset_run_url = f"{self._base_url}/project/{project_id}/datasets/{dataset_id}/runs/{dataset_run_id}" 2898 2899 except Exception: 2900 pass # URL generation is optional 2901 2902 # Store run-level evaluations as scores 2903 for evaluation in run_evaluations: 2904 try: 2905 if dataset_run_id: 2906 self.create_score( 2907 dataset_run_id=dataset_run_id, 2908 name=evaluation.name or "<unknown>", 2909 value=evaluation.value, # type: ignore 2910 comment=evaluation.comment, 2911 metadata=evaluation.metadata, 2912 data_type=evaluation.data_type, # type: ignore 2913 config_id=evaluation.config_id, 2914 ) 2915 2916 except Exception as e: 2917 langfuse_logger.error("Failed to store run evaluation: %s", e) 2918 2919 # Flush scores and traces 2920 self.flush() 2921 2922 return ExperimentResult( 2923 name=name, 2924 run_name=run_name, 2925 description=description, 2926 item_results=valid_results, 2927 run_evaluations=run_evaluations, 2928 experiment_id=dataset_run_id or shared_fallback_experiment_id, 2929 dataset_run_id=dataset_run_id, 2930 dataset_run_url=dataset_run_url, 2931 ) 2932 2933 async def _process_experiment_item( 2934 self, 2935 item: ExperimentItem, 2936 task: Callable, 2937 evaluators: List[Callable], 2938 composite_evaluator: Optional[CompositeEvaluatorFunction], 2939 fallback_experiment_id: str, 2940 experiment_name: str, 2941 experiment_run_name: str, 2942 experiment_description: Optional[str], 2943 experiment_metadata: Optional[Dict[str, Any]] = None, 2944 dataset_version: Optional[datetime] = None, 2945 ) -> ExperimentItemResult: 2946 with self.start_as_current_observation(name="experiment-item-run") as span: 2947 try: 2948 input_data = ( 2949 item.get("input") 2950 if isinstance(item, dict) 2951 else getattr(item, "input", None) 2952 ) 2953 2954 if input_data is None: 2955 raise ValueError("Experiment Item is missing input. Skipping item.") 2956 2957 expected_output = ( 2958 item.get("expected_output") 2959 if isinstance(item, dict) 2960 else getattr(item, "expected_output", None) 2961 ) 2962 2963 item_metadata = ( 2964 item.get("metadata") 2965 if isinstance(item, dict) 2966 else getattr(item, "metadata", None) 2967 ) 2968 2969 final_observation_metadata = { 2970 **(item_metadata if isinstance(item_metadata, dict) else {}), 2971 **(experiment_metadata or {}), 2972 "experiment_name": experiment_name, 2973 "experiment_run_name": experiment_run_name, 2974 } 2975 2976 trace_id = span.trace_id 2977 dataset_id = None 2978 dataset_item_id = None 2979 dataset_run_id = None 2980 2981 if ( 2982 not isinstance(item, dict) 2983 and hasattr(item, "dataset_id") 2984 and hasattr(item, "id") 2985 ): 2986 dataset_id = item.dataset_id 2987 dataset_item_id = item.id 2988 2989 final_observation_metadata.update( 2990 {"dataset_id": dataset_id, "dataset_item_id": dataset_item_id} 2991 ) 2992 2993 experiment_item_id = ( 2994 dataset_item_id or get_sha256_hash_hex(_serialize(input_data))[:16] 2995 ) 2996 experiment_span_attributes = { 2997 k: v 2998 for k, v in { 2999 LangfuseOtelSpanAttributes.ENVIRONMENT: LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, 3000 LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION: experiment_description, 3001 LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT: _serialize( 3002 expected_output 3003 ), 3004 }.items() 3005 if v is not None 3006 } 3007 span._otel_span.set_attributes(experiment_span_attributes) 3008 3009 with span.start_as_current_observation( 3010 name="experiment-item-task", 3011 as_type="span", 3012 input=input_data, 3013 metadata=final_observation_metadata, 3014 ) as task_span: 3015 task_span._otel_span.set_attributes(experiment_span_attributes) 3016 3017 # Link dataset runs to the canonical task observation so their 3018 # latency excludes the subsequent evaluator subtree. 3019 if hasattr(item, "id") and hasattr(item, "dataset_id"): 3020 try: 3021 # Use sync API to avoid event loop issues when 3022 # run_async_safely creates multiple event loops across 3023 # different threads. 3024 dataset_run_item = await asyncio.to_thread( 3025 self.api.dataset_run_items.create, 3026 run_name=experiment_run_name, 3027 run_description=experiment_description, 3028 metadata=experiment_metadata, 3029 dataset_item_id=item.id, # type: ignore 3030 trace_id=trace_id, 3031 observation_id=task_span.id, 3032 dataset_version=dataset_version, 3033 ) 3034 3035 dataset_run_id = dataset_run_item.dataset_run_id 3036 3037 except Exception as e: 3038 langfuse_logger.error( 3039 "Failed to create dataset run item: %s", e 3040 ) 3041 3042 experiment_id = dataset_run_id or fallback_experiment_id 3043 propagated_experiment_attributes = PropagatedExperimentAttributes( 3044 experiment_id=experiment_id, 3045 experiment_name=experiment_run_name, 3046 experiment_metadata=_flatten_and_serialize_metadata_values( 3047 experiment_metadata 3048 ), 3049 experiment_dataset_id=dataset_id, 3050 experiment_item_id=experiment_item_id, 3051 experiment_item_metadata=_flatten_and_serialize_metadata_values( 3052 item_metadata if isinstance(item_metadata, dict) else None 3053 ), 3054 experiment_item_root_observation_id=task_span.id, 3055 ) 3056 3057 with _propagate_attributes( 3058 experiment=propagated_experiment_attributes 3059 ): 3060 # _propagate_attributes updates the current task span and future children. 3061 # Explicitly backfill the parent item-run span to preserve experiment association. 3062 span._otel_span.set_attributes( 3063 _get_propagated_attributes_from_context( 3064 otel_context_api.get_current() 3065 ) 3066 ) 3067 try: 3068 output = await _run_task(task, item) 3069 except Exception as e: 3070 task_span.update( 3071 output=f"Error: {str(e)}", 3072 level="ERROR", 3073 status_message=str(e), 3074 ) 3075 raise 3076 3077 task_span.update(output=output) 3078 3079 span.update( 3080 input=input_data, 3081 output=output, 3082 metadata=final_observation_metadata, 3083 ) 3084 3085 except Exception as e: 3086 span.update( 3087 output=f"Error: {str(e)}", level="ERROR", status_message=str(e) 3088 ) 3089 raise e 3090 3091 evaluations: List[Evaluation] = [] 3092 failed_evaluator_count = 0 3093 eval_metadata = ( 3094 item.get("metadata") 3095 if isinstance(item, dict) 3096 else getattr(item, "metadata", None) 3097 ) 3098 3099 if len(evaluators) > 0: 3100 with _propagate_attributes(experiment=propagated_experiment_attributes): 3101 with span.start_as_current_observation( 3102 name="experiment-item-evaluation", as_type="span" 3103 ) as evaluation_span: 3104 for evaluator_index, evaluator in enumerate(evaluators): 3105 evaluator_name = _get_evaluator_name(evaluator) 3106 evaluator_input = { 3107 "input": input_data, 3108 "output": output, 3109 "expected_output": expected_output, 3110 "metadata": eval_metadata, 3111 } 3112 3113 with evaluation_span.start_as_current_observation( 3114 name=evaluator_name, 3115 as_type="evaluator", 3116 input=evaluator_input, 3117 metadata={ 3118 "evaluator_kind": "item", 3119 "evaluator_index": evaluator_index, 3120 }, 3121 ) as evaluator_span: 3122 try: 3123 eval_results = await _run_evaluator( 3124 evaluator, 3125 _raise_on_error=True, 3126 **evaluator_input, 3127 ) 3128 evaluator_span.update( 3129 output=_serialize_evaluations(eval_results) 3130 ) 3131 except Exception as e: 3132 failed_evaluator_count += 1 3133 evaluator_span.update( 3134 output={"error": str(e)}, 3135 level="ERROR", 3136 status_message=str(e), 3137 ) 3138 langfuse_logger.error("Evaluator failed: %s", e) 3139 continue 3140 3141 evaluations.extend(eval_results) 3142 3143 for evaluation in eval_results: 3144 try: 3145 self.create_score( 3146 trace_id=trace_id, 3147 observation_id=task_span.id, 3148 name=evaluation.name, 3149 value=evaluation.value, # type: ignore 3150 comment=evaluation.comment, 3151 metadata=evaluation.metadata, 3152 config_id=evaluation.config_id, 3153 data_type=evaluation.data_type, # type: ignore 3154 ) 3155 except Exception as e: 3156 langfuse_logger.error( 3157 "Failed to store evaluation: %s", e 3158 ) 3159 3160 if composite_evaluator and evaluations: 3161 composite_evaluator_name = _get_evaluator_name( 3162 composite_evaluator 3163 ) 3164 composite_input = { 3165 "input": input_data, 3166 "output": output, 3167 "expected_output": expected_output, 3168 "metadata": eval_metadata, 3169 "evaluations": _serialize_evaluations(evaluations), 3170 } 3171 3172 with evaluation_span.start_as_current_observation( 3173 name=composite_evaluator_name, 3174 as_type="evaluator", 3175 input=composite_input, 3176 metadata={"evaluator_kind": "composite"}, 3177 ) as composite_evaluator_span: 3178 try: 3179 result = composite_evaluator( 3180 input=input_data, 3181 output=output, 3182 expected_output=expected_output, 3183 metadata=eval_metadata, 3184 evaluations=evaluations, 3185 ) 3186 3187 if asyncio.iscoroutine(result): 3188 result = await result 3189 3190 composite_evals = _normalize_evaluator_result( 3191 result 3192 ) 3193 3194 composite_evaluator_span.update( 3195 output=_serialize_evaluations(composite_evals) 3196 ) 3197 except Exception as e: 3198 failed_evaluator_count += 1 3199 composite_evaluator_span.update( 3200 output={"error": str(e)}, 3201 level="ERROR", 3202 status_message=str(e), 3203 ) 3204 langfuse_logger.error( 3205 "Composite evaluator failed: %s", e 3206 ) 3207 composite_evals = [] 3208 3209 for composite_evaluation in composite_evals: 3210 evaluations.append(composite_evaluation) 3211 try: 3212 self.create_score( 3213 trace_id=trace_id, 3214 observation_id=task_span.id, 3215 name=composite_evaluation.name, 3216 value=composite_evaluation.value, # type: ignore 3217 comment=composite_evaluation.comment, 3218 metadata=composite_evaluation.metadata, 3219 config_id=composite_evaluation.config_id, 3220 data_type=composite_evaluation.data_type, # type: ignore 3221 ) 3222 except Exception as e: 3223 langfuse_logger.error( 3224 "Failed to store composite evaluation: %s", e 3225 ) 3226 3227 evaluation_span.update( 3228 output={ 3229 "evaluator_count": len(evaluators) 3230 + (1 if composite_evaluator else 0), 3231 "evaluation_count": len(evaluations), 3232 "failed_evaluator_count": failed_evaluator_count, 3233 "skipped_evaluator_count": ( 3234 1 if composite_evaluator and not evaluations else 0 3235 ), 3236 } 3237 ) 3238 3239 return ExperimentItemResult( 3240 item=item, 3241 output=output, 3242 evaluations=evaluations, 3243 trace_id=trace_id, 3244 dataset_run_id=dataset_run_id, 3245 ) 3246 3247 def _create_experiment_run_name( 3248 self, *, name: Optional[str] = None, run_name: Optional[str] = None 3249 ) -> str: 3250 if run_name: 3251 return run_name 3252 3253 iso_timestamp = _get_timestamp().isoformat().replace("+00:00", "Z") 3254 3255 return f"{name} - {iso_timestamp}" 3256 3257 def run_batched_evaluation( 3258 self, 3259 *, 3260 scope: Literal["traces", "observations"], 3261 mapper: MapperFunction, 3262 filter: Optional[str] = None, 3263 fetch_batch_size: int = 50, 3264 fetch_trace_fields: Optional[str] = None, 3265 max_items: Optional[int] = None, 3266 max_retries: int = 3, 3267 evaluators: List[EvaluatorFunction], 3268 composite_evaluator: Optional[CompositeEvaluatorFunction] = None, 3269 max_concurrency: int = 5, 3270 metadata: Optional[Dict[str, Any]] = None, 3271 _add_observation_scores_to_trace: bool = False, 3272 _additional_trace_tags: Optional[List[str]] = None, 3273 resume_from: Optional[BatchEvaluationResumeToken] = None, 3274 verbose: bool = False, 3275 ) -> BatchEvaluationResult: 3276 """Fetch traces or observations and run evaluations on each item. 3277 3278 This method provides a powerful way to evaluate existing data in Langfuse at scale. 3279 It fetches items based on filters, transforms them using a mapper function, runs 3280 evaluators on each item, and creates scores that are linked back to the original 3281 entities. This is ideal for: 3282 3283 - Running evaluations on production traces after deployment 3284 - Backtesting new evaluation metrics on historical data 3285 - Batch scoring of observations for quality monitoring 3286 - Periodic evaluation runs on recent data 3287 3288 The method uses a streaming/pipeline approach to process items in batches, making 3289 it memory-efficient for large datasets. It includes comprehensive error handling, 3290 retry logic, and resume capability for long-running evaluations. 3291 3292 Args: 3293 scope: The type of items to evaluate. Must be one of: 3294 - "traces": Evaluate complete traces with all their observations 3295 - "observations": Evaluate individual observations (spans, generations, events) 3296 mapper: Function that transforms API response objects into evaluator inputs. 3297 Receives a trace/observation object and returns an EvaluatorInputs 3298 instance with input, output, expected_output, and metadata fields. 3299 Can be sync or async. 3300 evaluators: List of evaluation functions to run on each item. Each evaluator 3301 receives the mapped inputs and returns Evaluation object(s). Evaluator 3302 failures are logged but don't stop the batch evaluation. 3303 filter: Optional JSON filter string for querying items (same format as Langfuse API). Examples: 3304 - '{"tags": ["production"]}' 3305 - '{"user_id": "user123", "timestamp": {"operator": ">", "value": "2024-01-01"}}' 3306 Default: None (fetches all items). 3307 fetch_batch_size: Number of items to fetch per API call and hold in memory. 3308 Larger values may be faster but use more memory. Default: 50. 3309 fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'. 3310 max_items: Maximum total number of items to process. If None, processes all 3311 items matching the filter. Useful for testing or limiting evaluation runs. 3312 Default: None (process all). 3313 max_concurrency: Maximum number of items to evaluate concurrently. Controls 3314 parallelism and resource usage. Default: 5. 3315 composite_evaluator: Optional function that creates a composite score from 3316 item-level evaluations. Receives the original item and its evaluations, 3317 returns a single Evaluation. Useful for weighted averages or combined metrics. 3318 Default: None. 3319 metadata: Optional metadata dict to add to all created scores. Useful for 3320 tracking evaluation runs, versions, or other context. Default: None. 3321 max_retries: Maximum number of retry attempts for failed batch fetches. 3322 Uses exponential backoff (1s, 2s, 4s). Default: 3. 3323 verbose: If True, logs progress information to console. Useful for monitoring 3324 long-running evaluations. Default: False. 3325 resume_from: Optional resume token from a previous incomplete run. Allows 3326 continuing evaluation after interruption or failure. Default: None. 3327 3328 3329 Returns: 3330 BatchEvaluationResult containing: 3331 - total_items_fetched: Number of items fetched from API 3332 - total_items_processed: Number of items successfully evaluated 3333 - total_items_failed: Number of items that failed evaluation 3334 - total_scores_created: Scores created by item-level evaluators 3335 - total_composite_scores_created: Scores created by composite evaluator 3336 - total_evaluations_failed: Individual evaluator failures 3337 - evaluator_stats: Per-evaluator statistics (success rate, scores created) 3338 - resume_token: Token for resuming if incomplete (None if completed) 3339 - completed: True if all items processed 3340 - duration_seconds: Total execution time 3341 - failed_item_ids: IDs of items that failed 3342 - error_summary: Error types and counts 3343 - has_more_items: True if max_items reached but more exist 3344 3345 Raises: 3346 ValueError: If invalid scope is provided. 3347 3348 Examples: 3349 Basic trace evaluation: 3350 ```python 3351 from langfuse import Langfuse, EvaluatorInputs, Evaluation 3352 3353 client = Langfuse() 3354 3355 # Define mapper to extract fields from traces 3356 def trace_mapper(trace): 3357 return EvaluatorInputs( 3358 input=trace.input, 3359 output=trace.output, 3360 expected_output=None, 3361 metadata={"trace_id": trace.id} 3362 ) 3363 3364 # Define evaluator 3365 def length_evaluator(*, input, output, expected_output, metadata): 3366 return Evaluation( 3367 name="output_length", 3368 value=len(output) if output else 0 3369 ) 3370 3371 # Run batch evaluation 3372 result = client.run_batched_evaluation( 3373 scope="traces", 3374 mapper=trace_mapper, 3375 evaluators=[length_evaluator], 3376 filter='{"tags": ["production"]}', 3377 max_items=1000, 3378 verbose=True 3379 ) 3380 3381 print(f"Processed {result.total_items_processed} traces") 3382 print(f"Created {result.total_scores_created} scores") 3383 ``` 3384 3385 Evaluation with composite scorer: 3386 ```python 3387 def accuracy_evaluator(*, input, output, expected_output, metadata): 3388 # ... evaluation logic 3389 return Evaluation(name="accuracy", value=0.85) 3390 3391 def relevance_evaluator(*, input, output, expected_output, metadata): 3392 # ... evaluation logic 3393 return Evaluation(name="relevance", value=0.92) 3394 3395 def composite_evaluator(*, item, evaluations): 3396 # Weighted average of evaluations 3397 weights = {"accuracy": 0.6, "relevance": 0.4} 3398 total = sum( 3399 e.value * weights.get(e.name, 0) 3400 for e in evaluations 3401 if isinstance(e.value, (int, float)) 3402 ) 3403 return Evaluation( 3404 name="composite_score", 3405 value=total, 3406 comment=f"Weighted average of {len(evaluations)} metrics" 3407 ) 3408 3409 result = client.run_batched_evaluation( 3410 scope="traces", 3411 mapper=trace_mapper, 3412 evaluators=[accuracy_evaluator, relevance_evaluator], 3413 composite_evaluator=composite_evaluator, 3414 filter='{"user_id": "important_user"}', 3415 verbose=True 3416 ) 3417 ``` 3418 3419 Handling incomplete runs with resume: 3420 ```python 3421 # Initial run that may fail or timeout 3422 result = client.run_batched_evaluation( 3423 scope="observations", 3424 mapper=obs_mapper, 3425 evaluators=[my_evaluator], 3426 max_items=10000, 3427 verbose=True 3428 ) 3429 3430 # Check if incomplete 3431 if not result.completed and result.resume_token: 3432 print(f"Processed {result.resume_token.items_processed} items before interruption") 3433 3434 # Resume from where it left off 3435 result = client.run_batched_evaluation( 3436 scope="observations", 3437 mapper=obs_mapper, 3438 evaluators=[my_evaluator], 3439 resume_from=result.resume_token, 3440 verbose=True 3441 ) 3442 3443 print(f"Total items processed: {result.total_items_processed}") 3444 ``` 3445 3446 Monitoring evaluator performance: 3447 ```python 3448 result = client.run_batched_evaluation(...) 3449 3450 for stats in result.evaluator_stats: 3451 success_rate = stats.successful_runs / stats.total_runs 3452 print(f"{stats.name}:") 3453 print(f" Success rate: {success_rate:.1%}") 3454 print(f" Scores created: {stats.total_scores_created}") 3455 3456 if stats.failed_runs > 0: 3457 print(f" ⚠️ Failed {stats.failed_runs} times") 3458 ``` 3459 3460 Note: 3461 - Evaluator failures are logged but don't stop the batch evaluation 3462 - Individual item failures are tracked but don't stop processing 3463 - Fetch failures are retried with exponential backoff 3464 - All scores are automatically flushed to Langfuse at the end 3465 - The resume mechanism uses timestamp-based filtering to avoid duplicates 3466 """ 3467 runner = BatchEvaluationRunner(self) 3468 3469 return cast( 3470 BatchEvaluationResult, 3471 run_async_safely( 3472 runner.run_async( 3473 scope=scope, 3474 mapper=mapper, 3475 evaluators=evaluators, 3476 filter=filter, 3477 fetch_batch_size=fetch_batch_size, 3478 fetch_trace_fields=fetch_trace_fields, 3479 max_items=max_items, 3480 max_concurrency=max_concurrency, 3481 composite_evaluator=composite_evaluator, 3482 metadata=metadata, 3483 _add_observation_scores_to_trace=_add_observation_scores_to_trace, 3484 _additional_trace_tags=_additional_trace_tags, 3485 max_retries=max_retries, 3486 verbose=verbose, 3487 resume_from=resume_from, 3488 ) 3489 ), 3490 ) 3491 3492 def auth_check(self) -> bool: 3493 """Check if the provided credentials (public and secret key) are valid. 3494 3495 Raises: 3496 Exception: If no projects were found for the provided credentials. 3497 3498 Note: 3499 This method is blocking. It is discouraged to use it in production code. 3500 """ 3501 try: 3502 projects = self.api.projects.get() 3503 langfuse_logger.debug( 3504 "Auth check successful, found %s projects", len(projects.data) 3505 ) 3506 if len(projects.data) == 0: 3507 raise Exception( 3508 "Auth check failed, no project found for the keys provided." 3509 ) 3510 return True 3511 3512 except AttributeError as e: 3513 langfuse_logger.warning( 3514 "Auth check failed: Client not properly initialized. Error: %s", e 3515 ) 3516 return False 3517 3518 except Error as e: 3519 handle_fern_exception(e) 3520 raise e 3521 3522 def create_dataset( 3523 self, 3524 *, 3525 name: str, 3526 description: Optional[str] = None, 3527 metadata: Optional[Any] = None, 3528 input_schema: Optional[Any] = None, 3529 expected_output_schema: Optional[Any] = None, 3530 ) -> Dataset: 3531 """Create a dataset with the given name on Langfuse. 3532 3533 Args: 3534 name: Name of the dataset to create. 3535 description: Description of the dataset. Defaults to None. 3536 metadata: Additional metadata. Defaults to None. 3537 input_schema: JSON Schema for validating dataset item inputs. When set, all new items will be validated against this schema. 3538 expected_output_schema: JSON Schema for validating dataset item expected outputs. When set, all new items will be validated against this schema. 3539 3540 Returns: 3541 Dataset: The created dataset as returned by the Langfuse API. 3542 """ 3543 try: 3544 langfuse_logger.debug("Creating datasets %s", name) 3545 3546 result = self.api.datasets.create( 3547 name=name, 3548 description=description, 3549 metadata=metadata, 3550 input_schema=input_schema, 3551 expected_output_schema=expected_output_schema, 3552 ) 3553 3554 return cast(Dataset, result) 3555 3556 except Error as e: 3557 handle_fern_exception(e) 3558 raise e 3559 3560 def create_dataset_item( 3561 self, 3562 *, 3563 dataset_name: str, 3564 input: Optional[Any] = None, 3565 expected_output: Optional[Any] = None, 3566 metadata: Optional[Any] = None, 3567 source_trace_id: Optional[str] = None, 3568 source_observation_id: Optional[str] = None, 3569 status: Optional[DatasetStatus] = None, 3570 id: Optional[str] = None, 3571 ) -> DatasetItem: 3572 """Create a dataset item. 3573 3574 Upserts if an item with id already exists. 3575 3576 Args: 3577 dataset_name: Name of the dataset in which the dataset item should be created. 3578 input: Input data. Defaults to None. Can contain any dict, list or scalar. 3579 expected_output: Expected output data. Defaults to None. Can contain any dict, list or scalar. 3580 metadata: Additional metadata. Defaults to None. Can contain any dict, list or scalar. 3581 source_trace_id: Id of the source trace. Defaults to None. 3582 source_observation_id: Id of the source observation. Defaults to None. 3583 status: Status of the dataset item. Defaults to ACTIVE for newly created items. 3584 id: Id of the dataset item. Defaults to None. Provide your own id if you want to dedupe dataset items. Id needs to be globally unique and cannot be reused across datasets. 3585 3586 Returns: 3587 DatasetItem: The created dataset item as returned by the Langfuse API. 3588 3589 Example: 3590 ```python 3591 from langfuse import Langfuse 3592 3593 langfuse = Langfuse() 3594 3595 # Uploading items to the Langfuse dataset named "capital_cities" 3596 langfuse.create_dataset_item( 3597 dataset_name="capital_cities", 3598 input={"input": {"country": "Italy"}}, 3599 expected_output={"expected_output": "Rome"}, 3600 metadata={"foo": "bar"} 3601 ) 3602 ``` 3603 """ 3604 try: 3605 langfuse_logger.debug("Creating dataset item for dataset %s", dataset_name) 3606 3607 # Media uploads must reference the (dataset, item) they belong to, and 3608 # the item need not exist yet — so settle on the item id up front and 3609 # reuse it for the create call below. 3610 item_id = id if id is not None else str(uuid.uuid4()) 3611 3612 # Single pass per field: swap each LangfuseMedia for its reference 3613 # string (derived from content, not the upload) and collect the media 3614 # still to upload, deduped by media id and tagged with its field. 3615 pending_media: Dict[str, Tuple[LangfuseMedia, str]] = {} 3616 input = self._process_dataset_item_media( 3617 data=input, 3618 pending_media=pending_media, 3619 field=DatasetItemMediaReferenceField.INPUT.value, 3620 ) 3621 expected_output = self._process_dataset_item_media( 3622 data=expected_output, 3623 pending_media=pending_media, 3624 field=DatasetItemMediaReferenceField.EXPECTED_OUTPUT.value, 3625 ) 3626 metadata = self._process_dataset_item_media( 3627 data=metadata, 3628 pending_media=pending_media, 3629 field=DatasetItemMediaReferenceField.METADATA.value, 3630 ) 3631 3632 # The upload needs the dataset id, but the create API only takes the 3633 # name. Resolve it once, and only when there is actually media to 3634 # upload — a plain item pays no extra datasets.get round-trip. 3635 if pending_media: 3636 assert self._resources is not None 3637 dataset_id = self.api.datasets.get(self._url_encode(dataset_name)).id 3638 for media, field in pending_media.values(): 3639 self._resources._media_manager._upload_media_sync( 3640 media=media, 3641 dataset_id=dataset_id, 3642 dataset_item_id=item_id, 3643 field=field, 3644 ) 3645 3646 result = self.api.dataset_items.create( 3647 dataset_name=dataset_name, 3648 input=input, 3649 expected_output=expected_output, 3650 metadata=metadata, 3651 source_trace_id=source_trace_id, 3652 source_observation_id=source_observation_id, 3653 status=status, 3654 id=item_id, 3655 ) 3656 3657 return cast(DatasetItem, result) 3658 except Error as e: 3659 handle_fern_exception(e) 3660 raise e 3661 3662 def _process_dataset_item_media( 3663 self, 3664 *, 3665 data: Any, 3666 pending_media: Dict[str, Tuple[LangfuseMedia, str]], 3667 field: str, 3668 ) -> Any: 3669 """Swap each ``LangfuseMedia`` for its reference string in ``data``. 3670 3671 Each replaced media is recorded in ``pending_media`` (keyed by media id, 3672 so the same media across fields uploads once) for the caller to upload 3673 after the dataset id has been resolved. 3674 """ 3675 if self._resources is None: 3676 return data 3677 3678 max_levels = 10 3679 3680 def _process_data_recursively( 3681 data: Any, level: int, ancestor_container_ids: set[int] 3682 ) -> Any: 3683 if isinstance(data, LangfuseMedia): 3684 reference_string = data._reference_string 3685 media_id = data._media_id 3686 if reference_string is None or media_id is None: 3687 raise ValueError( 3688 "Cannot create dataset item with invalid LangfuseMedia." 3689 ) 3690 # First field a media appears in wins; later duplicates dedupe. 3691 pending_media.setdefault(media_id, (data, field)) 3692 return reference_string 3693 3694 if isinstance(data, LangfuseMediaReference): 3695 return data.reference_string if data.reference_string else data 3696 3697 # Tuples are intentionally excluded: namedtuple subclasses can't be 3698 # rebuilt from an iterable, so media inside them is left untouched. 3699 if not isinstance(data, (list, set, frozenset, dict)): 3700 return data 3701 3702 # Container ids only protect against recursive cycles. 3703 data_id = id(data) 3704 if data_id in ancestor_container_ids or level > max_levels: 3705 return data 3706 3707 next_ancestor_container_ids = ancestor_container_ids | {data_id} 3708 3709 if isinstance(data, (list, set, frozenset)): 3710 processed = ( 3711 _process_data_recursively( 3712 item, level + 1, next_ancestor_container_ids 3713 ) 3714 for item in data 3715 ) 3716 return type(data)(processed) 3717 3718 return { 3719 key: _process_data_recursively( 3720 value, level + 1, next_ancestor_container_ids 3721 ) 3722 for key, value in data.items() 3723 } 3724 3725 return _process_data_recursively(data, 1, set()) 3726 3727 def _hydrate_dataset_item_media_references(self, item: DatasetItem) -> DatasetItem: 3728 media_references = item.media_references or [] 3729 if not media_references: 3730 return item 3731 3732 # Map the API enum member to the snake_case model attribute so this keeps 3733 # working regardless of the enum's wire value (e.g. "expectedOutput"). 3734 attr_by_field = { 3735 DatasetItemMediaReferenceField.INPUT: "input", 3736 DatasetItemMediaReferenceField.EXPECTED_OUTPUT: "expected_output", 3737 DatasetItemMediaReferenceField.METADATA: "metadata", 3738 } 3739 hydrated_fields = { 3740 "input": item.input, 3741 "expected_output": item.expected_output, 3742 "metadata": item.metadata, 3743 } 3744 3745 for media_reference in media_references: 3746 media = media_reference.media 3747 field = attr_by_field.get(media_reference.field) 3748 if field is None: 3749 continue 3750 3751 replacement = LangfuseMediaReference( 3752 media_id=media.media_id, 3753 content_type=media.content_type, 3754 url=media.url, 3755 url_expiry=media.url_expiry, 3756 content_length=media.content_length, 3757 reference_string=media_reference.reference_string, 3758 ) 3759 hydrated_fields[field] = self._replace_json_path_value( 3760 value=hydrated_fields[field], 3761 path=media_reference.json_path, 3762 replacement=replacement, 3763 ) 3764 3765 return item.model_copy( 3766 update={ 3767 "input": hydrated_fields["input"], 3768 "expected_output": hydrated_fields["expected_output"], 3769 "metadata": hydrated_fields["metadata"], 3770 } 3771 ) 3772 3773 def _replace_json_path_value( 3774 self, *, value: Any, path: str, replacement: LangfuseMediaReference 3775 ) -> Any: 3776 try: 3777 return json_path.set_value_at_path(value, path, replacement) 3778 except Exception as e: 3779 langfuse_logger.warning( 3780 "Failed to hydrate dataset media reference at JSONPath %s", 3781 path, 3782 exc_info=e, 3783 ) 3784 3785 return value 3786 3787 def resolve_media_references( 3788 self, 3789 *, 3790 obj: Any, 3791 resolve_with: Literal["base64_data_uri"], 3792 max_depth: int = 10, 3793 content_fetch_timeout_seconds: int = 5, 3794 ) -> Any: 3795 """Replace media reference strings in an object with base64 data URIs. 3796 3797 This method recursively traverses an object (up to max_depth) looking for media reference strings 3798 in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using 3799 the provided Langfuse client and replaces the reference string with a base64 data URI. 3800 3801 If fetching media content fails for a reference string, a warning is logged and the reference 3802 string is left unchanged. 3803 3804 Args: 3805 obj: The object to process. Can be a primitive value, array, or nested object. 3806 If the object has a __dict__ attribute, a dict will be returned instead of the original object type. 3807 resolve_with: The representation of the media content to replace the media reference string with. 3808 Currently only "base64_data_uri" is supported. 3809 max_depth: int: The maximum depth to traverse the object. Default is 10. 3810 content_fetch_timeout_seconds: int: The timeout in seconds for fetching media content. Default is 5. 3811 3812 Returns: 3813 A deep copy of the input object with all media references replaced with base64 data URIs where possible. 3814 If the input object has a __dict__ attribute, a dict will be returned instead of the original object type. 3815 3816 Example: 3817 obj = { 3818 "image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", 3819 "nested": { 3820 "pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" 3821 } 3822 } 3823 3824 result = await LangfuseMedia.resolve_media_references(obj, langfuse_client) 3825 3826 # Result: 3827 # { 3828 # "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", 3829 # "nested": { 3830 # "pdf": "data:application/pdf;base64,JVBERi0xLjcK..." 3831 # } 3832 # } 3833 """ 3834 return LangfuseMedia.resolve_media_references( 3835 langfuse_client=self, 3836 obj=obj, 3837 resolve_with=resolve_with, 3838 max_depth=max_depth, 3839 content_fetch_timeout_seconds=content_fetch_timeout_seconds, 3840 ) 3841 3842 @overload 3843 def get_prompt( 3844 self, 3845 name: str, 3846 *, 3847 version: Optional[int] = None, 3848 label: Optional[str] = None, 3849 type: Literal["chat"], 3850 cache_ttl_seconds: Optional[int] = None, 3851 fallback: Optional[List[ChatMessageDict]] = None, 3852 max_retries: Optional[int] = None, 3853 fetch_timeout_seconds: Optional[int] = None, 3854 ) -> ChatPromptClient: ... 3855 3856 @overload 3857 def get_prompt( 3858 self, 3859 name: str, 3860 *, 3861 version: Optional[int] = None, 3862 label: Optional[str] = None, 3863 type: Literal["text"] = "text", 3864 cache_ttl_seconds: Optional[int] = None, 3865 fallback: Optional[str] = None, 3866 max_retries: Optional[int] = None, 3867 fetch_timeout_seconds: Optional[int] = None, 3868 ) -> TextPromptClient: ... 3869 3870 def get_prompt( 3871 self, 3872 name: str, 3873 *, 3874 version: Optional[int] = None, 3875 label: Optional[str] = None, 3876 type: Literal["chat", "text"] = "text", 3877 cache_ttl_seconds: Optional[int] = None, 3878 fallback: Union[Optional[List[ChatMessageDict]], Optional[str]] = None, 3879 max_retries: Optional[int] = None, 3880 fetch_timeout_seconds: Optional[int] = None, 3881 ) -> PromptClient: 3882 """Get a prompt. 3883 3884 This method attempts to fetch the requested prompt from the local cache. If the prompt is not found 3885 in the cache or if the cached prompt has expired, it will try to fetch the prompt from the server again 3886 and update the cache. If fetching the new prompt fails, and there is an expired prompt in the cache, it will 3887 return the expired prompt as a fallback. 3888 3889 Args: 3890 name (str): The name of the prompt to retrieve. 3891 3892 Keyword Args: 3893 version (Optional[int]): The version of the prompt to retrieve. If no label and version is specified, the `production` label is returned. Specify either version or label, not both. 3894 label: Optional[str]: The label of the prompt to retrieve. If no label and version is specified, the `production` label is returned. Specify either version or label, not both. 3895 cache_ttl_seconds: Optional[int]: Time-to-live in seconds for caching the prompt. Must be specified as a 3896 keyword argument. If not set, defaults to 60 seconds. Disables caching if set to 0. 3897 type: Literal["chat", "text"]: The type of the prompt to retrieve. Defaults to "text". 3898 fallback: Union[Optional[List[ChatMessageDict]], Optional[str]]: The prompt string to return if fetching the prompt fails. Important on the first call where no cached prompt is available. Follows Langfuse prompt formatting with double curly braces for variables. Defaults to None. 3899 max_retries: Optional[int]: The maximum number of retries in case of API/network errors. Defaults to 2. The maximum value is 4. Retries have an exponential backoff with a maximum delay of 10 seconds. 3900 fetch_timeout_seconds: Optional[int]: The timeout in milliseconds for fetching the prompt. Defaults to the default timeout set on the SDK, which is 5 seconds per default. 3901 3902 Returns: 3903 The prompt object retrieved from the cache or directly fetched if not cached or expired of type 3904 - TextPromptClient, if type argument is 'text'. 3905 - ChatPromptClient, if type argument is 'chat'. 3906 3907 Raises: 3908 Exception: Propagates any exceptions raised during the fetching of a new prompt, unless there is an 3909 expired prompt in the cache, in which case it logs a warning and returns the expired prompt. 3910 """ 3911 if self._resources is None: 3912 raise Error( 3913 "SDK is not correctly initialized. Check the init logs for more details." 3914 ) 3915 if version is not None and label is not None: 3916 raise ValueError("Cannot specify both version and label at the same time.") 3917 3918 if not name: 3919 raise ValueError("Prompt name cannot be empty.") 3920 3921 cache_key = PromptCache.generate_cache_key(name, version=version, label=label) 3922 bounded_max_retries = self._get_bounded_max_retries( 3923 max_retries, default_max_retries=2, max_retries_upper_bound=4 3924 ) 3925 3926 langfuse_logger.debug("Getting prompt '%s'", cache_key) 3927 cached_prompt = self._resources.prompt_cache.get(cache_key) 3928 3929 if cached_prompt is None or cache_ttl_seconds == 0: 3930 langfuse_logger.debug( 3931 "Prompt '%s' not found in cache or caching disabled.", cache_key 3932 ) 3933 try: 3934 return self._fetch_prompt_and_update_cache( 3935 name, 3936 version=version, 3937 label=label, 3938 ttl_seconds=cache_ttl_seconds, 3939 max_retries=bounded_max_retries, 3940 fetch_timeout_seconds=fetch_timeout_seconds, 3941 ) 3942 except Exception as e: 3943 if fallback: 3944 langfuse_logger.warning( 3945 "Returning fallback prompt for '%s' due to fetch error: %s", 3946 cache_key, 3947 e, 3948 ) 3949 3950 fallback_client_args: Dict[str, Any] = { 3951 "name": name, 3952 "prompt": fallback, 3953 "type": type, 3954 "version": version or 0, 3955 "config": {}, 3956 "labels": [label] if label else [], 3957 "tags": [], 3958 } 3959 3960 if type == "text": 3961 return TextPromptClient( 3962 prompt=Prompt_Text(**fallback_client_args), 3963 is_fallback=True, 3964 ) 3965 3966 if type == "chat": 3967 return ChatPromptClient( 3968 prompt=Prompt_Chat(**fallback_client_args), 3969 is_fallback=True, 3970 ) 3971 3972 raise e 3973 3974 if cached_prompt.is_expired(): 3975 langfuse_logger.debug("Stale prompt '%s' found in cache.", cache_key) 3976 try: 3977 # refresh prompt in background thread, refresh_prompt deduplicates tasks 3978 langfuse_logger.debug( 3979 "Refreshing prompt '%s' in background.", cache_key 3980 ) 3981 3982 def refresh_task() -> None: 3983 self._fetch_prompt_and_update_cache( 3984 name, 3985 version=version, 3986 label=label, 3987 ttl_seconds=cache_ttl_seconds, 3988 max_retries=bounded_max_retries, 3989 fetch_timeout_seconds=fetch_timeout_seconds, 3990 ) 3991 3992 self._resources.prompt_cache.add_refresh_prompt_task_if_current( 3993 cache_key, 3994 cached_prompt, 3995 refresh_task, 3996 ) 3997 langfuse_logger.debug( 3998 "Returning stale prompt '%s' from cache.", cache_key 3999 ) 4000 # return stale prompt 4001 return cached_prompt.value 4002 4003 except Exception as e: 4004 langfuse_logger.warning( 4005 "Error when refreshing cached prompt '%s', returning cached version. " 4006 "Error: %s", 4007 cache_key, 4008 e, 4009 ) 4010 # creation of refresh prompt task failed, return stale prompt 4011 return cached_prompt.value 4012 4013 return cached_prompt.value 4014 4015 def _fetch_prompt_and_update_cache( 4016 self, 4017 name: str, 4018 *, 4019 version: Optional[int] = None, 4020 label: Optional[str] = None, 4021 ttl_seconds: Optional[int] = None, 4022 max_retries: int, 4023 fetch_timeout_seconds: Optional[int], 4024 ) -> PromptClient: 4025 cache_key = PromptCache.generate_cache_key(name, version=version, label=label) 4026 langfuse_logger.debug("Fetching prompt '%s' from server...", cache_key) 4027 4028 try: 4029 4030 @backoff.on_exception( 4031 backoff.constant, Exception, max_tries=max_retries + 1, logger=None 4032 ) 4033 def fetch_prompts() -> Any: 4034 return self.api.prompts.get( 4035 self._url_encode(name), 4036 version=version, 4037 label=label, 4038 request_options={ 4039 "timeout_in_seconds": fetch_timeout_seconds, 4040 } 4041 if fetch_timeout_seconds is not None 4042 else None, 4043 ) 4044 4045 prompt_response = fetch_prompts() 4046 4047 prompt: PromptClient 4048 if prompt_response.type == "chat": 4049 prompt = ChatPromptClient(prompt_response) 4050 else: 4051 prompt = TextPromptClient(prompt_response) 4052 4053 if self._resources is not None: 4054 self._resources.prompt_cache.set(cache_key, prompt, ttl_seconds) 4055 4056 return prompt 4057 4058 except NotFoundError as not_found_error: 4059 langfuse_logger.warning( 4060 "Prompt '%s' not found during refresh, evicting from cache.", cache_key 4061 ) 4062 if self._resources is not None: 4063 self._resources.prompt_cache.delete(cache_key) 4064 raise not_found_error 4065 4066 except Exception as e: 4067 langfuse_logger.error( 4068 "Error while fetching prompt '%s': %s", cache_key, str(e) 4069 ) 4070 raise e 4071 4072 def _get_bounded_max_retries( 4073 self, 4074 max_retries: Optional[int], 4075 *, 4076 default_max_retries: int = 2, 4077 max_retries_upper_bound: int = 4, 4078 ) -> int: 4079 if max_retries is None: 4080 return default_max_retries 4081 4082 bounded_max_retries = min( 4083 max(max_retries, 0), 4084 max_retries_upper_bound, 4085 ) 4086 4087 return bounded_max_retries 4088 4089 @overload 4090 def create_prompt( 4091 self, 4092 *, 4093 name: str, 4094 prompt: List[Union[ChatMessageDict, ChatMessageWithPlaceholdersDict]], 4095 labels: List[str] = [], 4096 tags: Optional[List[str]] = None, 4097 type: Optional[Literal["chat"]], 4098 config: Optional[Any] = None, 4099 commit_message: Optional[str] = None, 4100 ) -> ChatPromptClient: ... 4101 4102 @overload 4103 def create_prompt( 4104 self, 4105 *, 4106 name: str, 4107 prompt: str, 4108 labels: List[str] = [], 4109 tags: Optional[List[str]] = None, 4110 type: Optional[Literal["text"]] = "text", 4111 config: Optional[Any] = None, 4112 commit_message: Optional[str] = None, 4113 ) -> TextPromptClient: ... 4114 4115 def create_prompt( 4116 self, 4117 *, 4118 name: str, 4119 prompt: Union[ 4120 str, List[Union[ChatMessageDict, ChatMessageWithPlaceholdersDict]] 4121 ], 4122 labels: List[str] = [], 4123 tags: Optional[List[str]] = None, 4124 type: Optional[Literal["chat", "text"]] = "text", 4125 config: Optional[Any] = None, 4126 commit_message: Optional[str] = None, 4127 ) -> PromptClient: 4128 """Create a new prompt in Langfuse. 4129 4130 Keyword Args: 4131 name : The name of the prompt to be created. 4132 prompt : The content of the prompt to be created. 4133 is_active [DEPRECATED] : A flag indicating whether the prompt is active or not. This is deprecated and will be removed in a future release. Please use the 'production' label instead. 4134 labels: The labels of the prompt. Defaults to None. To create a default-served prompt, add the 'production' label. 4135 tags: The tags of the prompt. Defaults to None. Will be applied to all versions of the prompt. 4136 config: Additional structured data to be saved with the prompt. Defaults to None. 4137 type: The type of the prompt to be created. "chat" vs. "text". Defaults to "text". 4138 commit_message: Optional string describing the change. 4139 4140 Returns: 4141 TextPromptClient: The prompt if type argument is 'text'. 4142 ChatPromptClient: The prompt if type argument is 'chat'. 4143 """ 4144 try: 4145 langfuse_logger.debug("Creating prompt name=%r, labels=%r", name, labels) 4146 4147 if type == "chat": 4148 if not isinstance(prompt, list): 4149 raise ValueError( 4150 "For 'chat' type, 'prompt' must be a list of chat messages with role and content attributes." 4151 ) 4152 request: Union[CreateChatPromptRequest, CreateTextPromptRequest] = ( 4153 CreateChatPromptRequest( 4154 name=name, 4155 prompt=cast(Any, prompt), 4156 labels=labels, 4157 tags=tags, 4158 config=config or {}, 4159 commit_message=commit_message, 4160 type=CreateChatPromptType.CHAT, 4161 ) 4162 ) 4163 server_prompt = self.api.prompts.create(request=request) 4164 4165 if self._resources is not None: 4166 self._resources.prompt_cache.invalidate(name) 4167 4168 return ChatPromptClient(prompt=cast(Prompt_Chat, server_prompt)) 4169 4170 if not isinstance(prompt, str): 4171 raise ValueError("For 'text' type, 'prompt' must be a string.") 4172 4173 request = CreateTextPromptRequest( 4174 name=name, 4175 prompt=prompt, 4176 labels=labels, 4177 tags=tags, 4178 config=config or {}, 4179 commit_message=commit_message, 4180 ) 4181 4182 server_prompt = self.api.prompts.create(request=request) 4183 4184 if self._resources is not None: 4185 self._resources.prompt_cache.invalidate(name) 4186 4187 return TextPromptClient(prompt=cast(Prompt_Text, server_prompt)) 4188 4189 except Error as e: 4190 handle_fern_exception(e) 4191 raise e 4192 4193 def update_prompt( 4194 self, 4195 *, 4196 name: str, 4197 version: int, 4198 new_labels: List[str] = [], 4199 ) -> Any: 4200 """Update an existing prompt version in Langfuse. The Langfuse SDK prompt cache is invalidated for all prompts witht he specified name. 4201 4202 Args: 4203 name (str): The name of the prompt to update. 4204 version (int): The version number of the prompt to update. 4205 new_labels (List[str], optional): New labels to assign to the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. Defaults to []. 4206 4207 Returns: 4208 Prompt: The updated prompt from the Langfuse API. 4209 4210 """ 4211 updated_prompt = self.api.prompt_version.update( 4212 name=self._url_encode(name), 4213 version=version, 4214 new_labels=new_labels, 4215 ) 4216 4217 if self._resources is not None: 4218 self._resources.prompt_cache.invalidate(name) 4219 4220 return updated_prompt 4221 4222 def _url_encode(self, url: str, *, is_url_param: Optional[bool] = False) -> str: 4223 # httpx ≥ 0.28 does its own WHATWG-compliant quoting (eg. encodes bare 4224 # “%”, “?”, “#”, “|”, … in query/path parts). Re-quoting here would 4225 # double-encode, so we skip when the value is about to be sent straight 4226 # to httpx (`is_url_param=True`) and the installed version is ≥ 0.28. 4227 if is_url_param and Version(httpx.__version__) >= Version("0.28.0"): 4228 return url 4229 4230 # urllib.parse.quote does not escape slashes "/" by default; we need to add safe="" to force escaping 4231 # we need add safe="" to force escaping of slashes 4232 # This is necessary for prompts in prompt folders 4233 return urllib.parse.quote(url, safe="") 4234 4235 def clear_prompt_cache(self) -> None: 4236 """Clear the entire prompt cache, removing all cached prompts. 4237 4238 This method is useful when you want to force a complete refresh of all 4239 cached prompts, for example after major updates or when you need to 4240 ensure the latest versions are fetched from the server. 4241 """ 4242 if self._resources is not None: 4243 self._resources.prompt_cache.clear()
Main client for Langfuse tracing and platform features.
This class provides an interface for creating and managing traces, spans, and generations in Langfuse as well as interacting with the Langfuse API.
The client features a thread-safe singleton pattern for each unique public API key, ensuring consistent trace context propagation across your application. It implements efficient batching of spans with configurable flush settings and includes background thread management for media uploads and score ingestion.
Configuration is flexible through either direct parameters or environment variables, with graceful fallbacks and runtime configuration updates.
Attributes:
- api: Synchronous API client for Langfuse backend communication
- async_api: Asynchronous API client for Langfuse backend communication
- _otel_tracer: Internal LangfuseTracer instance managing OpenTelemetry components
Arguments:
- public_key (Optional[str]): Your Langfuse public API key. Can also be set via LANGFUSE_PUBLIC_KEY environment variable.
- secret_key (Optional[str]): Your Langfuse secret API key. Can also be set via LANGFUSE_SECRET_KEY environment variable.
- base_url (Optional[str]): The Langfuse API base URL. Defaults to "https://cloud.langfuse.com". Can also be set via LANGFUSE_BASE_URL environment variable.
- host (Optional[str]): Deprecated. Use base_url instead. The Langfuse API host URL. Defaults to "https://cloud.langfuse.com".
- timeout (Optional[int]): Timeout in seconds for API requests. Defaults to 5 seconds.
- httpx_client (Optional[httpx.Client]): Custom httpx client for making non-tracing HTTP requests. If not provided, a default client will be created.
Fork safety:
httpx.Clientis thread-safe but not process-safe. When usingfork()-based servers (e.g. Gunicorn with--preload), the SDK automatically recreates its internally-managed HTTP client in child processes after fork. A customhttpx_clientis intentionally left as-is (the fork-inherited copy is reused), so you retain the opportunity to handle process-safety yourself — for example by registering your ownos.register_at_fork(after_in_child=...)handler to close and reopen connections on the custom client. - debug (bool): Enable debug logging. Defaults to False. Can also be set via LANGFUSE_DEBUG environment variable.
- tracing_enabled (Optional[bool]): Enable or disable tracing. Defaults to True. Can also be set via LANGFUSE_TRACING_ENABLED environment variable.
- flush_at (Optional[int]): Number of spans to batch before sending to the API. Defaults to 512. Can also be set via LANGFUSE_FLUSH_AT environment variable.
- flush_interval (Optional[float]): Time in seconds between batch flushes. Defaults to 5 seconds. Can also be set via LANGFUSE_FLUSH_INTERVAL environment variable.
- environment (Optional[str]): Environment name for tracing. Default is 'default'. Can also be set via LANGFUSE_TRACING_ENVIRONMENT environment variable. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
- release (Optional[str]): Release version/hash of your application. Used for grouping analytics by release.
- media_upload_thread_count (Optional[int]): Number of background threads for handling media uploads. Defaults to 1. Can also be set via LANGFUSE_MEDIA_UPLOAD_THREAD_COUNT environment variable.
- sample_rate (Optional[float]): Sampling rate for traces (0.0 to 1.0). Defaults to 1.0 (100% of traces are sampled). Can also be set via LANGFUSE_SAMPLE_RATE environment variable.
- mask (Optional[MaskFunction]): Function to mask sensitive data synchronously when Langfuse SDK attributes are created. This applies only to data set through Langfuse SDK APIs such as
start_observation(),update(), andset_trace_io(). mask_otel_spans (Optional[MaskOtelSpansFunction]): Synchronous export-stage hook for masking raw OpenTelemetry span attributes before this Langfuse client sends them to Langfuse. Use this for spans created by third-party OpenTelemetry instrumentations, or when you need to inspect final span attributes after export filtering and Langfuse media handling. It does not modify spans already exported through other OpenTelemetry exporters.
The hook receives one OpenTelemetry export batch. A batch is not guaranteed to contain a complete trace, request, or Langfuse observation tree. The hook usually runs on the OpenTelemetry batch span processor worker thread; during
flush()and shutdown it may run on the caller thread. Keep it synchronous, deterministic, and fast.Return
Noneto leave the batch unchanged. ReturnMaskOtelSpansResultwithOtelSpanPatchvalues to delete or replace attributes on selected spans. If a batch contains duplicate trace and span identifiers, Langfuse keeps only the last matching span. If the hook raises or returns an invalid batch result, Langfuse drops the whole export batch. If one returned span patch is invalid, Langfuse drops only that span from the Langfuse export.Example:
from typing import Optional from langfuse import Langfuse from langfuse.types import ( MaskOtelSpansParams, MaskOtelSpansResult, OtelSpanPatch, ) def mask_otel_spans( *, params: MaskOtelSpansParams ) -> Optional[MaskOtelSpansResult]: patches = {} for identifier, span in params.spans.items(): if "gen_ai.prompt.0.content" in span.attributes: patches[identifier] = OtelSpanPatch( delete_attributes=("gen_ai.prompt.0.content",), set_attributes={"masking.applied": True}, ) return MaskOtelSpansResult(span_patches=patches) langfuse = Langfuse(mask_otel_spans=mask_otel_spans)blocked_instrumentation_scopes (Optional[List[str]]): Deprecated. Use
should_export_spaninstead. Equivalent behavior:from langfuse.span_filter import is_default_export_span blocked = {"sqlite", "requests"} should_export_span = lambda span: ( is_default_export_span(span) and ( span.instrumentation_scope is None or span.instrumentation_scope.name not in blocked ) )should_export_span (Optional[Callable[[ReadableSpan], bool]]): Callback to decide whether to export a span. If omitted, Langfuse uses the default filter (Langfuse SDK spans, spans with
gen_ai.*attributes, and known LLM instrumentation scopes).- additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and in the default OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. If
span_exporteris provided, these headers are not wired into that exporter and must be configured on the exporter instance directly. - tracer_provider(Optional[TracerProvider]): OpenTelemetry TracerProvider to use for Langfuse. This can be useful to set to have disconnected tracing between Langfuse and other OpenTelemetry-span emitting libraries. Note: To track active spans, the context is still shared between TracerProviders. This may lead to broken trace trees.
- id_generator (Optional[IdGenerator]): OpenTelemetry ID generator to use when Langfuse creates its own TracerProvider. If omitted, the OpenTelemetry SDK default is used. If
tracer_provideris provided, or an OpenTelemetry TracerProvider is already registered globally, configure the ID generator on that provider instead. - span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire
base_url, exporter headers, exporter auth, or exporter timeout into it. Configure endpoint, headers, and timeout on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, includex-langfuse-ingestion-version=4on the exporter to enable real time processing of exported spans.
Example:
from langfuse import Langfuse # Initialize the client (reads from env vars if not provided) langfuse = Langfuse( public_key="your-public-key", secret_key="your-secret-key", base_url="https://cloud.langfuse.com", # Optional, default shown ) # Create a trace span with langfuse.start_as_current_observation(name="process-query") as span: # Your application code here # Create a nested generation span for an LLM call with span.start_as_current_generation( name="generate-response", model="gpt-4", input={"query": "Tell me about AI"}, model_parameters={"temperature": 0.7, "max_tokens": 500} ) as generation: # Generate response here response = "AI is a field of computer science..." generation.update( output=response, usage_details={"prompt_tokens": 10, "completion_tokens": 50}, cost_details={"total_cost": 0.0023} ) # Score the generation (supports NUMERIC, BOOLEAN, CATEGORICAL) generation.score(name="relevance", value=0.95, data_type="NUMERIC")
314 def __init__( 315 self, 316 *, 317 public_key: Optional[str] = None, 318 secret_key: Optional[str] = None, 319 base_url: Optional[str] = None, 320 host: Optional[str] = None, 321 timeout: Optional[int] = None, 322 httpx_client: Optional[httpx.Client] = None, 323 debug: bool = False, 324 tracing_enabled: Optional[bool] = True, 325 flush_at: Optional[int] = None, 326 flush_interval: Optional[float] = None, 327 environment: Optional[str] = None, 328 release: Optional[str] = None, 329 media_upload_thread_count: Optional[int] = None, 330 sample_rate: Optional[float] = None, 331 mask: Optional[MaskFunction] = None, 332 mask_otel_spans: Optional[MaskOtelSpansFunction] = None, 333 blocked_instrumentation_scopes: Optional[List[str]] = None, 334 should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, 335 additional_headers: Optional[Dict[str, str]] = None, 336 tracer_provider: Optional[TracerProvider] = None, 337 id_generator: Optional[IdGenerator] = None, 338 span_exporter: Optional[SpanExporter] = None, 339 ): 340 self._base_url = ( 341 base_url 342 or os.environ.get(LANGFUSE_BASE_URL) 343 or host 344 or os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") 345 ) 346 self._environment = environment or cast( 347 str, os.environ.get(LANGFUSE_TRACING_ENVIRONMENT) 348 ) 349 self._release = ( 350 release 351 or os.environ.get(LANGFUSE_RELEASE, None) 352 or get_common_release_envs() 353 ) 354 self._project_id: Optional[str] = None 355 if sample_rate is None: 356 sample_rate = float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0)) 357 if not 0.0 <= sample_rate <= 1.0: 358 raise ValueError( 359 f"Sample rate must be between 0.0 and 1.0, got {sample_rate}" 360 ) 361 362 timeout = timeout or int(os.environ.get(LANGFUSE_TIMEOUT, 5)) 363 364 self._tracing_enabled = ( 365 tracing_enabled 366 and os.environ.get(LANGFUSE_TRACING_ENABLED, "true").lower() != "false" 367 ) 368 if not self._tracing_enabled: 369 langfuse_logger.info( 370 "Configuration: Langfuse tracing is explicitly disabled. No data will be sent to the Langfuse API." 371 ) 372 373 debug = ( 374 debug if debug else (os.getenv(LANGFUSE_DEBUG, "false").lower() == "true") 375 ) 376 if debug: 377 logging.basicConfig( 378 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 379 ) 380 langfuse_logger.setLevel(logging.DEBUG) 381 382 public_key = public_key or os.environ.get(LANGFUSE_PUBLIC_KEY) 383 if public_key is None: 384 langfuse_logger.warning( 385 "Authentication error: Langfuse client initialized without public_key. Client will be disabled. " 386 "Provide a public_key parameter or set LANGFUSE_PUBLIC_KEY environment variable. " 387 ) 388 self._otel_tracer = otel_trace_api.NoOpTracer() 389 return 390 391 secret_key = secret_key or os.environ.get(LANGFUSE_SECRET_KEY) 392 if secret_key is None: 393 langfuse_logger.warning( 394 "Authentication error: Langfuse client initialized without secret_key. Client will be disabled. " 395 "Provide a secret_key parameter or set LANGFUSE_SECRET_KEY environment variable. " 396 ) 397 self._otel_tracer = otel_trace_api.NoOpTracer() 398 return 399 400 if os.environ.get("OTEL_SDK_DISABLED", "false").lower() == "true": 401 langfuse_logger.warning( 402 "OTEL_SDK_DISABLED is set. Langfuse tracing will be disabled and no traces will appear in the UI." 403 ) 404 405 if blocked_instrumentation_scopes is not None: 406 warnings.warn( 407 "`blocked_instrumentation_scopes` is deprecated and will be removed in a future release. " 408 "Use `should_export_span` instead. Example: " 409 "from langfuse.span_filter import is_default_export_span; " 410 'blocked={"scope"}; should_export_span=lambda span: ' 411 "is_default_export_span(span) and (span.instrumentation_scope is None or " 412 "span.instrumentation_scope.name not in blocked).", 413 DeprecationWarning, 414 stacklevel=2, 415 ) 416 417 # Initialize api and tracer if requirements are met 418 self._resources = LangfuseResourceManager( 419 public_key=public_key, 420 secret_key=secret_key, 421 base_url=self._base_url, 422 timeout=timeout, 423 environment=self._environment, 424 release=release, 425 flush_at=flush_at, 426 flush_interval=flush_interval, 427 httpx_client=httpx_client, 428 media_upload_thread_count=media_upload_thread_count, 429 sample_rate=sample_rate, 430 mask=mask, 431 mask_otel_spans=mask_otel_spans, 432 tracing_enabled=self._tracing_enabled, 433 blocked_instrumentation_scopes=blocked_instrumentation_scopes, 434 should_export_span=should_export_span, 435 additional_headers=additional_headers, 436 tracer_provider=tracer_provider, 437 id_generator=id_generator, 438 span_exporter=span_exporter, 439 ) 440 self._mask = self._resources.mask 441 442 self._otel_tracer = ( 443 self._resources.tracer 444 if self._tracing_enabled and self._resources.tracer is not None 445 else otel_trace_api.NoOpTracer() 446 )
448 @property 449 def api(self) -> LangfuseAPI: 450 """Synchronous client for the full Langfuse REST API (traces, observations, scores, datasets, prompts, ...). 451 452 Use this to read or manage data on the Langfuse server; use the tracing methods 453 (`start_observation`, `@observe`) to create traces. Use `async_api` for the 454 asyncio variant. 455 456 Semantics that are easy to miss: 457 458 - **Ingestion is asynchronous.** `langfuse.flush()` only guarantees delivery to 459 the API, not read visibility: reads such as `api.trace.get(trace_id)` may 460 raise `langfuse.api.NotFoundError` until processing completes (typically 461 within 15-30 seconds; longer under load). The same applies to scores and 462 dataset run reads. Instead of a fixed sleep, retry with a deadline: 463 464 - **List endpoints return lightweight views.** `api.trace.list(...)` returns 465 `TraceWithDetails`, where `observations` and `scores` are lists of ID strings. 466 Fetch the full objects with `api.trace.get(trace_id)` (`TraceWithFullDetails`), 467 or prefer `api.observations.get_many(trace_id=...)` for row-level observation 468 queries. The same list-view vs. get-detail pattern applies to other resources. 469 470 - **Prefer the v2 data APIs — they are the defaults since SDK v4.** 471 `api.observations` and `api.metrics` map to the high-performance 472 `/api/public/v2/...` endpoints and are the recommended read path. Their v1 473 equivalents remain available under `api.legacy.observations_v1` / 474 `api.legacy.metrics_v1` but are less performant at scale, not recommended 475 for new workflows, and will be deprecated. 476 477 - For large-scale aggregation (usage/cost by model, user, etc.), prefer the 478 v2 Metrics API (`api.metrics.metrics(...)`) over paginating row-level data. 479 480 481 See also: `async_api`, 482 https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk 483 (ingestion lag: #ingestion-lag, list vs. get: #traces-list-vs-get), 484 https://langfuse.com/docs/api-and-data-platform/features/observations-api, 485 https://langfuse.com/docs/metrics/features/metrics-api 486 """ 487 if self._resources is None: 488 raise AttributeError("Langfuse client is not initialized") 489 490 return self._resources.api
Synchronous client for the full Langfuse REST API (traces, observations, scores, datasets, prompts, ...).
Use this to read or manage data on the Langfuse server; use the tracing methods
(start_observation, @observe) to create traces. Use async_api for the
asyncio variant.
Semantics that are easy to miss:
Ingestion is asynchronous.
langfuse.flush()only guarantees delivery to the API, not read visibility: reads such asapi.trace.get(trace_id)may raiselangfuse.api.NotFoundErroruntil processing completes (typically within 15-30 seconds; longer under load). The same applies to scores and dataset run reads. Instead of a fixed sleep, retry with a deadline:List endpoints return lightweight views.
api.trace.list(...)returnsTraceWithDetails, whereobservationsandscoresare lists of ID strings. Fetch the full objects withapi.trace.get(trace_id)(TraceWithFullDetails), or preferapi.observations.get_many(trace_id=...)for row-level observation queries. The same list-view vs. get-detail pattern applies to other resources.Prefer the v2 data APIs — they are the defaults since SDK v4.
api.observationsandapi.metricsmap to the high-performance/api/public/v2/...endpoints and are the recommended read path. Their v1 equivalents remain available underapi.legacy.observations_v1/api.legacy.metrics_v1but are less performant at scale, not recommended for new workflows, and will be deprecated.For large-scale aggregation (usage/cost by model, user, etc.), prefer the v2 Metrics API (
api.metrics.metrics(...)) over paginating row-level data.
See also: async_api,
https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk
(ingestion lag: #ingestion-lag, list vs. get: #traces-list-vs-get),
https://langfuse.com/docs/api-and-data-platform/features/observations-api,
https://langfuse.com/docs/metrics/features/metrics-api
660 def start_observation( 661 self, 662 *, 663 trace_context: Optional[TraceContext] = None, 664 name: str, 665 as_type: ObservationTypeLiteralNoEvent = "span", 666 input: Optional[Any] = None, 667 output: Optional[Any] = None, 668 metadata: Optional[Any] = None, 669 version: Optional[str] = None, 670 level: Optional[SpanLevel] = None, 671 status_message: Optional[str] = None, 672 completion_start_time: Optional[datetime] = None, 673 model: Optional[str] = None, 674 model_parameters: Optional[Dict[str, MapValue]] = None, 675 usage_details: Optional[Dict[str, int]] = None, 676 cost_details: Optional[Dict[str, float]] = None, 677 prompt: Optional[PromptClient] = None, 678 ) -> Union[ 679 LangfuseSpan, 680 LangfuseGeneration, 681 LangfuseAgent, 682 LangfuseTool, 683 LangfuseChain, 684 LangfuseRetriever, 685 LangfuseEvaluator, 686 LangfuseEmbedding, 687 LangfuseGuardrail, 688 ]: 689 """Create a new observation of the specified type. 690 691 This method creates a new observation but does not set it as the current span in the 692 context. To create and use an observation within a context, use start_as_current_observation(). 693 694 Args: 695 trace_context: Optional context for connecting to an existing trace 696 name: Name of the observation 697 as_type: Type of observation to create (defaults to "span") 698 input: Input data for the operation 699 output: Output data from the operation 700 metadata: Additional metadata to associate with the observation 701 version: Version identifier for the code or component 702 level: Importance level of the observation 703 status_message: Optional status message for the observation 704 completion_start_time: When the model started generating (for generation types) 705 model: Name/identifier of the AI model used (for generation types) 706 model_parameters: Parameters used for the model (for generation types) 707 usage_details: Token usage information (for generation types) 708 cost_details: Cost information (for generation types) 709 prompt: Associated prompt template (for generation types) 710 711 Returns: 712 An observation object of the appropriate type that must be ended with .end() 713 """ 714 if trace_context: 715 trace_id = trace_context.get("trace_id", None) 716 parent_span_id = trace_context.get("parent_span_id", None) 717 718 if trace_id: 719 remote_parent_span = self._create_remote_parent_span( 720 trace_id=trace_id, parent_span_id=parent_span_id 721 ) 722 723 with otel_trace_api.use_span( 724 cast(otel_trace_api.Span, remote_parent_span) 725 ): 726 otel_span = self._otel_tracer.start_span(name=name) 727 otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) 728 729 return self._create_observation_from_otel_span( 730 otel_span=otel_span, 731 as_type=as_type, 732 input=input, 733 output=output, 734 metadata=metadata, 735 version=version, 736 level=level, 737 status_message=status_message, 738 completion_start_time=completion_start_time, 739 model=model, 740 model_parameters=model_parameters, 741 usage_details=usage_details, 742 cost_details=cost_details, 743 prompt=prompt, 744 ) 745 746 otel_span = self._otel_tracer.start_span(name=name) 747 748 return self._create_observation_from_otel_span( 749 otel_span=otel_span, 750 as_type=as_type, 751 input=input, 752 output=output, 753 metadata=metadata, 754 version=version, 755 level=level, 756 status_message=status_message, 757 completion_start_time=completion_start_time, 758 model=model, 759 model_parameters=model_parameters, 760 usage_details=usage_details, 761 cost_details=cost_details, 762 prompt=prompt, 763 )
Create a new observation of the specified type.
This method creates a new observation but does not set it as the current span in the context. To create and use an observation within a context, use start_as_current_observation().
Arguments:
- trace_context: Optional context for connecting to an existing trace
- name: Name of the observation
- as_type: Type of observation to create (defaults to "span")
- input: Input data for the operation
- output: Output data from the operation
- metadata: Additional metadata to associate with the observation
- version: Version identifier for the code or component
- level: Importance level of the observation
- status_message: Optional status message for the observation
- completion_start_time: When the model started generating (for generation types)
- model: Name/identifier of the AI model used (for generation types)
- model_parameters: Parameters used for the model (for generation types)
- usage_details: Token usage information (for generation types)
- cost_details: Cost information (for generation types)
- prompt: Associated prompt template (for generation types)
Returns:
An observation object of the appropriate type that must be ended with .end()
993 def start_as_current_observation( 994 self, 995 *, 996 trace_context: Optional[TraceContext] = None, 997 name: str, 998 as_type: ObservationTypeLiteralNoEvent = "span", 999 input: Optional[Any] = None, 1000 output: Optional[Any] = None, 1001 metadata: Optional[Any] = None, 1002 version: Optional[str] = None, 1003 level: Optional[SpanLevel] = None, 1004 status_message: Optional[str] = None, 1005 completion_start_time: Optional[datetime] = None, 1006 model: Optional[str] = None, 1007 model_parameters: Optional[Dict[str, MapValue]] = None, 1008 usage_details: Optional[Dict[str, int]] = None, 1009 cost_details: Optional[Dict[str, float]] = None, 1010 prompt: Optional[PromptClient] = None, 1011 end_on_exit: Optional[bool] = None, 1012 ) -> Union[ 1013 _AgnosticContextManager[LangfuseGeneration], 1014 _AgnosticContextManager[LangfuseSpan], 1015 _AgnosticContextManager[LangfuseAgent], 1016 _AgnosticContextManager[LangfuseTool], 1017 _AgnosticContextManager[LangfuseChain], 1018 _AgnosticContextManager[LangfuseRetriever], 1019 _AgnosticContextManager[LangfuseEvaluator], 1020 _AgnosticContextManager[LangfuseEmbedding], 1021 _AgnosticContextManager[LangfuseGuardrail], 1022 ]: 1023 """Create a new observation and set it as the current span in a context manager. 1024 1025 This method creates a new observation of the specified type and sets it as the 1026 current span within a context manager. Use this method with a 'with' statement to 1027 automatically handle the observation lifecycle within a code block. 1028 1029 The created observation will be the child of the current span in the context. 1030 1031 Args: 1032 trace_context: Optional context for connecting to an existing trace 1033 name: Name of the observation (e.g., function or operation name) 1034 as_type: Type of observation to create (defaults to "span") 1035 input: Input data for the operation (can be any JSON-serializable object) 1036 output: Output data from the operation (can be any JSON-serializable object) 1037 metadata: Additional metadata to associate with the observation 1038 version: Version identifier for the code or component 1039 level: Importance level of the observation (info, warning, error) 1040 status_message: Optional status message for the observation 1041 end_on_exit (default: True): Whether to end the span automatically when leaving the context manager. If False, the span must be manually ended to avoid memory leaks. 1042 1043 The following parameters are available when as_type is: "generation" or "embedding". 1044 completion_start_time: When the model started generating the response 1045 model: Name/identifier of the AI model used (e.g., "gpt-4") 1046 model_parameters: Parameters used for the model (e.g., temperature, max_tokens) 1047 usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) 1048 cost_details: Cost information for the model call 1049 prompt: Associated prompt template from Langfuse prompt management 1050 1051 Returns: 1052 A context manager that yields the appropriate observation type based on as_type 1053 1054 Example: 1055 ```python 1056 # Create a span 1057 with langfuse.start_as_current_observation(name="process-query", as_type="span") as span: 1058 # Do work 1059 result = process_data() 1060 span.update(output=result) 1061 1062 # Create a child span automatically 1063 with span.start_as_current_observation(name="sub-operation") as child_span: 1064 # Do sub-operation work 1065 child_span.update(output="sub-result") 1066 1067 # Create a tool observation 1068 with langfuse.start_as_current_observation(name="web-search", as_type="tool") as tool: 1069 # Do tool work 1070 results = search_web(query) 1071 tool.update(output=results) 1072 1073 # Create a generation observation 1074 with langfuse.start_as_current_observation( 1075 name="answer-generation", 1076 as_type="generation", 1077 model="gpt-4" 1078 ) as generation: 1079 # Generate answer 1080 response = llm.generate(...) 1081 generation.update(output=response) 1082 ``` 1083 """ 1084 if as_type in get_observation_types_list(ObservationTypeGenerationLike): 1085 if trace_context: 1086 trace_id = trace_context.get("trace_id", None) 1087 parent_span_id = trace_context.get("parent_span_id", None) 1088 1089 if trace_id: 1090 remote_parent_span = self._create_remote_parent_span( 1091 trace_id=trace_id, parent_span_id=parent_span_id 1092 ) 1093 1094 return cast( 1095 Union[ 1096 _AgnosticContextManager[LangfuseGeneration], 1097 _AgnosticContextManager[LangfuseEmbedding], 1098 ], 1099 self._create_span_with_parent_context( 1100 as_type=as_type, 1101 name=name, 1102 remote_parent_span=remote_parent_span, 1103 parent=None, 1104 end_on_exit=end_on_exit, 1105 input=input, 1106 output=output, 1107 metadata=metadata, 1108 version=version, 1109 level=level, 1110 status_message=status_message, 1111 completion_start_time=completion_start_time, 1112 model=model, 1113 model_parameters=model_parameters, 1114 usage_details=usage_details, 1115 cost_details=cost_details, 1116 prompt=prompt, 1117 ), 1118 ) 1119 1120 return cast( 1121 Union[ 1122 _AgnosticContextManager[LangfuseGeneration], 1123 _AgnosticContextManager[LangfuseEmbedding], 1124 ], 1125 self._start_as_current_otel_span_with_processed_media( 1126 as_type=as_type, 1127 name=name, 1128 end_on_exit=end_on_exit, 1129 input=input, 1130 output=output, 1131 metadata=metadata, 1132 version=version, 1133 level=level, 1134 status_message=status_message, 1135 completion_start_time=completion_start_time, 1136 model=model, 1137 model_parameters=model_parameters, 1138 usage_details=usage_details, 1139 cost_details=cost_details, 1140 prompt=prompt, 1141 ), 1142 ) 1143 1144 if as_type in get_observation_types_list(ObservationTypeSpanLike): 1145 if trace_context: 1146 trace_id = trace_context.get("trace_id", None) 1147 parent_span_id = trace_context.get("parent_span_id", None) 1148 1149 if trace_id: 1150 remote_parent_span = self._create_remote_parent_span( 1151 trace_id=trace_id, parent_span_id=parent_span_id 1152 ) 1153 1154 return cast( 1155 Union[ 1156 _AgnosticContextManager[LangfuseSpan], 1157 _AgnosticContextManager[LangfuseAgent], 1158 _AgnosticContextManager[LangfuseTool], 1159 _AgnosticContextManager[LangfuseChain], 1160 _AgnosticContextManager[LangfuseRetriever], 1161 _AgnosticContextManager[LangfuseEvaluator], 1162 _AgnosticContextManager[LangfuseGuardrail], 1163 ], 1164 self._create_span_with_parent_context( 1165 as_type=as_type, 1166 name=name, 1167 remote_parent_span=remote_parent_span, 1168 parent=None, 1169 end_on_exit=end_on_exit, 1170 input=input, 1171 output=output, 1172 metadata=metadata, 1173 version=version, 1174 level=level, 1175 status_message=status_message, 1176 ), 1177 ) 1178 1179 return cast( 1180 Union[ 1181 _AgnosticContextManager[LangfuseSpan], 1182 _AgnosticContextManager[LangfuseAgent], 1183 _AgnosticContextManager[LangfuseTool], 1184 _AgnosticContextManager[LangfuseChain], 1185 _AgnosticContextManager[LangfuseRetriever], 1186 _AgnosticContextManager[LangfuseEvaluator], 1187 _AgnosticContextManager[LangfuseGuardrail], 1188 ], 1189 self._start_as_current_otel_span_with_processed_media( 1190 as_type=as_type, 1191 name=name, 1192 end_on_exit=end_on_exit, 1193 input=input, 1194 output=output, 1195 metadata=metadata, 1196 version=version, 1197 level=level, 1198 status_message=status_message, 1199 ), 1200 ) 1201 1202 # This should never be reached since all valid types are handled above 1203 langfuse_logger.warning( 1204 "Unknown observation type: %s, falling back to span", as_type 1205 ) 1206 return self._start_as_current_otel_span_with_processed_media( 1207 as_type="span", 1208 name=name, 1209 end_on_exit=end_on_exit, 1210 input=input, 1211 output=output, 1212 metadata=metadata, 1213 version=version, 1214 level=level, 1215 status_message=status_message, 1216 )
Create a new observation and set it as the current span in a context manager.
This method creates a new observation of the specified type and sets it as the current span within a context manager. Use this method with a 'with' statement to automatically handle the observation lifecycle within a code block.
The created observation will be the child of the current span in the context.
Arguments:
- trace_context: Optional context for connecting to an existing trace
- name: Name of the observation (e.g., function or operation name)
- as_type: Type of observation to create (defaults to "span")
- input: Input data for the operation (can be any JSON-serializable object)
- output: Output data from the operation (can be any JSON-serializable object)
- metadata: Additional metadata to associate with the observation
- version: Version identifier for the code or component
- level: Importance level of the observation (info, warning, error)
- status_message: Optional status message for the observation
- end_on_exit (default: True): Whether to end the span automatically when leaving the context manager. If False, the span must be manually ended to avoid memory leaks.
- The following parameters are available when as_type is: "generation" or "embedding".
- completion_start_time: When the model started generating the response
- model: Name/identifier of the AI model used (e.g., "gpt-4")
- model_parameters: Parameters used for the model (e.g., temperature, max_tokens)
- usage_details: Token usage information (e.g., prompt_tokens, completion_tokens)
- cost_details: Cost information for the model call
- prompt: Associated prompt template from Langfuse prompt management
Returns:
A context manager that yields the appropriate observation type based on as_type
Example:
# Create a span with langfuse.start_as_current_observation(name="process-query", as_type="span") as span: # Do work result = process_data() span.update(output=result) # Create a child span automatically with span.start_as_current_observation(name="sub-operation") as child_span: # Do sub-operation work child_span.update(output="sub-result") # Create a tool observation with langfuse.start_as_current_observation(name="web-search", as_type="tool") as tool: # Do tool work results = search_web(query) tool.update(output=results) # Create a generation observation with langfuse.start_as_current_observation( name="answer-generation", as_type="generation", model="gpt-4" ) as generation: # Generate answer response = llm.generate(...) generation.update(output=response)
1408 def update_current_generation( 1409 self, 1410 *, 1411 name: Optional[str] = None, 1412 input: Optional[Any] = None, 1413 output: Optional[Any] = None, 1414 metadata: Optional[Any] = None, 1415 version: Optional[str] = None, 1416 level: Optional[SpanLevel] = None, 1417 status_message: Optional[str] = None, 1418 completion_start_time: Optional[datetime] = None, 1419 model: Optional[str] = None, 1420 model_parameters: Optional[Dict[str, MapValue]] = None, 1421 usage_details: Optional[Dict[str, int]] = None, 1422 cost_details: Optional[Dict[str, float]] = None, 1423 prompt: Optional[PromptClient] = None, 1424 ) -> None: 1425 """Update the current active generation span with new information. 1426 1427 This method updates the current generation span in the active context with 1428 additional information. It's useful for adding output, usage stats, or other 1429 details that become available during or after model generation. 1430 1431 Args: 1432 name: The generation name 1433 input: Updated input data for the model 1434 output: Output from the model (e.g., completions) 1435 metadata: Additional metadata to associate with the generation 1436 version: Version identifier for the model or component 1437 level: Importance level of the generation (info, warning, error) 1438 status_message: Optional status message for the generation 1439 completion_start_time: When the model started generating the response 1440 model: Name/identifier of the AI model used (e.g., "gpt-4") 1441 model_parameters: Parameters used for the model (e.g., temperature, max_tokens) 1442 usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) 1443 cost_details: Cost information for the model call 1444 prompt: Associated prompt template from Langfuse prompt management 1445 1446 Example: 1447 ```python 1448 with langfuse.start_as_current_generation(name="answer-query") as generation: 1449 # Initial setup and API call 1450 response = llm.generate(...) 1451 1452 # Update with results that weren't available at creation time 1453 langfuse.update_current_generation( 1454 output=response.text, 1455 usage_details={ 1456 "prompt_tokens": response.usage.prompt_tokens, 1457 "completion_tokens": response.usage.completion_tokens 1458 } 1459 ) 1460 ``` 1461 """ 1462 if not self._tracing_enabled: 1463 langfuse_logger.debug( 1464 "Operation skipped: update_current_generation - Tracing is disabled or client is in no-op mode." 1465 ) 1466 return 1467 1468 current_otel_span = self._get_current_otel_span() 1469 1470 if current_otel_span is not None: 1471 generation = LangfuseGeneration( 1472 otel_span=current_otel_span, langfuse_client=self 1473 ) 1474 1475 if name: 1476 current_otel_span.update_name(name) 1477 1478 generation.update( 1479 input=input, 1480 output=output, 1481 metadata=metadata, 1482 version=version, 1483 level=level, 1484 status_message=status_message, 1485 completion_start_time=completion_start_time, 1486 model=model, 1487 model_parameters=model_parameters, 1488 usage_details=usage_details, 1489 cost_details=cost_details, 1490 prompt=prompt, 1491 )
Update the current active generation span with new information.
This method updates the current generation span in the active context with additional information. It's useful for adding output, usage stats, or other details that become available during or after model generation.
Arguments:
- name: The generation name
- input: Updated input data for the model
- output: Output from the model (e.g., completions)
- metadata: Additional metadata to associate with the generation
- version: Version identifier for the model or component
- level: Importance level of the generation (info, warning, error)
- status_message: Optional status message for the generation
- completion_start_time: When the model started generating the response
- model: Name/identifier of the AI model used (e.g., "gpt-4")
- model_parameters: Parameters used for the model (e.g., temperature, max_tokens)
- usage_details: Token usage information (e.g., prompt_tokens, completion_tokens)
- cost_details: Cost information for the model call
- prompt: Associated prompt template from Langfuse prompt management
Example:
with langfuse.start_as_current_generation(name="answer-query") as generation: # Initial setup and API call response = llm.generate(...) # Update with results that weren't available at creation time langfuse.update_current_generation( output=response.text, usage_details={ "prompt_tokens": response.usage.prompt_tokens, "completion_tokens": response.usage.completion_tokens } )
1493 def update_current_span( 1494 self, 1495 *, 1496 name: Optional[str] = None, 1497 input: Optional[Any] = None, 1498 output: Optional[Any] = None, 1499 metadata: Optional[Any] = None, 1500 version: Optional[str] = None, 1501 level: Optional[SpanLevel] = None, 1502 status_message: Optional[str] = None, 1503 ) -> None: 1504 """Update the current active span with new information. 1505 1506 This method updates the current span in the active context with 1507 additional information. It's useful for adding outputs or metadata 1508 that become available during execution. 1509 1510 Args: 1511 name: The span name 1512 input: Updated input data for the operation 1513 output: Output data from the operation 1514 metadata: Additional metadata to associate with the span 1515 version: Version identifier for the code or component 1516 level: Importance level of the span (info, warning, error) 1517 status_message: Optional status message for the span 1518 1519 Example: 1520 ```python 1521 with langfuse.start_as_current_observation(name="process-data") as span: 1522 # Initial processing 1523 result = process_first_part() 1524 1525 # Update with intermediate results 1526 langfuse.update_current_span(metadata={"intermediate_result": result}) 1527 1528 # Continue processing 1529 final_result = process_second_part(result) 1530 1531 # Final update 1532 langfuse.update_current_span(output=final_result) 1533 ``` 1534 """ 1535 if not self._tracing_enabled: 1536 langfuse_logger.debug( 1537 "Operation skipped: update_current_span - Tracing is disabled or client is in no-op mode." 1538 ) 1539 return 1540 1541 current_otel_span = self._get_current_otel_span() 1542 1543 if current_otel_span is not None: 1544 span_class = self._get_span_class( 1545 self._get_observation_type_from_otel_span(current_otel_span) 1546 ) 1547 span = span_class( 1548 otel_span=current_otel_span, 1549 langfuse_client=self, 1550 environment=self._environment, 1551 release=self._release, 1552 ) 1553 1554 if name: 1555 current_otel_span.update_name(name) 1556 1557 span.update( 1558 input=input, 1559 output=output, 1560 metadata=metadata, 1561 version=version, 1562 level=level, 1563 status_message=status_message, 1564 )
Update the current active span with new information.
This method updates the current span in the active context with additional information. It's useful for adding outputs or metadata that become available during execution.
Arguments:
- name: The span name
- input: Updated input data for the operation
- output: Output data from the operation
- metadata: Additional metadata to associate with the span
- version: Version identifier for the code or component
- level: Importance level of the span (info, warning, error)
- status_message: Optional status message for the span
Example:
with langfuse.start_as_current_observation(name="process-data") as span: # Initial processing result = process_first_part() # Update with intermediate results langfuse.update_current_span(metadata={"intermediate_result": result}) # Continue processing final_result = process_second_part(result) # Final update langfuse.update_current_span(output=final_result)
1566 @deprecated( 1567 "Trace-level input/output is deprecated. " 1568 "For trace attributes (user_id, session_id, tags, etc.), use propagate_attributes() instead. " 1569 "This method will be removed in a future major version." 1570 ) 1571 def set_current_trace_io( 1572 self, 1573 *, 1574 input: Optional[Any] = None, 1575 output: Optional[Any] = None, 1576 ) -> None: 1577 """Set trace-level input and output for the current span's trace. 1578 1579 .. deprecated:: 1580 This is a legacy method for backward compatibility with Langfuse platform 1581 features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge 1582 evaluators). It will be removed in a future major version. 1583 1584 For setting other trace attributes (user_id, session_id, metadata, tags, version), 1585 use :func:`langfuse.propagate_attributes` (top-level import) instead. 1586 1587 Args: 1588 input: Input data to associate with the trace. 1589 output: Output data to associate with the trace. 1590 """ 1591 if not self._tracing_enabled: 1592 langfuse_logger.debug( 1593 "Operation skipped: set_current_trace_io - Tracing is disabled or client is in no-op mode." 1594 ) 1595 return 1596 1597 current_otel_span = self._get_current_otel_span() 1598 1599 if current_otel_span is not None and current_otel_span.is_recording(): 1600 span_class = self._get_span_class( 1601 self._get_observation_type_from_otel_span(current_otel_span) 1602 ) 1603 span = span_class( 1604 otel_span=current_otel_span, 1605 langfuse_client=self, 1606 environment=self._environment, 1607 release=self._release, 1608 ) 1609 1610 span.set_trace_io( 1611 input=input, 1612 output=output, 1613 )
Set trace-level input and output for the current span's trace.
Deprecated since version : This is a legacy method for backward compatibility with Langfuse platform features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge evaluators). It will be removed in a future major version.
For setting other trace attributes (user_id, session_id, metadata, tags, version),
use langfuse.propagate_attributes() (top-level import) instead.
Arguments:
- input: Input data to associate with the trace.
- output: Output data to associate with the trace.
1615 def set_current_trace_as_public(self) -> None: 1616 """Make the current trace publicly accessible via its URL. 1617 1618 When a trace is published, anyone with the trace link can view the full trace 1619 without needing to be logged in to Langfuse. This action cannot be undone 1620 programmatically - once published, the entire trace becomes public. 1621 1622 This is a convenience method that publishes the trace from the currently 1623 active span context. Use this when you want to make a trace public from 1624 within a traced function without needing direct access to the span object. 1625 """ 1626 if not self._tracing_enabled: 1627 langfuse_logger.debug( 1628 "Operation skipped: set_current_trace_as_public - Tracing is disabled or client is in no-op mode." 1629 ) 1630 return 1631 1632 current_otel_span = self._get_current_otel_span() 1633 1634 if current_otel_span is not None and current_otel_span.is_recording(): 1635 span_class = self._get_span_class( 1636 self._get_observation_type_from_otel_span(current_otel_span) 1637 ) 1638 span = span_class( 1639 otel_span=current_otel_span, 1640 langfuse_client=self, 1641 environment=self._environment, 1642 ) 1643 1644 span.set_trace_as_public()
Make the current trace publicly accessible via its URL.
When a trace is published, anyone with the trace link can view the full trace without needing to be logged in to Langfuse. This action cannot be undone programmatically - once published, the entire trace becomes public.
This is a convenience method that publishes the trace from the currently active span context. Use this when you want to make a trace public from within a traced function without needing direct access to the span object.
1646 def create_event( 1647 self, 1648 *, 1649 trace_context: Optional[TraceContext] = None, 1650 name: str, 1651 input: Optional[Any] = None, 1652 output: Optional[Any] = None, 1653 metadata: Optional[Any] = None, 1654 version: Optional[str] = None, 1655 level: Optional[SpanLevel] = None, 1656 status_message: Optional[str] = None, 1657 ) -> LangfuseEvent: 1658 """Create a new Langfuse observation of type 'EVENT'. 1659 1660 The created Langfuse Event observation will be the child of the current span in the context. 1661 1662 Args: 1663 trace_context: Optional context for connecting to an existing trace 1664 name: Name of the span (e.g., function or operation name) 1665 input: Input data for the operation (can be any JSON-serializable object) 1666 output: Output data from the operation (can be any JSON-serializable object) 1667 metadata: Additional metadata to associate with the span 1668 version: Version identifier for the code or component 1669 level: Importance level of the span (info, warning, error) 1670 status_message: Optional status message for the span 1671 1672 Returns: 1673 The Langfuse Event object 1674 1675 Example: 1676 ```python 1677 event = langfuse.create_event(name="process-event") 1678 ``` 1679 """ 1680 timestamp = time_ns() 1681 1682 if trace_context: 1683 trace_id = trace_context.get("trace_id", None) 1684 parent_span_id = trace_context.get("parent_span_id", None) 1685 1686 if trace_id: 1687 remote_parent_span = self._create_remote_parent_span( 1688 trace_id=trace_id, parent_span_id=parent_span_id 1689 ) 1690 1691 with otel_trace_api.use_span( 1692 cast(otel_trace_api.Span, remote_parent_span) 1693 ): 1694 otel_span = self._otel_tracer.start_span( 1695 name=name, start_time=timestamp 1696 ) 1697 otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) 1698 1699 return cast( 1700 LangfuseEvent, 1701 LangfuseEvent( 1702 otel_span=otel_span, 1703 langfuse_client=self, 1704 environment=self._environment, 1705 release=self._release, 1706 input=input, 1707 output=output, 1708 metadata=metadata, 1709 version=version, 1710 level=level, 1711 status_message=status_message, 1712 ).end(end_time=timestamp), 1713 ) 1714 1715 otel_span = self._otel_tracer.start_span(name=name, start_time=timestamp) 1716 1717 return cast( 1718 LangfuseEvent, 1719 LangfuseEvent( 1720 otel_span=otel_span, 1721 langfuse_client=self, 1722 environment=self._environment, 1723 release=self._release, 1724 input=input, 1725 output=output, 1726 metadata=metadata, 1727 version=version, 1728 level=level, 1729 status_message=status_message, 1730 ).end(end_time=timestamp), 1731 )
Create a new Langfuse observation of type 'EVENT'.
The created Langfuse Event observation will be the child of the current span in the context.
Arguments:
- trace_context: Optional context for connecting to an existing trace
- name: Name of the span (e.g., function or operation name)
- input: Input data for the operation (can be any JSON-serializable object)
- output: Output data from the operation (can be any JSON-serializable object)
- metadata: Additional metadata to associate with the span
- version: Version identifier for the code or component
- level: Importance level of the span (info, warning, error)
- status_message: Optional status message for the span
Returns:
The Langfuse Event object
Example:
event = langfuse.create_event(name="process-event")
1824 @staticmethod 1825 def create_trace_id(*, seed: Optional[str] = None) -> str: 1826 """Create a unique trace ID for use with Langfuse. 1827 1828 This method generates a unique trace ID for use with various Langfuse APIs. 1829 It can either generate a random ID or create a deterministic ID based on 1830 a seed string. 1831 1832 Trace IDs must be 32 lowercase hexadecimal characters, representing 16 bytes. 1833 This method ensures the generated ID meets this requirement. If you need to 1834 correlate an external ID with a Langfuse trace ID, use the external ID as the 1835 seed to get a valid, deterministic Langfuse trace ID. 1836 1837 Args: 1838 seed: Optional string to use as a seed for deterministic ID generation. 1839 If provided, the same seed will always produce the same ID. 1840 If not provided, a random ID will be generated. 1841 1842 Returns: 1843 A 32-character lowercase hexadecimal string representing the Langfuse trace ID. 1844 1845 Example: 1846 ```python 1847 # Generate a random trace ID 1848 trace_id = langfuse.create_trace_id() 1849 1850 # Generate a deterministic ID based on a seed 1851 session_trace_id = langfuse.create_trace_id(seed="session-456") 1852 1853 # Correlate an external ID with a Langfuse trace ID 1854 external_id = "external-system-123456" 1855 correlated_trace_id = langfuse.create_trace_id(seed=external_id) 1856 1857 # Use the ID with trace context 1858 with langfuse.start_as_current_observation( 1859 name="process-request", 1860 trace_context={"trace_id": trace_id} 1861 ) as span: 1862 # Operation will be part of the specific trace 1863 pass 1864 ``` 1865 """ 1866 if not seed: 1867 trace_id_int = RandomIdGenerator().generate_trace_id() 1868 1869 return Langfuse._format_otel_trace_id(trace_id_int) 1870 1871 return sha256(seed.encode("utf-8")).digest()[:16].hex()
Create a unique trace ID for use with Langfuse.
This method generates a unique trace ID for use with various Langfuse APIs. It can either generate a random ID or create a deterministic ID based on a seed string.
Trace IDs must be 32 lowercase hexadecimal characters, representing 16 bytes. This method ensures the generated ID meets this requirement. If you need to correlate an external ID with a Langfuse trace ID, use the external ID as the seed to get a valid, deterministic Langfuse trace ID.
Arguments:
- seed: Optional string to use as a seed for deterministic ID generation. If provided, the same seed will always produce the same ID. If not provided, a random ID will be generated.
Returns:
A 32-character lowercase hexadecimal string representing the Langfuse trace ID.
Example:
# Generate a random trace ID trace_id = langfuse.create_trace_id() # Generate a deterministic ID based on a seed session_trace_id = langfuse.create_trace_id(seed="session-456") # Correlate an external ID with a Langfuse trace ID external_id = "external-system-123456" correlated_trace_id = langfuse.create_trace_id(seed=external_id) # Use the ID with trace context with langfuse.start_as_current_observation( name="process-request", trace_context={"trace_id": trace_id} ) as span: # Operation will be part of the specific trace pass
1953 def create_score( 1954 self, 1955 *, 1956 name: str, 1957 value: Union[float, str], 1958 session_id: Optional[str] = None, 1959 dataset_run_id: Optional[str] = None, 1960 trace_id: Optional[str] = None, 1961 observation_id: Optional[str] = None, 1962 score_id: Optional[str] = None, 1963 data_type: Optional[ScoreDataType] = None, 1964 comment: Optional[str] = None, 1965 config_id: Optional[str] = None, 1966 metadata: Optional[Any] = None, 1967 timestamp: Optional[datetime] = None, 1968 environment: Optional[str] = None, 1969 ) -> None: 1970 """Create a score for a specific trace or observation. 1971 1972 This method creates a score for evaluating a Langfuse trace or observation. Scores can be 1973 used to track quality metrics, user feedback, or automated evaluations. 1974 1975 Args: 1976 name: Name of the score (e.g., "relevance", "accuracy") 1977 value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION) 1978 session_id: ID of the Langfuse session to associate the score with 1979 dataset_run_id: ID of the Langfuse dataset run to associate the score with 1980 trace_id: ID of the Langfuse trace to associate the score with 1981 observation_id: Optional ID of the specific observation to score. Trace ID must be provided too. 1982 score_id: Optional custom ID for the score (auto-generated if not provided) 1983 data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION) 1984 comment: Optional comment or explanation for the score 1985 config_id: Optional ID of a score config defined in Langfuse 1986 metadata: Optional metadata to be attached to the score 1987 timestamp: Optional timestamp for the score (defaults to current UTC time) 1988 environment: Optional environment override for this score. If omitted, 1989 the score uses the client-level environment from 1990 `Langfuse(environment=...)` or `LANGFUSE_TRACING_ENVIRONMENT`. 1991 Langfuse observation wrapper methods pass their resolved span 1992 environment here so scores created via `span.score()` or 1993 `span.score_trace()` stay grouped with the scored observation or 1994 trace, including request-scoped environments propagated with 1995 `propagate_attributes(environment=...)`. 1996 1997 Example: 1998 ```python 1999 # Create a numeric score for accuracy 2000 langfuse.create_score( 2001 name="accuracy", 2002 value=0.92, 2003 trace_id="abcdef1234567890abcdef1234567890", 2004 data_type="NUMERIC", 2005 comment="High accuracy with minor irrelevant details" 2006 ) 2007 2008 # Create a categorical score for sentiment 2009 langfuse.create_score( 2010 name="sentiment", 2011 value="positive", 2012 trace_id="abcdef1234567890abcdef1234567890", 2013 observation_id="abcdef1234567890", 2014 data_type="CATEGORICAL" 2015 ) 2016 ``` 2017 """ 2018 if not self._tracing_enabled: 2019 return 2020 2021 score_id = score_id or self._create_observation_id() 2022 2023 try: 2024 new_body = ScoreBody( 2025 id=score_id, 2026 sessionId=session_id, 2027 datasetRunId=dataset_run_id, 2028 traceId=trace_id, 2029 observationId=observation_id, 2030 name=name, 2031 value=value, 2032 dataType=data_type, # type: ignore 2033 comment=comment, 2034 configId=config_id, 2035 environment=environment or self._environment, 2036 metadata=metadata, 2037 ) 2038 2039 event = { 2040 "id": self.create_trace_id(), 2041 "type": "score-create", 2042 "timestamp": timestamp or _get_timestamp(), 2043 "body": new_body, 2044 } 2045 2046 if self._resources is not None: 2047 # Force the score to be in sample if it was for a legacy trace ID, i.e. non-32 hexchar 2048 force_sample = ( 2049 not self._is_valid_trace_id(trace_id) if trace_id else True 2050 ) 2051 2052 self._resources.add_score_task( 2053 event, 2054 force_sample=force_sample, 2055 ) 2056 2057 except Exception as e: 2058 langfuse_logger.exception( 2059 "Error creating score: Failed to process score event for trace_id=%s, " 2060 "name=%s. Error: %s", 2061 trace_id, 2062 name, 2063 e, 2064 )
Create a score for a specific trace or observation.
This method creates a score for evaluating a Langfuse trace or observation. Scores can be used to track quality metrics, user feedback, or automated evaluations.
Arguments:
- name: Name of the score (e.g., "relevance", "accuracy")
- value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION)
- session_id: ID of the Langfuse session to associate the score with
- dataset_run_id: ID of the Langfuse dataset run to associate the score with
- trace_id: ID of the Langfuse trace to associate the score with
- observation_id: Optional ID of the specific observation to score. Trace ID must be provided too.
- score_id: Optional custom ID for the score (auto-generated if not provided)
- data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION)
- comment: Optional comment or explanation for the score
- config_id: Optional ID of a score config defined in Langfuse
- metadata: Optional metadata to be attached to the score
- timestamp: Optional timestamp for the score (defaults to current UTC time)
- environment: Optional environment override for this score. If omitted,
the score uses the client-level environment from
Langfuse(environment=...)orLANGFUSE_TRACING_ENVIRONMENT. Langfuse observation wrapper methods pass their resolved span environment here so scores created viaspan.score()orspan.score_trace()stay grouped with the scored observation or trace, including request-scoped environments propagated withpropagate_attributes(environment=...).
Example:
# Create a numeric score for accuracy langfuse.create_score( name="accuracy", value=0.92, trace_id="abcdef1234567890abcdef1234567890", data_type="NUMERIC", comment="High accuracy with minor irrelevant details" ) # Create a categorical score for sentiment langfuse.create_score( name="sentiment", value="positive", trace_id="abcdef1234567890abcdef1234567890", observation_id="abcdef1234567890", data_type="CATEGORICAL" )
2130 def score_current_span( 2131 self, 2132 *, 2133 name: str, 2134 value: Union[float, str], 2135 score_id: Optional[str] = None, 2136 data_type: Optional[ScoreDataType] = None, 2137 comment: Optional[str] = None, 2138 config_id: Optional[str] = None, 2139 metadata: Optional[Any] = None, 2140 ) -> None: 2141 """Create a score for the current active span. 2142 2143 This method scores the currently active span in the context. It's a convenient 2144 way to score the current operation without needing to know its trace and span IDs. 2145 If the active span has a `langfuse.environment` attribute, including one 2146 set by `propagate_attributes(environment=...)`, the score uses that 2147 environment. Otherwise it uses the client-level environment. 2148 2149 Args: 2150 name: Name of the score (e.g., "relevance", "accuracy") 2151 value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION) 2152 score_id: Optional custom ID for the score (auto-generated if not provided) 2153 data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION) 2154 comment: Optional comment or explanation for the score 2155 config_id: Optional ID of a score config defined in Langfuse 2156 metadata: Optional metadata to be attached to the score 2157 2158 Example: 2159 ```python 2160 with langfuse.start_as_current_generation(name="answer-query") as generation: 2161 # Generate answer 2162 response = generate_answer(...) 2163 generation.update(output=response) 2164 2165 # Score the generation 2166 langfuse.score_current_span( 2167 name="relevance", 2168 value=0.85, 2169 data_type="NUMERIC", 2170 comment="Mostly relevant but contains some tangential information", 2171 metadata={"model": "gpt-4", "prompt_version": "v2"} 2172 ) 2173 ``` 2174 """ 2175 current_span = self._get_current_otel_span() 2176 2177 if current_span is not None: 2178 trace_id = self._get_otel_trace_id(current_span) 2179 observation_id = self._get_otel_span_id(current_span) 2180 2181 langfuse_logger.info( 2182 "Score: Creating score name='%s' value=%s for current span (%s) in trace " 2183 "%s", 2184 name, 2185 value, 2186 observation_id, 2187 trace_id, 2188 ) 2189 2190 self.create_score( 2191 trace_id=trace_id, 2192 observation_id=observation_id, 2193 name=name, 2194 value=cast(str, value), 2195 score_id=score_id, 2196 data_type=cast(Literal["CATEGORICAL", "TEXT", "CORRECTION"], data_type), 2197 comment=comment, 2198 config_id=config_id, 2199 metadata=metadata, 2200 environment=get_string_span_attribute( 2201 current_span, LangfuseOtelSpanAttributes.ENVIRONMENT 2202 ), 2203 )
Create a score for the current active span.
This method scores the currently active span in the context. It's a convenient
way to score the current operation without needing to know its trace and span IDs.
If the active span has a langfuse.environment attribute, including one
set by propagate_attributes(environment=...), the score uses that
environment. Otherwise it uses the client-level environment.
Arguments:
- name: Name of the score (e.g., "relevance", "accuracy")
- value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION)
- score_id: Optional custom ID for the score (auto-generated if not provided)
- data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION)
- comment: Optional comment or explanation for the score
- config_id: Optional ID of a score config defined in Langfuse
- metadata: Optional metadata to be attached to the score
Example:
with langfuse.start_as_current_generation(name="answer-query") as generation: # Generate answer response = generate_answer(...) generation.update(output=response) # Score the generation langfuse.score_current_span( name="relevance", value=0.85, data_type="NUMERIC", comment="Mostly relevant but contains some tangential information", metadata={"model": "gpt-4", "prompt_version": "v2"} )
2233 def score_current_trace( 2234 self, 2235 *, 2236 name: str, 2237 value: Union[float, str], 2238 score_id: Optional[str] = None, 2239 data_type: Optional[ScoreDataType] = None, 2240 comment: Optional[str] = None, 2241 config_id: Optional[str] = None, 2242 metadata: Optional[Any] = None, 2243 ) -> None: 2244 """Create a score for the current trace. 2245 2246 This method scores the trace of the currently active span. Unlike score_current_span, 2247 this method associates the score with the entire trace rather than a specific span. 2248 It's useful for scoring overall performance or quality of the entire operation. 2249 If the active span has a `langfuse.environment` attribute, including one 2250 set by `propagate_attributes(environment=...)`, the score uses that 2251 environment. Otherwise it uses the client-level environment. 2252 2253 Args: 2254 name: Name of the score (e.g., "user_satisfaction", "overall_quality") 2255 value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION) 2256 score_id: Optional custom ID for the score (auto-generated if not provided) 2257 data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION) 2258 comment: Optional comment or explanation for the score 2259 config_id: Optional ID of a score config defined in Langfuse 2260 metadata: Optional metadata to be attached to the score 2261 2262 Example: 2263 ```python 2264 with langfuse.start_as_current_observation(name="process-user-request") as span: 2265 # Process request 2266 result = process_complete_request() 2267 span.update(output=result) 2268 2269 # Score the overall trace 2270 langfuse.score_current_trace( 2271 name="overall_quality", 2272 value=0.95, 2273 data_type="NUMERIC", 2274 comment="High quality end-to-end response", 2275 metadata={"evaluator": "gpt-4", "criteria": "comprehensive"} 2276 ) 2277 ``` 2278 """ 2279 current_span = self._get_current_otel_span() 2280 2281 if current_span is not None: 2282 trace_id = self._get_otel_trace_id(current_span) 2283 2284 langfuse_logger.info( 2285 "Score: Creating score name='%s' value=%s for entire trace %s", 2286 name, 2287 value, 2288 trace_id, 2289 ) 2290 2291 self.create_score( 2292 trace_id=trace_id, 2293 name=name, 2294 value=cast(str, value), 2295 score_id=score_id, 2296 data_type=cast(Literal["CATEGORICAL", "TEXT", "CORRECTION"], data_type), 2297 comment=comment, 2298 config_id=config_id, 2299 metadata=metadata, 2300 environment=get_string_span_attribute( 2301 current_span, LangfuseOtelSpanAttributes.ENVIRONMENT 2302 ), 2303 )
Create a score for the current trace.
This method scores the trace of the currently active span. Unlike score_current_span,
this method associates the score with the entire trace rather than a specific span.
It's useful for scoring overall performance or quality of the entire operation.
If the active span has a langfuse.environment attribute, including one
set by propagate_attributes(environment=...), the score uses that
environment. Otherwise it uses the client-level environment.
Arguments:
- name: Name of the score (e.g., "user_satisfaction", "overall_quality")
- value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT/CORRECTION)
- score_id: Optional custom ID for the score (auto-generated if not provided)
- data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, or CORRECTION)
- comment: Optional comment or explanation for the score
- config_id: Optional ID of a score config defined in Langfuse
- metadata: Optional metadata to be attached to the score
Example:
with langfuse.start_as_current_observation(name="process-user-request") as span: # Process request result = process_complete_request() span.update(output=result) # Score the overall trace langfuse.score_current_trace( name="overall_quality", value=0.95, data_type="NUMERIC", comment="High quality end-to-end response", metadata={"evaluator": "gpt-4", "criteria": "comprehensive"} )
2305 def flush(self) -> None: 2306 """Force flush all pending spans and events to the Langfuse API. 2307 2308 This method manually flushes any pending spans, scores, and other events to the 2309 Langfuse API. It's useful in scenarios where you want to ensure all data is sent 2310 before proceeding, without waiting for the automatic flush interval. 2311 2312 Example: 2313 ```python 2314 # Record some spans and scores 2315 with langfuse.start_as_current_observation(name="operation") as span: 2316 # Do work... 2317 pass 2318 2319 # Ensure all data is sent to Langfuse before proceeding 2320 langfuse.flush() 2321 2322 # Continue with other work 2323 ``` 2324 2325 Note: 2326 `flush()` guarantees data was *delivered* to the API, not that it is 2327 *readable* yet: server-side ingestion is asynchronous, so flushed data 2328 may not be queryable for 15-30 seconds — 2329 `api.observations.get_many(trace_id=...)` may return empty results and 2330 `api.trace.get()` may raise `langfuse.api.NotFoundError` right after a 2331 successful flush. See the `api` property docs for a bounded retry 2332 pattern, or 2333 https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk#ingestion-lag 2334 """ 2335 if self._resources is not None: 2336 self._resources.flush()
Force flush all pending spans and events to the Langfuse API.
This method manually flushes any pending spans, scores, and other events to the Langfuse API. It's useful in scenarios where you want to ensure all data is sent before proceeding, without waiting for the automatic flush interval.
Example:
# Record some spans and scores with langfuse.start_as_current_observation(name="operation") as span: # Do work... pass # Ensure all data is sent to Langfuse before proceeding langfuse.flush() # Continue with other work
Note:
flush()guarantees data was delivered to the API, not that it is readable yet: server-side ingestion is asynchronous, so flushed data may not be queryable for 15-30 seconds —api.observations.get_many(trace_id=...)may return empty results andapi.trace.get()may raiselangfuse.api.NotFoundErrorright after a successful flush. See theapiproperty docs for a bounded retry pattern, or https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk#ingestion-lag
2338 def shutdown(self) -> None: 2339 """Shut down the Langfuse client and flush all pending data. 2340 2341 This method cleanly shuts down the Langfuse client, ensuring all pending data 2342 is flushed to the API and all background threads are properly terminated. 2343 2344 It's important to call this method when your application is shutting down to 2345 prevent data loss and resource leaks. For most applications, using the client 2346 as a context manager or relying on the automatic shutdown via atexit is sufficient. 2347 2348 Example: 2349 ```python 2350 # Initialize Langfuse 2351 langfuse = Langfuse(public_key="...", secret_key="...") 2352 2353 # Use Langfuse throughout your application 2354 # ... 2355 2356 # When application is shutting down 2357 langfuse.shutdown() 2358 ``` 2359 """ 2360 if self._resources is not None: 2361 self._resources.shutdown()
Shut down the Langfuse client and flush all pending data.
This method cleanly shuts down the Langfuse client, ensuring all pending data is flushed to the API and all background threads are properly terminated.
It's important to call this method when your application is shutting down to prevent data loss and resource leaks. For most applications, using the client as a context manager or relying on the automatic shutdown via atexit is sufficient.
Example:
# Initialize Langfuse langfuse = Langfuse(public_key="...", secret_key="...") # Use Langfuse throughout your application # ... # When application is shutting down langfuse.shutdown()
2363 def get_current_trace_id(self) -> Optional[str]: 2364 """Get the trace ID of the current active span. 2365 2366 This method retrieves the trace ID from the currently active span in the context. 2367 It can be used to get the trace ID for referencing in logs, external systems, 2368 or for creating related operations. 2369 2370 Returns: 2371 The current trace ID as a 32-character lowercase hexadecimal string, 2372 or None if there is no active span. 2373 2374 Example: 2375 ```python 2376 with langfuse.start_as_current_observation(name="process-request") as span: 2377 # Get the current trace ID for reference 2378 trace_id = langfuse.get_current_trace_id() 2379 2380 # Use it for external correlation 2381 log.info(f"Processing request with trace_id: {trace_id}") 2382 2383 # Or pass to another system 2384 external_system.process(data, trace_id=trace_id) 2385 ``` 2386 """ 2387 if not self._tracing_enabled: 2388 langfuse_logger.debug( 2389 "Operation skipped: get_current_trace_id - Tracing is disabled or client is in no-op mode." 2390 ) 2391 return None 2392 2393 current_otel_span = self._get_current_otel_span() 2394 2395 return self._get_otel_trace_id(current_otel_span) if current_otel_span else None
Get the trace ID of the current active span.
This method retrieves the trace ID from the currently active span in the context. It can be used to get the trace ID for referencing in logs, external systems, or for creating related operations.
Returns:
The current trace ID as a 32-character lowercase hexadecimal string, or None if there is no active span.
Example:
with langfuse.start_as_current_observation(name="process-request") as span: # Get the current trace ID for reference trace_id = langfuse.get_current_trace_id() # Use it for external correlation log.info(f"Processing request with trace_id: {trace_id}") # Or pass to another system external_system.process(data, trace_id=trace_id)
2397 def get_current_observation_id(self) -> Optional[str]: 2398 """Get the observation ID (span ID) of the current active span. 2399 2400 This method retrieves the observation ID from the currently active span in the context. 2401 It can be used to get the observation ID for referencing in logs, external systems, 2402 or for creating scores or other related operations. 2403 2404 Returns: 2405 The current observation ID as a 16-character lowercase hexadecimal string, 2406 or None if there is no active span. 2407 2408 Example: 2409 ```python 2410 with langfuse.start_as_current_observation(name="process-user-query") as span: 2411 # Get the current observation ID 2412 observation_id = langfuse.get_current_observation_id() 2413 2414 # Store it for later reference 2415 cache.set(f"query_{query_id}_observation", observation_id) 2416 2417 # Process the query... 2418 ``` 2419 """ 2420 if not self._tracing_enabled: 2421 langfuse_logger.debug( 2422 "Operation skipped: get_current_observation_id - Tracing is disabled or client is in no-op mode." 2423 ) 2424 return None 2425 2426 current_otel_span = self._get_current_otel_span() 2427 2428 return self._get_otel_span_id(current_otel_span) if current_otel_span else None
Get the observation ID (span ID) of the current active span.
This method retrieves the observation ID from the currently active span in the context. It can be used to get the observation ID for referencing in logs, external systems, or for creating scores or other related operations.
Returns:
The current observation ID as a 16-character lowercase hexadecimal string, or None if there is no active span.
Example:
with langfuse.start_as_current_observation(name="process-user-query") as span: # Get the current observation ID observation_id = langfuse.get_current_observation_id() # Store it for later reference cache.set(f"query_{query_id}_observation", observation_id) # Process the query...
2441 def get_trace_url(self, *, trace_id: Optional[str] = None) -> Optional[str]: 2442 """Get the URL to view a trace in the Langfuse UI. 2443 2444 This method generates a URL that links directly to a trace in the Langfuse UI. 2445 It's useful for providing links in logs, notifications, or debugging tools. 2446 2447 Args: 2448 trace_id: Optional trace ID to generate a URL for. If not provided, 2449 the trace ID of the current active span will be used. 2450 2451 Returns: 2452 A URL string pointing to the trace in the Langfuse UI, 2453 or None if the project ID couldn't be retrieved or no trace ID is available. 2454 2455 Example: 2456 ```python 2457 # Get URL for the current trace 2458 with langfuse.start_as_current_observation(name="process-request") as span: 2459 trace_url = langfuse.get_trace_url() 2460 log.info(f"Processing trace: {trace_url}") 2461 2462 # Get URL for a specific trace 2463 specific_trace_url = langfuse.get_trace_url(trace_id="1234567890abcdef1234567890abcdef") 2464 send_notification(f"Review needed for trace: {specific_trace_url}") 2465 ``` 2466 """ 2467 final_trace_id = trace_id or self.get_current_trace_id() 2468 if not final_trace_id: 2469 return None 2470 2471 project_id = self._get_project_id() 2472 2473 return ( 2474 f"{self._base_url}/project/{project_id}/traces/{final_trace_id}" 2475 if project_id and final_trace_id 2476 else None 2477 )
Get the URL to view a trace in the Langfuse UI.
This method generates a URL that links directly to a trace in the Langfuse UI. It's useful for providing links in logs, notifications, or debugging tools.
Arguments:
- trace_id: Optional trace ID to generate a URL for. If not provided, the trace ID of the current active span will be used.
Returns:
A URL string pointing to the trace in the Langfuse UI, or None if the project ID couldn't be retrieved or no trace ID is available.
Example:
# Get URL for the current trace with langfuse.start_as_current_observation(name="process-request") as span: trace_url = langfuse.get_trace_url() log.info(f"Processing trace: {trace_url}") # Get URL for a specific trace specific_trace_url = langfuse.get_trace_url(trace_id="1234567890abcdef1234567890abcdef") send_notification(f"Review needed for trace: {specific_trace_url}")
2479 def get_dataset( 2480 self, 2481 name: str, 2482 *, 2483 fetch_items_page_size: Optional[int] = 50, 2484 version: Optional[datetime] = None, 2485 ) -> "DatasetClient": 2486 """Fetch a dataset by its name. 2487 2488 Args: 2489 name: The name of the dataset to fetch. 2490 fetch_items_page_size: All items of the dataset will be fetched in chunks of this size. Defaults to 50. 2491 version: Retrieve dataset items as they existed at this specific point in time (UTC). 2492 If provided, returns the state of items at the specified UTC timestamp. 2493 If not provided, returns the latest version. Must be a timezone-aware datetime object in UTC. 2494 2495 Returns: 2496 DatasetClient: The dataset with the given name. 2497 """ 2498 try: 2499 langfuse_logger.debug("Getting datasets %s", name) 2500 dataset = self.api.datasets.get(dataset_name=self._url_encode(name)) 2501 2502 dataset_items: List[DatasetItem] = [] 2503 page = 1 2504 2505 while True: 2506 new_items = self.api.dataset_items.list( 2507 dataset_name=self._url_encode(name, is_url_param=True), 2508 page=page, 2509 limit=fetch_items_page_size, 2510 version=version, 2511 ) 2512 dataset_items.extend( 2513 self._hydrate_dataset_item_media_references(item) 2514 for item in new_items.data 2515 ) 2516 2517 if new_items.meta.total_pages <= page: 2518 break 2519 2520 page += 1 2521 2522 return DatasetClient( 2523 dataset=dataset, 2524 items=dataset_items, 2525 version=version, 2526 langfuse_client=self, 2527 ) 2528 2529 except Error as e: 2530 handle_fern_exception(e) 2531 raise e
Fetch a dataset by its name.
Arguments:
- name: The name of the dataset to fetch.
- fetch_items_page_size: All items of the dataset will be fetched in chunks of this size. Defaults to 50.
- version: Retrieve dataset items as they existed at this specific point in time (UTC). If provided, returns the state of items at the specified UTC timestamp. If not provided, returns the latest version. Must be a timezone-aware datetime object in UTC.
Returns:
DatasetClient: The dataset with the given name.
2533 def get_dataset_run( 2534 self, *, dataset_name: str, run_name: str 2535 ) -> DatasetRunWithItems: 2536 """Fetch a dataset run by dataset name and run name. 2537 2538 Args: 2539 dataset_name (str): The name of the dataset. 2540 run_name (str): The name of the run. 2541 2542 Returns: 2543 DatasetRunWithItems: The dataset run with its items. 2544 """ 2545 try: 2546 return cast( 2547 DatasetRunWithItems, 2548 self.api.datasets.get_run( 2549 dataset_name=self._url_encode(dataset_name), 2550 run_name=self._url_encode(run_name), 2551 request_options=None, 2552 ), 2553 ) 2554 except Error as e: 2555 handle_fern_exception(e) 2556 raise e
Fetch a dataset run by dataset name and run name.
Arguments:
- dataset_name (str): The name of the dataset.
- run_name (str): The name of the run.
Returns:
DatasetRunWithItems: The dataset run with its items.
2558 def get_dataset_runs( 2559 self, 2560 *, 2561 dataset_name: str, 2562 page: Optional[int] = None, 2563 limit: Optional[int] = None, 2564 ) -> PaginatedDatasetRuns: 2565 """Fetch all runs for a dataset. 2566 2567 Args: 2568 dataset_name (str): The name of the dataset. 2569 page (Optional[int]): Page number, starts at 1. 2570 limit (Optional[int]): Limit of items per page. 2571 2572 Returns: 2573 PaginatedDatasetRuns: Paginated list of dataset runs. 2574 """ 2575 try: 2576 return cast( 2577 PaginatedDatasetRuns, 2578 self.api.datasets.get_runs( 2579 dataset_name=self._url_encode(dataset_name), 2580 page=page, 2581 limit=limit, 2582 request_options=None, 2583 ), 2584 ) 2585 except Error as e: 2586 handle_fern_exception(e) 2587 raise e
Fetch all runs for a dataset.
Arguments:
- dataset_name (str): The name of the dataset.
- page (Optional[int]): Page number, starts at 1.
- limit (Optional[int]): Limit of items per page.
Returns:
PaginatedDatasetRuns: Paginated list of dataset runs.
2589 def delete_dataset_run( 2590 self, *, dataset_name: str, run_name: str 2591 ) -> DeleteDatasetRunResponse: 2592 """Delete a dataset run and all its run items. This action is irreversible. 2593 2594 Args: 2595 dataset_name (str): The name of the dataset. 2596 run_name (str): The name of the run. 2597 2598 Returns: 2599 DeleteDatasetRunResponse: Confirmation of deletion. 2600 """ 2601 try: 2602 return cast( 2603 DeleteDatasetRunResponse, 2604 self.api.datasets.delete_run( 2605 dataset_name=self._url_encode(dataset_name), 2606 run_name=self._url_encode(run_name), 2607 request_options=None, 2608 ), 2609 ) 2610 except Error as e: 2611 handle_fern_exception(e) 2612 raise e
Delete a dataset run and all its run items. This action is irreversible.
Arguments:
- dataset_name (str): The name of the dataset.
- run_name (str): The name of the run.
Returns:
DeleteDatasetRunResponse: Confirmation of deletion.
2614 def run_experiment( 2615 self, 2616 *, 2617 name: str, 2618 run_name: Optional[str] = None, 2619 description: Optional[str] = None, 2620 data: ExperimentData, 2621 task: TaskFunction, 2622 evaluators: List[EvaluatorFunction] = [], 2623 composite_evaluator: Optional[CompositeEvaluatorFunction] = None, 2624 run_evaluators: List[RunEvaluatorFunction] = [], 2625 max_concurrency: int = 50, 2626 metadata: Optional[Dict[str, str]] = None, 2627 _dataset_version: Optional[datetime] = None, 2628 ) -> ExperimentResult: 2629 """Run an experiment on a dataset with automatic tracing and evaluation. 2630 2631 This method executes a task function on each item in the provided dataset, 2632 automatically traces all executions with Langfuse for observability, runs 2633 item-level and run-level evaluators on the outputs, and returns comprehensive 2634 results with evaluation metrics. 2635 2636 The experiment system provides: 2637 - Automatic tracing of all task executions 2638 - Concurrent processing with configurable limits 2639 - Comprehensive error handling that isolates failures 2640 - Integration with Langfuse datasets for experiment tracking 2641 - Flexible evaluation framework supporting both sync and async evaluators 2642 2643 Args: 2644 name: Human-readable name for the experiment. Used for identification 2645 in the Langfuse UI. 2646 run_name: Optional exact name for the experiment run. If provided, this will be 2647 used as the exact dataset run name if the `data` contains Langfuse dataset items. 2648 If not provided, this will default to the experiment name appended with an ISO timestamp. 2649 description: Optional description explaining the experiment's purpose, 2650 methodology, or expected outcomes. 2651 data: Array of data items to process. Can be either: 2652 - List of dict-like items with 'input', 'expected_output', 'metadata' keys 2653 - List of Langfuse DatasetItem objects from dataset.items 2654 task: Function that processes each data item and returns output. 2655 Must accept 'item' as keyword argument and can return sync or async results. 2656 The task function signature should be: task(*, item, **kwargs) -> Any 2657 evaluators: List of functions to evaluate each item's output individually. 2658 Each evaluator receives input, output, expected_output, and metadata. 2659 Can return single Evaluation dict or list of Evaluation dicts. 2660 composite_evaluator: Optional function that creates composite scores from item-level evaluations. 2661 Receives the same inputs as item-level evaluators (input, output, expected_output, metadata) 2662 plus the list of evaluations from item-level evaluators. Useful for weighted averages, 2663 pass/fail decisions based on multiple criteria, or custom scoring logic combining multiple metrics. 2664 run_evaluators: List of functions to evaluate the entire experiment run. 2665 Each run evaluator receives all item_results and can compute aggregate metrics. 2666 Useful for calculating averages, distributions, or cross-item comparisons. 2667 max_concurrency: Maximum number of concurrent task executions (default: 50). 2668 Controls the number of items processed simultaneously. Adjust based on 2669 API rate limits and system resources. 2670 metadata: Optional metadata dictionary to attach to all experiment traces. 2671 This metadata will be included in every trace created during the experiment. 2672 If `data` are Langfuse dataset items, the metadata will be attached to the dataset run, too. 2673 2674 Returns: 2675 ExperimentResult containing: 2676 - run_name: The experiment run name. This is equal to the dataset run name if experiment was on Langfuse dataset. 2677 - item_results: List of results for each processed item with outputs and evaluations 2678 - run_evaluations: List of aggregate evaluation results for the entire run 2679 - experiment_id: Stable identifier for the experiment run across all items 2680 - dataset_run_id: ID of the dataset run (if using Langfuse datasets) 2681 - dataset_run_url: Direct URL to view results in Langfuse UI (if applicable) 2682 2683 Raises: 2684 ValueError: If required parameters are missing or invalid 2685 Exception: If experiment setup fails (individual item failures are handled gracefully) 2686 2687 Examples: 2688 Basic experiment with local data: 2689 ```python 2690 def summarize_text(*, item, **kwargs): 2691 return f"Summary: {item['input'][:50]}..." 2692 2693 def length_evaluator(*, input, output, expected_output=None, **kwargs): 2694 return { 2695 "name": "output_length", 2696 "value": len(output), 2697 "comment": f"Output contains {len(output)} characters" 2698 } 2699 2700 result = langfuse.run_experiment( 2701 name="Text Summarization Test", 2702 description="Evaluate summarization quality and length", 2703 data=[ 2704 {"input": "Long article text...", "expected_output": "Expected summary"}, 2705 {"input": "Another article...", "expected_output": "Another summary"} 2706 ], 2707 task=summarize_text, 2708 evaluators=[length_evaluator] 2709 ) 2710 2711 print(f"Processed {len(result.item_results)} items") 2712 for item_result in result.item_results: 2713 print(f"Input: {item_result.item['input']}") 2714 print(f"Output: {item_result.output}") 2715 print(f"Evaluations: {item_result.evaluations}") 2716 ``` 2717 2718 Advanced experiment with async task and multiple evaluators: 2719 ```python 2720 async def llm_task(*, item, **kwargs): 2721 # Simulate async LLM call 2722 response = await openai_client.chat.completions.create( 2723 model="gpt-4", 2724 messages=[{"role": "user", "content": item["input"]}] 2725 ) 2726 return response.choices[0].message.content 2727 2728 def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): 2729 if expected_output and expected_output.lower() in output.lower(): 2730 return {"name": "accuracy", "value": 1.0, "comment": "Correct answer"} 2731 return {"name": "accuracy", "value": 0.0, "comment": "Incorrect answer"} 2732 2733 def toxicity_evaluator(*, input, output, expected_output=None, **kwargs): 2734 # Simulate toxicity check 2735 toxicity_score = check_toxicity(output) # Your toxicity checker 2736 return { 2737 "name": "toxicity", 2738 "value": toxicity_score, 2739 "comment": f"Toxicity level: {'high' if toxicity_score > 0.7 else 'low'}" 2740 } 2741 2742 def average_accuracy(*, item_results, **kwargs): 2743 accuracies = [ 2744 eval.value for result in item_results 2745 for eval in result.evaluations 2746 if eval.name == "accuracy" 2747 ] 2748 return { 2749 "name": "average_accuracy", 2750 "value": sum(accuracies) / len(accuracies) if accuracies else 0, 2751 "comment": f"Average accuracy across {len(accuracies)} items" 2752 } 2753 2754 result = langfuse.run_experiment( 2755 name="LLM Safety and Accuracy Test", 2756 description="Evaluate model accuracy and safety across diverse prompts", 2757 data=test_dataset, # Your dataset items 2758 task=llm_task, 2759 evaluators=[accuracy_evaluator, toxicity_evaluator], 2760 run_evaluators=[average_accuracy], 2761 max_concurrency=5, # Limit concurrent API calls 2762 metadata={"model": "gpt-4", "temperature": 0.7} 2763 ) 2764 ``` 2765 2766 Using with Langfuse datasets: 2767 ```python 2768 # Get dataset from Langfuse 2769 dataset = langfuse.get_dataset("my-eval-dataset") 2770 2771 result = dataset.run_experiment( 2772 name="Production Model Evaluation", 2773 description="Monthly evaluation of production model performance", 2774 task=my_production_task, 2775 evaluators=[accuracy_evaluator, latency_evaluator] 2776 ) 2777 2778 # Results automatically linked to dataset in Langfuse UI 2779 print(f"View results: {result['dataset_run_url']}") 2780 ``` 2781 2782 Note: 2783 - Task and evaluator functions can be either synchronous or asynchronous 2784 - Individual item failures are logged but don't stop the experiment 2785 - All executions are automatically traced and visible in Langfuse UI 2786 - When using Langfuse datasets, results are automatically linked for easy comparison 2787 - This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.) 2788 - Async execution is handled automatically with smart event loop detection 2789 """ 2790 return cast( 2791 ExperimentResult, 2792 run_async_safely( 2793 self._run_experiment_async( 2794 name=name, 2795 run_name=self._create_experiment_run_name( 2796 name=name, run_name=run_name 2797 ), 2798 description=description, 2799 data=data, 2800 task=task, 2801 evaluators=evaluators or [], 2802 composite_evaluator=composite_evaluator, 2803 run_evaluators=run_evaluators or [], 2804 max_concurrency=max_concurrency, 2805 metadata=metadata, 2806 dataset_version=_dataset_version, 2807 ), 2808 ), 2809 )
Run an experiment on a dataset with automatic tracing and evaluation.
This method executes a task function on each item in the provided dataset, automatically traces all executions with Langfuse for observability, runs item-level and run-level evaluators on the outputs, and returns comprehensive results with evaluation metrics.
The experiment system provides:
- Automatic tracing of all task executions
- Concurrent processing with configurable limits
- Comprehensive error handling that isolates failures
- Integration with Langfuse datasets for experiment tracking
- Flexible evaluation framework supporting both sync and async evaluators
Arguments:
- name: Human-readable name for the experiment. Used for identification in the Langfuse UI.
- run_name: Optional exact name for the experiment run. If provided, this will be
used as the exact dataset run name if the
datacontains Langfuse dataset items. If not provided, this will default to the experiment name appended with an ISO timestamp. - description: Optional description explaining the experiment's purpose, methodology, or expected outcomes.
- data: Array of data items to process. Can be either:
- List of dict-like items with 'input', 'expected_output', 'metadata' keys
- List of Langfuse DatasetItem objects from dataset.items
- task: Function that processes each data item and returns output. Must accept 'item' as keyword argument and can return sync or async results. The task function signature should be: task(*, item, **kwargs) -> Any
- evaluators: List of functions to evaluate each item's output individually. Each evaluator receives input, output, expected_output, and metadata. Can return single Evaluation dict or list of Evaluation dicts.
- composite_evaluator: Optional function that creates composite scores from item-level evaluations. Receives the same inputs as item-level evaluators (input, output, expected_output, metadata) plus the list of evaluations from item-level evaluators. Useful for weighted averages, pass/fail decisions based on multiple criteria, or custom scoring logic combining multiple metrics.
- run_evaluators: List of functions to evaluate the entire experiment run. Each run evaluator receives all item_results and can compute aggregate metrics. Useful for calculating averages, distributions, or cross-item comparisons.
- max_concurrency: Maximum number of concurrent task executions (default: 50). Controls the number of items processed simultaneously. Adjust based on API rate limits and system resources.
- metadata: Optional metadata dictionary to attach to all experiment traces.
This metadata will be included in every trace created during the experiment.
If
dataare Langfuse dataset items, the metadata will be attached to the dataset run, too.
Returns:
ExperimentResult containing:
- run_name: The experiment run name. This is equal to the dataset run name if experiment was on Langfuse dataset.
- item_results: List of results for each processed item with outputs and evaluations
- run_evaluations: List of aggregate evaluation results for the entire run
- experiment_id: Stable identifier for the experiment run across all items
- dataset_run_id: ID of the dataset run (if using Langfuse datasets)
- dataset_run_url: Direct URL to view results in Langfuse UI (if applicable)
Raises:
- ValueError: If required parameters are missing or invalid
- Exception: If experiment setup fails (individual item failures are handled gracefully)
Examples:
Basic experiment with local data:
def summarize_text(*, item, **kwargs): return f"Summary: {item['input'][:50]}..." def length_evaluator(*, input, output, expected_output=None, **kwargs): return { "name": "output_length", "value": len(output), "comment": f"Output contains {len(output)} characters" } result = langfuse.run_experiment( name="Text Summarization Test", description="Evaluate summarization quality and length", data=[ {"input": "Long article text...", "expected_output": "Expected summary"}, {"input": "Another article...", "expected_output": "Another summary"} ], task=summarize_text, evaluators=[length_evaluator] ) print(f"Processed {len(result.item_results)} items") for item_result in result.item_results: print(f"Input: {item_result.item['input']}") print(f"Output: {item_result.output}") print(f"Evaluations: {item_result.evaluations}")Advanced experiment with async task and multiple evaluators:
async def llm_task(*, item, **kwargs): # Simulate async LLM call response = await openai_client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": item["input"]}] ) return response.choices[0].message.content def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): if expected_output and expected_output.lower() in output.lower(): return {"name": "accuracy", "value": 1.0, "comment": "Correct answer"} return {"name": "accuracy", "value": 0.0, "comment": "Incorrect answer"} def toxicity_evaluator(*, input, output, expected_output=None, **kwargs): # Simulate toxicity check toxicity_score = check_toxicity(output) # Your toxicity checker return { "name": "toxicity", "value": toxicity_score, "comment": f"Toxicity level: {'high' if toxicity_score > 0.7 else 'low'}" } def average_accuracy(*, item_results, **kwargs): accuracies = [ eval.value for result in item_results for eval in result.evaluations if eval.name == "accuracy" ] return { "name": "average_accuracy", "value": sum(accuracies) / len(accuracies) if accuracies else 0, "comment": f"Average accuracy across {len(accuracies)} items" } result = langfuse.run_experiment( name="LLM Safety and Accuracy Test", description="Evaluate model accuracy and safety across diverse prompts", data=test_dataset, # Your dataset items task=llm_task, evaluators=[accuracy_evaluator, toxicity_evaluator], run_evaluators=[average_accuracy], max_concurrency=5, # Limit concurrent API calls metadata={"model": "gpt-4", "temperature": 0.7} )Using with Langfuse datasets:
# Get dataset from Langfuse dataset = langfuse.get_dataset("my-eval-dataset") result = dataset.run_experiment( name="Production Model Evaluation", description="Monthly evaluation of production model performance", task=my_production_task, evaluators=[accuracy_evaluator, latency_evaluator] ) # Results automatically linked to dataset in Langfuse UI print(f"View results: {result['dataset_run_url']}")
Note:
- Task and evaluator functions can be either synchronous or asynchronous
- Individual item failures are logged but don't stop the experiment
- All executions are automatically traced and visible in Langfuse UI
- When using Langfuse datasets, results are automatically linked for easy comparison
- This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.)
- Async execution is handled automatically with smart event loop detection
3257 def run_batched_evaluation( 3258 self, 3259 *, 3260 scope: Literal["traces", "observations"], 3261 mapper: MapperFunction, 3262 filter: Optional[str] = None, 3263 fetch_batch_size: int = 50, 3264 fetch_trace_fields: Optional[str] = None, 3265 max_items: Optional[int] = None, 3266 max_retries: int = 3, 3267 evaluators: List[EvaluatorFunction], 3268 composite_evaluator: Optional[CompositeEvaluatorFunction] = None, 3269 max_concurrency: int = 5, 3270 metadata: Optional[Dict[str, Any]] = None, 3271 _add_observation_scores_to_trace: bool = False, 3272 _additional_trace_tags: Optional[List[str]] = None, 3273 resume_from: Optional[BatchEvaluationResumeToken] = None, 3274 verbose: bool = False, 3275 ) -> BatchEvaluationResult: 3276 """Fetch traces or observations and run evaluations on each item. 3277 3278 This method provides a powerful way to evaluate existing data in Langfuse at scale. 3279 It fetches items based on filters, transforms them using a mapper function, runs 3280 evaluators on each item, and creates scores that are linked back to the original 3281 entities. This is ideal for: 3282 3283 - Running evaluations on production traces after deployment 3284 - Backtesting new evaluation metrics on historical data 3285 - Batch scoring of observations for quality monitoring 3286 - Periodic evaluation runs on recent data 3287 3288 The method uses a streaming/pipeline approach to process items in batches, making 3289 it memory-efficient for large datasets. It includes comprehensive error handling, 3290 retry logic, and resume capability for long-running evaluations. 3291 3292 Args: 3293 scope: The type of items to evaluate. Must be one of: 3294 - "traces": Evaluate complete traces with all their observations 3295 - "observations": Evaluate individual observations (spans, generations, events) 3296 mapper: Function that transforms API response objects into evaluator inputs. 3297 Receives a trace/observation object and returns an EvaluatorInputs 3298 instance with input, output, expected_output, and metadata fields. 3299 Can be sync or async. 3300 evaluators: List of evaluation functions to run on each item. Each evaluator 3301 receives the mapped inputs and returns Evaluation object(s). Evaluator 3302 failures are logged but don't stop the batch evaluation. 3303 filter: Optional JSON filter string for querying items (same format as Langfuse API). Examples: 3304 - '{"tags": ["production"]}' 3305 - '{"user_id": "user123", "timestamp": {"operator": ">", "value": "2024-01-01"}}' 3306 Default: None (fetches all items). 3307 fetch_batch_size: Number of items to fetch per API call and hold in memory. 3308 Larger values may be faster but use more memory. Default: 50. 3309 fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'. 3310 max_items: Maximum total number of items to process. If None, processes all 3311 items matching the filter. Useful for testing or limiting evaluation runs. 3312 Default: None (process all). 3313 max_concurrency: Maximum number of items to evaluate concurrently. Controls 3314 parallelism and resource usage. Default: 5. 3315 composite_evaluator: Optional function that creates a composite score from 3316 item-level evaluations. Receives the original item and its evaluations, 3317 returns a single Evaluation. Useful for weighted averages or combined metrics. 3318 Default: None. 3319 metadata: Optional metadata dict to add to all created scores. Useful for 3320 tracking evaluation runs, versions, or other context. Default: None. 3321 max_retries: Maximum number of retry attempts for failed batch fetches. 3322 Uses exponential backoff (1s, 2s, 4s). Default: 3. 3323 verbose: If True, logs progress information to console. Useful for monitoring 3324 long-running evaluations. Default: False. 3325 resume_from: Optional resume token from a previous incomplete run. Allows 3326 continuing evaluation after interruption or failure. Default: None. 3327 3328 3329 Returns: 3330 BatchEvaluationResult containing: 3331 - total_items_fetched: Number of items fetched from API 3332 - total_items_processed: Number of items successfully evaluated 3333 - total_items_failed: Number of items that failed evaluation 3334 - total_scores_created: Scores created by item-level evaluators 3335 - total_composite_scores_created: Scores created by composite evaluator 3336 - total_evaluations_failed: Individual evaluator failures 3337 - evaluator_stats: Per-evaluator statistics (success rate, scores created) 3338 - resume_token: Token for resuming if incomplete (None if completed) 3339 - completed: True if all items processed 3340 - duration_seconds: Total execution time 3341 - failed_item_ids: IDs of items that failed 3342 - error_summary: Error types and counts 3343 - has_more_items: True if max_items reached but more exist 3344 3345 Raises: 3346 ValueError: If invalid scope is provided. 3347 3348 Examples: 3349 Basic trace evaluation: 3350 ```python 3351 from langfuse import Langfuse, EvaluatorInputs, Evaluation 3352 3353 client = Langfuse() 3354 3355 # Define mapper to extract fields from traces 3356 def trace_mapper(trace): 3357 return EvaluatorInputs( 3358 input=trace.input, 3359 output=trace.output, 3360 expected_output=None, 3361 metadata={"trace_id": trace.id} 3362 ) 3363 3364 # Define evaluator 3365 def length_evaluator(*, input, output, expected_output, metadata): 3366 return Evaluation( 3367 name="output_length", 3368 value=len(output) if output else 0 3369 ) 3370 3371 # Run batch evaluation 3372 result = client.run_batched_evaluation( 3373 scope="traces", 3374 mapper=trace_mapper, 3375 evaluators=[length_evaluator], 3376 filter='{"tags": ["production"]}', 3377 max_items=1000, 3378 verbose=True 3379 ) 3380 3381 print(f"Processed {result.total_items_processed} traces") 3382 print(f"Created {result.total_scores_created} scores") 3383 ``` 3384 3385 Evaluation with composite scorer: 3386 ```python 3387 def accuracy_evaluator(*, input, output, expected_output, metadata): 3388 # ... evaluation logic 3389 return Evaluation(name="accuracy", value=0.85) 3390 3391 def relevance_evaluator(*, input, output, expected_output, metadata): 3392 # ... evaluation logic 3393 return Evaluation(name="relevance", value=0.92) 3394 3395 def composite_evaluator(*, item, evaluations): 3396 # Weighted average of evaluations 3397 weights = {"accuracy": 0.6, "relevance": 0.4} 3398 total = sum( 3399 e.value * weights.get(e.name, 0) 3400 for e in evaluations 3401 if isinstance(e.value, (int, float)) 3402 ) 3403 return Evaluation( 3404 name="composite_score", 3405 value=total, 3406 comment=f"Weighted average of {len(evaluations)} metrics" 3407 ) 3408 3409 result = client.run_batched_evaluation( 3410 scope="traces", 3411 mapper=trace_mapper, 3412 evaluators=[accuracy_evaluator, relevance_evaluator], 3413 composite_evaluator=composite_evaluator, 3414 filter='{"user_id": "important_user"}', 3415 verbose=True 3416 ) 3417 ``` 3418 3419 Handling incomplete runs with resume: 3420 ```python 3421 # Initial run that may fail or timeout 3422 result = client.run_batched_evaluation( 3423 scope="observations", 3424 mapper=obs_mapper, 3425 evaluators=[my_evaluator], 3426 max_items=10000, 3427 verbose=True 3428 ) 3429 3430 # Check if incomplete 3431 if not result.completed and result.resume_token: 3432 print(f"Processed {result.resume_token.items_processed} items before interruption") 3433 3434 # Resume from where it left off 3435 result = client.run_batched_evaluation( 3436 scope="observations", 3437 mapper=obs_mapper, 3438 evaluators=[my_evaluator], 3439 resume_from=result.resume_token, 3440 verbose=True 3441 ) 3442 3443 print(f"Total items processed: {result.total_items_processed}") 3444 ``` 3445 3446 Monitoring evaluator performance: 3447 ```python 3448 result = client.run_batched_evaluation(...) 3449 3450 for stats in result.evaluator_stats: 3451 success_rate = stats.successful_runs / stats.total_runs 3452 print(f"{stats.name}:") 3453 print(f" Success rate: {success_rate:.1%}") 3454 print(f" Scores created: {stats.total_scores_created}") 3455 3456 if stats.failed_runs > 0: 3457 print(f" ⚠️ Failed {stats.failed_runs} times") 3458 ``` 3459 3460 Note: 3461 - Evaluator failures are logged but don't stop the batch evaluation 3462 - Individual item failures are tracked but don't stop processing 3463 - Fetch failures are retried with exponential backoff 3464 - All scores are automatically flushed to Langfuse at the end 3465 - The resume mechanism uses timestamp-based filtering to avoid duplicates 3466 """ 3467 runner = BatchEvaluationRunner(self) 3468 3469 return cast( 3470 BatchEvaluationResult, 3471 run_async_safely( 3472 runner.run_async( 3473 scope=scope, 3474 mapper=mapper, 3475 evaluators=evaluators, 3476 filter=filter, 3477 fetch_batch_size=fetch_batch_size, 3478 fetch_trace_fields=fetch_trace_fields, 3479 max_items=max_items, 3480 max_concurrency=max_concurrency, 3481 composite_evaluator=composite_evaluator, 3482 metadata=metadata, 3483 _add_observation_scores_to_trace=_add_observation_scores_to_trace, 3484 _additional_trace_tags=_additional_trace_tags, 3485 max_retries=max_retries, 3486 verbose=verbose, 3487 resume_from=resume_from, 3488 ) 3489 ), 3490 )
Fetch traces or observations and run evaluations on each item.
This method provides a powerful way to evaluate existing data in Langfuse at scale. It fetches items based on filters, transforms them using a mapper function, runs evaluators on each item, and creates scores that are linked back to the original entities. This is ideal for:
- Running evaluations on production traces after deployment
- Backtesting new evaluation metrics on historical data
- Batch scoring of observations for quality monitoring
- Periodic evaluation runs on recent data
The method uses a streaming/pipeline approach to process items in batches, making it memory-efficient for large datasets. It includes comprehensive error handling, retry logic, and resume capability for long-running evaluations.
Arguments:
- scope: The type of items to evaluate. Must be one of:
- "traces": Evaluate complete traces with all their observations
- "observations": Evaluate individual observations (spans, generations, events)
- mapper: Function that transforms API response objects into evaluator inputs. Receives a trace/observation object and returns an EvaluatorInputs instance with input, output, expected_output, and metadata fields. Can be sync or async.
- evaluators: List of evaluation functions to run on each item. Each evaluator receives the mapped inputs and returns Evaluation object(s). Evaluator failures are logged but don't stop the batch evaluation.
- filter: Optional JSON filter string for querying items (same format as Langfuse API). Examples:
- '{"tags": ["production"]}'
- '{"user_id": "user123", "timestamp": {"operator": ">", "value": "2024-01-01"}}' Default: None (fetches all items).
- fetch_batch_size: Number of items to fetch per API call and hold in memory. Larger values may be faster but use more memory. Default: 50.
- fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'.
- max_items: Maximum total number of items to process. If None, processes all items matching the filter. Useful for testing or limiting evaluation runs. Default: None (process all).
- max_concurrency: Maximum number of items to evaluate concurrently. Controls parallelism and resource usage. Default: 5.
- composite_evaluator: Optional function that creates a composite score from item-level evaluations. Receives the original item and its evaluations, returns a single Evaluation. Useful for weighted averages or combined metrics. Default: None.
- metadata: Optional metadata dict to add to all created scores. Useful for tracking evaluation runs, versions, or other context. Default: None.
- max_retries: Maximum number of retry attempts for failed batch fetches. Uses exponential backoff (1s, 2s, 4s). Default: 3.
- verbose: If True, logs progress information to console. Useful for monitoring long-running evaluations. Default: False.
- resume_from: Optional resume token from a previous incomplete run. Allows continuing evaluation after interruption or failure. Default: None.
Returns:
BatchEvaluationResult containing: - total_items_fetched: Number of items fetched from API - total_items_processed: Number of items successfully evaluated - total_items_failed: Number of items that failed evaluation - total_scores_created: Scores created by item-level evaluators - total_composite_scores_created: Scores created by composite evaluator - total_evaluations_failed: Individual evaluator failures - evaluator_stats: Per-evaluator statistics (success rate, scores created) - resume_token: Token for resuming if incomplete (None if completed) - completed: True if all items processed - duration_seconds: Total execution time - failed_item_ids: IDs of items that failed - error_summary: Error types and counts - has_more_items: True if max_items reached but more exist
Raises:
- ValueError: If invalid scope is provided.
Examples:
Basic trace evaluation:
from langfuse import Langfuse, EvaluatorInputs, Evaluation client = Langfuse() # Define mapper to extract fields from traces def trace_mapper(trace): return EvaluatorInputs( input=trace.input, output=trace.output, expected_output=None, metadata={"trace_id": trace.id} ) # Define evaluator def length_evaluator(*, input, output, expected_output, metadata): return Evaluation( name="output_length", value=len(output) if output else 0 ) # Run batch evaluation result = client.run_batched_evaluation( scope="traces", mapper=trace_mapper, evaluators=[length_evaluator], filter='{"tags": ["production"]}', max_items=1000, verbose=True ) print(f"Processed {result.total_items_processed} traces") print(f"Created {result.total_scores_created} scores")Evaluation with composite scorer:
def accuracy_evaluator(*, input, output, expected_output, metadata): # ... evaluation logic return Evaluation(name="accuracy", value=0.85) def relevance_evaluator(*, input, output, expected_output, metadata): # ... evaluation logic return Evaluation(name="relevance", value=0.92) def composite_evaluator(*, item, evaluations): # Weighted average of evaluations weights = {"accuracy": 0.6, "relevance": 0.4} total = sum( e.value * weights.get(e.name, 0) for e in evaluations if isinstance(e.value, (int, float)) ) return Evaluation( name="composite_score", value=total, comment=f"Weighted average of {len(evaluations)} metrics" ) result = client.run_batched_evaluation( scope="traces", mapper=trace_mapper, evaluators=[accuracy_evaluator, relevance_evaluator], composite_evaluator=composite_evaluator, filter='{"user_id": "important_user"}', verbose=True )Handling incomplete runs with resume:
# Initial run that may fail or timeout result = client.run_batched_evaluation( scope="observations", mapper=obs_mapper, evaluators=[my_evaluator], max_items=10000, verbose=True ) # Check if incomplete if not result.completed and result.resume_token: print(f"Processed {result.resume_token.items_processed} items before interruption") # Resume from where it left off result = client.run_batched_evaluation( scope="observations", mapper=obs_mapper, evaluators=[my_evaluator], resume_from=result.resume_token, verbose=True ) print(f"Total items processed: {result.total_items_processed}")Monitoring evaluator performance:
result = client.run_batched_evaluation(...) for stats in result.evaluator_stats: success_rate = stats.successful_runs / stats.total_runs print(f"{stats.name}:") print(f" Success rate: {success_rate:.1%}") print(f" Scores created: {stats.total_scores_created}") if stats.failed_runs > 0: print(f" ⚠️ Failed {stats.failed_runs} times")
Note:
- Evaluator failures are logged but don't stop the batch evaluation
- Individual item failures are tracked but don't stop processing
- Fetch failures are retried with exponential backoff
- All scores are automatically flushed to Langfuse at the end
- The resume mechanism uses timestamp-based filtering to avoid duplicates
3492 def auth_check(self) -> bool: 3493 """Check if the provided credentials (public and secret key) are valid. 3494 3495 Raises: 3496 Exception: If no projects were found for the provided credentials. 3497 3498 Note: 3499 This method is blocking. It is discouraged to use it in production code. 3500 """ 3501 try: 3502 projects = self.api.projects.get() 3503 langfuse_logger.debug( 3504 "Auth check successful, found %s projects", len(projects.data) 3505 ) 3506 if len(projects.data) == 0: 3507 raise Exception( 3508 "Auth check failed, no project found for the keys provided." 3509 ) 3510 return True 3511 3512 except AttributeError as e: 3513 langfuse_logger.warning( 3514 "Auth check failed: Client not properly initialized. Error: %s", e 3515 ) 3516 return False 3517 3518 except Error as e: 3519 handle_fern_exception(e) 3520 raise e
Check if the provided credentials (public and secret key) are valid.
Raises:
- Exception: If no projects were found for the provided credentials.
Note:
This method is blocking. It is discouraged to use it in production code.
3522 def create_dataset( 3523 self, 3524 *, 3525 name: str, 3526 description: Optional[str] = None, 3527 metadata: Optional[Any] = None, 3528 input_schema: Optional[Any] = None, 3529 expected_output_schema: Optional[Any] = None, 3530 ) -> Dataset: 3531 """Create a dataset with the given name on Langfuse. 3532 3533 Args: 3534 name: Name of the dataset to create. 3535 description: Description of the dataset. Defaults to None. 3536 metadata: Additional metadata. Defaults to None. 3537 input_schema: JSON Schema for validating dataset item inputs. When set, all new items will be validated against this schema. 3538 expected_output_schema: JSON Schema for validating dataset item expected outputs. When set, all new items will be validated against this schema. 3539 3540 Returns: 3541 Dataset: The created dataset as returned by the Langfuse API. 3542 """ 3543 try: 3544 langfuse_logger.debug("Creating datasets %s", name) 3545 3546 result = self.api.datasets.create( 3547 name=name, 3548 description=description, 3549 metadata=metadata, 3550 input_schema=input_schema, 3551 expected_output_schema=expected_output_schema, 3552 ) 3553 3554 return cast(Dataset, result) 3555 3556 except Error as e: 3557 handle_fern_exception(e) 3558 raise e
Create a dataset with the given name on Langfuse.
Arguments:
- name: Name of the dataset to create.
- description: Description of the dataset. Defaults to None.
- metadata: Additional metadata. Defaults to None.
- input_schema: JSON Schema for validating dataset item inputs. When set, all new items will be validated against this schema.
- expected_output_schema: JSON Schema for validating dataset item expected outputs. When set, all new items will be validated against this schema.
Returns:
Dataset: The created dataset as returned by the Langfuse API.
3560 def create_dataset_item( 3561 self, 3562 *, 3563 dataset_name: str, 3564 input: Optional[Any] = None, 3565 expected_output: Optional[Any] = None, 3566 metadata: Optional[Any] = None, 3567 source_trace_id: Optional[str] = None, 3568 source_observation_id: Optional[str] = None, 3569 status: Optional[DatasetStatus] = None, 3570 id: Optional[str] = None, 3571 ) -> DatasetItem: 3572 """Create a dataset item. 3573 3574 Upserts if an item with id already exists. 3575 3576 Args: 3577 dataset_name: Name of the dataset in which the dataset item should be created. 3578 input: Input data. Defaults to None. Can contain any dict, list or scalar. 3579 expected_output: Expected output data. Defaults to None. Can contain any dict, list or scalar. 3580 metadata: Additional metadata. Defaults to None. Can contain any dict, list or scalar. 3581 source_trace_id: Id of the source trace. Defaults to None. 3582 source_observation_id: Id of the source observation. Defaults to None. 3583 status: Status of the dataset item. Defaults to ACTIVE for newly created items. 3584 id: Id of the dataset item. Defaults to None. Provide your own id if you want to dedupe dataset items. Id needs to be globally unique and cannot be reused across datasets. 3585 3586 Returns: 3587 DatasetItem: The created dataset item as returned by the Langfuse API. 3588 3589 Example: 3590 ```python 3591 from langfuse import Langfuse 3592 3593 langfuse = Langfuse() 3594 3595 # Uploading items to the Langfuse dataset named "capital_cities" 3596 langfuse.create_dataset_item( 3597 dataset_name="capital_cities", 3598 input={"input": {"country": "Italy"}}, 3599 expected_output={"expected_output": "Rome"}, 3600 metadata={"foo": "bar"} 3601 ) 3602 ``` 3603 """ 3604 try: 3605 langfuse_logger.debug("Creating dataset item for dataset %s", dataset_name) 3606 3607 # Media uploads must reference the (dataset, item) they belong to, and 3608 # the item need not exist yet — so settle on the item id up front and 3609 # reuse it for the create call below. 3610 item_id = id if id is not None else str(uuid.uuid4()) 3611 3612 # Single pass per field: swap each LangfuseMedia for its reference 3613 # string (derived from content, not the upload) and collect the media 3614 # still to upload, deduped by media id and tagged with its field. 3615 pending_media: Dict[str, Tuple[LangfuseMedia, str]] = {} 3616 input = self._process_dataset_item_media( 3617 data=input, 3618 pending_media=pending_media, 3619 field=DatasetItemMediaReferenceField.INPUT.value, 3620 ) 3621 expected_output = self._process_dataset_item_media( 3622 data=expected_output, 3623 pending_media=pending_media, 3624 field=DatasetItemMediaReferenceField.EXPECTED_OUTPUT.value, 3625 ) 3626 metadata = self._process_dataset_item_media( 3627 data=metadata, 3628 pending_media=pending_media, 3629 field=DatasetItemMediaReferenceField.METADATA.value, 3630 ) 3631 3632 # The upload needs the dataset id, but the create API only takes the 3633 # name. Resolve it once, and only when there is actually media to 3634 # upload — a plain item pays no extra datasets.get round-trip. 3635 if pending_media: 3636 assert self._resources is not None 3637 dataset_id = self.api.datasets.get(self._url_encode(dataset_name)).id 3638 for media, field in pending_media.values(): 3639 self._resources._media_manager._upload_media_sync( 3640 media=media, 3641 dataset_id=dataset_id, 3642 dataset_item_id=item_id, 3643 field=field, 3644 ) 3645 3646 result = self.api.dataset_items.create( 3647 dataset_name=dataset_name, 3648 input=input, 3649 expected_output=expected_output, 3650 metadata=metadata, 3651 source_trace_id=source_trace_id, 3652 source_observation_id=source_observation_id, 3653 status=status, 3654 id=item_id, 3655 ) 3656 3657 return cast(DatasetItem, result) 3658 except Error as e: 3659 handle_fern_exception(e) 3660 raise e
Create a dataset item.
Upserts if an item with id already exists.
Arguments:
- dataset_name: Name of the dataset in which the dataset item should be created.
- input: Input data. Defaults to None. Can contain any dict, list or scalar.
- expected_output: Expected output data. Defaults to None. Can contain any dict, list or scalar.
- metadata: Additional metadata. Defaults to None. Can contain any dict, list or scalar.
- source_trace_id: Id of the source trace. Defaults to None.
- source_observation_id: Id of the source observation. Defaults to None.
- status: Status of the dataset item. Defaults to ACTIVE for newly created items.
- id: Id of the dataset item. Defaults to None. Provide your own id if you want to dedupe dataset items. Id needs to be globally unique and cannot be reused across datasets.
Returns:
DatasetItem: The created dataset item as returned by the Langfuse API.
Example:
from langfuse import Langfuse langfuse = Langfuse() # Uploading items to the Langfuse dataset named "capital_cities" langfuse.create_dataset_item( dataset_name="capital_cities", input={"input": {"country": "Italy"}}, expected_output={"expected_output": "Rome"}, metadata={"foo": "bar"} )
3787 def resolve_media_references( 3788 self, 3789 *, 3790 obj: Any, 3791 resolve_with: Literal["base64_data_uri"], 3792 max_depth: int = 10, 3793 content_fetch_timeout_seconds: int = 5, 3794 ) -> Any: 3795 """Replace media reference strings in an object with base64 data URIs. 3796 3797 This method recursively traverses an object (up to max_depth) looking for media reference strings 3798 in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using 3799 the provided Langfuse client and replaces the reference string with a base64 data URI. 3800 3801 If fetching media content fails for a reference string, a warning is logged and the reference 3802 string is left unchanged. 3803 3804 Args: 3805 obj: The object to process. Can be a primitive value, array, or nested object. 3806 If the object has a __dict__ attribute, a dict will be returned instead of the original object type. 3807 resolve_with: The representation of the media content to replace the media reference string with. 3808 Currently only "base64_data_uri" is supported. 3809 max_depth: int: The maximum depth to traverse the object. Default is 10. 3810 content_fetch_timeout_seconds: int: The timeout in seconds for fetching media content. Default is 5. 3811 3812 Returns: 3813 A deep copy of the input object with all media references replaced with base64 data URIs where possible. 3814 If the input object has a __dict__ attribute, a dict will be returned instead of the original object type. 3815 3816 Example: 3817 obj = { 3818 "image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", 3819 "nested": { 3820 "pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" 3821 } 3822 } 3823 3824 result = await LangfuseMedia.resolve_media_references(obj, langfuse_client) 3825 3826 # Result: 3827 # { 3828 # "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", 3829 # "nested": { 3830 # "pdf": "data:application/pdf;base64,JVBERi0xLjcK..." 3831 # } 3832 # } 3833 """ 3834 return LangfuseMedia.resolve_media_references( 3835 langfuse_client=self, 3836 obj=obj, 3837 resolve_with=resolve_with, 3838 max_depth=max_depth, 3839 content_fetch_timeout_seconds=content_fetch_timeout_seconds, 3840 )
Replace media reference strings in an object with base64 data URIs.
This method recursively traverses an object (up to max_depth) looking for media reference strings in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using the provided Langfuse client and replaces the reference string with a base64 data URI.
If fetching media content fails for a reference string, a warning is logged and the reference string is left unchanged.
Arguments:
- obj: The object to process. Can be a primitive value, array, or nested object. If the object has a __dict__ attribute, a dict will be returned instead of the original object type.
- resolve_with: The representation of the media content to replace the media reference string with. Currently only "base64_data_uri" is supported.
- max_depth: int: The maximum depth to traverse the object. Default is 10.
- content_fetch_timeout_seconds: int: The timeout in seconds for fetching media content. Default is 5.
Returns:
A deep copy of the input object with all media references replaced with base64 data URIs where possible. If the input object has a __dict__ attribute, a dict will be returned instead of the original object type.
Example:
obj = { "image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", "nested": { "pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" } }
result = await LangfuseMedia.resolve_media_references(obj, langfuse_client)
Result:
{
"image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"nested": {
"pdf": "data:application/pdf;base64,JVBERi0xLjcK..."
}
}
3870 def get_prompt( 3871 self, 3872 name: str, 3873 *, 3874 version: Optional[int] = None, 3875 label: Optional[str] = None, 3876 type: Literal["chat", "text"] = "text", 3877 cache_ttl_seconds: Optional[int] = None, 3878 fallback: Union[Optional[List[ChatMessageDict]], Optional[str]] = None, 3879 max_retries: Optional[int] = None, 3880 fetch_timeout_seconds: Optional[int] = None, 3881 ) -> PromptClient: 3882 """Get a prompt. 3883 3884 This method attempts to fetch the requested prompt from the local cache. If the prompt is not found 3885 in the cache or if the cached prompt has expired, it will try to fetch the prompt from the server again 3886 and update the cache. If fetching the new prompt fails, and there is an expired prompt in the cache, it will 3887 return the expired prompt as a fallback. 3888 3889 Args: 3890 name (str): The name of the prompt to retrieve. 3891 3892 Keyword Args: 3893 version (Optional[int]): The version of the prompt to retrieve. If no label and version is specified, the `production` label is returned. Specify either version or label, not both. 3894 label: Optional[str]: The label of the prompt to retrieve. If no label and version is specified, the `production` label is returned. Specify either version or label, not both. 3895 cache_ttl_seconds: Optional[int]: Time-to-live in seconds for caching the prompt. Must be specified as a 3896 keyword argument. If not set, defaults to 60 seconds. Disables caching if set to 0. 3897 type: Literal["chat", "text"]: The type of the prompt to retrieve. Defaults to "text". 3898 fallback: Union[Optional[List[ChatMessageDict]], Optional[str]]: The prompt string to return if fetching the prompt fails. Important on the first call where no cached prompt is available. Follows Langfuse prompt formatting with double curly braces for variables. Defaults to None. 3899 max_retries: Optional[int]: The maximum number of retries in case of API/network errors. Defaults to 2. The maximum value is 4. Retries have an exponential backoff with a maximum delay of 10 seconds. 3900 fetch_timeout_seconds: Optional[int]: The timeout in milliseconds for fetching the prompt. Defaults to the default timeout set on the SDK, which is 5 seconds per default. 3901 3902 Returns: 3903 The prompt object retrieved from the cache or directly fetched if not cached or expired of type 3904 - TextPromptClient, if type argument is 'text'. 3905 - ChatPromptClient, if type argument is 'chat'. 3906 3907 Raises: 3908 Exception: Propagates any exceptions raised during the fetching of a new prompt, unless there is an 3909 expired prompt in the cache, in which case it logs a warning and returns the expired prompt. 3910 """ 3911 if self._resources is None: 3912 raise Error( 3913 "SDK is not correctly initialized. Check the init logs for more details." 3914 ) 3915 if version is not None and label is not None: 3916 raise ValueError("Cannot specify both version and label at the same time.") 3917 3918 if not name: 3919 raise ValueError("Prompt name cannot be empty.") 3920 3921 cache_key = PromptCache.generate_cache_key(name, version=version, label=label) 3922 bounded_max_retries = self._get_bounded_max_retries( 3923 max_retries, default_max_retries=2, max_retries_upper_bound=4 3924 ) 3925 3926 langfuse_logger.debug("Getting prompt '%s'", cache_key) 3927 cached_prompt = self._resources.prompt_cache.get(cache_key) 3928 3929 if cached_prompt is None or cache_ttl_seconds == 0: 3930 langfuse_logger.debug( 3931 "Prompt '%s' not found in cache or caching disabled.", cache_key 3932 ) 3933 try: 3934 return self._fetch_prompt_and_update_cache( 3935 name, 3936 version=version, 3937 label=label, 3938 ttl_seconds=cache_ttl_seconds, 3939 max_retries=bounded_max_retries, 3940 fetch_timeout_seconds=fetch_timeout_seconds, 3941 ) 3942 except Exception as e: 3943 if fallback: 3944 langfuse_logger.warning( 3945 "Returning fallback prompt for '%s' due to fetch error: %s", 3946 cache_key, 3947 e, 3948 ) 3949 3950 fallback_client_args: Dict[str, Any] = { 3951 "name": name, 3952 "prompt": fallback, 3953 "type": type, 3954 "version": version or 0, 3955 "config": {}, 3956 "labels": [label] if label else [], 3957 "tags": [], 3958 } 3959 3960 if type == "text": 3961 return TextPromptClient( 3962 prompt=Prompt_Text(**fallback_client_args), 3963 is_fallback=True, 3964 ) 3965 3966 if type == "chat": 3967 return ChatPromptClient( 3968 prompt=Prompt_Chat(**fallback_client_args), 3969 is_fallback=True, 3970 ) 3971 3972 raise e 3973 3974 if cached_prompt.is_expired(): 3975 langfuse_logger.debug("Stale prompt '%s' found in cache.", cache_key) 3976 try: 3977 # refresh prompt in background thread, refresh_prompt deduplicates tasks 3978 langfuse_logger.debug( 3979 "Refreshing prompt '%s' in background.", cache_key 3980 ) 3981 3982 def refresh_task() -> None: 3983 self._fetch_prompt_and_update_cache( 3984 name, 3985 version=version, 3986 label=label, 3987 ttl_seconds=cache_ttl_seconds, 3988 max_retries=bounded_max_retries, 3989 fetch_timeout_seconds=fetch_timeout_seconds, 3990 ) 3991 3992 self._resources.prompt_cache.add_refresh_prompt_task_if_current( 3993 cache_key, 3994 cached_prompt, 3995 refresh_task, 3996 ) 3997 langfuse_logger.debug( 3998 "Returning stale prompt '%s' from cache.", cache_key 3999 ) 4000 # return stale prompt 4001 return cached_prompt.value 4002 4003 except Exception as e: 4004 langfuse_logger.warning( 4005 "Error when refreshing cached prompt '%s', returning cached version. " 4006 "Error: %s", 4007 cache_key, 4008 e, 4009 ) 4010 # creation of refresh prompt task failed, return stale prompt 4011 return cached_prompt.value 4012 4013 return cached_prompt.value
Get a prompt.
This method attempts to fetch the requested prompt from the local cache. If the prompt is not found in the cache or if the cached prompt has expired, it will try to fetch the prompt from the server again and update the cache. If fetching the new prompt fails, and there is an expired prompt in the cache, it will return the expired prompt as a fallback.
Arguments:
- name (str): The name of the prompt to retrieve.
Keyword Args:
- version (Optional[int]): The version of the prompt to retrieve. If no label and version is specified, the
productionlabel is returned. Specify either version or label, not both. - label: Optional[str]: The label of the prompt to retrieve. If no label and version is specified, the
productionlabel is returned. Specify either version or label, not both. - cache_ttl_seconds: Optional[int]: Time-to-live in seconds for caching the prompt. Must be specified as a
- keyword argument. If not set, defaults to 60 seconds. Disables caching if set to 0.
- type: Literal["chat", "text"]: The type of the prompt to retrieve. Defaults to "text".
- fallback: Union[Optional[List[ChatMessageDict]], Optional[str]]: The prompt string to return if fetching the prompt fails. Important on the first call where no cached prompt is available. Follows Langfuse prompt formatting with double curly braces for variables. Defaults to None.
- max_retries: Optional[int]: The maximum number of retries in case of API/network errors. Defaults to 2. The maximum value is 4. Retries have an exponential backoff with a maximum delay of 10 seconds.
- fetch_timeout_seconds: Optional[int]: The timeout in milliseconds for fetching the prompt. Defaults to the default timeout set on the SDK, which is 5 seconds per default.
Returns:
The prompt object retrieved from the cache or directly fetched if not cached or expired of type
- TextPromptClient, if type argument is 'text'.
- ChatPromptClient, if type argument is 'chat'.
Raises:
- Exception: Propagates any exceptions raised during the fetching of a new prompt, unless there is an
- expired prompt in the cache, in which case it logs a warning and returns the expired prompt.
4115 def create_prompt( 4116 self, 4117 *, 4118 name: str, 4119 prompt: Union[ 4120 str, List[Union[ChatMessageDict, ChatMessageWithPlaceholdersDict]] 4121 ], 4122 labels: List[str] = [], 4123 tags: Optional[List[str]] = None, 4124 type: Optional[Literal["chat", "text"]] = "text", 4125 config: Optional[Any] = None, 4126 commit_message: Optional[str] = None, 4127 ) -> PromptClient: 4128 """Create a new prompt in Langfuse. 4129 4130 Keyword Args: 4131 name : The name of the prompt to be created. 4132 prompt : The content of the prompt to be created. 4133 is_active [DEPRECATED] : A flag indicating whether the prompt is active or not. This is deprecated and will be removed in a future release. Please use the 'production' label instead. 4134 labels: The labels of the prompt. Defaults to None. To create a default-served prompt, add the 'production' label. 4135 tags: The tags of the prompt. Defaults to None. Will be applied to all versions of the prompt. 4136 config: Additional structured data to be saved with the prompt. Defaults to None. 4137 type: The type of the prompt to be created. "chat" vs. "text". Defaults to "text". 4138 commit_message: Optional string describing the change. 4139 4140 Returns: 4141 TextPromptClient: The prompt if type argument is 'text'. 4142 ChatPromptClient: The prompt if type argument is 'chat'. 4143 """ 4144 try: 4145 langfuse_logger.debug("Creating prompt name=%r, labels=%r", name, labels) 4146 4147 if type == "chat": 4148 if not isinstance(prompt, list): 4149 raise ValueError( 4150 "For 'chat' type, 'prompt' must be a list of chat messages with role and content attributes." 4151 ) 4152 request: Union[CreateChatPromptRequest, CreateTextPromptRequest] = ( 4153 CreateChatPromptRequest( 4154 name=name, 4155 prompt=cast(Any, prompt), 4156 labels=labels, 4157 tags=tags, 4158 config=config or {}, 4159 commit_message=commit_message, 4160 type=CreateChatPromptType.CHAT, 4161 ) 4162 ) 4163 server_prompt = self.api.prompts.create(request=request) 4164 4165 if self._resources is not None: 4166 self._resources.prompt_cache.invalidate(name) 4167 4168 return ChatPromptClient(prompt=cast(Prompt_Chat, server_prompt)) 4169 4170 if not isinstance(prompt, str): 4171 raise ValueError("For 'text' type, 'prompt' must be a string.") 4172 4173 request = CreateTextPromptRequest( 4174 name=name, 4175 prompt=prompt, 4176 labels=labels, 4177 tags=tags, 4178 config=config or {}, 4179 commit_message=commit_message, 4180 ) 4181 4182 server_prompt = self.api.prompts.create(request=request) 4183 4184 if self._resources is not None: 4185 self._resources.prompt_cache.invalidate(name) 4186 4187 return TextPromptClient(prompt=cast(Prompt_Text, server_prompt)) 4188 4189 except Error as e: 4190 handle_fern_exception(e) 4191 raise e
Create a new prompt in Langfuse.
Keyword Args:
- name : The name of the prompt to be created.
- prompt : The content of the prompt to be created.
- is_active [DEPRECATED] : A flag indicating whether the prompt is active or not. This is deprecated and will be removed in a future release. Please use the 'production' label instead.
- labels: The labels of the prompt. Defaults to None. To create a default-served prompt, add the 'production' label.
- tags: The tags of the prompt. Defaults to None. Will be applied to all versions of the prompt.
- config: Additional structured data to be saved with the prompt. Defaults to None.
- type: The type of the prompt to be created. "chat" vs. "text". Defaults to "text".
- commit_message: Optional string describing the change.
Returns:
TextPromptClient: The prompt if type argument is 'text'. ChatPromptClient: The prompt if type argument is 'chat'.
4193 def update_prompt( 4194 self, 4195 *, 4196 name: str, 4197 version: int, 4198 new_labels: List[str] = [], 4199 ) -> Any: 4200 """Update an existing prompt version in Langfuse. The Langfuse SDK prompt cache is invalidated for all prompts witht he specified name. 4201 4202 Args: 4203 name (str): The name of the prompt to update. 4204 version (int): The version number of the prompt to update. 4205 new_labels (List[str], optional): New labels to assign to the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. Defaults to []. 4206 4207 Returns: 4208 Prompt: The updated prompt from the Langfuse API. 4209 4210 """ 4211 updated_prompt = self.api.prompt_version.update( 4212 name=self._url_encode(name), 4213 version=version, 4214 new_labels=new_labels, 4215 ) 4216 4217 if self._resources is not None: 4218 self._resources.prompt_cache.invalidate(name) 4219 4220 return updated_prompt
Update an existing prompt version in Langfuse. The Langfuse SDK prompt cache is invalidated for all prompts witht he specified name.
Arguments:
- name (str): The name of the prompt to update.
- version (int): The version number of the prompt to update.
- new_labels (List[str], optional): New labels to assign to the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. Defaults to [].
Returns:
Prompt: The updated prompt from the Langfuse API.
4235 def clear_prompt_cache(self) -> None: 4236 """Clear the entire prompt cache, removing all cached prompts. 4237 4238 This method is useful when you want to force a complete refresh of all 4239 cached prompts, for example after major updates or when you need to 4240 ensure the latest versions are fetched from the server. 4241 """ 4242 if self._resources is not None: 4243 self._resources.prompt_cache.clear()
Clear the entire prompt cache, removing all cached prompts.
This method is useful when you want to force a complete refresh of all cached prompts, for example after major updates or when you need to ensure the latest versions are fetched from the server.
99class LangfuseMedia: 100 """A class for wrapping media objects for upload to Langfuse. 101 102 This class handles the preparation and formatting of media content for Langfuse, 103 supporting both base64 data URIs and raw content bytes. 104 105 Args: 106 obj (Optional[object]): The source object to be wrapped. Can be accessed via the `obj` attribute. 107 base64_data_uri (Optional[str]): A base64-encoded data URI containing the media content 108 and content type (e.g., "data:image/jpeg;base64,/9j/4AAQ..."). 109 content_type (Optional[str]): The MIME type of the media content when providing raw bytes. 110 content_bytes (Optional[bytes]): Raw bytes of the media content. 111 file_path (Optional[str]): The path to the file containing the media content. For relative paths, 112 the current working directory is used. 113 114 Raises: 115 ValueError: If neither base64_data_uri or the combination of content_bytes 116 and content_type is provided. 117 """ 118 119 obj: object 120 121 _content_bytes: Optional[bytes] 122 _content_type: Optional[MediaContentType] 123 _source: Optional[str] 124 _media_id: Optional[str] 125 126 def __init__( 127 self, 128 *, 129 obj: Optional[object] = None, 130 base64_data_uri: Optional[str] = None, 131 content_type: Optional[MediaContentType] = None, 132 content_bytes: Optional[bytes] = None, 133 file_path: Optional[str] = None, 134 ): 135 """Initialize a LangfuseMedia object. 136 137 Args: 138 obj: The object to wrap. 139 140 base64_data_uri: A base64-encoded data URI containing the media content 141 and content type (e.g., "data:image/jpeg;base64,/9j/4AAQ..."). 142 content_type: The MIME type of the media content when providing raw bytes or reading from a file. 143 content_bytes: Raw bytes of the media content. 144 file_path: The path to the file containing the media content. For relative paths, 145 the current working directory is used. 146 """ 147 self.obj = obj 148 149 if base64_data_uri is not None: 150 parsed_data = self._parse_base64_data_uri(base64_data_uri) 151 self._content_bytes, self._content_type = parsed_data 152 self._source = "base64_data_uri" 153 154 elif content_bytes is not None and content_type is not None: 155 self._content_type = content_type 156 self._content_bytes = content_bytes 157 self._source = "bytes" 158 elif ( 159 file_path is not None 160 and content_type is not None 161 and os.path.exists(file_path) 162 ): 163 self._content_bytes = self._read_file(file_path) 164 self._content_type = content_type if self._content_bytes else None 165 self._source = "file" if self._content_bytes else None 166 else: 167 logger.error( 168 "base64_data_uri, or content_bytes and content_type, or file_path must be provided to LangfuseMedia" 169 ) 170 171 self._content_bytes = None 172 self._content_type = None 173 self._source = None 174 175 self._media_id = self._get_media_id() 176 177 def _read_file(self, file_path: str) -> Optional[bytes]: 178 try: 179 with open(file_path, "rb") as file: 180 return file.read() 181 except Exception as e: 182 logger.error("Error reading file at path %s", file_path, exc_info=e) 183 184 return None 185 186 def _get_media_id(self) -> Optional[str]: 187 content_hash = self._content_sha256_hash 188 189 if content_hash is None: 190 return None 191 192 # Convert hash to base64Url 193 url_safe_content_hash = content_hash.replace("+", "-").replace("/", "_") 194 195 return url_safe_content_hash[:22] 196 197 @property 198 def _content_length(self) -> Optional[int]: 199 return len(self._content_bytes) if self._content_bytes else None 200 201 @property 202 def _content_sha256_hash(self) -> Optional[str]: 203 if self._content_bytes is None: 204 return None 205 206 sha256_hash_bytes = hashlib.sha256(self._content_bytes).digest() 207 208 return base64.b64encode(sha256_hash_bytes).decode("utf-8") 209 210 @property 211 def _reference_string(self) -> Optional[str]: 212 if self._content_type is None or self._source is None or self._media_id is None: 213 return None 214 215 return f"@@@langfuseMedia:type={self._content_type}|id={self._media_id}|source={self._source}@@@" 216 217 @staticmethod 218 def parse_reference_string(reference_string: str) -> ParsedMediaReference: 219 """Parse a media reference string into a ParsedMediaReference. 220 221 Example reference string: 222 "@@@langfuseMedia:type=image/jpeg|id=some-uuid|source=base64_data_uri@@@" 223 224 Args: 225 reference_string: The reference string to parse. 226 227 Returns: 228 A TypedDict with the media_id, source, and content_type. 229 230 Raises: 231 ValueError: If the reference string is empty or not a string. 232 ValueError: If the reference string does not start with "@@@langfuseMedia:type=". 233 ValueError: If the reference string does not end with "@@@". 234 ValueError: If the reference string is missing required fields. 235 """ 236 if not reference_string: 237 raise ValueError("Reference string is empty") 238 239 if not isinstance(reference_string, str): 240 raise ValueError("Reference string is not a string") 241 242 if not reference_string.startswith("@@@langfuseMedia:type="): 243 raise ValueError( 244 "Reference string does not start with '@@@langfuseMedia:type='" 245 ) 246 247 if not reference_string.endswith("@@@"): 248 raise ValueError("Reference string does not end with '@@@'") 249 250 content = reference_string[len("@@@langfuseMedia:") :].rstrip("@@@") 251 252 # Split into key-value pairs 253 pairs = content.split("|") 254 parsed_data = {} 255 256 for pair in pairs: 257 key, value = pair.split("=", 1) 258 parsed_data[key] = value 259 260 # Verify all required fields are present 261 if not all(key in parsed_data for key in ["type", "id", "source"]): 262 raise ValueError("Missing required fields in reference string") 263 264 return ParsedMediaReference( 265 media_id=parsed_data["id"], 266 source=parsed_data["source"], 267 content_type=cast(MediaContentType, parsed_data["type"]), 268 ) 269 270 def _parse_base64_data_uri( 271 self, data: str 272 ) -> Tuple[Optional[bytes], Optional[MediaContentType]]: 273 # Example data URI: data:image/jpeg;base64,/9j/4AAQ... 274 try: 275 if not data or not isinstance(data, str): 276 raise ValueError("Data URI is not a string") 277 278 if not data.startswith("data:"): 279 raise ValueError("Data URI does not start with 'data:'") 280 281 header, actual_data = data[5:].split(",", 1) 282 if not header or not actual_data: 283 raise ValueError("Invalid URI") 284 285 # Split header into parts and check for base64 286 header_parts = header.split(";") 287 if "base64" not in header_parts: 288 raise ValueError("Data is not base64 encoded") 289 290 # Content type is the first part 291 content_type = header_parts[0] 292 if not content_type: 293 raise ValueError("Content type is empty") 294 295 translation_table = str.maketrans("-_", "+/") 296 decoded_data = base64.b64decode(actual_data.translate(translation_table)) 297 298 return decoded_data, cast(MediaContentType, content_type) 299 300 except Exception as e: 301 logger.error("Error parsing base64 data URI", exc_info=e) 302 303 return None, None 304 305 @staticmethod 306 def resolve_media_references( 307 *, 308 obj: T, 309 langfuse_client: "Langfuse", 310 resolve_with: Literal["base64_data_uri"], 311 max_depth: int = 10, 312 content_fetch_timeout_seconds: int = 10, 313 ) -> T: 314 """Replace media reference strings in an object with base64 data URIs. 315 316 This method recursively traverses an object (up to max_depth) looking for media reference strings 317 in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using 318 the provided Langfuse client and replaces the reference string with a base64 data URI. 319 320 If fetching media content fails for a reference string, a warning is logged and the reference 321 string is left unchanged. 322 323 Args: 324 obj: The object to process. Can be a primitive value, array, or nested object. 325 If the object has a __dict__ attribute, a dict will be returned instead of the original object type. 326 langfuse_client: Langfuse client instance used to fetch media content. 327 resolve_with: The representation of the media content to replace the media reference string with. 328 Currently only "base64_data_uri" is supported. 329 max_depth: Optional. Default is 10. The maximum depth to traverse the object. 330 331 Returns: 332 A deep copy of the input object with all media references replaced with base64 data URIs where possible. 333 If the input object has a __dict__ attribute, a dict will be returned instead of the original object type. 334 335 Example: 336 obj = { 337 "image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", 338 "nested": { 339 "pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" 340 } 341 } 342 343 result = await LangfuseMedia.resolve_media_references(obj, langfuse_client) 344 345 # Result: 346 # { 347 # "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", 348 # "nested": { 349 # "pdf": "data:application/pdf;base64,JVBERi0xLjcK..." 350 # } 351 # } 352 """ 353 354 def traverse(obj: Any, depth: int) -> Any: 355 if depth > max_depth: 356 return obj 357 358 # Handle string 359 if isinstance(obj, str): 360 regex = r"@@@langfuseMedia:.+?@@@" 361 reference_string_matches = re.findall(regex, obj) 362 if len(reference_string_matches) == 0: 363 return obj 364 365 result = obj 366 reference_string_to_media_content = {} 367 httpx_client = ( 368 langfuse_client._resources.httpx_client 369 if langfuse_client._resources is not None 370 else None 371 ) 372 373 for reference_string in reference_string_matches: 374 try: 375 parsed_media_reference = LangfuseMedia.parse_reference_string( 376 reference_string 377 ) 378 media_data = langfuse_client.api.media.get( 379 parsed_media_reference["media_id"] 380 ) 381 media_content = ( 382 httpx_client.get( 383 media_data.url, 384 timeout=content_fetch_timeout_seconds, 385 ) 386 if httpx_client is not None 387 else httpx.get( 388 media_data.url, timeout=content_fetch_timeout_seconds 389 ) 390 ) 391 media_content.raise_for_status() 392 393 base64_media_content = base64.b64encode( 394 media_content.content 395 ).decode() 396 base64_data_uri = f"data:{media_data.content_type};base64,{base64_media_content}" 397 398 reference_string_to_media_content[reference_string] = ( 399 base64_data_uri 400 ) 401 except Exception as e: 402 logger.warning( 403 "Error fetching media content for reference string %s: %s", 404 reference_string, 405 e, 406 ) 407 # Do not replace the reference string if there's an error 408 continue 409 410 for ( 411 ref_str, 412 media_content_str, 413 ) in reference_string_to_media_content.items(): 414 result = result.replace(ref_str, media_content_str) 415 416 return result 417 418 # Handle arrays 419 if isinstance(obj, list): 420 return [traverse(item, depth + 1) for item in obj] 421 422 # Handle dictionaries 423 if isinstance(obj, dict): 424 return {key: traverse(value, depth + 1) for key, value in obj.items()} 425 426 # Handle objects: 427 if hasattr(obj, "__dict__"): 428 return { 429 key: traverse(value, depth + 1) 430 for key, value in obj.__dict__.items() 431 } 432 433 return obj 434 435 return cast(T, traverse(obj, 0))
A class for wrapping media objects for upload to Langfuse.
This class handles the preparation and formatting of media content for Langfuse, supporting both base64 data URIs and raw content bytes.
Arguments:
- obj (Optional[object]): The source object to be wrapped. Can be accessed via the
objattribute. - base64_data_uri (Optional[str]): A base64-encoded data URI containing the media content and content type (e.g., "data:image/jpeg;base64,/9j/4AAQ...").
- content_type (Optional[str]): The MIME type of the media content when providing raw bytes.
- content_bytes (Optional[bytes]): Raw bytes of the media content.
- file_path (Optional[str]): The path to the file containing the media content. For relative paths, the current working directory is used.
Raises:
- ValueError: If neither base64_data_uri or the combination of content_bytes and content_type is provided.
126 def __init__( 127 self, 128 *, 129 obj: Optional[object] = None, 130 base64_data_uri: Optional[str] = None, 131 content_type: Optional[MediaContentType] = None, 132 content_bytes: Optional[bytes] = None, 133 file_path: Optional[str] = None, 134 ): 135 """Initialize a LangfuseMedia object. 136 137 Args: 138 obj: The object to wrap. 139 140 base64_data_uri: A base64-encoded data URI containing the media content 141 and content type (e.g., "data:image/jpeg;base64,/9j/4AAQ..."). 142 content_type: The MIME type of the media content when providing raw bytes or reading from a file. 143 content_bytes: Raw bytes of the media content. 144 file_path: The path to the file containing the media content. For relative paths, 145 the current working directory is used. 146 """ 147 self.obj = obj 148 149 if base64_data_uri is not None: 150 parsed_data = self._parse_base64_data_uri(base64_data_uri) 151 self._content_bytes, self._content_type = parsed_data 152 self._source = "base64_data_uri" 153 154 elif content_bytes is not None and content_type is not None: 155 self._content_type = content_type 156 self._content_bytes = content_bytes 157 self._source = "bytes" 158 elif ( 159 file_path is not None 160 and content_type is not None 161 and os.path.exists(file_path) 162 ): 163 self._content_bytes = self._read_file(file_path) 164 self._content_type = content_type if self._content_bytes else None 165 self._source = "file" if self._content_bytes else None 166 else: 167 logger.error( 168 "base64_data_uri, or content_bytes and content_type, or file_path must be provided to LangfuseMedia" 169 ) 170 171 self._content_bytes = None 172 self._content_type = None 173 self._source = None 174 175 self._media_id = self._get_media_id()
Initialize a LangfuseMedia object.
Arguments:
- obj: The object to wrap.
- base64_data_uri: A base64-encoded data URI containing the media content and content type (e.g., "data:image/jpeg;base64,/9j/4AAQ...").
- content_type: The MIME type of the media content when providing raw bytes or reading from a file.
- content_bytes: Raw bytes of the media content.
- file_path: The path to the file containing the media content. For relative paths, the current working directory is used.
217 @staticmethod 218 def parse_reference_string(reference_string: str) -> ParsedMediaReference: 219 """Parse a media reference string into a ParsedMediaReference. 220 221 Example reference string: 222 "@@@langfuseMedia:type=image/jpeg|id=some-uuid|source=base64_data_uri@@@" 223 224 Args: 225 reference_string: The reference string to parse. 226 227 Returns: 228 A TypedDict with the media_id, source, and content_type. 229 230 Raises: 231 ValueError: If the reference string is empty or not a string. 232 ValueError: If the reference string does not start with "@@@langfuseMedia:type=". 233 ValueError: If the reference string does not end with "@@@". 234 ValueError: If the reference string is missing required fields. 235 """ 236 if not reference_string: 237 raise ValueError("Reference string is empty") 238 239 if not isinstance(reference_string, str): 240 raise ValueError("Reference string is not a string") 241 242 if not reference_string.startswith("@@@langfuseMedia:type="): 243 raise ValueError( 244 "Reference string does not start with '@@@langfuseMedia:type='" 245 ) 246 247 if not reference_string.endswith("@@@"): 248 raise ValueError("Reference string does not end with '@@@'") 249 250 content = reference_string[len("@@@langfuseMedia:") :].rstrip("@@@") 251 252 # Split into key-value pairs 253 pairs = content.split("|") 254 parsed_data = {} 255 256 for pair in pairs: 257 key, value = pair.split("=", 1) 258 parsed_data[key] = value 259 260 # Verify all required fields are present 261 if not all(key in parsed_data for key in ["type", "id", "source"]): 262 raise ValueError("Missing required fields in reference string") 263 264 return ParsedMediaReference( 265 media_id=parsed_data["id"], 266 source=parsed_data["source"], 267 content_type=cast(MediaContentType, parsed_data["type"]), 268 )
Parse a media reference string into a ParsedMediaReference.
Example reference string:
"@@@langfuseMedia:type=image/jpeg|id=some-uuid|source=base64_data_uri@@@"
Arguments:
- reference_string: The reference string to parse.
Returns:
A TypedDict with the media_id, source, and content_type.
Raises:
- ValueError: If the reference string is empty or not a string.
- ValueError: If the reference string does not start with "@@@langfuseMedia:type=".
- ValueError: If the reference string does not end with "@@@".
- ValueError: If the reference string is missing required fields.
305 @staticmethod 306 def resolve_media_references( 307 *, 308 obj: T, 309 langfuse_client: "Langfuse", 310 resolve_with: Literal["base64_data_uri"], 311 max_depth: int = 10, 312 content_fetch_timeout_seconds: int = 10, 313 ) -> T: 314 """Replace media reference strings in an object with base64 data URIs. 315 316 This method recursively traverses an object (up to max_depth) looking for media reference strings 317 in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using 318 the provided Langfuse client and replaces the reference string with a base64 data URI. 319 320 If fetching media content fails for a reference string, a warning is logged and the reference 321 string is left unchanged. 322 323 Args: 324 obj: The object to process. Can be a primitive value, array, or nested object. 325 If the object has a __dict__ attribute, a dict will be returned instead of the original object type. 326 langfuse_client: Langfuse client instance used to fetch media content. 327 resolve_with: The representation of the media content to replace the media reference string with. 328 Currently only "base64_data_uri" is supported. 329 max_depth: Optional. Default is 10. The maximum depth to traverse the object. 330 331 Returns: 332 A deep copy of the input object with all media references replaced with base64 data URIs where possible. 333 If the input object has a __dict__ attribute, a dict will be returned instead of the original object type. 334 335 Example: 336 obj = { 337 "image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", 338 "nested": { 339 "pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" 340 } 341 } 342 343 result = await LangfuseMedia.resolve_media_references(obj, langfuse_client) 344 345 # Result: 346 # { 347 # "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", 348 # "nested": { 349 # "pdf": "data:application/pdf;base64,JVBERi0xLjcK..." 350 # } 351 # } 352 """ 353 354 def traverse(obj: Any, depth: int) -> Any: 355 if depth > max_depth: 356 return obj 357 358 # Handle string 359 if isinstance(obj, str): 360 regex = r"@@@langfuseMedia:.+?@@@" 361 reference_string_matches = re.findall(regex, obj) 362 if len(reference_string_matches) == 0: 363 return obj 364 365 result = obj 366 reference_string_to_media_content = {} 367 httpx_client = ( 368 langfuse_client._resources.httpx_client 369 if langfuse_client._resources is not None 370 else None 371 ) 372 373 for reference_string in reference_string_matches: 374 try: 375 parsed_media_reference = LangfuseMedia.parse_reference_string( 376 reference_string 377 ) 378 media_data = langfuse_client.api.media.get( 379 parsed_media_reference["media_id"] 380 ) 381 media_content = ( 382 httpx_client.get( 383 media_data.url, 384 timeout=content_fetch_timeout_seconds, 385 ) 386 if httpx_client is not None 387 else httpx.get( 388 media_data.url, timeout=content_fetch_timeout_seconds 389 ) 390 ) 391 media_content.raise_for_status() 392 393 base64_media_content = base64.b64encode( 394 media_content.content 395 ).decode() 396 base64_data_uri = f"data:{media_data.content_type};base64,{base64_media_content}" 397 398 reference_string_to_media_content[reference_string] = ( 399 base64_data_uri 400 ) 401 except Exception as e: 402 logger.warning( 403 "Error fetching media content for reference string %s: %s", 404 reference_string, 405 e, 406 ) 407 # Do not replace the reference string if there's an error 408 continue 409 410 for ( 411 ref_str, 412 media_content_str, 413 ) in reference_string_to_media_content.items(): 414 result = result.replace(ref_str, media_content_str) 415 416 return result 417 418 # Handle arrays 419 if isinstance(obj, list): 420 return [traverse(item, depth + 1) for item in obj] 421 422 # Handle dictionaries 423 if isinstance(obj, dict): 424 return {key: traverse(value, depth + 1) for key, value in obj.items()} 425 426 # Handle objects: 427 if hasattr(obj, "__dict__"): 428 return { 429 key: traverse(value, depth + 1) 430 for key, value in obj.__dict__.items() 431 } 432 433 return obj 434 435 return cast(T, traverse(obj, 0))
Replace media reference strings in an object with base64 data URIs.
This method recursively traverses an object (up to max_depth) looking for media reference strings in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using the provided Langfuse client and replaces the reference string with a base64 data URI.
If fetching media content fails for a reference string, a warning is logged and the reference string is left unchanged.
Arguments:
- obj: The object to process. Can be a primitive value, array, or nested object. If the object has a __dict__ attribute, a dict will be returned instead of the original object type.
- langfuse_client: Langfuse client instance used to fetch media content.
- resolve_with: The representation of the media content to replace the media reference string with. Currently only "base64_data_uri" is supported.
- max_depth: Optional. Default is 10. The maximum depth to traverse the object.
Returns:
A deep copy of the input object with all media references replaced with base64 data URIs where possible. If the input object has a __dict__ attribute, a dict will be returned instead of the original object type.
Example:
obj = { "image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", "nested": { "pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" } }
result = await LangfuseMedia.resolve_media_references(obj, langfuse_client)
Result:
{
"image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"nested": {
"pdf": "data:application/pdf;base64,JVBERi0xLjcK..."
}
}
24@dataclass(frozen=True) 25class LangfuseMediaReference: 26 """Resolved reference to media stored in Langfuse.""" 27 28 media_id: str 29 content_type: str 30 url: str 31 url_expiry: Optional[str] = None 32 content_length: Optional[int] = None 33 reference_string: Optional[str] = None 34 35 def is_url_expired(self) -> bool: 36 """Return whether the signed URL is already expired.""" 37 if self.url_expiry is None: 38 return False 39 40 expiry = self.url_expiry.replace("Z", "+00:00") 41 42 try: 43 expiry_datetime = datetime.fromisoformat(expiry) 44 except ValueError: 45 return False 46 47 if expiry_datetime.tzinfo is None: 48 expiry_datetime = expiry_datetime.replace(tzinfo=timezone.utc) 49 50 return expiry_datetime <= datetime.now(timezone.utc) 51 52 def fetch_bytes( 53 self, *, timeout: float = 30.0, client: Optional[httpx.Client] = None 54 ) -> bytes: 55 """Fetch the media content from the signed URL. 56 57 Args: 58 timeout: Request timeout in seconds. 59 client: Optional httpx client to use for the request. Pass this to 60 honor custom transport settings (proxy, CA bundle, mTLS) — in 61 particular when multiple Langfuse clients are configured, since 62 the SDK cannot otherwise tell which client produced this 63 reference. When omitted, the single configured client is used, 64 falling back to a default httpx client. 65 """ 66 from langfuse._client.resource_manager import LangfuseResourceManager 67 68 httpx_client = client or LangfuseResourceManager.get_singleton_httpx_client() 69 response = ( 70 httpx_client.get(self.url, timeout=timeout) 71 if httpx_client is not None 72 else httpx.get(self.url, timeout=timeout) 73 ) 74 response.raise_for_status() 75 76 return response.content 77 78 def fetch_base64( 79 self, *, timeout: float = 30.0, client: Optional[httpx.Client] = None 80 ) -> str: 81 """Fetch media and return raw base64 without a data URI prefix. 82 83 See :meth:`fetch_bytes` for the ``client`` argument. 84 """ 85 return base64.b64encode( 86 self.fetch_bytes(timeout=timeout, client=client) 87 ).decode() 88 89 def fetch_data_uri( 90 self, *, timeout: float = 30.0, client: Optional[httpx.Client] = None 91 ) -> str: 92 """Fetch media and return it as a data URI. 93 94 See :meth:`fetch_bytes` for the ``client`` argument. 95 """ 96 return f"data:{self.content_type};base64,{self.fetch_base64(timeout=timeout, client=client)}"
Resolved reference to media stored in Langfuse.
35 def is_url_expired(self) -> bool: 36 """Return whether the signed URL is already expired.""" 37 if self.url_expiry is None: 38 return False 39 40 expiry = self.url_expiry.replace("Z", "+00:00") 41 42 try: 43 expiry_datetime = datetime.fromisoformat(expiry) 44 except ValueError: 45 return False 46 47 if expiry_datetime.tzinfo is None: 48 expiry_datetime = expiry_datetime.replace(tzinfo=timezone.utc) 49 50 return expiry_datetime <= datetime.now(timezone.utc)
Return whether the signed URL is already expired.
52 def fetch_bytes( 53 self, *, timeout: float = 30.0, client: Optional[httpx.Client] = None 54 ) -> bytes: 55 """Fetch the media content from the signed URL. 56 57 Args: 58 timeout: Request timeout in seconds. 59 client: Optional httpx client to use for the request. Pass this to 60 honor custom transport settings (proxy, CA bundle, mTLS) — in 61 particular when multiple Langfuse clients are configured, since 62 the SDK cannot otherwise tell which client produced this 63 reference. When omitted, the single configured client is used, 64 falling back to a default httpx client. 65 """ 66 from langfuse._client.resource_manager import LangfuseResourceManager 67 68 httpx_client = client or LangfuseResourceManager.get_singleton_httpx_client() 69 response = ( 70 httpx_client.get(self.url, timeout=timeout) 71 if httpx_client is not None 72 else httpx.get(self.url, timeout=timeout) 73 ) 74 response.raise_for_status() 75 76 return response.content
Fetch the media content from the signed URL.
Arguments:
- timeout: Request timeout in seconds.
- client: Optional httpx client to use for the request. Pass this to honor custom transport settings (proxy, CA bundle, mTLS) — in particular when multiple Langfuse clients are configured, since the SDK cannot otherwise tell which client produced this reference. When omitted, the single configured client is used, falling back to a default httpx client.
78 def fetch_base64( 79 self, *, timeout: float = 30.0, client: Optional[httpx.Client] = None 80 ) -> str: 81 """Fetch media and return raw base64 without a data URI prefix. 82 83 See :meth:`fetch_bytes` for the ``client`` argument. 84 """ 85 return base64.b64encode( 86 self.fetch_bytes(timeout=timeout, client=client) 87 ).decode()
Fetch media and return raw base64 without a data URI prefix.
See fetch_bytes() for the client argument.
89 def fetch_data_uri( 90 self, *, timeout: float = 30.0, client: Optional[httpx.Client] = None 91 ) -> str: 92 """Fetch media and return it as a data URI. 93 94 See :meth:`fetch_bytes` for the ``client`` argument. 95 """ 96 return f"data:{self.content_type};base64,{self.fetch_base64(timeout=timeout, client=client)}"
Fetch media and return it as a data URI.
See fetch_bytes() for the client argument.
65def get_client(*, public_key: Optional[str] = None) -> Langfuse: 66 """Get or create a Langfuse client instance. 67 68 Returns an existing Langfuse client or creates a new one if none exists. In multi-project setups, 69 providing a public_key is required. Multi-project support is experimental - see Langfuse docs. 70 71 Behavior: 72 - Single project: Returns existing client or creates new one 73 - Multi-project: Requires public_key to return specific client 74 - No public_key in multi-project: Returns disabled client to prevent data leakage 75 76 The function uses a singleton pattern per public_key to conserve resources and maintain state. 77 78 Args: 79 public_key (Optional[str]): Project identifier 80 - With key: Returns client for that project 81 - Without key: Returns single client or disabled client if multiple exist 82 83 Returns: 84 Langfuse: Client instance in one of three states: 85 1. Client for specified public_key 86 2. Default client for single-project setup 87 3. Disabled client when multiple projects exist without key 88 89 Security: 90 Disables tracing when multiple projects exist without explicit key to prevent 91 cross-project data leakage. Multi-project setups are experimental. 92 93 Example: 94 ```python 95 # Single project 96 client = get_client() # Default client 97 98 # In multi-project usage: 99 client_a = get_client(public_key="project_a_key") # Returns project A's client 100 client_b = get_client(public_key="project_b_key") # Returns project B's client 101 102 # Without specific key in multi-project setup: 103 client = get_client() # Returns disabled client for safety 104 ``` 105 """ 106 with LangfuseResourceManager._lock: 107 active_instances = LangfuseResourceManager._instances 108 109 # If no explicit public_key provided, check execution context 110 if not public_key: 111 public_key = _current_public_key.get(None) 112 113 if not public_key: 114 if len(active_instances) == 0: 115 # No clients initialized yet, create default instance 116 return Langfuse() 117 118 if len(active_instances) == 1: 119 # Only one client exists, safe to use without specifying key 120 instance = list(active_instances.values())[0] 121 122 # Initialize with the credentials bound to the instance 123 # This is important if the original instance was instantiated 124 # via constructor arguments 125 return _create_client_from_instance(instance) 126 127 else: 128 # Multiple clients exist but no key specified - disable tracing 129 # to prevent cross-project data leakage 130 langfuse_logger.warning( 131 "No 'langfuse_public_key' passed to decorated function, but multiple langfuse clients are instantiated in current process. Skipping tracing for this function to avoid cross-project leakage." 132 ) 133 return Langfuse( 134 tracing_enabled=False, public_key="fake", secret_key="fake" 135 ) 136 137 else: 138 # Specific key provided, look up existing instance 139 target_instance: Optional[LangfuseResourceManager] = active_instances.get( 140 public_key, None 141 ) 142 143 if target_instance is None: 144 # No instance found with this key - client not initialized properly 145 langfuse_logger.warning( 146 "No Langfuse client with public key %s has been initialized. Skipping " 147 "tracing for decorated function.", 148 public_key, 149 ) 150 return Langfuse( 151 tracing_enabled=False, public_key="fake", secret_key="fake" 152 ) 153 154 # target_instance is guaranteed to be not None at this point 155 return _create_client_from_instance(target_instance, public_key)
Get or create a Langfuse client instance.
Returns an existing Langfuse client or creates a new one if none exists. In multi-project setups, providing a public_key is required. Multi-project support is experimental - see Langfuse docs.
Behavior:
- Single project: Returns existing client or creates new one
- Multi-project: Requires public_key to return specific client
- No public_key in multi-project: Returns disabled client to prevent data leakage
The function uses a singleton pattern per public_key to conserve resources and maintain state.
Arguments:
- public_key (Optional[str]): Project identifier
- With key: Returns client for that project
- Without key: Returns single client or disabled client if multiple exist
Returns:
Langfuse: Client instance in one of three states: 1. Client for specified public_key 2. Default client for single-project setup 3. Disabled client when multiple projects exist without key
Security:
Disables tracing when multiple projects exist without explicit key to prevent cross-project data leakage. Multi-project setups are experimental.
Example:
# Single project client = get_client() # Default client # In multi-project usage: client_a = get_client(public_key="project_a_key") # Returns project A's client client_b = get_client(public_key="project_b_key") # Returns project B's client # Without specific key in multi-project setup: client = get_client() # Returns disabled client for safety
91 def observe( 92 self, 93 func: Optional[F] = None, 94 *, 95 name: Optional[str] = None, 96 as_type: Optional[ObservationTypeLiteralNoEvent] = None, 97 capture_input: Optional[bool] = None, 98 capture_output: Optional[bool] = None, 99 transform_to_string: Optional[Callable[[Iterable], str]] = None, 100 ) -> Union[F, Callable[[F], F]]: 101 """Wrap a function to create and manage Langfuse tracing around its execution, supporting both synchronous and asynchronous functions. 102 103 This decorator provides seamless integration of Langfuse observability into your codebase. It automatically creates 104 spans or generations around function execution, capturing timing, inputs/outputs, and error states. The decorator 105 intelligently handles both synchronous and asynchronous functions, preserving function signatures and type hints. 106 107 Using OpenTelemetry's distributed tracing system, it maintains proper trace context propagation throughout your application, 108 enabling you to see hierarchical traces of function calls with detailed performance metrics and function-specific details. 109 110 Args: 111 func (Optional[Callable]): The function to decorate. When used with parentheses @observe(), this will be None. 112 name (Optional[str]): Custom name for the created trace or span. If not provided, the function name is used. 113 as_type (Optional[Literal]): Set the observation type. Supported values: 114 "generation", "span", "agent", "tool", "chain", "retriever", "embedding", "evaluator", "guardrail". 115 Observation types are highlighted in the Langfuse UI for filtering and visualization. 116 The types "generation" and "embedding" create a span on which additional attributes such as model, 117 usage_details, and cost_details can be set — use `as_type="generation"` for LLM calls and update the 118 observation via `langfuse.update_current_generation(...)` inside the function. 119 capture_input (Optional[bool]): Whether to capture the function's arguments as the observation's input. 120 Defaults to the LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED environment variable (True if unset). 121 Set to False for sensitive or very large inputs, then set input explicitly via 122 `langfuse.update_current_span(input=...)` if needed. 123 capture_output (Optional[bool]): Whether to capture the function's return value as the observation's output. 124 Same default and override mechanism as capture_input. 125 transform_to_string (Optional[Callable[[Iterable], str]]): For functions returning generators, joins the 126 yielded chunks into the string stored as output. Without it, chunks are concatenated if all are 127 strings, otherwise stored as a list. 128 129 Returns: 130 Callable: A wrapped version of the original function that automatically creates and manages Langfuse spans. 131 132 Example: 133 For general function tracing with automatic naming: 134 ```python 135 @observe() 136 def process_user_request(user_id, query): 137 # Function is automatically traced with name "process_user_request" 138 return get_response(query) 139 ``` 140 141 For language model generation tracking: 142 ```python 143 from langfuse import get_client, observe 144 145 @observe(name="answer-generation", as_type="generation") 146 async def generate_answer(query): 147 # Creates a generation-type observation with extended LLM metrics 148 response = await openai.chat.completions.create( 149 model="gpt-4", 150 messages=[{"role": "user", "content": query}] 151 ) 152 return response.choices[0].message.content 153 ``` 154 155 Disabling input/output capture (e.g. for sensitive or large payloads): 156 ```python 157 @observe(capture_input=False, capture_output=False) 158 def handle_pii(user_record): 159 return process(user_record) 160 ``` 161 162 For trace context propagation between functions: 163 ```python 164 @observe() 165 def main_process(): 166 # Parent span is created 167 return sub_process() # Child span automatically connected to parent 168 169 @observe() 170 def sub_process(): 171 # Automatically becomes a child span of main_process 172 return "result" 173 ``` 174 175 Raises: 176 Exception: Propagates any exceptions from the wrapped function after logging them in the trace. 177 178 Notes: 179 - The decorator preserves the original function's signature, docstring, and return type. 180 - Proper parent-child relationships between spans are automatically maintained. 181 - Special keyword arguments can be passed to control tracing: 182 - langfuse_trace_id: Explicitly set the trace ID for this function call 183 - langfuse_parent_observation_id: Explicitly set the parent span ID 184 - langfuse_public_key: Use a specific Langfuse project (when multiple clients exist) 185 - For async functions, the decorator returns an async function wrapper. 186 - For sync functions, the decorator returns a synchronous wrapper. 187 """ 188 valid_types = set(get_observation_types_list(ObservationTypeLiteralNoEvent)) 189 if as_type is not None and as_type not in valid_types: 190 logger.warning( 191 "Invalid as_type '%s'. Valid types are: %s. Defaulting to 'span'.", 192 as_type, 193 ", ".join(sorted(valid_types)), 194 ) 195 as_type = "span" 196 197 function_io_capture_enabled = os.environ.get( 198 LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED, "True" 199 ).lower() not in ("false", "0") 200 201 should_capture_input = ( 202 capture_input if capture_input is not None else function_io_capture_enabled 203 ) 204 205 should_capture_output = ( 206 capture_output 207 if capture_output is not None 208 else function_io_capture_enabled 209 ) 210 211 def decorator(func: F) -> F: 212 return ( 213 self._async_observe( 214 func, 215 name=name, 216 as_type=as_type, 217 capture_input=should_capture_input, 218 capture_output=should_capture_output, 219 transform_to_string=transform_to_string, 220 ) 221 if asyncio.iscoroutinefunction(func) 222 else self._sync_observe( 223 func, 224 name=name, 225 as_type=as_type, 226 capture_input=should_capture_input, 227 capture_output=should_capture_output, 228 transform_to_string=transform_to_string, 229 ) 230 ) 231 232 """Handle decorator with or without parentheses. 233 234 This logic enables the decorator to work both with and without parentheses: 235 - @observe - Python passes the function directly to the decorator 236 - @observe() - Python calls the decorator first, which must return a function decorator 237 238 When called without arguments (@observe), the func parameter contains the function to decorate, 239 so we directly apply the decorator to it. When called with parentheses (@observe()), 240 func is None, so we return the decorator function itself for Python to apply in the next step. 241 """ 242 if func is None: 243 return decorator 244 else: 245 return decorator(func)
Wrap a function to create and manage Langfuse tracing around its execution, supporting both synchronous and asynchronous functions.
This decorator provides seamless integration of Langfuse observability into your codebase. It automatically creates spans or generations around function execution, capturing timing, inputs/outputs, and error states. The decorator intelligently handles both synchronous and asynchronous functions, preserving function signatures and type hints.
Using OpenTelemetry's distributed tracing system, it maintains proper trace context propagation throughout your application, enabling you to see hierarchical traces of function calls with detailed performance metrics and function-specific details.
Arguments:
- func (Optional[Callable]): The function to decorate. When used with parentheses @observe(), this will be None.
- name (Optional[str]): Custom name for the created trace or span. If not provided, the function name is used.
- as_type (Optional[Literal]): Set the observation type. Supported values:
"generation", "span", "agent", "tool", "chain", "retriever", "embedding", "evaluator", "guardrail".
Observation types are highlighted in the Langfuse UI for filtering and visualization.
The types "generation" and "embedding" create a span on which additional attributes such as model,
usage_details, and cost_details can be set — use
as_type="generation"for LLM calls and update the observation vialangfuse.update_current_generation(...)inside the function. - capture_input (Optional[bool]): Whether to capture the function's arguments as the observation's input.
Defaults to the LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED environment variable (True if unset).
Set to False for sensitive or very large inputs, then set input explicitly via
langfuse.update_current_span(input=...)if needed. - capture_output (Optional[bool]): Whether to capture the function's return value as the observation's output. Same default and override mechanism as capture_input.
- transform_to_string (Optional[Callable[[Iterable], str]]): For functions returning generators, joins the yielded chunks into the string stored as output. Without it, chunks are concatenated if all are strings, otherwise stored as a list.
Returns:
Callable: A wrapped version of the original function that automatically creates and manages Langfuse spans.
Example:
For general function tracing with automatic naming:
@observe() def process_user_request(user_id, query): # Function is automatically traced with name "process_user_request" return get_response(query)For language model generation tracking:
from langfuse import get_client, observe @observe(name="answer-generation", as_type="generation") async def generate_answer(query): # Creates a generation-type observation with extended LLM metrics response = await openai.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": query}] ) return response.choices[0].message.contentDisabling input/output capture (e.g. for sensitive or large payloads):
@observe(capture_input=False, capture_output=False) def handle_pii(user_record): return process(user_record)For trace context propagation between functions:
@observe() def main_process(): # Parent span is created return sub_process() # Child span automatically connected to parent @observe() def sub_process(): # Automatically becomes a child span of main_process return "result"
Raises:
- Exception: Propagates any exceptions from the wrapped function after logging them in the trace.
Notes:
- The decorator preserves the original function's signature, docstring, and return type.
- Proper parent-child relationships between spans are automatically maintained.
- Special keyword arguments can be passed to control tracing:
- langfuse_trace_id: Explicitly set the trace ID for this function call
- langfuse_parent_observation_id: Explicitly set the parent span ID
- langfuse_public_key: Use a specific Langfuse project (when multiple clients exist)
- For async functions, the decorator returns an async function wrapper.
- For sync functions, the decorator returns a synchronous wrapper.
115def propagate_attributes( 116 *, 117 user_id: Optional[str] = None, 118 session_id: Optional[str] = None, 119 metadata: Optional[Dict[str, Any]] = None, 120 version: Optional[str] = None, 121 tags: Optional[List[str]] = None, 122 trace_name: Optional[str] = None, 123 environment: Optional[str] = None, 124 prompt: Optional[Union[PromptClient, Mapping[str, Any]]] = None, 125 as_baggage: bool = False, 126) -> _AgnosticContextManager[Any]: 127 """Propagate trace-level attributes to all spans created within this context. 128 129 This context manager sets attributes on the currently active span AND automatically 130 propagates them to all new child spans created within the context. This is the 131 recommended way to set trace-level attributes like user_id, session_id, 132 environment, and metadata dimensions that should be consistently applied across 133 all observations in a trace. 134 135 This is a module-level function, not a method on the Langfuse client: 136 import it with `from langfuse import propagate_attributes`. 137 138 **IMPORTANT**: Call this as early as possible within your trace/workflow — 139 ideally wrapping the creation of your root span, or immediately inside it. Only 140 the currently active span and spans created after entering this context will have 141 these attributes. Pre-existing spans will NOT be retroactively updated. 142 143 **Why this matters**: Langfuse aggregation queries (e.g., total cost by user_id, 144 filtering by session_id) only include observations that have the attribute set. 145 If you call `propagate_attributes` late in your workflow, earlier spans won't be 146 included in aggregations for that attribute. 147 148 Args: 149 user_id: User identifier to associate with all spans in this context. 150 Must be US-ASCII string, ≤200 characters. Use this to track which user 151 generated each trace and enable e.g. per-user cost/performance analysis. 152 session_id: Session identifier to associate with all spans in this context. 153 Must be US-ASCII string, ≤200 characters. Use this to group related traces 154 within a user session (e.g., a conversation thread, multi-turn interaction). 155 metadata: Additional key-value metadata to propagate to all spans. 156 - Keys must be US-ASCII strings 157 - Values are coerced to strings 158 - Coerced values must be ≤200 characters 159 - Use for dimensions like internal correlating identifiers 160 - AVOID: large payloads or sensitive data 161 version: Version identfier for parts of your application that are independently versioned, e.g. agents 162 tags: List of tags to categorize the group of observations 163 trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters. 164 Use this to set a consistent trace name for all spans created within this context. 165 prompt: Langfuse prompt to link to generations created within this context. 166 Accepts a `PromptClient` returned by `langfuse.get_prompt(...)` or any 167 object/dict exposing `name` (string) and `version` (integer) — e.g. 168 `{"name": "my-prompt", "version": 3}`. This is the recommended way to 169 link prompts to generations emitted by auto-instrumentation libraries 170 (e.g. LiteLLM's `langfuse_otel`, OpenAI Agents SDK, OpenInference) 171 where you don't create the generation via the Langfuse SDK yourself. 172 The prompt link is only applied to generation-type observations by the 173 Langfuse backend. Fallback prompts are never linked. An explicit 174 `prompt` passed to `start_observation` / `update_current_generation` 175 takes precedence over the propagated one. 176 environment: Langfuse environment to assign to spans created in this context. 177 Must be a lowercase alphanumeric string with optional hyphens or underscores, 178 must be ≤40 characters, and must not start with "langfuse". This maps to 179 the first-class `langfuse.environment` attribute, not to trace metadata. 180 Use it for request-scoped environments, for example when one shared proxy 181 handles calls from dev, staging, qa, and prod. A propagated environment 182 takes precedence over the local client default configured via 183 `Langfuse(environment=...)` or `LANGFUSE_TRACING_ENVIRONMENT` for spans 184 created while this propagation context is active. 185 as_baggage: If True, propagates attributes using OpenTelemetry baggage for 186 cross-process/service propagation. **Security warning**: When enabled, 187 attribute values are added to HTTP headers on ALL outbound requests. 188 This includes `environment` as the `langfuse_environment` baggage entry. 189 Only enable if values are safe to transmit via HTTP headers and you need 190 cross-service tracing. Default: False. 191 192 Returns: 193 Context manager that propagates attributes to all child spans. 194 195 Example: 196 Basic usage with user and session tracking (note: `propagate_attributes` is a 197 top-level import, not a client method): 198 199 ```python 200 from langfuse import Langfuse, propagate_attributes 201 202 langfuse = Langfuse() 203 204 # Set attributes early: wrap everything inside the root span 205 with langfuse.start_as_current_observation(name="user_workflow") as span: 206 with propagate_attributes( 207 user_id="user_123", 208 session_id="session_abc", 209 environment="production", 210 metadata={"experiment": "variant_a"} 211 ): 212 # All spans created here will have user_id, session_id, environment, and metadata 213 with langfuse.start_as_current_observation(name="llm_call") as llm_span: 214 # This span inherits user_id, session_id, environment, and experiment metadata 215 ... 216 217 with langfuse.start_as_current_observation( 218 name="completion", as_type="generation" 219 ) as gen: 220 # This span also inherits all attributes 221 ... 222 ``` 223 224 Prompt linking with auto-instrumented libraries: 225 226 ```python 227 from langfuse import Langfuse, propagate_attributes 228 229 langfuse = Langfuse() 230 prompt = langfuse.get_prompt("my-prompt") 231 232 with propagate_attributes(prompt=prompt): 233 # Generations emitted by auto-instrumentation (LiteLLM langfuse_otel, 234 # OpenAI Agents SDK, OpenInference, ...) within this context are 235 # linked to the prompt version. 236 completion = litellm.completion( 237 model="gpt-4o", 238 messages=prompt.compile(topic="chickens"), 239 ) 240 ``` 241 242 Late propagation (anti-pattern): 243 244 ```python 245 with langfuse.start_as_current_observation(name="workflow") as span: 246 # These spans WON'T have user_id 247 early_span = langfuse.start_observation(name="early_work") 248 early_span.end() 249 250 # Set attributes in the middle 251 with propagate_attributes(user_id="user_123"): 252 # Only spans created AFTER this point will have user_id 253 late_span = langfuse.start_observation(name="late_work") 254 late_span.end() 255 256 # Result: Aggregations by user_id will miss "early_work" span 257 ``` 258 259 Cross-service propagation with baggage (advanced): 260 261 ```python 262 # Service A - originating service 263 with langfuse.start_as_current_observation(name="api_request"): 264 with propagate_attributes( 265 user_id="user_123", 266 session_id="session_abc", 267 environment="staging", 268 as_baggage=True # Propagate via HTTP headers 269 ): 270 # Make HTTP request to Service B 271 response = requests.get("https://service-b.example.com/api") 272 # user_id, session_id, and environment are now in HTTP headers 273 274 # Service B - downstream service 275 # OpenTelemetry will automatically extract baggage from HTTP headers 276 # and propagate attributes to spans in Service B. If Service B has a local 277 # Langfuse environment configured, the propagated environment wins for 278 # spans created within this context. 279 ``` 280 281 Note: 282 - **Validation**: Attribute values (user_id, session_id, version, tags, 283 trace_name) must be strings ≤200 characters. Environment must also match 284 Langfuse's environment format: lowercase alphanumeric with optional 285 hyphens or underscores, must be ≤40 characters, and it must not start with "langfuse". Metadata 286 values are coerced to strings before the 200 character limit is applied. 287 Invalid values will be dropped with a warning logged. 288 - **OpenTelemetry**: This uses OpenTelemetry context propagation under the hood, 289 making it compatible with other OTel-instrumented libraries. 290 291 Raises: 292 No exceptions are raised. Invalid values are logged as warnings and dropped. 293 294 See also: 295 `Langfuse.start_as_current_observation` (create the root span this wraps), 296 https://langfuse.com/docs/observability/features/sessions, 297 https://langfuse.com/docs/observability/features/users, 298 https://langfuse.com/docs/observability/features/environments 299 """ 300 return _propagate_attributes( 301 user_id=user_id, 302 session_id=session_id, 303 metadata=metadata, 304 version=version, 305 tags=tags, 306 trace_name=trace_name, 307 environment=environment, 308 prompt=prompt, 309 as_baggage=as_baggage, 310 )
Propagate trace-level attributes to all spans created within this context.
This context manager sets attributes on the currently active span AND automatically propagates them to all new child spans created within the context. This is the recommended way to set trace-level attributes like user_id, session_id, environment, and metadata dimensions that should be consistently applied across all observations in a trace.
This is a module-level function, not a method on the Langfuse client:
import it with from langfuse import propagate_attributes.
IMPORTANT: Call this as early as possible within your trace/workflow — ideally wrapping the creation of your root span, or immediately inside it. Only the currently active span and spans created after entering this context will have these attributes. Pre-existing spans will NOT be retroactively updated.
Why this matters: Langfuse aggregation queries (e.g., total cost by user_id,
filtering by session_id) only include observations that have the attribute set.
If you call propagate_attributes late in your workflow, earlier spans won't be
included in aggregations for that attribute.
Arguments:
- user_id: User identifier to associate with all spans in this context. Must be US-ASCII string, ≤200 characters. Use this to track which user generated each trace and enable e.g. per-user cost/performance analysis.
- session_id: Session identifier to associate with all spans in this context. Must be US-ASCII string, ≤200 characters. Use this to group related traces within a user session (e.g., a conversation thread, multi-turn interaction).
- metadata: Additional key-value metadata to propagate to all spans.
- Keys must be US-ASCII strings
- Values are coerced to strings
- Coerced values must be ≤200 characters
- Use for dimensions like internal correlating identifiers
- AVOID: large payloads or sensitive data
- version: Version identfier for parts of your application that are independently versioned, e.g. agents
- tags: List of tags to categorize the group of observations
- trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters. Use this to set a consistent trace name for all spans created within this context.
- prompt: Langfuse prompt to link to generations created within this context.
Accepts a
PromptClientreturned bylangfuse.get_prompt(...)or any object/dict exposingname(string) andversion(integer) — e.g.{"name": "my-prompt", "version": 3}. This is the recommended way to link prompts to generations emitted by auto-instrumentation libraries (e.g. LiteLLM'slangfuse_otel, OpenAI Agents SDK, OpenInference) where you don't create the generation via the Langfuse SDK yourself. The prompt link is only applied to generation-type observations by the Langfuse backend. Fallback prompts are never linked. An explicitpromptpassed tostart_observation/update_current_generationtakes precedence over the propagated one. - environment: Langfuse environment to assign to spans created in this context.
Must be a lowercase alphanumeric string with optional hyphens or underscores,
must be ≤40 characters, and must not start with "langfuse". This maps to
the first-class
langfuse.environmentattribute, not to trace metadata. Use it for request-scoped environments, for example when one shared proxy handles calls from dev, staging, qa, and prod. A propagated environment takes precedence over the local client default configured viaLangfuse(environment=...)orLANGFUSE_TRACING_ENVIRONMENTfor spans created while this propagation context is active. - as_baggage: If True, propagates attributes using OpenTelemetry baggage for
cross-process/service propagation. Security warning: When enabled,
attribute values are added to HTTP headers on ALL outbound requests.
This includes
environmentas thelangfuse_environmentbaggage entry. Only enable if values are safe to transmit via HTTP headers and you need cross-service tracing. Default: False.
Returns:
Context manager that propagates attributes to all child spans.
Example:
Basic usage with user and session tracking (note:
propagate_attributesis a top-level import, not a client method):from langfuse import Langfuse, propagate_attributes langfuse = Langfuse() # Set attributes early: wrap everything inside the root span with langfuse.start_as_current_observation(name="user_workflow") as span: with propagate_attributes( user_id="user_123", session_id="session_abc", environment="production", metadata={"experiment": "variant_a"} ): # All spans created here will have user_id, session_id, environment, and metadata with langfuse.start_as_current_observation(name="llm_call") as llm_span: # This span inherits user_id, session_id, environment, and experiment metadata ... with langfuse.start_as_current_observation( name="completion", as_type="generation" ) as gen: # This span also inherits all attributes ...Prompt linking with auto-instrumented libraries:
from langfuse import Langfuse, propagate_attributes langfuse = Langfuse() prompt = langfuse.get_prompt("my-prompt") with propagate_attributes(prompt=prompt): # Generations emitted by auto-instrumentation (LiteLLM langfuse_otel, # OpenAI Agents SDK, OpenInference, ...) within this context are # linked to the prompt version. completion = litellm.completion( model="gpt-4o", messages=prompt.compile(topic="chickens"), )Late propagation (anti-pattern):
with langfuse.start_as_current_observation(name="workflow") as span: # These spans WON'T have user_id early_span = langfuse.start_observation(name="early_work") early_span.end() # Set attributes in the middle with propagate_attributes(user_id="user_123"): # Only spans created AFTER this point will have user_id late_span = langfuse.start_observation(name="late_work") late_span.end() # Result: Aggregations by user_id will miss "early_work" spanCross-service propagation with baggage (advanced):
# Service A - originating service with langfuse.start_as_current_observation(name="api_request"): with propagate_attributes( user_id="user_123", session_id="session_abc", environment="staging", as_baggage=True # Propagate via HTTP headers ): # Make HTTP request to Service B response = requests.get("https://service-b.example.com/api") # user_id, session_id, and environment are now in HTTP headers # Service B - downstream service # OpenTelemetry will automatically extract baggage from HTTP headers # and propagate attributes to spans in Service B. If Service B has a local # Langfuse environment configured, the propagated environment wins for # spans created within this context.
Note:
- Validation: Attribute values (user_id, session_id, version, tags, trace_name) must be strings ≤200 characters. Environment must also match Langfuse's environment format: lowercase alphanumeric with optional hyphens or underscores, must be ≤40 characters, and it must not start with "langfuse". Metadata values are coerced to strings before the 200 character limit is applied. Invalid values will be dropped with a warning logged.
- OpenTelemetry: This uses OpenTelemetry context propagation under the hood, making it compatible with other OTel-instrumented libraries.
Raises:
- No exceptions are raised. Invalid values are logged as warnings and dropped.
See also:
Langfuse.start_as_current_observation(create the root span this wraps), https://langfuse.com/docs/observability/features/sessions, https://langfuse.com/docs/observability/features/users, https://langfuse.com/docs/observability/features/environments
1269class LangfuseSpan(LangfuseObservationWrapper): 1270 """Standard span implementation for general operations in Langfuse. 1271 1272 This class represents a general-purpose span that can be used to trace 1273 any operation in your application. It extends the base LangfuseObservationWrapper 1274 with specific methods for creating child spans, generations, and updating 1275 span-specific attributes. If possible, use a more specific type for 1276 better observability and insights. 1277 """ 1278 1279 def __init__( 1280 self, 1281 *, 1282 otel_span: otel_trace_api.Span, 1283 langfuse_client: "Langfuse", 1284 input: Optional[Any] = None, 1285 output: Optional[Any] = None, 1286 metadata: Optional[Any] = None, 1287 environment: Optional[str] = None, 1288 release: Optional[str] = None, 1289 version: Optional[str] = None, 1290 level: Optional[SpanLevel] = None, 1291 status_message: Optional[str] = None, 1292 ): 1293 """Initialize a new LangfuseSpan. 1294 1295 Args: 1296 otel_span: The OpenTelemetry span to wrap 1297 langfuse_client: Reference to the parent Langfuse client 1298 input: Input data for the span (any JSON-serializable object) 1299 output: Output data from the span (any JSON-serializable object) 1300 metadata: Additional metadata to associate with the span 1301 environment: The tracing environment 1302 release: Release identifier for the application 1303 version: Version identifier for the code or component 1304 level: Importance level of the span (info, warning, error) 1305 status_message: Optional status message for the span 1306 """ 1307 super().__init__( 1308 otel_span=otel_span, 1309 as_type="span", 1310 langfuse_client=langfuse_client, 1311 input=input, 1312 output=output, 1313 metadata=metadata, 1314 environment=environment, 1315 release=release, 1316 version=version, 1317 level=level, 1318 status_message=status_message, 1319 )
Standard span implementation for general operations in Langfuse.
This class represents a general-purpose span that can be used to trace any operation in your application. It extends the base LangfuseObservationWrapper with specific methods for creating child spans, generations, and updating span-specific attributes. If possible, use a more specific type for better observability and insights.
1279 def __init__( 1280 self, 1281 *, 1282 otel_span: otel_trace_api.Span, 1283 langfuse_client: "Langfuse", 1284 input: Optional[Any] = None, 1285 output: Optional[Any] = None, 1286 metadata: Optional[Any] = None, 1287 environment: Optional[str] = None, 1288 release: Optional[str] = None, 1289 version: Optional[str] = None, 1290 level: Optional[SpanLevel] = None, 1291 status_message: Optional[str] = None, 1292 ): 1293 """Initialize a new LangfuseSpan. 1294 1295 Args: 1296 otel_span: The OpenTelemetry span to wrap 1297 langfuse_client: Reference to the parent Langfuse client 1298 input: Input data for the span (any JSON-serializable object) 1299 output: Output data from the span (any JSON-serializable object) 1300 metadata: Additional metadata to associate with the span 1301 environment: The tracing environment 1302 release: Release identifier for the application 1303 version: Version identifier for the code or component 1304 level: Importance level of the span (info, warning, error) 1305 status_message: Optional status message for the span 1306 """ 1307 super().__init__( 1308 otel_span=otel_span, 1309 as_type="span", 1310 langfuse_client=langfuse_client, 1311 input=input, 1312 output=output, 1313 metadata=metadata, 1314 environment=environment, 1315 release=release, 1316 version=version, 1317 level=level, 1318 status_message=status_message, 1319 )
Initialize a new LangfuseSpan.
Arguments:
- otel_span: The OpenTelemetry span to wrap
- langfuse_client: Reference to the parent Langfuse client
- input: Input data for the span (any JSON-serializable object)
- output: Output data from the span (any JSON-serializable object)
- metadata: Additional metadata to associate with the span
- environment: The tracing environment
- release: Release identifier for the application
- version: Version identifier for the code or component
- level: Importance level of the span (info, warning, error)
- status_message: Optional status message for the span
1322class LangfuseGeneration(LangfuseObservationWrapper): 1323 """Specialized span implementation for AI model generations in Langfuse. 1324 1325 This class represents a generation span specifically designed for tracking 1326 AI/LLM operations. It extends the base LangfuseObservationWrapper with specialized 1327 attributes for model details, token usage, and costs. 1328 """ 1329 1330 def __init__( 1331 self, 1332 *, 1333 otel_span: otel_trace_api.Span, 1334 langfuse_client: "Langfuse", 1335 input: Optional[Any] = None, 1336 output: Optional[Any] = None, 1337 metadata: Optional[Any] = None, 1338 environment: Optional[str] = None, 1339 release: Optional[str] = None, 1340 version: Optional[str] = None, 1341 level: Optional[SpanLevel] = None, 1342 status_message: Optional[str] = None, 1343 completion_start_time: Optional[datetime] = None, 1344 model: Optional[str] = None, 1345 model_parameters: Optional[Dict[str, MapValue]] = None, 1346 usage_details: Optional[Dict[str, int]] = None, 1347 cost_details: Optional[Dict[str, float]] = None, 1348 prompt: Optional[PromptClient] = None, 1349 ): 1350 """Initialize a new LangfuseGeneration span. 1351 1352 Args: 1353 otel_span: The OpenTelemetry span to wrap 1354 langfuse_client: Reference to the parent Langfuse client 1355 input: Input data for the generation (e.g., prompts) 1356 output: Output from the generation (e.g., completions) 1357 metadata: Additional metadata to associate with the generation 1358 environment: The tracing environment 1359 release: Release identifier for the application 1360 version: Version identifier for the model or component 1361 level: Importance level of the generation (info, warning, error) 1362 status_message: Optional status message for the generation 1363 completion_start_time: When the model started generating the response 1364 model: Name/identifier of the AI model used (e.g., "gpt-4") 1365 model_parameters: Parameters used for the model (e.g., temperature, max_tokens) 1366 usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) 1367 cost_details: Cost information for the model call 1368 prompt: Associated prompt template from Langfuse prompt management 1369 """ 1370 super().__init__( 1371 as_type="generation", 1372 otel_span=otel_span, 1373 langfuse_client=langfuse_client, 1374 input=input, 1375 output=output, 1376 metadata=metadata, 1377 environment=environment, 1378 release=release, 1379 version=version, 1380 level=level, 1381 status_message=status_message, 1382 completion_start_time=completion_start_time, 1383 model=model, 1384 model_parameters=model_parameters, 1385 usage_details=usage_details, 1386 cost_details=cost_details, 1387 prompt=prompt, 1388 )
Specialized span implementation for AI model generations in Langfuse.
This class represents a generation span specifically designed for tracking AI/LLM operations. It extends the base LangfuseObservationWrapper with specialized attributes for model details, token usage, and costs.
1330 def __init__( 1331 self, 1332 *, 1333 otel_span: otel_trace_api.Span, 1334 langfuse_client: "Langfuse", 1335 input: Optional[Any] = None, 1336 output: Optional[Any] = None, 1337 metadata: Optional[Any] = None, 1338 environment: Optional[str] = None, 1339 release: Optional[str] = None, 1340 version: Optional[str] = None, 1341 level: Optional[SpanLevel] = None, 1342 status_message: Optional[str] = None, 1343 completion_start_time: Optional[datetime] = None, 1344 model: Optional[str] = None, 1345 model_parameters: Optional[Dict[str, MapValue]] = None, 1346 usage_details: Optional[Dict[str, int]] = None, 1347 cost_details: Optional[Dict[str, float]] = None, 1348 prompt: Optional[PromptClient] = None, 1349 ): 1350 """Initialize a new LangfuseGeneration span. 1351 1352 Args: 1353 otel_span: The OpenTelemetry span to wrap 1354 langfuse_client: Reference to the parent Langfuse client 1355 input: Input data for the generation (e.g., prompts) 1356 output: Output from the generation (e.g., completions) 1357 metadata: Additional metadata to associate with the generation 1358 environment: The tracing environment 1359 release: Release identifier for the application 1360 version: Version identifier for the model or component 1361 level: Importance level of the generation (info, warning, error) 1362 status_message: Optional status message for the generation 1363 completion_start_time: When the model started generating the response 1364 model: Name/identifier of the AI model used (e.g., "gpt-4") 1365 model_parameters: Parameters used for the model (e.g., temperature, max_tokens) 1366 usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) 1367 cost_details: Cost information for the model call 1368 prompt: Associated prompt template from Langfuse prompt management 1369 """ 1370 super().__init__( 1371 as_type="generation", 1372 otel_span=otel_span, 1373 langfuse_client=langfuse_client, 1374 input=input, 1375 output=output, 1376 metadata=metadata, 1377 environment=environment, 1378 release=release, 1379 version=version, 1380 level=level, 1381 status_message=status_message, 1382 completion_start_time=completion_start_time, 1383 model=model, 1384 model_parameters=model_parameters, 1385 usage_details=usage_details, 1386 cost_details=cost_details, 1387 prompt=prompt, 1388 )
Initialize a new LangfuseGeneration span.
Arguments:
- otel_span: The OpenTelemetry span to wrap
- langfuse_client: Reference to the parent Langfuse client
- input: Input data for the generation (e.g., prompts)
- output: Output from the generation (e.g., completions)
- metadata: Additional metadata to associate with the generation
- environment: The tracing environment
- release: Release identifier for the application
- version: Version identifier for the model or component
- level: Importance level of the generation (info, warning, error)
- status_message: Optional status message for the generation
- completion_start_time: When the model started generating the response
- model: Name/identifier of the AI model used (e.g., "gpt-4")
- model_parameters: Parameters used for the model (e.g., temperature, max_tokens)
- usage_details: Token usage information (e.g., prompt_tokens, completion_tokens)
- cost_details: Cost information for the model call
- prompt: Associated prompt template from Langfuse prompt management
1391class LangfuseEvent(LangfuseObservationWrapper): 1392 """Specialized span implementation for Langfuse Events.""" 1393 1394 def __init__( 1395 self, 1396 *, 1397 otel_span: otel_trace_api.Span, 1398 langfuse_client: "Langfuse", 1399 input: Optional[Any] = None, 1400 output: Optional[Any] = None, 1401 metadata: Optional[Any] = None, 1402 environment: Optional[str] = None, 1403 release: Optional[str] = None, 1404 version: Optional[str] = None, 1405 level: Optional[SpanLevel] = None, 1406 status_message: Optional[str] = None, 1407 ): 1408 """Initialize a new LangfuseEvent span. 1409 1410 Args: 1411 otel_span: The OpenTelemetry span to wrap 1412 langfuse_client: Reference to the parent Langfuse client 1413 input: Input data for the event 1414 output: Output from the event 1415 metadata: Additional metadata to associate with the generation 1416 environment: The tracing environment 1417 release: Release identifier for the application 1418 version: Version identifier for the model or component 1419 level: Importance level of the generation (info, warning, error) 1420 status_message: Optional status message for the generation 1421 """ 1422 super().__init__( 1423 otel_span=otel_span, 1424 as_type="event", 1425 langfuse_client=langfuse_client, 1426 input=input, 1427 output=output, 1428 metadata=metadata, 1429 environment=environment, 1430 release=release, 1431 version=version, 1432 level=level, 1433 status_message=status_message, 1434 ) 1435 1436 def update( 1437 self, 1438 *, 1439 name: Optional[str] = None, 1440 input: Optional[Any] = None, 1441 output: Optional[Any] = None, 1442 metadata: Optional[Any] = None, 1443 version: Optional[str] = None, 1444 level: Optional[SpanLevel] = None, 1445 status_message: Optional[str] = None, 1446 completion_start_time: Optional[datetime] = None, 1447 model: Optional[str] = None, 1448 model_parameters: Optional[Dict[str, MapValue]] = None, 1449 usage_details: Optional[Dict[str, int]] = None, 1450 cost_details: Optional[Dict[str, float]] = None, 1451 prompt: Optional[PromptClient] = None, 1452 **kwargs: Any, 1453 ) -> "LangfuseEvent": 1454 """Update is not allowed for LangfuseEvent because events cannot be updated. 1455 1456 This method logs a warning and returns self without making changes. 1457 1458 Returns: 1459 self: Returns the unchanged LangfuseEvent instance 1460 """ 1461 langfuse_logger.warning( 1462 "Attempted to update LangfuseEvent observation. Events cannot be updated after creation." 1463 ) 1464 return self
Specialized span implementation for Langfuse Events.
1394 def __init__( 1395 self, 1396 *, 1397 otel_span: otel_trace_api.Span, 1398 langfuse_client: "Langfuse", 1399 input: Optional[Any] = None, 1400 output: Optional[Any] = None, 1401 metadata: Optional[Any] = None, 1402 environment: Optional[str] = None, 1403 release: Optional[str] = None, 1404 version: Optional[str] = None, 1405 level: Optional[SpanLevel] = None, 1406 status_message: Optional[str] = None, 1407 ): 1408 """Initialize a new LangfuseEvent span. 1409 1410 Args: 1411 otel_span: The OpenTelemetry span to wrap 1412 langfuse_client: Reference to the parent Langfuse client 1413 input: Input data for the event 1414 output: Output from the event 1415 metadata: Additional metadata to associate with the generation 1416 environment: The tracing environment 1417 release: Release identifier for the application 1418 version: Version identifier for the model or component 1419 level: Importance level of the generation (info, warning, error) 1420 status_message: Optional status message for the generation 1421 """ 1422 super().__init__( 1423 otel_span=otel_span, 1424 as_type="event", 1425 langfuse_client=langfuse_client, 1426 input=input, 1427 output=output, 1428 metadata=metadata, 1429 environment=environment, 1430 release=release, 1431 version=version, 1432 level=level, 1433 status_message=status_message, 1434 )
Initialize a new LangfuseEvent span.
Arguments:
- otel_span: The OpenTelemetry span to wrap
- langfuse_client: Reference to the parent Langfuse client
- input: Input data for the event
- output: Output from the event
- metadata: Additional metadata to associate with the generation
- environment: The tracing environment
- release: Release identifier for the application
- version: Version identifier for the model or component
- level: Importance level of the generation (info, warning, error)
- status_message: Optional status message for the generation
1436 def update( 1437 self, 1438 *, 1439 name: Optional[str] = None, 1440 input: Optional[Any] = None, 1441 output: Optional[Any] = None, 1442 metadata: Optional[Any] = None, 1443 version: Optional[str] = None, 1444 level: Optional[SpanLevel] = None, 1445 status_message: Optional[str] = None, 1446 completion_start_time: Optional[datetime] = None, 1447 model: Optional[str] = None, 1448 model_parameters: Optional[Dict[str, MapValue]] = None, 1449 usage_details: Optional[Dict[str, int]] = None, 1450 cost_details: Optional[Dict[str, float]] = None, 1451 prompt: Optional[PromptClient] = None, 1452 **kwargs: Any, 1453 ) -> "LangfuseEvent": 1454 """Update is not allowed for LangfuseEvent because events cannot be updated. 1455 1456 This method logs a warning and returns self without making changes. 1457 1458 Returns: 1459 self: Returns the unchanged LangfuseEvent instance 1460 """ 1461 langfuse_logger.warning( 1462 "Attempted to update LangfuseEvent observation. Events cannot be updated after creation." 1463 ) 1464 return self
Update is not allowed for LangfuseEvent because events cannot be updated.
This method logs a warning and returns self without making changes.
Returns:
self: Returns the unchanged LangfuseEvent instance
28class LangfuseOtelSpanAttributes: 29 # Langfuse-Trace attributes 30 TRACE_NAME = "langfuse.trace.name" 31 TRACE_USER_ID = "user.id" 32 TRACE_SESSION_ID = "session.id" 33 TRACE_TAGS = "langfuse.trace.tags" 34 TRACE_PUBLIC = "langfuse.trace.public" 35 TRACE_METADATA = "langfuse.trace.metadata" 36 TRACE_INPUT = "langfuse.trace.input" 37 TRACE_OUTPUT = "langfuse.trace.output" 38 39 # Langfuse-observation attributes 40 OBSERVATION_TYPE = "langfuse.observation.type" 41 OBSERVATION_METADATA = "langfuse.observation.metadata" 42 OBSERVATION_LEVEL = "langfuse.observation.level" 43 OBSERVATION_STATUS_MESSAGE = "langfuse.observation.status_message" 44 OBSERVATION_INPUT = "langfuse.observation.input" 45 OBSERVATION_OUTPUT = "langfuse.observation.output" 46 47 # Langfuse-observation of type Generation attributes 48 OBSERVATION_COMPLETION_START_TIME = "langfuse.observation.completion_start_time" 49 OBSERVATION_MODEL = "langfuse.observation.model.name" 50 OBSERVATION_MODEL_PARAMETERS = "langfuse.observation.model.parameters" 51 OBSERVATION_USAGE_DETAILS = "langfuse.observation.usage_details" 52 OBSERVATION_COST_DETAILS = "langfuse.observation.cost_details" 53 OBSERVATION_PROMPT_NAME = "langfuse.observation.prompt.name" 54 OBSERVATION_PROMPT_VERSION = "langfuse.observation.prompt.version" 55 56 # General 57 ENVIRONMENT = "langfuse.environment" 58 RELEASE = "langfuse.release" 59 VERSION = "langfuse.version" 60 61 # Internal 62 AS_ROOT = "langfuse.internal.as_root" 63 IS_APP_ROOT = "langfuse.internal.is_app_root" 64 65 # Experiments 66 EXPERIMENT_ID = "langfuse.experiment.id" 67 EXPERIMENT_NAME = "langfuse.experiment.name" 68 EXPERIMENT_DESCRIPTION = "langfuse.experiment.description" 69 EXPERIMENT_METADATA = "langfuse.experiment.metadata" 70 EXPERIMENT_DATASET_ID = "langfuse.experiment.dataset.id" 71 EXPERIMENT_ITEM_ID = "langfuse.experiment.item.id" 72 EXPERIMENT_ITEM_EXPECTED_OUTPUT = "langfuse.experiment.item.expected_output" 73 EXPERIMENT_ITEM_METADATA = "langfuse.experiment.item.metadata" 74 EXPERIMENT_ITEM_ROOT_OBSERVATION_ID = "langfuse.experiment.item.root_observation_id"
1467class LangfuseAgent(LangfuseObservationWrapper): 1468 """Agent observation for reasoning blocks that act on tools using LLM guidance.""" 1469 1470 def __init__(self, **kwargs: Any) -> None: 1471 """Initialize a new LangfuseAgent span.""" 1472 kwargs["as_type"] = "agent" 1473 super().__init__(**kwargs)
Agent observation for reasoning blocks that act on tools using LLM guidance.
1476class LangfuseTool(LangfuseObservationWrapper): 1477 """Tool observation representing external tool calls, e.g., calling a weather API.""" 1478 1479 def __init__(self, **kwargs: Any) -> None: 1480 """Initialize a new LangfuseTool span.""" 1481 kwargs["as_type"] = "tool" 1482 super().__init__(**kwargs)
Tool observation representing external tool calls, e.g., calling a weather API.
1485class LangfuseChain(LangfuseObservationWrapper): 1486 """Chain observation for connecting LLM application steps, e.g. passing context from retriever to LLM.""" 1487 1488 def __init__(self, **kwargs: Any) -> None: 1489 """Initialize a new LangfuseChain span.""" 1490 kwargs["as_type"] = "chain" 1491 super().__init__(**kwargs)
Chain observation for connecting LLM application steps, e.g. passing context from retriever to LLM.
1503class LangfuseEmbedding(LangfuseObservationWrapper): 1504 """Embedding observation for LLM embedding calls, typically used before retrieval.""" 1505 1506 def __init__(self, **kwargs: Any) -> None: 1507 """Initialize a new LangfuseEmbedding span.""" 1508 kwargs["as_type"] = "embedding" 1509 super().__init__(**kwargs)
Embedding observation for LLM embedding calls, typically used before retrieval.
1512class LangfuseEvaluator(LangfuseObservationWrapper): 1513 """Evaluator observation for assessing relevance, correctness, or helpfulness of LLM outputs.""" 1514 1515 def __init__(self, **kwargs: Any) -> None: 1516 """Initialize a new LangfuseEvaluator span.""" 1517 kwargs["as_type"] = "evaluator" 1518 super().__init__(**kwargs)
Evaluator observation for assessing relevance, correctness, or helpfulness of LLM outputs.
1494class LangfuseRetriever(LangfuseObservationWrapper): 1495 """Retriever observation for data retrieval steps, e.g. vector store or database queries.""" 1496 1497 def __init__(self, **kwargs: Any) -> None: 1498 """Initialize a new LangfuseRetriever span.""" 1499 kwargs["as_type"] = "retriever" 1500 super().__init__(**kwargs)
Retriever observation for data retrieval steps, e.g. vector store or database queries.
1521class LangfuseGuardrail(LangfuseObservationWrapper): 1522 """Guardrail observation for protection e.g. against jailbreaks or offensive content.""" 1523 1524 def __init__(self, **kwargs: Any) -> None: 1525 """Initialize a new LangfuseGuardrail span.""" 1526 kwargs["as_type"] = "guardrail" 1527 super().__init__(**kwargs)
Guardrail observation for protection e.g. against jailbreaks or offensive content.
101class Evaluation: 102 """Represents an evaluation result for an experiment item or an entire experiment run. 103 104 This class provides a strongly-typed way to create evaluation results in evaluator functions. 105 Users must use keyword arguments when instantiating this class. 106 107 Attributes: 108 name: Unique identifier for the evaluation metric. Should be descriptive 109 and consistent across runs (e.g., "accuracy", "bleu_score", "toxicity"). 110 Used for aggregation and comparison across experiment runs. 111 value: The evaluation score or result. Can be: 112 - Numeric (int/float): For quantitative metrics like accuracy (0.85), BLEU (0.42) 113 - String: For categorical results like "positive", "negative", "neutral" 114 - Boolean: For binary assessments like "passes_safety_check" 115 comment: Optional human-readable explanation of the evaluation result. 116 Useful for providing context, explaining scoring rationale, or noting 117 special conditions. Displayed in Langfuse UI for interpretability. 118 metadata: Optional structured metadata about the evaluation process. 119 Can include confidence scores, intermediate calculations, model versions, 120 or any other relevant technical details. 121 data_type: Optional score data type. Required if value is not NUMERIC. 122 One of NUMERIC, CATEGORICAL, or BOOLEAN. Defaults to NUMERIC. 123 config_id: Optional Langfuse score config ID. 124 125 Examples: 126 Basic accuracy evaluation: 127 ```python 128 from langfuse import Evaluation 129 130 def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): 131 if not expected_output: 132 return Evaluation(name="accuracy", value=0, comment="No expected output") 133 134 is_correct = output.strip().lower() == expected_output.strip().lower() 135 return Evaluation( 136 name="accuracy", 137 value=1.0 if is_correct else 0.0, 138 comment="Correct answer" if is_correct else "Incorrect answer" 139 ) 140 ``` 141 142 Multi-metric evaluator: 143 ```python 144 def comprehensive_evaluator(*, input, output, expected_output=None, **kwargs): 145 return [ 146 Evaluation(name="length", value=len(output), comment=f"Output length: {len(output)} chars"), 147 Evaluation(name="has_greeting", value="hello" in output.lower(), comment="Contains greeting"), 148 Evaluation( 149 name="quality", 150 value=0.85, 151 comment="High quality response", 152 metadata={"confidence": 0.92, "model": "gpt-4"} 153 ) 154 ] 155 ``` 156 157 Categorical evaluation: 158 ```python 159 def sentiment_evaluator(*, input, output, **kwargs): 160 sentiment = analyze_sentiment(output) # Returns "positive", "negative", or "neutral" 161 return Evaluation( 162 name="sentiment", 163 value=sentiment, 164 comment=f"Response expresses {sentiment} sentiment", 165 data_type="CATEGORICAL" 166 ) 167 ``` 168 169 Failed evaluation with error handling: 170 ```python 171 def external_api_evaluator(*, input, output, **kwargs): 172 try: 173 score = external_api.evaluate(output) 174 return Evaluation(name="external_score", value=score) 175 except Exception as e: 176 return Evaluation( 177 name="external_score", 178 value=0, 179 comment=f"API unavailable: {e}", 180 metadata={"error": str(e), "retry_count": 3} 181 ) 182 ``` 183 184 Note: 185 All arguments must be passed as keywords. Positional arguments are not allowed 186 to ensure code clarity and prevent errors from argument reordering. 187 """ 188 189 def __init__( 190 self, 191 *, 192 name: str, 193 value: Union[int, float, str, bool], 194 comment: Optional[str] = None, 195 metadata: Optional[Dict[str, Any]] = None, 196 data_type: Optional[ExperimentScoreType] = None, 197 config_id: Optional[str] = None, 198 ): 199 """Initialize an Evaluation with the provided data. 200 201 Args: 202 name: Unique identifier for the evaluation metric. 203 value: The evaluation score or result. 204 comment: Optional human-readable explanation of the result. 205 metadata: Optional structured metadata about the evaluation process. 206 data_type: Optional score data type (NUMERIC, CATEGORICAL, or BOOLEAN). 207 config_id: Optional Langfuse score config ID. 208 209 Note: 210 All arguments must be provided as keywords. Positional arguments will raise a TypeError. 211 """ 212 self.name = name 213 self.value = value 214 self.comment = comment 215 self.metadata = metadata 216 self.data_type = data_type 217 self.config_id = config_id
Represents an evaluation result for an experiment item or an entire experiment run.
This class provides a strongly-typed way to create evaluation results in evaluator functions. Users must use keyword arguments when instantiating this class.
Attributes:
- name: Unique identifier for the evaluation metric. Should be descriptive and consistent across runs (e.g., "accuracy", "bleu_score", "toxicity"). Used for aggregation and comparison across experiment runs.
- value: The evaluation score or result. Can be:
- Numeric (int/float): For quantitative metrics like accuracy (0.85), BLEU (0.42)
- String: For categorical results like "positive", "negative", "neutral"
- Boolean: For binary assessments like "passes_safety_check"
- comment: Optional human-readable explanation of the evaluation result. Useful for providing context, explaining scoring rationale, or noting special conditions. Displayed in Langfuse UI for interpretability.
- metadata: Optional structured metadata about the evaluation process. Can include confidence scores, intermediate calculations, model versions, or any other relevant technical details.
- data_type: Optional score data type. Required if value is not NUMERIC. One of NUMERIC, CATEGORICAL, or BOOLEAN. Defaults to NUMERIC.
- config_id: Optional Langfuse score config ID.
Examples:
Basic accuracy evaluation:
from langfuse import Evaluation def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): if not expected_output: return Evaluation(name="accuracy", value=0, comment="No expected output") is_correct = output.strip().lower() == expected_output.strip().lower() return Evaluation( name="accuracy", value=1.0 if is_correct else 0.0, comment="Correct answer" if is_correct else "Incorrect answer" )Multi-metric evaluator:
def comprehensive_evaluator(*, input, output, expected_output=None, **kwargs): return [ Evaluation(name="length", value=len(output), comment=f"Output length: {len(output)} chars"), Evaluation(name="has_greeting", value="hello" in output.lower(), comment="Contains greeting"), Evaluation( name="quality", value=0.85, comment="High quality response", metadata={"confidence": 0.92, "model": "gpt-4"} ) ]Categorical evaluation:
def sentiment_evaluator(*, input, output, **kwargs): sentiment = analyze_sentiment(output) # Returns "positive", "negative", or "neutral" return Evaluation( name="sentiment", value=sentiment, comment=f"Response expresses {sentiment} sentiment", data_type="CATEGORICAL" )Failed evaluation with error handling:
def external_api_evaluator(*, input, output, **kwargs): try: score = external_api.evaluate(output) return Evaluation(name="external_score", value=score) except Exception as e: return Evaluation( name="external_score", value=0, comment=f"API unavailable: {e}", metadata={"error": str(e), "retry_count": 3} )
Note:
All arguments must be passed as keywords. Positional arguments are not allowed to ensure code clarity and prevent errors from argument reordering.
189 def __init__( 190 self, 191 *, 192 name: str, 193 value: Union[int, float, str, bool], 194 comment: Optional[str] = None, 195 metadata: Optional[Dict[str, Any]] = None, 196 data_type: Optional[ExperimentScoreType] = None, 197 config_id: Optional[str] = None, 198 ): 199 """Initialize an Evaluation with the provided data. 200 201 Args: 202 name: Unique identifier for the evaluation metric. 203 value: The evaluation score or result. 204 comment: Optional human-readable explanation of the result. 205 metadata: Optional structured metadata about the evaluation process. 206 data_type: Optional score data type (NUMERIC, CATEGORICAL, or BOOLEAN). 207 config_id: Optional Langfuse score config ID. 208 209 Note: 210 All arguments must be provided as keywords. Positional arguments will raise a TypeError. 211 """ 212 self.name = name 213 self.value = value 214 self.comment = comment 215 self.metadata = metadata 216 self.data_type = data_type 217 self.config_id = config_id
Initialize an Evaluation with the provided data.
Arguments:
- name: Unique identifier for the evaluation metric.
- value: The evaluation score or result.
- comment: Optional human-readable explanation of the result.
- metadata: Optional structured metadata about the evaluation process.
- data_type: Optional score data type (NUMERIC, CATEGORICAL, or BOOLEAN).
- config_id: Optional Langfuse score config ID.
Note:
All arguments must be provided as keywords. Positional arguments will raise a TypeError.
38class EvaluatorInputs: 39 """Input data structure for evaluators, returned by mapper functions. 40 41 This class provides a strongly-typed container for transforming API response 42 objects (traces, observations) into the standardized format expected 43 by evaluator functions. It ensures consistent access to input, output, expected 44 output, and metadata regardless of the source entity type. 45 46 Attributes: 47 input: The input data that was provided to generate the output being evaluated. 48 For traces, this might be the initial prompt or request. For observations, 49 this could be the span's input. The exact meaning depends on your use case. 50 output: The actual output that was produced and needs to be evaluated. 51 For traces, this is typically the final response. For observations, 52 this might be the generation output or span result. 53 expected_output: Optional ground truth or expected result for comparison. 54 Used by evaluators to assess correctness. May be None if no ground truth 55 is available for the entity being evaluated. 56 metadata: Optional structured metadata providing additional context for evaluation. 57 Can include information about the entity, execution context, user attributes, 58 or any other relevant data that evaluators might use. 59 60 Examples: 61 Simple mapper for traces: 62 ```python 63 from langfuse import EvaluatorInputs 64 65 def trace_mapper(trace): 66 return EvaluatorInputs( 67 input=trace.input, 68 output=trace.output, 69 expected_output=None, # No ground truth available 70 metadata={"user_id": trace.user_id, "tags": trace.tags} 71 ) 72 ``` 73 74 Mapper for observations extracting specific fields: 75 ```python 76 def observation_mapper(observation): 77 # Extract input/output from observation's data 78 input_data = observation.input if hasattr(observation, 'input') else None 79 output_data = observation.output if hasattr(observation, 'output') else None 80 81 return EvaluatorInputs( 82 input=input_data, 83 output=output_data, 84 expected_output=None, 85 metadata={ 86 "observation_type": observation.type, 87 "model": observation.model, 88 "latency_ms": observation.end_time - observation.start_time 89 } 90 ) 91 ``` 92 ``` 93 94 Note: 95 All arguments must be passed as keywords when instantiating this class. 96 """ 97 98 def __init__( 99 self, 100 *, 101 input: Any, 102 output: Any, 103 expected_output: Any = None, 104 metadata: Optional[Dict[str, Any]] = None, 105 ): 106 """Initialize EvaluatorInputs with the provided data. 107 108 Args: 109 input: The input data for evaluation. 110 output: The output data to be evaluated. 111 expected_output: Optional ground truth for comparison. 112 metadata: Optional additional context for evaluation. 113 114 Note: 115 All arguments must be provided as keywords. 116 """ 117 self.input = input 118 self.output = output 119 self.expected_output = expected_output 120 self.metadata = metadata
Input data structure for evaluators, returned by mapper functions.
This class provides a strongly-typed container for transforming API response objects (traces, observations) into the standardized format expected by evaluator functions. It ensures consistent access to input, output, expected output, and metadata regardless of the source entity type.
Attributes:
- input: The input data that was provided to generate the output being evaluated. For traces, this might be the initial prompt or request. For observations, this could be the span's input. The exact meaning depends on your use case.
- output: The actual output that was produced and needs to be evaluated. For traces, this is typically the final response. For observations, this might be the generation output or span result.
- expected_output: Optional ground truth or expected result for comparison. Used by evaluators to assess correctness. May be None if no ground truth is available for the entity being evaluated.
- metadata: Optional structured metadata providing additional context for evaluation. Can include information about the entity, execution context, user attributes, or any other relevant data that evaluators might use.
Examples:
Simple mapper for traces:
from langfuse import EvaluatorInputs def trace_mapper(trace): return EvaluatorInputs( input=trace.input, output=trace.output, expected_output=None, # No ground truth available metadata={"user_id": trace.user_id, "tags": trace.tags} )Mapper for observations extracting specific fields:
def observation_mapper(observation): # Extract input/output from observation's data input_data = observation.input if hasattr(observation, 'input') else None output_data = observation.output if hasattr(observation, 'output') else None return EvaluatorInputs( input=input_data, output=output_data, expected_output=None, metadata={ "observation_type": observation.type, "model": observation.model, "latency_ms": observation.end_time - observation.start_time } )```
Note:
All arguments must be passed as keywords when instantiating this class.
98 def __init__( 99 self, 100 *, 101 input: Any, 102 output: Any, 103 expected_output: Any = None, 104 metadata: Optional[Dict[str, Any]] = None, 105 ): 106 """Initialize EvaluatorInputs with the provided data. 107 108 Args: 109 input: The input data for evaluation. 110 output: The output data to be evaluated. 111 expected_output: Optional ground truth for comparison. 112 metadata: Optional additional context for evaluation. 113 114 Note: 115 All arguments must be provided as keywords. 116 """ 117 self.input = input 118 self.output = output 119 self.expected_output = expected_output 120 self.metadata = metadata
Initialize EvaluatorInputs with the provided data.
Arguments:
- input: The input data for evaluation.
- output: The output data to be evaluated.
- expected_output: Optional ground truth for comparison.
- metadata: Optional additional context for evaluation.
Note:
All arguments must be provided as keywords.
123class MapperFunction(Protocol): 124 """Protocol defining the interface for mapper functions in batch evaluation. 125 126 Mapper functions transform API response objects (traces or observations) 127 into the standardized EvaluatorInputs format that evaluators expect. This abstraction 128 allows you to define how to extract and structure evaluation data from different 129 entity types. 130 131 Mapper functions must: 132 - Accept a single item parameter (trace, observation) 133 - Return an EvaluatorInputs instance with input, output, expected_output, metadata 134 - Can be either synchronous or asynchronous 135 - Should handle missing or malformed data gracefully 136 """ 137 138 def __call__( 139 self, 140 *, 141 item: Union["TraceWithFullDetails", "ObservationsView"], 142 **kwargs: Dict[str, Any], 143 ) -> Union[EvaluatorInputs, Awaitable[EvaluatorInputs]]: 144 """Transform an API response object into evaluator inputs. 145 146 This method defines how to extract evaluation-relevant data from the raw 147 API response object. The implementation should map entity-specific fields 148 to the standardized input/output/expected_output/metadata structure. 149 150 Args: 151 item: The API response object to transform. The type depends on the scope: 152 - TraceWithFullDetails: When evaluating traces 153 - ObservationsView: When evaluating observations 154 155 Returns: 156 EvaluatorInputs: A structured container with: 157 - input: The input data that generated the output 158 - output: The output to be evaluated 159 - expected_output: Optional ground truth for comparison 160 - metadata: Optional additional context 161 162 Can return either a direct EvaluatorInputs instance or an awaitable 163 (for async mappers that need to fetch additional data). 164 165 Examples: 166 Basic trace mapper: 167 ```python 168 def map_trace(trace): 169 return EvaluatorInputs( 170 input=trace.input, 171 output=trace.output, 172 expected_output=None, 173 metadata={"trace_id": trace.id, "user": trace.user_id} 174 ) 175 ``` 176 177 Observation mapper with conditional logic: 178 ```python 179 def map_observation(observation): 180 # Extract fields based on observation type 181 if observation.type == "GENERATION": 182 input_data = observation.input 183 output_data = observation.output 184 else: 185 # For other types, use different fields 186 input_data = observation.metadata.get("input") 187 output_data = observation.metadata.get("output") 188 189 return EvaluatorInputs( 190 input=input_data, 191 output=output_data, 192 expected_output=None, 193 metadata={"obs_id": observation.id, "type": observation.type} 194 ) 195 ``` 196 197 Async mapper (if additional processing needed): 198 ```python 199 async def map_trace_async(trace): 200 # Could do async processing here if needed 201 processed_output = await some_async_transformation(trace.output) 202 203 return EvaluatorInputs( 204 input=trace.input, 205 output=processed_output, 206 expected_output=None, 207 metadata={"trace_id": trace.id} 208 ) 209 ``` 210 """ 211 ...
Protocol defining the interface for mapper functions in batch evaluation.
Mapper functions transform API response objects (traces or observations) into the standardized EvaluatorInputs format that evaluators expect. This abstraction allows you to define how to extract and structure evaluation data from different entity types.
Mapper functions must:
- Accept a single item parameter (trace, observation)
- Return an EvaluatorInputs instance with input, output, expected_output, metadata
- Can be either synchronous or asynchronous
- Should handle missing or malformed data gracefully
1927def _no_init_or_replace_init(self, *args, **kwargs): 1928 cls = type(self) 1929 1930 if cls._is_protocol: 1931 raise TypeError('Protocols cannot be instantiated') 1932 1933 # Already using a custom `__init__`. No need to calculate correct 1934 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1935 if cls.__init__ is not _no_init_or_replace_init: 1936 return 1937 1938 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1939 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1940 # searches for a proper new `__init__` in the MRO. The new `__init__` 1941 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1942 # instantiation of the protocol subclass will thus use the new 1943 # `__init__` and no longer call `_no_init_or_replace_init`. 1944 for base in cls.__mro__: 1945 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1946 if init is not _no_init_or_replace_init: 1947 cls.__init__ = init 1948 break 1949 else: 1950 # should not happen 1951 cls.__init__ = object.__init__ 1952 1953 cls.__init__(self, *args, **kwargs)
214class CompositeEvaluatorFunction(Protocol): 215 """Protocol defining the interface for composite evaluator functions. 216 217 Composite evaluators create aggregate scores from multiple item-level evaluations. 218 This is commonly used to compute weighted averages, combined metrics, or other 219 composite assessments based on individual evaluation results. 220 221 Composite evaluators: 222 - Accept the same inputs as item-level evaluators (input, output, expected_output, metadata) 223 plus the list of evaluations 224 - Return either a single Evaluation, a list of Evaluations, or a dict 225 - Can be either synchronous or asynchronous 226 - Have access to both raw item data and evaluation results 227 """ 228 229 def __call__( 230 self, 231 *, 232 input: Optional[Any] = None, 233 output: Optional[Any] = None, 234 expected_output: Optional[Any] = None, 235 metadata: Optional[Dict[str, Any]] = None, 236 evaluations: List[Evaluation], 237 **kwargs: Dict[str, Any], 238 ) -> Union[ 239 Evaluation, 240 List[Evaluation], 241 Dict[str, Any], 242 Awaitable[Evaluation], 243 Awaitable[List[Evaluation]], 244 Awaitable[Dict[str, Any]], 245 ]: 246 r"""Create a composite evaluation from item-level evaluation results. 247 248 This method combines multiple evaluation scores into a single composite metric. 249 Common use cases include weighted averages, pass/fail decisions based on multiple 250 criteria, or custom scoring logic that considers multiple dimensions. 251 252 Args: 253 input: The input data that was provided to the system being evaluated. 254 output: The output generated by the system being evaluated. 255 expected_output: The expected/reference output for comparison (if available). 256 metadata: Additional metadata about the evaluation context. 257 evaluations: List of evaluation results from item-level evaluators. 258 Each evaluation contains name, value, comment, and metadata. 259 260 Returns: 261 Can return any of: 262 - Evaluation: A single composite evaluation result 263 - List[Evaluation]: Multiple composite evaluations 264 - Dict: A dict that will be converted to an Evaluation 265 - name: Identifier for the composite metric (e.g., "composite_score") 266 - value: The computed composite value 267 - comment: Optional explanation of how the score was computed 268 - metadata: Optional details about the composition logic 269 270 Can return either a direct Evaluation instance or an awaitable 271 (for async composite evaluators). 272 273 Examples: 274 Simple weighted average: 275 ```python 276 def weighted_composite(*, input, output, expected_output, metadata, evaluations): 277 weights = { 278 "accuracy": 0.5, 279 "relevance": 0.3, 280 "safety": 0.2 281 } 282 283 total_score = 0.0 284 total_weight = 0.0 285 286 for eval in evaluations: 287 if eval.name in weights and isinstance(eval.value, (int, float)): 288 total_score += eval.value * weights[eval.name] 289 total_weight += weights[eval.name] 290 291 final_score = total_score / total_weight if total_weight > 0 else 0.0 292 293 return Evaluation( 294 name="composite_score", 295 value=final_score, 296 comment=f"Weighted average of {len(evaluations)} metrics" 297 ) 298 ``` 299 300 Pass/fail composite based on thresholds: 301 ```python 302 def pass_fail_composite(*, input, output, expected_output, metadata, evaluations): 303 # Must pass all criteria 304 thresholds = { 305 "accuracy": 0.7, 306 "safety": 0.9, 307 "relevance": 0.6 308 } 309 310 passes = True 311 failing_metrics = [] 312 313 for metric, threshold in thresholds.items(): 314 eval_result = next((e for e in evaluations if e.name == metric), None) 315 if eval_result and isinstance(eval_result.value, (int, float)): 316 if eval_result.value < threshold: 317 passes = False 318 failing_metrics.append(metric) 319 320 return Evaluation( 321 name="passes_all_checks", 322 value=passes, 323 comment=f"Failed: {', '.join(failing_metrics)}" if failing_metrics else "All checks passed", 324 data_type="BOOLEAN" 325 ) 326 ``` 327 328 Async composite with external scoring: 329 ```python 330 async def llm_composite(*, input, output, expected_output, metadata, evaluations): 331 # Use LLM to synthesize multiple evaluation results 332 eval_summary = "\n".join( 333 f"- {e.name}: {e.value}" for e in evaluations 334 ) 335 336 prompt = f"Given these evaluation scores:\n{eval_summary}\n" 337 prompt += f"For the output: {output}\n" 338 prompt += "Provide an overall quality score from 0-1." 339 340 response = await openai.chat.completions.create( 341 model="gpt-4", 342 messages=[{"role": "user", "content": prompt}] 343 ) 344 345 score = float(response.choices[0].message.content.strip()) 346 347 return Evaluation( 348 name="llm_composite_score", 349 value=score, 350 comment="LLM-synthesized composite score" 351 ) 352 ``` 353 354 Context-aware composite: 355 ```python 356 def context_composite(*, input, output, expected_output, metadata, evaluations): 357 # Adjust weighting based on metadata 358 base_weights = {"accuracy": 0.5, "speed": 0.3, "cost": 0.2} 359 360 # If metadata indicates high importance, prioritize accuracy 361 if metadata and metadata.get('importance') == 'high': 362 weights = {"accuracy": 0.7, "speed": 0.2, "cost": 0.1} 363 else: 364 weights = base_weights 365 366 total = sum( 367 e.value * weights.get(e.name, 0) 368 for e in evaluations 369 if isinstance(e.value, (int, float)) 370 ) 371 372 return Evaluation( 373 name="weighted_composite", 374 value=total, 375 comment="Context-aware weighted composite" 376 ) 377 ``` 378 """ 379 ...
Protocol defining the interface for composite evaluator functions.
Composite evaluators create aggregate scores from multiple item-level evaluations. This is commonly used to compute weighted averages, combined metrics, or other composite assessments based on individual evaluation results.
Composite evaluators:
- Accept the same inputs as item-level evaluators (input, output, expected_output, metadata) plus the list of evaluations
- Return either a single Evaluation, a list of Evaluations, or a dict
- Can be either synchronous or asynchronous
- Have access to both raw item data and evaluation results
1927def _no_init_or_replace_init(self, *args, **kwargs): 1928 cls = type(self) 1929 1930 if cls._is_protocol: 1931 raise TypeError('Protocols cannot be instantiated') 1932 1933 # Already using a custom `__init__`. No need to calculate correct 1934 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1935 if cls.__init__ is not _no_init_or_replace_init: 1936 return 1937 1938 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1939 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1940 # searches for a proper new `__init__` in the MRO. The new `__init__` 1941 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1942 # instantiation of the protocol subclass will thus use the new 1943 # `__init__` and no longer call `_no_init_or_replace_init`. 1944 for base in cls.__mro__: 1945 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1946 if init is not _no_init_or_replace_init: 1947 cls.__init__ = init 1948 break 1949 else: 1950 # should not happen 1951 cls.__init__ = object.__init__ 1952 1953 cls.__init__(self, *args, **kwargs)
382class EvaluatorStats: 383 """Statistics for a single evaluator's performance during batch evaluation. 384 385 This class tracks detailed metrics about how a specific evaluator performed 386 across all items in a batch evaluation run. It helps identify evaluator issues, 387 understand reliability, and optimize evaluation pipelines. 388 389 Attributes: 390 name: The name of the evaluator function (extracted from __name__). 391 total_runs: Total number of times the evaluator was invoked. 392 successful_runs: Number of times the evaluator completed successfully. 393 failed_runs: Number of times the evaluator raised an exception or failed. 394 total_scores_created: Total number of evaluation scores created by this evaluator. 395 Can be higher than successful_runs if the evaluator returns multiple scores. 396 397 Examples: 398 Accessing evaluator stats from batch evaluation result: 399 ```python 400 result = client.run_batched_evaluation(...) 401 402 for stats in result.evaluator_stats: 403 print(f"Evaluator: {stats.name}") 404 print(f" Success rate: {stats.successful_runs / stats.total_runs:.1%}") 405 print(f" Scores created: {stats.total_scores_created}") 406 407 if stats.failed_runs > 0: 408 print(f" ⚠️ Failed {stats.failed_runs} times") 409 ``` 410 411 Identifying problematic evaluators: 412 ```python 413 result = client.run_batched_evaluation(...) 414 415 # Find evaluators with high failure rates 416 for stats in result.evaluator_stats: 417 failure_rate = stats.failed_runs / stats.total_runs 418 if failure_rate > 0.1: # More than 10% failures 419 print(f"⚠️ {stats.name} has {failure_rate:.1%} failure rate") 420 print(f" Consider debugging or removing this evaluator") 421 ``` 422 423 Note: 424 All arguments must be passed as keywords when instantiating this class. 425 """ 426 427 def __init__( 428 self, 429 *, 430 name: str, 431 total_runs: int = 0, 432 successful_runs: int = 0, 433 failed_runs: int = 0, 434 total_scores_created: int = 0, 435 ): 436 """Initialize EvaluatorStats with the provided metrics. 437 438 Args: 439 name: The evaluator function name. 440 total_runs: Total number of evaluator invocations. 441 successful_runs: Number of successful completions. 442 failed_runs: Number of failures. 443 total_scores_created: Total scores created by this evaluator. 444 445 Note: 446 All arguments must be provided as keywords. 447 """ 448 self.name = name 449 self.total_runs = total_runs 450 self.successful_runs = successful_runs 451 self.failed_runs = failed_runs 452 self.total_scores_created = total_scores_created
Statistics for a single evaluator's performance during batch evaluation.
This class tracks detailed metrics about how a specific evaluator performed across all items in a batch evaluation run. It helps identify evaluator issues, understand reliability, and optimize evaluation pipelines.
Attributes:
- name: The name of the evaluator function (extracted from __name__).
- total_runs: Total number of times the evaluator was invoked.
- successful_runs: Number of times the evaluator completed successfully.
- failed_runs: Number of times the evaluator raised an exception or failed.
- total_scores_created: Total number of evaluation scores created by this evaluator. Can be higher than successful_runs if the evaluator returns multiple scores.
Examples:
Accessing evaluator stats from batch evaluation result:
result = client.run_batched_evaluation(...) for stats in result.evaluator_stats: print(f"Evaluator: {stats.name}") print(f" Success rate: {stats.successful_runs / stats.total_runs:.1%}") print(f" Scores created: {stats.total_scores_created}") if stats.failed_runs > 0: print(f" ⚠️ Failed {stats.failed_runs} times")Identifying problematic evaluators:
result = client.run_batched_evaluation(...) # Find evaluators with high failure rates for stats in result.evaluator_stats: failure_rate = stats.failed_runs / stats.total_runs if failure_rate > 0.1: # More than 10% failures print(f"⚠️ {stats.name} has {failure_rate:.1%} failure rate") print(f" Consider debugging or removing this evaluator")
Note:
All arguments must be passed as keywords when instantiating this class.
427 def __init__( 428 self, 429 *, 430 name: str, 431 total_runs: int = 0, 432 successful_runs: int = 0, 433 failed_runs: int = 0, 434 total_scores_created: int = 0, 435 ): 436 """Initialize EvaluatorStats with the provided metrics. 437 438 Args: 439 name: The evaluator function name. 440 total_runs: Total number of evaluator invocations. 441 successful_runs: Number of successful completions. 442 failed_runs: Number of failures. 443 total_scores_created: Total scores created by this evaluator. 444 445 Note: 446 All arguments must be provided as keywords. 447 """ 448 self.name = name 449 self.total_runs = total_runs 450 self.successful_runs = successful_runs 451 self.failed_runs = failed_runs 452 self.total_scores_created = total_scores_created
Initialize EvaluatorStats with the provided metrics.
Arguments:
- name: The evaluator function name.
- total_runs: Total number of evaluator invocations.
- successful_runs: Number of successful completions.
- failed_runs: Number of failures.
- total_scores_created: Total scores created by this evaluator.
Note:
All arguments must be provided as keywords.
455class BatchEvaluationResumeToken: 456 """Token for resuming a failed batch evaluation run. 457 458 This class encapsulates all the information needed to resume a batch evaluation 459 that was interrupted or failed partway through. It uses timestamp-based filtering 460 to avoid re-processing items that were already evaluated, even if the underlying 461 dataset changed between runs. 462 463 Attributes: 464 scope: The type of items being evaluated ("traces", "observations"). 465 filter: The original JSON filter string used to query items. 466 last_processed_timestamp: ISO 8601 timestamp of the last successfully processed item. 467 Used to construct a filter that only fetches items after this timestamp. 468 last_processed_id: The ID of the last successfully processed item, for reference. 469 items_processed: Count of items successfully processed before interruption. 470 471 Examples: 472 Resuming a failed batch evaluation: 473 ```python 474 # Initial run that fails partway through 475 try: 476 result = client.run_batched_evaluation( 477 scope="traces", 478 mapper=my_mapper, 479 evaluators=[evaluator1, evaluator2], 480 filter='{"tags": ["production"]}', 481 max_items=10000 482 ) 483 except Exception as e: 484 print(f"Evaluation failed: {e}") 485 486 # Save the resume token 487 if result.resume_token: 488 # Store resume token for later (e.g., in a file or database) 489 import json 490 with open("resume_token.json", "w") as f: 491 json.dump({ 492 "scope": result.resume_token.scope, 493 "filter": result.resume_token.filter, 494 "last_timestamp": result.resume_token.last_processed_timestamp, 495 "last_id": result.resume_token.last_processed_id, 496 "items_done": result.resume_token.items_processed 497 }, f) 498 499 # Later, resume from where it left off 500 with open("resume_token.json") as f: 501 token_data = json.load(f) 502 503 resume_token = BatchEvaluationResumeToken( 504 scope=token_data["scope"], 505 filter=token_data["filter"], 506 last_processed_timestamp=token_data["last_timestamp"], 507 last_processed_id=token_data["last_id"], 508 items_processed=token_data["items_done"] 509 ) 510 511 # Resume the evaluation 512 result = client.run_batched_evaluation( 513 scope="traces", 514 mapper=my_mapper, 515 evaluators=[evaluator1, evaluator2], 516 resume_from=resume_token 517 ) 518 519 print(f"Processed {result.total_items_processed} additional items") 520 ``` 521 522 Handling partial completion: 523 ```python 524 result = client.run_batched_evaluation(...) 525 526 if not result.completed: 527 print(f"Evaluation incomplete. Processed {result.resume_token.items_processed} items") 528 print(f"Last item: {result.resume_token.last_processed_id}") 529 print(f"Resume from: {result.resume_token.last_processed_timestamp}") 530 531 # Optionally retry automatically 532 if result.resume_token: 533 print("Retrying...") 534 result = client.run_batched_evaluation( 535 scope=result.resume_token.scope, 536 mapper=my_mapper, 537 evaluators=my_evaluators, 538 resume_from=result.resume_token 539 ) 540 ``` 541 542 Note: 543 All arguments must be passed as keywords when instantiating this class. 544 The timestamp-based approach means that items created after the initial run 545 but before the timestamp will be skipped. This is intentional to avoid 546 duplicates and ensure consistent evaluation. 547 """ 548 549 def __init__( 550 self, 551 *, 552 scope: str, 553 filter: Optional[str], 554 last_processed_timestamp: str, 555 last_processed_id: str, 556 items_processed: int, 557 ): 558 """Initialize BatchEvaluationResumeToken with the provided state. 559 560 Args: 561 scope: The scope type ("traces", "observations"). 562 filter: The original JSON filter string. 563 last_processed_timestamp: ISO 8601 timestamp of last processed item. 564 last_processed_id: ID of last processed item. 565 items_processed: Count of items processed before interruption. 566 567 Note: 568 All arguments must be provided as keywords. 569 """ 570 self.scope = scope 571 self.filter = filter 572 self.last_processed_timestamp = last_processed_timestamp 573 self.last_processed_id = last_processed_id 574 self.items_processed = items_processed
Token for resuming a failed batch evaluation run.
This class encapsulates all the information needed to resume a batch evaluation that was interrupted or failed partway through. It uses timestamp-based filtering to avoid re-processing items that were already evaluated, even if the underlying dataset changed between runs.
Attributes:
- scope: The type of items being evaluated ("traces", "observations").
- filter: The original JSON filter string used to query items.
- last_processed_timestamp: ISO 8601 timestamp of the last successfully processed item. Used to construct a filter that only fetches items after this timestamp.
- last_processed_id: The ID of the last successfully processed item, for reference.
- items_processed: Count of items successfully processed before interruption.
Examples:
Resuming a failed batch evaluation:
# Initial run that fails partway through try: result = client.run_batched_evaluation( scope="traces", mapper=my_mapper, evaluators=[evaluator1, evaluator2], filter='{"tags": ["production"]}', max_items=10000 ) except Exception as e: print(f"Evaluation failed: {e}") # Save the resume token if result.resume_token: # Store resume token for later (e.g., in a file or database) import json with open("resume_token.json", "w") as f: json.dump({ "scope": result.resume_token.scope, "filter": result.resume_token.filter, "last_timestamp": result.resume_token.last_processed_timestamp, "last_id": result.resume_token.last_processed_id, "items_done": result.resume_token.items_processed }, f) # Later, resume from where it left off with open("resume_token.json") as f: token_data = json.load(f) resume_token = BatchEvaluationResumeToken( scope=token_data["scope"], filter=token_data["filter"], last_processed_timestamp=token_data["last_timestamp"], last_processed_id=token_data["last_id"], items_processed=token_data["items_done"] ) # Resume the evaluation result = client.run_batched_evaluation( scope="traces", mapper=my_mapper, evaluators=[evaluator1, evaluator2], resume_from=resume_token ) print(f"Processed {result.total_items_processed} additional items")Handling partial completion:
result = client.run_batched_evaluation(...) if not result.completed: print(f"Evaluation incomplete. Processed {result.resume_token.items_processed} items") print(f"Last item: {result.resume_token.last_processed_id}") print(f"Resume from: {result.resume_token.last_processed_timestamp}") # Optionally retry automatically if result.resume_token: print("Retrying...") result = client.run_batched_evaluation( scope=result.resume_token.scope, mapper=my_mapper, evaluators=my_evaluators, resume_from=result.resume_token )
Note:
All arguments must be passed as keywords when instantiating this class. The timestamp-based approach means that items created after the initial run but before the timestamp will be skipped. This is intentional to avoid duplicates and ensure consistent evaluation.
549 def __init__( 550 self, 551 *, 552 scope: str, 553 filter: Optional[str], 554 last_processed_timestamp: str, 555 last_processed_id: str, 556 items_processed: int, 557 ): 558 """Initialize BatchEvaluationResumeToken with the provided state. 559 560 Args: 561 scope: The scope type ("traces", "observations"). 562 filter: The original JSON filter string. 563 last_processed_timestamp: ISO 8601 timestamp of last processed item. 564 last_processed_id: ID of last processed item. 565 items_processed: Count of items processed before interruption. 566 567 Note: 568 All arguments must be provided as keywords. 569 """ 570 self.scope = scope 571 self.filter = filter 572 self.last_processed_timestamp = last_processed_timestamp 573 self.last_processed_id = last_processed_id 574 self.items_processed = items_processed
Initialize BatchEvaluationResumeToken with the provided state.
Arguments:
- scope: The scope type ("traces", "observations").
- filter: The original JSON filter string.
- last_processed_timestamp: ISO 8601 timestamp of last processed item.
- last_processed_id: ID of last processed item.
- items_processed: Count of items processed before interruption.
Note:
All arguments must be provided as keywords.
577class BatchEvaluationResult: 578 r"""Complete result structure for batch evaluation execution. 579 580 This class encapsulates comprehensive statistics and metadata about a batch 581 evaluation run, including counts, evaluator-specific metrics, timing information, 582 error details, and resume capability. 583 584 Attributes: 585 total_items_fetched: Total number of items fetched from the API. 586 total_items_processed: Number of items successfully evaluated. 587 total_items_failed: Number of items that failed during evaluation. 588 total_scores_created: Total scores created by all item-level evaluators. 589 total_composite_scores_created: Scores created by the composite evaluator. 590 total_evaluations_failed: Number of individual evaluator failures across all items. 591 evaluator_stats: List of per-evaluator statistics (success/failure rates, scores created). 592 resume_token: Token for resuming if evaluation was interrupted (None if completed). 593 completed: True if all items were processed, False if stopped early or failed. 594 duration_seconds: Total time taken to execute the batch evaluation. 595 failed_item_ids: List of IDs for items that failed evaluation. 596 error_summary: Dictionary mapping error types to occurrence counts. 597 has_more_items: True if max_items limit was reached but more items exist. 598 item_evaluations: Dictionary mapping item IDs to their evaluation results (both regular and composite). 599 600 Examples: 601 Basic result inspection: 602 ```python 603 result = client.run_batched_evaluation(...) 604 605 print(f"Processed: {result.total_items_processed}/{result.total_items_fetched}") 606 print(f"Scores created: {result.total_scores_created}") 607 print(f"Duration: {result.duration_seconds:.2f}s") 608 print(f"Success rate: {result.total_items_processed / result.total_items_fetched:.1%}") 609 ``` 610 611 Detailed analysis with evaluator stats: 612 ```python 613 result = client.run_batched_evaluation(...) 614 615 print(f"\n📊 Batch Evaluation Results") 616 print(f"{'='*50}") 617 print(f"Items processed: {result.total_items_processed}") 618 print(f"Items failed: {result.total_items_failed}") 619 print(f"Scores created: {result.total_scores_created}") 620 621 if result.total_composite_scores_created > 0: 622 print(f"Composite scores: {result.total_composite_scores_created}") 623 624 print(f"\n📈 Evaluator Performance:") 625 for stats in result.evaluator_stats: 626 success_rate = stats.successful_runs / stats.total_runs if stats.total_runs > 0 else 0 627 print(f"\n {stats.name}:") 628 print(f" Success rate: {success_rate:.1%}") 629 print(f" Scores created: {stats.total_scores_created}") 630 if stats.failed_runs > 0: 631 print(f" ⚠️ Failures: {stats.failed_runs}") 632 633 if result.error_summary: 634 print(f"\n⚠️ Errors encountered:") 635 for error_type, count in result.error_summary.items(): 636 print(f" {error_type}: {count}") 637 ``` 638 639 Handling incomplete runs: 640 ```python 641 result = client.run_batched_evaluation(...) 642 643 if not result.completed: 644 print("⚠️ Evaluation incomplete!") 645 646 if result.resume_token: 647 print(f"Processed {result.resume_token.items_processed} items before failure") 648 print(f"Use resume_from parameter to continue from:") 649 print(f" Timestamp: {result.resume_token.last_processed_timestamp}") 650 print(f" Last ID: {result.resume_token.last_processed_id}") 651 652 if result.has_more_items: 653 print(f"ℹ️ More items available beyond max_items limit") 654 ``` 655 656 Performance monitoring: 657 ```python 658 result = client.run_batched_evaluation(...) 659 660 items_per_second = result.total_items_processed / result.duration_seconds 661 avg_scores_per_item = result.total_scores_created / result.total_items_processed 662 663 print(f"Performance metrics:") 664 print(f" Throughput: {items_per_second:.2f} items/second") 665 print(f" Avg scores/item: {avg_scores_per_item:.2f}") 666 print(f" Total duration: {result.duration_seconds:.2f}s") 667 668 if result.total_evaluations_failed > 0: 669 failure_rate = result.total_evaluations_failed / ( 670 result.total_items_processed * len(result.evaluator_stats) 671 ) 672 print(f" Evaluation failure rate: {failure_rate:.1%}") 673 ``` 674 675 Note: 676 All arguments must be passed as keywords when instantiating this class. 677 """ 678 679 def __init__( 680 self, 681 *, 682 total_items_fetched: int, 683 total_items_processed: int, 684 total_items_failed: int, 685 total_scores_created: int, 686 total_composite_scores_created: int, 687 total_evaluations_failed: int, 688 evaluator_stats: List[EvaluatorStats], 689 resume_token: Optional[BatchEvaluationResumeToken], 690 completed: bool, 691 duration_seconds: float, 692 failed_item_ids: List[str], 693 error_summary: Dict[str, int], 694 has_more_items: bool, 695 item_evaluations: Dict[str, List["Evaluation"]], 696 ): 697 """Initialize BatchEvaluationResult with comprehensive statistics. 698 699 Args: 700 total_items_fetched: Total items fetched from API. 701 total_items_processed: Items successfully evaluated. 702 total_items_failed: Items that failed evaluation. 703 total_scores_created: Scores from item-level evaluators. 704 total_composite_scores_created: Scores from composite evaluator. 705 total_evaluations_failed: Individual evaluator failures. 706 evaluator_stats: Per-evaluator statistics. 707 resume_token: Token for resuming (None if completed). 708 completed: Whether all items were processed. 709 duration_seconds: Total execution time. 710 failed_item_ids: IDs of failed items. 711 error_summary: Error types and counts. 712 has_more_items: Whether more items exist beyond max_items. 713 item_evaluations: Dictionary mapping item IDs to their evaluation results. 714 715 Note: 716 All arguments must be provided as keywords. 717 """ 718 self.total_items_fetched = total_items_fetched 719 self.total_items_processed = total_items_processed 720 self.total_items_failed = total_items_failed 721 self.total_scores_created = total_scores_created 722 self.total_composite_scores_created = total_composite_scores_created 723 self.total_evaluations_failed = total_evaluations_failed 724 self.evaluator_stats = evaluator_stats 725 self.resume_token = resume_token 726 self.completed = completed 727 self.duration_seconds = duration_seconds 728 self.failed_item_ids = failed_item_ids 729 self.error_summary = error_summary 730 self.has_more_items = has_more_items 731 self.item_evaluations = item_evaluations 732 733 def __str__(self) -> str: 734 """Return a formatted string representation of the batch evaluation results. 735 736 Returns: 737 A multi-line string with a summary of the evaluation results. 738 """ 739 lines = [] 740 lines.append("=" * 60) 741 lines.append("Batch Evaluation Results") 742 lines.append("=" * 60) 743 744 # Summary statistics 745 lines.append(f"\nStatus: {'Completed' if self.completed else 'Incomplete'}") 746 lines.append(f"Duration: {self.duration_seconds:.2f}s") 747 lines.append(f"\nItems fetched: {self.total_items_fetched}") 748 lines.append(f"Items processed: {self.total_items_processed}") 749 750 if self.total_items_failed > 0: 751 lines.append(f"Items failed: {self.total_items_failed}") 752 753 # Success rate 754 if self.total_items_fetched > 0: 755 success_rate = self.total_items_processed / self.total_items_fetched * 100 756 lines.append(f"Success rate: {success_rate:.1f}%") 757 758 # Scores created 759 lines.append(f"\nScores created: {self.total_scores_created}") 760 if self.total_composite_scores_created > 0: 761 lines.append(f"Composite scores: {self.total_composite_scores_created}") 762 763 total_scores = self.total_scores_created + self.total_composite_scores_created 764 lines.append(f"Total scores: {total_scores}") 765 766 # Evaluator statistics 767 if self.evaluator_stats: 768 lines.append("\nEvaluator Performance:") 769 for stats in self.evaluator_stats: 770 lines.append(f" {stats.name}:") 771 if stats.total_runs > 0: 772 success_rate = ( 773 stats.successful_runs / stats.total_runs * 100 774 if stats.total_runs > 0 775 else 0 776 ) 777 lines.append( 778 f" Runs: {stats.successful_runs}/{stats.total_runs} " 779 f"({success_rate:.1f}% success)" 780 ) 781 lines.append(f" Scores created: {stats.total_scores_created}") 782 if stats.failed_runs > 0: 783 lines.append(f" Failed runs: {stats.failed_runs}") 784 785 # Performance metrics 786 if self.total_items_processed > 0 and self.duration_seconds > 0: 787 items_per_sec = self.total_items_processed / self.duration_seconds 788 lines.append("\nPerformance:") 789 lines.append(f" Throughput: {items_per_sec:.2f} items/second") 790 if self.total_scores_created > 0: 791 avg_scores = self.total_scores_created / self.total_items_processed 792 lines.append(f" Avg scores per item: {avg_scores:.2f}") 793 794 # Errors and warnings 795 if self.error_summary: 796 lines.append("\nErrors encountered:") 797 for error_type, count in self.error_summary.items(): 798 lines.append(f" {error_type}: {count}") 799 800 # Incomplete run information 801 if not self.completed: 802 lines.append("\nWarning: Evaluation incomplete") 803 if self.resume_token: 804 lines.append( 805 f" Last processed: {self.resume_token.last_processed_timestamp}" 806 ) 807 lines.append(f" Items processed: {self.resume_token.items_processed}") 808 lines.append(" Use resume_from parameter to continue") 809 810 if self.has_more_items: 811 lines.append("\nNote: More items available beyond max_items limit") 812 813 lines.append("=" * 60) 814 return "\n".join(lines)
Complete result structure for batch evaluation execution.
This class encapsulates comprehensive statistics and metadata about a batch evaluation run, including counts, evaluator-specific metrics, timing information, error details, and resume capability.
Attributes:
- total_items_fetched: Total number of items fetched from the API.
- total_items_processed: Number of items successfully evaluated.
- total_items_failed: Number of items that failed during evaluation.
- total_scores_created: Total scores created by all item-level evaluators.
- total_composite_scores_created: Scores created by the composite evaluator.
- total_evaluations_failed: Number of individual evaluator failures across all items.
- evaluator_stats: List of per-evaluator statistics (success/failure rates, scores created).
- resume_token: Token for resuming if evaluation was interrupted (None if completed).
- completed: True if all items were processed, False if stopped early or failed.
- duration_seconds: Total time taken to execute the batch evaluation.
- failed_item_ids: List of IDs for items that failed evaluation.
- error_summary: Dictionary mapping error types to occurrence counts.
- has_more_items: True if max_items limit was reached but more items exist.
- item_evaluations: Dictionary mapping item IDs to their evaluation results (both regular and composite).
Examples:
Basic result inspection:
result = client.run_batched_evaluation(...) print(f"Processed: {result.total_items_processed}/{result.total_items_fetched}") print(f"Scores created: {result.total_scores_created}") print(f"Duration: {result.duration_seconds:.2f}s") print(f"Success rate: {result.total_items_processed / result.total_items_fetched:.1%}")Detailed analysis with evaluator stats:
result = client.run_batched_evaluation(...) print(f"\n📊 Batch Evaluation Results") print(f"{'='*50}") print(f"Items processed: {result.total_items_processed}") print(f"Items failed: {result.total_items_failed}") print(f"Scores created: {result.total_scores_created}") if result.total_composite_scores_created > 0: print(f"Composite scores: {result.total_composite_scores_created}") print(f"\n📈 Evaluator Performance:") for stats in result.evaluator_stats: success_rate = stats.successful_runs / stats.total_runs if stats.total_runs > 0 else 0 print(f"\n {stats.name}:") print(f" Success rate: {success_rate:.1%}") print(f" Scores created: {stats.total_scores_created}") if stats.failed_runs > 0: print(f" ⚠️ Failures: {stats.failed_runs}") if result.error_summary: print(f"\n⚠️ Errors encountered:") for error_type, count in result.error_summary.items(): print(f" {error_type}: {count}")Handling incomplete runs:
result = client.run_batched_evaluation(...) if not result.completed: print("⚠️ Evaluation incomplete!") if result.resume_token: print(f"Processed {result.resume_token.items_processed} items before failure") print(f"Use resume_from parameter to continue from:") print(f" Timestamp: {result.resume_token.last_processed_timestamp}") print(f" Last ID: {result.resume_token.last_processed_id}") if result.has_more_items: print(f"ℹ️ More items available beyond max_items limit")Performance monitoring:
result = client.run_batched_evaluation(...) items_per_second = result.total_items_processed / result.duration_seconds avg_scores_per_item = result.total_scores_created / result.total_items_processed print(f"Performance metrics:") print(f" Throughput: {items_per_second:.2f} items/second") print(f" Avg scores/item: {avg_scores_per_item:.2f}") print(f" Total duration: {result.duration_seconds:.2f}s") if result.total_evaluations_failed > 0: failure_rate = result.total_evaluations_failed / ( result.total_items_processed * len(result.evaluator_stats) ) print(f" Evaluation failure rate: {failure_rate:.1%}")
Note:
All arguments must be passed as keywords when instantiating this class.
679 def __init__( 680 self, 681 *, 682 total_items_fetched: int, 683 total_items_processed: int, 684 total_items_failed: int, 685 total_scores_created: int, 686 total_composite_scores_created: int, 687 total_evaluations_failed: int, 688 evaluator_stats: List[EvaluatorStats], 689 resume_token: Optional[BatchEvaluationResumeToken], 690 completed: bool, 691 duration_seconds: float, 692 failed_item_ids: List[str], 693 error_summary: Dict[str, int], 694 has_more_items: bool, 695 item_evaluations: Dict[str, List["Evaluation"]], 696 ): 697 """Initialize BatchEvaluationResult with comprehensive statistics. 698 699 Args: 700 total_items_fetched: Total items fetched from API. 701 total_items_processed: Items successfully evaluated. 702 total_items_failed: Items that failed evaluation. 703 total_scores_created: Scores from item-level evaluators. 704 total_composite_scores_created: Scores from composite evaluator. 705 total_evaluations_failed: Individual evaluator failures. 706 evaluator_stats: Per-evaluator statistics. 707 resume_token: Token for resuming (None if completed). 708 completed: Whether all items were processed. 709 duration_seconds: Total execution time. 710 failed_item_ids: IDs of failed items. 711 error_summary: Error types and counts. 712 has_more_items: Whether more items exist beyond max_items. 713 item_evaluations: Dictionary mapping item IDs to their evaluation results. 714 715 Note: 716 All arguments must be provided as keywords. 717 """ 718 self.total_items_fetched = total_items_fetched 719 self.total_items_processed = total_items_processed 720 self.total_items_failed = total_items_failed 721 self.total_scores_created = total_scores_created 722 self.total_composite_scores_created = total_composite_scores_created 723 self.total_evaluations_failed = total_evaluations_failed 724 self.evaluator_stats = evaluator_stats 725 self.resume_token = resume_token 726 self.completed = completed 727 self.duration_seconds = duration_seconds 728 self.failed_item_ids = failed_item_ids 729 self.error_summary = error_summary 730 self.has_more_items = has_more_items 731 self.item_evaluations = item_evaluations
Initialize BatchEvaluationResult with comprehensive statistics.
Arguments:
- total_items_fetched: Total items fetched from API.
- total_items_processed: Items successfully evaluated.
- total_items_failed: Items that failed evaluation.
- total_scores_created: Scores from item-level evaluators.
- total_composite_scores_created: Scores from composite evaluator.
- total_evaluations_failed: Individual evaluator failures.
- evaluator_stats: Per-evaluator statistics.
- resume_token: Token for resuming (None if completed).
- completed: Whether all items were processed.
- duration_seconds: Total execution time.
- failed_item_ids: IDs of failed items.
- error_summary: Error types and counts.
- has_more_items: Whether more items exist beyond max_items.
- item_evaluations: Dictionary mapping item IDs to their evaluation results.
Note:
All arguments must be provided as keywords.
1071class RunnerContext: 1072 """Wraps :meth:`Langfuse.run_experiment` with CI-injected defaults. 1073 1074 Intended for use with the ``langfuse/experiment-action`` GitHub Action 1075 (https://github.com/langfuse/experiment-action). The action builds a 1076 ``RunnerContext`` before invoking the user's ``experiment(context)`` 1077 function. Defaults set here (dataset, metadata tags) are applied when 1078 the user omits them on the :meth:`run_experiment` call; users can 1079 override any default by passing the corresponding argument explicitly. 1080 """ 1081 1082 def __init__( 1083 self, 1084 *, 1085 client: "Langfuse", 1086 data: Optional[ExperimentData] = None, 1087 dataset_version: Optional[datetime] = None, 1088 metadata: Optional[Dict[str, str]] = None, 1089 ): 1090 """Build a ``RunnerContext`` populated with defaults for ``run_experiment``. 1091 1092 Typically called by the ``langfuse/experiment-action`` GitHub Action, 1093 not by end users directly. Every field except ``client`` is optional: 1094 fields left as ``None`` simply mean the corresponding argument must be 1095 supplied on the :meth:`run_experiment` call. 1096 1097 Args: 1098 client: Initialized Langfuse SDK client used to execute the 1099 experiment. The action creates this from the 1100 ``langfuse_public_key`` / ``langfuse_secret_key`` / 1101 ``langfuse_base_url`` inputs. 1102 data: Default dataset items to run the experiment on. Accepts 1103 either ``List[LocalExperimentItem]`` or ``List[DatasetItem]``. 1104 Injected by the action when ``dataset_name`` is configured. 1105 If ``None``, the user must pass ``data=`` to 1106 :meth:`run_experiment`. 1107 dataset_version: Optional pinned dataset version. Injected by the 1108 action when ``dataset_version`` is configured. 1109 metadata: Default metadata attached to every experiment trace and 1110 the dataset run. The action injects GitHub-sourced tags (SHA, 1111 PR link, workflow run link, branch, GH user, etc.). Merged 1112 with any ``metadata`` passed to :meth:`run_experiment`, with 1113 user-supplied keys winning on collision. 1114 """ 1115 self.client = client 1116 self.data = data 1117 self.dataset_version = dataset_version 1118 self.metadata = metadata 1119 1120 def run_experiment( 1121 self, 1122 *, 1123 name: str, 1124 run_name: Optional[str] = None, 1125 description: Optional[str] = None, 1126 data: Optional[ExperimentData] = None, 1127 task: TaskFunction, 1128 evaluators: List[EvaluatorFunction] = [], 1129 composite_evaluator: Optional["CompositeEvaluatorFunction"] = None, 1130 run_evaluators: List[RunEvaluatorFunction] = [], 1131 max_concurrency: int = 50, 1132 metadata: Optional[Dict[str, str]] = None, 1133 _dataset_version: Optional[datetime] = None, 1134 ) -> ExperimentResult: 1135 resolved_data = data if data is not None else self.data 1136 if resolved_data is None: 1137 raise ValueError( 1138 "`data` must be provided either on the RunnerContext or the run_experiment call" 1139 ) 1140 1141 resolved_dataset_version = ( 1142 _dataset_version if _dataset_version is not None else self.dataset_version 1143 ) 1144 1145 merged_metadata: Optional[Dict[str, str]] 1146 if self.metadata is None and metadata is None: 1147 merged_metadata = None 1148 else: 1149 merged_metadata = {**(self.metadata or {}), **(metadata or {})} 1150 1151 return self.client.run_experiment( 1152 name=name, 1153 run_name=run_name, 1154 description=description, 1155 data=resolved_data, 1156 task=task, 1157 evaluators=evaluators, 1158 composite_evaluator=composite_evaluator, 1159 run_evaluators=run_evaluators, 1160 max_concurrency=max_concurrency, 1161 metadata=merged_metadata, 1162 _dataset_version=resolved_dataset_version, 1163 )
Wraps Langfuse.run_experiment() with CI-injected defaults.
Intended for use with the langfuse/experiment-action GitHub Action
(https://github.com/langfuse/experiment-action). The action builds a
RunnerContext before invoking the user's experiment(context)
function. Defaults set here (dataset, metadata tags) are applied when
the user omits them on the run_experiment() call; users can
override any default by passing the corresponding argument explicitly.
1082 def __init__( 1083 self, 1084 *, 1085 client: "Langfuse", 1086 data: Optional[ExperimentData] = None, 1087 dataset_version: Optional[datetime] = None, 1088 metadata: Optional[Dict[str, str]] = None, 1089 ): 1090 """Build a ``RunnerContext`` populated with defaults for ``run_experiment``. 1091 1092 Typically called by the ``langfuse/experiment-action`` GitHub Action, 1093 not by end users directly. Every field except ``client`` is optional: 1094 fields left as ``None`` simply mean the corresponding argument must be 1095 supplied on the :meth:`run_experiment` call. 1096 1097 Args: 1098 client: Initialized Langfuse SDK client used to execute the 1099 experiment. The action creates this from the 1100 ``langfuse_public_key`` / ``langfuse_secret_key`` / 1101 ``langfuse_base_url`` inputs. 1102 data: Default dataset items to run the experiment on. Accepts 1103 either ``List[LocalExperimentItem]`` or ``List[DatasetItem]``. 1104 Injected by the action when ``dataset_name`` is configured. 1105 If ``None``, the user must pass ``data=`` to 1106 :meth:`run_experiment`. 1107 dataset_version: Optional pinned dataset version. Injected by the 1108 action when ``dataset_version`` is configured. 1109 metadata: Default metadata attached to every experiment trace and 1110 the dataset run. The action injects GitHub-sourced tags (SHA, 1111 PR link, workflow run link, branch, GH user, etc.). Merged 1112 with any ``metadata`` passed to :meth:`run_experiment`, with 1113 user-supplied keys winning on collision. 1114 """ 1115 self.client = client 1116 self.data = data 1117 self.dataset_version = dataset_version 1118 self.metadata = metadata
Build a RunnerContext populated with defaults for run_experiment.
Typically called by the langfuse/experiment-action GitHub Action,
not by end users directly. Every field except client is optional:
fields left as None simply mean the corresponding argument must be
supplied on the run_experiment() call.
Arguments:
- client: Initialized Langfuse SDK client used to execute the
experiment. The action creates this from the
langfuse_public_key/langfuse_secret_key/langfuse_base_urlinputs. - data: Default dataset items to run the experiment on. Accepts
either
List[LocalExperimentItem]orList[DatasetItem]. Injected by the action whendataset_nameis configured. IfNone, the user must passdata=torun_experiment(). - dataset_version: Optional pinned dataset version. Injected by the
action when
dataset_versionis configured. - metadata: Default metadata attached to every experiment trace and
the dataset run. The action injects GitHub-sourced tags (SHA,
PR link, workflow run link, branch, GH user, etc.). Merged
with any
metadatapassed torun_experiment(), with user-supplied keys winning on collision.
1120 def run_experiment( 1121 self, 1122 *, 1123 name: str, 1124 run_name: Optional[str] = None, 1125 description: Optional[str] = None, 1126 data: Optional[ExperimentData] = None, 1127 task: TaskFunction, 1128 evaluators: List[EvaluatorFunction] = [], 1129 composite_evaluator: Optional["CompositeEvaluatorFunction"] = None, 1130 run_evaluators: List[RunEvaluatorFunction] = [], 1131 max_concurrency: int = 50, 1132 metadata: Optional[Dict[str, str]] = None, 1133 _dataset_version: Optional[datetime] = None, 1134 ) -> ExperimentResult: 1135 resolved_data = data if data is not None else self.data 1136 if resolved_data is None: 1137 raise ValueError( 1138 "`data` must be provided either on the RunnerContext or the run_experiment call" 1139 ) 1140 1141 resolved_dataset_version = ( 1142 _dataset_version if _dataset_version is not None else self.dataset_version 1143 ) 1144 1145 merged_metadata: Optional[Dict[str, str]] 1146 if self.metadata is None and metadata is None: 1147 merged_metadata = None 1148 else: 1149 merged_metadata = {**(self.metadata or {}), **(metadata or {})} 1150 1151 return self.client.run_experiment( 1152 name=name, 1153 run_name=run_name, 1154 description=description, 1155 data=resolved_data, 1156 task=task, 1157 evaluators=evaluators, 1158 composite_evaluator=composite_evaluator, 1159 run_evaluators=run_evaluators, 1160 max_concurrency=max_concurrency, 1161 metadata=merged_metadata, 1162 _dataset_version=resolved_dataset_version, 1163 )
1166class RegressionError(Exception): 1167 """Raised by a user's ``experiment`` function to signal a CI gate failure. 1168 1169 Intended for use with the ``langfuse/experiment-action`` GitHub Action 1170 (https://github.com/langfuse/experiment-action). The action catches this 1171 exception and, when ``should_fail_on_error`` is enabled, fails the 1172 workflow run and renders a callout in the PR comment using 1173 ``metric``/``value``/``threshold`` if supplied, otherwise ``str(exc)``. 1174 1175 Callers choose one of three forms: 1176 1177 - ``RegressionError(result=r)`` — minimal, generic message. 1178 - ``RegressionError(result=r, message="...")`` — free-form message. 1179 - ``RegressionError(result=r, metric="acc", value=0.7, threshold=0.9)`` — 1180 structured; ``metric`` and ``value`` must be provided together so the 1181 action can render a targeted callout without ``None`` placeholders. 1182 """ 1183 1184 @overload 1185 def __init__(self, *, result: ExperimentResult) -> None: ... 1186 @overload 1187 def __init__(self, *, result: ExperimentResult, message: str) -> None: ... 1188 @overload 1189 def __init__( 1190 self, 1191 *, 1192 result: ExperimentResult, 1193 metric: str, 1194 value: float, 1195 threshold: Optional[float] = None, 1196 message: Optional[str] = None, 1197 ) -> None: ... 1198 def __init__( 1199 self, 1200 *, 1201 result: ExperimentResult, 1202 metric: Optional[str] = None, 1203 value: Optional[float] = None, 1204 threshold: Optional[float] = None, 1205 message: Optional[str] = None, 1206 ): 1207 self.result = result 1208 self.metric = metric 1209 self.value = value 1210 self.threshold = threshold 1211 if message is not None: 1212 formatted = message 1213 elif metric is not None and value is not None: 1214 formatted = f"Regression on `{metric}`: {value} (threshold {threshold})" 1215 else: 1216 formatted = "Experiment regression detected" 1217 super().__init__(formatted)
Raised by a user's experiment function to signal a CI gate failure.
Intended for use with the langfuse/experiment-action GitHub Action
(https://github.com/langfuse/experiment-action). The action catches this
exception and, when should_fail_on_error is enabled, fails the
workflow run and renders a callout in the PR comment using
metric/value/threshold if supplied, otherwise str(exc).
Callers choose one of three forms:
RegressionError(result=r)— minimal, generic message.RegressionError(result=r, message="...")— free-form message.RegressionError(result=r, metric="acc", value=0.7, threshold=0.9)— structured;metricandvaluemust be provided together so the action can render a targeted callout withoutNoneplaceholders.
1198 def __init__( 1199 self, 1200 *, 1201 result: ExperimentResult, 1202 metric: Optional[str] = None, 1203 value: Optional[float] = None, 1204 threshold: Optional[float] = None, 1205 message: Optional[str] = None, 1206 ): 1207 self.result = result 1208 self.metric = metric 1209 self.value = value 1210 self.threshold = threshold 1211 if message is not None: 1212 formatted = message 1213 elif metric is not None and value is not None: 1214 formatted = f"Regression on `{metric}`: {value} (threshold {threshold})" 1215 else: 1216 formatted = "Experiment regression detected" 1217 super().__init__(formatted)
105def is_default_export_span(span: ReadableSpan) -> bool: 106 """Return whether a span should be exported by default.""" 107 return ( 108 is_langfuse_span(span) or is_genai_span(span) or is_known_llm_instrumentor(span) 109 )
Return whether a span should be exported by default.
68def is_langfuse_span(span: ReadableSpan) -> bool: 69 """Return whether the span was created by the Langfuse SDK tracer.""" 70 return ( 71 span.instrumentation_scope is not None 72 and span.instrumentation_scope.name == LANGFUSE_TRACER_NAME 73 )
Return whether the span was created by the Langfuse SDK tracer.
76def is_genai_span(span: ReadableSpan) -> bool: 77 """Return whether the span has any ``gen_ai.*`` semantic convention attribute.""" 78 if span.attributes is None: 79 return False 80 81 return any( 82 isinstance(key, str) and key.startswith("gen_ai") 83 for key in span.attributes.keys() 84 )
Return whether the span has any gen_ai.* semantic convention attribute.
92def is_known_llm_instrumentor(span: ReadableSpan) -> bool: 93 """Return whether the span comes from a known LLM instrumentation scope.""" 94 if span.instrumentation_scope is None: 95 return False 96 97 scope_name = span.instrumentation_scope.name 98 99 return any( 100 _matches_scope_prefix(scope_name, prefix) 101 for prefix in KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES 102 )
Return whether the span comes from a known LLM instrumentation scope.
226class MaskOtelSpansFunction(Protocol): 227 """Function protocol for export-stage OpenTelemetry span masking. 228 229 `mask_otel_spans` runs after Langfuse decides which spans this client should 230 export and after export-stage media handling has converted supported media 231 payloads into Langfuse media references. It affects only the spans exported 232 by this Langfuse client. If the same OpenTelemetry spans are sent to another 233 exporter, that exporter receives its own unmodified copy. 234 235 The function is synchronous. It usually runs on the OpenTelemetry batch span 236 processor worker thread; during `flush()` and shutdown it may run on the 237 caller thread. Keep it deterministic and fast, and avoid relying on request 238 locals, the current active span, or async I/O. 239 240 Return `None` to leave the whole batch unchanged, or return 241 `MaskOtelSpansResult` with sparse patches for the spans that should change. 242 243 Example: 244 ```python 245 from typing import Optional 246 247 from langfuse import Langfuse 248 from langfuse.types import ( 249 MaskOtelSpansParams, 250 MaskOtelSpansResult, 251 OtelSpanPatch, 252 ) 253 254 def mask_otel_spans( 255 *, params: MaskOtelSpansParams 256 ) -> Optional[MaskOtelSpansResult]: 257 patches = {} 258 259 for identifier, span in params.spans.items(): 260 if span.instrumentation_scope_name == "openai": 261 patches[identifier] = OtelSpanPatch( 262 delete_attributes=( 263 "gen_ai.prompt.0.content", 264 "gen_ai.completion.0.content", 265 ), 266 set_attributes={"masking.applied": True}, 267 ) 268 269 return MaskOtelSpansResult(span_patches=patches) 270 271 langfuse = Langfuse(mask_otel_spans=mask_otel_spans) 272 ``` 273 """ 274 275 def __call__( 276 self, *, params: MaskOtelSpansParams 277 ) -> Optional[MaskOtelSpansResult]: ...
Function protocol for export-stage OpenTelemetry span masking.
mask_otel_spans runs after Langfuse decides which spans this client should
export and after export-stage media handling has converted supported media
payloads into Langfuse media references. It affects only the spans exported
by this Langfuse client. If the same OpenTelemetry spans are sent to another
exporter, that exporter receives its own unmodified copy.
The function is synchronous. It usually runs on the OpenTelemetry batch span
processor worker thread; during flush() and shutdown it may run on the
caller thread. Keep it deterministic and fast, and avoid relying on request
locals, the current active span, or async I/O.
Return None to leave the whole batch unchanged, or return
MaskOtelSpansResult with sparse patches for the spans that should change.
Example:
from typing import Optional from langfuse import Langfuse from langfuse.types import ( MaskOtelSpansParams, MaskOtelSpansResult, OtelSpanPatch, ) def mask_otel_spans( *, params: MaskOtelSpansParams ) -> Optional[MaskOtelSpansResult]: patches = {} for identifier, span in params.spans.items(): if span.instrumentation_scope_name == "openai": patches[identifier] = OtelSpanPatch( delete_attributes=( "gen_ai.prompt.0.content", "gen_ai.completion.0.content", ), set_attributes={"masking.applied": True}, ) return MaskOtelSpansResult(span_patches=patches) langfuse = Langfuse(mask_otel_spans=mask_otel_spans)
1927def _no_init_or_replace_init(self, *args, **kwargs): 1928 cls = type(self) 1929 1930 if cls._is_protocol: 1931 raise TypeError('Protocols cannot be instantiated') 1932 1933 # Already using a custom `__init__`. No need to calculate correct 1934 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1935 if cls.__init__ is not _no_init_or_replace_init: 1936 return 1937 1938 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1939 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1940 # searches for a proper new `__init__` in the MRO. The new `__init__` 1941 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1942 # instantiation of the protocol subclass will thus use the new 1943 # `__init__` and no longer call `_no_init_or_replace_init`. 1944 for base in cls.__mro__: 1945 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1946 if init is not _no_init_or_replace_init: 1947 cls.__init__ = init 1948 break 1949 else: 1950 # should not happen 1951 cls.__init__ = object.__init__ 1952 1953 cls.__init__(self, *args, **kwargs)
123@dataclass(frozen=True) 124class MaskOtelSpansParams: 125 """Input passed to an export-stage OpenTelemetry span masking function. 126 127 A single call receives one OpenTelemetry export batch, not necessarily a 128 complete trace, request, or Langfuse observation tree. Batch contents depend 129 on OpenTelemetry span processor settings such as `flush_at`, 130 `flush_interval`, explicit `flush()`, and shutdown. If a batch contains 131 duplicate trace and span identifiers, Langfuse keeps only the last matching 132 span before calling the masking function. 133 134 Example: 135 ```python 136 from typing import Optional 137 138 from langfuse.types import ( 139 MaskOtelSpansParams, 140 MaskOtelSpansResult, 141 OtelSpanPatch, 142 ) 143 144 def mask_otel_spans( 145 *, params: MaskOtelSpansParams 146 ) -> Optional[MaskOtelSpansResult]: 147 patches = {} 148 149 for identifier, span in params.spans.items(): 150 if "http.request.header.authorization" in span.attributes: 151 patches[identifier] = OtelSpanPatch( 152 delete_attributes=("http.request.header.authorization",), 153 set_attributes={"security.redacted": True}, 154 ) 155 156 return MaskOtelSpansResult(span_patches=patches) 157 ``` 158 159 Attributes: 160 spans: Read-only mapping from stable span identifiers to span snapshots. 161 Return patches using keys from this mapping. 162 """ 163 164 spans: Mapping[OtelSpanIdentifier, OtelSpanData]
Input passed to an export-stage OpenTelemetry span masking function.
A single call receives one OpenTelemetry export batch, not necessarily a
complete trace, request, or Langfuse observation tree. Batch contents depend
on OpenTelemetry span processor settings such as flush_at,
flush_interval, explicit flush(), and shutdown. If a batch contains
duplicate trace and span identifiers, Langfuse keeps only the last matching
span before calling the masking function.
Example:
from typing import Optional from langfuse.types import ( MaskOtelSpansParams, MaskOtelSpansResult, OtelSpanPatch, ) def mask_otel_spans( *, params: MaskOtelSpansParams ) -> Optional[MaskOtelSpansResult]: patches = {} for identifier, span in params.spans.items(): if "http.request.header.authorization" in span.attributes: patches[identifier] = OtelSpanPatch( delete_attributes=("http.request.header.authorization",), set_attributes={"security.redacted": True}, ) return MaskOtelSpansResult(span_patches=patches)
Attributes:
- spans: Read-only mapping from stable span identifiers to span snapshots. Return patches using keys from this mapping.
202@dataclass(frozen=True) 203class MaskOtelSpansResult: 204 """Patches returned by a `mask_otel_spans` function. 205 206 Omit spans that do not need changes. A mapping value of `None` also leaves 207 that span unchanged. Returning an invalid patch to drop a span is not a 208 supported API; use `should_export_span` when you need span-level export 209 filtering. 210 211 If `mask_otel_spans` raises or returns an object that is not a 212 `MaskOtelSpansResult`, Langfuse drops the whole export batch. If one 213 individual `OtelSpanPatch` is invalid, Langfuse drops only that span from 214 the export batch. 215 216 Attributes: 217 span_patches: Mapping from identifiers in `MaskOtelSpansParams.spans` to 218 sparse attribute patches. 219 """ 220 221 span_patches: Mapping[OtelSpanIdentifier, Optional[OtelSpanPatch]] = field( 222 default_factory=lambda: MappingProxyType({}) 223 )
Patches returned by a mask_otel_spans function.
Omit spans that do not need changes. A mapping value of None also leaves
that span unchanged. Returning an invalid patch to drop a span is not a
supported API; use should_export_span when you need span-level export
filtering.
If mask_otel_spans raises or returns an object that is not a
MaskOtelSpansResult, Langfuse drops the whole export batch. If one
individual OtelSpanPatch is invalid, Langfuse drops only that span from
the export batch.
Attributes:
- span_patches: Mapping from identifiers in
MaskOtelSpansParams.spansto sparse attribute patches.
82@dataclass(frozen=True) 83class OtelSpanData: 84 """Read-only OpenTelemetry span snapshot passed to `mask_otel_spans`. 85 86 The snapshot contains the span data that Langfuse is about to export after 87 the SDK has applied `should_export_span` filtering and export-stage media 88 processing. The mappings are immutable views and mutating them is not 89 supported; return an `OtelSpanPatch` to change exported attributes. 90 91 `mask_otel_spans` can only change span attributes. It cannot change the 92 span name, IDs, parent relationship, resource attributes, events, links, or 93 instrumentation scope. 94 95 Attributes: 96 trace_id: Lowercase 32-character hexadecimal OpenTelemetry trace ID. 97 span_id: Lowercase 16-character hexadecimal OpenTelemetry span ID. 98 parent_span_id: Lowercase hexadecimal parent span ID, or `None` for a 99 root span or when the parent is not available. 100 name: OpenTelemetry span name. 101 instrumentation_scope_name: Name of the instrumentation scope that 102 emitted the span, for example `openai` or `langfuse`. 103 instrumentation_scope_version: Version of the instrumentation scope, if 104 the instrumentation library provided one. 105 attributes: Read-only attributes that will be exported unless patched. 106 Values use OpenTelemetry `AttributeValue` types: strings, booleans, 107 numbers, or homogeneous sequences of those scalar values. 108 resource_attributes: Read-only resource attributes from the span's 109 OpenTelemetry resource. These are available for decisions only and 110 cannot be patched through `mask_otel_spans`. 111 """ 112 113 trace_id: str 114 span_id: str 115 parent_span_id: Optional[str] 116 name: str 117 instrumentation_scope_name: Optional[str] 118 instrumentation_scope_version: Optional[str] 119 attributes: Mapping[str, AttributeValue] 120 resource_attributes: Mapping[str, AttributeValue]
Read-only OpenTelemetry span snapshot passed to mask_otel_spans.
The snapshot contains the span data that Langfuse is about to export after
the SDK has applied should_export_span filtering and export-stage media
processing. The mappings are immutable views and mutating them is not
supported; return an OtelSpanPatch to change exported attributes.
mask_otel_spans can only change span attributes. It cannot change the
span name, IDs, parent relationship, resource attributes, events, links, or
instrumentation scope.
Attributes:
- trace_id: Lowercase 32-character hexadecimal OpenTelemetry trace ID.
- span_id: Lowercase 16-character hexadecimal OpenTelemetry span ID.
- parent_span_id: Lowercase hexadecimal parent span ID, or
Nonefor a root span or when the parent is not available. - name: OpenTelemetry span name.
- instrumentation_scope_name: Name of the instrumentation scope that
emitted the span, for example
openaiorlangfuse. - instrumentation_scope_version: Version of the instrumentation scope, if the instrumentation library provided one.
- attributes: Read-only attributes that will be exported unless patched.
Values use OpenTelemetry
AttributeValuetypes: strings, booleans, numbers, or homogeneous sequences of those scalar values. - resource_attributes: Read-only resource attributes from the span's
OpenTelemetry resource. These are available for decisions only and
cannot be patched through
mask_otel_spans.
65@dataclass(frozen=True) 66class OtelSpanIdentifier: 67 """Stable key for one OpenTelemetry span in a masking batch. 68 69 Use this object as the key when returning a patch for a span. It is a 70 frozen, hashable dataclass, so the safest pattern is to reuse the exact 71 identifier object from `MaskOtelSpansParams.spans` instead of rebuilding it. 72 73 Attributes: 74 trace_id: Lowercase 32-character hexadecimal OpenTelemetry trace ID. 75 span_id: Lowercase 16-character hexadecimal OpenTelemetry span ID. 76 """ 77 78 trace_id: str 79 span_id: str
Stable key for one OpenTelemetry span in a masking batch.
Use this object as the key when returning a patch for a span. It is a
frozen, hashable dataclass, so the safest pattern is to reuse the exact
identifier object from MaskOtelSpansParams.spans instead of rebuilding it.
Attributes:
- trace_id: Lowercase 32-character hexadecimal OpenTelemetry trace ID.
- span_id: Lowercase 16-character hexadecimal OpenTelemetry span ID.
167@dataclass(frozen=True) 168class OtelSpanPatch: 169 """Attribute changes to apply to one OpenTelemetry span before export. 170 171 Patches are sparse: include only the attributes that should change. Langfuse 172 deletes `delete_attributes` first and then applies `set_attributes`, so a key 173 present in both fields is exported with the value from `set_attributes`. 174 175 Attribute values must be valid OpenTelemetry attributes: strings, booleans, 176 integers, floats, or homogeneous sequences of those scalar types. If one 177 value is not valid for OpenTelemetry, Langfuse removes that attribute from 178 the export rather than sending an invalid span. 179 180 Example: 181 ```python 182 OtelSpanPatch( 183 delete_attributes=("gen_ai.prompt.0.content",), 184 set_attributes={ 185 "gen_ai.prompt.redacted": True, 186 "app.masking.rule": "drop_prompt_text", 187 }, 188 ) 189 ``` 190 191 Attributes: 192 set_attributes: Attribute values to add or replace on the exported span. 193 delete_attributes: Attribute keys to remove from the exported span. 194 """ 195 196 set_attributes: Mapping[str, AttributeValue] = field( 197 default_factory=lambda: MappingProxyType({}) 198 ) 199 delete_attributes: Sequence[str] = field(default_factory=tuple)
Attribute changes to apply to one OpenTelemetry span before export.
Patches are sparse: include only the attributes that should change. Langfuse
deletes delete_attributes first and then applies set_attributes, so a key
present in both fields is exported with the value from set_attributes.
Attribute values must be valid OpenTelemetry attributes: strings, booleans, integers, floats, or homogeneous sequences of those scalar types. If one value is not valid for OpenTelemetry, Langfuse removes that attribute from the export rather than sending an invalid span.
Example:
OtelSpanPatch( delete_attributes=("gen_ai.prompt.0.content",), set_attributes={ "gen_ai.prompt.redacted": True, "app.masking.rule": "drop_prompt_text", }, )
Attributes:
- set_attributes: Attribute values to add or replace on the exported span.
- delete_attributes: Attribute keys to remove from the exported span.