diff --git a/langfuse/api/__init__.py b/langfuse/api/__init__.py
index 1bdfac254..ec2bed17e 100644
--- a/langfuse/api/__init__.py
+++ b/langfuse/api/__init__.py
@@ -147,6 +147,7 @@
models,
observations,
projects,
+ prompt_version,
prompts,
score,
score_configs,
@@ -302,6 +303,7 @@
"models",
"observations",
"projects",
+ "prompt_version",
"prompts",
"score",
"score_configs",
diff --git a/langfuse/api/client.py b/langfuse/api/client.py
index 674d62721..932f5f3c2 100644
--- a/langfuse/api/client.py
+++ b/langfuse/api/client.py
@@ -19,6 +19,10 @@
from .resources.models.client import AsyncModelsClient, ModelsClient
from .resources.observations.client import AsyncObservationsClient, ObservationsClient
from .resources.projects.client import AsyncProjectsClient, ProjectsClient
+from .resources.prompt_version.client import (
+ AsyncPromptVersionClient,
+ PromptVersionClient,
+)
from .resources.prompts.client import AsyncPromptsClient, PromptsClient
from .resources.score.client import AsyncScoreClient, ScoreClient
from .resources.score_configs.client import AsyncScoreConfigsClient, ScoreConfigsClient
@@ -108,6 +112,7 @@ def __init__(
self.models = ModelsClient(client_wrapper=self._client_wrapper)
self.observations = ObservationsClient(client_wrapper=self._client_wrapper)
self.projects = ProjectsClient(client_wrapper=self._client_wrapper)
+ self.prompt_version = PromptVersionClient(client_wrapper=self._client_wrapper)
self.prompts = PromptsClient(client_wrapper=self._client_wrapper)
self.score_configs = ScoreConfigsClient(client_wrapper=self._client_wrapper)
self.score = ScoreClient(client_wrapper=self._client_wrapper)
@@ -199,6 +204,9 @@ def __init__(
self.models = AsyncModelsClient(client_wrapper=self._client_wrapper)
self.observations = AsyncObservationsClient(client_wrapper=self._client_wrapper)
self.projects = AsyncProjectsClient(client_wrapper=self._client_wrapper)
+ self.prompt_version = AsyncPromptVersionClient(
+ client_wrapper=self._client_wrapper
+ )
self.prompts = AsyncPromptsClient(client_wrapper=self._client_wrapper)
self.score_configs = AsyncScoreConfigsClient(
client_wrapper=self._client_wrapper
diff --git a/langfuse/api/reference.md b/langfuse/api/reference.md
index 37683e9f7..41d43849a 100644
--- a/langfuse/api/reference.md
+++ b/langfuse/api/reference.md
@@ -2237,6 +2237,100 @@ client.projects.get()
+
+
+
+
+## PromptVersion
+client.prompt_version.update(...)
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Update labels for a specific prompt version
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from langfuse.client import FernLangfuse
+
+client = FernLangfuse(
+ x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME",
+ x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION",
+ x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY",
+ username="YOUR_USERNAME",
+ password="YOUR_PASSWORD",
+ base_url="https://yourhost.com/path/to/api",
+)
+client.prompt_version.update(
+ name="string",
+ version=1,
+ new_labels=["string"],
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**name:** `str` — The name of the prompt
+
+
+
+
+
+-
+
+**version:** `int` — Version of the prompt to update
+
+
+
+
+
+-
+
+**new_labels:** `typing.Sequence[str]` — New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
@@ -2944,7 +3038,7 @@ client.score.get(
config_id="string",
queue_id="string",
data_type=ScoreDataType.NUMERIC,
- trace_tags=["string"],
+ trace_tags="string",
)
```
@@ -3065,9 +3159,7 @@ client.score.get(
-
-**trace_tags:** `typing.Optional[
- typing.Union[typing.Sequence[str], typing.Sequence[typing.Sequence[str]]]
-]` — Only scores linked to traces that include all of these tags will be returned.
+**trace_tags:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Only scores linked to traces that include all of these tags will be returned.
diff --git a/langfuse/api/resources/__init__.py b/langfuse/api/resources/__init__.py
index f838c2f8c..6ebdcbe69 100644
--- a/langfuse/api/resources/__init__.py
+++ b/langfuse/api/resources/__init__.py
@@ -13,6 +13,7 @@
models,
observations,
projects,
+ prompt_version,
prompts,
score,
score_configs,
@@ -299,6 +300,7 @@
"models",
"observations",
"projects",
+ "prompt_version",
"prompts",
"score",
"score_configs",
diff --git a/langfuse/api/resources/prompt_version/__init__.py b/langfuse/api/resources/prompt_version/__init__.py
new file mode 100644
index 000000000..f3ea2659b
--- /dev/null
+++ b/langfuse/api/resources/prompt_version/__init__.py
@@ -0,0 +1,2 @@
+# This file was auto-generated by Fern from our API Definition.
+
diff --git a/langfuse/api/resources/prompt_version/client.py b/langfuse/api/resources/prompt_version/client.py
new file mode 100644
index 000000000..638871082
--- /dev/null
+++ b/langfuse/api/resources/prompt_version/client.py
@@ -0,0 +1,197 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+from json.decoder import JSONDecodeError
+
+from ...core.api_error import ApiError
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.jsonable_encoder import jsonable_encoder
+from ...core.pydantic_utilities import pydantic_v1
+from ...core.request_options import RequestOptions
+from ..commons.errors.access_denied_error import AccessDeniedError
+from ..commons.errors.error import Error
+from ..commons.errors.method_not_allowed_error import MethodNotAllowedError
+from ..commons.errors.not_found_error import NotFoundError
+from ..commons.errors.unauthorized_error import UnauthorizedError
+from ..prompts.types.prompt import Prompt
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class PromptVersionClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ def update(
+ self,
+ name: str,
+ version: int,
+ *,
+ new_labels: typing.Sequence[str],
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> Prompt:
+ """
+ Update labels for a specific prompt version
+
+ Parameters
+ ----------
+ name : str
+ The name of the prompt
+
+ version : int
+ Version of the prompt to update
+
+ new_labels : typing.Sequence[str]
+ New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ Prompt
+
+ Examples
+ --------
+ from langfuse.client import FernLangfuse
+
+ client = FernLangfuse(
+ x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME",
+ x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION",
+ x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY",
+ username="YOUR_USERNAME",
+ password="YOUR_PASSWORD",
+ base_url="https://yourhost.com/path/to/api",
+ )
+ client.prompt_version.update(
+ name="string",
+ version=1,
+ new_labels=["string"],
+ )
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ f"api/public/v2/prompts/{jsonable_encoder(name)}/versions/{jsonable_encoder(version)}",
+ method="PATCH",
+ json={"newLabels": new_labels},
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore
+ if _response.status_code == 400:
+ raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ if _response.status_code == 403:
+ raise AccessDeniedError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ if _response.status_code == 404:
+ raise NotFoundError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, body=_response.text)
+ raise ApiError(status_code=_response.status_code, body=_response_json)
+
+
+class AsyncPromptVersionClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ async def update(
+ self,
+ name: str,
+ version: int,
+ *,
+ new_labels: typing.Sequence[str],
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> Prompt:
+ """
+ Update labels for a specific prompt version
+
+ Parameters
+ ----------
+ name : str
+ The name of the prompt
+
+ version : int
+ Version of the prompt to update
+
+ new_labels : typing.Sequence[str]
+ New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ Prompt
+
+ Examples
+ --------
+ import asyncio
+
+ from langfuse.client import AsyncFernLangfuse
+
+ client = AsyncFernLangfuse(
+ x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME",
+ x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION",
+ x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY",
+ username="YOUR_USERNAME",
+ password="YOUR_PASSWORD",
+ base_url="https://yourhost.com/path/to/api",
+ )
+
+
+ async def main() -> None:
+ await client.prompt_version.update(
+ name="string",
+ version=1,
+ new_labels=["string"],
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ f"api/public/v2/prompts/{jsonable_encoder(name)}/versions/{jsonable_encoder(version)}",
+ method="PATCH",
+ json={"newLabels": new_labels},
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore
+ if _response.status_code == 400:
+ raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ if _response.status_code == 403:
+ raise AccessDeniedError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ if _response.status_code == 404:
+ raise NotFoundError(
+ pydantic_v1.parse_obj_as(typing.Any, _response.json())
+ ) # type: ignore
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, body=_response.text)
+ raise ApiError(status_code=_response.status_code, body=_response_json)
diff --git a/langfuse/api/resources/score/client.py b/langfuse/api/resources/score/client.py
index 2c911449d..9c40a48f7 100644
--- a/langfuse/api/resources/score/client.py
+++ b/langfuse/api/resources/score/client.py
@@ -120,9 +120,7 @@ def get(
config_id: typing.Optional[str] = None,
queue_id: typing.Optional[str] = None,
data_type: typing.Optional[ScoreDataType] = None,
- trace_tags: typing.Optional[
- typing.Union[typing.Sequence[str], typing.Sequence[typing.Sequence[str]]]
- ] = None,
+ trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> GetScoresResponse:
"""
@@ -169,7 +167,7 @@ def get(
data_type : typing.Optional[ScoreDataType]
Retrieve only scores with a specific dataType.
- trace_tags : typing.Optional[typing.Union[typing.Sequence[str], typing.Sequence[typing.Sequence[str]]]]
+ trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]]
Only scores linked to traces that include all of these tags will be returned.
request_options : typing.Optional[RequestOptions]
@@ -212,7 +210,7 @@ def get(
config_id="string",
queue_id="string",
data_type=ScoreDataType.NUMERIC,
- trace_tags=["string"],
+ trace_tags="string",
)
"""
_response = self._client_wrapper.httpx_client.request(
@@ -236,7 +234,7 @@ def get(
"configId": config_id,
"queueId": queue_id,
"dataType": data_type,
- "traceTags": jsonable_encoder(trace_tags),
+ "traceTags": trace_tags,
},
request_options=request_options,
)
@@ -499,9 +497,7 @@ async def get(
config_id: typing.Optional[str] = None,
queue_id: typing.Optional[str] = None,
data_type: typing.Optional[ScoreDataType] = None,
- trace_tags: typing.Optional[
- typing.Union[typing.Sequence[str], typing.Sequence[typing.Sequence[str]]]
- ] = None,
+ trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> GetScoresResponse:
"""
@@ -548,7 +544,7 @@ async def get(
data_type : typing.Optional[ScoreDataType]
Retrieve only scores with a specific dataType.
- trace_tags : typing.Optional[typing.Union[typing.Sequence[str], typing.Sequence[typing.Sequence[str]]]]
+ trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]]
Only scores linked to traces that include all of these tags will be returned.
request_options : typing.Optional[RequestOptions]
@@ -595,7 +591,7 @@ async def main() -> None:
config_id="string",
queue_id="string",
data_type=ScoreDataType.NUMERIC,
- trace_tags=["string"],
+ trace_tags="string",
)
@@ -622,7 +618,7 @@ async def main() -> None:
"configId": config_id,
"queueId": queue_id,
"dataType": data_type,
- "traceTags": jsonable_encoder(trace_tags),
+ "traceTags": trace_tags,
},
request_options=request_options,
)
diff --git a/langfuse/client.py b/langfuse/client.py
index cbcab973d..e9a4a5a59 100644
--- a/langfuse/client.py
+++ b/langfuse/client.py
@@ -1357,6 +1357,32 @@ def create_prompt(
handle_fern_exception(e)
raise e
+ def update_prompt(
+ self,
+ *,
+ name: str,
+ version: int,
+ new_labels: List[str] = [],
+ ):
+ """Update an existing prompt version in Langfuse. The Langfuse SDK prompt cache is invalidated for all prompts witht he specified name.
+
+ Args:
+ 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.
+
+ """
+ updated_prompt = self.client.prompt_version.update(
+ name=name,
+ version=version,
+ new_labels=new_labels,
+ )
+ self.prompt_cache.invalidate(name)
+ return updated_prompt
+
def _url_encode(self, url: str) -> str:
return urllib.parse.quote(url)
diff --git a/langfuse/prompt_cache.py b/langfuse/prompt_cache.py
index d51711490..67611d50d 100644
--- a/langfuse/prompt_cache.py
+++ b/langfuse/prompt_cache.py
@@ -152,6 +152,12 @@ def set(self, key: str, value: PromptClient, ttl_seconds: Optional[int]):
self._cache[key] = PromptCacheItem(value, ttl_seconds)
+ def invalidate(self, prompt_name: str):
+ """Invalidate all cached prompts with the given prompt name."""
+ for key in list(self._cache):
+ if key.startswith(prompt_name):
+ del self._cache[key]
+
def add_refresh_prompt_task(self, key: str, fetch_func):
self._log.debug(f"Submitting refresh task for key: {key}")
self._task_manager.add_task(key, fetch_func)
diff --git a/langfuse/version.py b/langfuse/version.py
index 319214600..d42e9812e 100644
--- a/langfuse/version.py
+++ b/langfuse/version.py
@@ -1,3 +1,3 @@
"""@private"""
-__version__ = "2.57.13"
+__version__ = "2.57.13a0"
diff --git a/pyproject.toml b/pyproject.toml
index e70ba8b34..e88275c59 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,7 @@
[tool.poetry]
name = "langfuse"
-version = "2.57.13"
+
+version = "2.57.13a0"
description = "A client library for accessing langfuse"
authors = ["langfuse "]
license = "MIT"
diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py
index 2278b0bbc..e25d2a0b4 100644
--- a/tests/test_core_sdk.py
+++ b/tests/test_core_sdk.py
@@ -1297,7 +1297,7 @@ def test_fetch_traces():
fetched_trace = response.data[0]
assert fetched_trace.name == name
assert fetched_trace.session_id == "session-1"
- assert fetched_trace.input == '{"key":"value"}'
+ assert fetched_trace.input == {"key": "value"}
assert fetched_trace.output == "output-value"
# compare timestamps without microseconds and in UTC
assert fetched_trace.timestamp.replace(microsecond=0) == trace_params[1][
diff --git a/tests/test_updating_prompt.py b/tests/test_updating_prompt.py
new file mode 100644
index 000000000..addcd4528
--- /dev/null
+++ b/tests/test_updating_prompt.py
@@ -0,0 +1,35 @@
+from langfuse.client import Langfuse
+from tests.utils import create_uuid
+
+
+def test_update_prompt():
+ langfuse = Langfuse()
+ prompt_name = create_uuid()
+
+ # Create initial prompt
+ langfuse.create_prompt(
+ name=prompt_name,
+ prompt="test prompt",
+ labels=["production"],
+ )
+
+ # Update prompt labels
+ updated_prompt = langfuse.update_prompt(
+ name=prompt_name,
+ version=1,
+ new_labels=["john", "doe"],
+ )
+
+ # Fetch prompt after update (should be invalidated)
+ fetched_prompt = langfuse.get_prompt(prompt_name)
+
+ # Verify the fetched prompt matches the updated values
+ assert fetched_prompt.name == prompt_name
+ assert fetched_prompt.version == 1
+ print(f"Fetched prompt labels: {fetched_prompt.labels}")
+ print(f"Updated prompt labels: {updated_prompt.labels}")
+
+ # production was set by the first call, latest is managed and set by Langfuse
+ expected_labels = sorted(["latest", "doe", "production", "john"])
+ assert sorted(fetched_prompt.labels) == expected_labels
+ assert sorted(updated_prompt.labels) == expected_labels