Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/xai_sdk/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,12 +589,45 @@ def system(*args: Content) -> chat_pb2.Message:
return chat_pb2.Message(role=chat_pb2.MessageRole.ROLE_SYSTEM, content=[_process_content(c) for c in args])


def tool_result(result: str) -> chat_pb2.Message:
def tool_result(result: str, tool_call_id: Optional[str] = None) -> chat_pb2.Message:
"""Creates a new message of role "tool".

Use this to add the result of a tool call to conversation history via `append`.
Use this to provide the result of a client-side tool execution back to the model in the conversation history.
This enables multi-turn tool use and agentic workflows: the model calls a tool, you execute it, then append
the result.

Args:
result: The string output/result from your tool's execution. This will be sent to the model as content.
tool_call_id: Optional ID linking this result to a specific tool call (should match `tool_call.id` from
the assistant's tool_calls list). Essential for parallel_tool_calls or multiple tools to associate results
correctly.
If omitted (for single tool calls), the association may still work but is less explicit.

Examples:
Basic function calling loop:
```python
from xai_sdk.chat import tool_result
import json

# ... after chat.sample() or in stream
if response.tool_calls:
for tool_call in response.tool_calls:
# Parse and execute
args = json.loads(tool_call.function.arguments)
tool_output = get_weather(args["city"]) # Your tool function
# Append with tool_call_id for proper linking
chat.append(tool_result(tool_output, tool_call_id=tool_call.id))
# Continue conversation
response = chat.sample()
```

See `examples/sync/function_calling.py` and `examples/sync/server_side_tools.py` (for mixed client/server
tools) for complete patterns.

Returns:
A `chat_pb2.Message` object with ROLE_TOOL, ready to append to chat.
"""
return chat_pb2.Message(role=chat_pb2.MessageRole.ROLE_TOOL, content=[text(result)])
return chat_pb2.Message(role=chat_pb2.MessageRole.ROLE_TOOL, content=[text(result)], tool_call_id=tool_call_id)


def tool(name: str, description: str, parameters: dict[str, Any]) -> chat_pb2.Tool:
Expand Down
140 changes: 70 additions & 70 deletions src/xai_sdk/proto/v5/chat_pb2.py

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/xai_sdk/proto/v5/chat_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -367,20 +367,22 @@ class FileContent(_message.Message):
def __init__(self, file_id: _Optional[str] = ...) -> None: ...

class Message(_message.Message):
__slots__ = ("content", "reasoning_content", "role", "name", "tool_calls", "encrypted_content")
__slots__ = ("content", "reasoning_content", "role", "name", "tool_calls", "encrypted_content", "tool_call_id")
CONTENT_FIELD_NUMBER: _ClassVar[int]
REASONING_CONTENT_FIELD_NUMBER: _ClassVar[int]
ROLE_FIELD_NUMBER: _ClassVar[int]
NAME_FIELD_NUMBER: _ClassVar[int]
TOOL_CALLS_FIELD_NUMBER: _ClassVar[int]
ENCRYPTED_CONTENT_FIELD_NUMBER: _ClassVar[int]
TOOL_CALL_ID_FIELD_NUMBER: _ClassVar[int]
content: _containers.RepeatedCompositeFieldContainer[Content]
reasoning_content: str
role: MessageRole
name: str
tool_calls: _containers.RepeatedCompositeFieldContainer[ToolCall]
encrypted_content: str
def __init__(self, content: _Optional[_Iterable[_Union[Content, _Mapping]]] = ..., reasoning_content: _Optional[str] = ..., role: _Optional[_Union[MessageRole, str]] = ..., name: _Optional[str] = ..., tool_calls: _Optional[_Iterable[_Union[ToolCall, _Mapping]]] = ..., encrypted_content: _Optional[str] = ...) -> None: ...
tool_call_id: str
def __init__(self, content: _Optional[_Iterable[_Union[Content, _Mapping]]] = ..., reasoning_content: _Optional[str] = ..., role: _Optional[_Union[MessageRole, str]] = ..., name: _Optional[str] = ..., tool_calls: _Optional[_Iterable[_Union[ToolCall, _Mapping]]] = ..., encrypted_content: _Optional[str] = ..., tool_call_id: _Optional[str] = ...) -> None: ...

class ToolChoice(_message.Message):
__slots__ = ("mode", "function_name")
Expand Down
140 changes: 70 additions & 70 deletions src/xai_sdk/proto/v6/chat_pb2.py

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/xai_sdk/proto/v6/chat_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -368,20 +368,22 @@ class FileContent(_message.Message):
def __init__(self, file_id: _Optional[str] = ...) -> None: ...

class Message(_message.Message):
__slots__ = ("content", "reasoning_content", "role", "name", "tool_calls", "encrypted_content")
__slots__ = ("content", "reasoning_content", "role", "name", "tool_calls", "encrypted_content", "tool_call_id")
CONTENT_FIELD_NUMBER: _ClassVar[int]
REASONING_CONTENT_FIELD_NUMBER: _ClassVar[int]
ROLE_FIELD_NUMBER: _ClassVar[int]
NAME_FIELD_NUMBER: _ClassVar[int]
TOOL_CALLS_FIELD_NUMBER: _ClassVar[int]
ENCRYPTED_CONTENT_FIELD_NUMBER: _ClassVar[int]
TOOL_CALL_ID_FIELD_NUMBER: _ClassVar[int]
content: _containers.RepeatedCompositeFieldContainer[Content]
reasoning_content: str
role: MessageRole
name: str
tool_calls: _containers.RepeatedCompositeFieldContainer[ToolCall]
encrypted_content: str
def __init__(self, content: _Optional[_Iterable[_Union[Content, _Mapping]]] = ..., reasoning_content: _Optional[str] = ..., role: _Optional[_Union[MessageRole, str]] = ..., name: _Optional[str] = ..., tool_calls: _Optional[_Iterable[_Union[ToolCall, _Mapping]]] = ..., encrypted_content: _Optional[str] = ...) -> None: ...
tool_call_id: str
def __init__(self, content: _Optional[_Iterable[_Union[Content, _Mapping]]] = ..., reasoning_content: _Optional[str] = ..., role: _Optional[_Union[MessageRole, str]] = ..., name: _Optional[str] = ..., tool_calls: _Optional[_Iterable[_Union[ToolCall, _Mapping]]] = ..., encrypted_content: _Optional[str] = ..., tool_call_id: _Optional[str] = ...) -> None: ...

class ToolChoice(_message.Message):
__slots__ = ("mode", "function_name")
Expand Down
8 changes: 7 additions & 1 deletion tests/aio/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1756,13 +1756,19 @@ def test_chat_append_tool_result(client: AsyncClient):
chat = client.chat.create("grok-3")
chat.append(user("test message"))
chat.append(tool_result("test result"))
chat.append(tool_result("test result with id", tool_call_id="test-tool-call-id"))

expected_messages = [
chat_pb2.Message(role=chat_pb2.ROLE_USER, content=[chat_pb2.Content(text="test message")]),
chat_pb2.Message(role=chat_pb2.ROLE_TOOL, content=[chat_pb2.Content(text="test result")]),
chat_pb2.Message(
role=chat_pb2.ROLE_TOOL,
content=[chat_pb2.Content(text="test result with id")],
tool_call_id="test-tool-call-id",
),
]

assert len(chat.messages) == 2
assert len(chat.messages) == 3
assert chat.messages == expected_messages


Expand Down
8 changes: 7 additions & 1 deletion tests/sync/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1750,13 +1750,19 @@ def test_chat_append_tool_result(client: Client):
chat = client.chat.create("grok-3")
chat.append(user("test message"))
chat.append(tool_result("test result"))
chat.append(tool_result("test result with id", tool_call_id="test-tool-call-id"))

expected_messages = [
chat_pb2.Message(role=chat_pb2.ROLE_USER, content=[chat_pb2.Content(text="test message")]),
chat_pb2.Message(role=chat_pb2.ROLE_TOOL, content=[chat_pb2.Content(text="test result")]),
chat_pb2.Message(
role=chat_pb2.ROLE_TOOL,
content=[chat_pb2.Content(text="test result with id")],
tool_call_id="test-tool-call-id",
),
]

assert len(chat.messages) == 2
assert len(chat.messages) == 3
assert chat.messages == expected_messages


Expand Down