diff --git a/README.md b/README.md new file mode 100644 index 0000000..20dd28f --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Rasa Pro CI/CD Example + diff --git a/actions/__init__.py b/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/actions/action_template.py b/actions/action_template.py new file mode 100644 index 0000000..b5d4667 --- /dev/null +++ b/actions/action_template.py @@ -0,0 +1,27 @@ +# This files contains your custom actions which can be used to run +# custom Python code. +# +# See this guide on how to implement these action: +# https://rasa.com/docs/rasa-pro/concepts/custom-actions + + +# This is a simple example for a custom action which utters "Hello World!" + +# from typing import Any, Text, Dict, List +# +# from rasa_sdk import Action, Tracker +# from rasa_sdk.executor import CollectingDispatcher +# +# +# class ActionHelloWorld(Action): +# +# def name(self) -> Text: +# return "action_hello_world" +# +# def run(self, dispatcher: CollectingDispatcher, +# tracker: Tracker, +# domain: Dict[Text, Any]) -> List[Dict[Text, Any]]: +# +# dispatcher.utter_message(text="Hello World!") +# +# return [] diff --git a/actions/add_contact.py b/actions/add_contact.py new file mode 100644 index 0000000..bef564d --- /dev/null +++ b/actions/add_contact.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List, Text + +from rasa_sdk import Action, Tracker +from rasa_sdk.events import SlotSet +from rasa_sdk.executor import CollectingDispatcher + +from actions.db import add_contact, get_contacts, Contact + + +class AddContact(Action): + def name(self) -> str: + return "add_contact" + + def run( + self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[str, Any] + ) -> List[Dict[Text, Any]]: + contacts = get_contacts(tracker.sender_id) + name = tracker.get_slot("add_contact_name") + handle = tracker.get_slot("add_contact_handle") + + if name is None or handle is None: + return [SlotSet("return_value", "data_not_present")] + + existing_handles = {c.handle for c in contacts} + if handle in existing_handles: + return [SlotSet("return_value", "already_exists")] + + new_contact = Contact(name=name, handle=handle) + add_contact(tracker.sender_id, new_contact) + return [SlotSet("return_value", "success")] diff --git a/actions/db.py b/actions/db.py new file mode 100644 index 0000000..fa95f1e --- /dev/null +++ b/actions/db.py @@ -0,0 +1,57 @@ +import os +import shutil +import tempfile +from typing import Any, List + +from pydantic import BaseModel + +from rasa.nlu.utils import write_json_to_file +from rasa.shared.utils.io import read_json_file + +ORIGIN_DB_PATH = "db" +CONTACTS = "contacts.json" + + +class Contact(BaseModel): + name: str + handle: str + + +def get_session_db_path(session_id: str) -> str: + tempdir = tempfile.gettempdir() + project_name = "calm_starter" + return os.path.join(tempdir, project_name, session_id) + + +def prepare_db_file(session_id: str, db: str) -> str: + session_db_path = get_session_db_path(session_id) + os.makedirs(session_db_path, exist_ok=True) + destination_file = os.path.join(session_db_path, db) + if not os.path.exists(destination_file): + origin_file = os.path.join(ORIGIN_DB_PATH, db) + shutil.copy(origin_file, destination_file) + return destination_file + + +def read_db(session_id: str, db: str) -> Any: + db_file = prepare_db_file(session_id, db) + return read_json_file(db_file) + + +def write_db(session_id: str, db: str, data: Any) -> None: + db_file = prepare_db_file(session_id, db) + write_json_to_file(db_file, data) + + +def get_contacts(session_id: str) -> List[Contact]: + return [Contact(**item) for item in read_db(session_id, CONTACTS)] + + +def add_contact(session_id: str, contact: Contact) -> None: + contacts = get_contacts(session_id) + contacts.append(contact) + write_db(session_id, CONTACTS, [c.dict() for c in contacts]) + + +def write_contacts(session_id: str, contacts: List[Contact]) -> None: + write_db(session_id, CONTACTS, [c.dict() for c in contacts]) diff --git a/actions/list_contacts.py b/actions/list_contacts.py new file mode 100644 index 0000000..27975a2 --- /dev/null +++ b/actions/list_contacts.py @@ -0,0 +1,22 @@ +from typing import Any, Dict, List, Text + +from rasa_sdk import Action, Tracker +from rasa_sdk.events import SlotSet +from rasa_sdk.executor import CollectingDispatcher + +from actions.db import get_contacts + + +class ListContacts(Action): + def name(self) -> str: + return "list_contacts" + + def run( + self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[str, Any] + ) -> List[Dict[Text, Any]]: + contacts = get_contacts(tracker.sender_id) + if len(contacts) > 0: + contacts_list = "".join([f"- {c.name} ({c.handle}) \n" for c in contacts]) + return [SlotSet("contacts_list", contacts_list)] + else: + return [SlotSet("contacts_list", None)] diff --git a/actions/remove_contact.py b/actions/remove_contact.py new file mode 100644 index 0000000..51fe40c --- /dev/null +++ b/actions/remove_contact.py @@ -0,0 +1,35 @@ +from typing import Any, Dict, List, Text + +from rasa_sdk import Action, Tracker +from rasa_sdk.events import SlotSet +from rasa_sdk.executor import CollectingDispatcher + +from actions.db import get_contacts, write_contacts + + +class RemoveContact(Action): + def name(self) -> str: + return "remove_contact" + + def run( + self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[str, Any] + ) -> List[Dict[Text, Any]]: + contacts = get_contacts(tracker.sender_id) + handle = tracker.get_slot("remove_contact_handle") + + if handle is not None: + contact_indices_with_handle = [ + i for i, c in enumerate(contacts) if c.handle == handle + ] + if len(contact_indices_with_handle) == 0: + return [SlotSet("return_value", "not_found")] + else: + removed_contact = contacts.pop(contact_indices_with_handle[0]) + write_contacts(tracker.sender_id, contacts) + return [ + SlotSet("return_value", "success"), + SlotSet("remove_contact_name", removed_contact.name), + ] + + else: + return [SlotSet("return_value", "missing_handle")] diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..3f48913 --- /dev/null +++ b/config.yml @@ -0,0 +1,12 @@ +recipe: default.v1 +language: en +pipeline: +- name: SingleStepLLMCommandGenerator + llm: + model_name: gpt-4 + request_timeout: 7 + max_tokens: 256 + +policies: +- name: FlowPolicy +- name: IntentlessPolicy diff --git a/data/flows/add_contact.yml b/data/flows/add_contact.yml new file mode 100644 index 0000000..7a8b8ec --- /dev/null +++ b/data/flows/add_contact.yml @@ -0,0 +1,31 @@ +flows: + add_contact: + description: add a contact to your contact list + name: add a contact + steps: + - collect: "add_contact_handle" + description: "a user handle starting with @" + - collect: "add_contact_name" + description: "a name of a person" + - collect: "add_contact_confirmation" + ask_before_filling: true + next: + - if: "slots.add_contact_confirmation is not true" + then: + - action: utter_add_contact_cancelled + next: END + - else: add_contact + - id: add_contact + action: add_contact + next: + - if: "slots.return_value = 'success'" + then: + - action: utter_contact_added + next: END + - if: "slots.return_value = 'already_exists'" + then: + - action: utter_contact_already_exists + next: END + - else: + - action: utter_add_contact_error + next: END diff --git a/data/flows/list_contacts.yml b/data/flows/list_contacts.yml new file mode 100644 index 0000000..eabee59 --- /dev/null +++ b/data/flows/list_contacts.yml @@ -0,0 +1,14 @@ +flows: + list_contacts: + name: list your contacts + description: show your contact list + steps: + - action: list_contacts + next: + - if: "slots.contacts_list" + then: + - action: utter_list_contacts + next: END + - else: + - action: utter_no_contacts + next: END diff --git a/data/flows/remove_contact.yml b/data/flows/remove_contact.yml new file mode 100644 index 0000000..1c69f59 --- /dev/null +++ b/data/flows/remove_contact.yml @@ -0,0 +1,29 @@ +flows: + remove_contact: + name: remove a contact + description: remove a contact from your contact list + steps: + - collect: "remove_contact_handle" + description: "a contact handle starting with @" + - collect: "remove_contact_confirmation" + ask_before_filling: true + next: + - if: "slots.remove_contact_confirmation is not true" + then: + - action: utter_remove_contact_cancelled + next: END + - else: remove_contact + - id: "remove_contact" + action: remove_contact + next: + - if: "slots.return_value == 'success'" + then: + - action: utter_remove_contact_success + next: END + - if: "slots.return_value == 'not_found'" + then: + - action: utter_contact_not_in_list + next: END + - else: + - action: utter_remove_contact_error + next: END diff --git a/db/contacts.json b/db/contacts.json new file mode 100644 index 0000000..3915c44 --- /dev/null +++ b/db/contacts.json @@ -0,0 +1,10 @@ +[ + { + "name": "Joe", + "handle": "@JoeMyers" + }, + { + "name": "Mary", + "handle": "@MaryLu" + } +] diff --git a/domain/add_contact.yml b/domain/add_contact.yml new file mode 100644 index 0000000..f208e33 --- /dev/null +++ b/domain/add_contact.yml @@ -0,0 +1,39 @@ +version: "3.1" + +actions: + - add_contact + +slots: + add_contact_confirmation: + type: bool + mappings: + - type: from_llm + add_contact_name: + type: text + mappings: + - type: from_llm + add_contact_handle: + type: text + mappings: + - type: from_llm + +responses: + utter_ask_add_contact_confirmation: + - text: Do you want to add {add_contact_name}({add_contact_handle}) to your contacts? + buttons: + - payload: "/SetSlots(add_contact_confirmation=true)" + title: Yes + - payload: "/SetSlots(add_contact_confirmation=false)" + title: No, cancel + utter_ask_add_contact_handle: + - text: What's the handle of the user you want to add? + utter_ask_add_contact_name: + - text: What's the name of the user you want to add? + utter_add_contact_error: + - text: "Something went wrong, please try again." + utter_add_contact_cancelled: + - text: "Okay, I am cancelling this adding of a contact." + utter_contact_already_exists: + - text: "There's already a contact with that handle in your list." + utter_contact_added: + - text: "Contact added successfully." diff --git a/domain/list_contacts.yml b/domain/list_contacts.yml new file mode 100644 index 0000000..6265180 --- /dev/null +++ b/domain/list_contacts.yml @@ -0,0 +1,17 @@ +version: "3.1" + +actions: + - list_contacts + +slots: + contacts_list: + type: text + mappings: + - type: custom + action: list_contacts + +responses: + utter_no_contacts: + - text: "You have no contacts in your list." + utter_list_contacts: + - text: "You currently have the following contacts:\n{contacts_list}" diff --git a/domain/remove_contact.yml b/domain/remove_contact.yml new file mode 100644 index 0000000..59a6e7d --- /dev/null +++ b/domain/remove_contact.yml @@ -0,0 +1,38 @@ +version: "3.1" + +actions: + - remove_contact + +slots: + remove_contact_name: + type: text + mappings: + - type: custom + action: remove_contact + remove_contact_handle: + type: text + mappings: + - type: from_llm + remove_contact_confirmation: + type: text + mappings: + - type: from_llm + +responses: + utter_ask_remove_contact_handle: + - text: What's the handle of the user you want to remove? + utter_contact_not_in_list: + - text: "That contact is not in your list." + utter_remove_contact_error: + - text: "Something went wrong, please try again." + utter_remove_contact_success: + - text: "Removed {remove_contact_handle}({remove_contact_name}) from your contacts." + utter_ask_remove_contact_confirmation: + - buttons: + - payload: "/SetSlots(remove_contact_confirmation=true)" + title: Yes + - payload: "/SetSlots(remove_contact_confirmation=false)" + title: No, cancel the removal + text: "Should I remove {remove_contact_handle} from your contact list?" + utter_remove_contact_cancelled: + - text: "Okay, I am cancelling this removal of a contact." diff --git a/domain/shared.yml b/domain/shared.yml new file mode 100644 index 0000000..93f6fed --- /dev/null +++ b/domain/shared.yml @@ -0,0 +1,10 @@ +version: "3.1" + +slots: + return_value: + type: any + mappings: + - type: custom + action: add_contact + - type: custom + action: remove_contact diff --git a/e2e_tests/cancelations/user_cancels_during_a_correction.yml b/e2e_tests/cancelations/user_cancels_during_a_correction.yml new file mode 100644 index 0000000..9d7d79a --- /dev/null +++ b/e2e_tests/cancelations/user_cancels_during_a_correction.yml @@ -0,0 +1,16 @@ +test_cases: + - test_case: user cancels adding a contact during the correction + steps: + - user: I want to add a new contact + - utter: utter_ask_add_contact_handle + - user: it's @foo + - slot_was_set: + - add_contact_handle: "@foo" + - utter: utter_ask_add_contact_name + - user: Wait, no, the handle is @bar + - slot_was_set: + - add_contact_handle: "@bar" + - utter: utter_corrected_previous_input + - utter: utter_ask_add_contact_name + - user: I changed my mind, stop. + - utter: utter_flow_cancelled_rasa diff --git a/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml b/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml new file mode 100644 index 0000000..220fd40 --- /dev/null +++ b/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml @@ -0,0 +1,7 @@ +test_cases: + - test_case: user changes mind based on new info + steps: + - user: I want to add contact + - utter: utter_ask_add_contact_handle + - user: Stop + - utter: utter_flow_cancelled_rasa diff --git a/e2e_tests/corrections/user_corrects_contact_handle.yml b/e2e_tests/corrections/user_corrects_contact_handle.yml new file mode 100644 index 0000000..c874814 --- /dev/null +++ b/e2e_tests/corrections/user_corrects_contact_handle.yml @@ -0,0 +1,20 @@ +test_cases: + - test_case: user corrects the handle + steps: + - user: I want to add a new contact + - utter: utter_ask_add_contact_handle + - user: it's @foo + - slot_was_set: + - add_contact_handle: "@foo" + - utter: utter_ask_add_contact_name + - user: Wait, no, the handle is @bar + - slot_was_set: + - add_contact_handle: "@bar" + - utter: utter_corrected_previous_input + - utter: utter_ask_add_contact_name + - user: It's Barbar + - slot_was_set: + - add_contact_name: "Barbar" + - utter: utter_ask_add_contact_confirmation + - user: Yes + - utter: utter_contact_added diff --git a/e2e_tests/corrections/user_corrects_contact_name.yml b/e2e_tests/corrections/user_corrects_contact_name.yml new file mode 100644 index 0000000..3836acd --- /dev/null +++ b/e2e_tests/corrections/user_corrects_contact_name.yml @@ -0,0 +1,19 @@ +test_cases: + - test_case: user corrects the handle + steps: + - user: I want to add a new contact + - utter: utter_ask_add_contact_handle + - user: it's @jane + - slot_was_set: + - add_contact_handle: "@jane" + - utter: utter_ask_add_contact_name + - user: It's Jane Bar + - slot_was_set: + - add_contact_name: "Jane Bar" + - user: Wait, it's Jane Foo + - slot_was_set: + - add_contact_name: "Jane Foo" + - utter: utter_corrected_previous_input + - utter: utter_ask_add_contact_confirmation + - user: Yes + - utter: utter_contact_added diff --git a/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml b/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml new file mode 100644 index 0000000..b318972 --- /dev/null +++ b/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml @@ -0,0 +1,15 @@ +test_cases: + - test_case: user adds a contact to their list + steps: + - user: I want to add someone to my contact list + - utter: utter_ask_add_contact_handle + - user: it's @barts + - slot_was_set: + - add_contact_handle: "@barts" + - utter: utter_ask_add_contact_name + - user: just Bart + - slot_was_set: + - add_contact_name: Bart + - utter: utter_ask_add_contact_confirmation + - user: Yes + - utter: utter_contact_added diff --git a/e2e_tests/happy_paths/user_lists_contacts.yml b/e2e_tests/happy_paths/user_lists_contacts.yml new file mode 100644 index 0000000..427a72d --- /dev/null +++ b/e2e_tests/happy_paths/user_lists_contacts.yml @@ -0,0 +1,5 @@ +test_cases: + - test_case: user lists their contacts + steps: + - user: Please show me my contacts + - utter: utter_list_contacts diff --git a/e2e_tests/happy_paths/user_removes_contact.yml b/e2e_tests/happy_paths/user_removes_contact.yml new file mode 100644 index 0000000..89cd7d8 --- /dev/null +++ b/e2e_tests/happy_paths/user_removes_contact.yml @@ -0,0 +1,11 @@ +test_cases: + - test_case: user removes a contact + steps: + - user: I want to remove contact + - utter: utter_ask_remove_contact_handle + - user: "@MaryLu" + - slot_was_set: + - remove_contact_handle: "@MaryLu" + - utter: utter_ask_remove_contact_confirmation + - user: Yes + - utter: utter_remove_contact_success diff --git a/e2e_tests/happy_paths/user_removes_contact_from_list.yml b/e2e_tests/happy_paths/user_removes_contact_from_list.yml new file mode 100644 index 0000000..f1d47a9 --- /dev/null +++ b/e2e_tests/happy_paths/user_removes_contact_from_list.yml @@ -0,0 +1,12 @@ +test_cases: + - test_case: user removes a contact using contact list + steps: + - user: Please show me my contacts + - utter: utter_list_contacts + - utter: utter_can_do_something_else + - user: Remove contact @MaryLu + - slot_was_set: + - remove_contact_handle: "@MaryLu" + - utter: utter_ask_remove_contact_confirmation + - user: yes + - utter: utter_remove_contact_success diff --git a/endpoints.yml b/endpoints.yml new file mode 100644 index 0000000..8b48a56 --- /dev/null +++ b/endpoints.yml @@ -0,0 +1,45 @@ +# This file contains the different endpoints your bot can use. + +# Server where the models are pulled from. +# https://rasa.com/docs/rasa-pro/production/model-storage#fetching-models-from-a-server + +#models: +# url: http://my-server.com/models/default_core@latest +# wait_time_between_pulls: 10 # [optional](default: 100) + +# Server which runs your custom actions. +# https://rasa.com/docs/rasa-pro/concepts/custom-actions + +action_endpoint: + actions_module: "actions" + +# Tracker store which is used to store the conversations. +# By default the conversations are stored in memory. +# https://rasa.com/docs/rasa-pro/production/tracker-stores + +#tracker_store: +# type: redis +# url: +# port: +# db: +# password: +# use_ssl: + +#tracker_store: +# type: mongod +# url: +# db: +# username: +# password: + +# Event broker which all conversation events should be streamed to. +# https://rasa.com/docs/rasa-pro/production/event-brokers + +#event_broker: +# url: localhost +# username: username +# password: password +# queue: queue + +nlg: + type: rephrase