We are building a stateful technical support agent that can look up orders and process refunds through multi-turn dialogue. This pattern works for any customer-facing chatbot where context and tool use matter more than single-turn latency. I will use Oxlo.ai because its flat per-request pricing keeps long conversations predictable, and the OpenAI-compatible SDK means zero client-side changes.
What you'll need
Python 3.10 or newer, the OpenAI SDK, and an Oxlo.ai API key from https://portal.oxlo.ai. Install the SDK with pip install openai. I will use the llama-3.3-70b model for strong instruction following, though kimi-k2.6 or qwen-3-32b are solid alternatives on Oxlo.ai.
Step 1: Initialize the Oxlo.ai client
First, I import the OpenAI client and point it at Oxlo.ai. This is a drop-in replacement. I send a single-turn health check to confirm the endpoint is live.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "user", "content": "Hello, can you help with a refund?"}
]
)
print(response.choices[0].message.content)
Step 2: Design the system prompt
A dialogue system lives or dies by its system prompt. I keep the instructions explicit, limit the scope to refunds and order lookups, and tell the agent to ask for missing fields rather than hallucinating.
SYSTEM_PROMPT = """You are a support agent for a hardware store. Your job is to help customers with order lookups and refund requests.
Rules:
- Always ask for the order ID if the user mentions an order problem.
- Before issuing a refund, confirm the item name and reason with the user.
- Be concise. Do not make up policies. If you need data, use the available tools.
- Maintain a helpful, professional tone across all turns."""
Step 3: Add tools and mock data
In production these functions would query a database, but dictionaries are enough to exercise the inference loop. I define two tools, get_order and process_refund, plus the JSON schemas the model needs to invoke them.
import json
ORDERS = {
"ORD-1001": {"item": "Cordless Drill", "price": 129.00, "status": "delivered"},
"ORD-1002": {"item": "Safety Goggles", "price": 24.50, "status": "shipped"},
}
TOOLS = [
{
"type": "function",
"function": {
"name": "get_order",
"description": "Look up an order by ID",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"]
}
}
},
{
"type": "function",
"function": {
"name": "process_refund",
"description": "Process a refund for an order",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"reason": {"type": "string"}
},
"required": ["order_id", "reason"]
}
}
}
]
def get_order(order_id):
return ORDERS.get(order_id, {"error": "Order not found"})
def process_refund(order_id, reason):
if order_id in ORDERS:
ORDERS[order_id]["status"] = "refunded"
return {"success": True, "amount": ORDERS[order_id]["price"]}
return {"success": False, "error": "Order not found"}
Step 4: Handle a single turn with tool calling
This function appends the user message, runs inference, and handles any parallel tool calls. If the model requests data, I execute the local Python functions, append the results, and call the model again so it can respond in natural language.
def dialogue_turn(messages):
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
msg = response.choices[0].message
if msg.tool_calls:
messages.append({
"role": "assistant",
"content": msg.content or "",
"tool_calls": [tc.model_dump() for tc in msg.tool_calls]
})
for tc in msg.tool_calls:
fn_name = tc.function.name
args = json.loads(tc.function.arguments)
if fn_name == "get_order":
result = get_order(**args)
elif fn_name == "process_refund":
result = process_refund(**args)
else:
result = {"error": "Unknown function"}
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result)
})
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=messages,
tools=TOOLS
)
msg = response.choices[0].message
messages.append({"role": "assistant", "content": msg.content})
return msg.content
Step 5: Build the conversation loop
Now I wire the turn handler into a simple REPL that accumulates messages. Because Oxlo.ai charges per request rather than per token, extending this context with long system prompts or lengthy user histories does not inflate the cost of each turn.
def run_support_agent():
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("Support agent ready. Type 'exit' to quit.")
while True:
user_input = input("User: ")
if user_input.lower() == "exit":
break
messages.append({"role": "user", "content": user_input})
reply = dialogue_turn(messages)
print(f"Agent: {reply}")
if __name__ == "__main__":
run_support_agent()
Run it
Save the script as support_agent.py, replace YOUR_OXLO_API_KEY, and run python support_agent.py. Here is a sample transcript.
Support agent ready. Type 'exit' to quit.
User: I need a refund for my drill
Agent: I can help with that. Could you please provide your order ID?
User: ORD-1001
Agent: I found your order for Cordless Drill ($129.00). What is the reason for the refund?
User: It stopped working after two days
Agent: Thank you. To confirm, you want to process a refund for Cordless Drill (order ORD-1001) because it stopped working after two days. Is that correct?
User: Yes
Agent: Your refund for $129.00 has been processed. The order status is now refunded.
Wrap-up
That is the core inference loop for a tool-using dialogue system on Oxlo.ai. Two concrete next steps: replace the ORDERS dictionary with a real database connection and add a create_ticket tool for edge cases that need human review, and add conversation summarization or context truncation for 50-plus turn sessions, possibly switching to deepseek-v3.2 or kimi-k2.6 if you need larger context windows.
Top comments (0)