From 7b3433f92545f964be75ebb3b03848ef3b1d001f Mon Sep 17 00:00:00 2001 From: pseusys Date: Tue, 21 Nov 2023 10:11:47 +0100 Subject: [PATCH 01/18] examples mmerged into main repo --- .gitignore | 1 + MANIFEST.in | 1 + examples/README.md | 10 + examples/customer_service_bot/.env.example | 2 + examples/customer_service_bot/README.md | 64 +++++ .../Training_intent_catcher.ipynb | 184 +++++++++++++ .../customer_service_bot/bot/api/__init__.py | 0 .../customer_service_bot/bot/api/chatgpt.py | 72 +++++ .../bot/api/intent_catcher.py | 27 ++ .../bot/dialog_graph/__init__.py | 0 .../bot/dialog_graph/conditions.py | 39 +++ .../bot/dialog_graph/consts.py | 9 + .../bot/dialog_graph/processing.py | 123 +++++++++ .../bot/dialog_graph/response.py | 39 +++ .../bot/dialog_graph/script.py | 92 +++++++ examples/customer_service_bot/bot/dockerfile | 10 + .../customer_service_bot/bot/requirements.txt | 6 + examples/customer_service_bot/bot/run.py | 39 +++ examples/customer_service_bot/bot/test.py | 43 +++ .../customer_service_bot/docker-compose.yml | 24 ++ .../intent_catcher/dockerfile | 31 +++ .../intent_catcher/requirements.txt | 7 + .../intent_catcher/server.py | 85 ++++++ .../intent_catcher/test_server.py | 15 ++ .../telegram/.env.example | 1 + .../telegram/README.md | 56 ++++ .../telegram/bot/Dockerfile | 15 ++ .../telegram/bot/dialog_graph/__init__.py | 0 .../telegram/bot/dialog_graph/conditions.py | 26 ++ .../telegram/bot/dialog_graph/responses.py | 51 ++++ .../telegram/bot/dialog_graph/script.py | 40 +++ .../telegram/bot/faq_model/__init__.py | 0 .../bot/faq_model/faq_dataset_sample.json | 6 + .../telegram/bot/faq_model/model.py | 31 +++ .../bot/pipeline_services/__init__.py | 0 .../bot/pipeline_services/pre_services.py | 30 +++ .../telegram/bot/requirements.txt | 2 + .../telegram/bot/run.py | 37 +++ .../telegram/bot/test.py | 61 +++++ .../telegram/docker-compose.yml | 8 + .../web/README.md | 26 ++ .../web/docker-compose.yml | 32 +++ .../web/nginx.conf | 37 +++ .../web/web/Dockerfile | 17 ++ .../web/web/__init__.py | 0 .../web/web/app.py | 50 ++++ .../web/web/bot/__init__.py | 0 .../web/web/bot/dialog_graph/__init__.py | 0 .../web/web/bot/dialog_graph/responses.py | 49 ++++ .../web/web/bot/dialog_graph/script.py | 39 +++ .../web/web/bot/faq_model/__init__.py | 0 .../web/bot/faq_model/faq_dataset_sample.json | 6 + .../web/web/bot/faq_model/model.py | 29 ++ .../web/web/bot/pipeline.py | 23 ++ .../web/web/bot/pipeline_services/__init__.py | 0 .../web/bot/pipeline_services/pre_services.py | 30 +++ .../web/web/bot/test.py | 39 +++ .../web/web/requirements.txt | 5 + .../web/web/static/LICENSE.txt | 8 + .../web/web/static/index.css | 254 ++++++++++++++++++ .../web/web/static/index.html | 33 +++ .../web/web/static/index.js | 105 ++++++++ 62 files changed, 2069 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/customer_service_bot/.env.example create mode 100644 examples/customer_service_bot/README.md create mode 100644 examples/customer_service_bot/Training_intent_catcher.ipynb create mode 100644 examples/customer_service_bot/bot/api/__init__.py create mode 100644 examples/customer_service_bot/bot/api/chatgpt.py create mode 100644 examples/customer_service_bot/bot/api/intent_catcher.py create mode 100644 examples/customer_service_bot/bot/dialog_graph/__init__.py create mode 100644 examples/customer_service_bot/bot/dialog_graph/conditions.py create mode 100644 examples/customer_service_bot/bot/dialog_graph/consts.py create mode 100644 examples/customer_service_bot/bot/dialog_graph/processing.py create mode 100644 examples/customer_service_bot/bot/dialog_graph/response.py create mode 100644 examples/customer_service_bot/bot/dialog_graph/script.py create mode 100644 examples/customer_service_bot/bot/dockerfile create mode 100644 examples/customer_service_bot/bot/requirements.txt create mode 100644 examples/customer_service_bot/bot/run.py create mode 100644 examples/customer_service_bot/bot/test.py create mode 100644 examples/customer_service_bot/docker-compose.yml create mode 100644 examples/customer_service_bot/intent_catcher/dockerfile create mode 100644 examples/customer_service_bot/intent_catcher/requirements.txt create mode 100644 examples/customer_service_bot/intent_catcher/server.py create mode 100644 examples/customer_service_bot/intent_catcher/test_server.py create mode 100644 examples/frequently_asked_question_bot/telegram/.env.example create mode 100644 examples/frequently_asked_question_bot/telegram/README.md create mode 100644 examples/frequently_asked_question_bot/telegram/bot/Dockerfile create mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/__init__.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/faq_model/__init__.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json create mode 100644 examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/pipeline_services/__init__.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/requirements.txt create mode 100644 examples/frequently_asked_question_bot/telegram/bot/run.py create mode 100644 examples/frequently_asked_question_bot/telegram/bot/test.py create mode 100644 examples/frequently_asked_question_bot/telegram/docker-compose.yml create mode 100644 examples/frequently_asked_question_bot/web/README.md create mode 100644 examples/frequently_asked_question_bot/web/docker-compose.yml create mode 100644 examples/frequently_asked_question_bot/web/nginx.conf create mode 100644 examples/frequently_asked_question_bot/web/web/Dockerfile create mode 100644 examples/frequently_asked_question_bot/web/web/__init__.py create mode 100644 examples/frequently_asked_question_bot/web/web/app.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/__init__.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/dialog_graph/__init__.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/faq_model/__init__.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json create mode 100644 examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/pipeline.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/pipeline_services/__init__.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py create mode 100644 examples/frequently_asked_question_bot/web/web/bot/test.py create mode 100644 examples/frequently_asked_question_bot/web/web/requirements.txt create mode 100644 examples/frequently_asked_question_bot/web/web/static/LICENSE.txt create mode 100644 examples/frequently_asked_question_bot/web/web/static/index.css create mode 100644 examples/frequently_asked_question_bot/web/web/static/index.html create mode 100644 examples/frequently_asked_question_bot/web/web/static/index.js diff --git a/.gitignore b/.gitignore index a0f41b0cd..bfe56cd1d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ dbs benchmarks benchmark_results_files.json uploaded_benchmarks +**/.env diff --git a/MANIFEST.in b/MANIFEST.in index 2bf8bfe27..ddf3a5524 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include dff/context_storages/protocols.json exclude makefile recursive-exclude tests * +recursive-exclude examples * recursive-exclude tutorials * recursive-exclude * __pycache__ recursive-exclude * *.py[co] diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..4c02cd119 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,10 @@ +# DFF examples + +This repository contains examples of bots build using [DFF](https://github.com/deeppavlov/dialog_flow_framework) (Dialog Flow Framework). + +The Dialog Flow Framework (DFF) allows you to write conversational services. The service is written by defining a +special dialog graph that describes the behavior of the dialog service. The dialog graph contains the dialog script. +DFF offers a specialized language (DSL) for quickly writing dialog graphs. +You can use it in services such as writing skills for Amazon Alexa, etc., chatbots for social networks, website call centers, etc. + +In this repository, two bots are presented as examples: faq bot and customer service bot. Both bots use Telegram as an interface. diff --git a/examples/customer_service_bot/.env.example b/examples/customer_service_bot/.env.example new file mode 100644 index 000000000..9830af9e2 --- /dev/null +++ b/examples/customer_service_bot/.env.example @@ -0,0 +1,2 @@ +TG_BOT_TOKEN=bot_token +OPENAI_API_TOKEN=openai_api_token \ No newline at end of file diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md new file mode 100644 index 000000000..1b1eb9ad0 --- /dev/null +++ b/examples/customer_service_bot/README.md @@ -0,0 +1,64 @@ +## Description + +### Customer service bot + +Customer service bot built on `DFF`. Uses telegram as an interface. +This bot is designed to answer any type of user questions in a limited business domain (book shop). + +* [DeepPavlov Intent Catcher](#) force is used for intent retrieval. +* [ChatGPT](https://openai.com/pricing#language-models) is used for context based question answering. + +### Intent Catcher + +Intent catcher is a DistilBERT-based classifier for user intent classes. +We use DeepPavlov library for seamless training and inference. +Sample code for training the model can be found in `Training_intent_catcher.ipynb`. +The model is deployed as a separate microservice running at port 4999. + +Service bot interacts with the container via `/respond` endpoint. +The API expects a json object with the dialog history passed as an array and labeled 'dialog_contexts'. Intents will be extracted from the last utterance. + +```json +{ + "dialog_contexts": ["phrase_1", "phrase_2"] +} +``` + +The API responds with a nested array containing `label - score` pairs. + +```json +[["no",0.3393537402153015]] +``` + +Run the intent catcher: +```commandline +docker compose up --build --abort-on-container-exit --exit-code-from intent_client +``` + +## Run the bot + +### Run with Docker & Docker-Compose environment +In order for the bot to work, set the bot token via [.env](.env.example). You should start by creating your own `.env` file: +``` +echo TG_BOT_TOKEN=*** >> .env +echo OPENAI_API_TOKEN=*** >> .env +``` + +Build the bot: +```commandline +docker-compose build +``` +Testing the bot: +```commandline +docker-compose run assistant pytest test.py +``` + +Running the bot: +```commandline +docker-compose run assistant python run.py +``` + +Running in background +```commandline +docker-compose up -d +``` diff --git a/examples/customer_service_bot/Training_intent_catcher.ipynb b/examples/customer_service_bot/Training_intent_catcher.ipynb new file mode 100644 index 000000000..2b8ded7a8 --- /dev/null +++ b/examples/customer_service_bot/Training_intent_catcher.ipynb @@ -0,0 +1,184 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "mount_file_id": "1K8dSq-mrFOR44N6CwDp8WiqVDHdtDHJQ", + "authorship_tag": "ABX9TyP5keJL46m+Vgb5Qj+tw1SA", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + }, + "accelerator": "GPU", + "gpuClass": "standard" + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install --upgrade pip" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "rY6UASGLpmt6", + "outputId": "f44435da-8658-43f2-f56d-e8987663663c" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Requirement already satisfied: pip in /usr/local/lib/python3.10/dist-packages (23.1.2)\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install deeppavlov" + ], + "metadata": { + "id": "RboxW9XRp57X" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "!curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n", + "# The required version of 'tokenizers' library depends on a Rust compiler." + ], + "metadata": { + "id": "BfpE0tExLbN2" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "!export PATH=\"/$HOME/.cargo/bin:${PATH}\" && pip install 'tokenizers==0.10.3'\n", + "# Before installing 'tokenizers', we ensure system-wide Rust compiler availability." + ], + "metadata": { + "id": "aDJWGvk0tU1-" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Fl7obdeKFomg" + }, + "outputs": [], + "source": [ + "!git clone https://github.com/deeppavlov/dream.git" + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install 'xeger==0.3.5'\n", + "!pip install 'transformers==4.6.0'" + ], + "metadata": { + "id": "Gl9xIpKFqiLs" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# In order to train the model with custom classes, we need to modify the 'intent_phrases.json' file.\n", + "# Each intent in the json structure includes a 'phrases' section.\n", + "# Regular expressions from that section will be used to generate the data used during training.\n", + "import json\n", + "INTENT_PHRASES = './dream/annotators/IntentCatcherTransformers/intent_phrases.json'\n", + "\n", + "with open(INTENT_PHRASES, 'r') as file:\n", + " intents = json.load(file)\n", + "\n", + "intents['purchase'] = {\n", + " \"phrases\": [\n", + " \"i think i'll ((order)|(purchase)|(buy)) a book\",\n", + " \"i plan on ((buying)|(purchasing)|(ordering)) a book\",\n", + " \"i would ((love)|(like)) to ((order)|(purchase)|(buy)) a book\",\n", + " \"i'm interested in ((buying)|(purchasing)|(ordering)) a book\",\n", + " \"do you have this book in stock\",\n", + " \"i'm looking to ((order)|(purchase)|(buy)) a book\",\n", + " \"add this to my cart\",\n", + " \"i want to make an order\"\n", + " ],\n", + " \"reg_phrases\": [\n", + " \"i want to buy a book\",\n", + " \"order an item\",\n", + " \"order a book\"\n", + " ],\n", + " \"min_precision\": 0.94,\n", + " \"punctuation\": [\n", + " \".\",\n", + " \"?\"\n", + " ]\n", + "}\n", + "\n", + "with open(INTENT_PHRASES, 'w') as file:\n", + " json.dump(itents, file)" + ], + "metadata": { + "id": "d26Ko8xFF6sH" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "!cd /content/dream/annotators/IntentCatcherTransformers/ && export CUDA_VISIBLE_DEVICES=0 && python -m deeppavlov train intents_model_dp_config.json\n", + "# CUDA_VISIBLE_DEVICES variable is required for GPU-powered training with DeepPavlov." + ], + "metadata": { + "id": "lOmGOt6Wllly" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "!cp /root/.deeppavlov/models/classifiers/intents_model_v2/model.pth.tar /content/drive/MyDrive/\n", + "!cp /root/.deeppavlov/models/classifiers/intents_model_v2/classes.dict /content/drive/MyDrive/\n", + "# Weights and metadata produced during training can be copied to mounted Google drive." + ], + "metadata": { + "id": "YUeJ67-CeuX5" + }, + "execution_count": null, + "outputs": [] + } + ] +} diff --git a/examples/customer_service_bot/bot/api/__init__.py b/examples/customer_service_bot/bot/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/customer_service_bot/bot/api/chatgpt.py b/examples/customer_service_bot/bot/api/chatgpt.py new file mode 100644 index 000000000..7ba3fb69f --- /dev/null +++ b/examples/customer_service_bot/bot/api/chatgpt.py @@ -0,0 +1,72 @@ +""" +ChatGPT +------- +This module defines functions for OpenAI API interaction. +""" +import os +import openai + +CHATGPT_MAIN_PROMPT = """ +You are a helpful assistant for a book shop "Book Lovers Paradise". +Located at 123 Main Street. +Open seven days a week, from 9 AM to 9 PM. +Extensive collection of genres, including fiction, and non-fiction. +Knowledgeable staff. Online catalogue for easy browsing and ordering. +Comfortable seating areas and peaceful atmosphere. +Refund policy within 30 days of purchase. +Loyalty program for frequent customers (10% off purchases). +""" # shortened the prompt to reduce token consumption. + +CHATGPT_QUESTION_PROMPT = """ +What follows is a user query: answer if related to the given description or deny if unrelated. +""" + +CHATGPT_COHERENCE_PROMPT = """ +What follows is a question and an answer. Just write 'true' if the answer was satisfactory or 'false' otherwise. +""" + +openai.api_key = os.getenv("OPENAI_API_TOKEN") + + +def get_output_factory(): + """ + Construct a get_output function encapsulating the execution counter. + The function prompts ChatGPT for generated output. + The main prompt is only included + on the first invocation of the function. + """ + + def get_output_inner(request: str) -> str: + messages = [ + {"role": "system", "content": CHATGPT_MAIN_PROMPT}, + {"role": "system", "content": CHATGPT_QUESTION_PROMPT}, + {"role": "user", "content": request}, + ] # temporary fix until a better solution is found + get_output_inner.num_calls += 1 + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=messages, + ) + return response["choices"][0]["message"]["content"] + + get_output_inner.num_calls = 0 + return get_output_inner + + +def get_coherence(request: str, response: str) -> str: + """ + Prompt ChatGPT to evaluate the coherence of a request + response pair. + """ + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": CHATGPT_COHERENCE_PROMPT}, + {"role": "user", "content": request}, + {"role": "assistant", "content": response}, + ], + ) + return response["choices"][0]["message"]["content"] + + +get_output = get_output_factory() diff --git a/examples/customer_service_bot/bot/api/intent_catcher.py b/examples/customer_service_bot/bot/api/intent_catcher.py new file mode 100644 index 000000000..5a02e2e1d --- /dev/null +++ b/examples/customer_service_bot/bot/api/intent_catcher.py @@ -0,0 +1,27 @@ +""" +Intent Catcher +---- +This module includes queries to a local intent catcher service. +""" +import requests +from dff.script import Message + + +INTENT_CATCHER_SERVICE = "http://localhost:4999/respond" + + +def get_intents(request: Message): + """ + Query the local intent catcher service extracting intents from the + last user utterance. + """ + if not request.text: + return [] + request_body = {"dialog_contexts": [request.text]} + try: + response = requests.post(INTENT_CATCHER_SERVICE, json=request_body) + except requests.RequestException: + response = None + if response and response.status_code == 200: + return [response.json()[0][0]] + return [] diff --git a/examples/customer_service_bot/bot/dialog_graph/__init__.py b/examples/customer_service_bot/bot/dialog_graph/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/customer_service_bot/bot/dialog_graph/conditions.py b/examples/customer_service_bot/bot/dialog_graph/conditions.py new file mode 100644 index 000000000..bde5b1942 --- /dev/null +++ b/examples/customer_service_bot/bot/dialog_graph/conditions.py @@ -0,0 +1,39 @@ +""" +Conditions +----------- +This module defines transition conditions. +""" +from typing import Callable + +from dff.script import Context +from dff.pipeline import Pipeline + +from . import consts + + +def has_intent(labels: list) -> Callable: + """ + Check if any of the given intents are in the context. + """ + + def has_intent_inner(ctx: Context, _: Pipeline) -> bool: + if ctx.validation: + return False + + return any([label in ctx.misc.get(consts.INTENTS, []) for label in labels]) + + return has_intent_inner + + +def slots_filled(slots: list) -> Callable: + """ + Check if any of the given slots are filled. + """ + + def slots_filled_inner(ctx: Context, _: Pipeline) -> bool: + if ctx.validation: + return False + + return all([slot in ctx.misc[consts.SLOTS] for slot in slots]) + + return slots_filled_inner diff --git a/examples/customer_service_bot/bot/dialog_graph/consts.py b/examples/customer_service_bot/bot/dialog_graph/consts.py new file mode 100644 index 000000000..bf154a7fd --- /dev/null +++ b/examples/customer_service_bot/bot/dialog_graph/consts.py @@ -0,0 +1,9 @@ +""" +Consts +------ +This module contains constant variables to use in the `Context` object. +""" +SLOTS = "slots" +INTENTS = "caught_intents" +CHATGPT_OUTPUT = "chatgpt_output" +CHATGPT_COHERENCE = "chatgpt_coherence" diff --git a/examples/customer_service_bot/bot/dialog_graph/processing.py b/examples/customer_service_bot/bot/dialog_graph/processing.py new file mode 100644 index 000000000..f9d4475ff --- /dev/null +++ b/examples/customer_service_bot/bot/dialog_graph/processing.py @@ -0,0 +1,123 @@ +""" +Processing +---------- +This module contains processing routines for the customer service +chat bot. +""" +import re +from string import punctuation +from dff.script import Context +from dff.pipeline import Pipeline +from api import chatgpt, intent_catcher +from . import consts + + +def extract_intents(): + """ + Extract intents from intent catcher response. + """ + + def extract_intents_inner(ctx: Context, _: Pipeline) -> Context: + ctx.misc[consts.INTENTS] = intent_catcher.get_intents(ctx.last_request) + return ctx + + return extract_intents_inner + + +def clear_intents(): + """ + Clear intents container. + """ + + def clear_intents_inner(ctx: Context, _: Pipeline) -> Context: + ctx.misc[consts.INTENTS] = [] + return ctx + + return clear_intents_inner + + +def clear_slots(): + """ + Clear slots container. + """ + + def clear_slots_inner(ctx: Context, _: Pipeline) -> Context: + ctx.misc[consts.SLOTS] = {} + return ctx + + return clear_slots_inner + + +def generate_response(): + """ + Store ChatGPT output and ChatGPT coherence measure in the context. + """ + expression = re.compile(r"true", re.IGNORECASE) + + def generate_response_inner(ctx: Context, _: Pipeline) -> Context: + if ctx.validation: + return ctx + + chatgpt_output = chatgpt.get_output(ctx.last_request.text) + ctx.misc[consts.CHATGPT_OUTPUT] = chatgpt_output + coherence_output = chatgpt.get_coherence(ctx.last_request.text, chatgpt_output) + ctx.misc[consts.CHATGPT_COHERENCE] = True if re.search(expression, coherence_output) else False + return ctx + + return generate_response_inner + + +def extract_item(): + """ + Extract item slot. + """ + expression = re.compile(r".+") + + def extract_item_inner(ctx: Context, _: Pipeline) -> Context: + if ctx.validation: + return ctx + + text: str = ctx.last_request.text + search = re.search(expression, text) + if search is not None: + group = search.group() + ctx.misc[consts.SLOTS]["items"] = [item.strip(punctuation) for item in group.split(", ")] + return ctx + + return extract_item_inner + + +def extract_payment_method(): + """Extract payment method slot.""" + expression = re.compile(r"(card|cash)", re.IGNORECASE) + + def extract_payment_method_inner(ctx: Context, _: Pipeline) -> Context: + if ctx.validation: + return ctx + + text: str = ctx.last_request.text + search = re.search(expression, text) + if search is not None: + ctx.misc[consts.SLOTS]["payment_method"] = search.group() + return ctx + + return extract_payment_method_inner + + +def extract_delivery(): + """ + Extract delivery slot. + """ + expression = re.compile(r"(pickup|deliver)", re.IGNORECASE) + + def extract_delivery_inner(ctx: Context, _: Pipeline) -> Context: + if ctx.validation: + return ctx + + text: str = ctx.last_request.text + search = re.search(expression, text) + if search is not None: + ctx.misc[consts.SLOTS]["delivery"] = search.group() + return ctx + + return extract_delivery_inner diff --git a/examples/customer_service_bot/bot/dialog_graph/response.py b/examples/customer_service_bot/bot/dialog_graph/response.py new file mode 100644 index 000000000..c75ca3e4f --- /dev/null +++ b/examples/customer_service_bot/bot/dialog_graph/response.py @@ -0,0 +1,39 @@ +""" +Response +-------- +This module contains response customization functions. +""" +from dff.script import Context, Message +from dff.pipeline import Pipeline + +from . import consts + +FALLBACK_RESPONSE = ( + "I'm afraid I cannot elaborate on this subject. If you have any other questions, feel free to ask them." +) + + +def choose_response(ctx: Context, _: Pipeline) -> Message: + """ + Return ChatGPT response if it is coherent, fall back to + predetermined response otherwise. + """ + if ctx.validation: + return Message() + coherence = ctx.misc[consts.CHATGPT_COHERENCE] + response = ctx.misc[consts.CHATGPT_OUTPUT] + return Message(text=(response if coherence else FALLBACK_RESPONSE)) + + +def confirm(ctx: Context, _: Pipeline) -> Message: + if ctx.validation: + return Message() + msg_text = ( + "We registered your transaction. " + + f"Requested titles are: {', '.join(ctx.misc[consts.SLOTS]['items'])}. " + + f"Delivery method: {ctx.misc[consts.SLOTS]['delivery']}. " + + f"Payment method: {ctx.misc[consts.SLOTS]['payment_method']}. " + + "Type `abort` to cancel, type `ok` to continue." + ) + msg = Message(text=msg_text) + return msg diff --git a/examples/customer_service_bot/bot/dialog_graph/script.py b/examples/customer_service_bot/bot/dialog_graph/script.py new file mode 100644 index 000000000..5d31175ad --- /dev/null +++ b/examples/customer_service_bot/bot/dialog_graph/script.py @@ -0,0 +1,92 @@ +""" +Script +-------- +This module defines the bot script. +""" +from dff.script import RESPONSE, TRANSITIONS, LOCAL, PRE_TRANSITIONS_PROCESSING, PRE_RESPONSE_PROCESSING +from dff.script import Message +from dff.script import conditions as cnd +from dff.script import labels as lbl + +from . import conditions as loc_cnd +from . import response as loc_rsp +from . import processing as loc_prc + + +script = { + "general_flow": { + LOCAL: { + TRANSITIONS: { + ("form_flow", "ask_item", 1.0): cnd.any( + [loc_cnd.has_intent(["purchase"]), cnd.regexp(r"\border\b|\bpurchase\b")] + ), + ("chitchat_flow", "init_chitchat", 0.8): cnd.true(), + }, + PRE_TRANSITIONS_PROCESSING: {"1": loc_prc.extract_intents()}, + }, + "start_node": { + RESPONSE: Message(text=""), + }, + "fallback_node": { + RESPONSE: Message(text="Cannot recognize your query. Type 'ok' to continue."), + }, + }, + "chitchat_flow": { + LOCAL: { + TRANSITIONS: { + ("form_flow", "ask_item", 1.0): cnd.any( + [loc_cnd.has_intent(["purchase"]), cnd.regexp(r"\border\b|\bpurchase\b")] + ), + }, + PRE_TRANSITIONS_PROCESSING: {"1": loc_prc.clear_intents(), "2": loc_prc.extract_intents()}, + }, + "init_chitchat": { + RESPONSE: Message(text="'Book Lovers Paradise' welcomes you! Ask us anything you would like to know."), + TRANSITIONS: {("chitchat_flow", "chitchat", 0.8): cnd.true()}, + PRE_TRANSITIONS_PROCESSING: {"2": loc_prc.clear_slots()}, + }, + "chitchat": { + PRE_RESPONSE_PROCESSING: {"1": loc_prc.generate_response()}, + TRANSITIONS: {lbl.repeat(0.8): cnd.true()}, # repeat unless conditions for moving forward are met + RESPONSE: loc_rsp.choose_response, + }, + }, + "form_flow": { + LOCAL: { + TRANSITIONS: { + ("chitchat_flow", "init_chitchat", 1.2): cnd.any( + [cnd.regexp(r"\bcancel\b|\babort\b"), loc_cnd.has_intent(["no"])] + ), + } + }, + "ask_item": { + RESPONSE: Message( + text="Which books would you like to order? Please, separate the titles by commas (type 'abort' to cancel)." + ), + PRE_TRANSITIONS_PROCESSING: {"1": loc_prc.extract_item()}, + TRANSITIONS: {("form_flow", "ask_delivery"): loc_cnd.slots_filled(["items"]), lbl.repeat(0.8): cnd.true()}, + }, + "ask_delivery": { + RESPONSE: Message( + text="Which delivery method would you like to use? We currently offer pickup or home delivery." + ), + PRE_TRANSITIONS_PROCESSING: {"1": loc_prc.extract_delivery()}, + TRANSITIONS: { + ("form_flow", "ask_payment_method"): loc_cnd.slots_filled(["delivery"]), + lbl.repeat(0.8): cnd.true(), # repeat unless conditions for moving forward are met + }, + }, + "ask_payment_method": { + RESPONSE: Message(text="Please, enter the payment method you would like to use: cash or credit card."), + PRE_TRANSITIONS_PROCESSING: {"1": loc_prc.extract_payment_method()}, + TRANSITIONS: { + ("form_flow", "success"): loc_cnd.slots_filled(["payment_method"]), + lbl.repeat(0.8): cnd.true(), # repeat unless conditions for moving forward are met + }, + }, + "success": { + RESPONSE: loc_rsp.confirm, + TRANSITIONS: {("chitchat_flow", "init_chitchat"): cnd.true()}, + }, + }, +} diff --git a/examples/customer_service_bot/bot/dockerfile b/examples/customer_service_bot/bot/dockerfile new file mode 100644 index 000000000..ed4a0a4fa --- /dev/null +++ b/examples/customer_service_bot/bot/dockerfile @@ -0,0 +1,10 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.10-slim-buster +RUN apt update && apt install -y git + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY . . +CMD ["python3", "run.py"] diff --git a/examples/customer_service_bot/bot/requirements.txt b/examples/customer_service_bot/bot/requirements.txt new file mode 100644 index 000000000..d8835d1d7 --- /dev/null +++ b/examples/customer_service_bot/bot/requirements.txt @@ -0,0 +1,6 @@ +dff[telegram, tests] >= 0.4 +itsdangerous==2.0.1 +gunicorn==19.9.0 +sentry-sdk[flask]==0.14.1 +healthcheck==1.3.3 +openai==0.27.3 \ No newline at end of file diff --git a/examples/customer_service_bot/bot/run.py b/examples/customer_service_bot/bot/run.py new file mode 100644 index 000000000..7e973d5ea --- /dev/null +++ b/examples/customer_service_bot/bot/run.py @@ -0,0 +1,39 @@ +import os + +from dff.messengers.telegram import PollingTelegramInterface +from dff.pipeline import Pipeline + +from dialog_graph import script + + +def get_pipeline(use_cli_interface: bool = False) -> Pipeline: + telegram_token = os.getenv("TG_BOT_TOKEN") + openai_api_token = os.getenv("OPENAI_API_TOKEN") + + if not openai_api_token: + raise RuntimeError("Openai api token (`OPENAI_API_TOKEN`) system variable is required.") + + if use_cli_interface: + messenger_interface = None + elif telegram_token: + messenger_interface = PollingTelegramInterface(token=telegram_token) + + else: + raise RuntimeError( + "Telegram token (`TG_BOT_TOKEN`) is not set. `TG_BOT_TOKEN` can be set via `.env` file." + " For more info see README.md." + ) + + pipeline = Pipeline.from_script( + script=script.script, + start_label=("general_flow", "start_node"), + fallback_label=("general_flow", "fallback_node"), + messenger_interface=messenger_interface, + ) + + return pipeline + + +if __name__ == "__main__": + pipeline = get_pipeline() + pipeline.run() diff --git a/examples/customer_service_bot/bot/test.py b/examples/customer_service_bot/bot/test.py new file mode 100644 index 000000000..c06edaf6a --- /dev/null +++ b/examples/customer_service_bot/bot/test.py @@ -0,0 +1,43 @@ +import pytest +from dff.utils.testing.common import check_happy_path +from dff.messengers.telegram import TelegramMessage +from dff.script import RESPONSE, Message + +from dialog_graph.script import script +from run import get_pipeline + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "happy_path", + [ + ( + (TelegramMessage(text="/start"), script["chitchat_flow"]["init_chitchat"][RESPONSE]), + (TelegramMessage(text="I need to make an order"), script["form_flow"]["ask_item"][RESPONSE]), + (TelegramMessage(text="abort"), script["chitchat_flow"]["init_chitchat"][RESPONSE]), + (TelegramMessage(text="I need to make an order"), script["form_flow"]["ask_item"][RESPONSE]), + (TelegramMessage(text="'Pale Fire', 'Lolita'"), script["form_flow"]["ask_delivery"][RESPONSE]), + ( + TelegramMessage(text="I want it delivered to my place"), + script["form_flow"]["ask_payment_method"][RESPONSE], + ), + (TelegramMessage(text="abort"), script["chitchat_flow"]["init_chitchat"][RESPONSE]), + (TelegramMessage(text="I need to make an order"), script["form_flow"]["ask_item"][RESPONSE]), + (TelegramMessage(text="'Pale Fire', 'Lolita'"), script["form_flow"]["ask_delivery"][RESPONSE]), + ( + TelegramMessage(text="I want it delivered to my place"), + script["form_flow"]["ask_payment_method"][RESPONSE], + ), + (TelegramMessage(text="foo bar baz"), script["form_flow"]["ask_payment_method"][RESPONSE]), + ( + TelegramMessage(text="card"), + Message( + text="We registered your transaction. Requested titles are: 'Pale Fire', 'Lolita'. Delivery method: deliver. Payment method: card. Type `abort` to cancel, type `ok` to continue." + ), + ), + (TelegramMessage(text="ok"), script["chitchat_flow"]["init_chitchat"][RESPONSE]), + ) + ], +) +async def test_happy_path(happy_path): + check_happy_path(pipeline=get_pipeline(use_cli_interface=True), happy_path=happy_path) diff --git a/examples/customer_service_bot/docker-compose.yml b/examples/customer_service_bot/docker-compose.yml new file mode 100644 index 000000000..93f2261f8 --- /dev/null +++ b/examples/customer_service_bot/docker-compose.yml @@ -0,0 +1,24 @@ +version: "2" +services: + assistant: + env_file: [ .env ] + build: + args: + SERVICE_NAME: assistant + SERVICE_PORT: 5000 + context: bot/ + volumes: + - ./bot/:/app:ro + ports: + - 5000:5000 + + intent_catcher: + env_file: [ .env ] + build: + args: + SERVICE_PORT: 4999 + IC_WEIGHTS: https://huggingface.co/ruthenian8/deeppavlov-intent-catcher-transformers/resolve/main/model.pth.tar + IC_CLASSES: https://huggingface.co/ruthenian8/deeppavlov-intent-catcher-transformers/raw/main/classes.dict + context: ./intent_catcher/ + ports: + - 4999:4999 diff --git a/examples/customer_service_bot/intent_catcher/dockerfile b/examples/customer_service_bot/intent_catcher/dockerfile new file mode 100644 index 000000000..15b0287c8 --- /dev/null +++ b/examples/customer_service_bot/intent_catcher/dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1 +FROM pytorch/pytorch:1.6.0-cuda10.1-cudnn7-runtime as base + +RUN apt-get update && \ + apt-get install -y gnupg2 && \ + apt-get install -y curl && \ + apt-get install -y --allow-unauthenticated wget && \ + apt-get install -y git && \ + apt-get install -y unzip && \ + apt-key del 7fa2af80 && \ + rm -f /etc/apt/sources.list.d/cuda*.list && \ + curl https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-keyring_1.0-1_all.deb \ + -o cuda-keyring_1.0-1_all.deb && \ + dpkg -i cuda-keyring_1.0-1_all.deb + # update cuda keyring (https://developer.nvidia.com/blog/updating-the-cuda-linux-gpg-repository-key/) + +ARG SERVICE_PORT +ENV SERVICE_PORT ${SERVICE_PORT} +ARG IC_WEIGHTS +ARG IC_CLASSES + +WORKDIR /src +COPY ./requirements.txt /src/requirements.txt +RUN pip install -r /src/requirements.txt +RUN wget ${IC_WEIGHTS} +RUN wget ${IC_CLASSES} + +FROM base as prod +WORKDIR /src +COPY . /src +CMD gunicorn --workers=1 server:app -b 0.0.0.0:${SERVICE_PORT} --timeout=1200 diff --git a/examples/customer_service_bot/intent_catcher/requirements.txt b/examples/customer_service_bot/intent_catcher/requirements.txt new file mode 100644 index 000000000..5c430cebf --- /dev/null +++ b/examples/customer_service_bot/intent_catcher/requirements.txt @@ -0,0 +1,7 @@ +flask==2.2.5 +itsdangerous==2.0.1 +gunicorn==19.9.0 +requests==2.22.0 +sentry-sdk[flask]==0.14.1 +healthcheck==1.3.3 +transformers==4.6.0 diff --git a/examples/customer_service_bot/intent_catcher/server.py b/examples/customer_service_bot/intent_catcher/server.py new file mode 100644 index 000000000..e4ae0007a --- /dev/null +++ b/examples/customer_service_bot/intent_catcher/server.py @@ -0,0 +1,85 @@ +import logging +import time +import os +import random +import csv + +import torch +import sentry_sdk +from flask import Flask, request, jsonify +from sentry_sdk.integrations.flask import FlaskIntegration +from transformers import DistilBertConfig, AutoModelForSequenceClassification, AutoTokenizer, pipeline + + +sentry_sdk.init(dsn=os.getenv("SENTRY_DSN"), integrations=[FlaskIntegration()]) + +logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) +logger = logging.getLogger(__name__) + +random.seed(42) + +DEFAULT_CONFIDENCE = 0.9 +ZERO_CONFIDENCE = 0.0 +MODEL_PATH = "model.pth.tar" +CLASSES_PATH = "classes.dict" + +with open(CLASSES_PATH, "r") as file: + reader = csv.reader(file, delimiter="\t") + label2id = {line[0]: line[1] for line in reader} + +id2label = {value: key for key, value in label2id.items()} + +try: + if torch.cuda.is_available(): + no_cuda = False + else: + no_cuda = True + model = AutoModelForSequenceClassification.from_config(DistilBertConfig(num_labels=23)) + state = torch.load(MODEL_PATH, map_location = "cpu" if no_cuda else "gpu") + model.load_state_dict(state["model_state_dict"]) + tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") + pipe = pipeline('text-classification', model=model, tokenizer=tokenizer) + logger.info("predictor is ready") +except Exception as e: + sentry_sdk.capture_exception(e) + logger.exception(e) + raise e + +app = Flask(__name__) +logging.getLogger("werkzeug").setLevel("WARNING") + + +@app.route("/respond", methods=["POST"]) +def respond(): + """ + The API expects a json object with the dialog history passed as an array and labeled 'dialog_contexts'. + Intents will be extracted from the last utterance. + + .. code-block:: python + { + "dialog_contexts": ["phrase_1", "phrase_2"] + } + + The API responds with a nested array containing 'label - score' pairs. + + .. code-block:: python + [["definition",0.3393537402153015]] + + """ + st_time = time.time() + contexts = request.json.get("dialog_contexts", []) + + try: + results = pipe(contexts) + indices = [int(''.join(filter(lambda x: x.isdigit(), result['label']))) for result in results] + responses = [list(label2id.keys())[idx] for idx in indices] + confidences = [result['score'] for result in results] + except Exception as exc: + logger.exception(exc) + sentry_sdk.capture_exception(exc) + responses = [""] * len(contexts) + confidences = [ZERO_CONFIDENCE] * len(contexts) + + total_time = time.time() - st_time + logger.info(f"Intent catcher exec time: {total_time:.3f}s") + return jsonify(list(zip(responses, confidences))) diff --git a/examples/customer_service_bot/intent_catcher/test_server.py b/examples/customer_service_bot/intent_catcher/test_server.py new file mode 100644 index 000000000..1bca73f6c --- /dev/null +++ b/examples/customer_service_bot/intent_catcher/test_server.py @@ -0,0 +1,15 @@ +import os +import requests + + +def test_respond(): + url = "http://0.0.0.0:{}/respond".format(os.getenv("SERVICE_PORT")) + + contexts = [["I want to order food"], ["cancel_the_order"]] + result = requests.post(url, json={"dialog_contexts": contexts}).json() + assert [len(sample[0]) > 0 and sample[1] > 0.0 for sample in result], f"Got\n{result}\n, something is wrong" + print("Success") + + +if __name__ == "__main__": + test_respond() diff --git a/examples/frequently_asked_question_bot/telegram/.env.example b/examples/frequently_asked_question_bot/telegram/.env.example new file mode 100644 index 000000000..016ccd98a --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/.env.example @@ -0,0 +1 @@ +TG_BOT_TOKEN= \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/README.md b/examples/frequently_asked_question_bot/telegram/README.md new file mode 100644 index 000000000..ed0b32891 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/README.md @@ -0,0 +1,56 @@ +## Description + +Example FAQ bot built on `dff`. Uses telegram as an interface. + +This bot listens for user questions and finds similar questions in its database by using the `clips/mfaq` model. + +It displays found questions as buttons. Upon pressing a button, the bot sends an answer to the question from the database. + + +An example of bot usage: + +![image](https://user-images.githubusercontent.com/61429541/219064505-20e67950-cb88-4cff-afa5-7ce608e1282c.png) + +### Run with Docker & Docker-Compose environment +In order for the bot to work, set the bot token via [.env](.env.example). First step is creating your `.env` file: +``` +echo TG_BOT_TOKEN=******* >> .env +``` + +Build the bot: +```commandline +docker-compose build +``` +Testing the bot: +```commandline +docker-compose run bot pytest test.py +``` + +Running the bot: +```commandline +docker-compose run bot python run.py +``` + +Running in background +```commandline +docker-compose up -d +``` +### Run with Python environment +In order for the bot to work, set the bot token, example is in [.env](.env.example). First step is setting environment variables: +``` +export TG_BOT_TOKEN=******* +``` + +Build the bot: +```commandline +pip3 install -r requirements.txt +``` +Testing the bot: +```commandline +pytest test.py +``` + +Running the bot: +```commandline +python run.py +``` \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/bot/Dockerfile b/examples/frequently_asked_question_bot/telegram/bot/Dockerfile new file mode 100644 index 000000000..18fd9ff6b --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/Dockerfile @@ -0,0 +1,15 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.10-slim-buster + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +# cache mfaq model +RUN ["python3", "-c", "from sentence_transformers import SentenceTransformer; _ = SentenceTransformer('clips/mfaq')"] + +COPY . . + +CMD ["python3", "run.py"] diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/__init__.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py new file mode 100644 index 000000000..faa7cbdcd --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py @@ -0,0 +1,26 @@ +""" +Conditions +----------- +This module defines conditions for transitions between nodes. +""" +from typing import cast + +from dff.script import Context +from dff.pipeline import Pipeline +from dff.messengers.telegram import TelegramMessage + + +def received_text(ctx: Context, _: Pipeline): + """Return true if the last update from user contains text.""" + last_request = ctx.last_request + + return last_request.text is not None + + +def received_button_click(ctx: Context, _: Pipeline): + """Return true if the last update from user is a button press.""" + if ctx.validation: # Regular `Message` doesn't have `callback_query` field, so this fails during validation + return False + last_request = cast(TelegramMessage, ctx.last_request) + + return vars(last_request).get("callback_query") is not None diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py new file mode 100644 index 000000000..3edee676e --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py @@ -0,0 +1,51 @@ +""" +Responses +--------- +This module defines different responses the bot gives. +""" +from typing import cast + +from dff.script import Context +from dff.pipeline import Pipeline +from dff.script.core.message import Button +from dff.messengers.telegram import TelegramMessage, TelegramUI, ParseMode +from faq_model.model import faq + + +def suggest_similar_questions(ctx: Context, _: Pipeline): + """Suggest questions similar to user's query by showing buttons with those questions.""" + if ctx.validation: # this function requires non-empty fields and cannot be used during script validation + return TelegramMessage() + last_request = ctx.last_request + if last_request is None: + raise RuntimeError("No last requests.") + if last_request.annotations is None: + raise RuntimeError("No annotations.") + similar_questions = last_request.annotations.get("similar_questions") + if similar_questions is None: + raise RuntimeError("Last request has no text.") + + if len(similar_questions) == 0: # question is not similar to any questions + return TelegramMessage( + text="I don't have an answer to that question. Here's a list of questions I know an answer to:", + ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in faq]), + ) + else: + return TelegramMessage( + text="I found similar questions in my database:", + ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in similar_questions]), + ) + + +def answer_question(ctx: Context, _: Pipeline): + """Answer a question asked by a user by pressing a button.""" + if ctx.validation: # this function requires non-empty fields and cannot be used during script validation + return TelegramMessage() + last_request = ctx.last_request + if last_request is None: + raise RuntimeError("No last requests.") + last_request = cast(TelegramMessage, last_request) + if last_request.callback_query is None: + raise RuntimeError("No callback query") + + return TelegramMessage(text=faq[last_request.callback_query], parse_mode=ParseMode.HTML) diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py new file mode 100644 index 000000000..91b62780d --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py @@ -0,0 +1,40 @@ +""" +Script +-------- +This module defines a script that the bot follows during conversation. +""" +from dff.script import RESPONSE, TRANSITIONS, LOCAL +import dff.script.conditions as cnd +from dff.messengers.telegram import TelegramMessage + +from .responses import answer_question, suggest_similar_questions +from .conditions import received_button_click, received_text + +script = { + "service_flow": { + "start_node": { + TRANSITIONS: {("qa_flow", "welcome_node"): cnd.exact_match(TelegramMessage(text="/start"))}, + }, + "fallback_node": { + RESPONSE: TelegramMessage(text="Something went wrong. Use `/restart` to start over."), + TRANSITIONS: {("qa_flow", "welcome_node"): cnd.exact_match(TelegramMessage(text="/restart"))}, + }, + }, + "qa_flow": { + LOCAL: { + TRANSITIONS: { + ("qa_flow", "suggest_questions"): received_text, + ("qa_flow", "answer_question"): received_button_click, + }, + }, + "welcome_node": { + RESPONSE: TelegramMessage(text="Welcome! Ask me questions about Arch Linux."), + }, + "suggest_questions": { + RESPONSE: suggest_similar_questions, + }, + "answer_question": { + RESPONSE: answer_question, + }, + }, +} diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/__init__.py b/examples/frequently_asked_question_bot/telegram/bot/faq_model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json new file mode 100644 index 000000000..70e98e7c9 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json @@ -0,0 +1,6 @@ +{ + "What is Arch Linux?": "See the Arch Linux article.\n", + "Why would I not want to use Arch?": "You may not want to use Arch, if:\n\n you do not have the ability/time/desire for a 'do-it-yourself' GNU/Linux distribution.\n you require support for an architecture other than x86_64.\n you take a strong stance on using a distribution which only provides free software as defined by GNU.\n you believe an operating system should configure itself, run out of the box, and include a complete default set of software and desktop environment on the installation media.\n you do not want a rolling release GNU/Linux distribution.\n you are happy with your current OS.", + "Why would I want to use Arch?": "Because Arch is the best.\n", + "What architectures does Arch support?": "Arch only supports the x86_64 (sometimes called amd64) architecture. Support for i686 was dropped in November 2017 [1]. \nThere are unofficial ports for the i686 architecture [2] and ARM CPUs [3], each with their own community channels.\n" +} \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py b/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py new file mode 100644 index 000000000..4dacaa64b --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py @@ -0,0 +1,31 @@ +""" +Model +----- +This module defines AI-dependent functions. +""" +import json +from pathlib import Path + +import numpy as np +from sentence_transformers import SentenceTransformer + +model = SentenceTransformer("clips/mfaq") + +with open(Path(__file__).parent / "faq_dataset_sample.json", "r", encoding="utf-8") as file: + faq = json.load(file) + + +def find_similar_questions(question: str): + """Return a list of similar questions from the database.""" + questions = list(map(lambda x: "" + x, faq.keys())) + q_emb, *faq_emb = model.encode(["" + question] + questions) + + emb_with_scores = tuple(zip(questions, map(lambda x: np.linalg.norm(x - q_emb), faq_emb))) + + filtered_embeddings = tuple(sorted(filter(lambda x: x[1] < 10, emb_with_scores), key=lambda x: x[1])) + + result = [] + for question, score in filtered_embeddings: + question = question.removeprefix("") + result.append(question) + return result diff --git a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/__init__.py b/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py new file mode 100644 index 000000000..de0892965 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py @@ -0,0 +1,30 @@ +""" +Pre Services +--- +This module defines services that process user requests before script transition. +""" +from dff.script import Context + +from faq_model.model import find_similar_questions + + +def question_processor(ctx: Context): + """Store questions similar to user's query in the `annotations` field of a message.""" + last_request = ctx.last_request + if last_request is None: + return + else: + if last_request.annotations is None: + last_request.annotations = {} + else: + if last_request.annotations.get("similar_questions") is not None: + return + if last_request.text is None: + last_request.annotations["similar_questions"] = None + else: + last_request.annotations["similar_questions"] = find_similar_questions(last_request.text) + + ctx.set_last_request(last_request) + + +services = [question_processor] # pre-services run before bot sends a response diff --git a/examples/frequently_asked_question_bot/telegram/bot/requirements.txt b/examples/frequently_asked_question_bot/telegram/bot/requirements.txt new file mode 100644 index 000000000..ee3746494 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/requirements.txt @@ -0,0 +1,2 @@ +dff[telegram, tests]>=0.4 +sentence_transformers==2.2.2 \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/bot/run.py b/examples/frequently_asked_question_bot/telegram/bot/run.py new file mode 100644 index 000000000..f8c961f87 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/run.py @@ -0,0 +1,37 @@ +import os + +from dff.messengers.telegram import PollingTelegramInterface +from dff.pipeline import Pipeline + +from dialog_graph import script +from pipeline_services import pre_services + + +def get_pipeline(use_cli_interface: bool = False) -> Pipeline: + telegram_token = os.getenv("TG_BOT_TOKEN") + + if use_cli_interface: + messenger_interface = None + elif telegram_token: + messenger_interface = PollingTelegramInterface(token=telegram_token) + else: + raise RuntimeError( + "Telegram token (`TG_BOT_TOKEN`) is not set. `TG_BOT_TOKEN` can be set via `.env` file." + " For more info see README.md." + ) + + pipeline = Pipeline.from_script( + script=script.script, + start_label=("service_flow", "start_node"), + fallback_label=("service_flow", "fallback_node"), + messenger_interface=messenger_interface, + # pre-services run before bot sends a response + pre_services=pre_services.services, + ) + + return pipeline + + +if __name__ == "__main__": + pipeline = get_pipeline() + pipeline.run() diff --git a/examples/frequently_asked_question_bot/telegram/bot/test.py b/examples/frequently_asked_question_bot/telegram/bot/test.py new file mode 100644 index 000000000..d7f127f4e --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/bot/test.py @@ -0,0 +1,61 @@ +import pytest +from dff.utils.testing.common import check_happy_path +from dff.messengers.telegram import TelegramMessage, TelegramUI +from dff.script import RESPONSE +from dff.script.core.message import Button + +from dialog_graph import script +from run import get_pipeline +from faq_model.model import faq + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "happy_path", + [ + ( + (TelegramMessage(text="/start"), script.script["qa_flow"]["welcome_node"][RESPONSE]), + ( + TelegramMessage(text="Why use arch?"), + TelegramMessage( + text="I found similar questions in my database:", + ui=TelegramUI( + buttons=[ + Button(text=q, payload=q) + for q in ["Why would I want to use Arch?", "Why would I not want to use Arch?"] + ] + ), + ), + ), + ( + TelegramMessage(callback_query="Why would I want to use Arch?"), + TelegramMessage(text=faq["Why would I want to use Arch?"]), + ), + ( + TelegramMessage(callback_query="Why would I not want to use Arch?"), + TelegramMessage(text=faq["Why would I not want to use Arch?"]), + ), + ( + TelegramMessage(text="What is arch linux?"), + TelegramMessage( + text="I found similar questions in my database:", + ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in ["What is Arch Linux?"]]), + ), + ), + (TelegramMessage(callback_query="What is Arch Linux?"), TelegramMessage(text=faq["What is Arch Linux?"])), + ( + TelegramMessage(text="where am I?"), + TelegramMessage( + text="I don't have an answer to that question. Here's a list of questions I know an answer to:", + ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in faq]), + ), + ), + ( + TelegramMessage(callback_query="What architectures does Arch support?"), + TelegramMessage(text=faq["What architectures does Arch support?"]), + ), + ) + ], +) +async def test_happy_path(happy_path): + check_happy_path(pipeline=get_pipeline(use_cli_interface=True), happy_path=happy_path) diff --git a/examples/frequently_asked_question_bot/telegram/docker-compose.yml b/examples/frequently_asked_question_bot/telegram/docker-compose.yml new file mode 100644 index 000000000..948a97000 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/docker-compose.yml @@ -0,0 +1,8 @@ +version: "2" +services: + bot: + build: + context: bot/ + volumes: + - ./bot/:/app:ro + env_file: .env diff --git a/examples/frequently_asked_question_bot/web/README.md b/examples/frequently_asked_question_bot/web/README.md new file mode 100644 index 000000000..36c42054c --- /dev/null +++ b/examples/frequently_asked_question_bot/web/README.md @@ -0,0 +1,26 @@ +## Description + +Example FAQ bot built on `dff` with a web interface. + +This example contains a website with a chat interface using `WebSockets`. Chat history is stored inside a `postgresql` database. + +The website is accessible via http://localhost:80. + +The bot itself works in a following manner: + +Whenever a user asks a question it searches for the most similar question in its database using `clips/mfaq` an answer to which is sent to the user. + +A showcase of the website: +![faq_web](https://user-images.githubusercontent.com/61429541/233875303-b9bc81c9-522b-4596-8599-6efcfa708d1e.gif) + +### Run with Docker & Docker-Compose environment + +Build the bot: +```commandline +docker-compose build +``` + +Running in background +```commandline +docker-compose up -d +``` diff --git a/examples/frequently_asked_question_bot/web/docker-compose.yml b/examples/frequently_asked_question_bot/web/docker-compose.yml new file mode 100644 index 000000000..0aaf57ff4 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + web: + build: + context: web/ + volumes: + - ./web/:/app:ro + ports: + - 8000:8000 + env_file: + - ./.env + depends_on: + - db + db: + env_file: [.env] + image: postgres:latest + restart: unless-stopped + volumes: + - postgres_data:/var/lib/postgresql/data/ + nginx: + image: nginx + depends_on: + - web + ports: + - 80:80 + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./web/static:/app/static:ro + +volumes: + postgres_data: \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/nginx.conf b/examples/frequently_asked_question_bot/web/nginx.conf new file mode 100644 index 000000000..27e10ee4f --- /dev/null +++ b/examples/frequently_asked_question_bot/web/nginx.conf @@ -0,0 +1,37 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 80; + + location / { + proxy_pass http://web:8000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } + + location /ws/ { + proxy_pass http://web:8000/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + location /static/ { + alias /app/static/; + types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/x-javascript js; + application/atom+xml atom; + application/rss+xml rss; + } + } + } +} \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/web/Dockerfile b/examples/frequently_asked_question_bot/web/web/Dockerfile new file mode 100644 index 000000000..46fb25219 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.10-slim-buster + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +# cache mfaq model +RUN ["python3", "-c", "from sentence_transformers import SentenceTransformer; _ = SentenceTransformer('clips/mfaq')"] + +COPY . . + +RUN ["pytest", "bot/test.py"] + +CMD ["python3", "app.py"] diff --git a/examples/frequently_asked_question_bot/web/web/__init__.py b/examples/frequently_asked_question_bot/web/web/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/web/web/app.py b/examples/frequently_asked_question_bot/web/web/app.py new file mode 100644 index 000000000..b96e9010f --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/app.py @@ -0,0 +1,50 @@ +from bot.pipeline import pipeline + +import uvicorn +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse +from dff.script import Message, Context + + +app = FastAPI() + + +@app.get("/") +async def index(): + return FileResponse('static/index.html', media_type='text/html') + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: str): + await websocket.accept() + + # store user info in the dialogue context + await pipeline.context_storage.set_item_async( + client_id, + Context( + id=client_id, + misc={"ip": websocket.client.host, "headers": websocket.headers.raw} + ) + ) + + async def respond(request: Message): + context = await pipeline._run_pipeline(request, client_id) + response = context.last_response.text + await websocket.send_text(response) + return context + + try: + await respond(Message()) # display welcome message + + while True: + data = await websocket.receive_text() + await respond(Message(text=data)) + except WebSocketDisconnect: # ignore disconnects + pass + +if __name__ == "__main__": + uvicorn.run( + app, + host="0.0.0.0", + port=8000, + ) diff --git a/examples/frequently_asked_question_bot/web/web/bot/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py new file mode 100644 index 000000000..fd27c3d54 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py @@ -0,0 +1,49 @@ +""" +Responses +--------- +This module defines different responses the bot gives. +""" + +from dff.script import Context +from dff.script import Message +from dff.pipeline import Pipeline +from ..faq_model.model import faq + + +def get_bot_answer(question: str) -> Message: + """The Message the bot will return as an answer if the most similar question is `question`.""" + return Message(text=f"Q: {question}
A: {faq[question]}") + + +FALLBACK_ANSWER = Message( + text='I don\'t have an answer to that question. ' + 'You can find FAQ here.', +) +"""Fallback answer that the bot returns if user's query is not similar to any of the questions.""" + + +FIRST_MESSAGE = Message( + text="Welcome! Ask me questions about Arch Linux." +) + +FALLBACK_NODE_MESSAGE = Message( + text="Something went wrong.\n" + "You may continue asking me questions about Arch Linux." +) + + +def answer_similar_question(ctx: Context, _: Pipeline): + """Answer with the most similar question to user's query.""" + if ctx.validation: # this function requires non-empty fields and cannot be used during script validation + return Message() + last_request = ctx.last_request + if last_request is None: + raise RuntimeError("No last requests.") + if last_request.annotations is None: + raise RuntimeError("No annotations.") + similar_question = last_request.annotations.get("similar_question") + + if similar_question is None: # question is not similar to any of the questions + return FALLBACK_ANSWER + else: + return get_bot_answer(similar_question) diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py new file mode 100644 index 000000000..3b8ecfa47 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py @@ -0,0 +1,39 @@ +""" +Script +-------- +This module defines a script that the bot follows during conversation. +""" +from dff.script import RESPONSE, TRANSITIONS, GLOBAL, Message +import dff.script.conditions as cnd + +from .responses import answer_similar_question, FIRST_MESSAGE, FALLBACK_NODE_MESSAGE + + +pipeline_kwargs = { + "script": { + GLOBAL: { + TRANSITIONS: { + # an empty message is used to init a dialogue + ("qa_flow", "welcome_node"): cnd.exact_match(Message(), skip_none=False), + ("qa_flow", "answer_question"): cnd.true(), + }, + }, + "qa_flow": { + "welcome_node": { + RESPONSE: FIRST_MESSAGE, + }, + "answer_question": { + RESPONSE: answer_similar_question, + }, + }, + "service_flow": { + "start_node": {}, # this is the start node, it simply redirects to welcome node + + "fallback_node": { # this node will only be used if something goes wrong (e.g. an exception is raised) + RESPONSE: FALLBACK_NODE_MESSAGE, + }, + }, + }, + "start_label": ("service_flow", "start_node"), + "fallback_label": ("service_flow", "fallback_node") +} diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/faq_model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json new file mode 100644 index 000000000..70e98e7c9 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json @@ -0,0 +1,6 @@ +{ + "What is Arch Linux?": "See the Arch Linux article.\n", + "Why would I not want to use Arch?": "You may not want to use Arch, if:\n\n you do not have the ability/time/desire for a 'do-it-yourself' GNU/Linux distribution.\n you require support for an architecture other than x86_64.\n you take a strong stance on using a distribution which only provides free software as defined by GNU.\n you believe an operating system should configure itself, run out of the box, and include a complete default set of software and desktop environment on the installation media.\n you do not want a rolling release GNU/Linux distribution.\n you are happy with your current OS.", + "Why would I want to use Arch?": "Because Arch is the best.\n", + "What architectures does Arch support?": "Arch only supports the x86_64 (sometimes called amd64) architecture. Support for i686 was dropped in November 2017 [1]. \nThere are unofficial ports for the i686 architecture [2] and ARM CPUs [3], each with their own community channels.\n" +} \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py b/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py new file mode 100644 index 000000000..a12b5a3cc --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py @@ -0,0 +1,29 @@ +""" +Model +----- +This module defines AI-dependent functions. +""" +import json +from pathlib import Path + +import numpy as np +from sentence_transformers import SentenceTransformer + +model = SentenceTransformer("clips/mfaq") + +with open(Path(__file__).parent / "faq_dataset_sample.json", "r", encoding="utf-8") as file: + faq = json.load(file) + + +def find_similar_question(question: str) -> str | None: + """Return the most similar question from the faq database.""" + questions = list(map(lambda x: "" + x, faq.keys())) + q_emb, *faq_emb = model.encode(["" + question] + questions) + + emb_with_scores = tuple(zip(questions, map(lambda x: np.linalg.norm(x - q_emb), faq_emb))) + + sorted_embeddings = tuple(sorted(filter(lambda x: x[1] < 10, emb_with_scores), key=lambda x: x[1])) + + if len(sorted_embeddings) > 0: + return sorted_embeddings[0][0].removeprefix("") + return None diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline.py b/examples/frequently_asked_question_bot/web/web/bot/pipeline.py new file mode 100644 index 000000000..ff4af7d2f --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/pipeline.py @@ -0,0 +1,23 @@ +import os + +from dff.pipeline import Pipeline +from dff.context_storages import context_storage_factory + +from .dialog_graph import script +from .pipeline_services import pre_services + + +db_uri = "postgresql+asyncpg://{}:{}@db:5432/{}".format( + os.getenv("POSTGRES_USERNAME"), + os.getenv("POSTGRES_PASSWORD"), + os.getenv("POSTGRES_DB"), +) +db = context_storage_factory(db_uri) + + +pipeline: Pipeline = Pipeline.from_script( + **script.pipeline_kwargs, + context_storage=db, + # pre-services run before bot sends a response + pre_services=pre_services.services, +) diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py new file mode 100644 index 000000000..01829012e --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py @@ -0,0 +1,30 @@ +""" +Pre Services +--- +This module defines services that process user requests before script transition. +""" +from dff.script import Context + +from ..faq_model.model import find_similar_question + + +def question_processor(ctx: Context): + """Store the most similar question to user's query in the `annotations` field of a message.""" + last_request = ctx.last_request + if last_request is None or last_request.text is None: + return + else: + if last_request.annotations is None: + last_request.annotations = {} + else: + if last_request.annotations.get("similar_question") is not None: + return + if last_request.text is None: + last_request.annotations["similar_question"] = None + else: + last_request.annotations["similar_question"] = find_similar_question(last_request.text) + + ctx.set_last_request(last_request) + + +services = [question_processor] # pre-services run before bot sends a response diff --git a/examples/frequently_asked_question_bot/web/web/bot/test.py b/examples/frequently_asked_question_bot/web/web/bot/test.py new file mode 100644 index 000000000..191a1c26b --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/bot/test.py @@ -0,0 +1,39 @@ +import pytest +from dff.utils.testing.common import check_happy_path +from dff.script import Message +from dff.pipeline import Pipeline + +from .dialog_graph import script +from .pipeline_services import pre_services +from .dialog_graph.responses import get_bot_answer, FALLBACK_ANSWER, FIRST_MESSAGE + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "happy_path", + [ + ( + ( + Message(), + FIRST_MESSAGE, + ), + ( + Message(text="Why use arch?"), + get_bot_answer("Why would I want to use Arch?"), + ), + ( + Message(text="What is arch linux?"), + get_bot_answer("What is Arch Linux?"), + ), + ( + Message(text="where am I?"), + FALLBACK_ANSWER, + ), + ) + ], +) +async def test_happy_path(happy_path): + check_happy_path( + pipeline=Pipeline.from_script(**script.pipeline_kwargs, pre_services=pre_services.services), + happy_path=happy_path + ) diff --git a/examples/frequently_asked_question_bot/web/web/requirements.txt b/examples/frequently_asked_question_bot/web/web/requirements.txt new file mode 100644 index 000000000..a538182c0 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/requirements.txt @@ -0,0 +1,5 @@ +dff[tests, postgresql]>=0.3.1 +sentence_transformers==2.2.2 +uvicorn==0.21.1 +fastapi==0.95.1 +websockets==11.0.2 \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/web/static/LICENSE.txt b/examples/frequently_asked_question_bot/web/web/static/LICENSE.txt new file mode 100644 index 000000000..713fa215c --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/static/LICENSE.txt @@ -0,0 +1,8 @@ +Copyright (c) 2023 by neil kalman (https://codepen.io/thatkookooguy/pen/VPJpaW) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/examples/frequently_asked_question_bot/web/web/static/index.css b/examples/frequently_asked_question_bot/web/web/static/index.css new file mode 100644 index 000000000..ad782ecc3 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/static/index.css @@ -0,0 +1,254 @@ +@import 'https://fonts.googleapis.com/css?family=Noto+Sans'; +* { + box-sizing: border-box; +} + +body { + background: skyblue; + font: 12px/16px "Noto Sans", sans-serif; +} + +.floating-chat { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: white; + position: fixed; + bottom: 10px; + right: 10px; + width: 40px; + height: 40px; + transform: translateY(70px); + transition: all 250ms ease-out; + border-radius: 50%; + opacity: 0; + background: -moz-linear-gradient(-45deg, #183850 0, #183850 25%, #192C46 50%, #22254C 75%, #22254C 100%); + background: -webkit-linear-gradient(-45deg, #183850 0, #183850 25%, #192C46 50%, #22254C 75%, #22254C 100%); + background-repeat: no-repeat; + background-attachment: fixed; +} +.floating-chat.enter:hover { + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + opacity: 1; +} +.floating-chat.enter { + transform: translateY(0); + opacity: 0.6; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.12), 0px 1px 2px rgba(0, 0, 0, 0.14); +} +.floating-chat.expand { + width: 250px; + max-height: 400px; + height: 400px; + border-radius: 5px; + cursor: auto; + opacity: 1; +} +.floating-chat :focus { + outline: 0; + box-shadow: 0 0 3pt 2pt rgba(14, 200, 121, 0.3); +} +.floating-chat button { + background: transparent; + border: 0; + color: white; + text-transform: uppercase; + border-radius: 3px; + cursor: pointer; +} +.floating-chat .chat { + display: flex; + flex-direction: column; + position: absolute; + opacity: 0; + width: 1px; + height: 1px; + border-radius: 50%; + transition: all 250ms ease-out; + margin: auto; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +.floating-chat .chat.enter { + opacity: 1; + border-radius: 0; + margin: 10px; + width: auto; + height: auto; +} +.floating-chat .chat .header { + flex-shrink: 0; + padding-bottom: 10px; + display: flex; + background: transparent; +} +.floating-chat .chat .header .title { + flex-grow: 1; + flex-shrink: 1; + padding: 0 5px; +} +.floating-chat .chat .header button { + flex-shrink: 0; +} +.floating-chat .chat .messages { + padding: 10px; + margin: 0; + list-style: none; + overflow-y: scroll; + overflow-x: hidden; + flex-grow: 1; + border-radius: 4px; + background: transparent; +} +.floating-chat .chat .messages::-webkit-scrollbar { + width: 5px; +} +.floating-chat .chat .messages::-webkit-scrollbar-track { + border-radius: 5px; + background-color: rgba(25, 147, 147, 0.1); +} +.floating-chat .chat .messages::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(25, 147, 147, 0.2); +} +.floating-chat .chat .messages li { + position: relative; + clear: both; + display: inline-block; + padding: 14px; + margin: 0 0 20px 0; + font: 12px/16px "Noto Sans", sans-serif; + border-radius: 10px; + background-color: rgba(25, 147, 147, 0.2); + word-wrap: break-word; + max-width: 81%; +} +.floating-chat .chat .messages li:before { + position: absolute; + top: 0; + width: 25px; + height: 25px; + border-radius: 25px; + content: ""; + background-size: cover; +} +.floating-chat .chat .messages li:after { + position: absolute; + top: 10px; + content: ""; + width: 0; + height: 0; + border-top: 10px solid rgba(25, 147, 147, 0.2); +} +.floating-chat .chat .messages li.bot { + animation: show-chat-odd 0.15s 1 ease-in; + -moz-animation: show-chat-odd 0.15s 1 ease-in; + -webkit-animation: show-chat-odd 0.15s 1 ease-in; + float: right; + margin-right: 45px; + color: #0AD5C1; +} +.floating-chat .chat .messages li.bot:before { + right: -45px; + background-image: url(https://thumb.tildacdn.com/tild3665-3130-4938-a265-363663393337/-/resize/264x/-/format/webp/_DeepPavlov_200x200-.png); +} +.floating-chat .chat .messages li.bot:after { + border-right: 10px solid transparent; + right: -10px; +} +.floating-chat .chat .messages li.user { + animation: show-chat-even 0.15s 1 ease-in; + -moz-animation: show-chat-even 0.15s 1 ease-in; + -webkit-animation: show-chat-even 0.15s 1 ease-in; + float: left; + margin-left: 45px; + color: #0EC879; +} +.floating-chat .chat .messages li.user:before { + left: -45px; + background-image: url(https://lens-storage.storage.googleapis.com/png/2fa7d0ae96604dca94fb71f298d31dc8); +} +.floating-chat .chat .messages li.user:after { + border-left: 10px solid transparent; + left: -10px; +} +.floating-chat .chat .footer { + flex-shrink: 0; + display: flex; + padding-top: 10px; + max-height: 90px; + background: transparent; +} +.floating-chat .chat .footer .text-box { + border-radius: 3px; + background: rgba(25, 147, 147, 0.2); + min-height: 100%; + width: 100%; + margin-right: 5px; + color: #0EC879; + overflow-y: auto; + padding: 2px 5px; +} +.floating-chat .chat .footer .text-box::-webkit-scrollbar { + width: 5px; +} +.floating-chat .chat .footer .text-box::-webkit-scrollbar-track { + border-radius: 5px; + background-color: rgba(25, 147, 147, 0.1); +} +.floating-chat .chat .footer .text-box::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(25, 147, 147, 0.2); +} + +@keyframes show-chat-even { + 0% { + margin-left: -480px; + } + 100% { + margin-left: 0; + } +} +@-moz-keyframes show-chat-even { + 0% { + margin-left: -480px; + } + 100% { + margin-left: 0; + } +} +@-webkit-keyframes show-chat-even { + 0% { + margin-left: -480px; + } + 100% { + margin-left: 0; + } +} +@keyframes show-chat-odd { + 0% { + margin-right: -480px; + } + 100% { + margin-right: 0; + } +} +@-moz-keyframes show-chat-odd { + 0% { + margin-right: -480px; + } + 100% { + margin-right: 0; + } +} +@-webkit-keyframes show-chat-odd { + 0% { + margin-right: -480px; + } + 100% { + margin-right: 0; + } +} \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/web/static/index.html b/examples/frequently_asked_question_bot/web/web/static/index.html new file mode 100644 index 000000000..034761dc4 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/static/index.html @@ -0,0 +1,33 @@ + + + + Chat + + + + +
+ +
+
+ + FAQ Bot + + + +
+
    +
