DEV Community

shashank ms
shashank ms

Posted on

Building LLM-Powered Virtual Assistants: A Step-by-Step Guide

We're building a conversational support agent for an e-commerce store. It will look up orders and process returns using function calling through Oxlo.ai. This pattern works for any internal tool you need to expose through natural language.

What you'll need

Step 1: Verify the Connection

Before adding logic, I always make sure the client can reach Oxlo.ai and that my key works. This snippet calls Llama 3.3 70B with a simple prompt.

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": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Say hello and confirm you are online."},
    ],
)

print(response.choices[0].message.content)

Step 2: Lock Down the System Prompt

The system prompt is the agent's contract. It defines the tone, the available tools, and the guardrails. I keep it in its own variable so non-engineers can edit it without touching code.

SYSTEM_PROMPT = """You are an e-commerce support agent for Acme Goods.
Your job is to help customers check order status and initiate returns.
You have access to the following tools:
- check_order_status(order_id): Returns shipping status and items.
- initiate_return(order_id, reason): Starts a return and returns a confirmation code.
Rules:
1. Always ask for the order_id if it is missing.
2. Before initiating a return, confirm the reason with the user.
3. If the user is angry or asks for a human, apologize and offer to escalate.
4. Respond in a friendly, concise tone."""

Step 3: Mock the Business Logic

Real agents need real data. I will stub out an order database and two helper functions so the tutorial is fully runnable without setting up Postgres or a CRM.

ORDERS = {
    "ORD-1001": {"status": "shipped", "items": ["Wireless Mouse", "USB-C Cable"], "delivered": True},
    "ORD-1002": {"status": "processing", "items": ["Mechanical Keyboard"], "delivered": False},
}

def check_order_status(order_id: str) -> dict:
    if order_id not in ORDERS:
        return {"error": "Order not found."}
    return ORDERS[order_id]

def initiate_return(order_id: str, reason: str) -> dict:
    if order_id not in ORDERS:
        return {"error": "Order not found."}
    if not ORDERS[order_id]["delivered"]:
        return {"error": "Order has not been delivered yet."}
    return {"confirmation": f"RET-{order_id}", "reason": reason, "status": "return_initiated"}

Step 4: Describe the Tools to the Model

Oxlo.ai expects OpenAI-compatible tool schemas. By defining names, descriptions, and JSON schemas for parameters, we let the model decide when to call a function and what arguments to pass.

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "check_order_status",
            "description": "Look up the current status of an order by ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "The order identifier, e.g. ORD-1001"}
                },
                "required": ["order_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "initiate_return",
            "description": "Start a return for a delivered order.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string"},
                    "reason": {"type": "string", "description": "Why the customer wants to return the item."}
                },
                "required": ["order_id", "reason"]
            }
        }
    }
]

Step 5: Build the Agent Loop

This is the core. We send the conversation to Qwen 3 32B, which handles agent workflows well. If the model returns a tool call, we execute the local function, append the result, and send it back. We repeat until we get a final answer.

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

def run_agent(user_message: str, history: list = None) -> tuple[str, list]:
    if history is None:
        history = [{"role": "system", "content": SYSTEM_PROMPT}]
    
    history.append({"role": "user", "content": user_message})
    
    while True:
        response = client.chat.completions.create(
            model="qwen-3-32b",
            messages=history,
            tools=TOOLS,
            tool_choice="auto",
        )
        
        message = response.choices[0].message
        
        if message.tool_calls:
            history.append({
                "role": "assistant",
                "content": message.content or "",
                "tool_calls": [tc.model_dump() for tc in message.tool_calls]
            })
            
            for tc in message.tool_calls:
                fn_name = tc.function.name
                args = json.loads(tc.function.arguments)
                
                if fn_name == "check_order_status":
                    result = check_order_status(**args)
                elif fn_name == "initiate_return":
                    result = initiate_return(**args)
                else:
                    result = {"error": "Unknown function"}
                
                history.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(result)
                })
        else:
            history.append({"role": "assistant", "content": message.content})
            return message.content, history

# Quick test
answer, _ = run_agent("Where is my order ORD-1001?")
print(answer)

Step 6: Add Multi-Turn Memory

A real assistant remembers context. Because run_agent already appends to a history list, we just need to persist that list between user inputs instead of starting fresh each time.

def chat_session():
    history = None
    print("Support agent ready. Type 'exit' to quit.")
    while True:
        user_input = input("User: ").strip()
        if user_input.lower() == "exit":
            break
        reply, history = run_agent(user_input, history)
        print(f"Agent: {reply}")

if __name__ == "__main__":
    chat_session()

Run it

Here is a complete, self-contained script that ties everything together. Save it as support_agent.py, export your key, and run it.

import json
import os
from openai import OpenAI

# ------------------------------------------------------------------
# 1. Config
# ------------------------------------------------------------------
client = OpenAI(
    base_url="https://api.oxlo.ai/v1",
    api_key=os.getenv("OXLO_API_KEY", "YOUR_OXLO_API_KEY")
)

SYSTEM_PROMPT = """You are an e-commerce support agent for Acme Goods.
Your job is to help customers check order status and initiate returns.
You have access to the following tools:
- check_order_status(order_id): Returns shipping status and items.
- initiate_return(order_id, reason): Starts a return and returns a confirmation code.
Rules:
1. Always ask for the order_id if it is missing.
2. Before initiating a return, confirm the reason with the user.
3. If the user is angry or asks for a human, apologize and offer to escalate.
4. Respond in a friendly, concise tone."""

# ------------------------------------------------------------------
# 2. Mock Data Layer
# ------------------------------------------------------------------
ORDERS = {
    "ORD-1001": {"status": "shipped", "items": ["Wireless Mouse", "USB-C Cable"], "delivered": True},
    "ORD-1002": {"status": "processing", "items": ["Mechanical Keyboard"], "delivered": False},
}

def check_order_status(order_id: str) -> dict:
    if order_id not in ORDERS:
        return {"error": "Order not found."}
    return ORDERS[order_id]

def initiate_return(order_id: str, reason: str) -> dict:
    if order_id not in ORDERS:
        return {"error": "Order not found."}
    if not ORDERS[order_id]["delivered"]:
        return {"error": "Order has not been delivered yet."}
    return {"confirmation": f"RET-{order_id}", "reason": reason, "status": "return_initiated"}

# ------------------------------------------------------------------
# 3. Tool Schemas
# ------------------------------------------------------------------
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "check_order_status",
            "description": "Look up the current status of an order by ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "The order identifier, e.g. ORD-1001"}
                },
                "required": ["order_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "initiate_return",
            "description": "Start a return for a delivered order.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string"},
                    "reason": {"type": "string", "description": "Why the customer wants to return the item."}
                },
                "required": ["order_id", "reason"]
            }
        }
    }
]

# ------------------------------------------------------------------
# 4. Agent Loop
# ------------------------------------------------------------------
def run_agent(user_message: str, history: list = None) -> tuple[str, list]:
    if history is None:
        history = [{"role": "system", "content": SYSTEM_PROMPT}]
    history.append({"role": "user", "content": user_message})
    
    while True:
        response = client.chat.completions.create(
            model="qwen-3-32b",
            messages=history,
            tools=TOOLS,
            tool_choice="auto",
        )
        message = response.choices[0].message
        
        if message.tool_calls:
            history.append({
                "role": "assistant",
                "content": message.content or "",
                "tool_calls": [tc.model_dump() for tc in message.tool_calls]
            })
            for tc in message.tool_calls:
                fn_name = tc.function.name
                args = json.loads(tc.function.arguments)
                if fn_name == "check_order_status":
                    result = check_order_status(**args)
                elif fn_name == "initiate_return":
                    result = initiate_return(**args)
                else:
                    result = {"error": "Unknown function"}
                history.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(result)
                })
        else:
            history.append({"role": "assistant", "content": message.content})
            return message.content, history

# ------------------------------------------------------------------
# 5. Example Interaction
# ------------------------------------------------------------------
if __name__ == "__main__":
    print("Agent ready.\n")
    history = None
    
    for user_msg in [
        "Hi, I need help with order ORD-1001",
        "It arrived but the mouse is defective. Can I return it?",
        "Yes, the reason is defective on arrival."
    ]:
        print(f"User: {user_msg}")
        reply, history = run_agent(user_msg, history)
        print(f"Agent: {reply}\n")

Example output:

Agent ready.

User: Hi, I need help with order ORD-1001
Agent: I can help with that. Your order ORD-1001 has been shipped and contains: Wireless Mouse, USB-C Cable. It looks like it has been delivered.

User: It arrived but the mouse is defective. Can I return it?
Agent: I'm sorry to hear the mouse is defective. I can start a return for order ORD-1001. Could you confirm the reason for the return so I can proceed?

User: Yes, the reason is defective on arrival.
Agent: No problem. I've initiated your return. Your confirmation code is RET-ORD-1001. You should receive a prepaid shipping label via email shortly.

Wrap-up and Next Steps

This loop works because Oxlo.ai exposes an OpenAI-compatible function-calling interface, so the same code runs against Llama 3.3 70B, Qwen 3 32B, or Kimi K2.6 without changes. Because Oxlo.ai charges per request rather than per token, long system prompts and multi-turn histories do not inflate your cost the way they would on token-based providers. See https://oxlo.ai/pricing for plan details.

Two concrete ways to extend this:

  1. Replace the ORDERS dict with live queries to your CRM or Shopify API.
  2. Add a sentiment-check tool and auto-escalate to a human inbox when the model detects frustration, using the same tool-calling pattern.

Top comments (0)