Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 37 additions & 0 deletions langfuse/_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from langfuse._utils import _get_timestamp
from langfuse._utils.parse_error import handle_fern_exception
from langfuse._utils.prompt_cache import PromptCache
from langfuse.api.core.api_error import ApiError
from langfuse.api.resources.commons.errors.error import Error
from langfuse.api.resources.commons.errors.not_found_error import NotFoundError
from langfuse.api.resources.ingestion.types.score_body import ScoreBody
Expand Down Expand Up @@ -3775,6 +3776,42 @@ def update_prompt(

return updated_prompt

def delete_prompt(
self,
name: str,
*,
label: Optional[str] = None,
version: Optional[int] = None,
) -> None:
"""Delete a prompt or specific versions from Langfuse.

Also invalidates the Langfuse SDK prompt cache for the specified prompt.

Args:
name: The name of the prompt to delete.
label: Optional label of the prompt to delete.
version: Optional version of the prompt to delete.

Raises:
NotFoundError: If the prompt does not exist.
Error: If the API request fails.
"""
try:
self.api.prompts.delete(
prompt_name=self._url_encode(name),
label=label,
version=version,
)
except ApiError as e:
# 204 No Content is a successful deletion, but has empty body
if e.status_code == 204:
pass
else:
raise

if self._resources is not None:
self._resources.prompt_cache.invalidate(name)

def _url_encode(self, url: str, *, is_url_param: Optional[bool] = False) -> str:
# httpx ≥ 0.28 does its own WHATWG-compliant quoting (eg. encodes bare
# “%”, “?”, “#”, “|”, … in query/path parts). Re-quoting here would
Expand Down
91 changes: 91 additions & 0 deletions langfuse/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -5461,6 +5461,97 @@ client.prompts.create(
</dl>


</dd>
</dl>
</details>

<details><summary><code>client.prompts.<a href="src/langfuse/resources/prompts/client.py">delete</a>(...)</code></summary>
<dl>
<dd>

#### 📝 Description

<dl>
<dd>

<dl>
<dd>

Delete a prompt or specific versions
</dd>
</dl>
</dd>
</dl>

#### 🔌 Usage

<dl>
<dd>

<dl>
<dd>

```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.prompts.delete(
prompt_name="promptName",
)

```
</dd>
</dl>
</dd>
</dl>

#### ⚙️ Parameters

<dl>
<dd>

<dl>
<dd>

**prompt_name:** `str` — The name of the prompt

</dd>
</dl>

<dl>
<dd>

**label:** `typing.Optional[str]` — Optional label of the prompt to delete

</dd>
</dl>

<dl>
<dd>

**version:** `typing.Optional[int]` — Optional version of the prompt to delete

</dd>
</dl>

<dl>
<dd>

**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.

</dd>
</dl>
</dd>
</dl>


</dd>
</dl>
</details>
Expand Down
163 changes: 163 additions & 0 deletions langfuse/api/resources/prompts/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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 ..scim.types.empty_response import EmptyResponse
from .types.create_prompt_request import CreatePromptRequest
from .types.prompt import Prompt
from .types.prompt_meta_list_response import PromptMetaListResponse
Expand Down Expand Up @@ -292,6 +293,83 @@ def create(
raise ApiError(status_code=_response.status_code, body=_response.text)
raise ApiError(status_code=_response.status_code, body=_response_json)

def delete(
self,
prompt_name: str,
*,
label: typing.Optional[str] = None,
version: typing.Optional[int] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> EmptyResponse:
"""
Delete a prompt or specific versions

Parameters
----------
prompt_name : str
The name of the prompt

label : typing.Optional[str]
Optional label of the prompt to delete

version : typing.Optional[int]
Optional version of the prompt to delete

request_options : typing.Optional[RequestOptions]
Request-specific configuration.

Returns
-------
EmptyResponse

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.prompts.delete(
prompt_name="promptName",
)
"""
_response = self._client_wrapper.httpx_client.request(
f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}",
method="DELETE",
params={"label": label, "version": version},
request_options=request_options,
)
try:
if 200 <= _response.status_code < 300:
return pydantic_v1.parse_obj_as(EmptyResponse, _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 AsyncPromptsClient:
def __init__(self, *, client_wrapper: AsyncClientWrapper):
Expand Down Expand Up @@ -585,3 +663,88 @@ async def main() -> None:
except JSONDecodeError:
raise ApiError(status_code=_response.status_code, body=_response.text)
raise ApiError(status_code=_response.status_code, body=_response_json)

async def delete(
self,
prompt_name: str,
*,
label: typing.Optional[str] = None,
version: typing.Optional[int] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> EmptyResponse:
"""
Delete a prompt or specific versions

Parameters
----------
prompt_name : str
The name of the prompt

label : typing.Optional[str]
Optional label of the prompt to delete

version : typing.Optional[int]
Optional version of the prompt to delete

request_options : typing.Optional[RequestOptions]
Request-specific configuration.

Returns
-------
EmptyResponse

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.prompts.delete(
prompt_name="promptName",
)


asyncio.run(main())
"""
_response = await self._client_wrapper.httpx_client.request(
f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}",
method="DELETE",
params={"label": label, "version": version},
request_options=request_options,
)
try:
if 200 <= _response.status_code < 300:
return pydantic_v1.parse_obj_as(EmptyResponse, _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)
32 changes: 31 additions & 1 deletion tests/test_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ def test_prompt_end_to_end():
@pytest.fixture
def langfuse():
from langfuse._client.resource_manager import LangfuseResourceManager

langfuse_instance = Langfuse()
langfuse_instance.api = Mock()

Expand Down Expand Up @@ -1485,6 +1485,36 @@ def test_update_prompt():
assert sorted(updated_prompt.labels) == expected_labels


def test_delete_prompt():
"""Test that deleting a prompt works and invalidates cache."""
langfuse = Langfuse()
prompt_name = f"folder/subfolder/{create_uuid()}"

langfuse.create_prompt(
name=prompt_name,
prompt="test prompt",
labels=["production"],
)

# Fetch to populate cache
cached_prompt = langfuse.get_prompt(prompt_name)
assert cached_prompt.prompt == "test prompt"

cache_key = PromptCache.generate_cache_key(
prompt_name, version=None, label="production"
)
assert langfuse._resources.prompt_cache.get(cache_key) is not None

langfuse.delete_prompt(prompt_name)

# Verify cache is invalidated
assert langfuse._resources.prompt_cache.get(cache_key) is None

# Verify prompt is deleted from server
with pytest.raises(NotFoundError):
langfuse.get_prompt(prompt_name, cache_ttl_seconds=0)


def test_update_prompt_in_folder():
langfuse = Langfuse()
prompt_name = f"some-folder/{create_uuid()}"
Expand Down
Loading