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+
91138def 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"\n Finish 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 \n 2. Math Query" )
166+ # Example 2: Request that should trigger stock price tool
167+ print ("\n \n 2. 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"\n Finish 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 \n 3. 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 ("\n Getting 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