""" 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