BA-Chatbot/chatbot/actions/actions.py

721 lines
28 KiB
Python
Raw Permalink Normal View History

2023-11-15 14:28:48 +01:00
"""
This file contains a series of custom actions for a Rasa-based chatbot, designed to interact with a question-answering component.
The actions cater to various scenarios including expert searches, module recommendations (WPM Recommendations),
and providing answers related to the Studienprüfungsordnung (examination regulations) and crawled web data from Hochschule Mannheim.
The actions are structured to check slot values for different parameters like reader model, retrieval model, and reranking, allowing for dynamic configuration during runtime.
These actions enable the chatbot to adapt its response strategy based on user input and context.
Additionally, the chatbot includes a feedback mechanism and a function to provide top-k document references, enhancing user interaction and information retrieval effectiveness.
Some actions are equipped with fallback mechanisms to search in alternative indexes if the primary search doesn't yield an answer,
ensuring the bot can still provide relevant information in various situations.
"""
import datetime
from typing import Any, Text, Dict, List
from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.events import FollowupAction, SlotSet, ReminderScheduled
import requests
import json
from .helper.credit_mappings import CREDITS
import os
BACKEND_HOST = os.environ.get("FLASK_HOST", "localhost")
FEEDBACK_BUTTONS = [
{"title": "👍", "payload": "/liked_answer"},
{"title": "👎", "payload": "/disliked_answer"},
]
COULD_NOT_FIND_ANSWER = "Ich konnte keine Antwort finden"
NO_ANSWER = "Keine Antwort"
NO_INFO = "keine Information"
def get_intent_before_last(tracker: Tracker) -> str:
user_events = [event for event in tracker.events if event.get("event") == "user"]
if len(user_events) < 2:
return None
intent_before_last = (
user_events[-2].get("parse_data", {}).get("intent", {}).get("name")
)
return intent_before_last
def get_last_executed_action(tracker: Tracker, domain) -> str:
lastAction = next(
e["name"]
for e in reversed(tracker.events)
if e["event"] == "action"
and "name" in e
and (e["name"] in domain["actions"] or e["name"] in domain["forms"])
and e["name"] != "action_set_reminder"
)
return lastAction
def extract_answer_from_response(response, reader_model):
if reader_model == "GPT":
answer = response.json()["answer"]["choices"][0]["message"]["content"]
else:
answer = response.json()["answer"]["answers"][0]["answer"]
return answer
class ActionGreet(Action):
"""Sends a greeting message to the user and follows up with 'action_listen'."""
def name(self) -> Text:
return "action_greet"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
dispatcher.utter_message(response="utter_greet")
return [FollowupAction("action_listen")]
class ActionGetCreditsForModule(Action):
"""Fetches and responds with credit information for a specific module based on user query."""
def name(self) -> Text:
return "action_get_credits"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
entities = tracker.latest_message["entities"]
print(entities, flush=True)
module = next(
(
entity["value"]
for entity in entities
if "entity" in entity and "module" == entity["entity"]
),
None,
)
print(module, flush=True)
print("entity", flush=True)
if module:
payload = json.dumps({"module": module + "\n"})
headers = {
"Content-Type": "application/json",
}
resp = requests.post(
f"http://{BACKEND_HOST}:8000/get_module_credits",
headers=headers,
data=payload,
)
result = resp.json()
found_credits = None
if result:
print(result, flush=True)
found_module = result[0]
found_title = found_module["meta"]["title"].strip()
if "credits" in found_module["meta"]:
found_credits = found_module["meta"]["credits"]
if not found_credits and module in CREDITS:
found_credits = CREDITS[module] + " credits"
if found_credits:
dispatcher.utter_message(
f"Das Module: **{found_title}** gibt {found_credits} "
)
return []
dispatcher.utter_message(
f"Konnte zu dem Module: {module} keine Informationen finden..."
)
return []
class ActionRecommendModule(Action):
"""Provides module recommendations based on user's interests, career goals, and previous courses."""
def name(self) -> Text:
return "action_recommend_module"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
events = []
semester = tracker.get_slot("semester")
interests = tracker.get_slot("interests")
previous_courses = tracker.get_slot("previous_courses")
future_carrer = tracker.get_slot("future_carrer")
retrieval_method_or_model = (
tracker.get_slot("retrieval_method_or_model") or "ada"
)
rerank = (
tracker.get_slot("rerank")
if tracker.get_slot("rerank") is not None
else False
)
reader_model = tracker.get_slot("reader_model") or "GPT"
payload = json.dumps(
{
"interests": interests,
"future_carrer": future_carrer,
"previous_courses": previous_courses,
"index": "ib",
"retrieval_method_or_model": retrieval_method_or_model,
"recommendation_method": "generate_llm_answer" if reader_model == "GPT" else "generate_farm_reader_answer",
"rerank_retrieved_results": rerank,
}
)
headers = {
"Content-Type": "application/json",
}
resp = requests.post(
f"http://{BACKEND_HOST}:8080/recommend_wpms",
headers=headers,
data=payload,
)
if reader_model =="Bert":
courses = resp.json()
print(courses,'courses')
if courses:
dispatcher.utter_message(
"Basierend auf deinen Interessen kann ich dir folgende Wahlpflichtmodule empfehlen:"
)
title = ""
profs = ""
credits = ""
semester = None
uttered_modules = 0
for course in courses[:3]:
meta = course.get("meta", {})
is_wmp = meta.get("is_wpm")
if not is_wmp:
continue
title = meta.get("name_de")
description = meta.get("inhalte_de")
credits = meta.get("credits")
profs = meta.get("dozenten")
semester = meta.get("semester")
if uttered_modules > 3:
break
if (
semester
and semester in ["6", "7", "6/7"]
and title.strip()
not in ["Bachelorarbeit (BA)", "Wissenschaftliches Arbeiten (WIA)"]
):
dispatcher.utter_message(
text=f"**Titel** : {title}\n **ECTS**: {credits}\n **Beschreibung**:\n{description}\n\n **Dozenten**:{profs}"
)
uttered_modules += 1
if uttered_modules == 0:
dispatcher.utter_message(
"Basierend auf deinen Anfragen konnte ich leider keine Wahlpflichtmodule finden"
)
else:
dispatcher.utter_message(text="Haben dir die Empfehlungen gefallen?", buttons= FEEDBACK_BUTTONS )
else:
answer = resp.json()["answer"]["choices"][0]["message"]["content"]
events.append(SlotSet("wpm_recommendation_answer", answer))
dispatcher.utter_message(text=answer, buttons=FEEDBACK_BUTTONS)
return events
class ActionAnswerStupo(Action):
"""Finds and provides answers related to the Studienprüfungsordnung based on the user's query."""
def name(self) -> Text:
return "action_answer_stupo"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
url = f"http://{BACKEND_HOST}:8080/get_answer"
retrieval_method_or_model = (
tracker.get_slot("retrieval_method_or_model") or "ada"
)
reader_model = tracker.get_slot("reader_model") or "GPT"
rerank = (
tracker.get_slot("rerank")
if tracker.get_slot("rerank") is not None
else False
)
events = []
buttons = []
latest_message = tracker.latest_message
index = "stupo"
user_text = None
if latest_message:
user_text = latest_message["text"]
payload = {
"query": user_text,
"index": index,
"retrieval_method_or_model": retrieval_method_or_model,
"reader_model": reader_model,
"rerank_documents": rerank,
}
headers = {
"Content-Type": "application/json",
"Authorization": "Basic Og==",
}
response = requests.request(
"POST", url, headers=headers, data=json.dumps(payload)
)
buttons.extend(FEEDBACK_BUTTONS)
buttons.append(
{"title": "Liste die Referenzen auf.", "payload": "/ask_for_references"}
)
answer = extract_answer_from_response(
reader_model=reader_model, response=response
)
if answer is not None and (
COULD_NOT_FIND_ANSWER in answer
or NO_ANSWER in answer
or NO_INFO in answer
):
dispatcher.utter_message(
"Ich konnte keine Antwort in der Studienprüfungsordnung finden..."
)
dispatcher.utter_message(
"Ich suche nun nach Informationen auf den Hochschulseiten..."
)
index = "crawled_hsma"
payload["index"] = index
response = requests.request(
"POST", url, headers=headers, data=json.dumps(payload)
)
answer = extract_answer_from_response(
reader_model=reader_model, response=response
)
dispatcher.utter_message(
text=answer,
buttons=buttons,
)
events.append(SlotSet("query_stupo", user_text))
events.append(SlotSet("answer_stupo", answer))
events.append(
SlotSet("retrieval_method_or_model", retrieval_method_or_model)
)
events.append(SlotSet("reader_model", reader_model))
events.append(SlotSet("last_searched_index", index))
references = response.json().get("documents")
if references:
events.append(SlotSet("references", references))
return events
class ActionAskAboutCrawledHSMAData(Action):
"""Addresses general inquiries by searching through crawled data from the Hochschule Mannheim website."""
def name(self) -> Text:
return "action_ask_about_crawled_hsma_data"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
url = f"http://{BACKEND_HOST}:8080/get_answer"
latest_message = tracker.latest_message
retrieval_method_or_model = (
tracker.get_slot("retrieval_method_or_model") or "ada"
)
reader_model = tracker.get_slot("reader_model") or "GPT"
rerank = (
tracker.get_slot("rerank")
if tracker.get_slot("rerank") is not None
else False
)
user_text = None
index = "crawled_hsma"
events = []
buttons = []
if latest_message:
user_text = latest_message["text"]
payload = {
"query": user_text,
"index": index,
"retrieval_method_or_model": retrieval_method_or_model,
"reader_model": reader_model,
"rerank_documents": rerank,
}
headers = {
"Authorization": "Basic Og==",
"Content-Type": "application/json",
}
response = requests.request(
"POST", url, headers=headers, data=json.dumps(payload)
)
references = response.json().get("documents")
if references:
events.append(SlotSet("references", references))
buttons.extend(FEEDBACK_BUTTONS)
buttons.append(
{"title": "Liste die Referenzen auf", "payload": "/ask_for_references"}
)
answer = extract_answer_from_response(
reader_model=reader_model, response=response
)
if answer is not None and (
COULD_NOT_FIND_ANSWER in answer
or NO_ANSWER in answer
or NO_INFO in answer
):
dispatcher.utter_message(
"Ich konnte keine Antwort in auf den Hochschulseiten finden..."
)
dispatcher.utter_message(
"Ich suche nun nach Informationen in der Studienprüfungsordnung..."
)
index = "stupo"
payload["index"] = index
response = requests.request(
"POST", url, headers=headers, data=json.dumps(payload)
)
answer = extract_answer_from_response(
reader_model=reader_model, response=response
)
dispatcher.utter_message(
text=answer,
buttons=buttons,
)
events.append(SlotSet("query_crawled_data", user_text))
events.append(SlotSet("answer_crawled_data", answer))
events.append(
SlotSet("retrieval_method_or_model", retrieval_method_or_model)
)
events.append(SlotSet("reader_model", reader_model))
return events
class ActionProvideReferences(Action):
"""Lists references related to the user's last query from either StuPO or crawled data."""
def name(self) -> Text:
return "action_provide_references"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
references = tracker.get_slot("references")
intent_before_last = get_intent_before_last(tracker=tracker)
header_text = "Hier sind die Referenzen: \n"
if intent_before_last == "stupo_question":
header_text = "Hier sind die Referenzen aus der [StuPO](https://www.hs-mannheim.de/fileadmin/user_upload/hauptseite/pdf/SCS/Satzungen/SPO/Bachelor/230824_SPO_Bachelor.pdf): \n"
# Max length for each reference content
MAX_LEN = 150 # Adjust based on your requirements
markdown_references = []
for ref in references:
content = ref.get("content", "")
meta = ref.get("meta")
truncated_content = (
(content[:MAX_LEN] + "...") if len(content) > MAX_LEN else content
)
# Check if a URL is provided
url = meta.get("url", None)
if url:
markdown_references.append(f"- {truncated_content} [Link]({url})")
else:
markdown_references.append(f"- {truncated_content}")
# Join the references and dispatch them in markdown format
markdown_message = header_text + "\n".join(markdown_references)
dispatcher.utter_message(markdown_message)
return []
class ActionExampleStupoQuestions(Action):
"""Offers example questions related to Studienprüfungsordnung for user guidance."""
def name(self) -> Text:
return "action_provide_stupo_example_questions"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
events = []
buttons = [
{"title": question, "payload": question}
for question in [
"Wie kann man Elternzeit beantragen?",
"Darf ich die Frist der Bachelorthesis verlängern?",
"Welche Vorteile haben Studierende mit Kindern?",
"Wieviele Präsenztage muss ich im Praktischen Studiensemester ableisten?",
"Wie lange werden meine Daten und Prüfungsleistungen von der Hochschule gepeichert?",
]
]
text = "Ich kann versuchen, dir bei inhaltichen Fragen über die StuPo zu helfen.\n Einige Beispielfragen:"
dispatcher.utter_message(
text=text,
buttons=buttons,
)
return events
class ActionExampleGeneralQuestions(Action):
"""Presents sample general questions about Hochschule Mannheim to assist the user."""
def name(self) -> Text:
return "action_provide_general_example_questions"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
buttons = [
{"title": question, "payload": question}
for question in [
"Wo finde ich einen 3D-Drucker?",
"Wo befindet sich der lasercutter?",
"Wie setze ich mein Passwort für das Hochschulportal zurück?",
"An wen kann ich mich wenden, wenn ich mein Passwort und meine zentrale Kennung vergessen habe?",
"Wo befindet sich der Mars-Raum?",
]
]
text = "Ich kann versuchen, dir bei allgemeinen Fragen über die Hochschule zu helfen.\n Einige Beispielfragen:"
dispatcher.utter_message(
text=text,
buttons=buttons,
)
return []
class ActionExpertSearch(Action):
"""Conducts an expert search based on the user's query and provides relevant results."""
def name(self) -> Text:
return "action_expert_search"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
url = f"http://{BACKEND_HOST}:8080/search_experts"
buttons = [*FEEDBACK_BUTTONS]
events = []
query = tracker.get_slot("expert_search_query")
events.append(SlotSet("expert_search_query", query))
events.append(SlotSet("retrieval_method_or_model", "mpnet"))
events.append(SlotSet("reader_model", "GPT"))
events.append(FollowupAction("action_listen"))
retrieval_method_or_model = (
tracker.get_slot("retrieval_method_or_model") or "ada"
)
reader_model = tracker.get_slot("reader_model") or "GPT"
print(reader_model,'reader_model')
rerank = (
tracker.get_slot("rerank")
if tracker.get_slot("rerank") is not None
else False
)
payload = json.dumps(
{
"query": query,
"index": "stupo",
"retriever_model": retrieval_method_or_model,
"generate_answer": True,
"rerank_retrieved_results": rerank,
"search_method": "classic_retriever_reader" if reader_model =="GPT" else "retriever_farm_reader"
}
)
headers = {"Authorization": "Basic Og==", "Content-Type": "application/json"}
response = requests.request("POST", url, headers=headers, data=payload)
if response is not None:
response_json: Dict = response.json()
if reader_model =="Bert":
if response_json:
dispatcher.utter_message(
"Basierend auf deinen Interessen kann ich dir folgende Experten sortiert nach Relevanz empfehlen:"
)
expert = ""
title_work = None
uttered_experts = []
for doc in response_json:
meta = doc.get("meta", {})
expert = meta.get("author")
title = meta.get("title")
description = meta.get("abstract")
if len(uttered_experts) > 3:
break
if (
expert and title and expert not in uttered_experts
):
dispatcher.utter_message(
text= f"**Experte**: {expert}\n **Relevantes Paper** :{title}\n"
)
uttered_experts.append(expert)
if len(uttered_experts) == 0:
dispatcher.utter_message(
"Basierend auf deinen Anfragen konnte ich leider keine Experten finden"
)
else:
dispatcher.utter_message(text="Hat dir die Expertensuche gefallen?", buttons= FEEDBACK_BUTTONS )
return events
answer= extract_answer_from_response(response=response, reader_model=reader_model)
documents = response_json.get("documents")
if answer:
events.append(
SlotSet("expert_search_answer", answer)
)
answer = answer
dispatcher.utter_message(
text=answer,
buttons=buttons,
)
return events
class ActionHandleFeedback(Action):
"""Collects user feedback on the bot's responses and forwards it for processing."""
def name(self) -> Text:
return "action_handle_feedback"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
last_intent = tracker.get_intent_of_latest_message()
last_action = get_last_executed_action(tracker=tracker, domain=domain)
query_stupo = tracker.get_slot("query_stupo")
answer_stupo = tracker.get_slot("answer_stupo")
query_crawled_data = tracker.get_slot("query_crawled_data")
answer_crawled_data = tracker.get_slot("answer_crawled_data")
expert_search_query = tracker.get_slot("expert_search_query")
expert_search_answer = tracker.get_slot("expert_search_answer")
retrieval_method_or_model = tracker.get_slot("retrieval_method_or_model")
future_carrer = tracker.get_slot("future_carrer")
interests = tracker.get_slot("interests")
previous_courses = tracker.get_slot("previous_courses")
reader_model = tracker.get_slot("reader_model")
wpm_recommendation_answer = tracker.get_slot("wpm_recommendation_answer")
last_searched_index = tracker.get_slot("last_searched_index")
payload = {}
if last_action == "action_ask_about_crawled_hsma_data":
payload = json.dumps(
{
"type": "crawled_data",
"user_queston": query_crawled_data,
"provided_answer": answer_crawled_data,
"retrieval_method_or_model": retrieval_method_or_model,
"reader_model": reader_model,
"feedback": last_intent,
"last_searched_index": last_searched_index,
}
)
if last_action == "action_answer_stupo":
payload = json.dumps(
{
"type": "stupo",
"user_queston": query_stupo,
"provided_answer": answer_stupo,
"retrieval_method_or_model": retrieval_method_or_model,
"reader_model": reader_model,
"feedback": last_intent,
"last_searched_index": last_searched_index,
}
)
if last_action == "action_expert_search":
payload = json.dumps(
{
"type": "expert_search",
"user_queston": expert_search_query,
"provided_answer": expert_search_answer,
"retrieval_method_or_model": retrieval_method_or_model,
"reader_model": "GPT",
"feedback": last_intent,
}
)
if last_action == "action_recommend_module":
payload = json.dumps(
{
"type": "wpm_recommendation",
"user_queston": f"future_carrer:{future_carrer}\n interests: {interests}\n previous_courses:{previous_courses}",
"provided_answer": wpm_recommendation_answer,
"reader_model": "GPT",
"retrieval_method_or_model": retrieval_method_or_model,
"feedback": last_intent,
}
)
headers = {
"Content-Type": "application/json",
}
resp = requests.post(
f"http://{BACKEND_HOST}:8080/feedback",
headers=headers,
data=payload,
)
dispatcher.utter_message(text="Vielen Dank für das Feedback!")
dispatcher.utter_message(response="utter_how_can_i_help")
return [FollowupAction("action_listen")]
class ActionSetReminder(Action):
"""Sets a reminder for the user based on their request with a default one-minute delay."""
def name(self) -> Text:
return "action_set_reminder"
async def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
date = datetime.datetime.now() + datetime.timedelta(minutes=1)
entities = tracker.latest_message.get("entities")
reminder = ReminderScheduled(
"EXTERNAL_reminder",
trigger_date_time=date,
entities=entities,
name="my_reminder",
kill_on_user_message=True,
)
return [reminder]
class ActionResetSlots(Action):
"""Resets conversation slots to clear stored values from previous interactions."""
def name(self) -> Text:
return "action_reset_slots"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
events = []
events.append(SlotSet("expert_search_query", None))
events.append(SlotSet("interests", None))
events.append(SlotSet("future_carrer", None))
events.append(SlotSet("previous_courses", None))
events.append(SlotSet("references", None))
return events