Answering customer service automatically

Hi,

I want to create a service that reads periodically the open tickets in our customer service system, and answer it.

The closest post I find about it is: /t/fine-tuning-a-model-for-customer-service-for-our-specific-app/29376/15

I provide my current proof of concept below.

Some questions:

  1. Should I use the fine-tuning described here: OpenAI Platform - with a lot of example questions and answers
  2. Or should I use embeddings? - have now researched it yet, a bit unclear to me why, have to read up :nerd_face:
  3. How much information should be in the system prompt Vs. user?
  4. When I use the chat API, should i split up messages in the messages parameter, right now I concatenated messages of the current ticket + information about the customer’s order into one single user prompt
  5. What other improvements suggestions do you guys have? :slight_smile:
  6. Is it correct to use this API for this use case?

My idea is to provide all our customer service policies + order information + conversation (and maybe all tickets in the past few days from this customer for context), and then answer and tell the script which actions to do (cancelling order etc.)

import json
import os
from time import sleep

import openai
import requests

from shopify_utils import get_store


class OpenAI:
    def __init__(self):
        self.api_key = os.environ["OPENAI_API_KEY"]
        self.standard_system_prompt = """
        You are a customer service agent for clothing brand selling online. You are answering on email, 
        instagram direct and facebook messenger. 
        
        Format of response should be json:
        {
         "action": <action>
        "message_to_customer": <message> or "" if no message
        "internal_note": <note> 
        }
        
        Always include message to customer and an "internal_note" which explains the question and why you did answer as you did. 
        
        Actions: 
        
        "close ticket"
        "cancel order"
        "block order"
        "no action"
        
        Policies:
        * If a customer has returned an item, we will refund the item minus 59 kr. But, if they have placed a new order
        after making the return, they get a full refund on the products they return. 
        """
        self.examples = """
        * How to return? 
        
        Hei,

        Vi har skrevet utfyllende om retur her: ... 
        
        Der finner du skjemaet som skal fylles ut, og info om hvordan returen/byttet gjennomføres.
        
        Si ifra om det ikke skulle være tilstrekkelig informasjon på siden.
        
        
        * I have a complaint (my leggings is broken or similar)
        
        Hei, 

        Beklager at du har hatt en så uheldig opplevelse med plagget.
        
        Vi har et reklamasjon-skjema som du kan fylle ut her: 
        
        Vi vil kontakte deg så fort vi har gått gjennom reklamasjonen. 
  
       
        
        * Wants to change order or wrote the wrong address
        
        block order and tell them to make a new order with the correct address.
 
        
        * If there is a long ongoing conversation with an agent
        
        just do nothing "no action". Then it is too complicated for AI to handle in some cases, unless 
        there is a clear thing to be said or the conversation can be closed because there is nothing more to do 
        
        
        * Customer asks if she an amount will be deducted from her refund if she places a new order after returning 
        
        Check order information above, if she has placed new order, tell her she gets a full refund for all items. 
         
        
        Typical spam:
        
        * Marketers on instagram: 
        
        Hello there! This is Eva, reaching out on behalf of @sultanslacks . We saw your profile and it’s so interesting...

        * comment on photos: usually just bots commenting on photos with emojis and "love your content" etc.
        𝑆𝐸𝑁𝐷 𝑖𝑡 𝑜𝑛 👉@oslo.fam_😍
        or
        Dm it on @world_.of._.travel
        
        * From noreply emails
        
        Just close the ticket, no message to customer.        
        
        """

    def get_answer(self, message):
        if len(message) > 15000:
            message = message[:15000]
        openai.api_key = self.api_key
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-16k",
            messages=[
                {
                    "role": "system",
                    "content": self.standard_system_prompt
                },
                {
                    "role": "user",
                    "content": message
                }
            ]
        )
        return response.choices[0].message['content']


class Gorgias:
    def __init__(self):
        self.api_username = os.environ["GORGIAS_API_USERNAME"]
        self.api_token = os.environ["GORGIAS_API_TOKEN"]
        self.subdomain = os.environ["GORGIAS_SUBDOMAIN"]

    def get_tickets(self, view_id=1263608):
        tickets = []
        next_cursor = None
        while True:
            params = {"limit": 30,
                      "order_by": "created_datetime:desc",
                      "view_id": view_id
                      }
            if next_cursor:
                params['cursor'] = next_cursor
            response = requests.get(
                f"https://{self.subdomain}.gorgias.com/api/tickets",
                auth=(self.api_username, self.api_token),
                params=params
            )
            data = response.json()
            tickets.extend(data['data'])
            next_cursor = data.get('meta', {}).get('next_cursor')
            if not next_cursor:
                break
        return tickets

    def get_messages(self, ticket_id):
        response = requests.get(
            f"https://{self.subdomain}.gorgias.com/api/tickets/{ticket_id}/messages",
            auth=(self.api_username, self.api_token)
        )
        messages_data = response.json()['data']

        if not messages_data:  # No messages, seems to be come in if there is a started, but not started insta conversation
            return None

        if messages_data[0]['subject'] is not None:
            message_text = "Subject: " + messages_data[0]['subject'] + "\n\n"
        else:
            message_text = ""

        if messages_data[0]['channel'] is not None:
            message_text += "Channel: " + messages_data[0]['channel'] + "\n\n"

        for message in messages_data:
            if message['from_agent']:
                sender = "Agent"
            else:
                sender = "Customer"
            if message['body_text'] is not None:
                message_text += sender + ": " + message['body_text'] + "\n\n"
        return message_text

    def post_reply(self, ticket_id, message):
        data = {
            "body_text": message,  # assuming the message is in text format
            "from_agent": True,
            "channel": "email",  # replace with the appropriate channel
            "source": {
                "via": "email",  # replace with the appropriate value
            },
            "via": "email",  # replace with the appropriate value
            "sender": {
                "name": "Your Agent Name",  # replace with the appropriate value
                "email": "agent@example.com"  # replace with the appropriate value
            },
            # if needed, include receiver details
            # "receiver": {
            #     "name": "Customer Name",  # replace with the customer name
            #     "email": "customer@example.com"  # replace with the customer email
            # }
        }
        response = requests.post(
            f"https://{self.subdomain}.gorgias.com/api/tickets/{ticket_id}/messages",
            auth=(self.api_username, self.api_token),
            json=data
        )
        return response.json()

    def add_internal_note(self, ticket_id, note):
        data = {
            "body_text": note,
            "from_agent": True,
            "channel": "internal-note",
            "source": {
                "via": "chat",  # replace with appropriate value
            },
            "via": "chat",  # replace with appropriate value
            "sender": {
                "name": "AI customer service",
                "email": ""  # has to be one of the agents
            },
            "sent_datetime": "2023-06-18T00:00:00Z"  # replace with current date time in ISO 8601 format
        }
        response = requests.post(
            f"https://{self.subdomain}.gorgias.com/api/tickets/{ticket_id}/messages",
            auth=(self.api_username, self.api_token),
            json=data
        )
        return response.json()

    def close_ticket(self, ticket_id):
        data = {"status": "closed"}
        response = requests.put(
            f"https://{self.subdomain}.gorgias.com/api/tickets/{ticket_id}",
            auth=(self.api_username, self.api_token),
            json=data
        )
        return response.json()

    def delete_ticket(self, ticket_id):
        response = requests.delete(
            f"https://{self.subdomain}.gorgias.com/api/tickets/{ticket_id}",
            auth=(self.api_username, self.api_token)
        )
        return response.status_code  # returns HTTP status code. 204 for success.

    def cleanup_tickets(self, view_id=239088):
        tickets = self.get_tickets(view_id)
        for ticket in tickets:
            print(f"Deleting ticket: {ticket['id']}")  # optional, for logging
            sleep(0.2)  # optional, to avoid rate limiting
            self.delete_ticket(ticket['id'])


def auto_answer_open_tickets():
    all_open_view_id = 1263608
    tickets = gorgias.get_tickets(all_open_view_id)
    for ticket in tickets:
        latest_messages = gorgias.get_messages(ticket['id'])

        if "AI suggestion" in latest_messages:
            print("AI suggestion already sent, skipping.")
            continue

        if latest_messages is None:
            gorgias.close_ticket(ticket['id'])
            continue

        customer_email = ticket['customer']['email']
        order_info = ""
        if customer_email:
            orders = store.get_latest_orders(customer_email)
            if orders is not None:
                for order in orders:
                    order_info += f"\nOrder ID: {order['id']}\nOrder Name: {order['name']}\n"
                    order_info += f"Shipping Address: {order['billing_address']['address1']}, {order['billing_address']['city']}, {order['billing_address']['zip']}, {order['billing_address']['country']}\n"
                    order_info += f"Cancelled At: {order['cancelled_at']}\nCreated At: {order['created_at']}\nSubtotal Price: {order['current_subtotal_price']}\nFulfillment Status: {order['fulfillment_status']}\nPayment Gateway: {order['gateway']}\n"
                    for item in order['line_items']:
                        order_info += f"Item: {item['name']}, Quantity: {item['quantity']}\n"
                    if order.get('refunds'):
                        for refund in order['refunds']:
                            order_info += f"Refund Created At: {refund['created_at']}\n"
                            for refund_line_item in refund['refund_line_items']:
                                order_info += f"Refunded Item: {refund_line_item['line_item']['name']}, Quantity: {refund_line_item['quantity']}, amount: {refund_line_item['subtotal']} \n"

            else:
                print("No orders found for this email address.")

        # In the future, add more ticket metadata for better responses.

        ticket_information = f"""
        Customer email: {customer_email}
        Latest order(s) for customer, use the information here to answer customer: {order_info} 
        
        Latest Messages:\n{latest_messages}
        
        """

        ai_reply = openAI.get_answer(ticket_information)
        ai_reply_dict = json.loads(ai_reply)

        # For now, we just put an internal note telling with the AI would do and continue,
        # If we see it does the correct thing, we can add the actual actions later.
        gorgias.add_internal_note(
            ticket['id'],
            f"AI suggestion: {json.dumps(ai_reply_dict, indent=2, ensure_ascii=False)}"
        )

        continue

        action = ai_reply_dict["action"]
        message_to_customer = ai_reply_dict["message_to_customer"]
        internal_note = ai_reply_dict["internal_note"]

        if internal_note:
            print(f"internal_note: {internal_note}")
            gorgias.add_internal_note(ticket['id'], internal_note)

        # Perform actions based on AI reply
        if action == "close ticket":
            gorgias.close_ticket(ticket['id'])
        elif action == "cancel order":
            # call your cancel_order function
            pass
        elif action == "block order":
            # call your block_order function
            pass

        # If there's a message to send, send it
        if message_to_customer:
            gorgias.post_reply(ticket['id'], message_to_customer)


if __name__ == "__main__":
    gorgias = Gorgias()
    openAI = OpenAI()
    store = get_store()

    print("Start answering tickets:")
    auto_answer_open_tickets()
    print("Start cleanup:")
    gorgias.cleanup_tickets(93801)  # spam view
    gorgias.cleanup_tickets(239088)  # old tickets view