This guide explores building a customer service bot with tools and tracing using Opper. The bot demonstrates handling order status inquiries, product queries, and other customer service tasks while maintaining a natural conversation flow.

Key Features

  • Intent classification to understand user requests (get_order_status etc)
  • Control of intent requirements (email and order id required)
  • Tool support for performing outside system actions (query database)
  • Conversation history tracking
  • Natural language responses
  • Tracing for monitoring and debugging

Example run

Below is an example conversation showing how the bot handles an order status inquiry, followed by the implementation code.

# Example conversation demonstrating order status inquiry:

User: Hello
Assistant: Hi there! How can I assist you today?

User: I placed an order the other day and I wonder when to expect it
Assistant: Could you please provide me with your order ID and the email address you used to place the order? This will help me check the status for you.

User: my order id is 123123
Assistant: Thank you for providing your order ID. Could you also share the email address you used to place the order so I can look up the details for you?

User: santaclause@clau.se
Assistant: I'm sorry, but I couldn't find an order with the ID 123123. Could you double-check the order ID and make sure it's correct? If you need further assistance, please let me know!

User: oh actually my email is santa@clau.se
Assistant: Thank you for the updated email address. I have found your order with ID 123123, and it is currently out for delivery to Snowy Mountain 123, 421 23, Greenland. If you need further assistance, please let me know!

User: What did I order? I forgot
Assistant: Currently, I don't have details on the contents of your order. For more information, please check your email or the confirmation message you received after placing the order. If you need further assistance, feel free to ask!

User: ok thanks! 
Assistant: You're welcome! If you have any more questions or need further assistance in the future, feel free to reach out. Have a great day!

Implementation

Let’s look at the complete implementation of our customer service bot. The code below demonstrates how we use Opper’s function calling capabilities to handle intents, extract order information, and generate appropriate responses.

from opperai import Opper, trace
from pydantic import BaseModel
from typing import Literal
import os

# Initialize Opper client
opper = Opper(api_key=os.getenv("OPPER_API_KEY"))

# A test "database"
orders = {
    123123: {
        "email": "santa@clau.se", 
        "status": "delivering",
        "created_date": "2024-11-03", 
        "updated_date": "2024-11-06",
        "adress": "Snowy Mountain 123, 421 23, Greenland",
        "purchase": "Large sled 1999 SEK"
        }
    }

# A classification of the conversation
class IntentClassification(BaseModel):
    thoughts: str
    intent: Literal["get_order_status", "query_products", "unsupported"]

# Function to determine intent of conversation
def determine_intent(messages):
    intent, _ = opper.call(
        name="determine_intent",
        instructions="Analyze the user message and determine their intent. Supported intents are get_order_status and query products.",
        input={"messages": messages},
        output_type=IntentClassification
    )
    return intent
    
# A parsed order request
class ParsedOrder(BaseModel):
    thoughts: str
    order_id: int | None = None
    email: str | None = None

# Function to extract order data from conversation
def extract_order_from_messages(messages):
    order_info, _ = opper.call(
        name="extract_order_info",
        instructions="Extract order ID and email from the conversation if present",
        input={"messages": messages},
        output_type=ParsedOrder
    )
    return order_info

# Function to get the requested order
def get_order(id, email):
    if id in orders and orders[id]["email"] == email:
        return orders[id]
    else:
        return None

# Function to process messages
def process_message(messages):

    # We first determine intent with the conversation
    intent = determine_intent(messages)

    # Process based on intent
    if intent.intent == "get_order_status":

        # Extract requested order information
        order_request = extract_order_from_messages(messages)

        # Verify we have all needed order info
        if not order_request.order_id or not order_request.email:
            return  f"Need {'order ID and email' if not order_request.order_id and not order_request.email else 'order ID' if not order_request.order_id else 'email'}"

        order = get_order(id=order_request.order_id, email=order_request.email)
        if order:
            return {
                    "order_id": order_request.order_id,
                    "status": orders[order_request.order_id]["status"],
                    "email": orders[order_request.order_id]["email"],
                    "address": orders[order_request.order_id]["adress"]
                }
        else:
            return f"Could not find an order with id {order_request.order_id}"

    # Here we could have different tools      
    #elif intent.intent == "query_products": 
    #    return None
    
    elif intent.intent == "unsupported": 
        return f"Request is currently not supported: {messages[-1]}"
    
    else:
        return None

# Function to build a friendly AI response
def bake_response(messages):
    response, _ = opper.call(
        name="generate_response",
        instructions="Generate a helpful, friendly but brief response to the user's message in the conversation.",
        input={"messages": messages},
        output_type=str,
        # stream=True
    )
    return response

@trace
def run():
    # Here we have a conversatonal loop of user, function and assistant messages
    messages = []

    while True:
        
        # We make each queston/response a turn
        with opper.traces.start(name="on_message") as message_trace: 

            # Get user input
            user_input = input("User: ")
            if user_input.lower() == "quit":
                break

            messages.append({
                "role": "user",
                "content": user_input
            })

            # Analyse the conversation and return an analysis
            analysis = process_message(messages)

            messages.append({
                "role": "function",
                "content": analysis
            })

            # Bake response to the user 
            response = bake_response(messages)

            print(f"Assistant: {response}")

            messages.append({
                "role": "assistant",
                "content": response
            })
            
            # We set the trace input and end output values to make them nice to look at
            message_trace.update(input = user_input, output = response)
            
            # Here we could add thumbs up on this response 
            # message_trace.save_metric("thumbs_up", 1, "User pressed thumbs up button"))

if __name__ == "__main__":
    run()

Observing sessions with traces

The tracing capabilities allow us to monitor and debug the bot’s behavior. Here’s a visualization of a typical conversation trace: