Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
[![Discord](https://img.shields.io/discord/1111061815649124414?style=flat-square&logo=Discord&logoColor=white&label=Discord&color=%23434EE4)](https://discord.gg/7NXusRtqYU)
[![YC W23](https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square)](https://www.ycombinator.com/companies/langfuse)

The [Langfuse](https://langfuse.com) Python SDK covers the full platform: **observability/tracing** (OpenTelemetry-based, with OpenAI and LangChain integrations), **datasets & experiments** (offline evaluation and regression testing of prompt/model changes, including [CI via GitHub Actions](https://github.com/langfuse/experiment-action)), **LLM-as-a-judge and custom evaluations/scores**, **prompt management**, and a **full REST API client**.

## Installation

> [!IMPORTANT]
Expand All @@ -18,6 +20,35 @@
pip install langfuse
```

## Quickstart

```python
# 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-5.6") 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()
```

## Docs

Please [see our docs](https://langfuse.com/docs/sdk/python/sdk-v3) for detailed information on this SDK.
- SDK guide: https://langfuse.com/docs/observability/sdk/overview
- Full documentation: https://langfuse.com/docs
- Machine-readable docs index (for AI agents): https://langfuse.com/llms.txt
- API reference of this package: https://python.reference.langfuse.com
51 changes: 50 additions & 1 deletion langfuse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,53 @@
""".. include:: ../README.md"""
"""Langfuse Python SDK — observability, evaluation, and prompt management for LLM applications.

Capabilities:

- **Tracing / observability**: `@observe` decorator, `Langfuse.start_observation` /
`start_as_current_observation` context 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_experiment` for
offline evaluation and regression testing of prompt/model changes (CI support via
https://github.com/langfuse/experiment-action and `RegressionError`).
- **Evaluation / LLM-as-a-judge**: `Evaluation` results from custom or model-based
evaluators; scores via `Langfuse.create_score` / `span.score`.
- **Prompt management**: `Langfuse.get_prompt`, `Langfuse.create_prompt` with
client-side caching and version/label control.
- **Full REST API**: `Langfuse.api` (sync) / `Langfuse.async_api` (async) clients.

Quickstart:

```python
# 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

.. include:: ../README.md
"""

from langfuse.batch_evaluation import (
BatchEvaluationResult,
Expand Down
53 changes: 50 additions & 3 deletions langfuse/_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,13 @@

Example:
```python
from langfuse.otel import Langfuse
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",
host="https://cloud.langfuse.com", # Optional, default shown
base_url="https://cloud.langfuse.com", # Optional, default shown
)

# Create a trace span
Expand Down Expand Up @@ -420,7 +420,44 @@
)

@property
def api(self) -> LangfuseAPI:
"""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.

Check warning on line 428 in langfuse/_client/client.py

View check run for this annotation

Claude / Claude Code Review

async_api property still has no docstring despite PR claiming both were documented

The PR description says property docstrings were added for both `Langfuse.api` and `Langfuse.async_api` (including scores `get_many`/`get_by_id` guidance), but only `api` (client.py:423-460) actually received one — `async_api` (client.py:473-478) remains undocumented, and the scores guidance the description mentions doesn't appear anywhere in the file.
Comment on lines 423 to +428

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The PR description says property docstrings were added for both Langfuse.api and Langfuse.async_api (including scores get_many/get_by_id guidance), but only api (client.py:423-460) actually received one — async_api (client.py:473-478) remains undocumented, and the scores guidance the description mentions doesn't appear anywhere in the file.

Extended reasoning...

The PR's stated goal is to make the SDK's docstrings a first-class documentation surface, and its description explicitly frames the api/async_api work as a pair: "Langfuse.api / async_api — new property docstrings (previously none)", going on to list several semantics it claims were documented, including "scores are read via get_many/get_by_id — there is no api.scores.get".

In the actual diff, only the api property (client.py:423-460) gets the new docstring covering ingestion lag, list-vs-get semantics, and the v2 metrics/observations APIs. The async_api property, defined a few lines below right after the api setter (client.py:473-478), is completely unchanged by this PR and still has no docstring at all — its body goes straight from the @property decorator to the if self._resources is None guard. help(langfuse.async_api) / inspect.getdoc(langfuse.async_api) return None, while the same calls on langfuse.api now return the new text. Additionally, grepping client.py confirms the promised scores get_many/get_by_id/no-scores.get guidance was never actually written into either docstring — the new api docstring only mentions api.observations.get_many, not scores.

Concretely: (1) open client.py, jump to line 423 — the new docstring is present on api. (2) Scroll to line 473 — async_api's body has no docstring statement whatsoever. (3) Run import inspect; from langfuse import Langfuse; inspect.getdoc(Langfuse.async_api) — it returns None, contradicting the PR description's claim that both properties got documented. (4) Search the whole file for 'get_by_id' or 'scores are read' — no match, confirming that specific claimed addition was never written.

Since async_api is a first-class, identically-used surface for asyncio consumers (same underlying REST client, same ingestion-lag/list-vs-get caveats apply), leaving it silently undocumented while the sync twin gets detailed guidance is a real, in-scope completeness gap for a PR whose entire purpose is upgrading exactly this kind of documentation. It is not a functional defect — nothing crashes or behaves incorrectly at runtime, and the api docstring does at least point readers to async_api as 'the asyncio variant' — so it doesn't block merging. The fix is trivial: mirror the api docstring (or a short pointer with a note about awaiting the async client methods) onto async_api, and add the scores get_many/get_by_id guidance that was described but never written.


Semantics that are easy to miss:

- **Ingestion is asynchronous.** `langfuse.flush()` only guarantees delivery to
the API, not read visibility: reads such as `api.trace.get(trace_id)` may
raise `langfuse.api.NotFoundError` until 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:
Comment on lines +432 to +436

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dangling sentence — promised retry example is missing

The bullet ends with "Instead of a fixed sleep, retry with a deadline:" (colon implies a code block follows), but the next line is the next bullet point. The retry snippet that was meant to appear here was described in the PR as a key addition for agents, but it was never included. Any agent or developer reading this will be left without the concrete pattern to copy. The flush() docstring also cross-references "the api property docs for a bounded retry pattern", pointing at this non-existent snippet.

Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/_client/client.py
Line: 432-436

Comment:
**Dangling sentence — promised retry example is missing**

The bullet ends with "Instead of a fixed sleep, retry with a deadline:" (colon implies a code block follows), but the next line is the next bullet point. The retry snippet that was meant to appear here was described in the PR as a key addition for agents, but it was never included. Any agent or developer reading this will be left without the concrete pattern to copy. The `flush()` docstring also cross-references "the `api` property docs for a bounded retry pattern", pointing at this non-existent snippet.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


Check warning on line 437 in langfuse/_client/client.py

View check run for this annotation

Claude / Claude Code Review

api docstring promises a retry snippet that is never shown

The `api` property docstring (langfuse/_client/client.py:436) ends its ingestion-lag bullet with "Instead of a fixed sleep, retry with a deadline:" — a colon that promises a code snippet, but no snippet follows anywhere in the docstring. The PR description itself claims a "bounded retry snippet" was added, and the `flush()` docstring added in this same PR cross-references "the api property docs for a bounded retry pattern", which now points to nothing. Either add the promised retry example or re
Comment on lines +433 to +437

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The api property docstring (langfuse/_client/client.py:436) ends its ingestion-lag bullet with "Instead of a fixed sleep, retry with a deadline:" — a colon that promises a code snippet, but no snippet follows anywhere in the docstring. The PR description itself claims a "bounded retry snippet" was added, and the flush() docstring added in this same PR cross-references "the api property docs for a bounded retry pattern", which now points to nothing. Either add the promised retry example or remove the dangling colon and the dead cross-reference.

Extended reasoning...

The bug: The new api property docstring in langfuse/_client/client.py (lines 424-460) has a bullet on ingestion lag that ends with:

Instead of a fixed sleep, retry with a deadline:

A trailing colon here unambiguously signals that a code example follows — that's the standard convention this same PR uses elsewhere (e.g. the Example: sections in observe.py and propagation.py always follow a colon with a fenced python block). But the very next line in this docstring is blank, and the line after that immediately starts the next, unrelated bullet ("List endpoints return lightweight views..."). Scanning the full docstring body (lines 424-460) confirms there is no code block anywhere in it.

Corroborating evidence this isn't just a stray colon: Two independent signals confirm a snippet was intended and simply never written. First, the PR description explicitly states the api property docs add ingestion-lag guidance "with a bounded retry snippet" — the author's own summary says this snippet exists. Second, this same PR adds a Note: block to flush() (client.py, ~line 2286) that says: "See the api property docs for a bounded retry pattern" — a cross-reference that, as written, points at nothing, since no retry pattern/snippet actually appears in the api docstring.

Why nothing else catches this: This is purely docstring text with no executable code, so none of the PR's own verification steps surface it — ruff format --check/ruff check/mypy don't parse docstring prose, the unit test suite doesn't inspect docstring content, and the PR's scripted check only verifies that embedded python examples compile — it doesn't check that every colon promising an example actually has one attached.

Impact: This PR's stated goal is to make docstrings a reliable, first-class documentation surface that AI coding agents and humans read via help()/inspect.getdoc()/pdoc instead of visiting langfuse.com. A reader (human or agent) who hits NotFoundError right after flush(), follows the flush() docstring's pointer to "the api property docs for a bounded retry pattern," and then follows the api docstring's own colon expecting a retry example, gets nothing actionable in either place. That's exactly the failure mode this PR is trying to eliminate, reintroduced in the very code it touches.

Step-by-step proof:

  1. Open help(Langfuse.api) or read langfuse/_client/client.py lines 424-460 directly.
  2. Locate the first bullet: "Ingestion is asynchronous. langfuse.flush() only guarantees delivery... Instead of a fixed sleep, retry with a deadline:"
  3. Look at the next line: it is blank.
  4. Look at the line after that: "- List endpoints return lightweight views. ..." — a new, unrelated bullet has already started.
  5. Grep the whole docstring (lines 424-460) for a fenced code block or any indented Python snippet: there is none.
  6. Cross-check against flush()'s docstring Note (added in the same PR): it says "See the api property docs for a bounded retry pattern" — but step 5 already showed that pattern doesn't exist, so this cross-reference is dead.
  7. Cross-check against the PR description: it explicitly claims the api docs were updated "with a bounded retry snippet" — confirming the snippet was intended but never actually written into the docstring.

Fix: Either add the promised bounded-retry example (e.g. a small python block using a deadline loop against api.trace.get(trace_id) catching NotFoundError), or, if the snippet is being deferred, change the trailing colon to a period and drop/rewrite the flush() cross-reference so it doesn't point at nonexistent content.

- **List endpoints return lightweight views.** `api.trace.list(...)` returns
`TraceWithDetails`, where `observations` and `scores` are lists of ID strings.
Fetch the full objects with `api.trace.get(trace_id)` (`TraceWithFullDetails`),
or prefer `api.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.observations` and `api.metrics` map to the high-performance
`/api/public/v2/...` endpoints and are the recommended read path. Their v1
equivalents remain available under `api.legacy.observations_v1` /
`api.legacy.metrics_v1` but 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
"""
if self._resources is None:
raise AttributeError("Langfuse client is not initialized")

Expand Down Expand Up @@ -1519,7 +1556,7 @@
evaluators). It will be removed in a future major version.

For setting other trace attributes (user_id, session_id, metadata, tags, version),
use :meth:`propagate_attributes` instead.
use :func:`langfuse.propagate_attributes` (top-level import) instead.

Args:
input: Input data to associate with the trace.
Expand Down Expand Up @@ -2239,6 +2276,16 @@

# 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 and
`api.trace.get()` may raise `langfuse.api.NotFoundError` right after a
successful flush. See the `api` property docs for a bounded retry
pattern, or
https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk#ingestion-lag
"""
if self._resources is not None:
self._resources.flush()
Expand Down
25 changes: 22 additions & 3 deletions langfuse/_client/observe.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,18 @@ def observe(
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 metrics
can be set.
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 via `langfuse.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.
Expand All @@ -126,16 +136,25 @@ def process_user_request(user_id, query):

For language model generation tracking:
```python
from langfuse import get_client, observe

@observe(name="answer-generation", as_type="generation")
async def generate_answer(query):
# Creates a generation-type span with extended LLM metrics
# 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.content
```

Disabling input/output capture (e.g. for sensitive or large payloads):
```python
@observe(capture_input=False, capture_output=False)
def handle_pii(user_record):
return process(user_record)
```

For trace context propagation between functions:
```python
@observe()
Expand Down
39 changes: 26 additions & 13 deletions langfuse/_client/propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,13 @@ def propagate_attributes(
environment, and metadata dimensions that should be consistently applied across
all observations in a trace.

**IMPORTANT**: Call this as early as possible within your trace/workflow. Only the
currently active span and spans created after entering this context will have these
attributes. Pre-existing spans will NOT be retroactively updated.
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.
Expand Down Expand Up @@ -188,40 +192,43 @@ def propagate_attributes(
Context manager that propagates attributes to all child spans.

Example:
Basic usage with user and session tracking:
Basic usage with user and session tracking (note: `propagate_attributes` is a
top-level import, not a client method):

```python
from langfuse import Langfuse
from langfuse import Langfuse, propagate_attributes

langfuse = Langfuse()

# Set attributes early in the trace
# Set attributes early: wrap everything inside the root span
with langfuse.start_as_current_observation(name="user_workflow") as span:
with langfuse.propagate_attributes(
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_observation(name="llm_call") as llm_span:
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_generation(name="completion") as gen:
with langfuse.start_as_current_observation(
name="completion", as_type="generation"
) as gen:
Comment thread
claude[bot] marked this conversation as resolved.
# This span also inherits all attributes
...
```

Prompt linking with auto-instrumented libraries:

```python
from langfuse import Langfuse
from langfuse import Langfuse, propagate_attributes

langfuse = Langfuse()
prompt = langfuse.get_prompt("my-prompt")

with langfuse.propagate_attributes(prompt=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.
Expand All @@ -240,7 +247,7 @@ def propagate_attributes(
early_span.end()

# Set attributes in the middle
with langfuse.propagate_attributes(user_id="user_123"):
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()
Expand All @@ -253,7 +260,7 @@ def propagate_attributes(
```python
# Service A - originating service
with langfuse.start_as_current_observation(name="api_request"):
with langfuse.propagate_attributes(
with propagate_attributes(
user_id="user_123",
session_id="session_abc",
environment="staging",
Expand Down Expand Up @@ -282,6 +289,12 @@ def propagate_attributes(

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
"""
return _propagate_attributes(
user_id=user_id,
Expand Down
2 changes: 1 addition & 1 deletion langfuse/_client/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def set_trace_io(
evaluators). It will be removed in a future major version.

For setting other trace attributes (user_id, session_id, metadata, tags, version),
use :meth:`Langfuse.propagate_attributes` instead.
use :func:`langfuse.propagate_attributes` (top-level import) instead.

Args:
input: Input data to associate with the trace.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "langfuse"
version = "4.14.0"
description = "A client library for accessing langfuse"
description = "Langfuse Python SDK - LLM observability/tracing, datasets, experiments, LLM-as-a-judge evaluation, and prompt management"
readme = "README.md"
authors = [{ name = "langfuse", email = "developers@langfuse.com" }]
license = "MIT"
Expand Down