Skip to content

Instantly share code, notes, and snippets.

@krishagel
Created May 1, 2023 18:34
Show Gist options
  • Save krishagel/9a944452ed8e084198e493e98a6bc75f to your computer and use it in GitHub Desktop.
Save krishagel/9a944452ed8e084198e493e98a6bc75f to your computer and use it in GitHub Desktop.
# FreshGPT
# PSD401, written by Mason Pratz
#
# This is a Lambda function that is triggered via API call.
# It is designed to be called from Freshservice Workflow Automator, which passes in
# ticket information and receives script & GPT generated response data from this function.
import openai
import json
import os
import html
import re
openai.api_key = os.getenv('OPENAI_KEY')
# lambda function starts here
def lambda_handler(event, context):
ticket = clean_input_into_json(event['body'])
return process_ticket(ticket)
# input will be a string with escapes, due to how it passes from lambda authorizer in API Gateway
# this function returns it to a dictionary state for ease of use, and ensures the strings
# are formatted properly.
def clean_input_into_json(data):
data = remove_non_printable(data)
data = data.replace(r'\"', '"')
datajson = json.loads(data)
return replace_json_quote_escapes(datajson)
# cleans bad characters from input, can occassionally come through from copy-pastes into
# freshservice tickets.
def remove_non_printable(data):
only_printable = ''.join(list(s for s in data if s.isprintable() and s != '\n'))
return only_printable
# replace quotes into their non-html escaped form (ex. " --> ")
# Needs to be done since Freshservice escapes them in request body.
def replace_json_quote_escapes(datajson):
for key in datajson.keys():
if isinstance(datajson[key], str):
datajson[key] = html.unescape(html.unescape(datajson[key]))
elif isinstance(datajson[key], dict):
datajson[key] = replace_json_quote_escapes(datajson[key])
return datajson
# Retrieves first integer from short string.
def extract_score(response):
score = ''
for i in range(len(response)):
if response[i].isnumeric():
score += response[i]
elif score != '':
break
if score == '':
return -1
return int(score)
# Converts text into a format that will be suitable for HTML display inside Freshservice.
def html_safe(text):
return text.replace("&", "&amp;").replace("\n", "<br>").replace("\t", "&emsp;").replace("'", "&#39;").replace('"', "&quot;")
# Finds all links from <a> tags in raw HTML from description and stores them into a list.
def extract_links(text):
links = []
linksplit = text.split('<a href="')
del linksplit[0]
for i in range(len(linksplit)):
thislink = linksplit[i].split('"')[0]
links.append(thislink)
return links
# Main process. Takes the given ticket data and creates a response to return.
def process_ticket(ticket):
total_tokens = 0 # for calculating OpenAI cost at the end
detail_cutoff = 6 # the lowest score that we will not generate a reply for
# get all links provided in ticket description
links = extract_links(ticket["raw_description"])
links_string = "None"
if len(links) > 0:
links_string = "\n" + "\n".join(links)
# save a version of the description with all HTML tags removed.
ticket['description'] = re.sub('<[^<]+?>', '', ticket['raw_description'])
# these represent ticket information with different levels of detail
ticket_content_basic = f"Subject: {ticket['subject']}\nDescription: {ticket['description']}"
ticket_content = f"Subject: {ticket['subject']}\nDescription: {ticket['description']}\nCategory: {ticket['category']}\nSubcategory: {ticket['subcategory']}\nSubcategory Item: {ticket['subcategory_item']}\nRoom: {ticket['room']}\nSchool: {ticket['school']}\nLinks: {links_string}"
################## SCORING ###################
# Detail Score
msg = [
{"role": "system", "content": "You score help desk tickets based on a metric that is specified to the user prior to sending you the ticket information. You will return a single number between 1 and 10, and then justify your score on a new line, explaining why you gave that specific score. You always return the score first, without any leading text."},
{"role": "user", "content": "Score this ticket based on how much information it has. Please state your score as the first word in your response, and then follow it with your justification/explanation."},
{"role": "assistant", "content": "Waiting for ticket to score."},
{"role": "user", "content": ticket_content},
]
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=msg,
temperature= 0,
top_p= 1,
presence_penalty= 0,
frequency_penalty= 0,
max_tokens= 5
)
total_tokens += completion['usage']['total_tokens']
raw_result = completion['choices'][0]['message']['content']
result = extract_score(raw_result)
ticket["detail_score"] = result
# determine if we meet the requirement skip follow-up questions
if ticket["detail_score"] < detail_cutoff:
met_detail_threshold = False
else:
met_detail_threshold = True
################## ASSESSMENT ###################
# Mood Assessment
msg = [
{"role": "system", "content": "You assess help desk tickets for a school district based on a metric that is specified to the user prior to sending you the ticket information. You will return your answer without prefacing it, and then justify your answer on a new line, explaining why you gave that answer. You always return your answer first, without any leading text."},
{"role": "user", "content": "In one word, describe the requester's mood based on the tone of their writing. You should answer 'Neutral', unless their mood is very obvious. Respond first with your answer (without any leading text). Then in a second sentence, justify your answer."},
{"role": "assistant", "content": "Waiting for ticket to assess."},
{"role": "user", "content": ticket_content_basic},
]
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=msg,
temperature= 0,
top_p= 1,
presence_penalty= 0,
frequency_penalty= 0,
max_tokens= 5
)
total_tokens += completion['usage']['total_tokens']
result = completion['choices'][0]['message']['content']
ticket["mood_assess"] = result.split('.')[0]
################## QUESTIONS ###################
# Questions1
msg = [
{"role": "system", "content": "You are an automated Helpdesk bot for a school district. You will immediately ask 1-3 follow-up questions to get more information for the human technician that will take over after you. Ask as few questions as you can while still being helpful, so only ask 3 questions if all three are very likely to be really helpful. The list of questions will always be in a numbered list format, each question on a new line. After each question, justify your answer inside of brackets with an explanation for why that question is helpful to the technician. The explanation should always be on the same line as the question and inside of brackets, [like this]. You will not greet them, introduce yourself, or preface the questions in any way. You will be given additional guidelines for the questions. You will ONLY return the list questions without any introduction, preface, or prior text. When creating questions, follow these rules:\nYou will never ask for their password.\nYou will never refer them to someone else for help.\nYou will never ask the user to troubleshoot anything related to the server or backend infrastructure.\nNever ask them for model numbers.\nYou will never ask the user to attempt to download or install software."},
{"role": "user", "content": "Please ensure that the questions are things that the ticket creator could reasonably answer. Make sure the questions will help the technician figure out what's wrong. Never ask them to take action, only ask questions to provide the human technician with more information about the issue. Do not introduce yourself or chat, only ask the numbered questions. Fewer questions are better, unless they're crucial."},
{"role": "assistant", "content": "Waiting for ticket to generate follow-up questions."},
{"role": "user", "content": ticket_content},
]
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=msg,
temperature= 0,
top_p= 1,
presence_penalty= 0,
frequency_penalty= 0,
)
total_tokens += completion['usage']['total_tokens']
result = completion['choices'][0]['message']['content']
ticket["questions1"] = result
# Questions2
ENABLE_QUESTIONS2 = False # enable this if we want to potentially generate a second set of questions
ticket["questions2"] = ""
if not met_detail_threshold and ENABLE_QUESTIONS2:
msg.append({"role": "assistant", "content": result})
msg.append({"role": "user", "content": "Please generate 1-3 more. Do not preface them in any way, just reply with your questions."})
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=msg,
temperature= 0,
top_p= 1,
presence_penalty= 1,
frequency_penalty= 1,
)
total_tokens += completion['usage']['total_tokens']
result = completion['choices'][0]['message']['content']
ticket["questions2"] = result
# clean questions1
# necessary because we ask ChatGPT to justify their question in brackets
# this is done to improve question quality
qlist = ticket["questions1"].split("\n")
fixedquestions = []
for q in qlist:
fixedquestions.append(q.split("[")[0])
ticket["questions1"] = "\n".join(fixedquestions)
# clean questions2
# necessary because we ask ChatGPT to justify their question in brackets
# this is done to improve question quality
if ticket["questions2"] != "":
qlist = ticket["questions2"].split("\n")
fixedquestions = []
for q in qlist:
fixedquestions.append(q.split("[")[0])
ticket["questions2"] = "\n".join(fixedquestions)
################## REPLY ###################
# (only if we fail to meet the threshold)
if not met_detail_threshold:
reply_prefix = "Thank you for reaching out to the Peninsula School District!\nWhile you wait for a technician to receive your ticket, please consider answering the following questions. Answering them will likely lead to your issue being resolved more quickly.\n\n"
reply_suffix = "\n\n<i>*The above questions were generated using ChatGPT.</i>"
ticket["reply"] = html_safe(reply_prefix + ticket["questions1"] + reply_suffix)
else:
ticket["reply"] = ""
################## NOTE ###################
# Summary
msg = [
{"role": "system", "content": "You summarize help desk tickets for a school district. You generate a short and informative summary based on a provided ticket. Your summary should only be 1-2 sentences. You will be provided additional instructions prior to receiving the ticket information. You only reply with the summary, do not preface your response. You do not chat, only output summaries."},
{"role": "user", "content": "Please summarize this ticket. Include all relevant details for the technician to understand the requester's problem, while still keeping the summary brief. Limit yourself to two sentences. Only reply with this summary."},
{"role": "assistant", "content": "Waiting for ticket to generate short summary."},
{"role": "user", "content": ticket_content},
]
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=msg,
temperature= 0,
top_p= 1,
presence_penalty= 0,
frequency_penalty= 0,
)
total_tokens += completion['usage']['total_tokens']
result = completion['choices'][0]['message']['content']
ticket["summary"] = result
# Suggestions
msg = [
{"role": "system", "content": "You are an assistant to a Helpdesk Technician for a school district. Your task is to provide a few short, simple, and non-specific suggestions based on a provided ticket. Give brief advice on how the technician should approach solving the ticket, and/or what their next steps should be. You will be provided additional instructions prior to receiving the ticket information."},
{"role": "user", "content": "Please keep your response very brief. Follow-up questions have already been asked separately, do not suggest asking any questions. Write your suggestions as if you are speaking to the technician directly. Assume the technician has all required information, even if it is not included in the provided ticket. Do not suggest that they escalate the ticket."},
{"role": "assistant", "content": "Waiting for ticket to generate guidance."},
{"role": "user", "content": ticket_content},
]
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=msg,
temperature= 0,
top_p= 1,
presence_penalty= 0,
frequency_penalty= 0,
)
total_tokens += completion['usage']['total_tokens']
result = completion['choices'][0]['message']['content']
ticket["suggestions"] = result
# Construct the note string
# Summary
final_note = f'Summary: <i>{ticket["summary"]}</i>\n----------\n'
# Detail Score & Mood Assessment
detail_score = str(ticket["detail_score"]).replace("-1", "Unknown")
if detail_score != "Unknown":
detail_score += "/10"
mood_assess = str(ticket["mood_assess"])
final_note += f'Detail Score: {detail_score}\nMood: {mood_assess}'
# Questions (if applicable)
if ENABLE_QUESTIONS2 or met_detail_threshold:
if ticket["questions2"] == "":
notequestions = ticket["questions1"]
else:
notequestions = ticket["questions2"]
final_note += f"\n<u>Possible Follow-up Questions:</u>\n{notequestions}"
# Suggestions
final_note += f"\n\n<u>AI Suggestions:</u>\n{ticket['suggestions']}\n\n"
# Ticket Links
if links_string != "None":
final_note += f"Included links: {links_string}\n\n"
# AI Disclosure
final_note += "<i>*This note was generated using ChatGPT.</i>"
# Save
ticket["note"] = html_safe(final_note)
################## FINALIZE ###################
# Construct response & return
answer = {}
answer["reply"] = ticket["reply"]
answer["note"] = ticket["note"]
answer["total_tokens"] = total_tokens
answer["total_cost"] = total_tokens*(0.002/1000)
return answer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment