diff --git a/README.md b/README.md index f84c7703..04f0a08a 100644 --- a/README.md +++ b/README.md @@ -136,4 +136,4 @@ To remove the virtual environment and build artifacts: ```sh make clean -``` +``` \ No newline at end of file diff --git a/aidial_adapter_bedrock/chat_completion.py b/aidial_adapter_bedrock/chat_completion.py index db37029c..86120752 100644 --- a/aidial_adapter_bedrock/chat_completion.py +++ b/aidial_adapter_bedrock/chat_completion.py @@ -26,7 +26,10 @@ from aidial_adapter_bedrock.deployments import ChatCompletionDeployment from aidial_adapter_bedrock.dial_api.request import ModelParameters from aidial_adapter_bedrock.dial_api.token_usage import TokenUsage -from aidial_adapter_bedrock.llm.chat_model import ChatCompletionAdapter +from aidial_adapter_bedrock.llm.chat_model import ( + ChatCompletionAdapter, + TextCompletionAdapter, +) from aidial_adapter_bedrock.llm.consumer import ChoiceConsumer from aidial_adapter_bedrock.llm.errors import UserError, ValidationError from aidial_adapter_bedrock.llm.model.adapter import get_bedrock_adapter @@ -64,8 +67,11 @@ async def generate_response(usage: TokenUsage) -> None: nonlocal discarded_messages with response.create_choice() as choice: - tools_emulator = model.tools_emulator(params.tool_config) - consumer = ChoiceConsumer(tools_emulator, choice) + consumer = ChoiceConsumer(choice=choice) + if isinstance(model, TextCompletionAdapter): + consumer.set_tools_emulator( + model.tools_emulator(params.tool_config) + ) try: await model.chat(consumer, params, request.messages) diff --git a/aidial_adapter_bedrock/llm/chat_model.py b/aidial_adapter_bedrock/llm/chat_model.py index 5bca56c5..da470846 100644 --- a/aidial_adapter_bedrock/llm/chat_model.py +++ b/aidial_adapter_bedrock/llm/chat_model.py @@ -30,8 +30,6 @@ def _is_empty_system_message(msg: Message) -> bool: class ChatCompletionAdapter(ABC, BaseModel): - tools_emulator: Callable[[Optional[ToolsConfig]], ToolsEmulator] - class Config: arbitrary_types_allowed = True @@ -65,6 +63,7 @@ class TextCompletionPrompt(BaseModel): class TextCompletionAdapter(ChatCompletionAdapter): + tools_emulator: Callable[[Optional[ToolsConfig]], ToolsEmulator] @abstractmethod async def predict( diff --git a/aidial_adapter_bedrock/llm/consumer.py b/aidial_adapter_bedrock/llm/consumer.py index aee86c3d..0e7dec03 100644 --- a/aidial_adapter_bedrock/llm/consumer.py +++ b/aidial_adapter_bedrock/llm/consumer.py @@ -1,7 +1,12 @@ from abc import ABC, abstractmethod from typing import List, Optional, assert_never -from aidial_sdk.chat_completion import Choice, FinishReason +from aidial_sdk.chat_completion import ( + Choice, + FinishReason, + FunctionCall, + ToolCall, +) from pydantic import BaseModel from aidial_adapter_bedrock.dial_api.token_usage import TokenUsage @@ -42,23 +47,37 @@ def add_usage(self, usage: TokenUsage): def set_discarded_messages(self, discarded_messages: List[int]): pass + @abstractmethod + def create_function_tool_call(self, tool_call: ToolCall): + pass + + @abstractmethod + def create_function_call(self, function_call: FunctionCall): + pass + class ChoiceConsumer(Consumer): usage: TokenUsage choice: Choice discarded_messages: Optional[List[int]] - tools_emulator: ToolsEmulator + tools_emulator: Optional[ToolsEmulator] - def __init__(self, tools_emulator: ToolsEmulator, choice: Choice): + def __init__(self, choice: Choice): self.choice = choice self.usage = TokenUsage() self.discarded_messages = None + self.tools_emulator = None + + def set_tools_emulator(self, tools_emulator: ToolsEmulator): self.tools_emulator = tools_emulator def _process_content( self, content: str | None, finish_reason: FinishReason | None = None ): - res = self.tools_emulator.recognize_call(content) + if self.tools_emulator is not None: + res = self.tools_emulator.recognize_call(content) + else: + res = content if res is None: # Choice.close(finish_reason: Optional[FinishReason]) can be called only once @@ -72,11 +91,7 @@ def _process_content( if isinstance(res, AIToolCallMessage): for call in res.calls: - self.choice.create_function_tool_call( - id=call.id, - name=call.function.name, - arguments=call.function.arguments, - ) + self.create_function_tool_call(call) return if isinstance(res, AIFunctionCallMessage): @@ -102,3 +117,15 @@ def add_usage(self, usage: TokenUsage): def set_discarded_messages(self, discarded_messages: List[int]): self.discarded_messages = discarded_messages + + def create_function_tool_call(self, tool_call: ToolCall): + self.choice.create_function_tool_call( + id=tool_call.id, + name=tool_call.function.name, + arguments=tool_call.function.arguments, + ) + + def create_function_call(self, function_call: FunctionCall): + self.choice.create_function_call( + name=function_call.name, arguments=function_call.arguments + ) diff --git a/aidial_adapter_bedrock/llm/message.py b/aidial_adapter_bedrock/llm/message.py index 76486e3f..99e500bf 100644 --- a/aidial_adapter_bedrock/llm/message.py +++ b/aidial_adapter_bedrock/llm/message.py @@ -38,10 +38,12 @@ class AIRegularMessage(BaseModel): class AIToolCallMessage(BaseModel): calls: List[ToolCall] + content: Optional[str] = None class AIFunctionCallMessage(BaseModel): call: FunctionCall + content: Optional[str] = None BaseMessage = Union[SystemMessage, HumanRegularMessage, AIRegularMessage] @@ -63,18 +65,13 @@ def _parse_assistant_message( if content is not None and function_call is None and tool_calls is None: return AIRegularMessage(content=content, custom_content=custom_content) - if content is None and function_call is not None and tool_calls is None: - return AIFunctionCallMessage(call=function_call) + if function_call is not None and tool_calls is None: + return AIFunctionCallMessage(call=function_call, content=content) - if content is None and function_call is None and tool_calls is not None: - return AIToolCallMessage(calls=tool_calls) + if function_call is None and tool_calls is not None: + return AIToolCallMessage(calls=tool_calls, content=content) - raise ValidationError( - "Assistant message must have one and only one of the following fields not-none: " - f"content (is none: {content is None}), " - f"function_call (is none: {function_call is None}), " - f"tool_calls (is none: {tool_calls is None})" - ) + raise ValidationError("Unknown type of assistant message") def parse_dial_message(msg: Message) -> BaseMessage | ToolMessage: diff --git a/aidial_adapter_bedrock/llm/model/claude/v3/adapter.py b/aidial_adapter_bedrock/llm/model/claude/v3/adapter.py index 17125e47..ec514708 100644 --- a/aidial_adapter_bedrock/llm/model/claude/v3/adapter.py +++ b/aidial_adapter_bedrock/llm/model/claude/v3/adapter.py @@ -1,14 +1,24 @@ -from typing import List, Mapping, Optional, TypedDict, Union +from typing import List, Mapping, Optional, TypedDict, Union, assert_never from aidial_sdk.chat_completion import Message -from anthropic import NOT_GIVEN, NotGiven +from anthropic import NOT_GIVEN, MessageStopEvent, NotGiven from anthropic.lib.bedrock import AsyncAnthropicBedrock -from anthropic.lib.streaming import AsyncMessageStream +from anthropic.lib.streaming import ( + AsyncMessageStream, + InputJsonEvent, + TextEvent, +) from anthropic.types import ( + ContentBlockDeltaEvent, + ContentBlockStartEvent, + ContentBlockStopEvent, MessageDeltaEvent, MessageParam, MessageStartEvent, MessageStreamEvent, + TextBlock, + ToolParam, + ToolUseBlock, ) from aidial_adapter_bedrock.dial_api.request import ModelParameters @@ -20,15 +30,19 @@ from aidial_adapter_bedrock.llm.chat_model import ChatCompletionAdapter from aidial_adapter_bedrock.llm.consumer import Consumer from aidial_adapter_bedrock.llm.errors import ValidationError +from aidial_adapter_bedrock.llm.message import parse_dial_message from aidial_adapter_bedrock.llm.model.claude.v3.converters import ( ClaudeFinishReason, to_claude_messages, + to_claude_tool_config, to_dial_finish_reason, ) -from aidial_adapter_bedrock.llm.model.conf import DEFAULT_MAX_TOKENS_ANTHROPIC -from aidial_adapter_bedrock.llm.tools.claude_emulator import ( - legacy_tools_emulator, +from aidial_adapter_bedrock.llm.model.claude.v3.tools import ( + process_tools_block, + process_with_tools, ) +from aidial_adapter_bedrock.llm.model.conf import DEFAULT_MAX_TOKENS_ANTHROPIC +from aidial_adapter_bedrock.llm.tools.tools_config import ToolsMode from aidial_adapter_bedrock.utils.log_config import bedrock_logger as log @@ -51,6 +65,7 @@ class ChatParams(TypedDict): system: Union[str, NotGiven] temperature: Union[float, NotGiven] top_p: Union[float, NotGiven] + tools: Union[List[ToolParam], NotGiven] class Adapter(ChatCompletionAdapter): @@ -67,17 +82,27 @@ async def chat( if len(messages) == 0: raise ValidationError("List of messages must not be empty") - tools_emulator = self.tools_emulator(params.tool_config) - base_messages = tools_emulator.parse_dial_messages(messages) - tool_stop_sequences = tools_emulator.get_stop_sequences() + tools = NOT_GIVEN + tools_mode = None + if params.tool_config is not None: + tools = [ + to_claude_tool_config(tool_function) + for tool_function in params.tool_config.functions + ] + tools_mode = params.tool_config.tools_mode + + parsed_messages = [ + process_with_tools(parse_dial_message(m), tools_mode) + for m in messages + ] prompt, claude_messages = await to_claude_messages( - base_messages, self.storage + parsed_messages, self.storage ) completion_params = ChatParams( max_tokens=params.max_tokens or DEFAULT_MAX_TOKENS_ANTHROPIC, - stop_sequences=[*params.stop, *tool_stop_sequences], + stop_sequences=params.stop, system=prompt or NOT_GIVEN, temperature=( NOT_GIVEN @@ -85,15 +110,16 @@ async def chat( else params.temperature / 2 ), top_p=params.top_p or NOT_GIVEN, + tools=tools, ) if params.stream: await self.invoke_streaming( - consumer, claude_messages, completion_params + consumer, claude_messages, completion_params, tools_mode ) else: await self.invoke_non_streaming( - consumer, claude_messages, completion_params + consumer, claude_messages, completion_params, tools_mode ) async def invoke_streaming( @@ -101,6 +127,7 @@ async def invoke_streaming( consumer: Consumer, messages: List[MessageParam], params: ChatParams, + tools_mode: ToolsMode | None, ): log.debug( f"Streaming request: messages={messages}, model={self.model}, params={params}" @@ -108,17 +135,46 @@ async def invoke_streaming( async with self.client.messages.stream( messages=messages, model=self.model, - event_handler=UsageEventHandler, **params, ) as stream: - async for text in stream.text_stream: - consumer.append_content(text) - consumer.close_content(to_dial_finish_reason(stream.stop_reason)) + prompt_tokens = 0 + completion_tokens = 0 + stop_reason = None + async for event in stream: + match event: + case MessageStartEvent(): + prompt_tokens += event.message.usage.input_tokens + case TextEvent(): + consumer.append_content(event.text) + case MessageDeltaEvent(): + completion_tokens += event.usage.output_tokens + case ContentBlockStopEvent(): + if isinstance(event.content_block, ToolUseBlock): + process_tools_block( + consumer, event.content_block, tools_mode + ) + case MessageStopEvent(): + completion_tokens += event.message.usage.output_tokens + stop_reason = event.message.stop_reason + case ( + InputJsonEvent() + | ContentBlockStartEvent() + | ContentBlockDeltaEvent() + ): + pass + case _: + raise ValueError( + f"Unsupported event type! {type(event)}" + ) + + consumer.close_content( + to_dial_finish_reason(stop_reason, tools_mode) + ) consumer.add_usage( TokenUsage( - prompt_tokens=stream.prompt_tokens, - completion_tokens=stream.completion_tokens, + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, ) ) @@ -127,6 +183,7 @@ async def invoke_non_streaming( consumer: Consumer, messages: List[MessageParam], params: ChatParams, + tools_mode: ToolsMode | None, ): log.debug( f"Request: messages={messages}, model={self.model}, params={params}" @@ -134,19 +191,21 @@ async def invoke_non_streaming( message = await self.client.messages.create( messages=messages, model=self.model, **params, stream=False ) - prompt_tokens = 0 - completion_tokens = 0 for content in message.content: - usage = message.usage - prompt_tokens = usage.input_tokens - completion_tokens += usage.output_tokens - consumer.append_content(content.text) - consumer.close_content(to_dial_finish_reason(message.stop_reason)) + if isinstance(content, TextBlock): + consumer.append_content(content.text) + elif isinstance(content, ToolUseBlock): + process_tools_block(consumer, content, tools_mode) + else: + assert_never(content) + consumer.close_content( + to_dial_finish_reason(message.stop_reason, tools_mode) + ) consumer.add_usage( TokenUsage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + prompt_tokens=message.usage.input_tokens, + completion_tokens=message.usage.output_tokens, ) ) @@ -155,7 +214,6 @@ def create(cls, model: str, region: str, headers: Mapping[str, str]): storage: Optional[FileStorage] = create_file_storage(headers) return cls( model=model, - tools_emulator=legacy_tools_emulator, storage=storage, client=AsyncAnthropicBedrock(aws_region=region), ) diff --git a/aidial_adapter_bedrock/llm/model/claude/v3/converters.py b/aidial_adapter_bedrock/llm/model/claude/v3/converters.py index 1f8edd5d..d3d1b99d 100644 --- a/aidial_adapter_bedrock/llm/model/claude/v3/converters.py +++ b/aidial_adapter_bedrock/llm/model/claude/v3/converters.py @@ -1,8 +1,21 @@ +import json import mimetypes -from typing import Iterable, List, Literal, Optional, Tuple, assert_never, cast +from typing import List, Literal, Optional, Set, Tuple, assert_never, cast -from aidial_sdk.chat_completion import Attachment, FinishReason -from anthropic.types import ImageBlockParam, MessageParam, TextBlockParam +from aidial_sdk.chat_completion import ( + Attachment, + FinishReason, + Function, + ToolCall, +) +from anthropic.types import ( + ImageBlockParam, + MessageParam, + TextBlockParam, + ToolParam, + ToolResultBlockParam, + ToolUseBlockParam, +) from anthropic.types.image_block_param import Source from aidial_adapter_bedrock.dial_api.storage import ( @@ -12,14 +25,19 @@ from aidial_adapter_bedrock.llm.errors import UserError, ValidationError from aidial_adapter_bedrock.llm.message import ( AIRegularMessage, + AIToolCallMessage, BaseMessage, HumanRegularMessage, + HumanToolResultMessage, SystemMessage, ) +from aidial_adapter_bedrock.llm.tools.tools_config import ToolsMode -ClaudeFinishReason = Literal["end_turn", "max_tokens", "stop_sequence"] +ClaudeFinishReason = Literal[ + "end_turn", "max_tokens", "stop_sequence", "tool_use" +] ImageMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"] -IMAGE_MEDIA_TYPES: Iterable[ImageMediaType] = { +IMAGE_MEDIA_TYPES: Set[ImageMediaType] = { "image/png", "image/jpeg", "image/gif", @@ -58,24 +76,6 @@ async def _download_data(url: str, file_storage: Optional[FileStorage]) -> str: return await file_storage.download_file_as_base64(url) -def _to_claude_role( - message: BaseMessage, -) -> Tuple[ - Literal["user", "assistant"], AIRegularMessage | HumanRegularMessage -]: - match message: - case HumanRegularMessage(): - return "user", message - case AIRegularMessage(): - return "assistant", message - case SystemMessage(): - raise ValueError( - "System message is only allowed as the first message" - ) - case _: - assert_never(message) - - async def _to_claude_image( attachment: Attachment, file_storage: Optional[FileStorage] ) -> ImageBlockParam: @@ -101,7 +101,7 @@ async def _to_claude_image( raise ValidationError("Attachment data or URL is required") -async def _to_claude_content( +async def _to_claude_message( message: AIRegularMessage | HumanRegularMessage, file_storage: Optional[FileStorage], ) -> List[TextBlockParam | ImageBlockParam]: @@ -115,8 +115,28 @@ async def _to_claude_content( return content +def _to_claude_tool_call(call: ToolCall) -> ToolUseBlockParam: + return ToolUseBlockParam( + id=call.id, + name=call.function.name, + input=json.loads(call.function.arguments), + type="tool_use", + ) + + +def _to_claude_tool_result( + message: HumanToolResultMessage, +) -> ToolResultBlockParam: + return ToolResultBlockParam( + tool_use_id=message.id, + type="tool_result", + content=[TextBlockParam(text=message.content, type="text")], + ) + + async def to_claude_messages( - messages: List[BaseMessage], file_storage: Optional[FileStorage] + messages: List[BaseMessage | HumanToolResultMessage | AIToolCallMessage], + file_storage: Optional[FileStorage], ) -> Tuple[Optional[str], List[MessageParam]]: if not messages: return None, [] @@ -128,15 +148,56 @@ async def to_claude_messages( claude_messages: List[MessageParam] = [] for message in messages: - role, message = _to_claude_role(message) - content = await _to_claude_content(message, file_storage) - claude_messages.append(MessageParam(role=role, content=content)) + match message: + case HumanRegularMessage(): + claude_messages.append( + MessageParam( + role="user", + content=await _to_claude_message(message, file_storage), + ) + ) + case AIRegularMessage(): + claude_messages.append( + MessageParam( + role="assistant", + content=await _to_claude_message(message, file_storage), + ) + ) + case AIToolCallMessage(): + content: List[TextBlockParam | ToolUseBlockParam] = [ + _to_claude_tool_call(call) for call in message.calls + ] + if message.content is not None: + content.insert( + 0, TextBlockParam(text=message.content, type="text") + ) + + claude_messages.append( + MessageParam( + role="assistant", + content=content, + ) + ) + case HumanToolResultMessage(): + claude_messages.append( + MessageParam( + role="user", + content=[_to_claude_tool_result(message)], + ) + ) + case SystemMessage(): + raise ValueError( + "System message is only allowed as the first message" + ) + case _: + assert_never(message) return system_prompt, claude_messages def to_dial_finish_reason( finish_reason: Optional[ClaudeFinishReason], + tools_mode: ToolsMode | None, ) -> FinishReason: if finish_reason is None: return FinishReason.STOP @@ -148,10 +209,31 @@ def to_dial_finish_reason( return FinishReason.LENGTH case "stop_sequence": return FinishReason.STOP + case "tool_use": + match tools_mode: + case ToolsMode.TOOLS: + return FinishReason.TOOL_CALLS + case ToolsMode.FUNCTIONS: + return FinishReason.FUNCTION_CALL + case None: + raise ValidationError( + "A model has called a tool, but no tools were given to the model in the first place." + ) + case _: + raise Exception(f"Unknown {tools_mode} during tool use!") + case _: assert_never(finish_reason) +def to_claude_tool_config(function_call: Function) -> ToolParam: + return ToolParam( + input_schema=function_call.parameters, + name=function_call.name, + description=function_call.description or "", + ) + + def get_usage_message(supported_exts: List[str]) -> str: return f""" The application answers queries about attached images. diff --git a/aidial_adapter_bedrock/llm/model/claude/v3/tools.py b/aidial_adapter_bedrock/llm/model/claude/v3/tools.py new file mode 100644 index 00000000..3903a775 --- /dev/null +++ b/aidial_adapter_bedrock/llm/model/claude/v3/tools.py @@ -0,0 +1,105 @@ +import json +from typing import assert_never + +from aidial_sdk.chat_completion import FunctionCall, ToolCall +from anthropic.types import ToolUseBlock + +from aidial_adapter_bedrock.llm.consumer import Consumer +from aidial_adapter_bedrock.llm.errors import ValidationError +from aidial_adapter_bedrock.llm.message import ( + AIFunctionCallMessage, + AIToolCallMessage, + BaseMessage, + HumanFunctionResultMessage, + HumanRegularMessage, + HumanToolResultMessage, + ToolMessage, +) +from aidial_adapter_bedrock.llm.tools.tools_config import ToolsMode + + +def to_dial_tool_call(block: ToolUseBlock) -> ToolCall: + return ToolCall( + index=None, + id=block.id, + type="function", + function=FunctionCall( + name=block.name, + arguments=json.dumps(block.input), + ), + ) + + +def to_dial_function_call(block: ToolUseBlock) -> FunctionCall: + return FunctionCall(name=block.name, arguments=json.dumps(block.input)) + + +def process_tools_block( + consumer: Consumer, block: ToolUseBlock, tools_mode: ToolsMode | None +): + match tools_mode: + case ToolsMode.TOOLS: + consumer.create_function_tool_call(to_dial_tool_call(block)) + case ToolsMode.FUNCTIONS: + consumer.create_function_call(to_dial_function_call(block)) + case None: + raise ValidationError( + "A model has called a tool, but no tools were given to the model in the first place." + ) + case _: + raise Exception(f"Unknown {tools_mode} during tool use!") + + +def process_with_tools( + message: BaseMessage | ToolMessage, tools_mode: ToolsMode | None +) -> BaseMessage | HumanToolResultMessage | AIToolCallMessage: + """ + 1. Validates, that no Functions or Tools messages are used without config + 2. Validates, that client don't use Functions messages with tools config + 3. Validates, that client don't use Tools messages with functions config + 4. Convert Functions messages to Tools messages (Claude supports only Tools). + For tool id we just use function name + """ + if tools_mode is None: + if not isinstance(message, BaseMessage): + raise ValidationError( + "You cannot use messages with functions or tools without config. Please change your messages." + ) + return message + elif tools_mode == ToolsMode.TOOLS: + if isinstance(message, HumanFunctionResultMessage) or isinstance( + message, AIFunctionCallMessage + ): + raise ValidationError( + "You cannot use function messages with tools config." + ) + return message + elif tools_mode == ToolsMode.FUNCTIONS: + match message: + case HumanRegularMessage(): + return message + case HumanToolResultMessage() | AIToolCallMessage(): + raise ValidationError( + "You cannot use tools messages with functions config." + ) + case AIFunctionCallMessage(): + return AIToolCallMessage( + content=message.content, + calls=[ + ToolCall( + index=None, + id=message.call.name, + type="function", + function=message.call, + ) + ], + ) + case HumanFunctionResultMessage(): + return HumanToolResultMessage( + id=message.name, content=message.content + ) + case _: + raise ValueError(f"Unknown message type {type(message)}") + + else: + assert_never(tools_mode) diff --git a/aidial_adapter_bedrock/llm/tools/claude_protocol.py b/aidial_adapter_bedrock/llm/tools/claude_protocol.py index 8d0b4d2c..27bf8c86 100644 --- a/aidial_adapter_bedrock/llm/tools/claude_protocol.py +++ b/aidial_adapter_bedrock/llm/tools/claude_protocol.py @@ -8,7 +8,7 @@ AIFunctionCallMessage, AIToolCallMessage, ) -from aidial_adapter_bedrock.llm.tools.tools_config import ToolsConfig +from aidial_adapter_bedrock.llm.tools.tools_config import ToolsConfig, ToolsMode from aidial_adapter_bedrock.utils.pydantic import ExtraForbidModel from aidial_adapter_bedrock.utils.xml import parse_xml, tag, tag_nl @@ -169,7 +169,7 @@ def parse_call( return None call = _parse_function_call(text) - if config.is_tool: + if config.tools_mode == ToolsMode.TOOLS: id = config.create_fresh_tool_call_id(call.name) tool_call = ToolCall(index=0, id=id, type="function", function=call) return AIToolCallMessage(calls=[tool_call]) diff --git a/aidial_adapter_bedrock/llm/tools/tools_config.py b/aidial_adapter_bedrock/llm/tools/tools_config.py index 5daf220d..abe4f5aa 100644 --- a/aidial_adapter_bedrock/llm/tools/tools_config.py +++ b/aidial_adapter_bedrock/llm/tools/tools_config.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Dict, List, Literal, Self, Tuple, assert_never from aidial_sdk.chat_completion import ( @@ -13,6 +14,14 @@ from aidial_adapter_bedrock.llm.errors import ValidationError +class ToolsMode(Enum): + TOOLS = "TOOLS" + """ + Functions are deprecated instrument, that came before tools + """ + FUNCTIONS = "FUNCTIONS" + + class ToolsConfig(BaseModel): functions: List[Function] """ @@ -33,12 +42,15 @@ class ToolsConfig(BaseModel): """ @property - def is_tool(self) -> bool: - return self.tool_ids is not None + def tools_mode(self) -> ToolsMode: + if self.tool_ids is not None: + return ToolsMode.TOOLS + else: + return ToolsMode.FUNCTIONS def not_supported(self) -> None: if self.functions: - if self.is_tool: + if self.tools_mode == ToolsMode.TOOLS: raise ValidationError("The tools aren't supported") else: raise ValidationError("The functions aren't supported") diff --git a/poetry.lock b/poetry.lock index 9553ac55..bf67617e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aidial-sdk" @@ -147,13 +147,13 @@ frozenlist = ">=1.1.0" [[package]] name = "anthropic" -version = "0.19.2" +version = "0.28.1" description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.7" files = [ - {file = "anthropic-0.19.2-py3-none-any.whl", hash = "sha256:48f43091e977dac703beb46e46c44c172a443aebddf5fe6dd5776a660abb2907"}, - {file = "anthropic-0.19.2.tar.gz", hash = "sha256:ed3466ba365bdce5c5dbd2ce7915f59c6d6d8fcd69e79e4452cf06582741ca16"}, + {file = "anthropic-0.28.1-py3-none-any.whl", hash = "sha256:c4773ae2b42951a6b747bed328b0d03fa412938c95c3a8b9dce70d69badb710b"}, + {file = "anthropic-0.28.1.tar.gz", hash = "sha256:e3a6d595bde241141bdc685edc393903ec95c7fa378013a71186cfb8f32b1793"}, ] [package.dependencies] @@ -162,6 +162,7 @@ boto3 = {version = ">=1.28.57", optional = true, markers = "extra == \"bedrock\" botocore = {version = ">=1.31.57", optional = true, markers = "extra == \"bedrock\""} distro = ">=1.7.0,<2" httpx = ">=0.23.0,<1" +jiter = ">=0.4.0,<1" pydantic = ">=1.9.0,<3" sniffio = "*" tokenizers = ">=0.13.0" @@ -945,6 +946,76 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib" plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[[package]] +name = "jiter" +version = "0.4.2" +description = "Fast iterable JSON parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jiter-0.4.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c2b003ff58d14f5e182b875acd5177b2367245c19a03be9a2230535d296f7550"}, + {file = "jiter-0.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b48c77c25f094707731cd5bad6b776046846b60a27ee20efc8fadfb10a89415f"}, + {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f50ad6b172bde4d45f4d4ea10c49282a337b8bb735afc99763dfa55ea84a743"}, + {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f6001e86f525fbbc9706db2078dc22be078b0950de55b92d37041930f5f940"}, + {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16646ef23b62b007de80460d303ebb2d81e355dac9389c787cec87cdd7ffef2f"}, + {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b4e847c13b0bf1255c711a92330e7a8cb8b5cdd1e37d7db309627bcdd3367ff"}, + {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c536589be60e4c5f2b20fadc4db7e9f55d4c9df3551f29ddf1c4a18dcc9dd54"}, + {file = "jiter-0.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3b2763996167830889a854b4ded30bb90897f9b76be78069c50c3ec4540950e"}, + {file = "jiter-0.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:675e8ab98c99495091af6b6e9bf2b6353bcf81f25ab6ce27d36127e315b4505d"}, + {file = "jiter-0.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e48e43d9d999aaf55f53406b8846ff8cbe3e47ee4b9dc37e5a10a65ce760809f"}, + {file = "jiter-0.4.2-cp310-none-win32.whl", hash = "sha256:881b6e67c50bc36acb3570eda693763c8cd77d590940e06fa6d325d0da52ec1b"}, + {file = "jiter-0.4.2-cp310-none-win_amd64.whl", hash = "sha256:bb8f7b43259efc6add0d721ade2953e064b24e2026d26d979bc09ec080844cef"}, + {file = "jiter-0.4.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:24ad336ac47f274fa83f6fbedcabff9d3387c80f67c66b992688e6a8ba2c47e9"}, + {file = "jiter-0.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc392a220095730afe365ce1516f2f88bb085a2fd29ea191be9c6e3c71713d9a"}, + {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1fdc408de36c81460896de0176f2f7b9f3574dcd35693a0b2c00f4ca34c98e4"}, + {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10ad76722ee6a8c820b0db06a793c08b7d679e5201b9563015bd1e06c959a09"}, + {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb46d1e9c82bba87f0cbda38413e49448a7df35b1e55917124bff9f38974a23"}, + {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:194e28ef4b5f3b61408cb2ee6b6dcbcdb0c9063d01b92b01345b7605692849f5"}, + {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0a447533eccd62748a727e058efa10a8d7cf1de8ffe1a4d705ecb41dad9090"}, + {file = "jiter-0.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5f7704d7260bbb88cca3453951af739589132b26e896a3144fa2dae2263716d7"}, + {file = "jiter-0.4.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:01427458bc9550f2eda09d425755330e7d0eb09adce099577433bebf05d28d59"}, + {file = "jiter-0.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:159b8416879c0053b17c352f70b67b749ef5b2924c6154318ecf71918aab0905"}, + {file = "jiter-0.4.2-cp311-none-win32.whl", hash = "sha256:f2445234acfb79048ce1a0d5d0e181abb9afd9e4a29d8d9988fe26cc5773a81a"}, + {file = "jiter-0.4.2-cp311-none-win_amd64.whl", hash = "sha256:e15a65f233b6b0e5ac10ddf3b97ceb18aa9ffba096259961641d78b4ee321bd5"}, + {file = "jiter-0.4.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d61d59521aea9745447ce50f74d39a16ef74ec9d6477d9350d77e75a3d774ad2"}, + {file = "jiter-0.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eef607dc0acc251923427808dbd017f1998ae3c1a0430a261527aa5cbb3a942"}, + {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af6bf39954646e374fc47429c656372ac731a6a26b644158a5a84bcdbed33a47"}, + {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f509d23606e476852ee46a2b65b5c4ad3905f17424d9cc19c1dffa1c94ba3c6"}, + {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59672774daa44ee140aada0c781c82bee4d9ac5e522966186cfb6b3c217d8a51"}, + {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a0458efac5afeca254cf557b8a654e17013075a69905c78f88d557f129d871"}, + {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8860766d1c293e75c1bb4e25b74fa987e3adf199cac3f5f9e6e49c2bebf092f"}, + {file = "jiter-0.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a109f3281b72bbf4921fe43db1005c004a38559ca0b6c4985add81777dfe0a44"}, + {file = "jiter-0.4.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:faa7e667454b77ad2f0ef87db39f4944de759617aadf210ea2b73f26bb24755f"}, + {file = "jiter-0.4.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3512f8b00cafb6780b427cb6282800d2bf8277161d9c917830661bd4ed1d3528"}, + {file = "jiter-0.4.2-cp312-none-win32.whl", hash = "sha256:853b35d508ee5b66d06630473c1c0b7bb5e29bf4785c9d2202437116c94f7e21"}, + {file = "jiter-0.4.2-cp312-none-win_amd64.whl", hash = "sha256:4a3a8197784278eb8b24cb02c45e1cad67c2ce5b5b758adfb19b87f74bbdff9c"}, + {file = "jiter-0.4.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ca2a4d750aed3154b89f2efb148609fc985fad8db739460797aaf9b478acedda"}, + {file = "jiter-0.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e6c304b3cc6896256727e1fb8991c7179a345eca8224e201795e9cacf4683b0"}, + {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cc34ac708ae1750d077e490321761ec4b9a055b994cbdd1d6fbd37099e4aa7b"}, + {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c93383875ab8d2e4f760aaff335b4a12ff32d4f9cf49c4498d657734f611466"}, + {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce197ee044add576afca0955b42142dd0312639adb6ebadbdbe4277f2855614f"}, + {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a427716813ff65480ca5b5117cfa099f49b49cd38051f8609bd0d5493013ca0"}, + {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479990218353356234669e70fac53e5eb6f739a10db25316171aede2c97d9364"}, + {file = "jiter-0.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d35a91ec5ac74cf33234c431505299fa91c0a197c2dbafd47400aca7c69489d4"}, + {file = "jiter-0.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b27189847193708c94ad10ca0d891309342ae882725d2187cf5d2db02bde8d1b"}, + {file = "jiter-0.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76c255308cd1093fb411a03756b7bb220e48d4a98c30cbc79ed448bf3978e27d"}, + {file = "jiter-0.4.2-cp38-none-win32.whl", hash = "sha256:bb77438060bad49cc251941e6701b31138365c8a0ddaf10cdded2fcc6dd30701"}, + {file = "jiter-0.4.2-cp38-none-win_amd64.whl", hash = "sha256:ce858af19f7ce0d4b51c9f6c0c9d08f1e9dcef1986c5875efd0674a7054292ca"}, + {file = "jiter-0.4.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:6128838a2f357b3921b2a3242d5dc002ae4255ecc8f9f05c20d56d7d2d79c5ad"}, + {file = "jiter-0.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2420cebb9ba856cb57dcab1d2d8def949b464b0db09c22a4e4dbd52fff7b200"}, + {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d13d8128e853b320e00bb18bd4bb8b136cc0936091dc87633648fc688eb705"}, + {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eba5d6e54f149c508ba88677f97d3dc7dd75e9980d234bbac8027ac6db0763a3"}, + {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fad5d64af0bc0545237419bf4150d8de56f0bd217434bdd1a59730327252bef"}, + {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d179e7bca89cf5719bd761dd37a341ff0f98199ecaa9c14af09792e47e977cc"}, + {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36353caee9f103d8ee7bda077f6400505b0f370e27eabcab33a33d21de12a2a6"}, + {file = "jiter-0.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dd146c25bce576ca5db64fc7eccb8862af00f1f0e30108796953f12a53660e4c"}, + {file = "jiter-0.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:14b7c08cadbcd703041c66dc30e24e17de2f340281cac0e69374223ecf153aa4"}, + {file = "jiter-0.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a90f1a8b3d29aea198f8ea2b01148276ced8056e5103f32525266b3d880e65c9"}, + {file = "jiter-0.4.2-cp39-none-win32.whl", hash = "sha256:25b174997c780337b61ae57b1723455eecae9a17a9659044fd3c3b369190063f"}, + {file = "jiter-0.4.2-cp39-none-win_amd64.whl", hash = "sha256:bef62cea18521c5b99368147040c7e560c55098a35c93456f110678a2d34189a"}, + {file = "jiter-0.4.2.tar.gz", hash = "sha256:29b9d44f23f0c05f46d482f4ebf03213ee290d77999525d0975a17f875bf1eea"}, +] + [[package]] name = "jmespath" version = "1.0.1" @@ -2286,4 +2357,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11,<4.0" -content-hash = "712fc50447c4002f29474ca4037bf16fec7426a053a825aa7497ebc947f3f72d" +content-hash = "8d72a6b7220926dab5b2e36179e9447e0d8b9b359918557acf0722ec9feb0cf3" diff --git a/pyproject.toml b/pyproject.toml index 8da06b56..c2dd4d5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ python = "^3.11,<4.0" boto3 = "1.28.57" botocore = "1.31.57" aidial-sdk = {version = "0.8.0", extras = ["telemetry"]} -anthropic = {version = "0.19.2", extras = ["bedrock"]} +anthropic = {version = "0.28.1", extras = ["bedrock"]} fastapi = "0.109.2" openai = "1.13.3" uvicorn = "0.23.2"