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
from pydantic import BaseModel, Field
from typing import Literal
import os

# Initialize Opper client
opper = Opper(http_bearer=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"
        }
    }

# Input schema for intent classification
class ConversationMessages(BaseModel):
    messages: list[dict] = Field(description="List of conversation messages with role and content")

# Output schema for intent classification
class IntentClassification(BaseModel):
    thoughts: str = Field(description="The thoughts of the model while analyzing the intent")
    intent: Literal["get_order_status", "query_products", "unsupported"] = Field(description="The classified intent of the conversation")

# Function to determine intent of conversation
def determine_intent(messages, span=None):
    result = opper.call(
        name="determine_intent",
        instructions="Analyze the user message and determine their intent. Supported intents are get_order_status and query_products.",
        input_schema=ConversationMessages,
        output_schema=IntentClassification,
        input=ConversationMessages(messages=messages),
        parent_span_id=span.id if span else None
    )
    return result.json_payload
    
# Output schema for order extraction
class ParsedOrder(BaseModel):
    thoughts: str = Field(description="The thoughts of the model while extracting order information")
    order_id: int | None = Field(default=None, description="The order ID if found in the conversation")
    email: str | None = Field(default=None, description="The email address if found in the conversation")

# Function to extract order data from conversation
def extract_order_from_messages(messages, span=None):
    result = opper.call(
        name="extract_order_info",
        instructions="Extract order ID and email from the conversation if present",
        input_schema=ConversationMessages,
        output_schema=ParsedOrder,
        input=ConversationMessages(messages=messages),
        parent_span_id=span.id if span else None
    )
    return result.json_payload

# 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, span=None):

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

    # Process based on intent
    if intent["intent"] == "get_order_status":

        # Extract requested order information
        order_request = extract_order_from_messages(messages, span)

        # 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

# Input schema for response generation
class ResponseGenerationInput(BaseModel):
    messages: list[dict] = Field(description="List of conversation messages with role and content")

# Function to build a friendly AI response
def bake_response(messages, span=None):
    result = opper.call(
        name="generate_response",
        instructions="Generate a helpful, friendly but brief response to the user's message in the conversation.",
        input_schema=ResponseGenerationInput,
        input=ResponseGenerationInput(messages=messages),
        parent_span_id=span.id if span else None
    )
    return result.message

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

    # Create a session span to track the entire conversation
    session_span = opper.spans.create(
        name="conversation_session"
    )

    while True:
        
        # Start a span for this message turn (child of session span)
        message_span = opper.spans.create(
            name="on_message",
            parent_id=session_span.id
        )

        # 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, message_span)

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

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

        print(f"Assistant: {response}")

        messages.append({
            "role": "assistant",
            "content": response
        })
        
        # Update the message span with input and output values
        opper.spans.update(
            span_id=message_span.id,
            input=user_input,
            output=response
        )
        
        # Here we could add thumbs up on this response 
        # opper.span_metrics.create_metric(
        #     span_id=message_span.id,
        #     dimension="thumbs_up", 
        #     value=1, 
        #     comment="User pressed thumbs up button"
        # )

    # Update the session span with final conversation summary
    opper.spans.update(
        span_id=session_span.id,
        input=f"Conversation started with {len(messages)} total messages",
        output=f"Conversation completed with {len(messages)} total messages",
        meta={"total_messages": len(messages)}
    )

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: Tracing a chatbot with tools