{ "cells": [ { "cell_type": "markdown", "id": "11912be9159cc341", "metadata": {}, "source": [ "# Support Ticket Routing\n", "\n", "This recipe shows how to take an incoming support ticket/call transcript then use an LLM to summarize the issue and route it to the correct person.\n", "\n", "
\n", "

Mirascope Concepts Used

\n", "\n", "
\n", "\n", "
\n", "

Background

\n", "

\n", "Traditional machine learning techniques like text classification were previously used to solve routing. LLMs have enhanced routing by being able to better interpret nuances of inquiries as well as using client history and knowledge of the product to make more informed decisions.\n", "

\n", "
" ] }, { "cell_type": "markdown", "id": "0456fe8c", "metadata": {}, "source": [ "## Setup\n", "\n", "Let's start by installing Mirascope and its dependencies:" ] }, { "cell_type": "code", "execution_count": null, "id": "8b23208b", "metadata": {}, "outputs": [], "source": [ "!pip install \"mirascope[openai]\"" ] }, { "cell_type": "code", "execution_count": null, "id": "162ed462", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "os.environ[\"OPENAI_API_KEY\"] = \"YOUR_API_KEY\"\n", "# Set the appropriate API key for the provider you're using" ] }, { "cell_type": "markdown", "id": "5d09395b", "metadata": {}, "source": [ "## Imitating a Company's Database/Functionality\n", "\n", "
\n", "

Fake Data

\n", "

\n", "For both privacy and functionality purposes, these data types and functions in no way represent how a company's API should actually look like. However, extrapolate on these gross oversimplifications to see how the LLM would interact with the company's API.\n", "

\n", "
\n", "\n", "### User\n", "\n", "Let’s create a `User` class to represent a customer as well as the function `get_user_by_email()` to imitate how one might search for the user in the database with some identifying information:" ] }, { "cell_type": "code", "execution_count": 1, "id": "773080f97aa5b729", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T06:59:51.869123Z", "start_time": "2024-09-30T06:59:51.777712Z" } }, "outputs": [], "source": [ "from pydantic import BaseModel, Field\n", "\n", "\n", "class User(BaseModel):\n", " name: str\n", " email: str\n", " past_purchases: list[str]\n", " past_charges: list[float]\n", " payment_method: str\n", " password: str\n", " security_question: str\n", " security_answer: str\n", "\n", "\n", "def get_user_by_email(email: str):\n", " if email == \"johndoe@gmail.com\":\n", " return User(\n", " name=\"John Doe\",\n", " email=\"johndoe@gmail.com\",\n", " past_purchases=[\"TV\", \"Microwave\", \"Chair\"],\n", " past_charges=[349.99, 349.99, 99.99, 44.99],\n", " payment_method=\"AMEX 1234 1234 1234 1234\",\n", " password=\"password1!\",\n", " security_question=\"Childhood Pet Name\",\n", " security_answer=\"Piddles\",\n", " )\n", " else:\n", " return None" ] }, { "cell_type": "markdown", "id": "ebd938c4b12f99c9", "metadata": {}, "source": [ "### Data Pulling Functions\n", "\n", "Let’s also define some basic functions that one might expect a company to have for specific situations. `get_sale_items()` gets the items currently on sale, `get_rewards()` gets the rewards currently available to a user, `get_billing_details()` returns user data related to billing, and `get_account_details()` returns user data related to their account.\n", "\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "f07165f66eb13d27", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T06:59:53.874572Z", "start_time": "2024-09-30T06:59:53.869658Z" } }, "outputs": [], "source": [ "def get_sale_items():\n", " return \"Sale items: we have a monitor at half off for $80!\"\n", "\n", "\n", "def get_rewards(user: User):\n", " if sum(user.past_charges) > 300:\n", " return \"Rewards: for your loyalty, you get 10% off your next purchase!\"\n", " else:\n", " return \"Rewards: you have no rewards available right now.\"\n", "\n", "\n", "def get_billing_details(user: User):\n", " return {\n", " \"user_email\": user.email,\n", " \"user_name\": user.name,\n", " \"past_purchases\": user.past_purchases,\n", " \"past_charges\": user.past_charges,\n", " }\n", "\n", "\n", "def get_account_details(user: User):\n", " return {\n", " \"user_email\": user.email,\n", " \"user_name\": user.name,\n", " \"password\": user.password,\n", " \"security_question\": user.security_question,\n", " \"security_answer\": user.security_answer,\n", " }" ] }, { "cell_type": "markdown", "id": "29faaf442d205482", "metadata": {}, "source": [ "\n", "### Routing to Agent\n", "\n", "Since we don’t have an actual endpoint to route to a live agent, let’s use this function `route_to_agent()` as a placeholder:\n", "\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "8964cc3b39949a35", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T06:59:56.027390Z", "start_time": "2024-09-30T06:59:56.024632Z" } }, "outputs": [], "source": [ "from typing import Literal\n", "\n", "\n", "def route_to_agent(\n", " agent_type: Literal[\"billing\", \"sale\", \"support\"], summary: str\n", ") -> None:\n", " \"\"\"Routes the call to an appropriate agent with a summary of the issue.\"\"\"\n", " print(f\"Routed to: {agent_type}\\nSummary:\\n{summary}\")" ] }, { "cell_type": "markdown", "id": "b7cac139b7e138fb", "metadata": {}, "source": [ "\n", "## Handling the Ticket\n", "\n", "To handle the ticket, we will classify the issue of the ticket in one call, then use the classification to gather the corresponding context for a second call.\n", "\n", "### Classify the Transcript\n", "\n", "Assume we have a basic transcript from the customer’s initial interactions with a support bot where they give some identifying information and their issue. We define a Pydantic `BaseModel` schema to classify the issue as well as grab the identifying information. `calltype` classifies the transcript into one of the three categories `billing`, `sale`, and `support`, and `user_email` will grab their email, assuming that’s what the bot asks for. The `reasoning` field will not be used, but forcing the LLM to give a reasoning for its classification choice aids in extraction accuracy, which can be shaky:\n", "\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "82f53c7aec820197", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T07:00:01.006858Z", "start_time": "2024-09-30T07:00:00.995579Z" } }, "outputs": [], "source": [ "class CallClassification(BaseModel):\n", " calltype: Literal[\"billing\", \"sale\", \"support\"] = Field(\n", " ...,\n", " description=\"\"\"The classification of the customer's issue into one of the 3: \n", " 'billing' for an inquiry about charges or payment methods,\n", " 'sale' for making a purchase,\n", " 'support' for general FAQ or account-related questions\"\"\",\n", " )\n", " reasoning: str = Field(\n", " ...,\n", " description=\"\"\"A brief description of why the customer's issue fits into the\\\n", " chosen category\"\"\",\n", " )\n", " user_email: str = Field(..., description=\"email of the user in the chat\")" ] }, { "cell_type": "markdown", "id": "1b228e89ea36678", "metadata": {}, "source": [ "\n", "And we can extract information into this schema with the call `classify_transcript()`:\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "bdd1d596591e948b", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T07:00:04.018904Z", "start_time": "2024-09-30T07:00:02.968848Z" } }, "outputs": [], "source": [ "from mirascope.core import openai, prompt_template\n", "\n", "\n", "@openai.call(model=\"gpt-4o-mini\", response_model=CallClassification)\n", "@prompt_template(\n", " \"\"\"\n", " Classify the following transcript between a customer and the service bot:\n", " {transcript}\n", " \"\"\"\n", ")\n", "def classify_transcript(transcript: str): ..." ] }, { "cell_type": "markdown", "id": "bbec8f553f8b68e7", "metadata": {}, "source": [ "### Provide Ticket-Specific Context\n", "\n", "Now, depending on the output of `classify_transcript()`, we would want to provide different context to the next call - namely, a `billing` ticket would necessitate the details from `get_billing_details()`, a `sale` ticket would want the output of `get_sale_items()` and `get_rewards()`, and a `support_ticket` would require `get_account_details`. We define a second call `handle_ticket()` which calls `classify_transcript()` and calls the correct functions for the scenario via dynamic configuration:\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "f41f41fddfd046bb", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T07:00:11.667320Z", "start_time": "2024-09-30T07:00:11.663072Z" } }, "outputs": [], "source": [ "@openai.call(model=\"gpt-4o-mini\", tools=[route_to_agent])\n", "@prompt_template(\n", " \"\"\"\n", " SYSTEM:\n", " You are an intermediary between a customer's interaction with a support chatbot and\n", " a real life support agent. Organize the context so that the agent can best\n", " facilitate the customer, but leave in details or raw data that the agent would need\n", " to verify a person's identity or purchase. Then, route to the appropriate agent.\n", "\n", " USER:\n", " {context}\n", " \"\"\"\n", ")\n", "def handle_ticket(transcript: str) -> openai.OpenAIDynamicConfig:\n", " context = transcript\n", " call_classification = classify_transcript(transcript)\n", " user = get_user_by_email(call_classification.user_email)\n", " if isinstance(user, User):\n", " if call_classification.calltype == \"billing\":\n", " context += str(get_billing_details(user))\n", " elif call_classification.calltype == \"sale\":\n", " context += get_sale_items()\n", " context += get_rewards(user)\n", " elif call_classification.calltype == \"support\":\n", " context += str(get_account_details(user))\n", " else:\n", " context = \"This person cannot be found in our system.\"\n", "\n", " return {\"computed_fields\": {\"context\": context}}" ] }, { "cell_type": "markdown", "id": "a0300b3ed3020a4e", "metadata": {}, "source": [ "\n", "And there you have it! Let’s see how `handle_ticket` deals with each of the following transcripts:\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "96ea4e5c00b26d99", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T07:00:25.192515Z", "start_time": "2024-09-30T07:00:14.551214Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Routed to: billing\n", "Summary:\n", "Customer John Doe (email: johndoe@gmail.com) is requesting a refund for a double charge for a TV purchase made a week ago. The customer shows two charges of $349.99 on their bank statement.\n", "Routed to: sale\n", "Summary:\n", "Customer johndoe@gmail.com is interested in purchasing a new monitor and wants to know about discounts. There is a monitor available at half off for $80 and the customer is eligible for an additional 10% off for loyalty rewards.\n", "Routed to: support\n", "Summary:\n", "Customer John Doe (johndoe@gmail.com) forgot their site password and is locked out of their email. They are asking for alternative ways to verify their identity. Security question: Childhood Pet Name, Answer: Piddles.\n" ] } ], "source": [ "billing_transcript = \"\"\"\n", "BOT: Please enter your email.\n", "CUSTOMER: johndoe@gmail.com\n", "BOT: What brings you here today?\n", "CUSTOMER: I purchased a TV a week ago but the charge is showing up twice on my bank \\\n", "statement. Can I get a refund?\n", "\"\"\"\n", "\n", "sale_transcript = \"\"\"\n", "BOT: Please enter your email.\n", "CUSTOMER: johndoe@gmail.com\n", "BOT: What brings you here today?\n", "CUSTOMER: I'm looking to buy a new monitor. Any discounts available?\n", "\"\"\"\n", "\n", "support_transcript = \"\"\"\n", "BOT: Please enter your email.\n", "CUSTOMER: johndoe@gmail.com\n", "BOT: What brings you here today?\n", "CUSTOMER: I forgot my site password and I'm also locked out of my email, how else can I\n", "verify my identity?\n", "\"\"\"\n", "\n", "for transcript in [billing_transcript, sale_transcript, support_transcript]:\n", " response = handle_ticket(transcript)\n", " if tool := response.tool:\n", " tool.call()" ] }, { "cell_type": "markdown", "id": "cd88f367ba246229", "metadata": {}, "source": [ "\n", "
\n", "

Additional Real-World Examples

\n", "\n", "
\n", "\n", "When adapting this recipe to your specific use-case, consider the following:\n", "\n", " - Update the `response_model` to more accurately reflect your use-case.\n", " - Implement Pydantic `ValidationError` and Tenacity `retry` to improve reliability and accuracy.\n", " - Evaluate the quality of extraction by using another LLM to verify classification accuracy.\n", " - Use a local model like Ollama to protect company or other sensitive data.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 5 }