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