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: