Skip to content

Commit f6a23ed

Browse files
committed
fix: fix m serve tool-calling examples
- switch server example to OpenAIBackend - align tool-calling example with tested Granite model setup - narrow advertised tools when `tool_choice` selects a specific function - enable `tool_calls=True` in the serve path - replace calculator example with stock-price tool - examples 1/2 as tool-call-only demos - example 4 as the full tool execution round-trip - improve client diagnostics for empty/no-tool responses Signed-off-by: Mark Sturdevant <mark.sturdevant@ibm.com> Assisted-by: IBM Bob
1 parent d5169fc commit f6a23ed

2 files changed

Lines changed: 249 additions & 80 deletions

File tree

docs/examples/m_serve/client_tool_calling.py

Lines changed: 120 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,55 @@
2727
"name": "get_weather",
2828
"description": "Get the current weather in a given location",
2929
"parameters": {
30-
"type": "object",
31-
"properties": {
32-
"location": {
33-
"type": "string",
34-
"description": "The city name, e.g. San Francisco",
30+
"RootModel": {
31+
"type": "object",
32+
"properties": {
33+
"location": {
34+
"type": "string",
35+
"description": "The city name, e.g. San Francisco",
36+
},
37+
"units": {
38+
"type": "string",
39+
"enum": ["celsius", "fahrenheit"],
40+
"description": "Temperature units",
41+
},
3542
},
36-
"units": {
37-
"type": "string",
38-
"enum": ["celsius", "fahrenheit"],
39-
"description": "Temperature units",
40-
},
41-
},
42-
"required": ["location"],
43+
"required": ["location"],
44+
}
4345
},
4446
},
4547
},
4648
{
4749
"type": "function",
4850
"function": {
49-
"name": "calculator",
50-
"description": "Evaluate a mathematical expression",
51+
"name": "get_stock_price",
52+
"description": "Get the current stock price for a given ticker symbol",
5153
"parameters": {
52-
"type": "object",
53-
"properties": {
54-
"expression": {
55-
"type": "string",
56-
"description": "The mathematical expression to evaluate",
57-
}
58-
},
59-
"required": ["expression"],
54+
"RootModel": {
55+
"type": "object",
56+
"properties": {
57+
"symbol": {
58+
"type": "string",
59+
"description": "The stock ticker symbol, e.g. AAPL, GOOGL",
60+
}
61+
},
62+
"required": ["symbol"],
63+
}
6064
},
6165
},
6266
},
6367
]
6468

6569

66-
def make_request(messages: list[dict], tools: list[dict] | None = None) -> dict:
70+
def make_request(
71+
messages: list[dict], tools: list[dict] | None = None, tool_name: str | None = None
72+
) -> dict:
6773
"""Make a request to the m serve API.
6874
6975
Args:
7076
messages: List of message dictionaries
7177
tools: Optional list of tool definitions
78+
tool_name: Optional tool name to request explicitly
7279
7380
Returns:
7481
Response dictionary from the API
@@ -81,13 +88,53 @@ def make_request(messages: list[dict], tools: list[dict] | None = None) -> dict:
8188

8289
if tools:
8390
payload["tools"] = tools
84-
payload["tool_choice"] = "auto"
91+
if tool_name is not None:
92+
# m serve forwards tool_choice to compatible backends, but the
93+
# downstream provider/model may ignore it or treat it as a weak
94+
# preference rather than a guarantee. Use an explicit function
95+
# selection in this client so the example demonstrates the API
96+
# contract even when the model would otherwise decline to call tools.
97+
payload["tool_choice"] = {
98+
"type": "function",
99+
"function": {"name": tool_name},
100+
}
101+
else:
102+
payload["tool_choice"] = "auto"
85103

86104
response = requests.post(ENDPOINT, json=payload, timeout=30)
87-
response.raise_for_status()
105+
106+
if response.status_code >= 400:
107+
try:
108+
error_payload = response.json()
109+
except ValueError:
110+
error_payload = {"error": {"message": response.text}}
111+
112+
error_message = error_payload.get("error", {}).get("message", response.text)
113+
raise requests.HTTPError(
114+
f"{response.status_code} Server Error: {error_message}", response=response
115+
)
116+
88117
return response.json()
89118

90119

120+
def _run_local_tool(tool_name: str, args: dict) -> str:
121+
"""Simulate local execution of the example tools."""
122+
if tool_name == "get_weather":
123+
units = args.get("units") or "celsius"
124+
unit_suffix = "C" if units == "celsius" else "F"
125+
return f"The weather in {args['location']} is sunny and 22°{unit_suffix}"
126+
if tool_name == "get_stock_price":
127+
mock_prices = {
128+
"AAPL": "$175.43",
129+
"GOOGL": "$142.87",
130+
"MSFT": "$378.91",
131+
"TSLA": "$242.15",
132+
}
133+
symbol = args["symbol"].upper()
134+
return f"The current price of {symbol} is {mock_prices.get(symbol, '$100.00')}"
135+
return "Tool result"
136+
137+
91138
def main():
92139
"""Run example tool calling interactions."""
93140
print("=" * 60)
@@ -100,7 +147,7 @@ def main():
100147
messages = [{"role": "user", "content": "What's the weather like in Tokyo?"}]
101148

102149
print(f"User: {messages[0]['content']}")
103-
response = make_request(messages, tools=tools)
150+
response = make_request(messages, tools=tools, tool_name="get_weather")
104151

105152
choice = response["choices"][0]
106153
print(f"\nFinish Reason: {choice['finish_reason']}")
@@ -111,16 +158,18 @@ def main():
111158
func = tool_call["function"]
112159
args = json.loads(func["arguments"])
113160
print(f" - {func['name']}({json.dumps(args)})")
114-
else:
161+
elif choice.get("message", {}).get("content"):
115162
print(f"Assistant: {choice['message']['content']}")
163+
else:
164+
print("Assistant returned no content and no tool calls.")
116165

117-
# Example 2: Request that should trigger calculator tool
118-
print("\n\n2. Math Query")
166+
# Example 2: Request that should trigger stock price tool
167+
print("\n\n2. Stock Price Query")
119168
print("-" * 60)
120-
messages = [{"role": "user", "content": "What is 15 * 23 + 7?"}]
169+
messages = [{"role": "user", "content": "What's the current stock price of AAPL?"}]
121170

122171
print(f"User: {messages[0]['content']}")
123-
response = make_request(messages, tools=tools)
172+
response = make_request(messages, tools=tools, tool_name="get_stock_price")
124173

125174
choice = response["choices"][0]
126175
print(f"\nFinish Reason: {choice['finish_reason']}")
@@ -131,8 +180,10 @@ def main():
131180
func = tool_call["function"]
132181
args = json.loads(func["arguments"])
133182
print(f" - {func['name']}({json.dumps(args)})")
134-
else:
183+
elif choice.get("message", {}).get("content"):
135184
print(f"Assistant: {choice['message']['content']}")
185+
else:
186+
print("Assistant returned no content and no tool calls.")
136187

137188
# Example 3: Request without tools (normal chat)
138189
print("\n\n3. Normal Chat (No Tools)")
@@ -152,7 +203,7 @@ def main():
152203
messages = [{"role": "user", "content": "What's the weather in Paris?"}]
153204

154205
print(f"User: {messages[0]['content']}")
155-
response = make_request(messages, tools=tools)
206+
response = make_request(messages, tools=tools, tool_name="get_weather")
156207

157208
choice = response["choices"][0]
158209
assistant_message = choice["message"]
@@ -169,17 +220,17 @@ def main():
169220
}
170221
)
171222

223+
tool_results: list[str] = []
224+
172225
# Process each tool call and add tool responses
173226
for tool_call in assistant_message["tool_calls"]:
174227
func = tool_call["function"]
175228
args = json.loads(func["arguments"])
176229
print(f" - {func['name']}({json.dumps(args)})")
177230

178-
# Simulate tool execution
179-
if func["name"] == "get_weather":
180-
tool_result = f"The weather in {args['location']} is sunny and 22°C"
181-
else:
182-
tool_result = "Tool result"
231+
tool_result = _run_local_tool(func["name"], args)
232+
tool_results.append(tool_result)
233+
print(f" Result: {tool_result}")
183234

184235
# Add tool response to conversation
185236
messages.append(
@@ -190,11 +241,32 @@ def main():
190241
}
191242
)
192243

193-
# Get final response after tool execution
244+
# Get final response after tool execution.
245+
# Ask for a concise answer that explicitly uses the tool result so the
246+
# example output includes the actual weather/price instead of only a
247+
# conversational acknowledgement.
248+
messages.append(
249+
{
250+
"role": "user",
251+
"content": (
252+
f"Original question: {messages[0]['content']}\n"
253+
f"Tool result: {'; '.join(tool_results)}\n"
254+
"Answer the original question directly using only that tool "
255+
"result. Do not mention unrelated topics or other tools."
256+
),
257+
}
258+
)
194259
print("\nGetting final response after tool execution...")
195-
response = make_request(messages, tools=tools)
260+
response = make_request(messages, tools=None)
196261
choice = response["choices"][0]
197-
print(f"Assistant: {choice['message']['content']}")
262+
if choice.get("message", {}).get("content"):
263+
print(f"Assistant: {choice['message']['content']}")
264+
else:
265+
print("Assistant returned no content after tool execution.")
266+
elif assistant_message.get("content"):
267+
print(f"Assistant: {assistant_message['content']}")
268+
else:
269+
print("Assistant returned no content and no tool calls.")
198270

199271
print("\n" + "=" * 60)
200272
print("Examples completed!")
@@ -208,5 +280,12 @@ def main():
208280
print("Error: Could not connect to server.")
209281
print("Make sure the server is running:")
210282
print(" uv run m serve docs/examples/m_serve/m_serve_example_tool_calling.py")
283+
except requests.exceptions.HTTPError as e:
284+
print(f"Error: {e}")
285+
if e.response is not None:
286+
try:
287+
print("Server response:", json.dumps(e.response.json(), indent=2))
288+
except ValueError:
289+
print("Server response:", e.response.text)
211290
except Exception as e:
212291
print(f"Error: {e}")

0 commit comments

Comments
 (0)