Building the agent
Now let us build the complete OfficeManager class. It combines: memory (self.messages), native function calling (tool schemas), an autonomous loop (while True), and HITL gates (approval for send_email).
Most models handle 10-20 tools well. Beyond that, the LLM starts picking the wrong tool or hallucinating tool names. For our Office Manager, we keep it focused on just two tools. In production, you would use the router pattern from earlier to split tools across specialized agents.
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "send_email",
"description": "Send an email. REQUIRES APPROVAL.",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string"},
"body": {"type": "string"}
},
"required": ["to", "body"]
}
}
},
{
"type": "function",
"function": {
"name": "schedule_meeting",
"description": "Add a meeting to calendar.",
"parameters": {
"type": "object",
"properties": {
"time": {"type": "string"}
},
"required": ["time"]
}
}
}
]Tool schemas for the Office Manager. Note the description hints that send_email requires approval.
Now for the main event: the OfficeManager class ties together everything from this course: memory, tool calling, an autonomous loop, and a human-in-the-loop gate for sensitive actions.
class OfficeManager:
def __init__(self, model: str):
self.model = model
self.messages = [{
"role": "system",
"content": "You are an Office Manager. "
"Help the user manage their work."
}]
self.available_tools = {
"send_email": send_email,
"schedule_meeting": schedule_meeting
}
def run(self, topic: str):
self.messages.append(
{"role": "user", "content": topic}
)
while True:
response = completion(
model=self.model,
messages=self.messages,
tools=TOOL_SCHEMAS
)
message = response.choices[0].message
self.messages.append(message)
if not message.get("tool_calls"):
print(f"Manager: {message.content}")
break
for tool_call in message.get("tool_calls", []):
name = tool_call.function.name
args = json.loads(
tool_call.function.arguments
)
# HITL gate for sensitive tools
if name == "send_email":
print(f"SECURITY: send_email to "
f"{args['to']}")
confirm = input(
"Approve? (yes/no): "
).lower().strip()
if confirm != "yes":
res = "Error: Denied by human."
else:
res = self.available_tools[
name
](**args)
else:
res = self.available_tools[
name
](**args)
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": name,
"content": res
})The complete OfficeManager: memory, tool calling, autonomous loop, and HITL gate for send_email.
Fill in the blanks: Wire up the agent loop with safety
Loading practiceโฆ
Full scenario execution trace
Three steps: (1) Write the Python function. (2) Add its JSON schema to TOOL_SCHEMAS. (3) Add it to self.available_tools dict. If it is sensitive, add an HITL gate in the execution loop. The modular design makes this straightforward.