diff --git a/.env.template b/.env.template index ab27178..41d7fb3 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,7 @@ POETRY=poetry POETRY_PYTHON=python + +# For integration tests +DIAL_URL=dial-url +DIAL_API_KEY=dial-api-key +DIAL_MODEL=gpt-5.2 diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..cb84254 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD033": false +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 672ecfd..dbd3805 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,4 +23,6 @@ "files.insertFinalNewline": true, "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, + "python-envs.defaultEnvManager": "ms-python.python:poetry", + "python-envs.defaultPackageManager": "ms-python.python:poetry", } diff --git a/README.md b/README.md index 3bc2db7..73a432d 100644 --- a/README.md +++ b/README.md @@ -15,51 +15,55 @@ - [Usage](#usage) - [Authentication](#authentication) - - [API Keys](#api-keys) - - [Bearer Token](#bearer-token) + - [API Keys](#api-keys) + - [Bearer Token](#bearer-token) - [Lifecycle Management](#lifecycle-management) - [Deployments](#deployments) - - [List Deployments](#list-deployments) - - [Get Deployment by Id](#get-deployment-by-id) - - [Get Deployment Configuration](#get-deployment-configuration) - - [Make Chat Completions Requests](#make-completions-requests) - - [Without Streaming](#without-streaming) - - [With Streaming](#with-streaming) + - [List Deployments](#list-deployments) + - [Get Deployment by Id](#get-deployment-by-id) + - [Get Deployment Configuration](#get-deployment-configuration) + - [Make Chat Completions Requests](#make-chat-completions-requests) + - [Without Streaming](#without-streaming) + - [With Streaming](#with-streaming) - [Working with Files](#working-with-files) - - [Working with URLs](#working-with-urls) - - [Uploading Files](#uploading-files) - - [Downloading Files](#downloading-files) - - [Deleting Files](#deleting-files) - - [Accessing Metadata](#accessing-metadata) + - [Working with URLs](#working-with-urls) + - [Uploading Files](#uploading-files) + - [Downloading Files](#downloading-files) + - [Deleting Files](#deleting-files) + - [Moving and Copying Files](#moving-and-copying-files) + - [Accessing Metadata](#accessing-metadata) - [Prompts](#prompts) - - [Get Prompt](#get-prompt) - - [Get Prompt Metadata](#get-prompt-metadata) + - [Save Prompt](#save-prompt) + - [Get Prompt](#get-prompt) + - [Get Prompt Metadata](#get-prompt-metadata) + - [Delete Prompt](#delete-prompt) - [Applications](#applications) - - [List Applications](#list-applications) - - [Get Application by Id](#get-application-by-id) + - [List Applications](#list-applications) + - [Get Application by Id](#get-application-by-id) - [Models](#models) - - [Get Model by Name](#get-model-by-name) + - [Get Model by Name](#get-model-by-name) - [User](#user) - - [Get Authenticated User Info](#get-authenticated-user-info) + - [Get Authenticated User Info](#get-authenticated-user-info) - [Toolsets](#toolsets) - - [Get Toolset by Id](#get-toolset-by-id) + - [Get Toolset by Id](#get-toolset-by-id) - [Resource Permissions](#resource-permissions) - - [Grant Permissions](#grant-permissions) + - [Grant Permissions](#grant-permissions) - [Client Channel](#client-channel) - - [Sign In to Toolsets](#sign-in-to-toolsets) + - [Sign In to Toolsets](#sign-in-to-toolsets) - [Client Pool](#client-pool) - - [Synchronous Client Pool](#synchronous-client-pool) - - [Asynchronous Client Pool](#asynchronous-client-pool) + - [Synchronous Client Pool](#synchronous-client-pool) + - [Asynchronous Client Pool](#asynchronous-client-pool) - [Development](#development) - [Pre-requisites](#pre-requisites) - [Setup](#setup) - - [Main commands](#main-commands) - [Git hooks](#git-hooks) + - [Main commands](#main-commands) + - [Integration tests](#integration-tests) ## Usage -This section outlines how to use the AI DIAL Python client to interact with the DIAL Core API. -It covers authentication methods, making chat completion requests, working with files, managing applications, +This section outlines how to use the AI DIAL Python client to interact with the DIAL Core API. +It covers authentication methods, making chat completion requests, working with files, managing applications, and utilizing client pools for efficient connection management. ### Authentication @@ -284,7 +288,7 @@ The response is a plain `dict` whose shape is entirely deployment-specific: } ``` -### Make Completions Requests +### Make Chat Completions Requests #### Without Streaming @@ -466,8 +470,8 @@ ChatCompletionChunk( Files are AI DIAL resources that operate with URL-like objects. Use `pathlib.PurePosixPath` or `str` to create to create new URL-like objects or to get a `string` representation of them. -* Use `client.my_files_home()` to upload a file into your bucket in the AI DIAL storage. -* Use `await async_client.my_files_home()` to get the URL of your bucket and then use it to upload files. +- Use `client.my_files_home()` to upload a file into your bucket in the AI DIAL storage. +- Use `await async_client.my_files_home()` to get the URL of your bucket and then use it to upload files. The following example demonstrates how you can use the path-like object returned by `my_files_home()` function: @@ -602,7 +606,6 @@ await result.awrite_to("./some-local-file.txt") Use `delete()` to remove files from your storage bucket: - ```python await sync_client.files.delete( url=sync_client.my_files_home() / "relative_folder/my-file.txt" @@ -1095,7 +1098,6 @@ second_client = client_pool.create_client( ) ``` - ## Development To set up the development environment and run the project, follow the instructions below. @@ -1106,18 +1108,21 @@ The following tools are required to work with the project: 1. `Make` 2. `Python 3.10` -3. `Poetry 2.*`. Installation guidance can be found [here](https://python-poetry.org/docs/#installation) +3. `Poetry 2.*`. See the [Poetry installation guide](https://python-poetry.org/docs/#installation) ### Setup 1. Create `.env` file in the root of the project. Copy `.env.template` file data to the `.env` and customize the values if needed. You can customize python and poetry locations. 2. Create and activate virtual environment + ```bash make init_env source .venv/bin/activate ``` + 3. Install dependencies + ```bash make install ``` @@ -1147,3 +1152,13 @@ make install_git_hooks | `make integration_test` | Run integration tests | | `make coverage` | Generate test coverage report | | `make help` | Show available commands | + +### Integration tests + +`make integration_test` runs against a real DIAL deployment and requires the following environment variables: + +| Variable | Description | +|----------------|----------------------------------------------------| +| `DIAL_URL` | Base URL of the DIAL Core service | +| `DIAL_API_KEY` | API key used to authenticate requests in DIAL Core | +| `DIAL_MODEL` | Name of a chat model deployment to run tests with | diff --git a/tests/integration/fixtures.py b/tests/integration/conftest.py similarity index 77% rename from tests/integration/fixtures.py rename to tests/integration/conftest.py index 71796a1..757c288 100644 --- a/tests/integration/fixtures.py +++ b/tests/integration/conftest.py @@ -11,17 +11,24 @@ @pytest.fixture def dial_url() -> str: url = os.getenv("DIAL_URL") - assert url + assert url, "DIAL_URL environment variable is not set" return url @pytest.fixture def dial_api_key() -> str: api_key = os.getenv("DIAL_API_KEY") - assert api_key + assert api_key, "DIAL_API_KEY environment variable is not set" return api_key +@pytest.fixture +def dial_model() -> str: + model = os.getenv("DIAL_MODEL") + assert model, "DIAL_MODEL environment variable is not set" + return model + + @pytest.fixture def sync_client(dial_url, dial_api_key): return Dial(base_url=dial_url, api_key=dial_api_key) @@ -32,15 +39,6 @@ def async_client(dial_url, dial_api_key): return AsyncDial(base_url=dial_url, api_key=dial_api_key) -@pytest.fixture -def test_deployment(sync_client: Dial) -> str: - deployments = sync_client.deployments.list() - assert len(deployments) - deployment = next(d for d in deployments if d.id.startswith("gpt-")) - assert deployment - return deployment.id - - @pytest.fixture def absent_test_file(sync_client): def _save_delete_file(p): diff --git a/tests/integration/test_async_completions.py b/tests/integration/test_async_completions.py index e3645db..4fc634a 100644 --- a/tests/integration/test_async_completions.py +++ b/tests/integration/test_async_completions.py @@ -2,19 +2,15 @@ from aidial_client import AsyncDial from aidial_client._exception import DialException -from tests.integration.fixtures import * # noqa @pytest.mark.asyncio -async def test_async_default_api_version( - async_client: AsyncDial, - dial_url: str, - dial_api_key: str, - test_deployment: str, +async def test_async_missing_api_version_raises( + async_client: AsyncDial, dial_model: str ): with pytest.raises(DialException): await async_client.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=dial_model, stream=False, messages=[ { @@ -23,13 +19,19 @@ async def test_async_default_api_version( } ], ) + + +@pytest.mark.asyncio +async def test_async_client_default_api_version( + dial_url: str, dial_api_key: str, dial_model: str +): client_with_default_api_version = AsyncDial( base_url=dial_url, api_key=dial_api_key, api_version="2024-02-15-preview", ) await client_with_default_api_version.chat.completions.create( - deployment_name=test_deployment, + deployment_name=dial_model, stream=False, messages=[ { @@ -42,10 +44,10 @@ async def test_async_default_api_version( @pytest.mark.asyncio async def test_completions_without_streaming( - async_client: AsyncDial, test_deployment: str + async_client: AsyncDial, dial_model: str ): completion = await async_client.chat.completions.create( - deployment_name=test_deployment, + deployment_name=dial_model, stream=False, messages=[ { @@ -70,13 +72,11 @@ async def test_completions_without_streaming( @pytest.mark.asyncio -async def test_completions_with_streaming(async_client: AsyncDial): - deployments = await async_client.deployments.list() - assert len(deployments) - deployment = next(d for d in deployments if d.id.startswith("gpt-")) - assert deployment +async def test_completions_with_streaming( + async_client: AsyncDial, dial_model: str +): completion = await async_client.chat.completions.create( - deployment_name=deployment.id, + deployment_name=dial_model, stream=True, messages=[ { @@ -105,24 +105,3 @@ async def test_completions_with_streaming(async_client: AsyncDial): last_chunk.usage.completion_tokens + last_chunk.usage.prompt_tokens == last_chunk.usage.total_tokens ) - - -@pytest.mark.asyncio -async def test_error_during_streaming( - async_client: AsyncDial, test_deployment: str -): - completion = await async_client.chat.completions.create( - deployment_name=test_deployment, - stream=True, - messages=[ - { - "role": "system", - "content": "2+3=", - } - ], - max_tokens=20, - api_version="2024-02-15-preview", - ) - - async for chunk in completion: - print(chunk) diff --git a/tests/integration/test_async_files.py b/tests/integration/test_async_files.py index 1b5f3b1..4415ca8 100644 --- a/tests/integration/test_async_files.py +++ b/tests/integration/test_async_files.py @@ -5,7 +5,6 @@ from aidial_client import AsyncDial, DialException from aidial_client._exception import EtagMismatchError from aidial_client.types.metadata import FileItem -from tests.integration.fixtures import * # type: ignore # noqa current_file_path = os.path.abspath(__file__) file_name = "test-file-async" diff --git a/tests/integration/test_async_prompts.py b/tests/integration/test_async_prompts.py index 1ede0ec..a8b01b4 100644 --- a/tests/integration/test_async_prompts.py +++ b/tests/integration/test_async_prompts.py @@ -7,7 +7,6 @@ from aidial_client._exception import EtagMismatchError, ResourceNotFoundError from aidial_client.types.metadata import PromptItem from aidial_client.types.prompt import Prompt -from tests.integration.fixtures import * # type: ignore # noqa PROMPT_FOLDER = "test-folder-artifacts" diff --git a/tests/integration/test_sync_completions.py b/tests/integration/test_sync_completions.py index 435b90c..b29f8c9 100644 --- a/tests/integration/test_sync_completions.py +++ b/tests/integration/test_sync_completions.py @@ -2,12 +2,11 @@ from aidial_client import Dial from aidial_client._exception import DialException -from tests.integration.fixtures import * # type: ignore # noqa -def test_completions_without_streaming(sync_client: Dial, test_deployment: str): +def test_completions_without_streaming(sync_client: Dial, dial_model: str): completion = sync_client.chat.completions.create( - deployment_name=test_deployment, + deployment_name=dial_model, stream=False, messages=[ { @@ -31,12 +30,10 @@ def test_completions_without_streaming(sync_client: Dial, test_deployment: str): ) -def test_default_api_version( - sync_client: Dial, dial_url: str, dial_api_key: str -): +def test_missing_api_version_raises(sync_client: Dial, dial_model: str): with pytest.raises(DialException): sync_client.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=dial_model, stream=False, messages=[ { @@ -45,13 +42,18 @@ def test_default_api_version( } ], ) + + +def test_client_default_api_version( + dial_url: str, dial_api_key: str, dial_model: str +): client_with_default_api_version = Dial( base_url=dial_url, api_key=dial_api_key, api_version="2024-02-15-preview", ) client_with_default_api_version.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=dial_model, stream=False, messages=[ { @@ -62,13 +64,9 @@ def test_default_api_version( ) -def test_completions_with_streaming(sync_client: Dial): - deployments = sync_client.deployments.list() - assert len(deployments) - deployment = next(d for d in deployments if d.id.startswith("gpt-")) - assert deployment +def test_completions_with_streaming(sync_client: Dial, dial_model: str): completion = sync_client.chat.completions.create( - deployment_name=deployment.id, + deployment_name=dial_model, stream=True, messages=[ { diff --git a/tests/integration/test_sync_files.py b/tests/integration/test_sync_files.py index 03b0177..9b0357b 100644 --- a/tests/integration/test_sync_files.py +++ b/tests/integration/test_sync_files.py @@ -5,7 +5,6 @@ from aidial_client import Dial, DialException from aidial_client._exception import EtagMismatchError from aidial_client.types.metadata import FileItem -from tests.integration.fixtures import * # type: ignore # noqa current_file_path = os.path.abspath(__file__) file_name = "test-file" diff --git a/tests/integration/test_sync_prompts.py b/tests/integration/test_sync_prompts.py index 55a17bd..eadb546 100644 --- a/tests/integration/test_sync_prompts.py +++ b/tests/integration/test_sync_prompts.py @@ -7,7 +7,6 @@ from aidial_client._exception import EtagMismatchError, ResourceNotFoundError from aidial_client.types.metadata import PromptItem from aidial_client.types.prompt import Prompt -from tests.integration.fixtures import * # type: ignore # noqa PROMPT_FOLDER = "test-folder-artifacts" diff --git a/tests/resources/completions/test_completions_streaming_tool_call.py b/tests/resources/completions/test_completions_streaming_tool_call.py index b9a3a44..6808448 100644 --- a/tests/resources/completions/test_completions_streaming_tool_call.py +++ b/tests/resources/completions/test_completions_streaming_tool_call.py @@ -1,4 +1,3 @@ -import inspect from collections.abc import Iterable import pytest @@ -7,6 +6,8 @@ from tests.client_mock import get_async_client_mock, get_client_mock from tests.utils.chunks import create_mock_chunk, create_sse_data_field +_DIAL_MODEL = "gpt-4o" + _TOOL_DEFINITION: ToolParam = { "type": "function", "function": { @@ -173,7 +174,7 @@ def test_sync_streaming_tool_call(): ) response = client.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=_DIAL_MODEL, messages=[{"role": "user", "content": "what's the weather in Paris?"}], tools=[_TOOL_DEFINITION], stream=True, @@ -191,14 +192,12 @@ async def test_async_streaming_tool_call(): stream_chunks_mock=_STREAM_CHUNKS_MOCK, ) response = await async_client.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=_DIAL_MODEL, messages=[{"role": "user", "content": "what's the weather in Paris?"}], tools=[_TOOL_DEFINITION], stream=True, ) - assert inspect.isasyncgen(response) chunks = [chunk async for chunk in response] assert all(isinstance(chunk, ChatCompletionChunk) for chunk in chunks) _validate_chunks(chunks) - _validate_chunks(chunks) diff --git a/tests/resources/completions/test_completions_streaming_vanilla.py b/tests/resources/completions/test_completions_streaming_vanilla.py index dac087b..2a0d7ec 100644 --- a/tests/resources/completions/test_completions_streaming_vanilla.py +++ b/tests/resources/completions/test_completions_streaming_vanilla.py @@ -1,13 +1,12 @@ -import inspect -from collections.abc import Iterable - import pytest from aidial_client.types.chat import ChatCompletionChunk from tests.client_mock import get_async_client_mock, get_client_mock from tests.utils.chunks import create_mock_chunk, create_sse_data_field -STREAM_CHUNKS_MOCK: list[bytes] = [ +_DIAL_MODEL = "gpt-4o" + +_STREAM_CHUNKS_MOCK: list[bytes] = [ create_sse_data_field( create_mock_chunk(delta={"content": "", "role": "assistant"}) ) @@ -49,15 +48,14 @@ def _validate_chunks(chunks: list[ChatCompletionChunk]): def test_sync_streaming(): client = get_client_mock( status_code=200, - stream_chunks_mock=STREAM_CHUNKS_MOCK, + stream_chunks_mock=_STREAM_CHUNKS_MOCK, ) response = client.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=_DIAL_MODEL, messages=[{"role": "user", "content": "2+3="}], stream=True, ) - assert isinstance(response, Iterable) chunks = list(response) assert all(isinstance(chunk, ChatCompletionChunk) for chunk in chunks) _validate_chunks(chunks) @@ -67,15 +65,14 @@ def test_sync_streaming(): async def test_async_streaming(): async_client = get_async_client_mock( status_code=200, - stream_chunks_mock=STREAM_CHUNKS_MOCK, + stream_chunks_mock=_STREAM_CHUNKS_MOCK, ) response = await async_client.chat.completions.create( - deployment_name="gpt-35-turbo", + deployment_name=_DIAL_MODEL, messages=[{"role": "user", "content": "2+3="}], stream=True, ) - assert inspect.isasyncgen(response) chunks = [chunk async for chunk in response] assert all(isinstance(chunk, ChatCompletionChunk) for chunk in chunks) _validate_chunks(chunks) diff --git a/tests/utils/chunks.py b/tests/utils/chunks.py index 7434da1..fcb92d3 100644 --- a/tests/utils/chunks.py +++ b/tests/utils/chunks.py @@ -18,7 +18,7 @@ def create_mock_chunk( } ], "created": 1723806872, - "model": "gpt-35-turbo", + "model": "gpt-4o", "object": "chat.completion.chunk", "system_fingerprint": None, **({} if usage is None else {"usage": usage}),