4. Add Application-Managed Tools
Function calling lets the model ask your application for data or actions, but your code remains responsible for executing tools and returning results. This section defines local BrightCart tools, then walks through a single function call, multiple independent calls, and a custom text tool. The examples keep tool outputs deterministic so the request loop is easy to inspect.
4.1 Define Local Tool Schemas and Functions
Define local sample tools for order status and customer profile lookups. The tool schemas describe the names, descriptions, argument shapes, required fields, and strictness that the model can use when deciding what to call. The Python functions stand in for application systems such as order management, CRM, or policy services.
from __future__ import annotations
function_tools = [
{
"type": "function",
"name": "get_order_status",
"description": "Look up a sample BrightCart order status.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string", "description": "An order ID such as ORDER-8831."}},
"required": ["order_id"],
"additionalProperties": False,
},
"strict": True,
},
{
"type": "function",
"name": "get_customer_profile",
"description": "Look up sample customer context for a BrightCart support interaction.",
"parameters": {
"type": "object",
"properties": {"customer_id": {"type": "string", "description": "A customer ID such as CUST-1042."}},
"required": ["customer_id"],
"additionalProperties": False,
},
"strict": True,
},
]
def get_order_status(order_id: str) -> dict[str, Any]:
orders = {
"ORDER-8831": {
"order_id": "ORDER-8831",
"customer_id": "CUST-1042",
"item": "standing desk replacement",
"status": "delayed",
"carrier_scan": "No movement for 36 hours at Denver sort center",
"promised_delivery": (date.today() + timedelta(days=2)).isoformat(),
"recommended_policy": "If delay exceeds 48 hours, offer expedited replacement or 15% concession with agent approval.",
},
"ORDER-2044": {
"order_id": "ORDER-2044",
"customer_id": "CUST-1042",
"item": "ergonomic chair",
"status": "delivered",
"carrier_scan": "Delivered yesterday at front desk",
"promised_delivery": (date.today() - timedelta(days=1)).isoformat(),
"recommended_policy": "Confirm delivery details before opening a replacement request.",
},
}
return orders.get(order_id, {"order_id": order_id, "status": "unknown", "customer_id": None})
def get_customer_profile(customer_id: str) -> dict[str, Any]:
profiles = {
"CUST-1042": {
"customer_id": "CUST-1042",
"name": "Maya Chen",
"loyalty_tier": "Gold",
"region": "California",
"recent_issue": "Damaged standing desk replacement",
"contact_preference": "email with SMS updates for shipping changes",
}
}
return profiles.get(customer_id, {"customer_id": customer_id, "loyalty_tier": "unknown"})
def dispatch_tool_call(call: dict[str, Any]) -> dict[str, Any]:
name = call["name"]
args = json.loads(call["arguments"])
if name == "get_order_status":
output = get_order_status(**args)
elif name == "get_customer_profile":
output = get_customer_profile(**args)
else:
raise ValueError(f"Unsupported tool: {name}")
return {"type": "function_call_output", "call_id": call["call_id"], "output": json.dumps(output)}
print("Sample function tools:")
print_json([tool["name"] for tool in function_tools])
print("\nSample order lookup:")
print_json(get_order_status("ORDER-8831"))Sample function tools:
Sample order lookup:4.2 Call a Function Tool
This cell runs the basic function-calling loop. The first request gives the model an order-status tool and asks it to choose arguments. The application parses the returned function_call, runs the local Python function, sends a function_call_output item back, and asks for the final grounded answer. Inspect the tool arguments, local tool output, final model text, and response metadata.
from __future__ import annotations
function_input = [{"role": "user", "content": "Use get_order_status for ORDER-8831, then explain the next best action for the support agent in two labeled plain-text lines. Do not use leading hyphens or bold text."}]
order_status_tool = [tool for tool in function_tools if tool["name"] == "get_order_status"]
function_request = {
"model": MODEL_ID,
"input": function_input,
"tools": order_status_tool,
"tool_choice": "required",
"max_output_tokens": 1024,
"store": False,
}
def create_tool_plan_with_auto_fallback(request: dict[str, Any]) -> tuple[Any, str]:
try:
return create_response(**request), builtins.str(request.get("tool_choice"))
except Exception as first_exc:
fallback_request = {**request, "tool_choice": "auto"}
try:
return create_response(**fallback_request), "auto"
except Exception:
raise first_exc
print_request_shape(function_request)
try:
function_plan, tool_choice_used = create_tool_plan_with_auto_fallback(function_request)
function_calls = [item for item in response_items(function_plan) if item.get("type") == "function_call"]
if function_calls:
function_call = function_calls[0]
function_args = json.loads(function_call["arguments"])
require(function_args.get("order_id") == "ORDER-8831", f"Unexpected function arguments: {function_args}")
tool_output = dispatch_tool_call(function_call)
final_response = create_response(
model=MODEL_ID,
input=function_input + response_items(function_plan) + [tool_output],
tools=order_status_tool,
max_output_tokens=1024,
store=False,
)
final_answer = output_text(final_response).strip()
tool_output_payload = json.loads(tool_output["output"])
record_check("Function calling", "pass", {"tool_choice_used": tool_choice_used, "arguments": function_args})
record_response("Order-status tool answer", "text", final_answer)
print_labeled_json("Result: tool arguments", function_args)
print_labeled_json("Result: tool output", tool_output_payload)
print_labeled_text("Result: final model answer", final_answer)
print_response_summary(final_response)
print_key_takeaway('Function calling separates model-selected arguments from application-executed business logic.')
else:
fallback_order = get_order_status("ORDER-8831")
fallback_prompt = (
"The model response did not include a function_call item. Use this application lookup result "
"to answer in two labeled plain-text lines without leading hyphens or bold text: " + json.dumps(fallback_order)
)
final_response = create_response(
model=MODEL_ID,
input=function_input + [{"role": "user", "content": fallback_prompt}],
max_output_tokens=1024,
store=False,
)
final_answer = output_text(final_response).strip()
returned_item_types = [item.get("type") for item in response_items(function_plan)]
record_check("Function calling", "warn", {"tool_choice_used": tool_choice_used, "returned_item_types": returned_item_types})
record_response("Order-status local fallback answer", "text", final_answer)
print_labeled_json("Result: returned output item types", returned_item_types)
print_labeled_json("Result: local tool output", fallback_order)
print_labeled_text("Result: final model answer", final_answer)
print_response_summary(final_response)
print_key_takeaway('The local lookup keeps the function-calling pattern understandable even when the model returns text.')
except Exception as exc:
handle_example_error("Function calling", exc)| field | value |
|---|---|
| model | openai.gpt-5.4 |
| max_output_tokens | 1024 |
| store | False |
| tools | get_order_status |
| tool_choice | required |
| input | 1 item(s): user: Use get_order_status for ORDER-8831, then explain the next best action for the support agent in two labeled plain-text lines. Do not use leading hyphens or bold text. |
| field | value |
|---|---|
| id | resp_nqjijq6jvanvxdglnj4iuezrkdm2eoizutfiezzxbmm42xtynw6a |
| model | openai.gpt-5.4 |
| status | completed |
| output_item_types | ['message'] |
| input_tokens | 674 |
| cached_input_tokens | 0 |
| output_tokens | 75 |
| total_tokens | 749 |
| reasoning_output_tokens | 0 |
| service_tier | default |
4.3 Handle Multiple Tool Calls
Parallel tool calls let the model request more than one independent lookup from a single turn. This cell allows two order-status lookups, executes each local function call, and sends both outputs back before asking the model to compare the active shipping issues. Inspect the returned order IDs and final answer to confirm that application data, not model memory, grounds the response.
from __future__ import annotations
parallel_input = [{"role": "user", "content": "Use get_order_status for ORDER-8831 and ORDER-2044, then summarize whether Maya has one shipping problem or multiple active shipping problems in two labeled plain-text lines. Do not use leading hyphens or bold text."}]
parallel_request = {
"model": MODEL_ID,
"input": parallel_input,
"tools": function_tools,
"tool_choice": "auto",
"parallel_tool_calls": True,
"max_output_tokens": 1024,
"store": False,
}
print_request_shape(parallel_request)
try:
parallel_plan = create_response(**parallel_request)
parallel_calls = [item for item in response_items(parallel_plan) if item.get("type") == "function_call"]
parallel_outputs = [dispatch_tool_call(call) for call in parallel_calls]
parallel_order_ids = [json.loads(call["arguments"]).get("order_id") for call in parallel_calls if call.get("name") == "get_order_status"]
expected_order_ids = ["ORDER-8831", "ORDER-2044"]
missing_order_ids = [order_id for order_id in expected_order_ids if order_id not in builtins.set(parallel_order_ids)]
if not missing_order_ids:
parallel_final = create_response(
model=MODEL_ID,
input=parallel_input + response_items(parallel_plan) + parallel_outputs,
tools=function_tools,
max_output_tokens=1024,
store=False,
)
parallel_answer = output_text(parallel_final).strip()
record_check("Parallel tool calls", "pass", {"tool_call_count": len(parallel_calls), "order_ids": parallel_order_ids})
record_response("Parallel order lookup answer", "text", parallel_answer)
print_labeled_json("Result: tool calls", {"tool_call_count": len(parallel_calls), "order_ids": parallel_order_ids})
print_labeled_text("Result: final model answer", parallel_answer)
print_response_summary(parallel_final)
print_key_takeaway('Parallel tool calls let the model request multiple lookups, while the application still controls execution.')
else:
fallback_orders = [get_order_status(order_id) for order_id in expected_order_ids]
fallback_prompt = (
"The model did not request every expected order lookup. Use these application lookup results "
"to answer in two labeled plain-text lines without leading hyphens or bold text: " + json.dumps(fallback_orders)
)
parallel_final = create_response(
model=MODEL_ID,
input=parallel_input + [{"role": "user", "content": fallback_prompt}],
max_output_tokens=1024,
store=False,
)
parallel_answer = output_text(parallel_final).strip()
record_check("Parallel tool calls", "warn", {"returned_order_ids": parallel_order_ids, "missing_order_ids": missing_order_ids})
record_response("Parallel order lookup fallback answer", "text", parallel_answer)
print_labeled_json("Result: returned tool calls", {"tool_call_count": len(parallel_calls), "order_ids": parallel_order_ids})
print_labeled_json("Result: local tool outputs", fallback_orders)
print_labeled_text("Result: final model answer", parallel_answer)
print_response_summary(parallel_final)
print_key_takeaway('Local lookup outputs keep the parallel-tool pattern understandable if not every call is returned.')
except Exception as exc:
handle_example_error("Parallel tool calls", exc)| field | value |
|---|---|
| model | openai.gpt-5.4 |
| max_output_tokens | 1024 |
| store | False |
| parallel_tool_calls | True |
| tools | get_order_status, get_customer_profile |
| tool_choice | auto |
| input | 1 item(s): user: Use get_order_status for ORDER-8831 and ORDER-2044, then summarize whether Maya has one shipping problem or multiple active shipping problems in two labeled plain-text lines. Do not use leading hyphens or bold text. |
| field | value |
|---|---|
| id | resp_ovmlijo2lf2nmc7udlofbbw7n2xmlxs6mxdpftlp6iamfcjoeerq |
| model | openai.gpt-5.4 |
| status | completed |
| output_item_types | ['message'] |
| input_tokens | 404 |
| cached_input_tokens | 0 |
| output_tokens | 50 |
| total_tokens | 454 |
| reasoning_output_tokens | 0 |
| service_tier | default |
4.4 Use a Custom Text Tool
Custom tools pass freeform text to application-owned logic instead of requiring a structured JSON argument object. This cell defines a support-note normalizer, requests a custom tool call, and includes a local fallback if the endpoint returns ordinary text instead of a custom call. Inspect the output item types and the normalized note.
from __future__ import annotations
custom_tools = [
{
"type": "custom",
"name": "normalize_support_note",
"description": "Normalize a freeform support note written by an agent. Input is plain text.",
"format": {"type": "text"},
}
]
def normalize_support_note_text(note: str) -> str:
fields = [part.strip().upper() for part in note.split("|")]
labels = ["ORDER_ID", "CUSTOMER_ID", "ISSUE", "CUSTOMER_REQUEST", "POLICY_OPTION"]
return "\n".join(
f"{label}: {value}"
for label, value in zip(labels, fields)
if value
)
support_note = "order-8831 | cust-1042 | replacement delayed | customer wants supervisor | offer expedited replacement or 15% concession"
custom_input = [{
"role": "user",
"content": (
"Call normalize_support_note with this exact note. Do not answer directly; "
f"send the note to the custom tool: {support_note}"
),
}]
custom_request = {
"model": MODEL_ID,
"input": custom_input,
"tools": custom_tools,
"tool_choice": {"type": "custom", "name": "normalize_support_note"},
"max_output_tokens": 1024,
"store": False,
}
print_request_shape(custom_request)
print_labeled_text("Result: local fallback normalization", normalize_support_note_text(support_note))
try:
custom_plan = create_response(**custom_request)
returned_item_types = [item.get("type") for item in response_items(custom_plan)]
try:
custom_call = first_output_item(custom_plan, "custom_tool_call")
if custom_call is None:
raise LookupError("No custom_tool_call item returned.")
tool_input = custom_call.get("input", "").strip()
normalized_note = normalize_support_note_text(tool_input)
record_check("Custom tools", "pass", {"output_item_types": returned_item_types, "normalized_note": normalized_note})
record_response("Normalized support note", "text", normalized_note)
print_labeled_json("Result: returned output item types", returned_item_types)
print_labeled_text("Result: custom tool input", tool_input)
print_labeled_text("Result: application-owned normalized output", normalized_note)
except LookupError:
fallback_text = output_text(custom_plan).strip() or "No text content was returned."
normalized_note = normalize_support_note_text(support_note)
record_check("Custom tools", "warn", {
"expected": "custom_tool_call item named normalize_support_note",
"actual_output_item_types": returned_item_types,
"meaning": "The model response did not include a custom-tool invocation, so the application fallback normalization is shown for teaching.",
})
record_response("Custom tool text fallback", "text", fallback_text)
record_response("Application-owned normalization fallback", "text", normalized_note)
print_labeled_json("Result: returned output item types", returned_item_types or ["no typed output items returned"])
print_labeled_text("Result: model text response", fallback_text)
print_labeled_text("Result: application-owned normalization", normalized_note)
print_response_summary(custom_plan)
print_key_takeaway('Custom tools are useful when the application owns a freeform parsing or execution step.')
except Exception as exc:
handle_example_error("Custom tools", exc)| field | value |
|---|---|
| model | openai.gpt-5.4 |
| max_output_tokens | 1024 |
| store | False |
| tools | normalize_support_note |
| tool_choice | \{'type': 'custom', 'name': 'normalize_support_note'\} |
| input | 1 item(s): user: Call normalize_support_note with this exact note. Do not answer directly; send the note to the custom tool: order-8831 | cust-1042 | replacement delayed | customer wants supervisor | offer expedited replacement or 15% co... |
| field | value |
|---|---|
| id | resp_gwyauif44dnxpxrcssrxj4bh57tmgg3zwr67hfcklfswshbbscoa |
| model | openai.gpt-5.4 |
| status | completed |
| output_item_types | ['custom_tool_call'] |
| input_tokens | 674 |
| cached_input_tokens | 0 |
| output_tokens | 37 |
| total_tokens | 711 |
| reasoning_output_tokens | 0 |
| service_tier | default |