Task 4 — Add custom function tools
Part of the Build and extend AI agents lab. New here? Start with Getting started.
Set up (start here): This task needs a Foundry project and the starter code. If you haven’t already, complete Getting started to create your project, clone the code, and set
PROJECT_ENDPOINTandMODEL_DEPLOYMENT_NAMEinPython/.env. The helper filefunctions.pyis already in the starter folder. Then verify you’re ready:
python setup/check_env.py --task 4
Continuing from a previous task? If you just finished an earlier task in the same
Pythonfolder, your project, virtual environment, and.envare already set — go straight to reviewing functions.py in Set up below.
Goal: Give an agent tools backed by your own Python functions, and process the function calls it makes.
Concept reinforced: the function-calling loop — the agent decides which tool to call and with what arguments; your code executes it and returns the result.
Set up:
- In the
Labfiles/A-build-and-extend-ai-agents/Pythonfolder, activate the virtual environment (.\labenv\Scripts\Activate.ps1) and confirmPROJECT_ENDPOINTandMODEL_DEPLOYMENT_NAMEare set in .env (see Getting started). Then review functions.py, which contains the trip planner’s helper functions.
Try it first: Look at
next_available_trip(region)in functions.py. How would you describe its singleregionparameter to the model so it knows when and how to call it? Write the JSON schema before revealing the solution.
Show a solution
Work through the comments in functions_agent.py. Add references and connect to the project (the
same pattern as Task 2). The file is structured so your agent setup runs once, then a
respond() function handles each chat message and hands the reply to run_chat_app():
-
Define the three function tools. Each schema tells the model how to call one of the Python functions — for example the trip lookup tool:
# Define the trip lookup function tool trip_tool = FunctionTool( name="next_available_trip", description="Get the next available guided trip in a given region.", parameters={ "type": "object", "properties": { "region": { "type": "string", "description": "region to find the next guided trip in (e.g. 'pacific_northwest', 'rockies', 'patagonia')", }, }, "required": ["region"], "additionalProperties": False, }, strict=True, )Define
cost_tool(calculate_rental_cost) andreport_tool(generate_booking_report) the same way, matching each function’s parameters. -
Create the agent with all three tools:
agent = project_client.agents.create_version( agent_name="trip-planner-agent", definition=PromptAgentDefinition( model=model_deployment, instructions="""You are a trip planning assistant for Tailwind Traders that helps customers find guided trips and calculate gear rental costs. Use the available tools to assist users with their inquiries.""", tools=[trip_tool, cost_tool, report_tool], ), ) -
Fill in the tool-calling loop inside
respond()— read eachfunction_callfrom the response, run the matching Python function, and collect aFunctionCallOutput:# Process function calls for item in response.output: if item.type == "function_call": result = None if item.name == "next_available_trip": result = next_available_trip(**json.loads(item.arguments)) elif item.name == "calculate_rental_cost": result = calculate_rental_cost(**json.loads(item.arguments)) elif item.name == "generate_booking_report": result = generate_booking_report(**json.loads(item.arguments)) input_list.append( FunctionCallOutput( type="function_call_output", call_id=item.call_id, output=result, ) )The rest of
respond()(already provided) sends the outputs back and returns the final answer to the chat window. Note that it attaches the outputs to the same conversation so the tool calls are resolved in conversation state — sending them back withprevious_response_idinstead would make the next message fail with “No tool output found for function call”:# Send function call outputs back to the model and retrieve a response if input_list: response = openai_client.responses.create( conversation=conversation.id, input=input_list, extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, ) return AgentReply(text=response.output_text)
Run python functions_agent.py. Your browser opens the chat window — try a prompt that needs two
tools at once:
Find me the next trip I can join in Patagonia and give me the cost for 5 days of premium gear rental at priority service.
The agent calls both functions in one turn and combines the results, for example:
The next trip available in Patagonia is the Patagonia Glacier Trek on February 17th.
The cost for 5 days of premium gear rental at priority service is $1,875.
Close the browser tab and press Ctrl+C in the terminal to stop the app (the agent is deleted automatically on exit).
Stretch: add a fourth function tool of your own and update the instructions to mention it.
Compare: the same agent with the Microsoft Agent Framework
You just wrote two schemas per tool and a dispatch loop that matches each function_call to a
Python function. The Microsoft Agent Framework removes both. Open functions_agent_maf.py
(provided complete) and run it with python functions_agent_maf.py — it produces the same
trip-planner assistant.
The difference is the tool definition and the loop. Instead of a hand-written FunctionTool
schema, you decorate the function with @tool and describe each parameter inline:
from agent_framework import tool, Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from pydantic import Field
from typing import Annotated
@tool(approval_mode="never_require")
def next_available_trip(
region: Annotated[str, Field(description="Region to find the next guided trip in (e.g. 'pacific_northwest', 'rockies', 'patagonia')")],
) -> str:
"""Get the next available guided trip in a given region."""
return functions.next_available_trip(region)
Then you create the agent with the decorated functions and let agent.run() handle the whole
tool-calling loop — no reading response.output, no matching names, no sending outputs back:
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.getenv("PROJECT_ENDPOINT"),
model=os.getenv("MODEL_DEPLOYMENT_NAME"),
credential=AzureCliCredential(),
),
name="trip-planner-agent",
instructions="You are a trip planning assistant for Tailwind Traders...",
tools=[next_available_trip, calculate_rental_cost, generate_booking_report],
)
# agent.run() decides which tools to call, runs them, and returns the final answer
result = await agent.run(user_message, session=session)
Same result, far less code — because the framework does the plumbing you wrote by hand above.
Writing it yourself first is what makes it clear what agent.run() is doing for you.