diff --git a/docs/types/message.rst b/docs/types/message.rst index cb3a699e..1ce6e021 100644 --- a/docs/types/message.rst +++ b/docs/types/message.rst @@ -46,7 +46,17 @@ Completions-related message types Chat completions related message types -------------------------------------- -.. autotypeddict:: yandex_cloud_ml_sdk._chat.completions.message.ChatFunctionResultMessageDict +.. currentmodule:: yandex_cloud_ml_sdk._chat.completions.message + +.. autotypeddict:: ChatFunctionResultMessageDict + +.. autotypeddict:: MultimodalMessageDict + +.. autotypeddict:: TextContent + +.. autotypeddict:: ImageUrlContent + +.. autotypeddict:: ImageUrlDict Image generation messages diff --git a/examples/async/chat/example.png b/examples/async/chat/example.png new file mode 100644 index 00000000..8a7ca445 Binary files /dev/null and b/examples/async/chat/example.png differ diff --git a/examples/async/chat/multimodal.py b/examples/async/chat/multimodal.py new file mode 100755 index 00000000..77f1a84d --- /dev/null +++ b/examples/async/chat/multimodal.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import asyncio +import base64 +import pathlib + +from yandex_cloud_ml_sdk import AsyncYCloudML + + +def get_image_base64(): + image_path = pathlib.Path(__file__).parent / 'example.png' + image_data = image_path.read_bytes() + image_base64 = base64.b64encode(image_data) + return image_base64.decode('utf-8') + + +async def main() -> None: + sdk = AsyncYCloudML(folder_id='b1ghsjum2v37c2un8h64') + sdk.setup_default_logging() + + # at this moment this is only model which supports image processing + model = sdk.chat.completions('gemma-3-27b-it') + + request = [ + # this is special kind of multimodal message which allows you to + # mix image with text data; + { + 'role': 'user', + 'content': [ + { + 'type': 'text', 'text': "What is depicted in the following image", + }, + { + 'type': 'image_url', + 'image_url': { + 'url': f'data:image/png;base64,{get_image_base64()}' + } + } + ] + } + ] + + result = await model.run(request) + + print(result.text) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/sync/chat/example.png b/examples/sync/chat/example.png new file mode 100644 index 00000000..8a7ca445 Binary files /dev/null and b/examples/sync/chat/example.png differ diff --git a/examples/sync/chat/multimodal.py b/examples/sync/chat/multimodal.py new file mode 100755 index 00000000..1cfb48dd --- /dev/null +++ b/examples/sync/chat/multimodal.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import base64 +import pathlib + +from yandex_cloud_ml_sdk import YCloudML + + +def get_image_base64(): + image_path = pathlib.Path(__file__).parent / 'example.png' + image_data = image_path.read_bytes() + image_base64 = base64.b64encode(image_data) + return image_base64.decode('utf-8') + + +def main() -> None: + sdk = YCloudML(folder_id='b1ghsjum2v37c2un8h64') + sdk.setup_default_logging() + + # at this moment this is only model which supports image processing + model = sdk.chat.completions('gemma-3-27b-it') + + request = [ + # this is special kind of multimodal message which allows you to + # mix image with text data; + { + 'role': 'user', + 'content': [ + { + 'type': 'text', 'text': "What is depicted in the following image", + }, + { + 'type': 'image_url', + 'image_url': { + 'url': f'data:image/png;base64,{get_image_base64()}' + } + } + ] + } + ] + + result = model.run(request) + + print(result.text) + + +if __name__ == '__main__': + main() diff --git a/src/yandex_cloud_ml_sdk/_chat/completions/message.py b/src/yandex_cloud_ml_sdk/_chat/completions/message.py index 76abd3f2..6f8a6602 100644 --- a/src/yandex_cloud_ml_sdk/_chat/completions/message.py +++ b/src/yandex_cloud_ml_sdk/_chat/completions/message.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Iterable -from typing import TypedDict, Union, cast +from collections.abc import Iterable, Sequence +from typing import Literal, TypedDict, Union, cast from typing_extensions import NotRequired, Required @@ -20,7 +20,26 @@ class ChatFunctionResultMessageDict(TypedDict): content: Required[str] -ChatCompletionsMessageType = Union[MessageType, ChatFunctionResultMessageDict, MessageInputType] +class ImageUrlDict(TypedDict): + url: str + + +class ImageUrlContent(TypedDict): + type: Literal['image_url'] + image_url: ImageUrlDict + + +class TextContent(TypedDict): + type: Literal['text'] + text: str + + +class MultimodalMessageDict(TypedDict): + role: NotRequired[str] + content: Sequence[ImageUrlDict | TextContent] + + +ChatCompletionsMessageType = Union[MessageType, ChatFunctionResultMessageDict, MessageInputType, MultimodalMessageDict] ChatMessageInputType = Union[ChatCompletionsMessageType, Iterable[ChatCompletionsMessageType]] @@ -43,41 +62,46 @@ def message_to_json(message: ChatCompletionsMessageType, tool_name_ids: dict[str "content": message.text, "role": message.role, } + if isinstance(message, dict): - text = message.get('text') or message.get('content', '') + role: str | None = message.get('role') + content: Sequence | str | None = message.get('content') # type: ignore[assignment] + if isinstance(content, Sequence) and not isinstance(content, (str, bytes)): + return { + 'role': role or 'user', + 'content': list(content), + } + + text: str | None = message.get('text') or content or '' # type: ignore[assignment] assert isinstance(text, str) if tool_call_id := message.get('tool_call_id'): assert isinstance(tool_call_id, str) message = cast(ChatFunctionResultMessageDict, message) - role = message.get('role', 'tool') return { - 'role': role, + 'role': role or 'tool', 'content': text, 'tool_call_id': tool_call_id, } if tool_calls := message.get('tool_calls'): tool_calls = cast(JsonObject, tool_calls) - role = message.get('role', 'assistant') return { 'tool_calls': tool_calls, - 'role': role, + 'role': role or 'assistant', } if text: message = cast(TextMessageDict, message) - role = message.get('role', 'user') return { 'content': text, - 'role': role + 'role': role or 'user' } if tool_results := message.get('tool_results'): assert isinstance(tool_results, list) message = cast(FunctionResultMessageDict, message) - role = message.get('role', 'tool') result: list[JsonObject] = [] for tool_result in tool_results: tool_result = cast(ToolResultDictType, tool_result) @@ -91,7 +115,7 @@ def message_to_json(message: ChatCompletionsMessageType, tool_name_ids: dict[str ) result.append({ - 'role': role, + 'role': role or 'tool', 'content': content, 'tool_call_id': id_, }) diff --git a/tests/chat/cassettes/test_completions/test_multimodal.yaml b/tests/chat/cassettes/test_completions/test_multimodal.yaml new file mode 100644 index 00000000..d017e62a --- /dev/null +++ b/tests/chat/cassettes/test_completions/test_multimodal.yaml @@ -0,0 +1,56 @@ +interactions: +- request: + body: '{"model":"gpt://b1ghsjum2v37c2un8h64/gemma-3-27b-it/latest","messages":[{"role":"user","content":[{"type":"text","text":"What + is depicted in the following image"},{"type":"image_url","image_url":{"url":"data:image/png;base64,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"}}]}],"stream":false}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate, zstd + authorization: + - Bearer t1.9euelZrKjJORx5OLm5yeiseNmsuKke3rnpWalM3InMbJl5qUmJnHmcfPzsnl8_dFHFo5-e92cFNT_d3z9wVLVzn573ZwU1P9zef1656Vmp7Ik4yMx4nMjsiPlJvKyIma7_zF656Vmp7Ik4yMx4nMjsiPlJvKyImaveuelZqJjo_MkZ2WyI6bzJCYk4yXz7XrhpzRlp6S0ZCPmpGWm9KMmo2Jmo0._uBa0UALhuAPdknWaHJdWrE1R8e9c9TA-bAZzBwjL-Kao5U3-2wM_UThPVLrWmlY5Ei1LLYJRZAvjARfvLd1BQ + connection: + - keep-alive + content-length: + - '32219' + content-type: + - application/json + host: + - llm.api.cloud.yandex.net + user-agent: + - yandex-cloud-ml-sdk/0.15.0 python/3.12 + x-client-request-id: + - f274eb16-096c-4c33-b813-b976ef1fc98d + method: POST + uri: https://llm.api.cloud.yandex.net/v1/chat/completions + response: + body: + string: "{\"id\":\"chatcmpl-b5792776-fe99-4889-8e03-d5619e6213ef\",\"object\":\"chat.completion\",\"created\":1758044878,\"model\":\"gpt://gemma-3-27b-it/latest\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"The + image depicts a highly detailed LEGO replica of a computer case, specifically + a high-end gaming PC. \\n\\nHere are the key features visible:\\n\\n* **Open + Case:** The LEGO computer case is designed to be open, showcasing the internal + components.\\n* **Detailed Internals:** The interior is filled with LEGO + bricks configured to resemble components like a motherboard, CPU, cooler, + graphics card, RAM, and power supply. \\n* **Circular Component:** There\u2019s + a circular element in the center, likely representing a fan or cooler.\\n* + \ **Realistic Build:** The creator has clearly put a lot of work into making + it look like a real computer, with accurate placement and color-coding of + the LEGO bricks. \\n* **Background:** There is a plant and wooden paneling + in the background.\\n\\nIt is an impressive model built entirely from LEGO + bricks, demonstrating the builder's attention to detail and understanding + of computer hardware.\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":276,\"total_tokens\":472,\"completion_tokens\":196}}\n" + headers: + content-length: + - '1236' + content-type: + - application/json + date: + - Tue, 16 Sep 2025 17:48:06 GMT + server: + - ycalb + x-server-trace-id: + - 4113486ca509f33a:c182fc5027dd4d2f:4113486ca509f33a:1 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/chat/example.png b/tests/chat/example.png new file mode 100644 index 00000000..8a7ca445 Binary files /dev/null and b/tests/chat/example.png differ diff --git a/tests/chat/test_completions.py b/tests/chat/test_completions.py index 0f985404..e4e876bc 100644 --- a/tests/chat/test_completions.py +++ b/tests/chat/test_completions.py @@ -1,6 +1,8 @@ from __future__ import annotations +import base64 import json +import pathlib from typing import cast import pytest @@ -359,3 +361,30 @@ async def test_tool_choice(async_sdk: AsyncYCloudML, tool, schema) -> None: model = model.configure(tool_choice=None) # type: ignore[arg-type] result = await model.run(message) assert result.status.name == 'TOOL_CALLS' + + +async def test_multimodal(async_sdk: AsyncYCloudML) -> None: + model = async_sdk.chat.completions('gemma-3-27b-it') + image_path = pathlib.Path(__file__).parent / 'example.png' + image_data = image_path.read_bytes() + image_base64 = base64.b64encode(image_data) + image = image_base64.decode('utf-8') + + request = [ + { + 'role': 'user', + 'content': [ + { + 'type': 'text', 'text': "What is depicted in the following image", + }, + { + 'type': 'image_url', + 'image_url': { + 'url': f'data:image/png;base64,{image}' + } + } + ] + } + ] + result = await model.run(request) + assert 'bricks' in result.text