diff --git a/README.md b/README.md index 24e71a9..0c70490 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ - [Downloading Files](#downloading-files) - [Deleting Files](#deleting-files) - [Accessing Metadata](#accessing-metadata) + - [Prompts](#prompts) + - [Get Prompt](#get-prompt) + - [Get Prompt Metadata](#get-prompt-metadata) - [Applications](#applications) - [List Applications](#list-applications) - [Get Application by Id](#get-application-by-id) @@ -556,6 +559,57 @@ FileMetadata( ) ``` +### Prompts + +#### Get Prompt + +Use `get()` to fetch a single prompt by its storage path: + +```python +# Sync +prompt = client.prompts.get("prompts/my-bucket/my-folder/my-prompt") +# Async +prompt = await async_client.prompts.get("prompts/my-bucket/my-folder/my-prompt") +``` + +As a result, you will receive a `Prompt` object: + +```python +Prompt( + id="prompts/my-bucket/my-folder/my-prompt", + name="my-prompt", + folder_id="my-folder", + content="You are a helpful assistant.", +) +``` + +#### Get Prompt Metadata + +Use `get_metadata()` to access metadata of a prompt: + +```python +# Sync +metadata = client.prompts.get_metadata("prompts/my-bucket/my-folder/my-prompt") +# Async +metadata = await async_client.prompts.get_metadata( + "prompts/my-bucket/my-folder/my-prompt" +) +``` + +As a result, you will receive a `PromptMetadata` object: + +```python +PromptMetadata( + name="my-prompt", + parent_path="my-folder", + bucket="my-bucket", + url="prompts/my-bucket/my-folder/my-prompt", + node_type="ITEM", + resource_type="PROMPT", + items=[], +) +``` + ### Applications #### List Applications diff --git a/aidial_client/_client.py b/aidial_client/_client.py index ba17864..2cb9d8b 100644 --- a/aidial_client/_client.py +++ b/aidial_client/_client.py @@ -107,6 +107,11 @@ def _init_resources(self) -> None: metadata=self.metadata, dial_api_url=self.api_url, ) + self.prompts = resources.Prompts( + http_client=self._http_client, + metadata=self.metadata, + dial_api_url=self.api_url, + ) self.deployments = resources.Deployments(http_client=self._http_client) self.application = resources.Application(http_client=self._http_client) self.toolset = resources.Toolset(http_client=self._http_client) @@ -188,6 +193,11 @@ def _init_resources(self) -> None: metadata=self.metadata, dial_api_url=self.api_url, ) + self.prompts = resources.AsyncPrompts( + http_client=self._http_client, + metadata=self.metadata, + dial_api_url=self.api_url, + ) self.deployments = resources.AsyncDeployments( http_client=self._http_client ) diff --git a/aidial_client/resources/__init__.py b/aidial_client/resources/__init__.py index 26abbc3..8e587ca 100644 --- a/aidial_client/resources/__init__.py +++ b/aidial_client/resources/__init__.py @@ -11,6 +11,7 @@ from .bucket import AsyncBucket, Bucket from .chat import AsyncChat, Chat from .files import AsyncFiles, Files +from .prompts import AsyncPrompts, Prompts __all__ = [ "Chat", @@ -19,6 +20,8 @@ "AsyncBucket", "Files", "AsyncFiles", + "Prompts", + "AsyncPrompts", "AsyncDeployments", "Deployments", "AsyncMetadata", diff --git a/aidial_client/resources/prompts.py b/aidial_client/resources/prompts.py new file mode 100644 index 0000000..01c763e --- /dev/null +++ b/aidial_client/resources/prompts.py @@ -0,0 +1,70 @@ +from pathlib import PurePosixPath +from typing import Optional, Union +from urllib.parse import urljoin + +import httpx + +from aidial_client._constants import API_PREFIX +from aidial_client._exception import DialException, ResourceNotFoundError +from aidial_client._internal_types._http_request import FinalRequestOptions +from aidial_client.helpers.storage_resource import DialStorageResourceMixin +from aidial_client.resources.base import AsyncResource, Resource +from aidial_client.resources.metadata import AsyncMetadata, Metadata +from aidial_client.types.metadata import PromptMetadata +from aidial_client.types.prompt import Prompt + + +def _prompts_error_processor( + http_status_error: httpx.HTTPStatusError, +) -> Optional[DialException]: + if http_status_error.response.status_code == 404: + return ResourceNotFoundError( + message=http_status_error.response.text, + ) + return None + + +class Prompts(Resource, DialStorageResourceMixin): + metadata: Metadata + resource_type: str = "prompts" + + def get(self, url: Union[str, PurePosixPath]) -> Prompt: + """Fetch a single prompt by its storage path.""" + return self.http_client.request( + cast_to=Prompt, + options=FinalRequestOptions( + method="GET", + url=urljoin(API_PREFIX, self.get_api_path(str(url))), + ), + on_http_error=_prompts_error_processor, + ) + + def get_metadata(self, url: Union[str, PurePosixPath]) -> PromptMetadata: + return self.metadata.get( + resource="prompts", + relative_url=self.get_api_path(str(url)), + ) + + +class AsyncPrompts(AsyncResource, DialStorageResourceMixin): + metadata: AsyncMetadata + resource_type: str = "prompts" + + async def get(self, url: Union[str, PurePosixPath]) -> Prompt: + """Fetch a single prompt by its storage path.""" + return await self.http_client.request( + cast_to=Prompt, + options=FinalRequestOptions( + method="GET", + url=urljoin(API_PREFIX, self.get_api_path(str(url))), + ), + on_http_error=_prompts_error_processor, + ) + + async def get_metadata( + self, url: Union[str, PurePosixPath] + ) -> PromptMetadata: + return await self.metadata.get( + resource="prompts", + relative_url=self.get_api_path(str(url)), + ) diff --git a/aidial_client/types/prompt.py b/aidial_client/types/prompt.py new file mode 100644 index 0000000..5b642bc --- /dev/null +++ b/aidial_client/types/prompt.py @@ -0,0 +1,25 @@ +from typing import Optional + +from aidial_client._compatibility.pydantic import PYDANTIC_V2 +from aidial_client._internal_types._model import ExtraAllowModel +from aidial_client._utils._alias import to_camel + + +class Prompt(ExtraAllowModel): + """A DIAL prompt resource.""" + + if PYDANTIC_V2: + model_config = { + "alias_generator": to_camel, + "populate_by_name": True, + } + else: + + class Config: + alias_generator = to_camel + allow_population_by_field_name = True + + id: str + name: str + folder_id: str + content: Optional[str] = None diff --git a/tests/resources/test_prompts.py b/tests/resources/test_prompts.py new file mode 100644 index 0000000..2e91737 --- /dev/null +++ b/tests/resources/test_prompts.py @@ -0,0 +1,148 @@ +import pytest + +from aidial_client._exception import DialException, ResourceNotFoundError +from aidial_client.types.metadata import PromptMetadata +from aidial_client.types.prompt import Prompt +from tests.client_mock import get_async_client_mock, get_client_mock + +PROMPT_MOCK = { + "id": "prompts/test-bucket/my-folder/my-prompt", + "name": "my-prompt", + "folderId": "my-folder", + "content": "You are a helpful assistant.", +} + +PROMPT_NO_CONTENT_MOCK = { + "id": "prompts/test-bucket/my-folder/my-prompt", + "name": "my-prompt", + "folderId": "my-folder", +} + +PROMPT_METADATA_MOCK = { + "name": "my-prompt", + "parentPath": "my-folder", + "bucket": "test-bucket", + "url": "prompts/test-bucket/my-folder/my-prompt", + "nodeType": "ITEM", + "resourceType": "PROMPT", + "items": [], +} + + +# --------------------------------------------------------------------------- +# prompts.get() +# --------------------------------------------------------------------------- + + +def test_get_prompt(): + client = get_client_mock(status_code=200, json_mock=PROMPT_MOCK) + result = client.prompts.get("prompts/test-bucket/my-folder/my-prompt") + assert isinstance(result, Prompt) + assert result.id == "prompts/test-bucket/my-folder/my-prompt" + assert result.name == "my-prompt" + assert result.folder_id == "my-folder" + assert result.content == "You are a helpful assistant." + + +@pytest.mark.asyncio +async def test_async_get_prompt(): + client = get_async_client_mock(status_code=200, json_mock=PROMPT_MOCK) + result = await client.prompts.get("prompts/test-bucket/my-folder/my-prompt") + assert isinstance(result, Prompt) + assert result.id == "prompts/test-bucket/my-folder/my-prompt" + assert result.name == "my-prompt" + assert result.folder_id == "my-folder" + assert result.content == "You are a helpful assistant." + + +def test_get_prompt_no_content(): + client = get_client_mock(status_code=200, json_mock=PROMPT_NO_CONTENT_MOCK) + result = client.prompts.get("prompts/test-bucket/my-folder/my-prompt") + assert isinstance(result, Prompt) + assert result.content is None + + +def test_get_prompt_not_found(): + client = get_client_mock( + status_code=404, + json_mock={ + "error": { + "message": "Not found", + "type": "not_found", + } + }, + ) + with pytest.raises(ResourceNotFoundError): + client.prompts.get("prompts/test-bucket/nonexistent/prompt") + + +@pytest.mark.asyncio +async def test_async_get_prompt_not_found(): + client = get_async_client_mock( + status_code=404, + json_mock={ + "error": { + "message": "Not found", + "type": "not_found", + } + }, + ) + with pytest.raises(ResourceNotFoundError): + await client.prompts.get("prompts/test-bucket/nonexistent/prompt") + + +def test_get_prompt_http_error(): + client = get_client_mock( + status_code=401, + json_mock={ + "error": { + "message": "Unauthorized", + "type": "auth_error", + } + }, + ) + with pytest.raises(DialException): + client.prompts.get("prompts/test-bucket/my-folder/my-prompt") + + +@pytest.mark.asyncio +async def test_async_get_prompt_http_error(): + client = get_async_client_mock( + status_code=401, + json_mock={ + "error": { + "message": "Unauthorized", + "type": "auth_error", + } + }, + ) + with pytest.raises(DialException): + await client.prompts.get("prompts/test-bucket/my-folder/my-prompt") + + +# --------------------------------------------------------------------------- +# prompts.get_metadata() +# --------------------------------------------------------------------------- + + +def test_get_prompt_metadata(): + client = get_client_mock(status_code=200, json_mock=PROMPT_METADATA_MOCK) + result = client.prompts.get_metadata( + "prompts/test-bucket/my-folder/my-prompt" + ) + assert isinstance(result, PromptMetadata) + assert result.node_type == "ITEM" + assert result.bucket == "test-bucket" + + +@pytest.mark.asyncio +async def test_async_get_prompt_metadata(): + client = get_async_client_mock( + status_code=200, json_mock=PROMPT_METADATA_MOCK + ) + result = await client.prompts.get_metadata( + "prompts/test-bucket/my-folder/my-prompt" + ) + assert isinstance(result, PromptMetadata) + assert result.node_type == "ITEM" + assert result.bucket == "test-bucket"