+ +
+
+ + + + + diff --git a/examples/frequently_asked_question_bot/web/web/static/index.js b/examples/frequently_asked_question_bot/web/web/static/index.js new file mode 100644 index 000000000..32f89e8d3 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/web/static/index.js @@ -0,0 +1,105 @@ +var element = $('.floating-chat'); +var client_id = createUUID(); + +/* +Here Websocket URI is computed using current location. (https://stackoverflow.com/a/10418013) +I don't know how reliable that is, in prod it is probably +better to use hardcoded uri, e.g. ws_uri = "ws://example.com/ws/..." +*/ +var loc = window.location, ws_uri; +if (loc.protocol === "https:") { + ws_uri = "wss:"; +} else { + ws_uri = "ws:"; +} +ws_uri += "//" + loc.host; +ws_uri += loc.pathname + "ws/" + client_id; + +var ws = new WebSocket(ws_uri); +ws.onmessage = receiveBotMessage; + +setTimeout(function() { + element.addClass('enter'); +}, 1000); + +element.click(openElement); + +function openElement() { + var messages = element.find('.messages'); + var textInput = element.find('.text-box'); + element.find('>i').hide(); + element.addClass('expand'); + element.find('.chat').addClass('enter'); + var strLength = textInput.val().length * 2; + textInput.keydown(onMetaAndEnter).prop("disabled", false).focus(); + element.off('click', openElement); + element.find('.header button').click(closeElement); + element.find('#sendMessage').click(sendNewMessage); + messages.scrollTop(messages.prop("scrollHeight")); +} + +function closeElement() { + element.find('.chat').removeClass('enter').hide(); + element.find('>i').show(); + element.removeClass('expand'); + element.find('.header button').off('click', closeElement); + element.find('#sendMessage').off('click', sendNewMessage); + element.find('.text-box').off('keydown', onMetaAndEnter).prop("disabled", true).blur(); + setTimeout(function() { + element.find('.chat').removeClass('enter').show() + element.click(openElement); + }, 500); +} + +function createUUID() { + // http://www.ietf.org/rfc/rfc4122.txt + var s = []; + var hexDigits = "0123456789abcdef"; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010 + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01 + s[8] = s[13] = s[18] = s[23] = "-"; + + var uuid = s.join(""); + return uuid; +} + +function addMessageToContainer(messageToAdd, type) { + var newMessage = messageToAdd; + + if (!newMessage) return; + + var messagesContainer = $('.messages'); + + messagesContainer.append([ + `
  • `, + newMessage, + '
  • ' + ].join('')); + + messagesContainer.animate({ + scrollTop: messagesContainer.prop("scrollHeight") + }, 250); + + return newMessage; +} + +function sendNewMessage() { + var input = document.getElementById("messageText"); + var result = addMessageToContainer(input.value, "user"); + ws.send(result); + input.value = ''; + event.preventDefault(); +} + +function receiveBotMessage(event) { + addMessageToContainer(event.data, "bot"); +} + +function onMetaAndEnter(event) { + if (event.keyCode == 13) { + sendNewMessage(); + } +} \ No newline at end of file From fb9ebc939537bb54bbf7059679cfaeb809d7be33 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 23 Nov 2023 13:44:38 +0300 Subject: [PATCH 02/18] Apply suggestions to customer_service_bot && FAQ bot --- examples/README.md | 11 ++------ examples/customer_service_bot/README.md | 28 ++++++++++--------- .../customer_service_bot/bot/api/chatgpt.py | 11 ++++++-- .../customer_service_bot/bot/requirements.txt | 2 +- examples/customer_service_bot/bot/test.py | 2 +- .../bot/pipeline_services/pre_services.py | 2 +- .../telegram/bot/requirements.txt | 2 +- .../web/docker-compose.yml | 8 +++++- .../web/bot/pipeline_services/pre_services.py | 2 +- .../web/web/requirements.txt | 4 +-- 10 files changed, 40 insertions(+), 32 deletions(-) diff --git a/examples/README.md b/examples/README.md index 4c02cd119..604c59de0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,10 +1,5 @@ -# DFF examples +# DFF example projects -This repository contains examples of bots build using [DFF](https://github.com/deeppavlov/dialog_flow_framework) (Dialog Flow Framework). +This directory contains examples of bots built using [DFF](https://github.com/deeppavlov/dialog_flow_framework) (Dialog Flow Framework). -The Dialog Flow Framework (DFF) allows you to write conversational services. The service is written by defining a -special dialog graph that describes the behavior of the dialog service. The dialog graph contains the dialog script. -DFF offers a specialized language (DSL) for quickly writing dialog graphs. -You can use it in services such as writing skills for Amazon Alexa, etc., chatbots for social networks, website call centers, etc. - -In this repository, two bots are presented as examples: faq bot and customer service bot. Both bots use Telegram as an interface. +The example projects include a FAQ bot for potential Linux users and a customer service bot for a book shop. Both bots use Telegram as an interface. diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md index 1b1eb9ad0..bc2ad362c 100644 --- a/examples/customer_service_bot/README.md +++ b/examples/customer_service_bot/README.md @@ -5,17 +5,17 @@ Customer service bot built on `DFF`. Uses telegram as an interface. This bot is designed to answer any type of user questions in a limited business domain (book shop). -* [DeepPavlov Intent Catcher](#) force is used for intent retrieval. +* [DeepPavlov Intent Catcher](#) is used for intent retrieval. * [ChatGPT](https://openai.com/pricing#language-models) is used for context based question answering. ### Intent Catcher Intent catcher is a DistilBERT-based classifier for user intent classes. -We use DeepPavlov library for seamless training and inference. +We use the DeepPavlov library for a seamless training and inference experience. Sample code for training the model can be found in `Training_intent_catcher.ipynb`. The model is deployed as a separate microservice running at port 4999. -Service bot interacts with the container via `/respond` endpoint. +The bot interacts with the container via `/respond` endpoint. The API expects a json object with the dialog history passed as an array and labeled 'dialog_contexts'. Intents will be extracted from the last utterance. ```json @@ -38,27 +38,29 @@ docker compose up --build --abort-on-container-exit --exit-code-from intent_clie ## Run the bot ### Run with Docker & Docker-Compose environment -In order for the bot to work, set the bot token via [.env](.env.example). You should start by creating your own `.env` file: +To interact with external APIs, the bot requires API tokens that can be set through [.env](.env.example). You should start by creating your own `.env` file: ``` echo TG_BOT_TOKEN=*** >> .env echo OPENAI_API_TOKEN=*** >> .env ``` -Build the bot: +*The commands below need to be run from the /examples/customer_service_bot directory* + +Building the bot and launching it in the background can be done with a single command given that the environment variables have been configured correctly. Then you can immediately interact with your bot in Telegram. ```commandline -docker-compose build +docker-compose up -d ``` -Testing the bot: + +If any of the source files have received updates, you can rebuild and sync the bot using the docker-compose build command. ```commandline -docker-compose run assistant pytest test.py +docker compose build ``` - -Running the bot: +In case of bugs, you can test whether the bot correctly handles basic functionality using the following command: ```commandline -docker-compose run assistant python run.py +docker compose run assistant pytest test.py ``` -Running in background +The bot can also be run as a self-standing service, i.e. without the intent catcher for a less resource-demanding workflow: ```commandline -docker-compose up -d +docker compose run assistant python run.py ``` diff --git a/examples/customer_service_bot/bot/api/chatgpt.py b/examples/customer_service_bot/bot/api/chatgpt.py index 7ba3fb69f..039192ec9 100644 --- a/examples/customer_service_bot/bot/api/chatgpt.py +++ b/examples/customer_service_bot/bot/api/chatgpt.py @@ -7,7 +7,7 @@ import openai CHATGPT_MAIN_PROMPT = """ -You are a helpful assistant for a book shop "Book Lovers Paradise". +Pretend that you are a member of staff for a book shop "Book Lovers Paradise". Located at 123 Main Street. Open seven days a week, from 9 AM to 9 PM. Extensive collection of genres, including fiction, and non-fiction. @@ -15,10 +15,15 @@ Comfortable seating areas and peaceful atmosphere. Refund policy within 30 days of purchase. Loyalty program for frequent customers (10% off purchases). -""" # shortened the prompt to reduce token consumption. +As a member of staff, you have access to information +outside of this description, e.g. you can make it up. +Don't mention your role in your replies, so that they seem natural. +""" CHATGPT_QUESTION_PROMPT = """ -What follows is a user query: answer if related to the given description or deny if unrelated. +What follows is a user query: answer it if you can do so +as a book shop's member of staff. If the query is unrelated to +your role, deny it. """ CHATGPT_COHERENCE_PROMPT = """ diff --git a/examples/customer_service_bot/bot/requirements.txt b/examples/customer_service_bot/bot/requirements.txt index d8835d1d7..b0275c119 100644 --- a/examples/customer_service_bot/bot/requirements.txt +++ b/examples/customer_service_bot/bot/requirements.txt @@ -1,4 +1,4 @@ -dff[telegram, tests] >= 0.4 +dff[telegram, tests] >= 0.6.3 itsdangerous==2.0.1 gunicorn==19.9.0 sentry-sdk[flask]==0.14.1 diff --git a/examples/customer_service_bot/bot/test.py b/examples/customer_service_bot/bot/test.py index c06edaf6a..7940a5795 100644 --- a/examples/customer_service_bot/bot/test.py +++ b/examples/customer_service_bot/bot/test.py @@ -32,7 +32,7 @@ ( TelegramMessage(text="card"), Message( - text="We registered your transaction. Requested titles are: 'Pale Fire', 'Lolita'. Delivery method: deliver. Payment method: card. Type `abort` to cancel, type `ok` to continue." + text="We registered your transaction. Requested titles are: Pale Fire, Lolita. Delivery method: deliver. Payment method: card. Type `abort` to cancel, type `ok` to continue." ), ), (TelegramMessage(text="ok"), script["chitchat_flow"]["init_chitchat"][RESPONSE]), diff --git a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py index de0892965..ebaefb079 100644 --- a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py +++ b/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py @@ -24,7 +24,7 @@ def question_processor(ctx: Context): else: last_request.annotations["similar_questions"] = find_similar_questions(last_request.text) - ctx.set_last_request(last_request) + ctx.last_request = last_request services = [question_processor] # pre-services run before bot sends a response diff --git a/examples/frequently_asked_question_bot/telegram/bot/requirements.txt b/examples/frequently_asked_question_bot/telegram/bot/requirements.txt index ee3746494..ee51a8a82 100644 --- a/examples/frequently_asked_question_bot/telegram/bot/requirements.txt +++ b/examples/frequently_asked_question_bot/telegram/bot/requirements.txt @@ -1,2 +1,2 @@ -dff[telegram, tests]>=0.4 +dff[telegram, tests]>=0.6.3 sentence_transformers==2.2.2 \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/docker-compose.yml b/examples/frequently_asked_question_bot/web/docker-compose.yml index 0aaf57ff4..470af8fdf 100644 --- a/examples/frequently_asked_question_bot/web/docker-compose.yml +++ b/examples/frequently_asked_question_bot/web/docker-compose.yml @@ -11,11 +11,17 @@ services: env_file: - ./.env depends_on: - - db + db: + condition: service_healthy db: env_file: [.env] image: postgres:latest restart: unless-stopped + healthcheck: + test: pg_isready --username=$${POSTGRES_USERNAME} + interval: 4s + timeout: 3s + retries: 3 volumes: - postgres_data:/var/lib/postgresql/data/ nginx: diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py index 01829012e..34f2f07b5 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py +++ b/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py @@ -24,7 +24,7 @@ def question_processor(ctx: Context): else: last_request.annotations["similar_question"] = find_similar_question(last_request.text) - ctx.set_last_request(last_request) + ctx.last_request = last_request services = [question_processor] # pre-services run before bot sends a response diff --git a/examples/frequently_asked_question_bot/web/web/requirements.txt b/examples/frequently_asked_question_bot/web/web/requirements.txt index a538182c0..f505ec9d9 100644 --- a/examples/frequently_asked_question_bot/web/web/requirements.txt +++ b/examples/frequently_asked_question_bot/web/web/requirements.txt @@ -1,5 +1,5 @@ -dff[tests, postgresql]>=0.3.1 +dff[tests, postgresql]>=0.6.3 sentence_transformers==2.2.2 uvicorn==0.21.1 -fastapi==0.95.1 +fastapi>=0.95.1 websockets==11.0.2 \ No newline at end of file From 1607b77c0477f25746936e306a53dccd19b40099 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 23 Nov 2023 13:48:36 +0300 Subject: [PATCH 03/18] Update README.md --- examples/customer_service_bot/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md index bc2ad362c..c223b4bd2 100644 --- a/examples/customer_service_bot/README.md +++ b/examples/customer_service_bot/README.md @@ -5,7 +5,7 @@ Customer service bot built on `DFF`. Uses telegram as an interface. This bot is designed to answer any type of user questions in a limited business domain (book shop). -* [DeepPavlov Intent Catcher](#) is used for intent retrieval. +* [DeepPavlov Intent Catcher](https://docs.deeppavlov.ai/en/0.14.1/features/models/intent_catcher.html) is used for intent retrieval. * [ChatGPT](https://openai.com/pricing#language-models) is used for context based question answering. ### Intent Catcher From ac1ef93f1704dbf017dd89fc06a51d48e7db2d63 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 23 Nov 2023 13:41:36 +0000 Subject: [PATCH 04/18] Update readmes; add .env files; do formatting --- .gitignore | 1 - .../customer_service_bot/{.env.example => .env} | 0 examples/customer_service_bot/README.md | 6 +++--- .../customer_service_bot/bot/dialog_graph/script.py | 2 +- examples/customer_service_bot/bot/test.py | 4 +++- .../customer_service_bot/intent_catcher/server.py | 8 ++++---- .../frequently_asked_question_bot/telegram/.env | 1 + .../telegram/.env.example | 1 - .../telegram/README.md | 6 ++++-- examples/frequently_asked_question_bot/web/.env | 3 +++ .../frequently_asked_question_bot/web/README.md | 7 ++++++- .../frequently_asked_question_bot/web/web/app.py | 9 +++------ .../web/web/bot/dialog_graph/responses.py | 13 ++++--------- .../web/web/bot/dialog_graph/script.py | 3 +-- .../web/web/bot/test.py | 2 +- 15 files changed, 34 insertions(+), 32 deletions(-) rename examples/customer_service_bot/{.env.example => .env} (100%) create mode 100644 examples/frequently_asked_question_bot/telegram/.env delete mode 100644 examples/frequently_asked_question_bot/telegram/.env.example create mode 100644 examples/frequently_asked_question_bot/web/.env diff --git a/.gitignore b/.gitignore index bfe56cd1d..a0f41b0cd 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,3 @@ dbs benchmarks benchmark_results_files.json uploaded_benchmarks -**/.env diff --git a/examples/customer_service_bot/.env.example b/examples/customer_service_bot/.env similarity index 100% rename from examples/customer_service_bot/.env.example rename to examples/customer_service_bot/.env diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md index c223b4bd2..ab0672554 100644 --- a/examples/customer_service_bot/README.md +++ b/examples/customer_service_bot/README.md @@ -38,10 +38,10 @@ docker compose up --build --abort-on-container-exit --exit-code-from intent_clie ## Run the bot ### Run with Docker & Docker-Compose environment -To interact with external APIs, the bot requires API tokens that can be set through [.env](.env.example). You should start by creating your own `.env` file: +To interact with external APIs, the bot requires API tokens that can be set through the [.env](.env) file. Update it replacing templates with actual token values. ``` -echo TG_BOT_TOKEN=*** >> .env -echo OPENAI_API_TOKEN=*** >> .env +TG_BOT_TOKEN=*** +OPENAI_API_TOKEN=*** ``` *The commands below need to be run from the /examples/customer_service_bot directory* diff --git a/examples/customer_service_bot/bot/dialog_graph/script.py b/examples/customer_service_bot/bot/dialog_graph/script.py index 5d31175ad..f641da159 100644 --- a/examples/customer_service_bot/bot/dialog_graph/script.py +++ b/examples/customer_service_bot/bot/dialog_graph/script.py @@ -61,7 +61,7 @@ }, "ask_item": { RESPONSE: Message( - text="Which books would you like to order? Please, separate the titles by commas (type 'abort' to cancel)." + text="Which books would you like to order? Separate the titles by commas (type 'abort' to cancel)" ), PRE_TRANSITIONS_PROCESSING: {"1": loc_prc.extract_item()}, TRANSITIONS: {("form_flow", "ask_delivery"): loc_cnd.slots_filled(["items"]), lbl.repeat(0.8): cnd.true()}, diff --git a/examples/customer_service_bot/bot/test.py b/examples/customer_service_bot/bot/test.py index 7940a5795..b9a19660a 100644 --- a/examples/customer_service_bot/bot/test.py +++ b/examples/customer_service_bot/bot/test.py @@ -32,7 +32,9 @@ ( TelegramMessage(text="card"), Message( - text="We registered your transaction. Requested titles are: Pale Fire, Lolita. Delivery method: deliver. Payment method: card. Type `abort` to cancel, type `ok` to continue." + text="We registered your transaction. Requested titles are: Pale Fire, Lolita. " + "Delivery method: deliver. Payment method: card. " + "Type `abort` to cancel, type `ok` to continue." ), ), (TelegramMessage(text="ok"), script["chitchat_flow"]["init_chitchat"][RESPONSE]), diff --git a/examples/customer_service_bot/intent_catcher/server.py b/examples/customer_service_bot/intent_catcher/server.py index e4ae0007a..7d7871960 100644 --- a/examples/customer_service_bot/intent_catcher/server.py +++ b/examples/customer_service_bot/intent_catcher/server.py @@ -35,10 +35,10 @@ else: no_cuda = True model = AutoModelForSequenceClassification.from_config(DistilBertConfig(num_labels=23)) - state = torch.load(MODEL_PATH, map_location = "cpu" if no_cuda else "gpu") + state = torch.load(MODEL_PATH, map_location="cpu" if no_cuda else "gpu") model.load_state_dict(state["model_state_dict"]) tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") - pipe = pipeline('text-classification', model=model, tokenizer=tokenizer) + pipe = pipeline("text-classification", model=model, tokenizer=tokenizer) logger.info("predictor is ready") except Exception as e: sentry_sdk.capture_exception(e) @@ -71,9 +71,9 @@ def respond(): try: results = pipe(contexts) - indices = [int(''.join(filter(lambda x: x.isdigit(), result['label']))) for result in results] + indices = [int("".join(filter(lambda x: x.isdigit(), result["label"]))) for result in results] responses = [list(label2id.keys())[idx] for idx in indices] - confidences = [result['score'] for result in results] + confidences = [result["score"] for result in results] except Exception as exc: logger.exception(exc) sentry_sdk.capture_exception(exc) diff --git a/examples/frequently_asked_question_bot/telegram/.env b/examples/frequently_asked_question_bot/telegram/.env new file mode 100644 index 000000000..6282da982 --- /dev/null +++ b/examples/frequently_asked_question_bot/telegram/.env @@ -0,0 +1 @@ +TG_BOT_TOKEN=tg_bot_token \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/.env.example b/examples/frequently_asked_question_bot/telegram/.env.example deleted file mode 100644 index 016ccd98a..000000000 --- a/examples/frequently_asked_question_bot/telegram/.env.example +++ /dev/null @@ -1 +0,0 @@ -TG_BOT_TOKEN= \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/README.md b/examples/frequently_asked_question_bot/telegram/README.md index ed0b32891..86513db11 100644 --- a/examples/frequently_asked_question_bot/telegram/README.md +++ b/examples/frequently_asked_question_bot/telegram/README.md @@ -12,9 +12,11 @@ An example of bot usage: ![image](https://user-images.githubusercontent.com/61429541/219064505-20e67950-cb88-4cff-afa5-7ce608e1282c.png) ### Run with Docker & Docker-Compose environment -In order for the bot to work, set the bot token via [.env](.env.example). First step is creating your `.env` file: + +In order for the bot to work, update the [.env](.env) file replacing the template with the actual value of your Telegram token. + ``` -echo TG_BOT_TOKEN=******* >> .env +TG_BOT_TOKEN=*** ``` Build the bot: diff --git a/examples/frequently_asked_question_bot/web/.env b/examples/frequently_asked_question_bot/web/.env new file mode 100644 index 000000000..813946eb4 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/.env @@ -0,0 +1,3 @@ +POSTGRES_USERNAME=postgres +POSTGRES_PASSWORD=pass +POSTGRES_DB=test \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/README.md b/examples/frequently_asked_question_bot/web/README.md index 36c42054c..5576f69d3 100644 --- a/examples/frequently_asked_question_bot/web/README.md +++ b/examples/frequently_asked_question_bot/web/README.md @@ -14,7 +14,12 @@ A showcase of the website: ![faq_web](https://user-images.githubusercontent.com/61429541/233875303-b9bc81c9-522b-4596-8599-6efcfa708d1e.gif) ### Run with Docker & Docker-Compose environment - +The Postgresql image needs to be configured with variables that can be set through the [.env](.env) file. Update the file replacing templates with desired values. +``` +POSTGRES_USERNAME=*** +POSTGRES_PASSWORD=*** +POSTGRES_DB=*** +``` Build the bot: ```commandline docker-compose build diff --git a/examples/frequently_asked_question_bot/web/web/app.py b/examples/frequently_asked_question_bot/web/web/app.py index b96e9010f..5423fc666 100644 --- a/examples/frequently_asked_question_bot/web/web/app.py +++ b/examples/frequently_asked_question_bot/web/web/app.py @@ -11,7 +11,7 @@ @app.get("/") async def index(): - return FileResponse('static/index.html', media_type='text/html') + return FileResponse("static/index.html", media_type="text/html") @app.websocket("/ws/{client_id}") @@ -20,11 +20,7 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str): # store user info in the dialogue context await pipeline.context_storage.set_item_async( - client_id, - Context( - id=client_id, - misc={"ip": websocket.client.host, "headers": websocket.headers.raw} - ) + client_id, Context(id=client_id, misc={"ip": websocket.client.host, "headers": websocket.headers.raw}) ) async def respond(request: Message): @@ -42,6 +38,7 @@ async def respond(request: Message): except WebSocketDisconnect: # ignore disconnects pass + if __name__ == "__main__": uvicorn.run( app, diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py index fd27c3d54..696199c80 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py +++ b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py @@ -16,20 +16,15 @@ def get_bot_answer(question: str) -> Message: FALLBACK_ANSWER = Message( - text='I don\'t have an answer to that question. ' - 'You can find FAQ here.', + text="I don't have an answer to that question. " + 'You can find FAQ here.', ) """Fallback answer that the bot returns if user's query is not similar to any of the questions.""" -FIRST_MESSAGE = Message( - text="Welcome! Ask me questions about Arch Linux." -) +FIRST_MESSAGE = Message(text="Welcome! Ask me questions about Arch Linux.") -FALLBACK_NODE_MESSAGE = Message( - text="Something went wrong.\n" - "You may continue asking me questions about Arch Linux." -) +FALLBACK_NODE_MESSAGE = Message(text="Something went wrong.\n" "You may continue asking me questions about Arch Linux.") def answer_similar_question(ctx: Context, _: Pipeline): diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py index 3b8ecfa47..8c83b9d0c 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py +++ b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py @@ -28,12 +28,11 @@ }, "service_flow": { "start_node": {}, # this is the start node, it simply redirects to welcome node - "fallback_node": { # this node will only be used if something goes wrong (e.g. an exception is raised) RESPONSE: FALLBACK_NODE_MESSAGE, }, }, }, "start_label": ("service_flow", "start_node"), - "fallback_label": ("service_flow", "fallback_node") + "fallback_label": ("service_flow", "fallback_node"), } diff --git a/examples/frequently_asked_question_bot/web/web/bot/test.py b/examples/frequently_asked_question_bot/web/web/bot/test.py index 191a1c26b..b14f647cb 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/test.py +++ b/examples/frequently_asked_question_bot/web/web/bot/test.py @@ -35,5 +35,5 @@ async def test_happy_path(happy_path): check_happy_path( pipeline=Pipeline.from_script(**script.pipeline_kwargs, pre_services=pre_services.services), - happy_path=happy_path + happy_path=happy_path, ) From 30be03d54571421c2af293e433e4ef1e1cdb9259 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 23 Nov 2023 17:40:20 +0300 Subject: [PATCH 05/18] Update README.md --- examples/customer_service_bot/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md index ab0672554..778fc4e39 100644 --- a/examples/customer_service_bot/README.md +++ b/examples/customer_service_bot/README.md @@ -35,15 +35,16 @@ Run the intent catcher: docker compose up --build --abort-on-container-exit --exit-code-from intent_client ``` -## Run the bot +## Running the bot -### Run with Docker & Docker-Compose environment +### Step 1: Configuring the docker services To interact with external APIs, the bot requires API tokens that can be set through the [.env](.env) file. Update it replacing templates with actual token values. ``` TG_BOT_TOKEN=*** OPENAI_API_TOKEN=*** ``` +### Step 2: Launching the project *The commands below need to be run from the /examples/customer_service_bot directory* Building the bot and launching it in the background can be done with a single command given that the environment variables have been configured correctly. Then you can immediately interact with your bot in Telegram. From 4d25513064c664f8835aec18dfb84ba9dd54ad09 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 23 Nov 2023 17:46:16 +0300 Subject: [PATCH 06/18] Update README.md --- .../telegram/README.md | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/examples/frequently_asked_question_bot/telegram/README.md b/examples/frequently_asked_question_bot/telegram/README.md index 86513db11..5ce066602 100644 --- a/examples/frequently_asked_question_bot/telegram/README.md +++ b/examples/frequently_asked_question_bot/telegram/README.md @@ -11,7 +11,9 @@ An example of bot usage: ![image](https://user-images.githubusercontent.com/61429541/219064505-20e67950-cb88-4cff-afa5-7ce608e1282c.png) -### Run with Docker & Docker-Compose environment +## Running the bot in docker + +### Step 1: Configuring the docker services In order for the bot to work, update the [.env](.env) file replacing the template with the actual value of your Telegram token. @@ -19,6 +21,9 @@ In order for the bot to work, update the [.env](.env) file replacing the templat TG_BOT_TOKEN=*** ``` +## Step 2: Launching the docker project +*The commands below should be run from the /examples/frequently_asked_question_bot/telegram directory.* + Build the bot: ```commandline docker-compose build @@ -37,22 +42,29 @@ Running in background ```commandline docker-compose up -d ``` -### Run with Python environment -In order for the bot to work, set the bot token, example is in [.env](.env.example). First step is setting environment variables: +## Running the bot in the local Python environment + +### Step 1: Configuring the service + +In order for the bot to work, update the [.env](.env) file replacing the template with the actual value of your Telegram token. + ``` -export TG_BOT_TOKEN=******* +TG_BOT_TOKEN=*** ``` +### Step 2: Installing dependencies Build the bot: ```commandline pip3 install -r requirements.txt ``` -Testing the bot: -```commandline -pytest test.py -``` +### Step 3: Runnig with CLI Running the bot: ```commandline python run.py -``` \ No newline at end of file +``` + +To launch the test suite, run: +```commandline +pytest test.py +``` From 805b778da0d615f28cab2c581e55b3ff8663a987 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 23 Nov 2023 17:49:30 +0300 Subject: [PATCH 07/18] Update README.md --- .../frequently_asked_question_bot/web/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/frequently_asked_question_bot/web/README.md b/examples/frequently_asked_question_bot/web/README.md index 5576f69d3..fbed6837b 100644 --- a/examples/frequently_asked_question_bot/web/README.md +++ b/examples/frequently_asked_question_bot/web/README.md @@ -13,19 +13,21 @@ Whenever a user asks a question it searches for the most similar question in its A showcase of the website: ![faq_web](https://user-images.githubusercontent.com/61429541/233875303-b9bc81c9-522b-4596-8599-6efcfa708d1e.gif) -### Run with Docker & Docker-Compose environment +## Running the project + +### Step 1: Configuring docker services + The Postgresql image needs to be configured with variables that can be set through the [.env](.env) file. Update the file replacing templates with desired values. ``` POSTGRES_USERNAME=*** POSTGRES_PASSWORD=*** POSTGRES_DB=*** ``` -Build the bot: -```commandline -docker-compose build -``` -Running in background +### Step 2: Launching the docker project +*The commands below should be run from the /examples/frequently_asked_question_bot/web directory.* + +Launching the project ```commandline -docker-compose up -d +docker-compose up --build -d ``` From ab85f1c8888307e0d1664424e3946b964d288512 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Fri, 24 Nov 2023 09:04:11 +0000 Subject: [PATCH 08/18] create documentation placeholders --- docs/source/examples.rst | 16 ++++++++++++++- docs/source/examples/customer_service_bot.rst | 20 +++++++++++++++++++ docs/source/examples/faq_bot.rst | 20 +++++++++++++++++++ examples/customer_service_bot/README.md | 2 +- .../telegram/README.md | 4 ++-- .../web/README.md | 2 +- 6 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 docs/source/examples/customer_service_bot.rst create mode 100644 docs/source/examples/faq_bot.rst diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 6c5854dbc..941961cf6 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -1,4 +1,18 @@ Examples -------- -Examples are available in this `repository `_. +:doc:`FAQ bot <./examples/faq_bot>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FAQ bot + +:doc:`Customer service bot <./examples/customer_service_bot>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Customer service bot + +.. toctree:: + :hidden: + + examples/faq_bot + examples/customer_service_bot diff --git a/docs/source/examples/customer_service_bot.rst b/docs/source/examples/customer_service_bot.rst new file mode 100644 index 000000000..165864b48 --- /dev/null +++ b/docs/source/examples/customer_service_bot.rst @@ -0,0 +1,20 @@ +Customer service bot +-------------------- + +.. code-block:: shell + + project/ + ├── myapp + │ ├── dialog_graph + │ │ ├── __init__.py + │ │ ├── conditions.py # Condition callbacks + │ │ ├── processing.py # Processing callbacks + │ │ ├── response.py # Response callbacks + │ │ └── script.py # DFF script and pipeline are constructed here + │ ├── dockerfile + │ ├── requirements.txt + │ ├── web_app.py # the web app imports the DFF pipeline from dialog_graph + │ └── test.py # End-to-end testing happy path is defined here + ├── ...Folders for other docker-based services, if applicable + ├── venv/ + └── docker-compose.yml \ No newline at end of file diff --git a/docs/source/examples/faq_bot.rst b/docs/source/examples/faq_bot.rst new file mode 100644 index 000000000..bb3eaca03 --- /dev/null +++ b/docs/source/examples/faq_bot.rst @@ -0,0 +1,20 @@ +FAQ Bot +------- + +.. code-block:: shell + + project/ + ├── myapp + │ ├── dialog_graph + │ │ ├── __init__.py + │ │ ├── conditions.py # Condition callbacks + │ │ ├── processing.py # Processing callbacks + │ │ ├── response.py # Response callbacks + │ │ └── script.py # DFF script and pipeline are constructed here + │ ├── dockerfile + │ ├── requirements.txt + │ ├── web_app.py # the web app imports the DFF pipeline from dialog_graph + │ └── test.py # End-to-end testing happy path is defined here + ├── ...Folders for other docker-based services, if applicable + ├── venv/ + └── docker-compose.yml \ No newline at end of file diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md index 778fc4e39..58c2e9863 100644 --- a/examples/customer_service_bot/README.md +++ b/examples/customer_service_bot/README.md @@ -38,7 +38,7 @@ docker compose up --build --abort-on-container-exit --exit-code-from intent_clie ## Running the bot ### Step 1: Configuring the docker services -To interact with external APIs, the bot requires API tokens that can be set through the [.env](.env) file. Update it replacing templates with actual token values. +To interact with external APIs, the bot requires API tokens that can be set through the [.env](.env) file. Update it replacing the placeholders with actual token values. ``` TG_BOT_TOKEN=*** OPENAI_API_TOKEN=*** diff --git a/examples/frequently_asked_question_bot/telegram/README.md b/examples/frequently_asked_question_bot/telegram/README.md index 5ce066602..3f2375678 100644 --- a/examples/frequently_asked_question_bot/telegram/README.md +++ b/examples/frequently_asked_question_bot/telegram/README.md @@ -15,7 +15,7 @@ An example of bot usage: ### Step 1: Configuring the docker services -In order for the bot to work, update the [.env](.env) file replacing the template with the actual value of your Telegram token. +In order for the bot to work, update the [.env](.env) file replacing the placeholders with the actual value of your Telegram token. ``` TG_BOT_TOKEN=*** @@ -46,7 +46,7 @@ docker-compose up -d ### Step 1: Configuring the service -In order for the bot to work, update the [.env](.env) file replacing the template with the actual value of your Telegram token. +In order for the bot to work, update the [.env](.env) file replacing the placeholders with the actual value of your Telegram token. ``` TG_BOT_TOKEN=*** diff --git a/examples/frequently_asked_question_bot/web/README.md b/examples/frequently_asked_question_bot/web/README.md index fbed6837b..c2eaa114d 100644 --- a/examples/frequently_asked_question_bot/web/README.md +++ b/examples/frequently_asked_question_bot/web/README.md @@ -17,7 +17,7 @@ A showcase of the website: ### Step 1: Configuring docker services -The Postgresql image needs to be configured with variables that can be set through the [.env](.env) file. Update the file replacing templates with desired values. +The Postgresql image needs to be configured with variables that can be set through the [.env](.env) file. Update the file replacing the placeholders with desired values. ``` POSTGRES_USERNAME=*** POSTGRES_PASSWORD=*** From 558d2a839d9a5ed8bf128f3642d80ef9daa30cf3 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Fri, 24 Nov 2023 09:46:40 +0000 Subject: [PATCH 09/18] Update descriptions --- docs/source/examples.rst | 7 +++++-- docs/source/examples/customer_service_bot.rst | 4 ++++ docs/source/examples/faq_bot.rst | 3 +++ examples/customer_service_bot/README.md | 2 +- examples/frequently_asked_question_bot/telegram/README.md | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 941961cf6..9a2d55382 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -4,12 +4,15 @@ Examples :doc:`FAQ bot <./examples/faq_bot>` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -FAQ bot +FAQ bot for Arch Linux users built using `DFF`. +Can be run with Telegram or with a web interface. :doc:`Customer service bot <./examples/customer_service_bot>` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Customer service bot +Customer service bot built using `DFF`. +This bot is designed to answer any type of user questions in a limited business domain (book shop). +Uses a Telegram interface. .. toctree:: :hidden: diff --git a/docs/source/examples/customer_service_bot.rst b/docs/source/examples/customer_service_bot.rst index 165864b48..fbb21eb8b 100644 --- a/docs/source/examples/customer_service_bot.rst +++ b/docs/source/examples/customer_service_bot.rst @@ -1,6 +1,10 @@ Customer service bot -------------------- +Customer service bot built using `DFF`. +This bot is designed to answer any type of user questions in a limited business domain (book shop). +Uses a Telegram interface. + .. code-block:: shell project/ diff --git a/docs/source/examples/faq_bot.rst b/docs/source/examples/faq_bot.rst index bb3eaca03..5a1141e1e 100644 --- a/docs/source/examples/faq_bot.rst +++ b/docs/source/examples/faq_bot.rst @@ -1,6 +1,9 @@ FAQ Bot ------- +FAQ bot for Arch Linux users built using `DFF`. +Can be run with Telegram or with a web interface. + .. code-block:: shell project/ diff --git a/examples/customer_service_bot/README.md b/examples/customer_service_bot/README.md index 58c2e9863..ab2310f30 100644 --- a/examples/customer_service_bot/README.md +++ b/examples/customer_service_bot/README.md @@ -2,7 +2,7 @@ ### Customer service bot -Customer service bot built on `DFF`. Uses telegram as an interface. +Customer service bot built using `DFF`. Uses Telegram as an interface. This bot is designed to answer any type of user questions in a limited business domain (book shop). * [DeepPavlov Intent Catcher](https://docs.deeppavlov.ai/en/0.14.1/features/models/intent_catcher.html) is used for intent retrieval. diff --git a/examples/frequently_asked_question_bot/telegram/README.md b/examples/frequently_asked_question_bot/telegram/README.md index 3f2375678..28e7ddaf4 100644 --- a/examples/frequently_asked_question_bot/telegram/README.md +++ b/examples/frequently_asked_question_bot/telegram/README.md @@ -1,6 +1,6 @@ ## Description -Example FAQ bot built on `dff`. Uses telegram as an interface. +Example FAQ bot built using `dff`. Uses Telegram as an interface. This bot listens for user questions and finds similar questions in its database by using the `clips/mfaq` model. From c9a9299b3732712f6eaf38119c217ac4460815eb Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Fri, 24 Nov 2023 14:16:57 +0000 Subject: [PATCH 10/18] update rst files --- docs/source/examples/customer_service_bot.rst | 50 +++++++++++++---- docs/source/examples/faq_bot.rst | 53 +++++++++++++------ 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/docs/source/examples/customer_service_bot.rst b/docs/source/examples/customer_service_bot.rst index fbb21eb8b..f1a4360e2 100644 --- a/docs/source/examples/customer_service_bot.rst +++ b/docs/source/examples/customer_service_bot.rst @@ -5,20 +5,52 @@ Customer service bot built using `DFF`. This bot is designed to answer any type of user questions in a limited business domain (book shop). Uses a Telegram interface. +Project structure +~~~~~~~~~~~~~~~~~ + +While DFF allows you to choose any structure for your own projects, +we propose a schema of how project files can be meaningfully split +into services and modules. + +* In our projects, we go for docker-based deployment due to its scalability and universal + applicability. If you decide to go for the same deployment scheme, you will always + have at least one service that wraps your bot. + +* Neural network models that you run locally can be factored out into a separate service. + This way your main service, i.e. the service wrapping the bot, won't crash if something + unexpected happens with the model. + +* In the main service directory, we make a separate package for all DFF-related abstractions. + There, we put the script into a separate module, also creating modules for + `processing, condition, and response functions <#>`__. + +* The rest of the project-related Python code is factored out into other packages. + +* We also create 'run.py' and 'test.py' at the project root. These files import the ready pipeline + and execute it to test or run the service. + .. code-block:: shell - project/ - ├── myapp - │ ├── dialog_graph + examples/customer_service_bot/ + ├── docker-compose.yml # docker-compose orchestrates the services + ├── bot # main docker service + │ ├── api + │ │ ├── __init__.py + │ │ ├── chatgpt.py + │ │ └── intent_catcher.py + │ ├── dialog_graph # Separate package for DFF-related abstractions │ │ ├── __init__.py │ │ ├── conditions.py # Condition callbacks + │ │ ├── consts.py # Constant values for keys │ │ ├── processing.py # Processing callbacks │ │ ├── response.py # Response callbacks │ │ └── script.py # DFF script and pipeline are constructed here - │ ├── dockerfile + │ ├── dockerfile # The dockerfile takes care of setting up the project. See the file for more details │ ├── requirements.txt - │ ├── web_app.py # the web app imports the DFF pipeline from dialog_graph - │ └── test.py # End-to-end testing happy path is defined here - ├── ...Folders for other docker-based services, if applicable - ├── venv/ - └── docker-compose.yml \ No newline at end of file + │ ├── run.py + │ └── test.py + └── intent_catcher # intent catching model wrapped as a docker service + ├── dockerfile + ├── requirements.txt + ├── server.py + └── test_server.py \ No newline at end of file diff --git a/docs/source/examples/faq_bot.rst b/docs/source/examples/faq_bot.rst index 5a1141e1e..32ce43d29 100644 --- a/docs/source/examples/faq_bot.rst +++ b/docs/source/examples/faq_bot.rst @@ -4,20 +4,43 @@ FAQ Bot FAQ bot for Arch Linux users built using `DFF`. Can be run with Telegram or with a web interface. +Project structure +~~~~~~~~~~~~~~~~~ + +* In our projects, we go for docker-based deployment due to its scalability and universal + applicability. If you decide to go for the same deployment scheme, you will always + have at least one service that wraps your bot. + +* In the main service directory, we make a separate package for all DFF-related abstractions. + There, we put the `script <#>`__ into a separate module, also creating modules for + `condition and response functions <#>`__. + +* We also create a separate package for `pipeline services <#>`__. + +* The rest of the project-related Python code is factored out into other packages. + +* We also create 'run.py' and 'test.py' at the project root. These files import the ready pipeline + and execute it to test or run the service. + .. code-block:: shell - project/ - ├── myapp - │ ├── dialog_graph - │ │ ├── __init__.py - │ │ ├── conditions.py # Condition callbacks - │ │ ├── processing.py # Processing callbacks - │ │ ├── response.py # Response callbacks - │ │ └── script.py # DFF script and pipeline are constructed here - │ ├── dockerfile - │ ├── requirements.txt - │ ├── web_app.py # the web app imports the DFF pipeline from dialog_graph - │ └── test.py # End-to-end testing happy path is defined here - ├── ...Folders for other docker-based services, if applicable - ├── venv/ - └── docker-compose.yml \ No newline at end of file + examples/frequently_asked_question_bot/telegram/ + ├── docker-compose.yml # docker-compose orchestrates the services + └── bot # main docker service + ├── Dockerfile # The dockerfile takes care of setting up the project. View the dockerfile for more detail + ├── dialog_graph # Separate module for DFF-related abstractions + │ ├── __init__.py + │ ├── conditions.py # Condition callbacks + │ ├── responses.py # Response callbacks + │ └── script.py # DFF script and pipeline are constructed here + ├── faq_model + │ ├── __init__.py + │ ├── faq_dataset_sample.json + │ └── model.py + ├── pipeline_services + │ ├── __init__.py + │ └── pre_services.py + ├── requirements.txt + ├── run.py # the web app imports the DFF pipeline from dialog_graph + └── test.py # End-to-end testing happy path is defined here + \ No newline at end of file From 99e3461c235c508cb406006fb283e99211b553f3 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Mon, 27 Nov 2023 08:27:22 +0000 Subject: [PATCH 11/18] Update faq_dataset --- docs/source/examples/customer_service_bot.rst | 15 +++++++++++++-- docs/source/examples/faq_bot.rst | 12 +++++++++++- .../bot/faq_model/faq_dataset_sample.json | 8 ++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/docs/source/examples/customer_service_bot.rst b/docs/source/examples/customer_service_bot.rst index f1a4360e2..76ea8ecc1 100644 --- a/docs/source/examples/customer_service_bot.rst +++ b/docs/source/examples/customer_service_bot.rst @@ -5,6 +5,8 @@ Customer service bot built using `DFF`. This bot is designed to answer any type of user questions in a limited business domain (book shop). Uses a Telegram interface. +You can read more about deploying the project in its README file. + Project structure ~~~~~~~~~~~~~~~~~ @@ -22,7 +24,7 @@ into services and modules. * In the main service directory, we make a separate package for all DFF-related abstractions. There, we put the script into a separate module, also creating modules for - `processing, condition, and response functions <#>`__. + `processing, condition, and response functions <../user_guides/basic_conceptions#>`__. * The rest of the project-related Python code is factored out into other packages. @@ -53,4 +55,13 @@ into services and modules. ├── dockerfile ├── requirements.txt ├── server.py - └── test_server.py \ No newline at end of file + └── test_server.py + +Models +~~~~~~ + +Two differently designed models are used to power the customer service bot: an intent classifier and a generative model. +The classifier is being deployed as a separate service while ChatGPT is being interacted with via API. + +* [DeepPavlov Intent Catcher](https://docs.deeppavlov.ai/en/0.14.1/features/models/intent_catcher.html) is used for intent retrieval. +* [ChatGPT](https://openai.com/pricing#language-models) is used for context based question answering. \ No newline at end of file diff --git a/docs/source/examples/faq_bot.rst b/docs/source/examples/faq_bot.rst index 32ce43d29..e13cb49a6 100644 --- a/docs/source/examples/faq_bot.rst +++ b/docs/source/examples/faq_bot.rst @@ -4,6 +4,8 @@ FAQ Bot FAQ bot for Arch Linux users built using `DFF`. Can be run with Telegram or with a web interface. +You can read more about deploying the project in its README file. + Project structure ~~~~~~~~~~~~~~~~~ @@ -43,4 +45,12 @@ Project structure ├── requirements.txt ├── run.py # the web app imports the DFF pipeline from dialog_graph └── test.py # End-to-end testing happy path is defined here - \ No newline at end of file + +Models +~~~~~~~ + +The project makes use of the `clips/mfaq `__ model that powers the bot's ability to understand queries in multiple languages. +A number of techniques is employed to make the usage more efficient. + +* The project's Dockerfile illustrates caching a model using SentenceTransformer in a Docker container. + The model is constructed during image build, so that the weights that the Huggingface library fetches from the web are downloaded in advance. At runtime, the fetched weights will be quickly read from the disk. diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json index 70e98e7c9..41b08f614 100644 --- a/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json +++ b/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json @@ -1,6 +1,6 @@ { - "What is Arch Linux?": "See the Arch Linux article.\n", - "Why would I not want to use Arch?": "You may not want to use Arch, if:\n\n you do not have the ability/time/desire for a 'do-it-yourself' GNU/Linux distribution.\n you require support for an architecture other than x86_64.\n you take a strong stance on using a distribution which only provides free software as defined by GNU.\n you believe an operating system should configure itself, run out of the box, and include a complete default set of software and desktop environment on the installation media.\n you do not want a rolling release GNU/Linux distribution.\n you are happy with your current OS.", - "Why would I want to use Arch?": "Because Arch is the best.\n", - "What architectures does Arch support?": "Arch only supports the x86_64 (sometimes called amd64) architecture. Support for i686 was dropped in November 2017 [1]. \nThere are unofficial ports for the i686 architecture [2] and ARM CPUs [3], each with their own community channels.\n" + "What is Deeppavlov?": "Deeppavlov is an open-source stack of technologies in Conversational AI that facilitate the development of the complex dialog systems.\n Find more info at the official website.", + "What can the Deeppavlov library do?": "Deeppavlov is designed for natural language understanding and handles various NLP tasks, like intent recognition or named entity detection.\n A powerful demonstration app is available here.\n", + "Why would I want to use Deeppavlov?": "Deeppavlov is the technology behind some of the award-winning solutions for the Amazon Alexa chat bot competition.\n It's employed by the Dream architecture.", + "How do I learn more about Deeppavlov?": "Here, you can find the documentation to the latest version of the Deeppavlov library,\n including installation and usage instructions.\n" } \ No newline at end of file From 4fc7cf49a8c055fd7f5874fff6a14297f13220af Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Mon, 27 Nov 2023 09:28:36 +0000 Subject: [PATCH 12/18] Update FAQ: replace Arch with Deeppavlov --- docs/source/examples.rst | 2 +- docs/source/examples/faq_bot.rst | 2 +- .../telegram/bot/dialog_graph/script.py | 2 +- .../telegram/bot/test.py | 27 ++++++++----------- .../web/web/bot/dialog_graph/responses.py | 4 +-- .../web/bot/faq_model/faq_dataset_sample.json | 8 +++--- .../web/web/bot/test.py | 8 +++--- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 9a2d55382..0a688c067 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -4,7 +4,7 @@ Examples :doc:`FAQ bot <./examples/faq_bot>` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -FAQ bot for Arch Linux users built using `DFF`. +FAQ bot for Deeppavlov users built using `DFF`. Can be run with Telegram or with a web interface. :doc:`Customer service bot <./examples/customer_service_bot>` diff --git a/docs/source/examples/faq_bot.rst b/docs/source/examples/faq_bot.rst index e13cb49a6..c1a71a8a7 100644 --- a/docs/source/examples/faq_bot.rst +++ b/docs/source/examples/faq_bot.rst @@ -1,7 +1,7 @@ FAQ Bot ------- -FAQ bot for Arch Linux users built using `DFF`. +FAQ bot for Deeppavlov users built using `DFF`. Can be run with Telegram or with a web interface. You can read more about deploying the project in its README file. diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py index 91b62780d..1ab0d1549 100644 --- a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py +++ b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py @@ -28,7 +28,7 @@ }, }, "welcome_node": { - RESPONSE: TelegramMessage(text="Welcome! Ask me questions about Arch Linux."), + RESPONSE: TelegramMessage(text="Welcome! Ask me questions about Deeppavlov."), }, "suggest_questions": { RESPONSE: suggest_similar_questions, diff --git a/examples/frequently_asked_question_bot/telegram/bot/test.py b/examples/frequently_asked_question_bot/telegram/bot/test.py index d7f127f4e..b003e7451 100644 --- a/examples/frequently_asked_question_bot/telegram/bot/test.py +++ b/examples/frequently_asked_question_bot/telegram/bot/test.py @@ -16,33 +16,28 @@ ( (TelegramMessage(text="/start"), script.script["qa_flow"]["welcome_node"][RESPONSE]), ( - TelegramMessage(text="Why use arch?"), + TelegramMessage(text="Why use Deeppavlov?"), TelegramMessage( text="I found similar questions in my database:", - ui=TelegramUI( - buttons=[ - Button(text=q, payload=q) - for q in ["Why would I want to use Arch?", "Why would I not want to use Arch?"] - ] - ), + ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in ["Why would I want to use Deeppavlov?"]]), ), ), ( - TelegramMessage(callback_query="Why would I want to use Arch?"), - TelegramMessage(text=faq["Why would I want to use Arch?"]), + TelegramMessage(callback_query="Why would I want to use Deeppavlov?"), + TelegramMessage(text=faq["Why would I want to use Deeppavlov?"]), ), ( - TelegramMessage(callback_query="Why would I not want to use Arch?"), - TelegramMessage(text=faq["Why would I not want to use Arch?"]), + TelegramMessage(callback_query="What can the Deeppavlov library do?"), + TelegramMessage(text=faq["What can the Deeppavlov library do?"]), ), ( - TelegramMessage(text="What is arch linux?"), + TelegramMessage(text="What is deeppavlov?"), TelegramMessage( text="I found similar questions in my database:", - ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in ["What is Arch Linux?"]]), + ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in ["What is Deeppavlov?"]]), ), ), - (TelegramMessage(callback_query="What is Arch Linux?"), TelegramMessage(text=faq["What is Arch Linux?"])), + (TelegramMessage(callback_query="What is Deeppavlov?"), TelegramMessage(text=faq["What is Deeppavlov?"])), ( TelegramMessage(text="where am I?"), TelegramMessage( @@ -51,8 +46,8 @@ ), ), ( - TelegramMessage(callback_query="What architectures does Arch support?"), - TelegramMessage(text=faq["What architectures does Arch support?"]), + TelegramMessage(callback_query="How do I learn more about Deeppavlov?"), + TelegramMessage(text=faq["How do I learn more about Deeppavlov?"]), ), ) ], diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py index 696199c80..6b7c79b58 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py +++ b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py @@ -22,9 +22,9 @@ def get_bot_answer(question: str) -> Message: """Fallback answer that the bot returns if user's query is not similar to any of the questions.""" -FIRST_MESSAGE = Message(text="Welcome! Ask me questions about Arch Linux.") +FIRST_MESSAGE = Message(text="Welcome! Ask me questions about Deeppavlov.") -FALLBACK_NODE_MESSAGE = Message(text="Something went wrong.\n" "You may continue asking me questions about Arch Linux.") +FALLBACK_NODE_MESSAGE = Message(text="Something went wrong.\n" "You may continue asking me questions about Deeppavlov.") def answer_similar_question(ctx: Context, _: Pipeline): diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json index 70e98e7c9..41b08f614 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json +++ b/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json @@ -1,6 +1,6 @@ { - "What is Arch Linux?": "See the Arch Linux article.\n", - "Why would I not want to use Arch?": "You may not want to use Arch, if:\n\n you do not have the ability/time/desire for a 'do-it-yourself' GNU/Linux distribution.\n you require support for an architecture other than x86_64.\n you take a strong stance on using a distribution which only provides free software as defined by GNU.\n you believe an operating system should configure itself, run out of the box, and include a complete default set of software and desktop environment on the installation media.\n you do not want a rolling release GNU/Linux distribution.\n you are happy with your current OS.", - "Why would I want to use Arch?": "Because Arch is the best.\n", - "What architectures does Arch support?": "Arch only supports the x86_64 (sometimes called amd64) architecture. Support for i686 was dropped in November 2017 [1]. \nThere are unofficial ports for the i686 architecture [2] and ARM CPUs [3], each with their own community channels.\n" + "What is Deeppavlov?": "Deeppavlov is an open-source stack of technologies in Conversational AI that facilitate the development of the complex dialog systems.\n Find more info at the official website.", + "What can the Deeppavlov library do?": "Deeppavlov is designed for natural language understanding and handles various NLP tasks, like intent recognition or named entity detection.\n A powerful demonstration app is available here.\n", + "Why would I want to use Deeppavlov?": "Deeppavlov is the technology behind some of the award-winning solutions for the Amazon Alexa chat bot competition.\n It's employed by the Dream architecture.", + "How do I learn more about Deeppavlov?": "Here, you can find the documentation to the latest version of the Deeppavlov library,\n including installation and usage instructions.\n" } \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/web/bot/test.py b/examples/frequently_asked_question_bot/web/web/bot/test.py index b14f647cb..43c4c7ba8 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/test.py +++ b/examples/frequently_asked_question_bot/web/web/bot/test.py @@ -18,12 +18,12 @@ FIRST_MESSAGE, ), ( - Message(text="Why use arch?"), - get_bot_answer("Why would I want to use Arch?"), + Message(text="Why use Deeppavlov?"), + get_bot_answer("Why would I want to use Deeppavlov?"), ), ( - Message(text="What is arch linux?"), - get_bot_answer("What is Arch Linux?"), + Message(text="What is deeppavlov?"), + get_bot_answer("What is Deeppavlov?"), ), ( Message(text="where am I?"), From 89a53bb4dec104622325cd2aa5360ed1aec83c15 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Mon, 27 Nov 2023 10:00:37 +0000 Subject: [PATCH 13/18] Update threshold --- .../telegram/bot/faq_model/model.py | 2 +- .../web/web/bot/faq_model/model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py b/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py index 4dacaa64b..abfc599c1 100644 --- a/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py +++ b/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py @@ -22,7 +22,7 @@ def find_similar_questions(question: str): emb_with_scores = tuple(zip(questions, map(lambda x: np.linalg.norm(x - q_emb), faq_emb))) - filtered_embeddings = tuple(sorted(filter(lambda x: x[1] < 10, emb_with_scores), key=lambda x: x[1])) + filtered_embeddings = tuple(sorted(filter(lambda x: x[1] < 5, emb_with_scores), key=lambda x: x[1])) result = [] for question, score in filtered_embeddings: diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py b/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py index a12b5a3cc..96a884420 100644 --- a/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py +++ b/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py @@ -22,7 +22,7 @@ def find_similar_question(question: str) -> str | None: emb_with_scores = tuple(zip(questions, map(lambda x: np.linalg.norm(x - q_emb), faq_emb))) - sorted_embeddings = tuple(sorted(filter(lambda x: x[1] < 10, emb_with_scores), key=lambda x: x[1])) + sorted_embeddings = tuple(sorted(filter(lambda x: x[1] < 5, emb_with_scores), key=lambda x: x[1])) if len(sorted_embeddings) > 0: return sorted_embeddings[0][0].removeprefix("") From 30ba54584867d6e6f2a5392c1d3eda64f94164ed Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Wed, 6 Dec 2023 10:36:41 +0000 Subject: [PATCH 14/18] use telegram interface in web services --- .../{web => }/.env | 4 +- .../{web => }/README.md | 17 +++-- .../{web => }/docker-compose.yml | 0 .../{web => }/nginx.conf | 0 .../telegram/.env | 1 - .../telegram/README.md | 70 ------------------- .../telegram/bot/Dockerfile | 15 ---- .../telegram/bot/dialog_graph/conditions.py | 26 ------- .../telegram/bot/dialog_graph/responses.py | 51 -------------- .../telegram/bot/dialog_graph/script.py | 40 ----------- .../telegram/bot/faq_model/model.py | 31 -------- .../bot/pipeline_services/pre_services.py | 30 -------- .../telegram/bot/requirements.txt | 2 - .../telegram/bot/run.py | 37 ---------- .../telegram/bot/test.py | 56 --------------- .../telegram/docker-compose.yml | 8 --- .../web/web/bot/dialog_graph/__init__.py | 0 .../web/web/bot/faq_model/__init__.py | 0 .../web/bot/faq_model/faq_dataset_sample.json | 6 -- .../web/web/bot/pipeline.py | 23 ------ .../web/web/bot/pipeline_services/__init__.py | 0 .../{web/web => web_interface}/Dockerfile | 0 .../__init__.py | 0 .../{web/web => web_interface}/app.py | 15 ++-- .../bot}/__init__.py | 0 .../bot/dialog_graph}/__init__.py | 0 .../bot/dialog_graph/responses.py | 0 .../bot/dialog_graph/script.py | 0 .../bot/faq_model}/__init__.py | 0 .../bot/faq_model/faq_dataset_sample.json | 0 .../bot/faq_model/model.py | 0 .../web_interface/bot/pipeline.py | 42 +++++++++++ .../bot/pipeline_services}/__init__.py | 0 .../bot/pipeline_services/pre_services.py | 0 .../{web/web => web_interface}/bot/test.py | 0 .../web => web_interface}/requirements.txt | 0 .../web => web_interface}/static/LICENSE.txt | 0 .../web => web_interface}/static/index.css | 0 .../web => web_interface}/static/index.html | 0 .../web => web_interface}/static/index.js | 0 40 files changed, 68 insertions(+), 406 deletions(-) rename examples/frequently_asked_question_bot/{web => }/.env (50%) rename examples/frequently_asked_question_bot/{web => }/README.md (50%) rename examples/frequently_asked_question_bot/{web => }/docker-compose.yml (100%) rename examples/frequently_asked_question_bot/{web => }/nginx.conf (100%) delete mode 100644 examples/frequently_asked_question_bot/telegram/.env delete mode 100644 examples/frequently_asked_question_bot/telegram/README.md delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/Dockerfile delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/requirements.txt delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/run.py delete mode 100644 examples/frequently_asked_question_bot/telegram/bot/test.py delete mode 100644 examples/frequently_asked_question_bot/telegram/docker-compose.yml delete mode 100644 examples/frequently_asked_question_bot/web/web/bot/dialog_graph/__init__.py delete mode 100644 examples/frequently_asked_question_bot/web/web/bot/faq_model/__init__.py delete mode 100644 examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json delete mode 100644 examples/frequently_asked_question_bot/web/web/bot/pipeline.py delete mode 100644 examples/frequently_asked_question_bot/web/web/bot/pipeline_services/__init__.py rename examples/frequently_asked_question_bot/{web/web => web_interface}/Dockerfile (100%) rename examples/frequently_asked_question_bot/{telegram/bot/dialog_graph => web_interface}/__init__.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/app.py (84%) rename examples/frequently_asked_question_bot/{telegram/bot/faq_model => web_interface/bot}/__init__.py (100%) rename examples/frequently_asked_question_bot/{telegram/bot/pipeline_services => web_interface/bot/dialog_graph}/__init__.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/bot/dialog_graph/responses.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/bot/dialog_graph/script.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface/bot/faq_model}/__init__.py (100%) rename examples/frequently_asked_question_bot/{telegram => web_interface}/bot/faq_model/faq_dataset_sample.json (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/bot/faq_model/model.py (100%) create mode 100644 examples/frequently_asked_question_bot/web_interface/bot/pipeline.py rename examples/frequently_asked_question_bot/{web/web/bot => web_interface/bot/pipeline_services}/__init__.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/bot/pipeline_services/pre_services.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/bot/test.py (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/requirements.txt (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/static/LICENSE.txt (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/static/index.css (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/static/index.html (100%) rename examples/frequently_asked_question_bot/{web/web => web_interface}/static/index.js (100%) diff --git a/examples/frequently_asked_question_bot/web/.env b/examples/frequently_asked_question_bot/.env similarity index 50% rename from examples/frequently_asked_question_bot/web/.env rename to examples/frequently_asked_question_bot/.env index 813946eb4..30bd8ad55 100644 --- a/examples/frequently_asked_question_bot/web/.env +++ b/examples/frequently_asked_question_bot/.env @@ -1,3 +1,5 @@ POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=pass -POSTGRES_DB=test \ No newline at end of file +POSTGRES_DB=test +TELEGRAM_TOKEN=*** +INTERFACE=web \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/README.md b/examples/frequently_asked_question_bot/README.md similarity index 50% rename from examples/frequently_asked_question_bot/web/README.md rename to examples/frequently_asked_question_bot/README.md index c2eaa114d..a2085eb22 100644 --- a/examples/frequently_asked_question_bot/web/README.md +++ b/examples/frequently_asked_question_bot/README.md @@ -2,9 +2,13 @@ Example FAQ bot built on `dff` with a web interface. -This example contains a website with a chat interface using `WebSockets`. Chat history is stored inside a `postgresql` database. +This example serves bot responses either through Telegram or through a website with a chat interface using `WebSockets`. You can configure the service to use either of those using the +"INTERFACE" environment variable by setting it to "telegram" or "web", respectively. +Chat history is stored inside a `postgresql` database. -The website is accessible via http://localhost:80. + +The web interface is accessible via http://localhost:80. In case with Telegram, +the service will power the bot the token of which you pass at the configuration stage. The bot itself works in a following manner: @@ -17,11 +21,16 @@ A showcase of the website: ### Step 1: Configuring docker services -The Postgresql image needs to be configured with variables that can be set through the [.env](.env) file. Update the file replacing the placeholders with desired values. -``` +The project services need to be configured with variables that can be set through the [.env](.env) file. Update the file replacing the placeholders with desired values. + +```shell POSTGRES_USERNAME=*** POSTGRES_PASSWORD=*** POSTGRES_DB=*** +TELEGRAM_TOKEN=*** +INTERFACE=telegram +# or INTERFACE=web +# or INTERFACE=cli ``` ### Step 2: Launching the docker project diff --git a/examples/frequently_asked_question_bot/web/docker-compose.yml b/examples/frequently_asked_question_bot/docker-compose.yml similarity index 100% rename from examples/frequently_asked_question_bot/web/docker-compose.yml rename to examples/frequently_asked_question_bot/docker-compose.yml diff --git a/examples/frequently_asked_question_bot/web/nginx.conf b/examples/frequently_asked_question_bot/nginx.conf similarity index 100% rename from examples/frequently_asked_question_bot/web/nginx.conf rename to examples/frequently_asked_question_bot/nginx.conf diff --git a/examples/frequently_asked_question_bot/telegram/.env b/examples/frequently_asked_question_bot/telegram/.env deleted file mode 100644 index 6282da982..000000000 --- a/examples/frequently_asked_question_bot/telegram/.env +++ /dev/null @@ -1 +0,0 @@ -TG_BOT_TOKEN=tg_bot_token \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/README.md b/examples/frequently_asked_question_bot/telegram/README.md deleted file mode 100644 index 28e7ddaf4..000000000 --- a/examples/frequently_asked_question_bot/telegram/README.md +++ /dev/null @@ -1,70 +0,0 @@ -## Description - -Example FAQ bot built using `dff`. Uses Telegram as an interface. - -This bot listens for user questions and finds similar questions in its database by using the `clips/mfaq` model. - -It displays found questions as buttons. Upon pressing a button, the bot sends an answer to the question from the database. - - -An example of bot usage: - -![image](https://user-images.githubusercontent.com/61429541/219064505-20e67950-cb88-4cff-afa5-7ce608e1282c.png) - -## Running the bot in docker - -### Step 1: Configuring the docker services - -In order for the bot to work, update the [.env](.env) file replacing the placeholders with the actual value of your Telegram token. - -``` -TG_BOT_TOKEN=*** -``` - -## Step 2: Launching the docker project -*The commands below should be run from the /examples/frequently_asked_question_bot/telegram directory.* - -Build the bot: -```commandline -docker-compose build -``` -Testing the bot: -```commandline -docker-compose run bot pytest test.py -``` - -Running the bot: -```commandline -docker-compose run bot python run.py -``` - -Running in background -```commandline -docker-compose up -d -``` -## Running the bot in the local Python environment - -### Step 1: Configuring the service - -In order for the bot to work, update the [.env](.env) file replacing the placeholders with the actual value of your Telegram token. - -``` -TG_BOT_TOKEN=*** -``` -### Step 2: Installing dependencies - -Build the bot: -```commandline -pip3 install -r requirements.txt -``` -### Step 3: Runnig with CLI - -Running the bot: -```commandline -python run.py -``` - -To launch the test suite, run: -```commandline -pytest test.py -``` diff --git a/examples/frequently_asked_question_bot/telegram/bot/Dockerfile b/examples/frequently_asked_question_bot/telegram/bot/Dockerfile deleted file mode 100644 index 18fd9ff6b..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM python:3.10-slim-buster - -WORKDIR /app - -COPY requirements.txt requirements.txt -RUN pip3 install -r requirements.txt - -# cache mfaq model -RUN ["python3", "-c", "from sentence_transformers import SentenceTransformer; _ = SentenceTransformer('clips/mfaq')"] - -COPY . . - -CMD ["python3", "run.py"] diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py deleted file mode 100644 index faa7cbdcd..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/conditions.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Conditions ------------ -This module defines conditions for transitions between nodes. -""" -from typing import cast - -from dff.script import Context -from dff.pipeline import Pipeline -from dff.messengers.telegram import TelegramMessage - - -def received_text(ctx: Context, _: Pipeline): - """Return true if the last update from user contains text.""" - last_request = ctx.last_request - - return last_request.text is not None - - -def received_button_click(ctx: Context, _: Pipeline): - """Return true if the last update from user is a button press.""" - if ctx.validation: # Regular `Message` doesn't have `callback_query` field, so this fails during validation - return False - last_request = cast(TelegramMessage, ctx.last_request) - - return vars(last_request).get("callback_query") is not None diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py deleted file mode 100644 index 3edee676e..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/responses.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Responses ---------- -This module defines different responses the bot gives. -""" -from typing import cast - -from dff.script import Context -from dff.pipeline import Pipeline -from dff.script.core.message import Button -from dff.messengers.telegram import TelegramMessage, TelegramUI, ParseMode -from faq_model.model import faq - - -def suggest_similar_questions(ctx: Context, _: Pipeline): - """Suggest questions similar to user's query by showing buttons with those questions.""" - if ctx.validation: # this function requires non-empty fields and cannot be used during script validation - return TelegramMessage() - last_request = ctx.last_request - if last_request is None: - raise RuntimeError("No last requests.") - if last_request.annotations is None: - raise RuntimeError("No annotations.") - similar_questions = last_request.annotations.get("similar_questions") - if similar_questions is None: - raise RuntimeError("Last request has no text.") - - if len(similar_questions) == 0: # question is not similar to any questions - return TelegramMessage( - text="I don't have an answer to that question. Here's a list of questions I know an answer to:", - ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in faq]), - ) - else: - return TelegramMessage( - text="I found similar questions in my database:", - ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in similar_questions]), - ) - - -def answer_question(ctx: Context, _: Pipeline): - """Answer a question asked by a user by pressing a button.""" - if ctx.validation: # this function requires non-empty fields and cannot be used during script validation - return TelegramMessage() - last_request = ctx.last_request - if last_request is None: - raise RuntimeError("No last requests.") - last_request = cast(TelegramMessage, last_request) - if last_request.callback_query is None: - raise RuntimeError("No callback query") - - return TelegramMessage(text=faq[last_request.callback_query], parse_mode=ParseMode.HTML) diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py deleted file mode 100644 index 1ab0d1549..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/script.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Script --------- -This module defines a script that the bot follows during conversation. -""" -from dff.script import RESPONSE, TRANSITIONS, LOCAL -import dff.script.conditions as cnd -from dff.messengers.telegram import TelegramMessage - -from .responses import answer_question, suggest_similar_questions -from .conditions import received_button_click, received_text - -script = { - "service_flow": { - "start_node": { - TRANSITIONS: {("qa_flow", "welcome_node"): cnd.exact_match(TelegramMessage(text="/start"))}, - }, - "fallback_node": { - RESPONSE: TelegramMessage(text="Something went wrong. Use `/restart` to start over."), - TRANSITIONS: {("qa_flow", "welcome_node"): cnd.exact_match(TelegramMessage(text="/restart"))}, - }, - }, - "qa_flow": { - LOCAL: { - TRANSITIONS: { - ("qa_flow", "suggest_questions"): received_text, - ("qa_flow", "answer_question"): received_button_click, - }, - }, - "welcome_node": { - RESPONSE: TelegramMessage(text="Welcome! Ask me questions about Deeppavlov."), - }, - "suggest_questions": { - RESPONSE: suggest_similar_questions, - }, - "answer_question": { - RESPONSE: answer_question, - }, - }, -} diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py b/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py deleted file mode 100644 index abfc599c1..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/faq_model/model.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Model ------ -This module defines AI-dependent functions. -""" -import json -from pathlib import Path - -import numpy as np -from sentence_transformers import SentenceTransformer - -model = SentenceTransformer("clips/mfaq") - -with open(Path(__file__).parent / "faq_dataset_sample.json", "r", encoding="utf-8") as file: - faq = json.load(file) - - -def find_similar_questions(question: str): - """Return a list of similar questions from the database.""" - questions = list(map(lambda x: "" + x, faq.keys())) - q_emb, *faq_emb = model.encode(["" + question] + questions) - - emb_with_scores = tuple(zip(questions, map(lambda x: np.linalg.norm(x - q_emb), faq_emb))) - - filtered_embeddings = tuple(sorted(filter(lambda x: x[1] < 5, emb_with_scores), key=lambda x: x[1])) - - result = [] - for question, score in filtered_embeddings: - question = question.removeprefix("") - result.append(question) - return result diff --git a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py deleted file mode 100644 index ebaefb079..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/pre_services.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Pre Services ---- -This module defines services that process user requests before script transition. -""" -from dff.script import Context - -from faq_model.model import find_similar_questions - - -def question_processor(ctx: Context): - """Store questions similar to user's query in the `annotations` field of a message.""" - last_request = ctx.last_request - if last_request is None: - return - else: - if last_request.annotations is None: - last_request.annotations = {} - else: - if last_request.annotations.get("similar_questions") is not None: - return - if last_request.text is None: - last_request.annotations["similar_questions"] = None - else: - last_request.annotations["similar_questions"] = find_similar_questions(last_request.text) - - ctx.last_request = last_request - - -services = [question_processor] # pre-services run before bot sends a response diff --git a/examples/frequently_asked_question_bot/telegram/bot/requirements.txt b/examples/frequently_asked_question_bot/telegram/bot/requirements.txt deleted file mode 100644 index ee51a8a82..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -dff[telegram, tests]>=0.6.3 -sentence_transformers==2.2.2 \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/telegram/bot/run.py b/examples/frequently_asked_question_bot/telegram/bot/run.py deleted file mode 100644 index f8c961f87..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/run.py +++ /dev/null @@ -1,37 +0,0 @@ -import os - -from dff.messengers.telegram import PollingTelegramInterface -from dff.pipeline import Pipeline - -from dialog_graph import script -from pipeline_services import pre_services - - -def get_pipeline(use_cli_interface: bool = False) -> Pipeline: - telegram_token = os.getenv("TG_BOT_TOKEN") - - if use_cli_interface: - messenger_interface = None - elif telegram_token: - messenger_interface = PollingTelegramInterface(token=telegram_token) - else: - raise RuntimeError( - "Telegram token (`TG_BOT_TOKEN`) is not set. `TG_BOT_TOKEN` can be set via `.env` file." - " For more info see README.md." - ) - - pipeline = Pipeline.from_script( - script=script.script, - start_label=("service_flow", "start_node"), - fallback_label=("service_flow", "fallback_node"), - messenger_interface=messenger_interface, - # pre-services run before bot sends a response - pre_services=pre_services.services, - ) - - return pipeline - - -if __name__ == "__main__": - pipeline = get_pipeline() - pipeline.run() diff --git a/examples/frequently_asked_question_bot/telegram/bot/test.py b/examples/frequently_asked_question_bot/telegram/bot/test.py deleted file mode 100644 index b003e7451..000000000 --- a/examples/frequently_asked_question_bot/telegram/bot/test.py +++ /dev/null @@ -1,56 +0,0 @@ -import pytest -from dff.utils.testing.common import check_happy_path -from dff.messengers.telegram import TelegramMessage, TelegramUI -from dff.script import RESPONSE -from dff.script.core.message import Button - -from dialog_graph import script -from run import get_pipeline -from faq_model.model import faq - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "happy_path", - [ - ( - (TelegramMessage(text="/start"), script.script["qa_flow"]["welcome_node"][RESPONSE]), - ( - TelegramMessage(text="Why use Deeppavlov?"), - TelegramMessage( - text="I found similar questions in my database:", - ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in ["Why would I want to use Deeppavlov?"]]), - ), - ), - ( - TelegramMessage(callback_query="Why would I want to use Deeppavlov?"), - TelegramMessage(text=faq["Why would I want to use Deeppavlov?"]), - ), - ( - TelegramMessage(callback_query="What can the Deeppavlov library do?"), - TelegramMessage(text=faq["What can the Deeppavlov library do?"]), - ), - ( - TelegramMessage(text="What is deeppavlov?"), - TelegramMessage( - text="I found similar questions in my database:", - ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in ["What is Deeppavlov?"]]), - ), - ), - (TelegramMessage(callback_query="What is Deeppavlov?"), TelegramMessage(text=faq["What is Deeppavlov?"])), - ( - TelegramMessage(text="where am I?"), - TelegramMessage( - text="I don't have an answer to that question. Here's a list of questions I know an answer to:", - ui=TelegramUI(buttons=[Button(text=q, payload=q) for q in faq]), - ), - ), - ( - TelegramMessage(callback_query="How do I learn more about Deeppavlov?"), - TelegramMessage(text=faq["How do I learn more about Deeppavlov?"]), - ), - ) - ], -) -async def test_happy_path(happy_path): - check_happy_path(pipeline=get_pipeline(use_cli_interface=True), happy_path=happy_path) diff --git a/examples/frequently_asked_question_bot/telegram/docker-compose.yml b/examples/frequently_asked_question_bot/telegram/docker-compose.yml deleted file mode 100644 index 948a97000..000000000 --- a/examples/frequently_asked_question_bot/telegram/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: "2" -services: - bot: - build: - context: bot/ - volumes: - - ./bot/:/app:ro - env_file: .env diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/faq_model/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json deleted file mode 100644 index 41b08f614..000000000 --- a/examples/frequently_asked_question_bot/web/web/bot/faq_model/faq_dataset_sample.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "What is Deeppavlov?": "Deeppavlov is an open-source stack of technologies in Conversational AI that facilitate the development of the complex dialog systems.\n Find more info at the official website.", - "What can the Deeppavlov library do?": "Deeppavlov is designed for natural language understanding and handles various NLP tasks, like intent recognition or named entity detection.\n A powerful demonstration app is available here.\n", - "Why would I want to use Deeppavlov?": "Deeppavlov is the technology behind some of the award-winning solutions for the Amazon Alexa chat bot competition.\n It's employed by the Dream architecture.", - "How do I learn more about Deeppavlov?": "Here, you can find the documentation to the latest version of the Deeppavlov library,\n including installation and usage instructions.\n" -} \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline.py b/examples/frequently_asked_question_bot/web/web/bot/pipeline.py deleted file mode 100644 index ff4af7d2f..000000000 --- a/examples/frequently_asked_question_bot/web/web/bot/pipeline.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -from dff.pipeline import Pipeline -from dff.context_storages import context_storage_factory - -from .dialog_graph import script -from .pipeline_services import pre_services - - -db_uri = "postgresql+asyncpg://{}:{}@db:5432/{}".format( - os.getenv("POSTGRES_USERNAME"), - os.getenv("POSTGRES_PASSWORD"), - os.getenv("POSTGRES_DB"), -) -db = context_storage_factory(db_uri) - - -pipeline: Pipeline = Pipeline.from_script( - **script.pipeline_kwargs, - context_storage=db, - # pre-services run before bot sends a response - pre_services=pre_services.services, -) diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/__init__.py b/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/frequently_asked_question_bot/web/web/Dockerfile b/examples/frequently_asked_question_bot/web_interface/Dockerfile similarity index 100% rename from examples/frequently_asked_question_bot/web/web/Dockerfile rename to examples/frequently_asked_question_bot/web_interface/Dockerfile diff --git a/examples/frequently_asked_question_bot/telegram/bot/dialog_graph/__init__.py b/examples/frequently_asked_question_bot/web_interface/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/telegram/bot/dialog_graph/__init__.py rename to examples/frequently_asked_question_bot/web_interface/__init__.py diff --git a/examples/frequently_asked_question_bot/web/web/app.py b/examples/frequently_asked_question_bot/web_interface/app.py similarity index 84% rename from examples/frequently_asked_question_bot/web/web/app.py rename to examples/frequently_asked_question_bot/web_interface/app.py index 5423fc666..dab6f74f6 100644 --- a/examples/frequently_asked_question_bot/web/web/app.py +++ b/examples/frequently_asked_question_bot/web_interface/app.py @@ -1,3 +1,4 @@ +import os from bot.pipeline import pipeline import uvicorn @@ -40,8 +41,12 @@ async def respond(request: Message): if __name__ == "__main__": - uvicorn.run( - app, - host="0.0.0.0", - port=8000, - ) + interface_type = os.getenv("INTERFACE") + if interface_type == "web": + uvicorn.run( + app, + host="0.0.0.0", + port=8000, + ) + else: + pipeline.run() diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/__init__.py b/examples/frequently_asked_question_bot/web_interface/bot/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/telegram/bot/faq_model/__init__.py rename to examples/frequently_asked_question_bot/web_interface/bot/__init__.py diff --git a/examples/frequently_asked_question_bot/telegram/bot/pipeline_services/__init__.py b/examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/telegram/bot/pipeline_services/__init__.py rename to examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/__init__.py diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/responses.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/bot/dialog_graph/responses.py rename to examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/responses.py diff --git a/examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/script.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/bot/dialog_graph/script.py rename to examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/script.py diff --git a/examples/frequently_asked_question_bot/web/web/__init__.py b/examples/frequently_asked_question_bot/web_interface/bot/faq_model/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/__init__.py rename to examples/frequently_asked_question_bot/web_interface/bot/faq_model/__init__.py diff --git a/examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/web_interface/bot/faq_model/faq_dataset_sample.json similarity index 100% rename from examples/frequently_asked_question_bot/telegram/bot/faq_model/faq_dataset_sample.json rename to examples/frequently_asked_question_bot/web_interface/bot/faq_model/faq_dataset_sample.json diff --git a/examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py b/examples/frequently_asked_question_bot/web_interface/bot/faq_model/model.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/bot/faq_model/model.py rename to examples/frequently_asked_question_bot/web_interface/bot/faq_model/model.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/pipeline.py b/examples/frequently_asked_question_bot/web_interface/bot/pipeline.py new file mode 100644 index 000000000..da10f4e92 --- /dev/null +++ b/examples/frequently_asked_question_bot/web_interface/bot/pipeline.py @@ -0,0 +1,42 @@ +import os + +from dff.pipeline import Pipeline +from dff.context_storages import context_storage_factory +from dff.messengers.telegram import PollingTelegramInterface + +from .dialog_graph import script +from .pipeline_services import pre_services + + +db_uri = "postgresql+asyncpg://{}:{}@db:5432/{}".format( + os.getenv("POSTGRES_USERNAME"), + os.getenv("POSTGRES_PASSWORD"), + os.getenv("POSTGRES_DB"), +) +db = context_storage_factory(db_uri) + + +def get_pipeline(): + interface_type = os.getenv("INTERFACE") + telegram_token = os.getenv("TG_BOT_TOKEN") + + if interface_type == "telegram" and telegram_token is not None: + messenger_interface = PollingTelegramInterface(token=telegram_token) + elif interface_type == "web" or interface_type == "cli": + messenger_interface = None + else: + raise RuntimeError( + "INTERFACE environment variable must be set to one of the following:" "`telegram`, `web`, or `cli`." + ) + + pipeline: Pipeline = Pipeline.from_script( + **script.pipeline_kwargs, + messenger_interface=messenger_interface, + context_storage=db, + # pre-services run before bot sends a response + pre_services=pre_services.services, + ) + return pipeline + + +pipeline = get_pipeline() diff --git a/examples/frequently_asked_question_bot/web/web/bot/__init__.py b/examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/bot/__init__.py rename to examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/__init__.py diff --git a/examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/pre_services.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/bot/pipeline_services/pre_services.py rename to examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/pre_services.py diff --git a/examples/frequently_asked_question_bot/web/web/bot/test.py b/examples/frequently_asked_question_bot/web_interface/bot/test.py similarity index 100% rename from examples/frequently_asked_question_bot/web/web/bot/test.py rename to examples/frequently_asked_question_bot/web_interface/bot/test.py diff --git a/examples/frequently_asked_question_bot/web/web/requirements.txt b/examples/frequently_asked_question_bot/web_interface/requirements.txt similarity index 100% rename from examples/frequently_asked_question_bot/web/web/requirements.txt rename to examples/frequently_asked_question_bot/web_interface/requirements.txt diff --git a/examples/frequently_asked_question_bot/web/web/static/LICENSE.txt b/examples/frequently_asked_question_bot/web_interface/static/LICENSE.txt similarity index 100% rename from examples/frequently_asked_question_bot/web/web/static/LICENSE.txt rename to examples/frequently_asked_question_bot/web_interface/static/LICENSE.txt diff --git a/examples/frequently_asked_question_bot/web/web/static/index.css b/examples/frequently_asked_question_bot/web_interface/static/index.css similarity index 100% rename from examples/frequently_asked_question_bot/web/web/static/index.css rename to examples/frequently_asked_question_bot/web_interface/static/index.css diff --git a/examples/frequently_asked_question_bot/web/web/static/index.html b/examples/frequently_asked_question_bot/web_interface/static/index.html similarity index 100% rename from examples/frequently_asked_question_bot/web/web/static/index.html rename to examples/frequently_asked_question_bot/web_interface/static/index.html diff --git a/examples/frequently_asked_question_bot/web/web/static/index.js b/examples/frequently_asked_question_bot/web_interface/static/index.js similarity index 100% rename from examples/frequently_asked_question_bot/web/web/static/index.js rename to examples/frequently_asked_question_bot/web_interface/static/index.js From 3850629e37930abcd60b46394b07988c93995dc8 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Wed, 6 Dec 2023 10:37:50 +0000 Subject: [PATCH 15/18] rename back to web --- .../{web_interface => web}/Dockerfile | 0 .../{web_interface => web}/__init__.py | 0 .../frequently_asked_question_bot/{web_interface => web}/app.py | 0 .../{web_interface => web}/bot/__init__.py | 0 .../{web_interface => web}/bot/dialog_graph/__init__.py | 0 .../{web_interface => web}/bot/dialog_graph/responses.py | 0 .../{web_interface => web}/bot/dialog_graph/script.py | 0 .../{web_interface => web}/bot/faq_model/__init__.py | 0 .../{web_interface => web}/bot/faq_model/faq_dataset_sample.json | 0 .../{web_interface => web}/bot/faq_model/model.py | 0 .../{web_interface => web}/bot/pipeline.py | 0 .../{web_interface => web}/bot/pipeline_services/__init__.py | 0 .../{web_interface => web}/bot/pipeline_services/pre_services.py | 0 .../{web_interface => web}/bot/test.py | 0 .../{web_interface => web}/requirements.txt | 0 .../{web_interface => web}/static/LICENSE.txt | 0 .../{web_interface => web}/static/index.css | 0 .../{web_interface => web}/static/index.html | 0 .../{web_interface => web}/static/index.js | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename examples/frequently_asked_question_bot/{web_interface => web}/Dockerfile (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/__init__.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/app.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/__init__.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/dialog_graph/__init__.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/dialog_graph/responses.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/dialog_graph/script.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/faq_model/__init__.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/faq_model/faq_dataset_sample.json (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/faq_model/model.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/pipeline.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/pipeline_services/__init__.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/pipeline_services/pre_services.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/bot/test.py (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/requirements.txt (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/static/LICENSE.txt (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/static/index.css (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/static/index.html (100%) rename examples/frequently_asked_question_bot/{web_interface => web}/static/index.js (100%) diff --git a/examples/frequently_asked_question_bot/web_interface/Dockerfile b/examples/frequently_asked_question_bot/web/Dockerfile similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/Dockerfile rename to examples/frequently_asked_question_bot/web/Dockerfile diff --git a/examples/frequently_asked_question_bot/web_interface/__init__.py b/examples/frequently_asked_question_bot/web/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/__init__.py rename to examples/frequently_asked_question_bot/web/__init__.py diff --git a/examples/frequently_asked_question_bot/web_interface/app.py b/examples/frequently_asked_question_bot/web/app.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/app.py rename to examples/frequently_asked_question_bot/web/app.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/__init__.py b/examples/frequently_asked_question_bot/web/bot/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/__init__.py rename to examples/frequently_asked_question_bot/web/bot/__init__.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/__init__.py b/examples/frequently_asked_question_bot/web/bot/dialog_graph/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/__init__.py rename to examples/frequently_asked_question_bot/web/bot/dialog_graph/__init__.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/responses.py rename to examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/script.py b/examples/frequently_asked_question_bot/web/bot/dialog_graph/script.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/dialog_graph/script.py rename to examples/frequently_asked_question_bot/web/bot/dialog_graph/script.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/faq_model/__init__.py b/examples/frequently_asked_question_bot/web/bot/faq_model/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/faq_model/__init__.py rename to examples/frequently_asked_question_bot/web/bot/faq_model/__init__.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/faq_model/faq_dataset_sample.json b/examples/frequently_asked_question_bot/web/bot/faq_model/faq_dataset_sample.json similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/faq_model/faq_dataset_sample.json rename to examples/frequently_asked_question_bot/web/bot/faq_model/faq_dataset_sample.json diff --git a/examples/frequently_asked_question_bot/web_interface/bot/faq_model/model.py b/examples/frequently_asked_question_bot/web/bot/faq_model/model.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/faq_model/model.py rename to examples/frequently_asked_question_bot/web/bot/faq_model/model.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/pipeline.py b/examples/frequently_asked_question_bot/web/bot/pipeline.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/pipeline.py rename to examples/frequently_asked_question_bot/web/bot/pipeline.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/__init__.py b/examples/frequently_asked_question_bot/web/bot/pipeline_services/__init__.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/__init__.py rename to examples/frequently_asked_question_bot/web/bot/pipeline_services/__init__.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/pipeline_services/pre_services.py rename to examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py diff --git a/examples/frequently_asked_question_bot/web_interface/bot/test.py b/examples/frequently_asked_question_bot/web/bot/test.py similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/bot/test.py rename to examples/frequently_asked_question_bot/web/bot/test.py diff --git a/examples/frequently_asked_question_bot/web_interface/requirements.txt b/examples/frequently_asked_question_bot/web/requirements.txt similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/requirements.txt rename to examples/frequently_asked_question_bot/web/requirements.txt diff --git a/examples/frequently_asked_question_bot/web_interface/static/LICENSE.txt b/examples/frequently_asked_question_bot/web/static/LICENSE.txt similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/static/LICENSE.txt rename to examples/frequently_asked_question_bot/web/static/LICENSE.txt diff --git a/examples/frequently_asked_question_bot/web_interface/static/index.css b/examples/frequently_asked_question_bot/web/static/index.css similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/static/index.css rename to examples/frequently_asked_question_bot/web/static/index.css diff --git a/examples/frequently_asked_question_bot/web_interface/static/index.html b/examples/frequently_asked_question_bot/web/static/index.html similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/static/index.html rename to examples/frequently_asked_question_bot/web/static/index.html diff --git a/examples/frequently_asked_question_bot/web_interface/static/index.js b/examples/frequently_asked_question_bot/web/static/index.js similarity index 100% rename from examples/frequently_asked_question_bot/web_interface/static/index.js rename to examples/frequently_asked_question_bot/web/static/index.js From 7425b9f8e9e7c7c96d9c0820e8335a6765674844 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Thu, 7 Dec 2023 11:26:37 +0000 Subject: [PATCH 16/18] Use webhook setup --- examples/frequently_asked_question_bot/.env | 3 +- .../frequently_asked_question_bot/README.md | 5 ++- .../frequently_asked_question_bot/web/app.py | 39 ++++++++++++++----- .../web/bot/pipeline.py | 32 +++------------ .../web/requirements.txt | 2 +- 5 files changed, 42 insertions(+), 39 deletions(-) diff --git a/examples/frequently_asked_question_bot/.env b/examples/frequently_asked_question_bot/.env index 30bd8ad55..6ee31f4c5 100644 --- a/examples/frequently_asked_question_bot/.env +++ b/examples/frequently_asked_question_bot/.env @@ -2,4 +2,5 @@ POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=pass POSTGRES_DB=test TELEGRAM_TOKEN=*** -INTERFACE=web \ No newline at end of file +INTERFACE=web +HOST=*** \ No newline at end of file diff --git a/examples/frequently_asked_question_bot/README.md b/examples/frequently_asked_question_bot/README.md index a2085eb22..3673a68ce 100644 --- a/examples/frequently_asked_question_bot/README.md +++ b/examples/frequently_asked_question_bot/README.md @@ -10,7 +10,9 @@ Chat history is stored inside a `postgresql` database. The web interface is accessible via http://localhost:80. In case with Telegram, the service will power the bot the token of which you pass at the configuration stage. -The bot itself works in a following manner: +**Note that Telegram needs to configure a web hook, so you'll only be able to launch it using an SSL-protected url which needs to be passed through the HOST environment variable.** + +The bot itself works as follows: Whenever a user asks a question it searches for the most similar question in its database using `clips/mfaq` an answer to which is sent to the user. @@ -31,6 +33,7 @@ TELEGRAM_TOKEN=*** INTERFACE=telegram # or INTERFACE=web # or INTERFACE=cli +HOST=*** # required for telegram ``` ### Step 2: Launching the docker project diff --git a/examples/frequently_asked_question_bot/web/app.py b/examples/frequently_asked_question_bot/web/app.py index dab6f74f6..3df7a2d48 100644 --- a/examples/frequently_asked_question_bot/web/app.py +++ b/examples/frequently_asked_question_bot/web/app.py @@ -1,11 +1,19 @@ import os +import asyncio from bot.pipeline import pipeline import uvicorn -from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from telebot import types +from dff.messengers.telegram.messenger import TelegramMessenger +from dff.messengers.telegram.interface import extract_telegram_request_and_id +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request from fastapi.responses import FileResponse from dff.script import Message, Context +HOST = os.getenv("HOST", "0.0.0.0") +PORT = 8000 +FULL_URI = f"https://{HOST}:{PORT}/telegram" +telegram_token = os.getenv("TELEGRAM_TOKEN") app = FastAPI() @@ -40,13 +48,24 @@ async def respond(request: Message): pass +if telegram_token is not None: + messenger = TelegramMessenger(telegram_token) + messenger.remove_webhook() + messenger.set_webhook(FULL_URI) + + @app.post("/telegram") + async def endpoint(request: Request): + json_string = (await request.body()).decode("utf-8") + update = types.Update.de_json(json_string) + request, ctx_id = extract_telegram_request_and_id(update, messenger) + resp = asyncio.run(pipeline(request, ctx_id)) + messenger.send_response(resp.id, resp.last_response) + return "" + + if __name__ == "__main__": - interface_type = os.getenv("INTERFACE") - if interface_type == "web": - uvicorn.run( - app, - host="0.0.0.0", - port=8000, - ) - else: - pipeline.run() + uvicorn.run( + app, + host="0.0.0.0", + port=PORT, + ) diff --git a/examples/frequently_asked_question_bot/web/bot/pipeline.py b/examples/frequently_asked_question_bot/web/bot/pipeline.py index da10f4e92..fb0ba9ec3 100644 --- a/examples/frequently_asked_question_bot/web/bot/pipeline.py +++ b/examples/frequently_asked_question_bot/web/bot/pipeline.py @@ -2,7 +2,6 @@ from dff.pipeline import Pipeline from dff.context_storages import context_storage_factory -from dff.messengers.telegram import PollingTelegramInterface from .dialog_graph import script from .pipeline_services import pre_services @@ -15,28 +14,9 @@ ) db = context_storage_factory(db_uri) - -def get_pipeline(): - interface_type = os.getenv("INTERFACE") - telegram_token = os.getenv("TG_BOT_TOKEN") - - if interface_type == "telegram" and telegram_token is not None: - messenger_interface = PollingTelegramInterface(token=telegram_token) - elif interface_type == "web" or interface_type == "cli": - messenger_interface = None - else: - raise RuntimeError( - "INTERFACE environment variable must be set to one of the following:" "`telegram`, `web`, or `cli`." - ) - - pipeline: Pipeline = Pipeline.from_script( - **script.pipeline_kwargs, - messenger_interface=messenger_interface, - context_storage=db, - # pre-services run before bot sends a response - pre_services=pre_services.services, - ) - return pipeline - - -pipeline = get_pipeline() +pipeline: Pipeline = Pipeline.from_script( + **script.pipeline_kwargs, + context_storage=db, + # pre-services run before bot sends a response + pre_services=pre_services.services, +) diff --git a/examples/frequently_asked_question_bot/web/requirements.txt b/examples/frequently_asked_question_bot/web/requirements.txt index f505ec9d9..60efc7a36 100644 --- a/examples/frequently_asked_question_bot/web/requirements.txt +++ b/examples/frequently_asked_question_bot/web/requirements.txt @@ -1,4 +1,4 @@ -dff[tests, postgresql]>=0.6.3 +dff[tests, postgresql, telegram]>=0.6.3 sentence_transformers==2.2.2 uvicorn==0.21.1 fastapi>=0.95.1 From 4ca284e59c33b7df583ac0e78a1a68a8c45186aa Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Mon, 11 Dec 2023 09:42:14 +0000 Subject: [PATCH 17/18] add translation blueprint --- docs/source/examples/faq_bot.rst | 45 ++++++++++-------- .../{docker-compose.yml => compose.yml} | 0 .../web/bot/dialog_graph/responses.py | 46 ++++++++++++------ .../web/bot/faq_model/model.py | 19 +++++--- .../bot/faq_model/request_translations.json | 38 +++++++++++++++ .../bot/faq_model/response_translations.json | 38 +++++++++++++++ .../web/bot/pipeline_services/pre_services.py | 47 +++++++++++++++---- .../web/bot/test.py | 4 +- .../web/requirements.txt | 3 +- 9 files changed, 187 insertions(+), 53 deletions(-) rename examples/frequently_asked_question_bot/{docker-compose.yml => compose.yml} (100%) create mode 100644 examples/frequently_asked_question_bot/web/bot/faq_model/request_translations.json create mode 100644 examples/frequently_asked_question_bot/web/bot/faq_model/response_translations.json diff --git a/docs/source/examples/faq_bot.rst b/docs/source/examples/faq_bot.rst index c1a71a8a7..8631cc89d 100644 --- a/docs/source/examples/faq_bot.rst +++ b/docs/source/examples/faq_bot.rst @@ -21,30 +21,35 @@ Project structure * The rest of the project-related Python code is factored out into other packages. -* We also create 'run.py' and 'test.py' at the project root. These files import the ready pipeline - and execute it to test or run the service. .. code-block:: shell - examples/frequently_asked_question_bot/telegram/ - ├── docker-compose.yml # docker-compose orchestrates the services - └── bot # main docker service - ├── Dockerfile # The dockerfile takes care of setting up the project. View the dockerfile for more detail - ├── dialog_graph # Separate module for DFF-related abstractions - │ ├── __init__.py - │ ├── conditions.py # Condition callbacks - │ ├── responses.py # Response callbacks - │ └── script.py # DFF script and pipeline are constructed here - ├── faq_model - │ ├── __init__.py - │ ├── faq_dataset_sample.json - │ └── model.py - ├── pipeline_services - │ ├── __init__.py - │ └── pre_services.py + examples/frequently_asked_question_bot/ + ├── README.md + ├── compose.yml # docker compose file orchestrates the services + ├── nginx.conf # web service proxy configurations + └── web + ├── Dockerfile + ├── app.py + ├── bot + │ ├── dialog_graph # A separate module for DFF-related abstractions + │ │ ├── responses.py + │ │ └── script.py # DFF script is constructed here + │ ├── faq_model # model-related code + │ │ ├── faq_dataset_sample.json + │ │ ├── model.py + │ │ ├── request_translations.json + │ │ └── response_translations.json + │ ├── pipeline.py + │ ├── pipeline_services # Separately stored pipeline service functions + │ │ └── pre_services.py + │ └── test.py ├── requirements.txt - ├── run.py # the web app imports the DFF pipeline from dialog_graph - └── test.py # End-to-end testing happy path is defined here + └── static + ├── LICENSE.txt + ├── index.css + ├── index.html + └── index.js Models ~~~~~~~ diff --git a/examples/frequently_asked_question_bot/docker-compose.yml b/examples/frequently_asked_question_bot/compose.yml similarity index 100% rename from examples/frequently_asked_question_bot/docker-compose.yml rename to examples/frequently_asked_question_bot/compose.yml diff --git a/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py index 6b7c79b58..585d6bdd2 100644 --- a/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py +++ b/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py @@ -7,22 +7,39 @@ from dff.script import Context from dff.script import Message from dff.pipeline import Pipeline -from ..faq_model.model import faq +from ..faq_model.model import faq, response_translations -def get_bot_answer(question: str) -> Message: +def get_bot_answer(question: str, language: str) -> Message: """The Message the bot will return as an answer if the most similar question is `question`.""" - return Message(text=f"Q: {question}
    A: {faq[question]}") - - -FALLBACK_ANSWER = Message( - text="I don't have an answer to that question. " - 'You can find FAQ here.', + index = list(faq.keys()).index(question) + return Message(text=f"A: {response_translations[language][index]}") + + +def get_fallback_answer(language: str): + """Fallback answer that the bot returns if user's query is not similar to any of the questions.""" + fallbacks = { + "en": "I don't have an answer to that question. ", + "es": "No tengo una respuesta a esa pregunta. ", + "fr": "Je n'ai pas de réponse à cette question. ", + "de": "Ich habe keine Antwort auf diese Frage. ", + "zh-cn": "我对这个问题没有答案。", + "ru": "У меня нет ответа на этот вопрос. ", + } + + return Message( + text=fallbacks[language], + ) + + +FIRST_MESSAGE = Message( + text="Welcome! Ask me questions about Deeppavlov.\n" + "¡Bienvenido! Hazme preguntas sobre Deeppavlov.\n" + "Bienvenue ! Posez-moi des questions sur Deeppavlov.\n" + "Willkommen! Stellen Sie mir Fragen zu Deeppavlov.\n" + "欢迎!向我询问有关Deeppavlov的问题。\n" + "Добро пожаловать! Задайте мне вопросы о Deeppavlov." ) -"""Fallback answer that the bot returns if user's query is not similar to any of the questions.""" - - -FIRST_MESSAGE = Message(text="Welcome! Ask me questions about Deeppavlov.") FALLBACK_NODE_MESSAGE = Message(text="Something went wrong.\n" "You may continue asking me questions about Deeppavlov.") @@ -32,6 +49,7 @@ def answer_similar_question(ctx: Context, _: Pipeline): if ctx.validation: # this function requires non-empty fields and cannot be used during script validation return Message() last_request = ctx.last_request + language = last_request.annotations["user_language"] if last_request is None: raise RuntimeError("No last requests.") if last_request.annotations is None: @@ -39,6 +57,6 @@ def answer_similar_question(ctx: Context, _: Pipeline): similar_question = last_request.annotations.get("similar_question") if similar_question is None: # question is not similar to any of the questions - return FALLBACK_ANSWER + return get_fallback_answer(language) else: - return get_bot_answer(similar_question) + return get_bot_answer(similar_question, language) diff --git a/examples/frequently_asked_question_bot/web/bot/faq_model/model.py b/examples/frequently_asked_question_bot/web/bot/faq_model/model.py index 96a884420..8dac6ccd3 100644 --- a/examples/frequently_asked_question_bot/web/bot/faq_model/model.py +++ b/examples/frequently_asked_question_bot/web/bot/faq_model/model.py @@ -11,19 +11,24 @@ model = SentenceTransformer("clips/mfaq") +with open(Path(__file__).parent / "request_translations.json", "r", encoding="utf-8") as file: + request_translations = json.load(file) + +with open(Path(__file__).parent / "response_translations.json", "r", encoding="utf-8") as file: + response_translations = json.load(file) + with open(Path(__file__).parent / "faq_dataset_sample.json", "r", encoding="utf-8") as file: faq = json.load(file) -def find_similar_question(question: str) -> str | None: +def find_similar_question(question: str, lang: str) -> str | None: """Return the most similar question from the faq database.""" - questions = list(map(lambda x: "" + x, faq.keys())) + questions = list(map(lambda x: "" + x, request_translations[lang])) q_emb, *faq_emb = model.encode(["" + question] + questions) - emb_with_scores = tuple(zip(questions, map(lambda x: np.linalg.norm(x - q_emb), faq_emb))) - - sorted_embeddings = tuple(sorted(filter(lambda x: x[1] < 5, emb_with_scores), key=lambda x: x[1])) + scores = list(map(lambda x: np.linalg.norm(x - q_emb), faq_emb)) - if len(sorted_embeddings) > 0: - return sorted_embeddings[0][0].removeprefix("") + argmin = scores.index(min(scores)) + if argmin < 5: + return list(faq.keys())[argmin] return None diff --git a/examples/frequently_asked_question_bot/web/bot/faq_model/request_translations.json b/examples/frequently_asked_question_bot/web/bot/faq_model/request_translations.json new file mode 100644 index 000000000..f79437681 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/bot/faq_model/request_translations.json @@ -0,0 +1,38 @@ +{ + "en": [ + "What is Deeppavlov?", + "What can the Deeppavlov library do?", + "Why would I want to use Deeppavlov?", + "How do I learn more about Deeppavlov?" + ], + "es": [ + "¿Qué es Deeppavlov?", + "¿Qué puede hacer la biblioteca Deeppavlov?", + "¿Por qué querría usar Deeppavlov?", + "¿Cómo aprendo más sobre Deeppavlov?" + ], + "fr": [ + "Qu'est-ce que Deeppavlov?", + "Que peut faire la bibliothèque Deeppavlov?", + "Pourquoi voudrais-je utiliser Deeppavlov?", + "Comment en savoir plus sur Deeppavlov?" + ], + "de": [ + "Was ist Deeppavlov?", + "Was kann die Deeppavlov-Bibliothek tun?", + "Warum sollte ich Deeppavlov verwenden wollen?", + "Wie erfahre ich mehr über Deeppavlov?" + ], + "zh-cn": [ + "什么是Deeppavlov?", + "Deeppavlov图书馆能做什么?", + "我为什么要使用Deeppavlov?", + "我如何了解更多关于Deeppavlov的信息?" + ], + "ru": [ + "Что такое Deeppavlov?", + "Что может делать библиотека Deeppavlov?", + "Зачем мне использовать Deeppavlov?", + "Как мне узнать больше о Deeppavlov?" + ] +} diff --git a/examples/frequently_asked_question_bot/web/bot/faq_model/response_translations.json b/examples/frequently_asked_question_bot/web/bot/faq_model/response_translations.json new file mode 100644 index 000000000..f5c0c0dc0 --- /dev/null +++ b/examples/frequently_asked_question_bot/web/bot/faq_model/response_translations.json @@ -0,0 +1,38 @@ +{ + "en": [ + "Deeppavlov is an open-source stack of technologies in Conversational AI that facilitate the development of the complex dialog systems.\n Find more info at the official website.", + "Deeppavlov is designed for natural language understanding and handles various NLP tasks, like intent recognition or named entity detection.\n A powerful demonstration app is available here.\n", + "Deeppavlov is the technology behind some of the award-winning solutions for the Amazon Alexa chat bot competition.\n It's employed by the Dream architecture.", + "Here, you can find the documentation to the latest version of the Deeppavlov library,\n including installation and usage instructions.\n" + ], + "es": [ + "Deeppavlov es un conjunto de tecnologías de código abierto en Inteligencia Artificial Conversacional que facilitan el desarrollo de sistemas de diálogo complejos.\n Encuentra más información en el sitio web oficial.", + "Deeppavlov está diseñado para la comprensión del lenguaje natural y maneja diversas tareas de PLN, como el reconocimiento de intenciones o la detección de entidades nombradas.\n Una potente aplicación de demostración está disponible aquí.\n", + "Deeppavlov es la tecnología detrás de algunas de las soluciones ganadoras de premios para la competición de chatbots de Amazon Alexa.\n Está empleada por la arquitectura Dream.", + "Aquí, puedes encontrar la documentación de la última versión de la biblioteca Deeppavlov,\n incluyendo instrucciones de instalación y uso.\n" + ], + "fr": [ + "Deeppavlov est une pile de technologies open-source en IA Conversationnelle qui facilite le développement de systèmes de dialogues complexes.\n Trouvez plus d'infos sur le site officiel.", + "Deeppavlov est conçu pour la compréhension du langage naturel et gère diverses tâches de TAL, comme la reconnaissance d'intentions ou la détection d'entités nommées.\n Une application de démonstration puissante est disponible ici.\n", + "Deeppavlov est la technologie derrière certaines des solutions primées pour le concours de chatbot Amazon Alexa.\n Elle est utilisée par l'architecture Dream.", + "Ici, vous pouvez trouver la documentation sur la dernière version de la bibliothèque Deeppavlov,\n incluant les instructions d'installation et d'utilisation.\n" + ], + "de": [ + "Deeppavlov ist ein Open-Source-Technologiestack in Conversational AI, der die Entwicklung komplexer Dialogsysteme erleichtert.\n Weitere Informationen finden Sie auf der offiziellen Website.", + "Deeppavlov ist für das Verständnis natürlicher Sprache konzipiert und bewältigt verschiedene NLP-Aufgaben, wie die Erkennung von Absichten oder die Erkennung benannter Entitäten.\n Eine leistungsfähige Demonstrations-App ist hier verfügbar.\n", + "Deeppavlov ist die Technologie hinter einigen der preisgekrönten Lösungen für den Amazon Alexa Chatbot-Wettbewerb.\n Es wird von der Dream-Architektur verwendet.", + "Hier finden Sie die Dokumentation zur neuesten Version der Deeppavlov-Bibliothek,\n einschließlich Installations- und Gebrauchsanweisungen.\n" + ], + "zh-cn": [ + "Deeppavlov是一个开源的对话人工智能技术堆栈,促进了复杂对话系统的开发。\n 在官方网站上找到更多信息。", + "Deeppavlov被设计用于自然语言理解,并处理各种自然语言处理任务,如意图识别或命名实体检测。\n 一个强大的演示应用程序可在这里找到。\n", + "Deeppavlov是亚马逊Alexa聊天机器人比赛中一些获奖解决方案背后的技术。\n 它被Dream架构所采用。", + "这里可以找到Deeppavlov库的最新版本文档,\n 包括安装和使用说明。\n" + ], + "ru": [ + "Deeppavlov - это открытый стек технологий в разговорном ИИ, который облегчает разработку сложных диалоговых систем.\n Больше информации на официальном сайте.", + "Deeppavlov предназначен для понимания естественного языка и обрабатывает различные задачи NLP, такие как распознавание интентов или именованных сущностей.\n Демо библиотеки доступно по ссылке.\n", + "Deeppavlov - это технология, стоящая за некоторыми из призовых решений для соревнований чат-ботов Amazon Alexa.\n Она используется в архитектуре Dream.", + "Здесь вы можете найти документацию к последней версии библиотеки." + ] +} diff --git a/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py index 34f2f07b5..98773c7c7 100644 --- a/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py +++ b/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py @@ -4,25 +4,54 @@ This module defines services that process user requests before script transition. """ from dff.script import Context +from langdetect import detect_langs from ..faq_model.model import find_similar_question +PROCESSED_LANGUAGES = ["en", "de", "fr", "es", "ru", "zh-cn"] + + +def language_processor(ctx: Context): + """Store the user language; the language is detected from the last user utterance. + The value can be one of: English, German, Spanish, French, Mandarin Chinese or Russian. + """ + last_request = ctx.last_request + if last_request is None or last_request.text is None: + return + if last_request.annotations is None: + last_request.annotations = {} + else: + if last_request.annotations.get("user_language") is not None: + return + + candidate_languages = detect_langs(last_request.text) + if len(candidate_languages) == 0: + last_request.annotations["user_language"] = "en" + else: + most_probable_language = candidate_languages[0] + if most_probable_language.prob < 0.3: + last_request.annotations["user_language"] = "en" + elif most_probable_language.lang not in PROCESSED_LANGUAGES: + last_request.annotations["user_language"] = "en" + else: + last_request.annotations["user_language"] = most_probable_language.lang + + ctx.last_request = last_request + def question_processor(ctx: Context): """Store the most similar question to user's query in the `annotations` field of a message.""" last_request = ctx.last_request if last_request is None or last_request.text is None: return + if last_request.annotations is None: + last_request.annotations = {} else: - if last_request.annotations is None: - last_request.annotations = {} - else: - if last_request.annotations.get("similar_question") is not None: - return - if last_request.text is None: - last_request.annotations["similar_question"] = None - else: - last_request.annotations["similar_question"] = find_similar_question(last_request.text) + if last_request.annotations.get("similar_question") is not None: + return + + language = last_request.annotations["user_language"] + last_request.annotations["similar_question"] = find_similar_question(last_request.text, language) ctx.last_request = last_request diff --git a/examples/frequently_asked_question_bot/web/bot/test.py b/examples/frequently_asked_question_bot/web/bot/test.py index 43c4c7ba8..5daa40b01 100644 --- a/examples/frequently_asked_question_bot/web/bot/test.py +++ b/examples/frequently_asked_question_bot/web/bot/test.py @@ -5,7 +5,7 @@ from .dialog_graph import script from .pipeline_services import pre_services -from .dialog_graph.responses import get_bot_answer, FALLBACK_ANSWER, FIRST_MESSAGE +from .dialog_graph.responses import get_bot_answer, get_fallback_answer, FIRST_MESSAGE @pytest.mark.asyncio @@ -27,7 +27,7 @@ ), ( Message(text="where am I?"), - FALLBACK_ANSWER, + get_fallback_answer("en"), ), ) ], diff --git a/examples/frequently_asked_question_bot/web/requirements.txt b/examples/frequently_asked_question_bot/web/requirements.txt index 60efc7a36..e0ac43786 100644 --- a/examples/frequently_asked_question_bot/web/requirements.txt +++ b/examples/frequently_asked_question_bot/web/requirements.txt @@ -2,4 +2,5 @@ dff[tests, postgresql, telegram]>=0.6.3 sentence_transformers==2.2.2 uvicorn==0.21.1 fastapi>=0.95.1 -websockets==11.0.2 \ No newline at end of file +websockets==11.0.2 +langdetect==1.0.9 \ No newline at end of file From 2f8805bdf3c48a409ea89727a1ee34b088ff9a25 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Mon, 11 Dec 2023 11:26:46 +0000 Subject: [PATCH 18/18] debug translations --- .../web/bot/dialog_graph/responses.py | 5 +++-- .../frequently_asked_question_bot/web/bot/faq_model/model.py | 2 +- .../web/bot/pipeline_services/pre_services.py | 2 +- examples/frequently_asked_question_bot/web/bot/test.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py b/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py index 585d6bdd2..0adefd447 100644 --- a/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py +++ b/examples/frequently_asked_question_bot/web/bot/dialog_graph/responses.py @@ -7,13 +7,14 @@ from dff.script import Context from dff.script import Message from dff.pipeline import Pipeline -from ..faq_model.model import faq, response_translations +from ..faq_model.model import faq, response_translations, request_translations def get_bot_answer(question: str, language: str) -> Message: """The Message the bot will return as an answer if the most similar question is `question`.""" index = list(faq.keys()).index(question) - return Message(text=f"A: {response_translations[language][index]}") + request = list(request_translations[language])[index] + return Message(text=f"Q: {request}\nA: {response_translations[language][index]}") def get_fallback_answer(language: str): diff --git a/examples/frequently_asked_question_bot/web/bot/faq_model/model.py b/examples/frequently_asked_question_bot/web/bot/faq_model/model.py index 8dac6ccd3..749886116 100644 --- a/examples/frequently_asked_question_bot/web/bot/faq_model/model.py +++ b/examples/frequently_asked_question_bot/web/bot/faq_model/model.py @@ -29,6 +29,6 @@ def find_similar_question(question: str, lang: str) -> str | None: scores = list(map(lambda x: np.linalg.norm(x - q_emb), faq_emb)) argmin = scores.index(min(scores)) - if argmin < 5: + if scores[argmin] < 5: return list(faq.keys())[argmin] return None diff --git a/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py b/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py index 98773c7c7..f15192065 100644 --- a/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py +++ b/examples/frequently_asked_question_bot/web/bot/pipeline_services/pre_services.py @@ -56,4 +56,4 @@ def question_processor(ctx: Context): ctx.last_request = last_request -services = [question_processor] # pre-services run before bot sends a response +services = [language_processor, question_processor] # pre-services run before bot sends a response diff --git a/examples/frequently_asked_question_bot/web/bot/test.py b/examples/frequently_asked_question_bot/web/bot/test.py index 5daa40b01..9e93e1984 100644 --- a/examples/frequently_asked_question_bot/web/bot/test.py +++ b/examples/frequently_asked_question_bot/web/bot/test.py @@ -19,11 +19,11 @@ ), ( Message(text="Why use Deeppavlov?"), - get_bot_answer("Why would I want to use Deeppavlov?"), + get_bot_answer("Why would I want to use Deeppavlov?", "en"), ), ( Message(text="What is deeppavlov?"), - get_bot_answer("What is Deeppavlov?"), + get_bot_answer("What is Deeppavlov?", "en"), ), ( Message(text="where am I?"),