Skip to content

Commit f25b8cf

Browse files
committed
Merge branch 'main' into marlies/lfe-dataset-fetch-consistencies
2 parents a162a6d + 25c5ef9 commit f25b8cf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+3845
-386
lines changed

.github/workflows/release.yml

Lines changed: 439 additions & 10 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ This is the Langfuse Python SDK, a client library for accessing the Langfuse obs
1212
```bash
1313
# Install Poetry plugins (one-time setup)
1414
poetry self add poetry-dotenv-plugin
15-
poetry self add poetry-bumpversion
1615

1716
# Install all dependencies including optional extras
1817
poetry install --all-extras
@@ -50,16 +49,21 @@ poetry run pre-commit run --all-files
5049

5150
### Building and Releasing
5251
```bash
53-
# Build the package
52+
# Build the package locally (for testing)
5453
poetry build
5554

56-
# Run release script (handles versioning, building, tagging, and publishing)
57-
poetry run release
58-
5955
# Generate documentation
6056
poetry run pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse
6157
```
6258

59+
Releases are automated via GitHub Actions. To release:
60+
1. Go to Actions > "Release Python SDK" workflow
61+
2. Click "Run workflow"
62+
3. Select version bump type (patch/minor/major/prerelease)
63+
4. For prereleases, select the type (alpha/beta/rc)
64+
65+
The workflow handles versioning, building, PyPI publishing (via OIDC), and GitHub release creation.
66+
6367
## Architecture
6468

6569
### Core Components
@@ -112,7 +116,7 @@ Environment variables (defined in `_client/environment_variables.py`):
112116
- `pyproject.toml`: Poetry configuration, dependencies, and tool settings
113117
- `ruff.toml`: Local development linting config (stricter)
114118
- `ci.ruff.toml`: CI linting config (more permissive)
115-
- `langfuse/version.py`: Version string (updated by release script)
119+
- `langfuse/version.py`: Version string (updated by CI release workflow)
116120

117121
## API Generation
118122

CONTRIBUTING.md

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
```
88
poetry self add poetry-dotenv-plugin
9-
poetry self add poetry-bumpversion
109
```
1110

1211
### Install dependencies
@@ -58,34 +57,25 @@ poetry run mypy langfuse --no-error-summary
5857

5958
### Publish release
6059

61-
#### Release script
62-
63-
Make sure you have your PyPi API token setup in your poetry config. If not, you can set it up by running:
64-
65-
```sh
66-
poetry config pypi-token.pypi $your-api-token
67-
```
68-
69-
Run the release script:
70-
71-
```sh
72-
poetry run release
73-
```
74-
75-
#### Manual steps (for prepatch versions)
76-
77-
1. `poetry version patch`
78-
- `poetry version prepatch` for pre-release versions
79-
2. `poetry install`
80-
3. `poetry build`
81-
4. `git commit -am "chore: release v{version}"`
82-
5. `git push`
83-
6. `git tag v{version}`
84-
7. `git push --tags`
85-
8. `poetry publish`
86-
- Create PyPi API token: <https://pypi.org/manage/account/token/>
87-
- Setup: `poetry config pypi-token.pypi your-api-token`
88-
9. Create a release on GitHub with the changelog
60+
Releases are automated via GitHub Actions using PyPI Trusted Publishing (OIDC).
61+
62+
To create a release:
63+
64+
1. Go to [Actions > Release Python SDK](https://github.com/langfuse/langfuse-python/actions/workflows/release.yml)
65+
2. Click "Run workflow"
66+
3. Select the version bump type:
67+
- `patch` - Bug fixes (1.0.0 → 1.0.1)
68+
- `minor` - New features (1.0.0 → 1.1.0)
69+
- `major` - Breaking changes (1.0.0 → 2.0.0)
70+
- `prerelease` - Pre-release versions (1.0.0 → 1.0.0a1)
71+
4. For pre-releases, select the type: `alpha`, `beta`, or `rc`
72+
5. Click "Run workflow"
73+
74+
The workflow will automatically:
75+
- Bump the version in `pyproject.toml` and `langfuse/version.py`
76+
- Build the package
77+
- Publish to PyPI
78+
- Create a git tag and GitHub release with auto-generated release notes
8979

9080
### SDK Reference
9181

langfuse/_client/client.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
DeleteDatasetRunResponse,
8585
PaginatedDatasetRuns,
8686
)
87+
from langfuse.api.resources.commons.errors.not_found_error import NotFoundError
8788
from langfuse.api.resources.ingestion.types.score_body import ScoreBody
8889
from langfuse.api.resources.prompts.types import (
8990
CreatePromptRequest_Chat,
@@ -1976,6 +1977,7 @@ def create_score(
19761977
comment: Optional[str] = None,
19771978
config_id: Optional[str] = None,
19781979
metadata: Optional[Any] = None,
1980+
timestamp: Optional[datetime] = None,
19791981
) -> None: ...
19801982

19811983
@overload
@@ -1993,6 +1995,7 @@ def create_score(
19931995
comment: Optional[str] = None,
19941996
config_id: Optional[str] = None,
19951997
metadata: Optional[Any] = None,
1998+
timestamp: Optional[datetime] = None,
19961999
) -> None: ...
19972000

19982001
def create_score(
@@ -2009,6 +2012,7 @@ def create_score(
20092012
comment: Optional[str] = None,
20102013
config_id: Optional[str] = None,
20112014
metadata: Optional[Any] = None,
2015+
timestamp: Optional[datetime] = None,
20122016
) -> None:
20132017
"""Create a score for a specific trace or observation.
20142018
@@ -2027,6 +2031,7 @@ def create_score(
20272031
comment: Optional comment or explanation for the score
20282032
config_id: Optional ID of a score config defined in Langfuse
20292033
metadata: Optional metadata to be attached to the score
2034+
timestamp: Optional timestamp for the score (defaults to current UTC time)
20302035
20312036
Example:
20322037
```python
@@ -2073,7 +2078,7 @@ def create_score(
20732078
event = {
20742079
"id": self.create_trace_id(),
20752080
"type": "score-create",
2076-
"timestamp": _get_timestamp(),
2081+
"timestamp": timestamp or _get_timestamp(),
20772082
"body": new_body,
20782083
}
20792084

@@ -3327,20 +3332,28 @@ def create_dataset(
33273332
name: str,
33283333
description: Optional[str] = None,
33293334
metadata: Optional[Any] = None,
3335+
input_schema: Optional[Any] = None,
3336+
expected_output_schema: Optional[Any] = None,
33303337
) -> Dataset:
33313338
"""Create a dataset with the given name on Langfuse.
33323339
33333340
Args:
33343341
name: Name of the dataset to create.
33353342
description: Description of the dataset. Defaults to None.
33363343
metadata: Additional metadata. Defaults to None.
3344+
input_schema: JSON Schema for validating dataset item inputs. When set, all new items will be validated against this schema.
3345+
expected_output_schema: JSON Schema for validating dataset item expected outputs. When set, all new items will be validated against this schema.
33373346
33383347
Returns:
33393348
Dataset: The created dataset as returned by the Langfuse API.
33403349
"""
33413350
try:
33423351
body = CreateDatasetRequest(
3343-
name=name, description=description, metadata=metadata
3352+
name=name,
3353+
description=description,
3354+
metadata=metadata,
3355+
inputSchema=input_schema,
3356+
expectedOutputSchema=expected_output_schema,
33443357
)
33453358
langfuse_logger.debug(f"Creating datasets {body}")
33463359

@@ -3674,6 +3687,14 @@ def fetch_prompts() -> Any:
36743687

36753688
return prompt
36763689

3690+
except NotFoundError as not_found_error:
3691+
langfuse_logger.warning(
3692+
f"Prompt '{cache_key}' not found during refresh, evicting from cache."
3693+
)
3694+
if self._resources is not None:
3695+
self._resources.prompt_cache.delete(cache_key)
3696+
raise not_found_error
3697+
36773698
except Exception as e:
36783699
langfuse_logger.error(
36793700
f"Error while fetching prompt '{cache_key}': {str(e)}"

langfuse/_client/get_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def _create_client_from_instance(
5252
mask=instance.mask,
5353
blocked_instrumentation_scopes=instance.blocked_instrumentation_scopes,
5454
additional_headers=instance.additional_headers,
55+
tracer_provider=instance.tracer_provider,
56+
httpx_client=instance.httpx_client,
5557
)
5658

5759

langfuse/_client/propagation.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"metadata",
3333
"version",
3434
"tags",
35+
"trace_name",
3536
]
3637

3738
InternalPropagatedKeys = Literal[
@@ -50,6 +51,7 @@
5051
"metadata",
5152
"version",
5253
"tags",
54+
"trace_name",
5355
"experiment_id",
5456
"experiment_name",
5557
"experiment_metadata",
@@ -77,6 +79,7 @@ def propagate_attributes(
7779
metadata: Optional[Dict[str, str]] = None,
7880
version: Optional[str] = None,
7981
tags: Optional[List[str]] = None,
82+
trace_name: Optional[str] = None,
8083
as_baggage: bool = False,
8184
) -> _AgnosticContextManager[Any]:
8285
"""Propagate trace-level attributes to all spans created within this context.
@@ -109,6 +112,8 @@ def propagate_attributes(
109112
- AVOID: large payloads, sensitive data, non-string values (will be dropped with warning)
110113
version: Version identfier for parts of your application that are independently versioned, e.g. agents
111114
tags: List of tags to categorize the group of observations
115+
trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters.
116+
Use this to set a consistent trace name for all spans created within this context.
112117
as_baggage: If True, propagates attributes using OpenTelemetry baggage for
113118
cross-process/service propagation. **Security warning**: When enabled,
114119
attribute values are added to HTTP headers on ALL outbound requests.
@@ -195,6 +200,7 @@ def propagate_attributes(
195200
metadata=metadata,
196201
version=version,
197202
tags=tags,
203+
trace_name=trace_name,
198204
as_baggage=as_baggage,
199205
)
200206

@@ -207,6 +213,7 @@ def _propagate_attributes(
207213
metadata: Optional[Dict[str, str]] = None,
208214
version: Optional[str] = None,
209215
tags: Optional[List[str]] = None,
216+
trace_name: Optional[str] = None,
210217
as_baggage: bool = False,
211218
experiment: Optional[PropagatedExperimentAttributes] = None,
212219
) -> Generator[Any, Any, Any]:
@@ -218,6 +225,7 @@ def _propagate_attributes(
218225
"session_id": session_id,
219226
"version": version,
220227
"tags": tags,
228+
"trace_name": trace_name,
221229
}
222230

223231
propagated_string_attributes = propagated_string_attributes | (
@@ -456,6 +464,7 @@ def _get_propagated_span_key(key: str) -> str:
456464
"user_id": LangfuseOtelSpanAttributes.TRACE_USER_ID,
457465
"version": LangfuseOtelSpanAttributes.VERSION,
458466
"tags": LangfuseOtelSpanAttributes.TRACE_TAGS,
467+
"trace_name": LangfuseOtelSpanAttributes.TRACE_NAME,
459468
"experiment_id": LangfuseOtelSpanAttributes.EXPERIMENT_ID,
460469
"experiment_name": LangfuseOtelSpanAttributes.EXPERIMENT_NAME,
461470
"experiment_metadata": LangfuseOtelSpanAttributes.EXPERIMENT_METADATA,

langfuse/_client/resource_manager.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,14 @@ def _initialize_instance(
173173
self.sample_rate = sample_rate
174174
self.blocked_instrumentation_scopes = blocked_instrumentation_scopes
175175
self.additional_headers = additional_headers
176+
self.tracer_provider: Optional[TracerProvider] = None
176177

177178
# OTEL Tracer
178179
if tracing_enabled:
179180
tracer_provider = tracer_provider or _init_tracer_provider(
180181
environment=environment, release=release, sample_rate=sample_rate
181182
)
183+
self.tracer_provider = tracer_provider
182184

183185
langfuse_processor = LangfuseSpanProcessor(
184186
public_key=self.public_key,
@@ -397,9 +399,10 @@ def _stop_and_join_consumer_threads(self) -> None:
397399
)
398400

399401
def flush(self) -> None:
400-
tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider())
401-
if not isinstance(tracer_provider, otel_trace_api.ProxyTracerProvider):
402-
tracer_provider.force_flush()
402+
if self.tracer_provider is not None and not isinstance(
403+
self.tracer_provider, otel_trace_api.ProxyTracerProvider
404+
):
405+
self.tracer_provider.force_flush()
403406
langfuse_logger.debug("Successfully flushed OTEL tracer provider")
404407

405408
self._score_ingestion_queue.join()
@@ -412,10 +415,7 @@ def shutdown(self) -> None:
412415
# Unregister the atexit handler first
413416
atexit.unregister(self.shutdown)
414417

415-
tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider())
416-
if not isinstance(tracer_provider, otel_trace_api.ProxyTracerProvider):
417-
tracer_provider.force_flush()
418-
418+
self.flush()
419419
self._stop_and_join_consumer_threads()
420420

421421

langfuse/_client/span.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def score(
276276
data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None,
277277
comment: Optional[str] = None,
278278
config_id: Optional[str] = None,
279+
timestamp: Optional[datetime] = None,
279280
) -> None: ...
280281

281282
@overload
@@ -288,6 +289,7 @@ def score(
288289
data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL",
289290
comment: Optional[str] = None,
290291
config_id: Optional[str] = None,
292+
timestamp: Optional[datetime] = None,
291293
) -> None: ...
292294

293295
def score(
@@ -299,6 +301,7 @@ def score(
299301
data_type: Optional[ScoreDataType] = None,
300302
comment: Optional[str] = None,
301303
config_id: Optional[str] = None,
304+
timestamp: Optional[datetime] = None,
302305
) -> None:
303306
"""Create a score for this specific span.
304307
@@ -312,6 +315,7 @@ def score(
312315
data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL)
313316
comment: Optional comment or explanation for the score
314317
config_id: Optional ID of a score config defined in Langfuse
318+
timestamp: Optional timestamp for the score (defaults to current UTC time)
315319
316320
Example:
317321
```python
@@ -337,6 +341,7 @@ def score(
337341
data_type=cast(Literal["CATEGORICAL"], data_type),
338342
comment=comment,
339343
config_id=config_id,
344+
timestamp=timestamp,
340345
)
341346

342347
@overload
@@ -349,6 +354,7 @@ def score_trace(
349354
data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None,
350355
comment: Optional[str] = None,
351356
config_id: Optional[str] = None,
357+
timestamp: Optional[datetime] = None,
352358
) -> None: ...
353359

354360
@overload
@@ -361,6 +367,7 @@ def score_trace(
361367
data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL",
362368
comment: Optional[str] = None,
363369
config_id: Optional[str] = None,
370+
timestamp: Optional[datetime] = None,
364371
) -> None: ...
365372

366373
def score_trace(
@@ -372,6 +379,7 @@ def score_trace(
372379
data_type: Optional[ScoreDataType] = None,
373380
comment: Optional[str] = None,
374381
config_id: Optional[str] = None,
382+
timestamp: Optional[datetime] = None,
375383
) -> None:
376384
"""Create a score for the entire trace that this span belongs to.
377385
@@ -386,6 +394,7 @@ def score_trace(
386394
data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL)
387395
comment: Optional comment or explanation for the score
388396
config_id: Optional ID of a score config defined in Langfuse
397+
timestamp: Optional timestamp for the score (defaults to current UTC time)
389398
390399
Example:
391400
```python
@@ -410,6 +419,7 @@ def score_trace(
410419
data_type=cast(Literal["CATEGORICAL"], data_type),
411420
comment=comment,
412421
config_id=config_id,
422+
timestamp=timestamp,
413423
)
414424

415425
def _set_processed_span_attributes(

0 commit comments

Comments
 (0)