Skip to content

Commit b0145e8

Browse files
Merge pull request #914 from NexaAI/feat/mengsheng/functioncall
feat: enhance function calling with nested schema support and improved parameter formatting
2 parents b69d530 + b6634de commit b0145e8

4 files changed

Lines changed: 145 additions & 35 deletions

File tree

demos/function-calling/app/flask_ui.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,29 @@ async def get_response(task_id):
149149
summary=event.get("summary", "No Title")
150150
date = "N/A"
151151
if event.get("start"):
152-
start_time = event["start"].get("dateTime", "N/A")
153-
date = start_time.split("T")[0]
154-
start_time = start_time.split("T")[1] if "T" in start_time else "N/A"
152+
start_time = event["start"].get("dateTime", None)
153+
from datetime import datetime
154+
if start_time is not None:
155+
dt = datetime.fromisoformat(start_time)
156+
date = dt.date().isoformat()
157+
start_time = dt.strftime("%I:%M %p")
158+
else:
159+
date = "N/A"
160+
start_time = "N/A"
155161
else:
156162
start_time = "N/A"
157163
if event.get("end"):
158-
end_time = event["end"].get("dateTime", "N/A")
159-
end_time = end_time.split("T")[1] if "T" in end_time else "N/A"
164+
end_time = event["end"].get("dateTime", None)
165+
if end_time is not None:
166+
dt = datetime.fromisoformat(end_time)
167+
end_time = dt.strftime("%I:%M %p")
168+
else:
169+
end_time = "N/A"
160170
else:
161171
end_time = "N/A"
162172
venue = event.get("location", "N/A")
163173
description = event.get("description", summary)
164-
address = event.get("address", "N/A")
174+
address = event.get("address", venue)
165175
htmlLink = event.get("htmlLink", "")
166176
bot_response = add_bot_response(
167177
response_type='event',

demos/function-calling/app/templates/chat.html

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -809,14 +809,8 @@
809809
<div class="event-detail">
810810
<div class="event-detail-label">Event Location</div>
811811
<div style="margin-left: 16px;">
812-
<span style="color: #ADA8A4; font-size: 14px;">Venue:</span>
813-
<span class="event-detail-value">${escapeHtml(event.venue)}</span>
814-
815-
<span>&nbsp;&nbsp;</span>
816-
817812
<span style="color: #ADA8A4; font-size: 14px;">Address:</span>
818813
<span class="event-detail-value">${escapeHtml(event.address)}</span>
819-
820814
</div>
821815
</div>
822816

demos/function-calling/image.png

-341 KB
Binary file not shown.

demos/function-calling/main.py

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,44 @@
1616
from mcp.client.stdio import stdio_client
1717

1818

19+
def _convert_schema_property(prop_schema: Dict[str, Any]) -> Dict[str, Any]:
20+
"""Recursively convert a schema property, handling nested objects."""
21+
result = {
22+
"type": prop_schema.get("type", "string"),
23+
}
24+
25+
if "description" in prop_schema:
26+
result["description"] = prop_schema["description"]
27+
28+
# Handle nested objects
29+
if prop_schema.get("type") == "object" and "properties" in prop_schema:
30+
nested_props = {}
31+
nested_required = []
32+
33+
for nested_name, nested_schema in prop_schema["properties"].items():
34+
nested_props[nested_name] = _convert_schema_property(nested_schema)
35+
if nested_name in prop_schema.get("required", []):
36+
nested_required.append(nested_name)
37+
38+
result["properties"] = nested_props
39+
if nested_required:
40+
result["required"] = nested_required
41+
42+
# Handle arrays of objects
43+
if prop_schema.get("type") == "array" and "items" in prop_schema:
44+
result["items"] = _convert_schema_property(prop_schema["items"])
45+
46+
return result
47+
48+
1949
def mcp_tool_to_openai_format(tool) -> Dict[str, Any]:
2050
"""Convert MCP tool to OpenAI function calling format."""
2151
properties = {}
2252
required = []
2353

2454
if tool.inputSchema and "properties" in tool.inputSchema:
2555
for prop_name, prop_schema in tool.inputSchema["properties"].items():
26-
properties[prop_name] = {
27-
"type": prop_schema.get("type", "string"),
28-
"description": prop_schema.get("description", ""),
29-
}
56+
properties[prop_name] = _convert_schema_property(prop_schema)
3057
if tool.inputSchema.get("required") and prop_name in tool.inputSchema["required"]:
3158
required.append(prop_name)
3259

@@ -140,6 +167,43 @@ def extract_function_call(text: str):
140167
return None
141168

142169

170+
def _format_nested_properties(props: Dict[str, Any], required: List[str], prefix: str = "", indent: int = 2) -> List[str]:
171+
"""Recursively format nested object properties with required field indicators."""
172+
param_list = []
173+
indent_str = " " * indent
174+
175+
for param_name, param_info in props.items():
176+
param_type = param_info.get('type', 'string')
177+
param_desc = param_info.get('description', '')
178+
is_required = param_name in required
179+
full_param_name = f"{prefix}.{param_name}" if prefix else param_name
180+
181+
# Handle nested objects
182+
if param_type == 'object' and 'properties' in param_info:
183+
nested_props = param_info.get('properties', {})
184+
nested_required = param_info.get('required', [])
185+
req_mark = " (REQUIRED)" if is_required else ""
186+
187+
if param_desc:
188+
param_list.append(f"{indent_str}{param_name} (object){req_mark}: {param_desc}")
189+
else:
190+
param_list.append(f"{indent_str}{param_name} (object){req_mark}")
191+
192+
# Recursively format nested properties
193+
nested_params = _format_nested_properties(
194+
nested_props, nested_required, full_param_name, indent + 2
195+
)
196+
param_list.extend(nested_params)
197+
else:
198+
req_mark = " (REQUIRED)" if is_required else ""
199+
if param_desc:
200+
param_list.append(f"{indent_str}{param_name} ({param_type}){req_mark}: {param_desc}")
201+
else:
202+
param_list.append(f"{indent_str}{param_name} ({param_type}){req_mark}")
203+
204+
return param_list
205+
206+
143207
def build_system_prompt(tools: list) -> str:
144208
"""Build system prompt from tool schemas."""
145209
tools_descriptions = []
@@ -151,34 +215,48 @@ def build_system_prompt(tools: list) -> str:
151215
props = params.get('properties', {})
152216
required = params.get('required', [])
153217

154-
param_list = []
155-
for param_name, param_info in props.items():
156-
param_type = param_info.get('type', 'string')
157-
param_desc = param_info.get('description', '')
158-
is_required = param_name in required
159-
req_mark = " (required)" if is_required else ""
160-
if param_desc:
161-
param_list.append(f" {param_name} ({param_type}){req_mark}: {param_desc}")
162-
else:
163-
param_list.append(f" {param_name} ({param_type}){req_mark}")
218+
# Format parameters with nested object support
219+
param_list = _format_nested_properties(props, required)
164220

165221
params_str = "\n".join(param_list) if param_list else " (no parameters)"
166-
tools_descriptions.append(f"{name}: {desc}\n{params_str}")
222+
223+
# Highlight required parameters at the top
224+
required_params = [p for p in required]
225+
required_str = f"\n REQUIRED parameters: {', '.join(required_params)}" if required_params else ""
226+
227+
tools_descriptions.append(f"{name}: {desc}{required_str}\n{params_str}")
167228

168229
tools_list = "\n\n".join([f"{i+1}. {td}" for i, td in enumerate(tools_descriptions)])
230+
231+
# Add example for create-event
232+
example_json = """{
233+
"name": "create-event",
234+
"arguments": {
235+
"calendarId": "primary",
236+
"summary": "Meeting",
237+
"start": "2025-01-01T10:00:00",
238+
"end": "2025-01-01T11:00:00"
239+
}
240+
}"""
241+
169242
return f"""You are a calendar assistant. When the user requests calendar actions, respond with ONLY a JSON object in this format:
170243
171244
{{"name": "function_name", "arguments": {{"param": "value"}}}}
172245
246+
CRITICAL RULES:
247+
- You MUST include ALL required parameters (marked as REQUIRED)
248+
- For nested objects, ALL required fields within the object must be included
249+
- If a parameter is marked as REQUIRED, it cannot be omitted
250+
- Output ONLY valid JSON, no other text before or after
251+
- Use exact function and parameter names (case-sensitive)
252+
253+
Example for create-event:
254+
{example_json}
255+
173256
Available functions:
174257
{tools_list}
175258
176-
Rules:
177-
- Output ONLY valid JSON, no other text
178-
- Use exact function and parameter names (case-sensitive)
179-
- Include all required parameters
180-
- For calendar events, use create-event
181-
- For current time, use get-current-time
259+
IMPORTANT: Before creating events, you may need to call get-current-time first to get accurate date/time context. Always include calendarId="primary" for create-event unless specified otherwise.
182260
"""
183261

184262

@@ -320,10 +398,38 @@ async def call_agent(
320398
print('[debug] calling function:', func_name)
321399
func_result = await _execute_with_retry(session, func_name, func_args, tools)
322400
print('[debug] func_result:', func_result)
401+
402+
# Parse function result to extract success/error message
403+
result_message = ""
404+
try:
405+
result_data = json.loads(func_result) if isinstance(func_result, str) else func_result
406+
if result_data.get('isError', False):
407+
# Extract error message
408+
if isinstance(result_data.get('content'), list):
409+
for item in result_data['content']:
410+
if item.get('type') == 'text':
411+
result_message = item.get('text', '')
412+
break
413+
else:
414+
# Extract success message or summary
415+
if isinstance(result_data.get('content'), list):
416+
for item in result_data['content']:
417+
if item.get('type') == 'text':
418+
result_message = item.get('text', '')
419+
break
420+
except Exception:
421+
result_message = str(func_result)
422+
323423
followup = conversation + [
424+
VlmChatMessage(role="assistant", contents=[VlmContent(type="text", text=response_text)]),
324425
VlmChatMessage(role="user", contents=[VlmContent(type="text",
325-
text=f"You called {func_name} with {func_args}. Result: {func_result}. "
326-
f"Provide a natural language response. Do NOT call any function again.")])
426+
text=f"Function execution completed. Result: {result_message}\n\n"
427+
f"Now respond to the user in natural language. You are in RESPONSE MODE, not function calling mode.\n"
428+
f"- DO NOT output any JSON format\n"
429+
f"- DO NOT use {{}} brackets\n"
430+
f"- DO NOT call any function\n"
431+
f"- Just speak naturally like a helpful assistant\n"
432+
f"- Tell the user what happened with the calendar event in a friendly way")])
327433
]
328434
followup_response = ""
329435
for token in vlm.generate_stream(

0 commit comments

Comments
 (0)