Today we will build a customer support resolution agent that triages incoming requests, looks up order data, and processes refunds without human intervention. This pattern applies to any agentic workload where an LLM must plan steps, call tools, and maintain state across multiple turns. We will run the whole thing on Oxlo.ai, where request-based pricing means the cost per turn stays flat even as the conversation context grows.
What you'll need
- Python 3.10 or newer
- The OpenAI SDK:
pip install openai - An Oxlo.ai API key from https://portal.oxlo.ai
1. Define the tools and client
Start by importing the OpenAI SDK and pointing it at Oxlo.ai. Then define the JSON schemas for the two actions our agent can take: looking up an order and processing a refund.
from openai import OpenAI
import json
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
TOOLS = [
{
"type": "function",
"function": {
"name": "lookup_order",
"description": "Retrieve order details by order ID.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order identifier."
}
},
"required": ["order_id"]
}
}
},
{
"type": "function",
"function": {
"name": "process_refund",
"description": "Process a refund for a given order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"amount": {
"type": "number",
"description": "Refund amount in USD."
},
"reason": {"type": "string"}
},
"required": ["order_id", "amount", "reason"]
}
}
}
]
2. Write the system prompt
The system prompt is the contract that keeps the agent predictable. It tells the model when to ask for missing data, when to call a tool, and how to format its final answer.
SYSTEM_PROMPT = """You are a customer support agent. Your job is to resolve refund requests.
Rules:
1. If the user does not provide an order_id, ask them for it.
2. Once you have an order_id, call lookup_order to verify the order exists.
3. If the order is eligible for refund (status is delivered), call process_refund with the correct amount and a concise reason.
4. After processing the refund, summarize what you did for the user.
5. Never make up order details. Only use data returned from tools.
6. Keep responses under three sentences unless the user asks for detail."""
3. Build the tool executor
The LLM emits JSON tool calls, but our Python code has to run them. We will build a small dispatcher that routes to real functions and returns structured results.
# Mock database
ORDERS = {
"ORD-2024-8891": {
"status": "delivered",
"amount": 149.99,
"item": "Wireless Headphones"
},
"ORD-2024-9102": {
"status": "shipped",
"amount": 45.00,
"item": "USB-C Cable"
},
}
def lookup_order(order_id: str) -> dict:
order = ORDERS.get(order_id)
if not order:
return {"error": "Order not found"}
return order
def process_refund(order_id: str, amount: float, reason: str) -> dict:
order = ORDERS.get(order_id)
if not order:
return {"error": "Order not found"}
if order["status"] != "delivered":
return {"error": f"Cannot refund order with status {order['status']}"}
return {
"success": True,
"refund_id": f"REF-{order_id}",
"amount": amount,
"reason": reason
}
TOOL_MAP = {
"lookup_order": lookup_order,
"process_refund": process_refund,
}
def execute_tool(call) -> dict:
name = call.function.name
args = json.loads(call.function.arguments)
func = TOOL_MAP[name]
return func(**args)
4. Wire the agent loop
Now we connect the LLM to the tool executor. We keep a message list in memory, append the model's tool_calls, run the functions, and feed the results back as tool messages. I use qwen-3-32b here because it handles multi-step agent workflows reliably.
def run_agent(user_message: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
while True:
response = client.chat.completions.create(
model="qwen-3-32b",
messages=messages,
tools=TOOLS,
tool_choice="auto",
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content
for tool_call in message.tool_calls:
result = execute_tool(tool_call)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": json.dumps(result),
})
5. Add guardrails
Before returning to the user, we run a second pass with llama-3.3-70b to strip any internal IDs or technical jargon. This keeps the external interface clean without adding manual string parsing.
def sanitize_response(raw_text: str) -> str:
guard_prompt = (
"Remove any internal IDs like REF-xxx or ORD-xxx. "
"Rewrite the following support response in plain, friendly language. "
"Do not add greetings if they are already present.\n\n"
f"Text: {raw_text}"
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": guard_prompt}],
)
return response.choices[0].message.content
def resolve_ticket(user_message: str) -> str:
raw = run_agent(user_message)
return sanitize_response(raw)
Run it
Test the full pipeline with two scenarios: one where the agent is missing the order ID, and one where it runs the complete lookup-refund flow.
if __name__ == "__main__":
# Scenario 1: missing order ID
print("User: I want a refund")
print("Agent:", resolve_ticket("I want a refund"))
# Scenario 2: full flow
print("\nUser: I want a refund for ORD-2024-8891, they arrived broken")
print("Agent:", resolve_ticket(
"I want a refund for ORD-2024-8891, they arrived broken"
))
Expected output:
User: I want a refund
Agent: Sure, I can help with that. Could you please provide your order ID?
User: I want a refund for ORD-2024-8891, they arrived broken
Agent: I've processed your refund for the Wireless Headphones. You should see the amount back in your account within 5 business days.
Next steps
Replace the mock ORDERS dictionary with a real database or internal API, and persist the messages list to Redis between turns so the agent remembers context across sessions. If your tool schemas grow, you can expand the system prompt with few-shot examples without worrying about ballooning token costs, because Oxlo.ai charges per request rather than per token. For details on request limits and plans, see https://oxlo.ai/pricing.
Top comments (0)