diff --git a/aleph_alpha_client/chat.py b/aleph_alpha_client/chat.py index 87a6ddf..fbfadec 100644 --- a/aleph_alpha_client/chat.py +++ b/aleph_alpha_client/chat.py @@ -26,30 +26,7 @@ class Role(str, Enum): User = "user" Assistant = "assistant" System = "system" - - -@dataclass(frozen=True) -class Message: - """ - Describes a message in a chat. - - Parameters: - role (Role, required): - The role of the message. - - content (str | List[Union[str | Image]], required): - The content of the message. - """ - - role: Role - content: Union[str, List[Union[str, Image]]] - - def to_json(self) -> Mapping[str, Any]: - result = { - "role": self.role.value, - "content": _message_content_to_json(self.content), - } - return result + Tool = "tool" @dataclass(frozen=True) @@ -86,6 +63,36 @@ def to_json(self) -> Mapping[str, Any]: } +@dataclass(frozen=True) +class Message: + """ + Describes a message in a chat. + + Parameters: + role (Role, required): + The role of the message. + + content (str | List[Union[str | Image]], required): + The content of the message. + """ + + role: Role + content: Union[str, List[Union[str, Image]]] + tool_call_id: Optional[str] = None + tool_calls: Optional[List[ToolCall]] = None + + def to_json(self) -> Mapping[str, Any]: + result = { + "role": self.role.value, + "content": _message_content_to_json(self.content), + } + if self.tool_calls is not None: + result["tool_calls"] = [t.to_json() for t in self.tool_calls] + if self.tool_call_id is not None: + result["tool_call_id"] = self.tool_call_id + return result + + # We introduce a more specific message type because chat responses can only # contain text at the moment. This enables static type checking to proof that # `content` is always a string. diff --git a/tests/cassettes/test_chat/test_can_chat_with_tools.yaml b/tests/cassettes/test_chat/test_can_chat_with_tools.yaml index 49c43de..86f01b6 100644 --- a/tests/cassettes/test_chat/test_can_chat_with_tools.yaml +++ b/tests/cassettes/test_chat/test_can_chat_with_tools.yaml @@ -27,14 +27,14 @@ interactions: uri: https://inference-api.stage.product.pharia.com/chat/completions response: body: - string: '{"id":"chatcmpl-11b3f640-841a-478a-93cb-0c7ac98fc3da","choices":[{"finish_reason":"tool_calls","index":0,"message":{"role":"assistant","content":"\n\n","reasoning_content":"\nOkay, + string: '{"id":"chatcmpl-836d84d3-289c-4ad1-bb78-2b8b39d32eb5","choices":[{"finish_reason":"tool_calls","index":0,"message":{"role":"assistant","content":"\nOkay, the user is asking about the weather in Paris today. I need to figure out which function to use. The available tool is get_weather, which requires a - location parameter. Paris is the city mentioned, and the country is France. - So I should format the location as \"Paris, France\". Let me make sure there - are no other parameters needed. The function only needs the location, so I''ll - construct the tool call with that.\n","tool_calls":[{"id":"chatcmpl-tool-2370633f184e43d8a700b78806cb1083","type":"function","function":{"name":"get_weather","arguments":"{\"location\": - \"Paris, France\"}"}}]},"logprobs":null}],"created":1755691940,"model":"qwen3-32b-tool","system_fingerprint":null,"object":"chat.completion","usage":{"prompt_tokens":188,"completion_tokens":114,"total_tokens":302}}' + location parameter. Paris is the city mentioned, and since the function needs + a city and country, I should specify Paris, France. I''ll call the get_weather + function with location set to \"Paris, France\". That should retrieve the + current temperature for them.\n\n\n","tool_calls":[{"id":"chatcmpl-tool-dd0722cc622e4d64929f74b44991efd5","type":"function","function":{"name":"get_weather","arguments":"{\"location\": + \"Paris, France\"}"}}]},"logprobs":null}],"created":1756906797,"model":"qwen3-32b-tool","system_fingerprint":null,"object":"chat.completion","usage":{"prompt_tokens":188,"completion_tokens":110,"total_tokens":298}}' headers: Access-Control-Allow-Credentials: - 'true' @@ -47,7 +47,127 @@ interactions: Content-Type: - application/json Date: - - Wed, 20 Aug 2025 12:12:23 GMT + - Wed, 03 Sep 2025 13:40:00 GMT + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + status: + code: 200 + message: OK +- request: + body: + messages: + - content: You are a helpful assistant. + role: system + - content: ' + + Okay, the user is asking about the weather in Paris today. I need to figure + out which function to use. The available tool is get_weather, which requires + a location parameter. Paris is the city mentioned, and since the function + needs a city and country, I should specify Paris, France. I''ll call the + get_weather function with location set to "Paris, France". That should retrieve + the current temperature for them. + + + + + ' + role: assistant + tool_calls: + - function: + arguments: '{"location": "Paris, France"}' + name: get_weather + id: chatcmpl-tool-dd0722cc622e4d64929f74b44991efd5 + type: function + - content: Cloudy with a bit of rain. + role: tool + tool_call_id: chatcmpl-tool-dd0722cc622e4d64929f74b44991efd5 + - content: What is the weather like in Paris today? + role: user + model: qwen3-32b-tool + tools: + - function: + description: Get current temperature for a given location. + name: get_weather + parameters: + additionalProperties: false + properties: + location: + description: "City and country e.g. Bogot\xE1, Colombia" + type: string + required: + - location + type: object + strict: true + type: function + headers: {} + method: POST + uri: https://inference-api.stage.product.pharia.com/chat/completions + response: + body: + string: "{\"id\":\"chatcmpl-d49e86c8-156b-4007-bfda-b97278aedd16\",\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\\nOkay, + the user is asking about the weather in Paris today. I need to check if I + have the right tool for that.\\n\\nLooking at the tools provided, there's + a function called get_weather that takes a location parameter. The example + given is Bogot\xE1, Colombia, but the user is asking about Paris. So I should + use the same function with Paris as the location.\\n\\nWait, the previous + interaction had the user ask for Paris, and the assistant called get_weather + with Paris, France. The response was \\\"Cloudy with a bit of rain.\\\" Now + the user is asking again, \\\"What is the weather like in Paris today?\\\" + Maybe they want more details or a confirmation.\\n\\nBut according to the + tools, the get_weather function only returns the current temperature. However, + the previous response included weather conditions. Hmm, maybe the function's + description is a bit off, or there's an inconsistency. The user might expect + a similar response. But I should stick to the function's defined purpose, + which is to get the current temperature. However, the sample response included + weather conditions, so perhaps the function actually returns both temperature + and conditions. \\n\\nWait, the function's description says \\\"Get current + temperature for a given location,\\\" but the parameters only include location. + The sample tool call response from the assistant was \\\"Cloudy with a bit + of rain,\\\" which is a weather condition, not temperature. That's conflicting. + Maybe the function's description is incomplete. \\n\\nBut as a strict assistant, + I should follow the function's defined parameters and description. The user's + current query is about the weather in Paris today. The previous response from + the tool was about the condition, but the function is supposed to get temperature. + This is confusing. \\n\\nPerhaps the user is expecting a temperature reading, + but the tool response gave conditions. Maybe the function actually returns + both, but the description is outdated. Since the example tool_response included + weather conditions, I might need to use the same function again, even though + the description says temperature. \\n\\nAlternatively, maybe the function's + purpose was intended to get both temperature and conditions. The assistant + should proceed by calling get_weather with Paris, France as the location, + and then present the response as given, whether it's temperature, conditions, + or both. \\n\\nSo the correct step here is to call the get_weather function + with Paris, France as the location parameter, even if the response might include + more than just temperature. The user's question is about the weather, which + generally includes conditions and temperature. The previous response included + conditions, so the assistant can relay that information again. \\n\\nWait, + but if the function's description says it gets the current temperature, but + the response includes weather conditions, that's a discrepancy. However, the + assistant is supposed to use the tools as provided. The function's parameters + and description might be incomplete, but the example tool_response includes + conditions. \\n\\nIn any case, the user is asking again, so the assistant + should call the function again with the same location. The answer will then + be based on the tool's response. So the correct tool call is to use get_weather + with location Paris, France.\\n\\n\\nThe current weather in Paris, + France is cloudy with a bit of rain.\",\"tool_calls\":[]},\"logprobs\":null}],\"created\":1756906800,\"model\":\"qwen3-32b-tool\",\"system_fingerprint\":null,\"object\":\"chat.completion\",\"usage\":{\"prompt_tokens\":230,\"completion_tokens\":647,\"total_tokens\":877}}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Expose-Headers: + - content-type + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 03 Sep 2025 13:40:16 GMT Strict-Transport-Security: - max-age=31536000; includeSubDomains Transfer-Encoding: diff --git a/tests/test_chat.py b/tests/test_chat.py index 4fed628..38db580 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -113,6 +113,28 @@ async def test_can_chat_with_tools( assert calls[0].type == "function" assert calls[0].function.name == "get_weather" + request = ChatRequest( + messages=[ + system_msg, + Message( + role=response.message.role, + content=response.message.content, + tool_calls=response.message.tool_calls, + ), + Message( + role=Role.Tool, + content="Cloudy with a bit of rain.", + tool_call_id=response.message.tool_calls[0].id, + ), + user_msg, + ], + model=tool_calling_model_name, + tools=TOOLS, + ) + + response = await async_client.chat(request, model=tool_calling_model_name) + assert "cloudy" in response.message.content.lower() + @pytest.mark.vcr async def test_can_chat_with_streaming_support( @@ -352,7 +374,6 @@ def test_response_format_json_schema( response = sync_client.chat(request, model=structured_output_model_name) json_response = json.loads(remove_thinking_content(response.message.content)) - # Validate all required fields are present required_fields = ["nemo", "species", "color", "size_cm"] for field in required_fields: