Multi-Agent IT Helpdesk Triage System
Build an autonomous IT helpdesk that routes support tickets to specialist AI agents.
Introduction
30 Second Summary
Every IT helpdesk gets flooded with support tickets. Password resets go to the network team. Software crashes get routed to the wrong specialist. Misrouted tickets waste hours of human time and leave employees stuck waiting.
In this project, you will build an autonomous IT helpdesk triage system powered by multiple AI agents that collaborate to classify and route support requests to the correct specialist.
What You'll Build
A terminal-based multi-agent system where a Triage Agent classifies incoming IT support requests and hands them off to specialist agents, each equipped with function tools to take real actions like resetting passwords and running network diagnostics.
By the end of this project, you'll have:
- A Triage Agent that classifies and routes support tickets to the correct specialist using the handoff pattern.
- Three specialist agents with domain-specific function tools that simulate real IT operations like password resets, network diagnostics, and service status checks.
- An input guardrail that blocks non-IT-related queries before they reach your expensive specialist agents.
- Secret Mission: Add a human-in-the-loop escalation path where critical issues require manual approval before action is taken.
Are there any prerequisites?
You should be comfortable with Python basics (functions, classes, async/await) and have some familiarity with calling LLM APIs. No prior experience with multi-agent systems is needed.
Before We Start
Before We Start...
Set Up Your Project Environment
Before you can build a multi-agent system, you need a clean workspace with the right packages installed. The OpenAI Agents SDK provides the orchestration framework that lets multiple AI agents collaborate and hand off tasks to each other.
In this step, you'll create an isolated Python environment, install the SDK and its dependencies, and configure your API credentials.
In this step, get ready to:
- Create a project directory and Python virtual environment.
- Install the OpenAI Agents SDK and python-dotenv.
- Configure your OpenRouter API key in a .env file.
Create your project directory and virtual environment
A virtual environment gives your project its own isolated set of packages. This prevents version conflicts between different Python projects on your machine.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir it-helpdesk-agents
cd it-helpdesk-agents
- Confirm the folder exists by running:
ls
You should see an empty directory with no output listed.
- Create and activate your virtual environment by running the commands for your operating system:
macOS
python3 -m venv venv
source venv/bin/activate
You should see (venv) appear at the start of your terminal prompt.
Windows
python -m venv venv
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt.
What is a virtual environment?
A virtual environment is a self-contained Python installation inside your project folder. Packages you install here stay isolated from your system Python, so different projects can use different package versions without conflicts.
Don't see (venv) in your prompt?
Make sure you ran the activate command for your operating system. On macOS, the command starts with source. On Windows, the path uses backslashes.
If Windows blocks the script with an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell first, then try activating again.
help me activate my Python virtual environment
Install the required packages
The OpenAI Agents SDK is the framework that handles agent creation, handoffs, and tool calling. The python-dotenv package loads your API key from a file so you never hardcode secrets.
- Install both packages by running this command:
pip install openai-agents python-dotenv
The installation takes about 30 seconds. You should see output ending with a line like Successfully installed followed by a list of package names.
What did this install?
The openai-agents package (version 0.17.4) is the OpenAI Agents SDK. It automatically pulls in the openai Python client as a dependency.
The python-dotenv package reads key-value pairs from a .env file and loads them as environment variables so your code can access them securely.
- Verify the SDK installed correctly by running:
python -c "from agents import Agent; print('SDK installed successfully')"
You should see SDK installed successfully printed in your terminal.
Seeing a ModuleNotFoundError?
Make sure (venv) is visible in your prompt. If it disappeared, re-run the activate command from the previous substep.
If you see a network error during install, check your internet connection and run the pip install command again.
help me fix a ModuleNotFoundError after installing openai-agents
Create your environment file
Your OpenRouter API key needs to live in a .env file so your code can load it at runtime without exposing it in your source code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type the name of your code editor (e.g. Cursor or VS Code) and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the it-helpdesk-agents folder, then click Open.
- Create a new file by clicking File then New File.
- Save the file immediately as .env by pressing Cmd+S (macOS) or Ctrl+S (Windows) and typing .env as the filename.
- Add your OpenRouter API key to the .env file by pasting this line:
OPENROUTER_API_KEY=your-openrouter-api-key-here
- Replace your-openrouter-api-key-here with your actual OpenRouter API key (it starts with sk-or-v1-).
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
Why a .env file?
Storing API keys in a .env file keeps them out of your source code. If you ever push this project to GitHub, you can add .env to your .gitignore file so the key never gets committed.
- Verify your .env file loads correctly by switching back to your terminal and running:
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print('Key loaded:', 'OPENROUTER_API_KEY' in os.environ)"
You should see Key loaded: True printed in your terminal.
Seeing Key loaded: False?
Make sure the .env file is saved inside your it-helpdesk-agents folder (the same directory where your terminal is running).
Check that the file is named exactly .env (with the leading dot) and not env.txt or .env.txt.
help me debug why my .env file isn't loading
✔️ Awesome, I've got everything!
Great. Double check you've saved your .env file and you're ready to move on.
ⓧ I'd like to double check the full code
Here is the complete .env file for comparison:
OPENROUTER_API_KEY=your-openrouter-api-key-here
Your environment is ready. Next up, you'll configure the OpenAI Agents SDK to use OpenRouter as its model provider and test your first agent connection.
Configure OpenRouter as the Model Provider
Your project environment is ready with the OpenAI Agents SDK installed and your API key safely stored. Now it's time to wire the SDK up to a model so your agents can actually think.
The SDK defaults to OpenAI's own Responses API. Since you're using OpenRouter (which gives you access to hundreds of models from different providers), you need to point the SDK at OpenRouter's endpoint and switch it to Chat Completions mode.
In this step, get ready to:
- Create main.py with the OpenRouter client configuration.
- Configure the SDK to use Chat Completions API mode.
- Verify the connection works by running a test agent.
Create main.py and connect to OpenRouter
Every agent in your system will share the same model provider connection. You'll set this up once at the top of main.py and all agents will inherit it.
- Create a new file called main.py in your project folder.
- Add the imports and environment variable setup by pasting this code:
import asyncio
import os
from dotenv import load_dotenv
from agents import (
Agent,
OpenAIChatCompletionsModel,
Runner,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
# --- Load environment variables ---
load_dotenv()
What does this code do?
- asyncio and os are Python standard library modules for running async code and reading environment variables.
- load_dotenv() reads your .env file and loads the OPENROUTER_API_KEY into your environment.
- The agents import pulls in the SDK's configuration functions and core classes you'll use throughout the project.
- AsyncOpenAI is the async HTTP client that talks to OpenRouter's API endpoint.
You won't see any output yet. The next chunk adds the configuration that actually connects to OpenRouter.
Configure SDK settings and select a model
The SDK needs three configuration calls to work with a non-OpenAI provider. Each one addresses a specific requirement.
- Add the OpenRouter configuration below load_dotenv() in main.py:
# --- OpenRouter Configuration ---
client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
set_default_openai_client(client, use_for_tracing=False)
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
MODEL = "google/gemini-2.5-flash"
What does this code do?
- AsyncOpenAI(base_url=..., api_key=...) creates an HTTP client pointed at OpenRouter's endpoint instead of OpenAI's.
- set_default_openai_client(client, use_for_tracing=False) tells the SDK to use your OpenRouter client for all LLM requests. The use_for_tracing=False flag prevents the SDK from trying to upload traces to OpenAI (which would fail without an OpenAI key).
- set_default_openai_api("chat_completions") switches the SDK from the default Responses API to the Chat Completions API. OpenRouter only supports Chat Completions.
- set_tracing_disabled(True) fully disables tracing since you don't have an OpenAI platform key.
- MODEL stores the model identifier. Gemini 2.5 Flash is fast, affordable, and excellent at tool calling.
Why Chat Completions mode?
The OpenAI Agents SDK defaults to OpenAI's Responses API, which is a newer API surface that most third-party providers don't support yet. OpenRouter exposes an OpenAI-compatible Chat Completions endpoint, so you switch the SDK to use that instead.
This single setting is what makes the SDK provider-agnostic. You get all the SDK's orchestration features (handoffs, tools, guardrails) while routing through any model available on OpenRouter.
- Save main.py.
You won't see output yet. The next substep tests the connection.
Test the connection with a minimal agent
Before building the full helpdesk system, you should confirm that the SDK can reach OpenRouter and get a response. You'll add a temporary test at the bottom of the file, run it, then remove it.
- Add this test code at the bottom of main.py (below the MODEL line):
# --- Quick connection test (remove after verifying) ---
async def main():
agent = Agent(
name="Test Agent",
instructions="You are a helpful assistant. Reply in one sentence.",
model=MODEL,
)
result = await Runner.run(agent, "Say hello!")
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())
What does this test do?
This creates a minimal agent with simple instructions, sends it a prompt, and prints the response. If OpenRouter is configured correctly, you'll see a greeting printed in your terminal.
- Save main.py.
- Run the test by executing this command in your terminal:
python main.py
- You should see a short greeting printed in your terminal (something like "Hello! How can I help you today?").
Seeing an error?
- If you see AuthenticationError or a 401 status, double-check your .env file has OPENROUTER_API_KEY set correctly with no extra spaces.
- If you see a 404 error, make sure set_default_openai_api("chat_completions") is in your code. Without it, the SDK tries the Responses API which OpenRouter doesn't support.
- If you see ModuleNotFoundError, make sure your virtual environment is activated.
help me troubleshoot my OpenRouter connection error
- Once you see a successful response, delete the test code you just added (everything from the # --- Quick connection test comment to the end of the file).
- Save main.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and that the test code has been removed.
ⓧ I'd like to double check the full code
Here's what main.py should look like at the end of this step (with the test code removed):
import asyncio
import os
from dotenv import load_dotenv
from agents import (
Agent,
OpenAIChatCompletionsModel,
Runner,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
# --- Load environment variables ---
load_dotenv()
# --- OpenRouter Configuration ---
client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
set_default_openai_client(client, use_for_tracing=False)
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
MODEL = "google/gemini-2.5-flash"
Your SDK is connected and talking to OpenRouter. Next up, you'll build the function tools that give your specialist agents the ability to take real actions like resetting passwords and running diagnostics.
Build Function Tools for IT Operations
Your agents need the ability to take real actions. Without tools, an agent can only generate text responses. With tools, it can check account statuses, reset passwords, run diagnostics, and create tickets.
In this step, you'll build six function tools that simulate real IT helpdesk operations. The OpenAI Agents SDK uses a @function_tool decorator to automatically convert Python functions into tools your agents can call.
In this step, get ready to:
- Update your imports for the full project.
- Create six function tools that simulate IT operations.
- Verify the file runs without errors.
Update your imports
Before writing the tools, you need to import function_tool from the SDK. You'll also add imports for guardrails and structured outputs that later steps will use.
- In main.py, find the import section at the top of the file and replace it entirely with this updated version:
import asyncio
import os
from dotenv import load_dotenv
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
OpenAIChatCompletionsModel,
RunContextWrapper,
Runner,
TResponseInputItem,
function_tool,
input_guardrail,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
What does this import block do?
The key new import is function_tool. This is the decorator you'll use to turn plain Python functions into tools that agents can call.
The other new imports (GuardrailFunctionOutput, InputGuardrailTripwireTriggered, RunContextWrapper, TResponseInputItem, input_guardrail, and BaseModel from pydantic) will be used in later steps for guardrails and structured outputs. Adding them all now keeps the import block clean and avoids revisiting it.
- Save main.py.
Create account and ticket management tools
The first three tools handle account lookups, password resets, and ticket status checks. These are the bread and butter of any IT helpdesk.
- In main.py, find the line that says MODEL = "google/gemini-2.5-flash". Below it, add the following code:
# --- Function Tools ---
@function_tool
def check_ticket_status(ticket_id: str) -> str:
"""Look up the current status of a support ticket by its ID."""
tickets = {
"TKT-001": "Open - Assigned to Network Team",
"TKT-002": "In Progress - Password reset pending",
"TKT-003": "Resolved - Software reinstalled",
}
return tickets.get(ticket_id, f"Ticket {ticket_id} not found in the system.")
@function_tool
def reset_password(username: str) -> str:
"""Reset the password for a user account and send a temporary password."""
return (
f"Password reset initiated for '{username}'. "
f"A temporary password has been sent to their registered email. "
f"Temporary password: TempPass_{username[:3].upper()}2024"
)
@function_tool
def check_account_status(username: str) -> str:
"""Check if a user account is locked, active, or disabled."""
return (
f"Account '{username}' status: LOCKED. "
f"Reason: 5 failed login attempts. "
f"Last attempt: 10 minutes ago."
)
What does this code do?
- The @function_tool decorator converts each function into an agent-callable tool. The SDK reads the function's name, type hints, and docstring to automatically generate the JSON schema the LLM needs.
- The docstring becomes the tool's description. The LLM reads this to decide when to call the tool.
- Each function returns mock data that simulates a real IT system response. In production, these would call actual APIs or databases.
- The check_ticket_status tool uses a dictionary to simulate a ticket database. The reset_password and check_account_status tools generate dynamic responses using the username parameter.
Create infrastructure and escalation tools
The remaining three tools handle network diagnostics, service status checks, and escalation ticket creation. These give your infrastructure-focused agent the ability to investigate connectivity issues and escalate problems.
- Below the check_account_status function you just added, paste the following code:
@function_tool
def run_network_diagnostic(target: str) -> str:
"""Run a network diagnostic (ping, traceroute) against a target host or IP."""
return (
f"Network diagnostic for '{target}':\n"
f"- Ping: 45ms average (normal)\n"
f"- Packet loss: 0%\n"
f"- DNS resolution: OK\n"
f"- Traceroute: 12 hops, no timeouts\n"
f"- Status: Connection appears healthy from server side. "
f"Issue may be client-side."
)
@function_tool
def check_system_status(service_name: str) -> str:
"""Check the operational status of an internal service or application."""
services = {
"email": "DEGRADED - High latency detected since 09:00 AM",
"vpn": "OPERATIONAL - All endpoints healthy",
"crm": "DOWN - Scheduled maintenance until 2:00 PM",
"erp": "OPERATIONAL - Running normally",
}
return services.get(
service_name.lower(),
f"Service '{service_name}' not found. Available: email, vpn, crm, erp",
)
@function_tool
def create_escalation_ticket(summary: str, priority: str, assigned_team: str) -> str:
"""Create a new escalation ticket for issues requiring human intervention."""
ticket_id = f"ESC-{abs(hash(summary)) % 10000:04d}"
return (
f"Escalation ticket created:\n"
f"- ID: {ticket_id}\n"
f"- Priority: {priority}\n"
f"- Assigned to: {assigned_team}\n"
f"- Summary: {summary}\n"
f"- Status: OPEN - Awaiting human review"
)
What does this code do?
- The run_network_diagnostic tool returns a multi-line diagnostic report. When the agent calls it, the LLM sees realistic network data it can interpret for the user.
- The check_system_status tool simulates a service monitoring system with four internal services at different health levels.
- The create_escalation_ticket tool takes three parameters (summary, priority, assigned_team). The SDK auto-generates a JSON schema from these type hints so the LLM knows exactly what arguments to provide.
- Save main.py.
- Verify the file has no syntax errors by running this command in your terminal:
python main.py
The script should exit silently with no output. If you see an error, check for missing commas, unmatched parentheses, or indentation issues.
Seeing an import error?
- If you see ModuleNotFoundError: No module named 'agents', make sure your virtual environment is activated. Run source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows) and try again.
- If you see ImportError for a specific name like GuardrailFunctionOutput, double-check that your import block matches the code above exactly. Every name must be on its own line inside the parentheses.
Still stuck? Why is my main.py throwing an import error after updating the imports?
Why use mock data instead of real APIs?
Mock tools let you test the entire multi-agent routing flow without needing access to Active Directory, a ticketing system, or network infrastructure. The agent's decision-making (which tool to call, what arguments to pass) is identical whether the tool returns real or simulated data.
In a production system, you would replace the return statements with actual API calls. The @function_tool interface stays the same.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and that running it produces no output or errors.
ⓧ I'd like to double check the full code
Here is the complete main.py at this point in the project:
import asyncio
import os
from dotenv import load_dotenv
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
OpenAIChatCompletionsModel,
RunContextWrapper,
Runner,
TResponseInputItem,
function_tool,
input_guardrail,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
# --- Load environment variables ---
load_dotenv()
# --- OpenRouter Configuration ---
client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
set_default_openai_client(client, use_for_tracing=False)
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
MODEL = "google/gemini-2.5-flash"
# --- Function Tools ---
@function_tool
def check_ticket_status(ticket_id: str) -> str:
"""Look up the current status of a support ticket by its ID."""
tickets = {
"TKT-001": "Open - Assigned to Network Team",
"TKT-002": "In Progress - Password reset pending",
"TKT-003": "Resolved - Software reinstalled",
}
return tickets.get(ticket_id, f"Ticket {ticket_id} not found in the system.")
@function_tool
def reset_password(username: str) -> str:
"""Reset the password for a user account and send a temporary password."""
return (
f"Password reset initiated for '{username}'. "
f"A temporary password has been sent to their registered email. "
f"Temporary password: TempPass_{username[:3].upper()}2024"
)
@function_tool
def check_account_status(username: str) -> str:
"""Check if a user account is locked, active, or disabled."""
return (
f"Account '{username}' status: LOCKED. "
f"Reason: 5 failed login attempts. "
f"Last attempt: 10 minutes ago."
)
@function_tool
def run_network_diagnostic(target: str) -> str:
"""Run a network diagnostic (ping, traceroute) against a target host or IP."""
return (
f"Network diagnostic for '{target}':\n"
f"- Ping: 45ms average (normal)\n"
f"- Packet loss: 0%\n"
f"- DNS resolution: OK\n"
f"- Traceroute: 12 hops, no timeouts\n"
f"- Status: Connection appears healthy from server side. "
f"Issue may be client-side."
)
@function_tool
def check_system_status(service_name: str) -> str:
"""Check the operational status of an internal service or application."""
services = {
"email": "DEGRADED - High latency detected since 09:00 AM",
"vpn": "OPERATIONAL - All endpoints healthy",
"crm": "DOWN - Scheduled maintenance until 2:00 PM",
"erp": "OPERATIONAL - Running normally",
}
return services.get(
service_name.lower(),
f"Service '{service_name}' not found. Available: email, vpn, crm, erp",
)
@function_tool
def create_escalation_ticket(summary: str, priority: str, assigned_team: str) -> str:
"""Create a new escalation ticket for issues requiring human intervention."""
ticket_id = f"ESC-{abs(hash(summary)) % 10000:04d}"
return (
f"Escalation ticket created:\n"
f"- ID: {ticket_id}\n"
f"- Priority: {priority}\n"
f"- Assigned to: {assigned_team}\n"
f"- Summary: {summary}\n"
f"- Status: OPEN - Awaiting human review"
)
Your toolkit is ready. Next up, you'll create three specialist agents and assign each one a curated set of these tools.
Create the Specialist Agents
Your function tools are ready to go, but tools alone can't help anyone. They need an agent to wield them.
In this step, you'll create three specialist agents, each focused on a specific domain of IT support. Each agent gets its own curated set of tools and a system prompt that defines its expertise.
In this step, get ready to:
- Create a Password & Access Agent with account and credential tools.
- Create a Software Support Agent with system status and escalation tools.
- Create a Network & Hardware Agent with diagnostic and monitoring tools.
Create the Password & Access Agent
The first specialist handles the most common IT request: password and account issues. It needs tools to check whether an account is locked and to perform resets.
- In main.py, scroll to the bottom of the file (after the last function tool).
- Add the following code to define the Password & Access Agent:
# --- Specialist Agents ---
password_agent = Agent(
name="Password & Access Agent",
handoff_description="Specialist for password resets, account lockouts, and access permission issues",
instructions=(
"You are an IT support specialist for password and access issues. "
"You help users with: password resets, account lockouts, access permission "
"requests, and multi-factor authentication issues. "
"Always check the account status before performing a reset. "
"Be professional and security-conscious."
),
model=MODEL,
tools=[reset_password, check_account_status, check_ticket_status],
)
What does this code do?
This creates your first specialist agent using the Agent class from the OpenAI Agents SDK.
- name is a human-readable identifier for the agent.
- handoff_description tells the triage agent when to route requests here. The triage agent reads this description to decide which specialist fits the user's problem.
- instructions is the system prompt that defines the agent's personality and expertise.
- model=MODEL points to the google/gemini-2.5-flash model you configured earlier via OpenRouter.
- tools is the list of function tools this agent can call. It gets reset_password, check_account_status, and check_ticket_status.
You won't see a change yet. The next two agents follow the same pattern.
Create the Software Support Agent
The second specialist handles application crashes, installation problems, and license issues. It can check service status for known outages and create escalation tickets when it cannot resolve an issue.
- Directly below the password_agent definition, add the Software Support Agent:
software_agent = Agent(
name="Software Support Agent",
handoff_description="Specialist for software installation, crashes, errors, and application troubleshooting",
instructions=(
"You are an IT support specialist for software issues. "
"You help users with: application crashes and errors, software installation "
"problems, license and activation issues, and performance problems. "
"Check system status for known outages before troubleshooting. "
"Provide clear step-by-step guidance."
),
model=MODEL,
tools=[check_system_status, check_ticket_status, create_escalation_ticket],
)
What does this code do?
This agent follows the same structure as the password agent but targets a different domain.
- Its handoff_description mentions software, crashes, and errors so the triage agent can distinguish it from the password specialist.
- Its instructions tell it to check for known outages before troubleshooting, saving the user from debugging something that's already down.
- Its tools include create_escalation_ticket so it can escalate issues it cannot resolve on its own.
You still won't see a change yet. One more agent to go.
Create the Network & Hardware Agent
The final specialist handles connectivity problems, VPN issues, and hardware failures. It has the most tools because network troubleshooting often requires multiple diagnostic steps.
- Directly below the software_agent definition, add the Network & Hardware Agent:
network_agent = Agent(
name="Network & Hardware Agent",
handoff_description="Specialist for network connectivity, VPN, Wi-Fi, printer, and hardware issues",
instructions=(
"You are an IT support specialist for network and hardware issues. "
"You help users with: network connectivity problems, VPN connection issues, "
"Wi-Fi troubleshooting, printer and peripheral problems, and hardware failures. "
"Run diagnostics before suggesting solutions. "
"Guide users through basic troubleshooting before escalating."
),
model=MODEL,
tools=[
run_network_diagnostic,
check_system_status,
check_ticket_status,
create_escalation_ticket,
],
)
What does this code do?
This agent is the broadest specialist, covering network and hardware domains.
- Its instructions tell it to run diagnostics before suggesting solutions. This prevents it from jumping to conclusions.
- It has four tools: run_network_diagnostic for connectivity checks, check_system_status for service health, check_ticket_status for existing tickets, and create_escalation_ticket for issues beyond its scope.
- Save main.py.
- Confirm there are no syntax errors by running this command:
python main.py
The script should exit silently with no output. No errors means your agents are defined correctly.
Seeing a syntax error?
- Check that each Agent( call has a matching closing ). A missing parenthesis is the most common cause.
- Make sure the tools list uses the exact function names from the previous step (e.g. reset_password, not resetPassword).
- Confirm each string in instructions uses matching quotes. Mixing single and double quotes across lines causes SyntaxError.
Help me debug a syntax error in my specialist agent definitions.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and the script ran without errors.
ⓧ I'd like to double check the full code
Here is the full main.py at this point:
import asyncio
import os
from dotenv import load_dotenv
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
OpenAIChatCompletionsModel,
RunContextWrapper,
Runner,
TResponseInputItem,
function_tool,
input_guardrail,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
# --- Load environment variables ---
load_dotenv()
# --- OpenRouter Configuration ---
client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
set_default_openai_client(client, use_for_tracing=False)
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
MODEL = "google/gemini-2.5-flash"
# --- Function Tools ---
@function_tool
def check_ticket_status(ticket_id: str) -> str:
"""Look up the current status of a support ticket by its ID."""
tickets = {
"TKT-001": "Open - Assigned to Network Team",
"TKT-002": "In Progress - Password reset pending",
"TKT-003": "Resolved - Software reinstalled",
}
return tickets.get(ticket_id, f"Ticket {ticket_id} not found in the system.")
@function_tool
def reset_password(username: str) -> str:
"""Reset the password for a user account and send a temporary password."""
return (
f"Password reset initiated for '{username}'. "
f"A temporary password has been sent to their registered email. "
f"Temporary password: TempPass_{username[:3].upper()}2024"
)
@function_tool
def check_account_status(username: str) -> str:
"""Check if a user account is locked, active, or disabled."""
return (
f"Account '{username}' status: LOCKED. "
f"Reason: 5 failed login attempts. "
f"Last attempt: 10 minutes ago."
)
@function_tool
def run_network_diagnostic(target: str) -> str:
"""Run a network diagnostic (ping, traceroute) against a target host or IP."""
return (
f"Network diagnostic for '{target}':\n"
f"- Ping: 45ms average (normal)\n"
f"- Packet loss: 0%\n"
f"- DNS resolution: OK\n"
f"- Traceroute: 12 hops, no timeouts\n"
f"- Status: Connection appears healthy from server side. "
f"Issue may be client-side."
)
@function_tool
def check_system_status(service_name: str) -> str:
"""Check the operational status of an internal service or application."""
services = {
"email": "DEGRADED - High latency detected since 09:00 AM",
"vpn": "OPERATIONAL - All endpoints healthy",
"crm": "DOWN - Scheduled maintenance until 2:00 PM",
"erp": "OPERATIONAL - Running normally",
}
return services.get(
service_name.lower(),
f"Service '{service_name}' not found. Available: email, vpn, crm, erp",
)
@function_tool
def create_escalation_ticket(summary: str, priority: str, assigned_team: str) -> str:
"""Create a new escalation ticket for issues requiring human intervention."""
ticket_id = f"ESC-{abs(hash(summary)) % 10000:04d}"
return (
f"Escalation ticket created:\n"
f"- ID: {ticket_id}\n"
f"- Priority: {priority}\n"
f"- Assigned to: {assigned_team}\n"
f"- Summary: {summary}\n"
f"- Status: OPEN - Awaiting human review"
)
# --- Specialist Agents ---
password_agent = Agent(
name="Password & Access Agent",
handoff_description="Specialist for password resets, account lockouts, and access permission issues",
instructions=(
"You are an IT support specialist for password and access issues. "
"You help users with: password resets, account lockouts, access permission "
"requests, and multi-factor authentication issues. "
"Always check the account status before performing a reset. "
"Be professional and security-conscious."
),
model=MODEL,
tools=[reset_password, check_account_status, check_ticket_status],
)
software_agent = Agent(
name="Software Support Agent",
handoff_description="Specialist for software installation, crashes, errors, and application troubleshooting",
instructions=(
"You are an IT support specialist for software issues. "
"You help users with: application crashes and errors, software installation "
"problems, license and activation issues, and performance problems. "
"Check system status for known outages before troubleshooting. "
"Provide clear step-by-step guidance."
),
model=MODEL,
tools=[check_system_status, check_ticket_status, create_escalation_ticket],
)
network_agent = Agent(
name="Network & Hardware Agent",
handoff_description="Specialist for network connectivity, VPN, Wi-Fi, printer, and hardware issues",
instructions=(
"You are an IT support specialist for network and hardware issues. "
"You help users with: network connectivity problems, VPN connection issues, "
"Wi-Fi troubleshooting, printer and peripheral problems, and hardware failures. "
"Run diagnostics before suggesting solutions. "
"Guide users through basic troubleshooting before escalating."
),
model=MODEL,
tools=[
run_network_diagnostic,
check_system_status,
check_ticket_status,
create_escalation_ticket,
],
)
Why use handoff_description?
When you pass an agent as a handoff target (which you'll do in the next step), the SDK exposes handoff_description to the triage agent as a tool description. The triage agent reads these descriptions to decide which specialist matches the user's problem.
A vague description means the triage agent guesses. A specific one means it routes accurately on the first try.
Your three specialists are ready and waiting for assignments. Next up, you'll build the Triage Agent that reads incoming requests and hands them off to the right specialist.
Build the Triage Agent with Handoffs
Your three specialist agents are ready and waiting for work. The missing piece is a front door that decides which specialist should handle each request.
In this step, you'll create the Triage Agent that acts as the first point of contact. It reads incoming support requests, classifies them, and uses handoffs to route each request to the correct specialist automatically.
In this step, get ready to:
- Create the Triage Agent with handoffs to all three specialists.
- Wire up an interactive terminal loop to test the system end-to-end.
Create the Triage Agent with handoffs
A handoff is the mechanism that lets one agent transfer control to another. The Triage Agent doesn't solve problems itself. It classifies the request and passes the conversation to whichever specialist is best suited to handle it.
- In main.py, scroll down to the bottom of the file, below the network_agent definition.
- Add the Triage Agent by pasting this code:
# --- Triage Agent ---
triage_agent = Agent(
name="IT Helpdesk Triage Agent",
instructions=(
"You are the first point of contact for the IT helpdesk. "
"Your job is to understand the user's issue and route them to the "
"correct specialist:\n"
"- Password & Access Agent: For password resets, account lockouts, "
"permission issues\n"
"- Software Support Agent: For application errors, installations, "
"software crashes\n"
"- Network & Hardware Agent: For connectivity, VPN, Wi-Fi, printers, "
"hardware problems\n\n"
"Ask a brief clarifying question if the issue category is ambiguous. "
"Be friendly and professional."
),
model=MODEL,
handoffs=[password_agent, software_agent, network_agent],
)
What does this code do?
The handoffs parameter is where the routing magic happens. By passing a list of agents, you tell the Triage Agent which specialists it can transfer conversations to.
- handoffs=[password_agent, software_agent, network_agent] gives the triage agent three possible routes.
- The SDK reads each specialist's handoff_description (set in the previous step) to decide which one matches the user's request.
- The instructions tell the agent how to classify requests and when to ask clarifying questions.
Why handoffs instead of function calls?
You could build routing with a single agent and if/else logic. Handoffs are better because they give each specialist its own isolated context, instructions, and tools. The specialist never sees tools it doesn't need, which reduces confusion and token waste.
This is the same pattern companies like Klarna and Uber use for autonomous customer support routing in production.
- Save main.py.
You won't see a change yet. The next chunk wires up the interactive loop so you can actually talk to the triage agent.
Wire up the interactive terminal loop
The agents are configured, but you need a way to send messages and see responses. The Runner is the SDK's execution engine that drives the agent loop: it sends your input, handles any handoffs between agents, runs tools, and returns the final result.
- Below the triage_agent definition, add the interactive loop by pasting this code:
# --- Main Interactive Loop ---
async def main():
print("=" * 60)
print(" IT Helpdesk Triage System (Multi-Agent)")
print(" Powered by OpenAI Agents SDK + OpenRouter")
print("=" * 60)
print("\nType your IT support request below. Type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("\nThank you for using IT Helpdesk. Goodbye!")
break
if not user_input:
continue
result = await Runner.run(triage_agent, user_input)
print(f"\n[Handled by: {result.last_agent.name}]")
print(f"Agent: {result.final_output}\n")
if __name__ == "__main__":
asyncio.run(main())
What does this code do?
- Runner.run(triage_agent, user_input) sends the user's message to the Triage Agent. The Runner handles the full execution cycle: classification, handoff to a specialist, tool calls, and final response.
- result.last_agent.name tells you which agent ultimately handled the request. If the triage agent handed off to the Password & Access Agent, you'll see that name printed.
- result.final_output contains the specialist's response text.
- asyncio.run(main()) starts the async event loop. The SDK uses async operations internally, so the entry point must be async.
- Save main.py.
- Run the system by executing this command in your terminal:
python main.py
- You should see the helpdesk banner print, followed by a You: prompt waiting for input.
- Type quit to exit for now. You'll do full scenario testing in the next steps.
Not seeing the banner or getting an error?
- Make sure you're running python main.py from your project directory (where main.py lives).
- Check that your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
- If you see an indentation error, make sure the if __name__ block is at the top level (no indentation), not inside the main() function.
Help me debug why my main.py isn't running
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete main.py at this point:
import asyncio
import os
from dotenv import load_dotenv
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
OpenAIChatCompletionsModel,
RunContextWrapper,
Runner,
TResponseInputItem,
function_tool,
input_guardrail,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
# --- Load environment variables ---
load_dotenv()
# --- OpenRouter Configuration ---
client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
set_default_openai_client(client, use_for_tracing=False)
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
MODEL = "google/gemini-2.5-flash"
# --- Function Tools ---
@function_tool
def check_ticket_status(ticket_id: str) -> str:
"""Look up the current status of a support ticket by its ID."""
tickets = {
"TKT-001": "Open - Assigned to Network Team",
"TKT-002": "In Progress - Password reset pending",
"TKT-003": "Resolved - Software reinstalled",
}
return tickets.get(ticket_id, f"Ticket {ticket_id} not found in the system.")
@function_tool
def reset_password(username: str) -> str:
"""Reset the password for a user account and send a temporary password."""
return (
f"Password reset initiated for '{username}'. "
f"A temporary password has been sent to their registered email. "
f"Temporary password: TempPass_{username[:3].upper()}2024"
)
@function_tool
def check_account_status(username: str) -> str:
"""Check if a user account is locked, active, or disabled."""
return (
f"Account '{username}' status: LOCKED. "
f"Reason: 5 failed login attempts. "
f"Last attempt: 10 minutes ago."
)
@function_tool
def run_network_diagnostic(target: str) -> str:
"""Run a network diagnostic (ping, traceroute) against a target host or IP."""
return (
f"Network diagnostic for '{target}':\n"
f"- Ping: 45ms average (normal)\n"
f"- Packet loss: 0%\n"
f"- DNS resolution: OK\n"
f"- Traceroute: 12 hops, no timeouts\n"
f"- Status: Connection appears healthy from server side. "
f"Issue may be client-side."
)
@function_tool
def check_system_status(service_name: str) -> str:
"""Check the operational status of an internal service or application."""
services = {
"email": "DEGRADED - High latency detected since 09:00 AM",
"vpn": "OPERATIONAL - All endpoints healthy",
"crm": "DOWN - Scheduled maintenance until 2:00 PM",
"erp": "OPERATIONAL - Running normally",
}
return services.get(
service_name.lower(),
f"Service '{service_name}' not found. Available: email, vpn, crm, erp",
)
@function_tool
def create_escalation_ticket(summary: str, priority: str, assigned_team: str) -> str:
"""Create a new escalation ticket for issues requiring human intervention."""
ticket_id = f"ESC-{abs(hash(summary)) % 10000:04d}"
return (
f"Escalation ticket created:\n"
f"- ID: {ticket_id}\n"
f"- Priority: {priority}\n"
f"- Assigned to: {assigned_team}\n"
f"- Summary: {summary}\n"
f"- Status: OPEN - Awaiting human review"
)
# --- Specialist Agents ---
password_agent = Agent(
name="Password & Access Agent",
handoff_description="Specialist for password resets, account lockouts, and access permission issues",
instructions=(
"You are an IT support specialist for password and access issues. "
"You help users with: password resets, account lockouts, access permission "
"requests, and multi-factor authentication issues. "
"Always check the account status before performing a reset. "
"Be professional and security-conscious."
),
model=MODEL,
tools=[reset_password, check_account_status, check_ticket_status],
)
software_agent = Agent(
name="Software Support Agent",
handoff_description="Specialist for software installation, crashes, errors, and application troubleshooting",
instructions=(
"You are an IT support specialist for software issues. "
"You help users with: application crashes and errors, software installation "
"problems, license and activation issues, and performance problems. "
"Check system status for known outages before troubleshooting. "
"Provide clear step-by-step guidance."
),
model=MODEL,
tools=[check_system_status, check_ticket_status, create_escalation_ticket],
)
network_agent = Agent(
name="Network & Hardware Agent",
handoff_description="Specialist for network connectivity, VPN, Wi-Fi, printer, and hardware issues",
instructions=(
"You are an IT support specialist for network and hardware issues. "
"You help users with: network connectivity problems, VPN connection issues, "
"Wi-Fi troubleshooting, printer and peripheral problems, and hardware failures. "
"Run diagnostics before suggesting solutions. "
"Guide users through basic troubleshooting before escalating."
),
model=MODEL,
tools=[
run_network_diagnostic,
check_system_status,
check_ticket_status,
create_escalation_ticket,
],
)
# --- Triage Agent ---
triage_agent = Agent(
name="IT Helpdesk Triage Agent",
instructions=(
"You are the first point of contact for the IT helpdesk. "
"Your job is to understand the user's issue and route them to the "
"correct specialist:\n"
"- Password & Access Agent: For password resets, account lockouts, "
"permission issues\n"
"- Software Support Agent: For application errors, installations, "
"software crashes\n"
"- Network & Hardware Agent: For connectivity, VPN, Wi-Fi, printers, "
"hardware problems\n\n"
"Ask a brief clarifying question if the issue category is ambiguous. "
"Be friendly and professional."
),
model=MODEL,
handoffs=[password_agent, software_agent, network_agent],
)
# --- Main Interactive Loop ---
async def main():
print("=" * 60)
print(" IT Helpdesk Triage System (Multi-Agent)")
print(" Powered by OpenAI Agents SDK + OpenRouter")
print("=" * 60)
print("\nType your IT support request below. Type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("\nThank you for using IT Helpdesk. Goodbye!")
break
if not user_input:
continue
result = await Runner.run(triage_agent, user_input)
print(f"\n[Handled by: {result.last_agent.name}]")
print(f"Agent: {result.final_output}\n")
if __name__ == "__main__":
asyncio.run(main())
Your multi-agent system is now runnable. The triage agent can classify requests and route them to specialists. Next up, you'll add a guardrail that blocks non-IT queries before they reach any specialist agent.
Add an Input Guardrail
Your triage system routes IT requests beautifully. But what happens when someone asks it for a pizza recipe or stock tips? Right now, the system would try to route that query to a specialist agent, wasting API credits on irrelevant requests.
Input guardrails solve this by screening every message before it reaches the expensive specialist agents. You will build a lightweight guardrail agent that classifies whether an input is IT-related, and reject off-topic queries immediately.
In this step, get ready to:
- Build a guardrail agent that classifies input as IT-related or not.
- Attach the guardrail to the Triage Agent so off-topic queries are blocked.
- Handle rejected queries gracefully in the interactive loop.
Build the guardrail agent with structured output
A guardrail needs to make a binary decision: is this input IT-related, yes or no? To get a reliable answer from the LLM, you will use a structured output that forces the response into a Pydantic model with a boolean field.
- In main.py, find the # --- Triage Agent --- comment.
- Add the following code directly above that comment to create the guardrail section:
# --- Input Guardrail ---
class TopicCheck(BaseModel):
is_it_related: bool
reasoning: str
guardrail_agent = Agent(
name="Topic Guardrail",
instructions=(
"Determine if the user's message is related to IT support "
"(passwords, software, hardware, network, accounts, computers, printers, "
"VPN, email, applications, servers, databases, security, etc.). "
"Return is_it_related=True if it is IT-related, False otherwise."
),
model=MODEL,
output_type=TopicCheck,
)
What does this code do?
The TopicCheck class defines the exact shape the guardrail agent must respond with. The is_it_related boolean is the decision, and reasoning forces the model to explain its classification (which improves accuracy).
Setting output_type=TopicCheck on the agent tells the SDK to parse the LLM response into that Pydantic model automatically. If the model returns anything else, the SDK retries.
Now you need the actual guardrail function that the SDK calls when input arrives.
- Add the following function directly below the guardrail_agent definition you just added, still above the # --- Triage Agent --- comment:
@input_guardrail
async def it_topic_guardrail(
ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
"""Checks if the input is related to IT support."""
result = await Runner.run(guardrail_agent, input, context=ctx.context)
return GuardrailFunctionOutput(
output_info=result.final_output,
tripwire_triggered=not result.final_output.is_it_related,
)
What does this code do?
The @input_guardrail decorator registers this function as a guardrail the SDK can attach to any agent. When triggered, it runs the lightweight guardrail_agent to classify the input.
The key line is tripwire_triggered=not result.final_output.is_it_related. When the input is NOT IT-related, the tripwire fires and the SDK raises an exception instead of continuing to the specialist agents.
Why use a separate agent for the guardrail?
The guardrail agent is intentionally lightweight. It only classifies input into two buckets (IT or not-IT). This classification runs on the same model but costs far fewer tokens than letting an off-topic query flow through the full triage and specialist pipeline.
Input guardrails only run on the first agent in the chain. Once a request passes the guardrail and gets handed off to a specialist, the guardrail does not re-run.
- Save main.py.
Seeing a red underline on TopicCheck or input_guardrail?
Both BaseModel and input_guardrail were already imported at the top of your file in a previous step. If your editor shows an error, scroll up and confirm the imports include from pydantic import BaseModel and input_guardrail in the from agents import (...) block.
Help me debug an import error in my guardrail code.
Attach the guardrail to the Triage Agent
The guardrail function exists, but the Triage Agent does not know about it yet. You need to register it on the agent so the SDK runs it automatically on every incoming message.
- In main.py, scroll down to the triage_agent definition.
- Find this line at the end of the agent definition:
handoffs=[password_agent, software_agent, network_agent],
)
- Replace the closing ) with an input_guardrails parameter, so the last two lines of the definition look like this:
handoffs=[password_agent, software_agent, network_agent],
input_guardrails=[it_topic_guardrail],
)
What does this code do?
The input_guardrails parameter accepts a list of guardrail functions. You can stack multiple guardrails here (for example, one that blocks profanity and one that blocks off-topic queries). The SDK runs all of them in parallel before processing the request.
- Save main.py.
Handle rejected queries in the main loop
When the guardrail trips, the SDK raises an InputGuardrailTripwireTriggered exception. Without a try/except block, this would crash your program. You need to catch it and show a friendly message instead.
- In main.py, scroll down to the main() function.
- Find these three lines inside the while True loop:
result = await Runner.run(triage_agent, user_input)
print(f"\n[Handled by: {result.last_agent.name}]")
print(f"Agent: {result.final_output}\n")
- Replace those three lines with the following try/except block:
try:
result = await Runner.run(triage_agent, user_input)
print(f"\n[Handled by: {result.last_agent.name}]")
print(f"Agent: {result.final_output}\n")
except InputGuardrailTripwireTriggered:
print(
"\nAgent: I'm sorry, I can only help with IT support requests. "
"Please describe a technical issue like a password reset, "
"software problem, or network connectivity issue.\n"
)
What does this code do?
The try block wraps the normal agent execution. If the input passes the guardrail, everything works as before.
If the guardrail trips (the input is not IT-related), the SDK raises InputGuardrailTripwireTriggered and your code catches it gracefully. The user sees a helpful message pointing them back to IT topics, and no specialist agent is invoked.
- Save main.py.
Seeing an indentation error?
Make sure the entire try/except block is indented at the same level as the other code inside the while True loop (8 spaces or 2 tabs). The lines inside try: and except: need one additional level of indentation (12 spaces or 3 tabs).
Help me fix an indentation error in my main loop.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py.
ⓧ I'd like to double check the full code
import asyncio
import os
from dotenv import load_dotenv
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
OpenAIChatCompletionsModel,
RunContextWrapper,
Runner,
TResponseInputItem,
function_tool,
input_guardrail,
set_default_openai_api,
set_default_openai_client,
set_tracing_disabled,
)
from openai import AsyncOpenAI
# --- Load environment variables ---
load_dotenv()
# --- OpenRouter Configuration ---
client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
set_default_openai_client(client, use_for_tracing=False)
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
MODEL = "google/gemini-2.5-flash"
# --- Function Tools ---
@function_tool
def check_ticket_status(ticket_id: str) -> str:
"""Look up the current status of a support ticket by its ID."""
tickets = {
"TKT-001": "Open - Assigned to Network Team",
"TKT-002": "In Progress - Password reset pending",
"TKT-003": "Resolved - Software reinstalled",
}
return tickets.get(ticket_id, f"Ticket {ticket_id} not found in the system.")
@function_tool
def reset_password(username: str) -> str:
"""Reset the password for a user account and send a temporary password."""
return (
f"Password reset initiated for '{username}'. "
f"A temporary password has been sent to their registered email. "
f"Temporary password: TempPass_{username[:3].upper()}2024"
)
@function_tool
def check_account_status(username: str) -> str:
"""Check if a user account is locked, active, or disabled."""
return (
f"Account '{username}' status: LOCKED. "
f"Reason: 5 failed login attempts. "
f"Last attempt: 10 minutes ago."
)
@function_tool
def run_network_diagnostic(target: str) -> str:
"""Run a network diagnostic (ping, traceroute) against a target host or IP."""
return (
f"Network diagnostic for '{target}':\n"
f"- Ping: 45ms average (normal)\n"
f"- Packet loss: 0%\n"
f"- DNS resolution: OK\n"
f"- Traceroute: 12 hops, no timeouts\n"
f"- Status: Connection appears healthy from server side. "
f"Issue may be client-side."
)
@function_tool
def check_system_status(service_name: str) -> str:
"""Check the operational status of an internal service or application."""
services = {
"email": "DEGRADED - High latency detected since 09:00 AM",
"vpn": "OPERATIONAL - All endpoints healthy",
"crm": "DOWN - Scheduled maintenance until 2:00 PM",
"erp": "OPERATIONAL - Running normally",
}
return services.get(
service_name.lower(),
f"Service '{service_name}' not found. Available: email, vpn, crm, erp",
)
@function_tool
def create_escalation_ticket(summary: str, priority: str, assigned_team: str) -> str:
"""Create a new escalation ticket for issues requiring human intervention."""
ticket_id = f"ESC-{abs(hash(summary)) % 10000:04d}"
return (
f"Escalation ticket created:\n"
f"- ID: {ticket_id}\n"
f"- Priority: {priority}\n"
f"- Assigned to: {assigned_team}\n"
f"- Summary: {summary}\n"
f"- Status: OPEN - Awaiting human review"
)
# --- Specialist Agents ---
password_agent = Agent(
name="Password & Access Agent",
handoff_description="Specialist for password resets, account lockouts, and access permission issues",
instructions=(
"You are an IT support specialist for password and access issues. "
"You help users with: password resets, account lockouts, access permission "
"requests, and multi-factor authentication issues. "
"Always check the account status before performing a reset. "
"Be professional and security-conscious."
),
model=MODEL,
tools=[reset_password, check_account_status, check_ticket_status],
)
software_agent = Agent(
name="Software Support Agent",
handoff_description="Specialist for software installation, crashes, errors, and application troubleshooting",
instructions=(
"You are an IT support specialist for software issues. "
"You help users with: application crashes and errors, software installation "
"problems, license and activation issues, and performance problems. "
"Check system status for known outages before troubleshooting. "
"Provide clear step-by-step guidance."
),
model=MODEL,
tools=[check_system_status, check_ticket_status, create_escalation_ticket],
)
network_agent = Agent(
name="Network & Hardware Agent",
handoff_description="Specialist for network connectivity, VPN, Wi-Fi, printer, and hardware issues",
instructions=(
"You are an IT support specialist for network and hardware issues. "
"You help users with: network connectivity problems, VPN connection issues, "
"Wi-Fi troubleshooting, printer and peripheral problems, and hardware failures. "
"Run diagnostics before suggesting solutions. "
"Guide users through basic troubleshooting before escalating."
),
model=MODEL,
tools=[
run_network_diagnostic,
check_system_status,
check_ticket_status,
create_escalation_ticket,
],
)
# --- Input Guardrail ---
class TopicCheck(BaseModel):
is_it_related: bool
reasoning: str
guardrail_agent = Agent(
name="Topic Guardrail",
instructions=(
"Determine if the user's message is related to IT support "
"(passwords, software, hardware, network, accounts, computers, printers, "
"VPN, email, applications, servers, databases, security, etc.). "
"Return is_it_related=True if it is IT-related, False otherwise."
),
model=MODEL,
output_type=TopicCheck,
)
@input_guardrail
async def it_topic_guardrail(
ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
"""Checks if the input is related to IT support."""
result = await Runner.run(guardrail_agent, input, context=ctx.context)
return GuardrailFunctionOutput(
output_info=result.final_output,
tripwire_triggered=not result.final_output.is_it_related,
)
# --- Triage Agent ---
triage_agent = Agent(
name="IT Helpdesk Triage Agent",
instructions=(
"You are the first point of contact for the IT helpdesk. "
"Your job is to understand the user's issue and route them to the "
"correct specialist:\n"
"- Password & Access Agent: For password resets, account lockouts, "
"permission issues\n"
"- Software Support Agent: For application errors, installations, "
"software crashes\n"
"- Network & Hardware Agent: For connectivity, VPN, Wi-Fi, printers, "
"hardware problems\n\n"
"Ask a brief clarifying question if the issue category is ambiguous. "
"Be friendly and professional."
),
model=MODEL,
handoffs=[password_agent, software_agent, network_agent],
input_guardrails=[it_topic_guardrail],
)
# --- Main Interactive Loop ---
async def main():
print("=" * 60)
print(" IT Helpdesk Triage System (Multi-Agent)")
print(" Powered by OpenAI Agents SDK + OpenRouter")
print("=" * 60)
print("\nType your IT support request below. Type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("\nThank you for using IT Helpdesk. Goodbye!")
break
if not user_input:
continue
try:
result = await Runner.run(triage_agent, user_input)
print(f"\n[Handled by: {result.last_agent.name}]")
print(f"Agent: {result.final_output}\n")
except InputGuardrailTripwireTriggered:
print(
"\nAgent: I'm sorry, I can only help with IT support requests. "
"Please describe a technical issue like a password reset, "
"software problem, or network connectivity issue.\n"
)
if __name__ == "__main__":
asyncio.run(main())
Your helpdesk now has a bouncer at the door. Next up, you will run the full system and test it with real scenarios to see the handoffs, tools, and guardrail all working together.
Test Your Multi-Agent System
Your multi-agent system is fully built. You have a Triage Agent that routes requests, three specialist agents with their own tools, and an input guardrail that blocks off-topic queries.
Now it's time to put it all to the test. You'll run the system and send it different types of IT support requests to confirm each specialist handles the right category.
In this step, get ready to:
- Run the multi-agent helpdesk system.
- Test routing to each specialist agent.
- Verify the input guardrail blocks off-topic queries.
Run the multi-agent system
The interactive loop in main.py waits for your input, sends it through the Triage Agent, and prints which specialist handled the request.
- Start the system by running this command in your terminal:
python main.py
You should see the helpdesk banner appear in your terminal:
============================================================
IT Helpdesk Triage System (Multi-Agent)
Powered by OpenAI Agents SDK + OpenRouter
============================================================
Type your IT support request below. Type 'quit' to exit.
Not seeing the banner?
Make sure your virtual environment is activated. You should see the environment name in parentheses at the start of your terminal prompt.
Check that your .env file is in the same directory as main.py and contains your OPENROUTER_API_KEY.
Help me troubleshoot why my multi-agent system won't start
Test routing to each specialist
Each request you type goes through the Triage Agent first. The Triage Agent classifies the issue and hands it off to the correct specialist. The [Handled by: ...] line in the output tells you which agent took over.
- At the You: prompt, type a password-related request like:
I forgot my password and I'm locked out of my account. My username is jsmith.
- Check the output. You should see [Handled by: Password & Access Agent] followed by the agent checking account status and offering to reset the password.
What is happening behind the scenes?
The Triage Agent reads your message and decides it's a password issue. It performs a handoff to the Password & Access Agent, which then uses the check_account_status and reset_password function tools to take action.
The result.last_agent.name property is what prints the agent name in the [Handled by: ...] line. This confirms the handoff worked correctly.
- Now test a software issue. Type something like:
The CRM application keeps crashing when I try to open it. Is there a known outage?
- You should see [Handled by: Software Support Agent] in the output. The agent will use check_system_status and report that the CRM is down for scheduled maintenance.
- Finally, test a network issue. Type something like:
My VPN keeps disconnecting and I can't reach the internal servers.
- You should see [Handled by: Network & Hardware Agent] in the output. The agent will run diagnostics using run_network_diagnostic and guide you through troubleshooting steps.
Try your own scenarios
The test prompts above are suggestions. Try phrasing requests in your own words to see how well the Triage Agent classifies them. Ambiguous requests might trigger a clarifying question from the Triage Agent before it routes.
Test the input guardrail with off-topic queries
The input guardrail runs before any specialist agent does. It's designed to reject queries that aren't related to IT support, saving you from wasting API credits on irrelevant requests.
- Type an off-topic query like:
What's the best pizza place near the office?
- You should see the rejection message: "I'm sorry, I can only help with IT support requests." This means the guardrail caught the off-topic input before it reached any specialist agent.
- Try another off-topic query to confirm consistent blocking:
Can you write me a poem about cats?
- You should see the same rejection message again. The guardrail agent classified this as non-IT-related and triggered the InputGuardrailTripwireTriggered exception.
- When you're done testing, type quit to exit the system.
Why do guardrails save money?
Without the guardrail, every off-topic message would travel through the Triage Agent and potentially get handed off to a specialist. That's multiple LLM calls per junk request.
The guardrail uses a single cheap classification call to block irrelevant queries before the expensive specialist agents ever run. In production systems handling thousands of requests, this adds up fast.
Requests routing to the wrong agent?
LLMs are probabilistic, so occasional misrouting is normal. If routing is consistently wrong, check that your handoff_description values on each specialist agent clearly describe the issues they handle.
If the guardrail is blocking valid IT queries, the guardrail agent's instructions might be too restrictive. Check that the instructions on your guardrail_agent include the IT categories you're testing.
Help me debug why my agents are routing incorrectly
Secret mission
Add Human-in-the-Loop Escalation
Your agents can now route and act autonomously. But should a password reset really happen without a human saying "yes"? In production, critical actions need a human checkpoint. Add an approval gate so password resets pause for your explicit go-ahead before executing.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine with API calls to OpenRouter. There are no ongoing costs since the script only uses API credits when you manually run it.
Decide whether to keep your project files, or delete them entirely.
Resources you used:
- Project folder on your Desktop (contains main.py, .env, and your Python virtual environment).
- OpenRouter API key (stored in your OpenRouter account and locally in .env).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your multi-agent system.
- Your project folder sits on your Desktop. You can re-run the helpdesk anytime by activating the virtual environment and running python main.py.
- Your OpenRouter API key remains active. It only costs credits when you run the script.
Pause - I'll come back to this later
No running processes to stop. The script only runs when you manually start it.
- If your terminal is still open with the helpdesk running, type quit to exit the script.
- Your project folder and OpenRouter API key remain intact for when you return.
Delete - I don't want to use this again
Remove all project files and revoke your API key to clean up completely.
- Delete your project folder by running this command in your terminal:
cd ~/Desktop
rm -rf it-helpdesk
What if my folder has a different name?
Replace it-helpdesk with whatever you named your project folder in Step 1. If you are unsure, run ls ~/Desktop to find it.
- To revoke your OpenRouter API key, visit your keys page at openrouter.ai/keys.
- Find the key you created for this project and delete it.
Windows users
If you are on Windows, open PowerShell and run the following instead:
cd ~\Desktop
Remove-Item -Recurse -Force it-helpdesk
Nice Work!
Nice Work!
You just built a production-grade multi-agent IT helpdesk triage system. Your agents autonomously classify support requests, route them to the right specialist, and take real actions using function tools.
You've learned how to:
- Built three specialist AI agents with domain-specific function tools that simulate real IT operations.
- Implemented the handoff pattern for autonomous request routing between a triage agent and specialist agents.
- Added an input guardrail to block off-topic queries before they reach expensive specialist agents.
- Added human-in-the-loop approval so critical operations like password resets pause for manual confirmation before executing.
Ready to quiz yourself?