Skip to content

Commit 844541a

Browse files
committed
feat(prompts): allow prompt deletion
1 parent f3eedca commit 844541a

4 files changed

Lines changed: 322 additions & 1 deletion

File tree

langfuse/_client/client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
from langfuse._utils import _get_timestamp
7979
from langfuse._utils.parse_error import handle_fern_exception
8080
from langfuse._utils.prompt_cache import PromptCache
81+
from langfuse.api.core.api_error import ApiError
8182
from langfuse.api.resources.commons.errors.error import Error
8283
from langfuse.api.resources.commons.errors.not_found_error import NotFoundError
8384
from langfuse.api.resources.ingestion.types.score_body import ScoreBody
@@ -3775,6 +3776,42 @@ def update_prompt(
37753776

37763777
return updated_prompt
37773778

3779+
def delete_prompt(
3780+
self,
3781+
name: str,
3782+
*,
3783+
label: Optional[str] = None,
3784+
version: Optional[int] = None,
3785+
) -> None:
3786+
"""Delete a prompt or specific versions from Langfuse.
3787+
3788+
Also invalidates the Langfuse SDK prompt cache for the specified prompt.
3789+
3790+
Args:
3791+
name: The name of the prompt to delete.
3792+
label: Optional label of the prompt to delete.
3793+
version: Optional version of the prompt to delete.
3794+
3795+
Raises:
3796+
NotFoundError: If the prompt does not exist.
3797+
Error: If the API request fails.
3798+
"""
3799+
try:
3800+
self.api.prompts.delete(
3801+
prompt_name=self._url_encode(name),
3802+
label=label,
3803+
version=version,
3804+
)
3805+
except ApiError as e:
3806+
# 204 No Content is a successful deletion, but has empty body
3807+
if e.status_code == 204:
3808+
pass
3809+
else:
3810+
raise
3811+
3812+
if self._resources is not None:
3813+
self._resources.prompt_cache.invalidate(name)
3814+
37783815
def _url_encode(self, url: str, *, is_url_param: Optional[bool] = False) -> str:
37793816
# httpx ≥ 0.28 does its own WHATWG-compliant quoting (eg. encodes bare
37803817
# “%”, “?”, “#”, “|”, … in query/path parts). Re-quoting here would

langfuse/api/reference.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5461,6 +5461,97 @@ client.prompts.create(
54615461
</dl>
54625462

54635463

5464+
</dd>
5465+
</dl>
5466+
</details>
5467+
5468+
<details><summary><code>client.prompts.<a href="src/langfuse/resources/prompts/client.py">delete</a>(...)</code></summary>
5469+
<dl>
5470+
<dd>
5471+
5472+
#### 📝 Description
5473+
5474+
<dl>
5475+
<dd>
5476+
5477+
<dl>
5478+
<dd>
5479+
5480+
Delete a prompt or specific versions
5481+
</dd>
5482+
</dl>
5483+
</dd>
5484+
</dl>
5485+
5486+
#### 🔌 Usage
5487+
5488+
<dl>
5489+
<dd>
5490+
5491+
<dl>
5492+
<dd>
5493+
5494+
```python
5495+
from langfuse.client import FernLangfuse
5496+
5497+
client = FernLangfuse(
5498+
x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME",
5499+
x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION",
5500+
x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY",
5501+
username="YOUR_USERNAME",
5502+
password="YOUR_PASSWORD",
5503+
base_url="https://yourhost.com/path/to/api",
5504+
)
5505+
client.prompts.delete(
5506+
prompt_name="promptName",
5507+
)
5508+
5509+
```
5510+
</dd>
5511+
</dl>
5512+
</dd>
5513+
</dl>
5514+
5515+
#### ⚙️ Parameters
5516+
5517+
<dl>
5518+
<dd>
5519+
5520+
<dl>
5521+
<dd>
5522+
5523+
**prompt_name:** `str` — The name of the prompt
5524+
5525+
</dd>
5526+
</dl>
5527+
5528+
<dl>
5529+
<dd>
5530+
5531+
**label:** `typing.Optional[str]` — Optional label of the prompt to delete
5532+
5533+
</dd>
5534+
</dl>
5535+
5536+
<dl>
5537+
<dd>
5538+
5539+
**version:** `typing.Optional[int]` — Optional version of the prompt to delete
5540+
5541+
</dd>
5542+
</dl>
5543+
5544+
<dl>
5545+
<dd>
5546+
5547+
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
5548+
5549+
</dd>
5550+
</dl>
5551+
</dd>
5552+
</dl>
5553+
5554+
54645555
</dd>
54655556
</dl>
54665557
</details>

langfuse/api/resources/prompts/client.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ..commons.errors.method_not_allowed_error import MethodNotAllowedError
1616
from ..commons.errors.not_found_error import NotFoundError
1717
from ..commons.errors.unauthorized_error import UnauthorizedError
18+
from ..scim.types.empty_response import EmptyResponse
1819
from .types.create_prompt_request import CreatePromptRequest
1920
from .types.prompt import Prompt
2021
from .types.prompt_meta_list_response import PromptMetaListResponse
@@ -292,6 +293,83 @@ def create(
292293
raise ApiError(status_code=_response.status_code, body=_response.text)
293294
raise ApiError(status_code=_response.status_code, body=_response_json)
294295

296+
def delete(
297+
self,
298+
prompt_name: str,
299+
*,
300+
label: typing.Optional[str] = None,
301+
version: typing.Optional[int] = None,
302+
request_options: typing.Optional[RequestOptions] = None,
303+
) -> EmptyResponse:
304+
"""
305+
Delete a prompt or specific versions
306+
307+
Parameters
308+
----------
309+
prompt_name : str
310+
The name of the prompt
311+
312+
label : typing.Optional[str]
313+
Optional label of the prompt to delete
314+
315+
version : typing.Optional[int]
316+
Optional version of the prompt to delete
317+
318+
request_options : typing.Optional[RequestOptions]
319+
Request-specific configuration.
320+
321+
Returns
322+
-------
323+
EmptyResponse
324+
325+
Examples
326+
--------
327+
from langfuse.client import FernLangfuse
328+
329+
client = FernLangfuse(
330+
x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME",
331+
x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION",
332+
x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY",
333+
username="YOUR_USERNAME",
334+
password="YOUR_PASSWORD",
335+
base_url="https://yourhost.com/path/to/api",
336+
)
337+
client.prompts.delete(
338+
prompt_name="promptName",
339+
)
340+
"""
341+
_response = self._client_wrapper.httpx_client.request(
342+
f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}",
343+
method="DELETE",
344+
params={"label": label, "version": version},
345+
request_options=request_options,
346+
)
347+
try:
348+
if 200 <= _response.status_code < 300:
349+
return pydantic_v1.parse_obj_as(EmptyResponse, _response.json()) # type: ignore
350+
if _response.status_code == 400:
351+
raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore
352+
if _response.status_code == 401:
353+
raise UnauthorizedError(
354+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
355+
) # type: ignore
356+
if _response.status_code == 403:
357+
raise AccessDeniedError(
358+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
359+
) # type: ignore
360+
if _response.status_code == 405:
361+
raise MethodNotAllowedError(
362+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
363+
) # type: ignore
364+
if _response.status_code == 404:
365+
raise NotFoundError(
366+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
367+
) # type: ignore
368+
_response_json = _response.json()
369+
except JSONDecodeError:
370+
raise ApiError(status_code=_response.status_code, body=_response.text)
371+
raise ApiError(status_code=_response.status_code, body=_response_json)
372+
295373

296374
class AsyncPromptsClient:
297375
def __init__(self, *, client_wrapper: AsyncClientWrapper):
@@ -585,3 +663,88 @@ async def main() -> None:
585663
except JSONDecodeError:
586664
raise ApiError(status_code=_response.status_code, body=_response.text)
587665
raise ApiError(status_code=_response.status_code, body=_response_json)
666+
667+
async def delete(
668+
self,
669+
prompt_name: str,
670+
*,
671+
label: typing.Optional[str] = None,
672+
version: typing.Optional[int] = None,
673+
request_options: typing.Optional[RequestOptions] = None,
674+
) -> EmptyResponse:
675+
"""
676+
Delete a prompt or specific versions
677+
678+
Parameters
679+
----------
680+
prompt_name : str
681+
The name of the prompt
682+
683+
label : typing.Optional[str]
684+
Optional label of the prompt to delete
685+
686+
version : typing.Optional[int]
687+
Optional version of the prompt to delete
688+
689+
request_options : typing.Optional[RequestOptions]
690+
Request-specific configuration.
691+
692+
Returns
693+
-------
694+
EmptyResponse
695+
696+
Examples
697+
--------
698+
import asyncio
699+
700+
from langfuse.client import AsyncFernLangfuse
701+
702+
client = AsyncFernLangfuse(
703+
x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME",
704+
x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION",
705+
x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY",
706+
username="YOUR_USERNAME",
707+
password="YOUR_PASSWORD",
708+
base_url="https://yourhost.com/path/to/api",
709+
)
710+
711+
712+
async def main() -> None:
713+
await client.prompts.delete(
714+
prompt_name="promptName",
715+
)
716+
717+
718+
asyncio.run(main())
719+
"""
720+
_response = await self._client_wrapper.httpx_client.request(
721+
f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}",
722+
method="DELETE",
723+
params={"label": label, "version": version},
724+
request_options=request_options,
725+
)
726+
try:
727+
if 200 <= _response.status_code < 300:
728+
return pydantic_v1.parse_obj_as(EmptyResponse, _response.json()) # type: ignore
729+
if _response.status_code == 400:
730+
raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore
731+
if _response.status_code == 401:
732+
raise UnauthorizedError(
733+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
734+
) # type: ignore
735+
if _response.status_code == 403:
736+
raise AccessDeniedError(
737+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
738+
) # type: ignore
739+
if _response.status_code == 405:
740+
raise MethodNotAllowedError(
741+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
742+
) # type: ignore
743+
if _response.status_code == 404:
744+
raise NotFoundError(
745+
pydantic_v1.parse_obj_as(typing.Any, _response.json())
746+
) # type: ignore
747+
_response_json = _response.json()
748+
except JSONDecodeError:
749+
raise ApiError(status_code=_response.status_code, body=_response.text)
750+
raise ApiError(status_code=_response.status_code, body=_response_json)

tests/test_prompt.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ def test_prompt_end_to_end():
682682
@pytest.fixture
683683
def langfuse():
684684
from langfuse._client.resource_manager import LangfuseResourceManager
685-
685+
686686
langfuse_instance = Langfuse()
687687
langfuse_instance.api = Mock()
688688

@@ -1485,6 +1485,36 @@ def test_update_prompt():
14851485
assert sorted(updated_prompt.labels) == expected_labels
14861486

14871487

1488+
def test_delete_prompt():
1489+
"""Test that deleting a prompt works and invalidates cache."""
1490+
langfuse = Langfuse()
1491+
prompt_name = f"folder/subfolder/{create_uuid()}"
1492+
1493+
langfuse.create_prompt(
1494+
name=prompt_name,
1495+
prompt="test prompt",
1496+
labels=["production"],
1497+
)
1498+
1499+
# Fetch to populate cache
1500+
cached_prompt = langfuse.get_prompt(prompt_name)
1501+
assert cached_prompt.prompt == "test prompt"
1502+
1503+
cache_key = PromptCache.generate_cache_key(
1504+
prompt_name, version=None, label="production"
1505+
)
1506+
assert langfuse._resources.prompt_cache.get(cache_key) is not None
1507+
1508+
langfuse.delete_prompt(prompt_name)
1509+
1510+
# Verify cache is invalidated
1511+
assert langfuse._resources.prompt_cache.get(cache_key) is None
1512+
1513+
# Verify prompt is deleted from server
1514+
with pytest.raises(NotFoundError):
1515+
langfuse.get_prompt(prompt_name, cache_ttl_seconds=0)
1516+
1517+
14881518
def test_update_prompt_in_folder():
14891519
langfuse = Langfuse()
14901520
prompt_name = f"some-folder/{create_uuid()}"

0 commit comments

Comments
 (0)