Skip to content

Commit 957d067

Browse files
docs: upgrade docstrings as a first-class documentation surface for AI agents (#1760)
* docs: upgrade docstrings as a first-class documentation surface for AI agents Coding agents treat the installed package as primary documentation (help(), inspect, grepping site-packages), so this improves the docstrings they hit most, verified against code and tests: - propagate_attributes: fix broken examples that called it as a client method (it is a top-level import) and used the nonexistent start_generation; document wrap-the-root-span ordering; add See also - Langfuse.api/async_api: new docstrings covering list-vs-get semantics (list endpoints return observation/score IDs as strings), asynchronous ingestion with a bounded retry snippet, and scores read methods (get_many/get_by_id; there is no scores.get) - flush(): note that server-side ingestion lags flush by a few seconds - @observe: document capture_input/capture_output/transform_to_string, as_type="generation" with update_current_generation example - run_experiment/Evaluation: add RAG LLM-as-a-judge example (faithfulness, answer relevance) and CI regression testing via langfuse/experiment-action; docstring for RunnerContext.run_experiment - package docstring/README/pyproject: lead with full capability map, current env var names, llms.txt pointer; fix stale v3 docs link and broken langfuse.otel import in the Langfuse class example - deprecation hygiene: emit DeprecationWarning when host= or LANGFUSE_HOST is the effective base URL source (behavior change); deprecation messages now name the exact replacement import Generated code under langfuse/api/ is untouched; list-vs-get and ingestion notes for it live on the non-generated api property and should also be mirrored into the fern spec in langfuse/langfuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: remove runtime DeprecationWarning for host/LANGFUSE_HOST Keep the change docs-only; the deprecation remains documented in the host parameter docstring and the LANGFUSE_HOST env var description. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: align docstrings with langfuse.com docs wording and links Cross-checked against the langfuse-docs repo: - api/async_api/flush: align ingestion-lag wording with query-via-sdk docs (typically 15-30s, not "a few seconds"), replace retry snippet with the documented deadline-based pattern (capped backoff), note that a successful trace.get does not imply observations/scores are complete, recommend observations.get_many(trace_id=...) for row-level reads and the Metrics API for aggregation; add anchored doc links - run_experiment RAG example: use item.input (DatasetItem attribute) instead of item["input"], matching the documented dataset task shape - RegressionError: correct action input name to should_fail_on_regression (was should_fail_on_error; verified against experiment-action action.yml) - add See-also links: observation-types (observe/as_type) and environments (propagate_attributes) All 10 docstring URLs verified to map to content pages in langfuse-docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: highlight v2 data APIs as recommended, correct score read guidance - api property: call out api.observations and api.metrics as the recommended high-performance v2 endpoints (SDK v4 defaults) and note their v1 equivalents under api.legacy.* are less performant, not recommended for new workflows, and will be deprecated - scores: point reads at api.scores_v3.get_many_v3; the v2 api.scores.get_many/get_by_id are deprecated per the API spec and no longer available on Langfuse v4+ servers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: address PR review — runnable propagation example, observations-first examples - propagate_attributes example: start_observation() returns a plain observation (no __enter__), so both `with` usages crashed at runtime; switch to start_as_current_observation (smoke-ran the example) - api/async_api/flush docstrings: use the v2 observations endpoint (api.observations.get_many with fields selection) as the primary example per review, matching the query-via-sdk docs page examples Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: clarify v2 score reads are removed in future Langfuse v4 servers Current servers are v3.x; the spec's removal note is forward-looking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * edits * docs: revert deprecation message changes to keep PR strictly docs-only Restore the original @deprecated decorator strings on set_trace_io and set_current_trace_io so no runtime-visible string changes remain; the corrected propagate_attributes import guidance stays in the docstrings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * push --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 25257a5 commit 957d067

7 files changed

Lines changed: 182 additions & 23 deletions

File tree

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
[![Discord](https://img.shields.io/discord/1111061815649124414?style=flat-square&logo=Discord&logoColor=white&label=Discord&color=%23434EE4)](https://discord.gg/7NXusRtqYU)
1010
[![YC W23](https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square)](https://www.ycombinator.com/companies/langfuse)
1111

12+
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**.
13+
1214
## Installation
1315

1416
> [!IMPORTANT]
@@ -18,6 +20,35 @@
1820
pip install langfuse
1921
```
2022

23+
## Quickstart
24+
25+
```python
26+
# env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
27+
28+
from langfuse import get_client
29+
30+
langfuse = get_client()
31+
32+
# Create a span using a context manager
33+
with langfuse.start_as_current_observation(as_type="span", name="process-request") as span:
34+
# Your processing logic here
35+
span.update(output="Processing complete")
36+
37+
# Create a nested generation for an LLM call
38+
with langfuse.start_as_current_observation(as_type="generation", name="llm-response", model="gpt-5.6") as generation:
39+
# Your LLM call logic here
40+
generation.update(output="Generated response")
41+
42+
# All spans are automatically closed when exiting their context blocks
43+
44+
45+
# Flush events in short-lived applications
46+
langfuse.flush()
47+
```
48+
2149
## Docs
2250

23-
Please [see our docs](https://langfuse.com/docs/sdk/python/sdk-v3) for detailed information on this SDK.
51+
- SDK guide: https://langfuse.com/docs/observability/sdk/overview
52+
- Full documentation: https://langfuse.com/docs
53+
- Machine-readable docs index (for AI agents): https://langfuse.com/llms.txt
54+
- API reference of this package: https://python.reference.langfuse.com

langfuse/__init__.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,53 @@
1-
""".. include:: ../README.md"""
1+
"""Langfuse Python SDK — observability, evaluation, and prompt management for LLM applications.
2+
3+
Capabilities:
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+
19+
Quickstart:
20+
21+
```python
22+
# env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
23+
from langfuse import get_client
24+
25+
langfuse = get_client()
26+
27+
# Create a span using a context manager
28+
with 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
40+
langfuse.flush()
41+
```
42+
43+
Configuration 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`
45+
for the full list.
46+
47+
Docs: https://langfuse.com/docs — machine-readable index: https://langfuse.com/llms.txt
48+
49+
.. include:: ../README.md
50+
"""
251

352
from langfuse.batch_evaluation import (
453
BatchEvaluationResult,

langfuse/_client/client.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,13 @@ def mask_otel_spans(
248248
249249
Example:
250250
```python
251-
from langfuse.otel import Langfuse
251+
from langfuse import Langfuse
252252
253253
# Initialize the client (reads from env vars if not provided)
254254
langfuse = Langfuse(
255255
public_key="your-public-key",
256256
secret_key="your-secret-key",
257-
host="https://cloud.langfuse.com", # Optional, default shown
257+
base_url="https://cloud.langfuse.com", # Optional, default shown
258258
)
259259
260260
# Create a trace span
@@ -421,6 +421,43 @@ def __init__(
421421

422422
@property
423423
def api(self) -> LangfuseAPI:
424+
"""Synchronous client for the full Langfuse REST API (traces, observations, scores, datasets, prompts, ...).
425+
426+
Use this to read or manage data on the Langfuse server; use the tracing methods
427+
(`start_observation`, `@observe`) to create traces. Use `async_api` for the
428+
asyncio variant.
429+
430+
Semantics that are easy to miss:
431+
432+
- **Ingestion is asynchronous.** `langfuse.flush()` only guarantees delivery to
433+
the API, not read visibility: reads such as `api.trace.get(trace_id)` may
434+
raise `langfuse.api.NotFoundError` until processing completes (typically
435+
within 15-30 seconds; longer under load). The same applies to scores and
436+
dataset run reads. Instead of a fixed sleep, retry with a deadline:
437+
438+
- **List endpoints return lightweight views.** `api.trace.list(...)` returns
439+
`TraceWithDetails`, where `observations` and `scores` are lists of ID strings.
440+
Fetch the full objects with `api.trace.get(trace_id)` (`TraceWithFullDetails`),
441+
or prefer `api.observations.get_many(trace_id=...)` for row-level observation
442+
queries. The same list-view vs. get-detail pattern applies to other resources.
443+
444+
- **Prefer the v2 data APIs — they are the defaults since SDK v4.**
445+
`api.observations` and `api.metrics` map to the high-performance
446+
`/api/public/v2/...` endpoints and are the recommended read path. Their v1
447+
equivalents remain available under `api.legacy.observations_v1` /
448+
`api.legacy.metrics_v1` but are less performant at scale, not recommended
449+
for new workflows, and will be deprecated.
450+
451+
- For large-scale aggregation (usage/cost by model, user, etc.), prefer the
452+
v2 Metrics API (`api.metrics.metrics(...)`) over paginating row-level data.
453+
454+
455+
See also: `async_api`,
456+
https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk
457+
(ingestion lag: #ingestion-lag, list vs. get: #traces-list-vs-get),
458+
https://langfuse.com/docs/api-and-data-platform/features/observations-api,
459+
https://langfuse.com/docs/metrics/features/metrics-api
460+
"""
424461
if self._resources is None:
425462
raise AttributeError("Langfuse client is not initialized")
426463

@@ -1519,7 +1556,7 @@ def set_current_trace_io(
15191556
evaluators). It will be removed in a future major version.
15201557
15211558
For setting other trace attributes (user_id, session_id, metadata, tags, version),
1522-
use :meth:`propagate_attributes` instead.
1559+
use :func:`langfuse.propagate_attributes` (top-level import) instead.
15231560
15241561
Args:
15251562
input: Input data to associate with the trace.
@@ -2239,6 +2276,16 @@ def flush(self) -> None:
22392276
22402277
# Continue with other work
22412278
```
2279+
2280+
Note:
2281+
`flush()` guarantees data was *delivered* to the API, not that it is
2282+
*readable* yet: server-side ingestion is asynchronous, so flushed data
2283+
may not be queryable for 15-30 seconds —
2284+
`api.observations.get_many(trace_id=...)` may return empty results and
2285+
`api.trace.get()` may raise `langfuse.api.NotFoundError` right after a
2286+
successful flush. See the `api` property docs for a bounded retry
2287+
pattern, or
2288+
https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk#ingestion-lag
22422289
"""
22432290
if self._resources is not None:
22442291
self._resources.flush()

langfuse/_client/observe.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,18 @@ def observe(
109109
as_type (Optional[Literal]): Set the observation type. Supported values:
110110
"generation", "span", "agent", "tool", "chain", "retriever", "embedding", "evaluator", "guardrail".
111111
Observation types are highlighted in the Langfuse UI for filtering and visualization.
112-
The types "generation" and "embedding" create a span on which additional attributes such as model metrics
113-
can be set.
112+
The types "generation" and "embedding" create a span on which additional attributes such as model,
113+
usage_details, and cost_details can be set — use `as_type="generation"` for LLM calls and update the
114+
observation via `langfuse.update_current_generation(...)` inside the function.
115+
capture_input (Optional[bool]): Whether to capture the function's arguments as the observation's input.
116+
Defaults to the LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED environment variable (True if unset).
117+
Set to False for sensitive or very large inputs, then set input explicitly via
118+
`langfuse.update_current_span(input=...)` if needed.
119+
capture_output (Optional[bool]): Whether to capture the function's return value as the observation's output.
120+
Same default and override mechanism as capture_input.
121+
transform_to_string (Optional[Callable[[Iterable], str]]): For functions returning generators, joins the
122+
yielded chunks into the string stored as output. Without it, chunks are concatenated if all are
123+
strings, otherwise stored as a list.
114124
115125
Returns:
116126
Callable: A wrapped version of the original function that automatically creates and manages Langfuse spans.
@@ -126,16 +136,25 @@ def process_user_request(user_id, query):
126136
127137
For language model generation tracking:
128138
```python
139+
from langfuse import get_client, observe
140+
129141
@observe(name="answer-generation", as_type="generation")
130142
async def generate_answer(query):
131-
# Creates a generation-type span with extended LLM metrics
143+
# Creates a generation-type observation with extended LLM metrics
132144
response = await openai.chat.completions.create(
133145
model="gpt-4",
134146
messages=[{"role": "user", "content": query}]
135147
)
136148
return response.choices[0].message.content
137149
```
138150
151+
Disabling input/output capture (e.g. for sensitive or large payloads):
152+
```python
153+
@observe(capture_input=False, capture_output=False)
154+
def handle_pii(user_record):
155+
return process(user_record)
156+
```
157+
139158
For trace context propagation between functions:
140159
```python
141160
@observe()

langfuse/_client/propagation.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,13 @@ def propagate_attributes(
131131
environment, and metadata dimensions that should be consistently applied across
132132
all observations in a trace.
133133
134-
**IMPORTANT**: Call this as early as possible within your trace/workflow. Only the
135-
currently active span and spans created after entering this context will have these
136-
attributes. Pre-existing spans will NOT be retroactively updated.
134+
This is a module-level function, not a method on the Langfuse client:
135+
import it with `from langfuse import propagate_attributes`.
136+
137+
**IMPORTANT**: Call this as early as possible within your trace/workflow —
138+
ideally wrapping the creation of your root span, or immediately inside it. Only
139+
the currently active span and spans created after entering this context will have
140+
these attributes. Pre-existing spans will NOT be retroactively updated.
137141
138142
**Why this matters**: Langfuse aggregation queries (e.g., total cost by user_id,
139143
filtering by session_id) only include observations that have the attribute set.
@@ -188,40 +192,43 @@ def propagate_attributes(
188192
Context manager that propagates attributes to all child spans.
189193
190194
Example:
191-
Basic usage with user and session tracking:
195+
Basic usage with user and session tracking (note: `propagate_attributes` is a
196+
top-level import, not a client method):
192197
193198
```python
194-
from langfuse import Langfuse
199+
from langfuse import Langfuse, propagate_attributes
195200
196201
langfuse = Langfuse()
197202
198-
# Set attributes early in the trace
203+
# Set attributes early: wrap everything inside the root span
199204
with langfuse.start_as_current_observation(name="user_workflow") as span:
200-
with langfuse.propagate_attributes(
205+
with propagate_attributes(
201206
user_id="user_123",
202207
session_id="session_abc",
203208
environment="production",
204209
metadata={"experiment": "variant_a"}
205210
):
206211
# All spans created here will have user_id, session_id, environment, and metadata
207-
with langfuse.start_observation(name="llm_call") as llm_span:
212+
with langfuse.start_as_current_observation(name="llm_call") as llm_span:
208213
# This span inherits user_id, session_id, environment, and experiment metadata
209214
...
210215
211-
with langfuse.start_generation(name="completion") as gen:
216+
with langfuse.start_as_current_observation(
217+
name="completion", as_type="generation"
218+
) as gen:
212219
# This span also inherits all attributes
213220
...
214221
```
215222
216223
Prompt linking with auto-instrumented libraries:
217224
218225
```python
219-
from langfuse import Langfuse
226+
from langfuse import Langfuse, propagate_attributes
220227
221228
langfuse = Langfuse()
222229
prompt = langfuse.get_prompt("my-prompt")
223230
224-
with langfuse.propagate_attributes(prompt=prompt):
231+
with propagate_attributes(prompt=prompt):
225232
# Generations emitted by auto-instrumentation (LiteLLM langfuse_otel,
226233
# OpenAI Agents SDK, OpenInference, ...) within this context are
227234
# linked to the prompt version.
@@ -240,7 +247,7 @@ def propagate_attributes(
240247
early_span.end()
241248
242249
# Set attributes in the middle
243-
with langfuse.propagate_attributes(user_id="user_123"):
250+
with propagate_attributes(user_id="user_123"):
244251
# Only spans created AFTER this point will have user_id
245252
late_span = langfuse.start_observation(name="late_work")
246253
late_span.end()
@@ -253,7 +260,7 @@ def propagate_attributes(
253260
```python
254261
# Service A - originating service
255262
with langfuse.start_as_current_observation(name="api_request"):
256-
with langfuse.propagate_attributes(
263+
with propagate_attributes(
257264
user_id="user_123",
258265
session_id="session_abc",
259266
environment="staging",
@@ -282,6 +289,12 @@ def propagate_attributes(
282289
283290
Raises:
284291
No exceptions are raised. Invalid values are logged as warnings and dropped.
292+
293+
See also:
294+
`Langfuse.start_as_current_observation` (create the root span this wraps),
295+
https://langfuse.com/docs/observability/features/sessions,
296+
https://langfuse.com/docs/observability/features/users,
297+
https://langfuse.com/docs/observability/features/environments
285298
"""
286299
return _propagate_attributes(
287300
user_id=user_id,

langfuse/_client/span.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def set_trace_io(
245245
evaluators). It will be removed in a future major version.
246246
247247
For setting other trace attributes (user_id, session_id, metadata, tags, version),
248-
use :meth:`Langfuse.propagate_attributes` instead.
248+
use :func:`langfuse.propagate_attributes` (top-level import) instead.
249249
250250
Args:
251251
input: Input data to associate with the trace.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "langfuse"
33
version = "4.14.0"
4-
description = "A client library for accessing langfuse"
4+
description = "Langfuse Python SDK - LLM observability/tracing, datasets, experiments, LLM-as-a-judge evaluation, and prompt management"
55
readme = "README.md"
66
authors = [{ name = "langfuse", email = "developers@langfuse.com" }]
77
license = "MIT"

0 commit comments

Comments
 (0)