02 — Hosted Agent with Tools & File Persistence
Add custom tools and per-session file storage to your hosted agent. The agent can now look up patient records, calculate BMI, and save session notes that persist across turns.
What you'll learn
- Define custom tools with the
@tooldecorator. - Pass structured parameters with type annotations and Pydantic
Fielddescriptions. - Use the per-session sandbox filesystem (
$HOME) for file persistence. - Understand session isolation — files written in one session are not visible in another.
How tools work in MAF
When you register tools with an Agent, the framework:
- Extracts the function signature and docstring to build a tool schema.
- Sends the schema to the model alongside the user prompt.
- When the model decides to call a tool, MAF executes the function locally and returns the result.
sequenceDiagram
participant User
participant Agent
participant Model
participant Tool
User->>Agent: "Look up patient P-1001"
Agent->>Model: prompt + tool schemas
Model-->>Agent: tool_call: lookup_patient_record(patient_id="P-1001")
Agent->>Tool: execute function
Tool-->>Agent: JSON result
Agent->>Model: tool result
Model-->>Agent: natural language response
Agent-->>User: "Alice Johnson, age 34, blood type A+..."
Per-session file persistence
Hosted agents map $HOME to a per-session persistent filesystem. Key properties:
| Property | Detail |
|---|---|
| Persistent path | $HOME |
| Scope | Per session — each conversation gets its own filesystem |
| Lifetime | Persists for the lifetime of the session |
| Isolation | Other sessions cannot read or write to this session's files |
| Use cases | Notes, generated reports, intermediate computation results |
Project structure
examples/02-tools-and-files/
├── main.py
├── Dockerfile
├── .dockerignore
├── requirements.txt
└── resources/
└── sample_patient_data.txt
azure.yaml is generated by azd ai agent init
As in lesson 01, the hosting manifest (azure.yaml)
is generated by init and git-ignored — there is no committed agent.yaml.
The code
examples/02-tools-and-files/main.py
"""Lesson 02 — Hosted Agent with Tools & File Persistence."""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Annotated
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
from pydantic import Field
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
load_dotenv()
credential = DefaultAzureCredential()
# The hosted runtime injects FOUNDRY_PROJECT_ENDPOINT; fall back to
# AZURE_AI_PROJECT_ENDPOINT for local .env-based runs.
project_endpoint = os.environ.get("FOUNDRY_PROJECT_ENDPOINT") or os.environ["AZURE_AI_PROJECT_ENDPOINT"]
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=credential,
)
@tool(approval_mode="never_require")
def lookup_patient_record(
patient_id: Annotated[str, Field(description="The patient identifier, e.g., P-1001")],
) -> str:
"""Look up a patient record by ID. Returns basic demographics and vitals."""
records = {
"P-1001": {
"name": "Alice Johnson", "age": 34, "blood_type": "A+",
"last_visit": "2025-06-10", "conditions": ["asthma"],
},
"P-1002": {
"name": "Bob Martinez", "age": 58, "blood_type": "O-",
"last_visit": "2025-07-22", "conditions": ["type 2 diabetes", "hypertension"],
},
"P-1003": {
"name": "Carol Lee", "age": 45, "blood_type": "B+",
"last_visit": "2025-08-05", "conditions": [],
},
}
record = records.get(patient_id)
if record is None:
return f"No patient found with ID {patient_id}."
return json.dumps(record, indent=2)
@tool(approval_mode="never_require")
def calculate_bmi(
weight_kg: Annotated[float, Field(description="Weight in kilograms")],
height_m: Annotated[float, Field(description="Height in metres")],
) -> str:
"""Calculate Body Mass Index (BMI) from weight and height."""
if height_m <= 0:
return "Height must be greater than zero."
bmi = weight_kg / (height_m ** 2)
category = (
"underweight" if bmi < 18.5
else "normal weight" if bmi < 25
else "overweight" if bmi < 30
else "obese"
)
return f"BMI: {bmi:.1f} ({category})"
@tool(approval_mode="never_require")
def save_session_note(
note: Annotated[str, Field(description="The note text to save")],
) -> str:
"""Save a note to the per-session sandbox filesystem."""
notes_dir = Path.home() / "notes"
notes_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = notes_dir / f"note_{timestamp}.txt"
with filepath.open("w") as f:
f.write(note)
return f"Note saved to {filepath}"
@tool(approval_mode="never_require")
def list_session_notes() -> str:
"""List all notes saved in the current session."""
notes_dir = Path.home() / "notes"
if not notes_dir.exists():
return "No notes found."
files = sorted(notes_dir.iterdir())
if not files:
return "No notes found."
results = []
for filepath in files:
with filepath.open("r") as f:
content = f.read()
results.append(f"--- {filepath.name} ---\n{content}")
return "\n\n".join(results)
agent = Agent(
client=client,
instructions=(
"You are a healthcare assistant with access to patient records and health tools. "
"Use the lookup_patient_record tool when asked about a patient. "
"Use calculate_bmi when asked about BMI. "
"Use save_session_note and list_session_notes to manage session notes. "
"Always remind the user your answers are informational only."
),
tools=[lookup_patient_record, calculate_bmi, save_session_note, list_session_notes],
default_options={"store": False},
)
server = ResponsesHostServer(agent)
server.run()
Step-by-step walkthrough
1. Define tools with @tool
from agent_framework import tool
@tool(approval_mode="never_require")
def lookup_patient_record(
patient_id: Annotated[str, Field(description="The patient identifier")],
) -> str:
"""Look up a patient record by ID."""
...
@tool(approval_mode="never_require")— the tool runs automatically without asking the user for permission.Annotated[str, Field(description="...")]— Pydantic annotations that become the tool's parameter description for the model.- The docstring becomes the tool's description.
2. Patient record lookup
The lookup_patient_record tool simulates a database query with a dictionary of sample patient records. In production, this would call an API or query a database.
3. BMI calculator
calculate_bmi demonstrates a pure computation tool — no side effects, just math.
4. File persistence tools
@tool(approval_mode="never_require")
def save_session_note(note: Annotated[str, Field(description="The note text to save")]) -> str:
notes_dir = Path.home() / "notes"
notes_dir.mkdir(parents=True, exist_ok=True)
...
Foundry maps $HOME to the per-session sandbox. Files written beneath it:
- Persist across turns within the same session.
- Are isolated from other sessions.
- Are automatically cleaned up when the session ends.
5. Register tools with the agent
agent = Agent(
client=client,
instructions="...",
tools=[lookup_patient_record, calculate_bmi, save_session_note, list_session_notes],
...
)
Pass all tools as a list. The agent's instructions should mention the tools so the model knows when to use them.
Try it
Generate the project manifest with azd ai agent init
Bind to your existing project and ACR (see lesson 01 for the full wizard walkthrough):
cd examples/02-tools-and-files
export BASE_NAME=<your-unique-name>
export RESOURCE_GROUP=rg-foundry-advanced-workshop
PROJECT_ID="$(az cognitiveservices account show \
--name $BASE_NAME --resource-group $RESOURCE_GROUP \
--query id -o tsv)/projects/${BASE_NAME}-project"
azd ai agent init \
--agent-name tools-and-files-agent \
--project-id "$PROJECT_ID" \
--deploy-mode container \
--model-deployment gpt-5-mini
--model-deployment skips the model picker
Passing --model-deployment gpt-5-mini selects your existing deployment without
the interactive model prompt. Answer the remaining prompts as in lesson 01
(use the code in the current directory, protocols → responses, ACR login server).
Run locally
Invoke (in a separate terminal)
Expected:
Patient P-1001 is Alice Johnson, age 34, blood type A+. Her last visit was
on 2025-06-10 and she has asthma listed as a condition.
This information is for reference purposes only.
Expected:
Local file behavior
The tools also work locally, but use your local home directory. Session isolation and persistence across hosted sandbox restarts apply only after deployment to Foundry.
Deploy to the cloud
No manual agent-identity role assignment is needed (see lesson 01). Then invoke remotely:
azd ai agent invoke "Look up patient P-1002 and calculate their BMI if weight is 95kg and height 1.80m"
Test hosted file persistence
Expected (the model confirms the save — wording may vary):
Expected (the model summarizes the saved notes):
Same session required
Notes persist within a session. The second invoke reuses the same session by default. Pass --new-session to start fresh.
Key takeaways
@toolturns any Python function into a tool the agent can call.- Pydantic
Field(description=...)helps the model understand parameters. $HOMEprovides per-session file persistence on hosted agents.- Each session has its own isolated sandbox filesystem.
- Tools are registered as a list in the
Agentconstructor.