diff --git a/README.md b/README.md index 5a62028f..b1996ad3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,16 @@ Please see the examples in the following chapters. ## LLM side (needs a GPU) The second part (LLM side) is a model server for the speech-to-text model, the LLM, the embedding system, and the text-to-speech model. + +#### Installation +In order to quickly run the LLM side, you can use the following installation commands: +```bash +pip install wafl-llm +wafl-llm start +``` +which will use the default models and start the server on port 8080. + +#### Docker A docker image can be used to run it as in the following: ```bash diff --git a/documentation/build/doctrees/environment.pickle b/documentation/build/doctrees/environment.pickle deleted file mode 100644 index ca42bd9e..00000000 Binary files a/documentation/build/doctrees/environment.pickle and /dev/null differ diff --git a/documentation/build/doctrees/index.doctree b/documentation/build/doctrees/index.doctree index f358705e..fda9448d 100644 Binary files a/documentation/build/doctrees/index.doctree and b/documentation/build/doctrees/index.doctree differ diff --git a/documentation/build/doctrees/installation.doctree b/documentation/build/doctrees/installation.doctree index 4eabba05..2e9f2a12 100644 Binary files a/documentation/build/doctrees/installation.doctree and b/documentation/build/doctrees/installation.doctree differ diff --git a/documentation/build/html/_sources/index.rst.txt b/documentation/build/html/_sources/index.rst.txt index 431643d7..9f32cc73 100644 --- a/documentation/build/html/_sources/index.rst.txt +++ b/documentation/build/html/_sources/index.rst.txt @@ -17,6 +17,8 @@ Welcome to WAFL's 0.0.80 documentation! running_WAFL facts_and_rules examples + testcases + actions license Indices and tables diff --git a/documentation/build/html/_sources/installation.rst.txt b/documentation/build/html/_sources/installation.rst.txt index 2eb6156d..362a1398 100644 --- a/documentation/build/html/_sources/installation.rst.txt +++ b/documentation/build/html/_sources/installation.rst.txt @@ -32,20 +32,24 @@ Please see the examples in the following chapters. LLM side (needs a GPU) ---------------------- +The second part (LLM side) is a model server for the speech-to-text model, the LLM, the embedding system, and the text-to-speech model. +In order to quickly run the LLM side, you can use the following installation commands: -The second part is a machine that runs on a machine accessible from the interface side. -The initial configuration is for a local deployment of language models. -No action is needed to run WAFL if you want to run it as a local instance. +.. code-block:: bash + + $ pip install wafl-llm + $ wafl-llm start + + which will use the default models and start the server on port 8080. -However, a multi-user setup will benefit for a dedicated server. -In this case, a docker image can be used +Alternatively, a Docker image can be used to run it as in the following: .. code-block:: bash - $ docker run -p8080:8080 --env NVIDIA_DISABLE_REQUIRE=1 --gpus all fractalego/wafl-llm:latest + $ docker run -p8080:8080 --env NVIDIA_DISABLE_REQUIRE=1 --gpus all fractalego/wafl-llm:0.80 The interface side has a `config.json` file that needs to be filled with the IP address of the LLM side. The default is localhost. -Alternatively, you can run the LLM side by cloning `this repository `_. +Finally, you can run the LLM side by cloning [this repository](https://github.com/fractalego/wafl-llm). diff --git a/documentation/build/html/examples.html b/documentation/build/html/examples.html index c5e07594..b8761aea 100644 --- a/documentation/build/html/examples.html +++ b/documentation/build/html/examples.html @@ -55,6 +55,8 @@
  • Rule with remember command
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • diff --git a/documentation/build/html/genindex.html b/documentation/build/html/genindex.html index c7713889..cfd700d2 100644 --- a/documentation/build/html/genindex.html +++ b/documentation/build/html/genindex.html @@ -46,6 +46,8 @@
  • Running WAFL
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • diff --git a/documentation/build/html/index.html b/documentation/build/html/index.html index 56d3b8f9..0d63a690 100644 --- a/documentation/build/html/index.html +++ b/documentation/build/html/index.html @@ -48,6 +48,8 @@
  • Running WAFL
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • @@ -110,6 +112,12 @@

    Welcome to WAFL’s 0.0.80 documentation!Rule with remember command +
  • Creating a testcase +
  • +
  • Running Actions
  • License
  • diff --git a/documentation/build/html/installation.html b/documentation/build/html/installation.html index f6aab704..c836decf 100644 --- a/documentation/build/html/installation.html +++ b/documentation/build/html/installation.html @@ -53,6 +53,8 @@
  • Running WAFL
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • @@ -103,17 +105,21 @@

    Interface side

    LLM side (needs a GPU)

    -

    The second part is a machine that runs on a machine accessible from the interface side. -The initial configuration is for a local deployment of language models. -No action is needed to run WAFL if you want to run it as a local instance.

    -

    However, a multi-user setup will benefit for a dedicated server. -In this case, a docker image can be used

    -
    $ docker run -p8080:8080 --env NVIDIA_DISABLE_REQUIRE=1 --gpus all fractalego/wafl-llm:latest
    +

    The second part (LLM side) is a model server for the speech-to-text model, the LLM, the embedding system, and the text-to-speech model. +In order to quickly run the LLM side, you can use the following installation commands:

    +
    $ pip install wafl-llm
    +$ wafl-llm start
    +
    +which will use the default models and start the server on port 8080.
    +
    +
    +

    Alternatively, a Docker image can be used to run it as in the following:

    +
    $ docker run -p8080:8080 --env NVIDIA_DISABLE_REQUIRE=1 --gpus all fractalego/wafl-llm:0.80
     

    The interface side has a config.json file that needs to be filled with the IP address of the LLM side. -The default is localhost. -Alternatively, you can run the LLM side by cloning this repository.

    +The default is localhost.

    +

    Finally, you can run the LLM side by cloning [this repository](https://github.com/fractalego/wafl-llm).

    diff --git a/documentation/build/html/introduction.html b/documentation/build/html/introduction.html index 741fbb29..dec9ff0b 100644 --- a/documentation/build/html/introduction.html +++ b/documentation/build/html/introduction.html @@ -49,6 +49,8 @@
  • Running WAFL
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • diff --git a/documentation/build/html/license.html b/documentation/build/html/license.html index c769ebd6..8c2b7507 100644 --- a/documentation/build/html/license.html +++ b/documentation/build/html/license.html @@ -17,7 +17,7 @@ - + @@ -48,6 +48,8 @@
  • Running WAFL
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • @@ -88,7 +90,7 @@

    License - +


    diff --git a/documentation/build/html/objects.inv b/documentation/build/html/objects.inv index ead68abd..1f53eace 100644 Binary files a/documentation/build/html/objects.inv and b/documentation/build/html/objects.inv differ diff --git a/documentation/build/html/running_WAFL.html b/documentation/build/html/running_WAFL.html index 30bd180c..f4519c39 100644 --- a/documentation/build/html/running_WAFL.html +++ b/documentation/build/html/running_WAFL.html @@ -55,6 +55,8 @@
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • diff --git a/documentation/build/html/search.html b/documentation/build/html/search.html index 785dd13b..e4efc086 100644 --- a/documentation/build/html/search.html +++ b/documentation/build/html/search.html @@ -49,6 +49,8 @@
  • Running WAFL
  • The rules.yaml file
  • Examples
  • +
  • Creating a testcase
  • +
  • Running Actions
  • License
  • diff --git a/documentation/build/html/searchindex.js b/documentation/build/html/searchindex.js index e8b86b8d..844a7f7c 100644 --- a/documentation/build/html/searchindex.js +++ b/documentation/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["configuration", "examples", "facts_and_rules", "index", "initialization", "installation", "introduction", "license", "rule_with_examples", "rules_with_execute_command", "rules_with_remember_command", "running_WAFL", "simple_rule"], "filenames": ["configuration.rst", "examples.rst", "facts_and_rules.rst", "index.rst", "initialization.rst", "installation.rst", "introduction.rst", "license.rst", "rule_with_examples.rst", "rules_with_execute_command.rst", "rules_with_remember_command.rst", "running_WAFL.rst", "simple_rule.rst"], "titles": ["Configuration", "Examples", "The rules.yaml file", "Welcome to WAFL\u2019s 0.0.80 documentation!", "Initialization", "Installation", "Introduction", "License", "Rule with examples", "Rule with execute command", "Rule with remember command", "Running WAFL", "Simple rule"], "terms": {"The": [0, 3, 4, 5, 6, 7, 9, 10, 11, 12], "file": [0, 3, 4, 5, 7, 9, 11], "config": [0, 4, 5, 11], "json": [0, 4, 5, 11], "contain": [0, 4, 11], "some": [0, 4, 8, 9], "paramet": [0, 4], "chatbot": [0, 4, 11], "url": [0, 4, 10], "connect": [0, 4, 7], "backend": [0, 4], "A": [0, 5, 6, 7, 9, 12], "typic": 0, "look": 0, "like": [0, 2], "thi": [0, 2, 4, 5, 6, 7, 9, 11, 12], "waking_up_word": 0, "comput": [0, 2, 8, 9, 11], "waking_up_sound": 0, "true": 0, "deactivate_sound": 0, "rule": [0, 1, 3, 4, 5, 6, 11], "yaml": [0, 3, 4], "function": [0, 1, 3, 4, 6], "py": [0, 4, 9], "llm_model": 0, "model_host": 0, "localhost": [0, 5], "model_port": 0, "8080": [0, 5], "listener_model": 0, "listener_hotword_logp": 0, "8": 0, "listener_volume_threshold": 0, "0": 0, "6": 0, "listener_silence_timeout": 0, "7": 0, "speaker_model": 0, "text_embedding_model": 0, "These": [0, 2, 9], "set": [0, 2, 4, 5, 6], "regul": 0, "follow": [0, 2, 5, 6, 7, 8, 9, 10], "i": [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12], "name": [0, 11], "bot": [0, 2, 8, 9, 12], "us": [0, 4, 5, 6, 7, 8, 9, 10, 11], "wake": 0, "up": 0, "system": [0, 5, 11], "run": [0, 3, 4, 5], "audio": [0, 3], "mode": [0, 11], "ar": [0, 2, 4, 9, 10, 11, 12], "plai": [0, 6], "signal": 0, "back": 0, "idl": 0, "fact": [0, 3, 4], "guid": [0, 2, 4], "default": [0, 5, 11], "can": [0, 2, 4, 5, 8, 9, 10, 11, 12], "llm": [0, 3, 4], "model": [0, 2, 5, 6, 8, 9, 10], "listen": [0, 11], "detect": 0, "word": [0, 11], "similar": [0, 12], "threshold": 0, "volum": 0, "ani": [0, 7], "convers": [0, 2, 4], "utter": 0, "below": 0, "ignor": 0, "silenc": 0, "timeout": 0, "If": 0, "time": [0, 2], "longer": 0, "than": 0, "consid": [0, 8, 9, 10], "finish": 0, "speaker": [0, 5], "text": [0, 6, 9], "embed": 0, "simpl": [1, 2, 3], "execut": [1, 2, 3, 10, 11, 12], "command": [1, 3, 4, 5, 11], "local": [1, 3, 4, 5, 11], "rememb": [1, 3], "languag": [2, 5, 6, 8, 9, 10], "through": [2, 10], "list": [2, 8, 9], "retriev": 2, "dure": 2, "written": 2, "format": [2, 8], "do": [2, 7], "well": 2, "call": [2, 6], "user": [2, 5, 6, 8, 9, 10, 11, 12], "want": [2, 5, 8, 9, 10, 11], "know": [2, 9], "output": [2, 8, 9, 10], "get_tim": 2, "For": [2, 8, 9], "exampl": [2, 3, 5, 9, 11, 12], "ask": 2, "how": [2, 11, 12], "you": [2, 5, 10, 11, 12], "add": 2, "its": 2, "prompt": [2, 8, 9, 10], "eventu": 2, "gener": [2, 8, 9, 10], "an": [2, 4, 7, 10], "answer": 2, "am": 2, "fine": 2, "compos": 2, "condit": [2, 7], "action": [2, 5, 7, 12], "trigger": 2, "match": [2, 12], "against": 2, "input": [2, 12], "In": [2, 5, 9, 12], "abov": [2, 7, 8, 9], "whole": 2, "ad": [2, 8, 9, 10], "item": 2, "order": [2, 12], "should": [2, 8, 9], "think": [2, 8, 9], "introduct": 3, "instal": 3, "interfac": [3, 4], "side": [3, 4], "need": [3, 4, 10], "gpu": 3, "initi": [3, 5], "configur": [3, 5], "server": [3, 5], "cli": 3, "test": [3, 4], "licens": 3, "index": 3, "modul": 3, "search": 3, "page": 3, "initialis": 4, "wafl": [4, 5, 6], "": [4, 9, 10], "work": [4, 6, 11, 12], "environ": 4, "init": [4, 5], "It": [4, 6, 11], "creat": [4, 5, 6], "l": 4, "db": 4, "main": 4, "requir": [4, 5, 6, 8, 9], "txt": [4, 11], "secret": 4, "start_llm": 4, "sh": 4, "testcas": [4, 11], "auxiliari": 4, "inform": 4, "about": 4, "state": 4, "edit": [4, 5], "manual": 4, "script": 4, "start": 4, "webserv": [4, 11], "python": [4, 8, 9, 10], "packag": 4, "mai": 4, "credenti": 4, "simpli": 4, "docker": [4, 5], "imag": [4, 5], "case": [4, 5, 6, 9, 12], "version": [5, 6], "built": 5, "two": [5, 9, 10], "part": 5, "both": 5, "same": [5, 8], "machin": [5, 9], "first": 5, "your": [5, 6], "have": [5, 12], "access": 5, "microphon": 5, "To": 5, "sudo": 5, "apt": 5, "get": 5, "portaudio19": 5, "dev": 5, "ffmpeg": 5, "pip": 5, "after": 5, "which": [5, 11], "chang": [5, 11], "standard": 5, "also": 5, "pleas": 5, "see": [5, 9], "chapter": 5, "second": 5, "from": [5, 7], "deploy": 5, "No": 5, "instanc": 5, "howev": [5, 9], "multi": 5, "setup": 5, "benefit": 5, "dedic": 5, "p8080": 5, "env": 5, "nvidia_disable_requir": 5, "1": 5, "all": [5, 7, 11, 12], "fractalego": [5, 7], "latest": 5, "ha": 5, "fill": 5, "ip": 5, "address": 5, "altern": 5, "clone": 5, "repositori": 5, "framework": 6, "person": [6, 7], "agent": 6, "integr": 6, "larg": 6, "speech": 6, "recognit": 6, "combin": 6, "predict": 6, "behavior": 6, "defin": [6, 9, 11], "support": 6, "memori": [6, 9], "progress": 6, "current": [6, 9], "specifi": [6, 8, 9], "while": 6, "readi": 6, "might": 6, "product": 6, "depend": 6, "softwar": 7, "under": 7, "mit": 7, "copyright": 7, "c": 7, "2024": 7, "alberto": 7, "cetoli": 7, "io": 7, "permiss": 7, "herebi": 7, "grant": 7, "free": 7, "charg": 7, "obtain": 7, "copi": 7, "associ": 7, "document": [7, 11], "deal": 7, "without": 7, "restrict": 7, "includ": 7, "limit": 7, "right": 7, "modifi": 7, "merg": 7, "publish": 7, "distribut": 7, "sublicens": 7, "sell": 7, "permit": 7, "whom": 7, "furnish": 7, "so": 7, "subject": 7, "notic": [7, 8], "shall": 7, "substanti": 7, "portion": 7, "THE": [7, 8, 9], "provid": [7, 10], "AS": 7, "warranti": 7, "OF": 7, "kind": 7, "express": 7, "OR": 7, "impli": 7, "BUT": 7, "NOT": 7, "TO": 7, "merchant": 7, "fit": 7, "FOR": 7, "particular": 7, "purpos": [7, 11], "AND": 7, "noninfring": 7, "IN": 7, "NO": 7, "event": 7, "author": 7, "holder": 7, "BE": 7, "liabl": 7, "claim": 7, "damag": 7, "other": 7, "liabil": 7, "whether": 7, "contract": 7, "tort": 7, "otherwis": 7, "aris": 7, "out": 7, "WITH": 7, "make": [8, 9], "clearer": [8, 9], "effect": [8, 9], "each": [8, 9], "suggest": [8, 9], "go": [8, 9], "math": [8, 9], "oper": [8, 9], "code": [8, 9, 10], "solv": [8, 9], "problem": [8, 9], "assign": [8, 9], "result": [8, 9, 10, 11], "variabl": [8, 9], "what": [8, 9, 10], "2": [8, 9], "anoth": [8, 9], "squar": [8, 9], "root": [8, 9], "import": [8, 9], "sqrt": [8, 9], "exactli": [8, 9, 10], "THAT": [8, 9], "when": [8, 9, 10], "request": [8, 9, 10, 11], "pi": [8, 9], "one": 8, "There": [9, 10, 11], "special": [9, 10], "tag": [9, 10], "host": 9, "everyth": 9, "between": 9, "substitut": 9, "valu": 9, "within": 9, "desir": 9, "date": 9, "todai": 9, "get_dat": 9, "As": 9, "long": 9, "def": 9, "return": [9, 11], "datetim": 9, "now": 9, "strftime": 9, "y": 9, "m": 9, "d": 9, "string": 9, "intermedi": 10, "final": 10, "summaris": 10, "websit": 10, "ll": 10, "content": 10, "get_websit": 10, "website_url": 10, "given": 10, "summari": 10, "check": 10, "Then": 10, "insert": 10, "prior": 10, "step": 10, "few": 11, "four": 11, "loop": 11, "wait": 11, "speak": 11, "activ": 11, "whatev": 11, "web": 11, "http": 11, "port": 11, "8889": 11, "act": 11, "line": 11, "doe": 11, "show": 12, "engin": 12, "sai": 12, "hello": 12, "repli": 12, "howdi": 12, "must": 12, "multipl": 12}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"configur": 0, "exampl": [1, 8], "The": 2, "rule": [2, 8, 9, 10, 12], "yaml": 2, "file": 2, "fact": 2, "welcom": 3, "wafl": [3, 11], "": 3, "0": 3, "80": 3, "document": 3, "content": 3, "indic": 3, "tabl": 3, "initi": 4, "instal": 5, "interfac": 5, "side": 5, "llm": 5, "need": 5, "gpu": 5, "introduct": 6, "licens": 7, "execut": 9, "command": [9, 10], "local": 9, "function": 9, "rememb": 10, "run": 11, "audio": 11, "server": 11, "cli": 11, "test": 11, "simpl": 12}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Configuration": [[0, "configuration"]], "Examples": [[1, "examples"]], "The rules.yaml file": [[2, "the-rules-yaml-file"]], "Facts": [[2, "facts"]], "Rules": [[2, "rules"]], "Welcome to WAFL\u2019s 0.0.80 documentation!": [[3, "welcome-to-wafl-s-0-0-80-documentation"]], "Contents:": [[3, null]], "Indices and tables": [[3, "indices-and-tables"]], "Initialization": [[4, "initialization"]], "Installation": [[5, "installation"]], "Interface side": [[5, "interface-side"]], "LLM side (needs a GPU)": [[5, "llm-side-needs-a-gpu"]], "Introduction": [[6, "introduction"]], "License": [[7, "license"]], "Rule with examples": [[8, "rule-with-examples"]], "Rule with execute command": [[9, "rule-with-execute-command"]], "Local functions": [[9, "local-functions"]], "Rule with remember command": [[10, "rule-with-remember-command"]], "Running WAFL": [[11, "running-wafl"]], "$ wafl run-audio": [[11, "wafl-run-audio"]], "$ wafl run-server": [[11, "wafl-run-server"]], "$ wafl run-cli": [[11, "wafl-run-cli"]], "$ wafl run-tests": [[11, "wafl-run-tests"]], "Simple rule": [[12, "simple-rule"]]}, "indexentries": {}}) \ No newline at end of file +Search.setIndex({"docnames": ["actions", "configuration", "examples", "facts_and_rules", "index", "initialization", "installation", "introduction", "license", "rule_with_examples", "rules_with_execute_command", "rules_with_remember_command", "running_WAFL", "simple_rule", "testcases"], "filenames": ["actions.rst", "configuration.rst", "examples.rst", "facts_and_rules.rst", "index.rst", "initialization.rst", "installation.rst", "introduction.rst", "license.rst", "rule_with_examples.rst", "rules_with_execute_command.rst", "rules_with_remember_command.rst", "running_WAFL.rst", "simple_rule.rst", "testcases.rst"], "titles": ["Running Actions", "Configuration", "Examples", "The rules.yaml file", "Welcome to WAFL\u2019s 0.0.80 documentation!", "Initialization", "Installation", "Introduction", "License", "Rule with examples", "Rule with execute command", "Rule with remember command", "Running WAFL", "Simple rule", "Creating a testcase"], "terms": {"It": [0, 5, 7, 12], "i": [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "possibl": 0, "from": [0, 8, 14], "command": [0, 2, 4, 5, 6, 12, 14], "line": [0, 12, 14], "thi": [0, 1, 3, 5, 6, 7, 8, 10, 12, 13, 14], "us": [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 14], "carri": 0, "out": [0, 8], "convers": [0, 1, 3, 5, 14], "task": 0, "script": [0, 5], "cron": 0, "job": 0, "To": [0, 6, 14], "an": [0, 3, 5, 8, 11, 14], "follow": [0, 1, 3, 6, 7, 8, 9, 10, 11, 14], "wafl": [0, 5, 6, 7, 14], "name": [0, 1, 12, 14], "For": [0, 3, 9, 10], "exampl": [0, 3, 4, 6, 10, 12, 13], "hello": [0, 13, 14], "world": 0, "The": [0, 1, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14], "look": [0, 1], "file": [0, 1, 4, 5, 6, 8, 10, 12, 14], "yaml": [0, 1, 4, 5], "A": [0, 1, 6, 7, 8, 10, 13], "typic": [0, 1], "like": [0, 1, 3], "sai": [0, 13], "expect": [0, 14], "bot": [0, 1, 3, 9, 10, 13, 14], "output": [0, 3, 9, 10, 11], "greet": [0, 14], "again": 0, "anoth": [0, 9, 10], "each": [0, 9, 10, 14], "list": [0, 3, 9, 10, 14], "step": [0, 11], "dictionari": 0, "two": [0, 6, 10, 11], "kei": 0, "execut": [0, 2, 3, 4, 11, 12, 13], "respons": 0, "If": [0, 1], "doe": [0, 12, 14], "respond": [0, 14], "until": 0, "success": 0, "maximum": 0, "10": 0, "time": [0, 1, 3], "print": 0, "result": [0, 9, 10, 11, 12], "consol": 0, "call": [0, 3, 7], "rule": [0, 1, 2, 4, 5, 6, 7, 12], "function": [0, 1, 2, 4, 5, 7], "normal": 0, "config": [1, 5, 6, 12], "json": [1, 5, 6, 12], "contain": [1, 5, 12, 14], "some": [1, 5, 9, 10], "paramet": [1, 5], "chatbot": [1, 5, 12], "url": [1, 5, 11], "connect": [1, 5, 8], "backend": [1, 5], "waking_up_word": 1, "comput": [1, 3, 9, 10, 12], "waking_up_sound": 1, "true": 1, "deactivate_sound": 1, "py": [1, 5, 10], "llm_model": 1, "model_host": 1, "localhost": [1, 6], "model_port": 1, "8080": [1, 6], "listener_model": 1, "listener_hotword_logp": 1, "8": 1, "listener_volume_threshold": 1, "0": [1, 6], "6": 1, "listener_silence_timeout": 1, "7": 1, "speaker_model": 1, "text_embedding_model": 1, "These": [1, 3, 10], "set": [1, 3, 5, 6, 7], "regul": 1, "wake": 1, "up": 1, "system": [1, 6, 12], "run": [1, 4, 5, 6], "audio": [1, 4], "mode": [1, 12], "ar": [1, 3, 5, 10, 11, 12, 13, 14], "plai": [1, 7], "signal": 1, "back": 1, "idl": 1, "fact": [1, 4, 5], "guid": [1, 3, 5], "default": [1, 6, 12], "can": [1, 3, 5, 6, 9, 10, 11, 12, 13], "llm": [1, 4, 5], "model": [1, 3, 6, 7, 9, 10, 11], "listen": [1, 12], "detect": 1, "word": [1, 12], "similar": [1, 13], "threshold": 1, "volum": 1, "ani": [1, 8], "utter": [1, 14], "below": 1, "ignor": 1, "silenc": 1, "timeout": 1, "longer": 1, "than": 1, "consid": [1, 9, 10, 11], "finish": 1, "speaker": [1, 6], "text": [1, 6, 7, 10], "embed": [1, 6], "simpl": [2, 3, 4], "local": [2, 4, 5, 6, 12], "rememb": [2, 4], "languag": [3, 7, 9, 10, 11], "through": [3, 11], "retriev": 3, "dure": 3, "written": 3, "format": [3, 9], "do": [3, 8], "well": 3, "user": [3, 7, 9, 10, 11, 12, 13, 14], "want": [3, 9, 10, 11, 12], "know": [3, 10], "get_tim": 3, "ask": 3, "how": [3, 12, 13], "you": [3, 6, 11, 12, 13, 14], "add": 3, "its": 3, "prompt": [3, 9, 10, 11], "eventu": 3, "gener": [3, 9, 10, 11], "answer": [3, 14], "am": 3, "fine": 3, "compos": 3, "condit": [3, 8], "action": [3, 4, 8, 13], "trigger": 3, "match": [3, 13], "against": 3, "input": [3, 13], "In": [3, 6, 10, 13], "abov": [3, 8, 9, 10], "whole": 3, "ad": [3, 9, 10, 11], "item": 3, "order": [3, 6, 13], "should": [3, 9, 10], "think": [3, 9, 10], "introduct": 4, "instal": 4, "interfac": [4, 5], "side": [4, 5], "need": [4, 5, 11], "gpu": 4, "initi": [4, 6], "configur": 4, "server": [4, 6], "cli": 4, "test": [4, 5, 14], "creat": [4, 5, 6, 7], "testcas": [4, 5, 12], "neg": 4, "licens": 4, "index": 4, "modul": 4, "search": 4, "page": 4, "initialis": 5, "": [5, 10, 11], "work": [5, 7, 12, 13, 14], "environ": 5, "init": [5, 6], "l": 5, "db": 5, "main": 5, "requir": [5, 6, 7, 9, 10], "txt": [5, 12, 14], "secret": 5, "start_llm": 5, "sh": 5, "auxiliari": 5, "inform": 5, "about": 5, "state": 5, "edit": [5, 6], "manual": 5, "start": [5, 6, 14], "webserv": [5, 12], "python": [5, 9, 10, 11], "packag": 5, "mai": 5, "credenti": 5, "simpli": 5, "docker": [5, 6], "imag": [5, 6], "case": [5, 7, 10, 13], "version": [6, 7], "built": 6, "part": 6, "both": 6, "same": [6, 9], "machin": [6, 10], "first": 6, "your": [6, 7, 14], "have": [6, 13], "access": 6, "microphon": 6, "sudo": 6, "apt": 6, "get": 6, "portaudio19": 6, "dev": 6, "ffmpeg": 6, "pip": 6, "after": 6, "which": [6, 12], "chang": [6, 12], "standard": 6, "also": 6, "pleas": 6, "see": [6, 10], "chapter": 6, "second": 6, "speech": [6, 7], "quickli": 6, "port": [6, 12], "altern": 6, "p8080": 6, "env": 6, "nvidia_disable_requir": 6, "1": 6, "all": [6, 8, 12, 13, 14], "fractalego": [6, 8], "80": 6, "ha": 6, "fill": 6, "ip": 6, "address": 6, "final": [6, 11], "clone": 6, "repositori": 6, "http": [6, 12], "github": 6, "com": 6, "framework": 7, "person": [7, 8], "agent": 7, "integr": 7, "larg": 7, "recognit": 7, "combin": 7, "predict": 7, "behavior": 7, "defin": [7, 10, 12], "support": 7, "memori": [7, 10], "progress": 7, "current": [7, 10], "specifi": [7, 9, 10], "while": 7, "readi": 7, "might": 7, "product": 7, "depend": 7, "softwar": 8, "under": 8, "mit": 8, "copyright": 8, "c": 8, "2024": 8, "alberto": 8, "cetoli": 8, "io": 8, "permiss": 8, "herebi": 8, "grant": 8, "free": 8, "charg": 8, "obtain": 8, "copi": 8, "associ": 8, "document": [8, 12], "deal": 8, "without": 8, "restrict": 8, "includ": 8, "limit": 8, "right": 8, "modifi": 8, "merg": 8, "publish": 8, "distribut": 8, "sublicens": 8, "sell": 8, "permit": 8, "whom": 8, "furnish": 8, "so": 8, "subject": 8, "notic": [8, 9], "shall": 8, "substanti": 8, "portion": 8, "THE": [8, 9, 10], "provid": [8, 11], "AS": 8, "warranti": 8, "OF": 8, "kind": 8, "express": 8, "OR": 8, "impli": 8, "BUT": 8, "NOT": 8, "TO": 8, "merchant": 8, "fit": 8, "FOR": 8, "particular": 8, "purpos": [8, 12], "AND": 8, "noninfring": 8, "IN": 8, "NO": 8, "event": 8, "author": 8, "holder": 8, "BE": 8, "liabl": 8, "claim": 8, "damag": 8, "other": 8, "liabil": 8, "whether": 8, "contract": 8, "tort": 8, "otherwis": 8, "aris": 8, "WITH": 8, "make": [9, 10], "clearer": [9, 10], "effect": [9, 10], "suggest": [9, 10], "go": [9, 10], "math": [9, 10], "oper": [9, 10], "code": [9, 10, 11], "solv": [9, 10], "problem": [9, 10], "assign": [9, 10], "variabl": [9, 10], "what": [9, 10, 11, 14], "2": [9, 10], "squar": [9, 10], "root": [9, 10], "import": [9, 10], "sqrt": [9, 10], "exactli": [9, 10, 11], "THAT": [9, 10], "when": [9, 10, 11], "request": [9, 10, 11, 12], "pi": [9, 10], "one": 9, "There": [10, 11, 12], "special": [10, 11], "tag": [10, 11], "host": 10, "howev": 10, "everyth": 10, "between": 10, "substitut": 10, "valu": 10, "within": 10, "desir": 10, "date": 10, "todai": 10, "get_dat": 10, "As": 10, "long": 10, "def": 10, "return": [10, 12], "datetim": 10, "now": 10, "strftime": 10, "y": 10, "m": 10, "d": 10, "string": 10, "intermedi": 11, "summaris": 11, "websit": 11, "ll": 11, "content": 11, "get_websit": 11, "website_url": 11, "given": 11, "summari": 11, "check": 11, "Then": 11, "insert": 11, "prior": 11, "few": 12, "four": 12, "loop": 12, "wait": 12, "speak": 12, "activ": 12, "whatev": 12, "web": 12, "8889": 12, "act": 12, "show": 13, "engin": 13, "repli": 13, "howdi": 13, "must": 13, "multipl": 13, "consist": 14, "titl": 14, "bob": 14, "nice": 14, "meet": 14, "pass": 14, "wai": 14, "fail": 14, "thei": 14, "certain": 14, "prefix": 14, "correct": 14, "unknown": 14}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"run": [0, 12, 14], "action": 0, "configur": 1, "exampl": [2, 9], "The": 3, "rule": [3, 9, 10, 11, 13], "yaml": 3, "file": 3, "fact": 3, "welcom": 4, "wafl": [4, 12], "": 4, "0": 4, "80": 4, "document": 4, "content": 4, "indic": 4, "tabl": 4, "initi": 5, "instal": 6, "interfac": 6, "side": 6, "llm": 6, "need": 6, "gpu": 6, "introduct": 7, "licens": 8, "execut": 10, "command": [10, 11], "local": 10, "function": 10, "rememb": 11, "audio": 12, "server": 12, "cli": 12, "test": 12, "simpl": 13, "creat": 14, "testcas": 14, "neg": 14}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Running Actions": [[0, "running-actions"]], "Configuration": [[1, "configuration"]], "Examples": [[2, "examples"]], "The rules.yaml file": [[3, "the-rules-yaml-file"]], "Facts": [[3, "facts"]], "Rules": [[3, "rules"]], "Welcome to WAFL\u2019s 0.0.80 documentation!": [[4, "welcome-to-wafl-s-0-0-80-documentation"]], "Contents:": [[4, null]], "Indices and tables": [[4, "indices-and-tables"]], "Initialization": [[5, "initialization"]], "Installation": [[6, "installation"]], "Interface side": [[6, "interface-side"]], "LLM side (needs a GPU)": [[6, "llm-side-needs-a-gpu"]], "Introduction": [[7, "introduction"]], "License": [[8, "license"]], "Rule with examples": [[9, "rule-with-examples"]], "Rule with execute command": [[10, "rule-with-execute-command"]], "Local functions": [[10, "local-functions"]], "Rule with remember command": [[11, "rule-with-remember-command"]], "Running WAFL": [[12, "running-wafl"]], "$ wafl run-audio": [[12, "wafl-run-audio"]], "$ wafl run-server": [[12, "wafl-run-server"]], "$ wafl run-cli": [[12, "wafl-run-cli"]], "$ wafl run-tests": [[12, "wafl-run-tests"]], "Simple rule": [[13, "simple-rule"]], "Creating a testcase": [[14, "creating-a-testcase"]], "Running the testcases": [[14, "running-the-testcases"]], "Negative testcases": [[14, "negative-testcases"]]}, "indexentries": {}}) \ No newline at end of file diff --git a/documentation/source/actions.rst b/documentation/source/actions.rst new file mode 100644 index 00000000..d3279fc2 --- /dev/null +++ b/documentation/source/actions.rst @@ -0,0 +1,42 @@ +Running Actions +=============== + +It is possible to run actions from the command line. +This is useful to carry out conversational tasks as command line scripts or cron jobs. + +To run an action from the command line, use the following command: + +.. code-block:: bash + + $ wafl run-action + +For example, to run the ``hello-world`` action, use the following command: + +.. code-block:: bash + + $ wafl run-action hello-world + + +The ``run-action`` command will look in to the file actions.yaml. +A typical actions.yaml file looks like this: + +.. code-block:: yaml + + hello-world: + - + action: say "hello world" + expected: the bot outputs a greeting + - + action: say "hello world" again + expected: the bot outputs another greeting + + +Each action is a list of steps. +Each step is a dictionary with two keys: ``action`` and ``expected``. +The ``action`` key is the action to be executed. +The ``expected`` key is the expected response from the bot. +If the bot does not respond as expected, the action will run again until it is successful for a maximum of 10 times. + +The ``run-action`` command will run the action and print the result to the console. +Each action will call rules and functions as in a normal conversation. + diff --git a/documentation/source/index.rst b/documentation/source/index.rst index 431643d7..9f32cc73 100644 --- a/documentation/source/index.rst +++ b/documentation/source/index.rst @@ -17,6 +17,8 @@ Welcome to WAFL's 0.0.80 documentation! running_WAFL facts_and_rules examples + testcases + actions license Indices and tables diff --git a/documentation/source/installation.rst b/documentation/source/installation.rst index 2eb6156d..362a1398 100644 --- a/documentation/source/installation.rst +++ b/documentation/source/installation.rst @@ -32,20 +32,24 @@ Please see the examples in the following chapters. LLM side (needs a GPU) ---------------------- +The second part (LLM side) is a model server for the speech-to-text model, the LLM, the embedding system, and the text-to-speech model. +In order to quickly run the LLM side, you can use the following installation commands: -The second part is a machine that runs on a machine accessible from the interface side. -The initial configuration is for a local deployment of language models. -No action is needed to run WAFL if you want to run it as a local instance. +.. code-block:: bash + + $ pip install wafl-llm + $ wafl-llm start + + which will use the default models and start the server on port 8080. -However, a multi-user setup will benefit for a dedicated server. -In this case, a docker image can be used +Alternatively, a Docker image can be used to run it as in the following: .. code-block:: bash - $ docker run -p8080:8080 --env NVIDIA_DISABLE_REQUIRE=1 --gpus all fractalego/wafl-llm:latest + $ docker run -p8080:8080 --env NVIDIA_DISABLE_REQUIRE=1 --gpus all fractalego/wafl-llm:0.80 The interface side has a `config.json` file that needs to be filled with the IP address of the LLM side. The default is localhost. -Alternatively, you can run the LLM side by cloning `this repository `_. +Finally, you can run the LLM side by cloning [this repository](https://github.com/fractalego/wafl-llm). diff --git a/documentation/source/testcases.rst b/documentation/source/testcases.rst new file mode 100644 index 00000000..f7be4e06 --- /dev/null +++ b/documentation/source/testcases.rst @@ -0,0 +1,49 @@ +Creating a testcase +=================== + +The file testcases.txt contains a list of testcases. +Each testcase consists of a title and a list of utterances. + +.. code-block:: bash + + test the greetings work + user: Hello + bot: Hello there! What is your name + user: Bob + bot: Nice to meet you, bob! + + +The title is used to name the testcase. +Each line starting with "user:" is an utterance from the user. +Conversely, each line starting with "bot:" is an utterance from the bot. +The test passes if the bot responds with the utterance from the bot in a way that is consistent with the +answers in the testcase. +The test fails if the bot responds with an utterance that is not in the testcase. + + +Running the testcases +--------------------- + +To run the testcases, run the following command: + +.. code-block:: bash + + $ wafl run-tests + +This will run all the testcases in the testcases.txt file. + + +Negative testcases +------------------ + +Negative testcases are testcases that are expected to fail. +They are useful to test that the bot does not respond in a certain way. +Negative testcases are prefixed with "!". + +.. code-block:: bash + + ! test the greetings uses the correct name + user: Hello + bot: Hello there! What is your name + user: Bob + bot: Nice to meet you, unknown! \ No newline at end of file diff --git a/setup.py b/setup.py index e4b1e407..ba201367 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ "wafl.connectors.remote", "wafl.events", "wafl.extractors", - "wafl.filter", "wafl.inference", "wafl.interface", "wafl.knowledge", diff --git a/tests/config.json b/tests/config.json index 2dfa3b8c..4bb4fa90 100644 --- a/tests/config.json +++ b/tests/config.json @@ -1,48 +1,26 @@ { - "allow_interruptions": true, "waking_up_word": "computer", "waking_up_sound": true, "deactivate_sound": true, - "improvise_tasks": false, "rules": "rules.yaml", "functions": "functions.py", "llm_model": { - "model_is_local": false, - "local_model": "mistralai/Mistral-7B-Instruct-v0.1", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "model_host": "localhost", + "model_port": 8080 }, "listener_model": { - "model_is_local": false, - "local_model": "fractalego/personal-whisper-distilled-model", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - }, + "model_host": "localhost", + "model_port": 8080, "listener_hotword_logp": -8, "listener_volume_threshold": 0.6, "listener_silence_timeout": 0.7 }, "speaker_model": { - "model_is_local": false, - "local_model": "facebook/fastspeech2-en-ljspeech", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } - }, - "entailment_model": { - "model_is_local": false, - "local_model": "MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli" + "model_host": "localhost", + "model_port": 8080 }, "text_embedding_model": { - "model_is_local": false, - "local_model": "TaylorAI/gte-tiny", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "model_host": "localhost", + "model_port": 8080 } } diff --git a/tests/test_connection.py b/tests/test_connection.py index dd2df36e..d81daf20 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,16 +1,10 @@ import asyncio import os -import wave from unittest import TestCase - -import numpy as np - from wafl.config import Configuration from wafl.connectors.bridges.llm_chitchat_answer_bridge import LLMChitChatAnswerBridge -from wafl.connectors.local.local_llm_connector import LocalLLMConnector from wafl.connectors.remote.remote_llm_connector import RemoteLLMConnector -from wafl.listener.whisper_listener import WhisperListener from wafl.speaker.fairseq_speaker import FairSeqSpeaker _path = os.path.dirname(__file__) @@ -52,33 +46,3 @@ def test__connection_to_generative_model_can_generate_a_python_list(self): prediction = asyncio.run(connector.predict(prompt)) print(prediction) assert len(prediction) > 0 - - def test__local_llm_connector_can_generate_a_python_list(self): - config = Configuration.load_from_filename("local_config.json") - connector = LocalLLMConnector(config.get_value("llm_model")) - connector._num_prediction_tokens = 200 - prompt = "Generate a list of 4 chapters names for a space opera book. The output needs to be a python list of strings: " - prediction = asyncio.run(connector.predict(prompt)) - print(prediction) - assert len(prediction) > 0 - - def test__chit_chat_bridge_can_run_locally(self): - config = Configuration.load_from_filename("local_config.json") - dialogue_bridge = LLMChitChatAnswerBridge(config) - answer = asyncio.run(dialogue_bridge.get_answer("", "", "bot: hello")) - assert len(answer) > 0 - - def test__listener_local_connector(self): - config = Configuration.load_from_filename("local_config.json") - listener = WhisperListener(config) - f = wave.open(os.path.join(_path, "data/1002.wav"), "rb") - waveform = np.frombuffer(f.readframes(f.getnframes()), dtype=np.int16) / 32768 - result = asyncio.run(listener.input_waveform(waveform)) - expected = "DELETE BATTERIES FROM THE GROCERY LIST" - assert expected.lower() in result - - def test__speaker_local_connector(self): - config = Configuration.load_from_filename("local_config.json") - speaker = FairSeqSpeaker(config) - text = "Hello world" - asyncio.run(speaker.speak(text)) diff --git a/tests/test_testcases.py b/tests/test_testcases.py new file mode 100644 index 00000000..3a5fd0c7 --- /dev/null +++ b/tests/test_testcases.py @@ -0,0 +1,40 @@ +import asyncio +from unittest import TestCase + +from wafl.config import Configuration +from wafl.testcases import ConversationTestCases + +_wafl_example = """ +rules: + - the user says "hello": + - You must answer the user by writing "Hello. What is your name" + + - the user says their name: + - You must answer the user by writing "nice to meet you, NAME_OF_THE_USER!" +""".strip() + + +_test_case_greetings = """ + +test the greetings work + user: Hello + bot: Hello there! What is your name + user: Bob + bot: Nice to meet you, bob! + +! test the greetings uses the correct name + user: Hello + bot: Hello there! What is your name + user: Bob + bot: Nice to meet you, unknown! + + +""".strip() + + +class TestConversationalTestCases(TestCase): + def test_conversation_testcase_run_all(self): + config = Configuration.load_local_config() + config.set_value("rules", _wafl_example) + testcase = ConversationTestCases(config=config, text=_test_case_greetings) + self.assertTrue(asyncio.run(testcase.run())) diff --git a/tests/test_voice.py b/tests/test_voice.py index a4abbd12..6a95cba8 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -68,14 +68,6 @@ def test__random_sounds_are_excluded(self): expected = "[unclear]" assert result == expected - def test__voice_interface_receives_config(self): - config = Configuration.load_local_config() - interface = VoiceInterface(config) - assert ( - interface.listener_model_name - == config.get_value("listener_model")["local_model"] - ) - def test__hotword_listener_activated_using_recording_of_hotword(self): f = wave.open(os.path.join(_path, "data/computer.wav"), "rb") waveform = np.frombuffer(f.readframes(f.getnframes()), dtype=np.int16) / 32768 diff --git a/todo.txt b/todo.txt index 9808b652..d0d39bbb 100644 --- a/todo.txt +++ b/todo.txt @@ -1,11 +1,26 @@ ### TODO +/* create actions from command line + /* add condition of when to stop to the actions -* push docker image to docker hub -* update all to the vast.ai -* write new docs -* new version on github! -* make it easy to run the llm on the server (something more than docker perhaps)? +#### Find way to delete cache in remote llm connector +#### Put colors in action output (and dummy interface) +#### Add green for when an expectation is matched +#### write docs about actions +#### push new version to main + +* Perhaps the expectation pattern could be build in the rules themselves + +/* BUG: the prior memory leaks even when re-loading the interface!!! + +* clean single_file_knowledge: it still divides facts, question, and incomplete for rule retrieval. + Use just one retriever and threshold for all + +/* push docker image to docker hub +/* update all to the vast.ai +/* write new docs +/* new version on github! +/* make it easy to run the llm on the server (something more than docker perhaps)? /* re-train the whisper model using the distilled version /* make rules reloadable @@ -15,16 +30,16 @@ /* update tests -* lots of duplicates in facts! Avoid that - * use timestamp for facts (or an index in terms of conversation item) - * select only most n recent timestamps - * do not add facts that are already in the list (before cluster_facts) +/* lots of duplicates in facts! Avoid that +/ * use timestamp for facts (or an index in terms of conversation item) +/ * select only most n recent timestamps +/ * do not add facts that are already in the list (before cluster_facts) -* redeploy locally and on the server -* new version on github +/* redeploy locally and on the server +/* new version on github -* add rules for +/* add rules for / shopping lists trains and music diff --git a/wafl/answerer/dialogue_answerer.py b/wafl/answerer/dialogue_answerer.py index 53dce11b..7fe8c68c 100644 --- a/wafl/answerer/dialogue_answerer.py +++ b/wafl/answerer/dialogue_answerer.py @@ -10,6 +10,7 @@ get_last_user_utterance, ) from wafl.answerer.base_answerer import BaseAnswerer +from wafl.answerer.rule_creator import RuleCreator from wafl.connectors.bridges.llm_chitchat_answer_bridge import LLMChitChatAnswerBridge from wafl.exceptions import CloseConversation from wafl.extractors.dataclasses import Query, Answer @@ -18,6 +19,7 @@ class DialogueAnswerer(BaseAnswerer): def __init__(self, config, knowledge, interface, code_path, logger): + self._delete_current_rule = "" self._bridge = LLMChitChatAnswerBridge(config) self._knowledge = knowledge self._logger = logger @@ -27,15 +29,22 @@ def __init__(self, config, knowledge, interface, code_path, logger): self._max_num_past_utterances_for_rules = 0 self._prior_facts_with_timestamp = [] self._init_python_module(code_path.replace(".py", "")) + self._prior_rule_with_timestamp = None self._max_predictions = 3 + self._rule_creator = RuleCreator( + knowledge, + config, + interface, + max_num_rules=1, + delete_current_rule=self._delete_current_rule, + ) async def answer(self, query_text): if self._logger: self._logger.write(f"Dialogue Answerer: the query is {query_text}") - query = Query.create_from_text(query_text) + query = Query.create_from_text("The user says: " + query_text) rules_texts = await self._get_relevant_rules(query) - dialogue = self._interface.get_utterances_list_with_timestamp()[ -self._max_num_past_utterances : ] @@ -48,6 +57,20 @@ async def answer(self, query_text): dialogue_items = dialogue dialogue_items = sorted(dialogue_items, key=lambda x: x[0]) + if rules_texts: + last_timestamp = dialogue_items[-1][0] + self._prior_rule_with_timestamp = (last_timestamp, rules_texts) + dialogue_items = self._insert_rule_into_dialogue_items( + rules_texts, last_timestamp, dialogue_items + ) + + elif self._prior_rule_with_timestamp: + last_timestamp = self._prior_rule_with_timestamp[0] + rules_texts = self._prior_rule_with_timestamp[1] + dialogue_items = self._insert_rule_into_dialogue_items( + rules_texts, last_timestamp, dialogue_items + ) + last_bot_utterances = get_last_bot_utterances(dialogue_items, num_utterances=3) last_user_utterance = get_last_user_utterance(dialogue_items) dialogue_items = [item[1] for item in dialogue_items if item[0] >= start_time] @@ -76,6 +99,11 @@ async def answer(self, query_text): dialogue_items = last_user_utterance continue + if self._delete_current_rule in answer_text: + self._prior_rule_with_timestamp = None + dialogue_items += f"\n{original_answer_text}" + continue + if not memories: break @@ -98,14 +126,19 @@ async def _get_relevant_facts( > conversational_timestamp - self._max_num_past_utterances_for_facts ] facts_and_thresholds = await self._knowledge.ask_for_facts_with_threshold( - query, is_from_user=True, knowledge_name="/", threshold=0.8 + query, is_from_user=True, threshold=0.8 ) if facts_and_thresholds: - facts = [item[0].text for item in facts_and_thresholds if item[0].text not in memory] + facts = [ + item[0].text + for item in facts_and_thresholds + if item[0].text not in memory + ] self._prior_facts_with_timestamp.extend( (item, conversational_timestamp) for item in facts ) memory = "\n".join([item[0] for item in self._prior_facts_with_timestamp]) + await self._interface.add_fact(f"The bot remembers the facts:\n{memory}") else: if is_question(query.text) and not has_prior_rules: @@ -119,22 +152,8 @@ async def _get_relevant_facts( return memory - async def _get_relevant_rules(self, query, max_num_rules=1): - rules = await self._knowledge.ask_for_rule_backward( - query, - knowledge_name="/", - ) - rules = rules[:max_num_rules] - rules_texts = [] - for rule in rules: - rules_text = f"- If {rule.effect.text} go through the following points:\n" - for cause_index, causes in enumerate(rule.causes): - rules_text += f" {cause_index + 1}) {causes.text}\n" - - rules_texts.append(rules_text) - await self._interface.add_fact(f"The bot remembers the rule:\n{rules_text}") - - return "\n".join(rules_texts) + async def _get_relevant_rules(self, query): + return await self._rule_creator.create_from_query(query) def _init_python_module(self, module_name): self._module = import_module(module_name) @@ -196,3 +215,26 @@ async def _run_code(self, to_execute): result = f'\n"""python\n{to_execute}\n"""' return result + + def _insert_rule_into_dialogue_items( + self, rules_texts, rule_timestamp, dialogue_items + ): + new_dialogue_items = [] + already_inserted = False + for timestamp, utterance in dialogue_items: + if ( + not already_inserted + and utterance.startswith("user:") + and rule_timestamp == timestamp + ): + new_dialogue_items.append( + ( + rule_timestamp, + f"user: I want you to follow these rules:\n{rules_texts}", + ) + ) + already_inserted = True + + new_dialogue_items.append((timestamp, utterance)) + + return new_dialogue_items diff --git a/wafl/answerer/entailer.py b/wafl/answerer/entailer.py new file mode 100644 index 00000000..0daad8f3 --- /dev/null +++ b/wafl/answerer/entailer.py @@ -0,0 +1,46 @@ +import os + +from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory + +_path = os.path.dirname(__file__) + + +class Entailer: + def __init__(self, config): + self._connector = LLMConnectorFactory.get_connector(config) + self._config = config + + async def left_entails_right(self, lhs: str, rhs: str, dialogue: str) -> str: + prompt = await self._get_answer_prompt(lhs, rhs, dialogue) + result = await self._connector.generate(prompt) + result = self._clean_result(result) + return result == "yes" + + async def _get_answer_prompt(self, lhs, rhs, dialogue): + prompt = f""" + +This is a conversation between two agents ("bot" and "user"): +bot: what can I do for you? + +Given this dialogue, the task is to determine whether the following two utterances have the same meaning: +1) user: I need to book a flight to Paris. +2) user: I'd like to buy a plane ticket to paris. +Please answer "yes" or "no": yes + + + +This is a conversation between two agents ("bot" and "user"): +{dialogue} + +Given this dialogue, the task is to determine whether the following two utterances have the same meaning: +1) {lhs.lower()} +2) {rhs.lower()} +Please answer "yes" or "no": + """.strip() + return prompt + + def _clean_result(self, result): + result = result.replace("", "") + result = result.split("\n")[0] + result = result.strip() + return result.lower() diff --git a/wafl/answerer/rule_creator.py b/wafl/answerer/rule_creator.py new file mode 100644 index 00000000..a1a49ba0 --- /dev/null +++ b/wafl/answerer/rule_creator.py @@ -0,0 +1,49 @@ +class RuleCreator: + def __init__( + self, + knowledge, + config, + interface, + max_num_rules, + delete_current_rule, + max_recursion=1, + ): + self._knowledge = knowledge + self._config = config + self._interface = interface + self._max_num_rules = max_num_rules + self._delete_current_rule = delete_current_rule + self._max_indentation = max_recursion + self._indent_str = " " + + async def create_from_query(self, query): + rules = await self._knowledge.ask_for_rule_backward(query) + rules = rules[: self._max_num_rules] + rules_texts = [] + for rule in rules: + rules_text = f"- If {rule.effect.text} go through the following points:\n" + for cause_index, cause in enumerate(rule.causes): + rules_text += f"{self._indent_str}{cause_index + 1}) {cause.text}\n" + rules_text += await self.recursively_add_rules(cause) + + rules_text += f'{self._indent_str}{len(rule.causes) + 1}) After you completed all the steps output "{self._delete_current_rule}" and continue the conversation.\n' + + rules_texts.append(rules_text) + await self._interface.add_fact(f"The bot remembers the rule:\n{rules_text}") + + return "\n".join(rules_texts) + + async def recursively_add_rules(self, query, depth=2): + rules = await self._knowledge.ask_for_rule_backward(query, threshold=0.95) + rules = rules[: self._max_num_rules] + rules_texts = [] + for rule in rules: + rules_text = f"- If {rule.effect.text} go through the following points:\n" + for cause_index, causes in enumerate(rule.causes): + indentation = self._indent_str * depth + rules_text += f"{indentation}{cause_index + 1}) {causes.text}\n" + rules_text += await self.recursively_add_rules(causes.text, depth + 1) + + rules_texts.append(rules_text) + + return "\n".join(rules_texts) diff --git a/wafl/command_line.py b/wafl/command_line.py index 87b81ada..14fbb6cd 100644 --- a/wafl/command_line.py +++ b/wafl/command_line.py @@ -7,8 +7,9 @@ run_from_command_line, run_testcases, print_incipit, - download_models, + download_models ) +from wafl.runners.run_from_actions import run_action from wafl.runners.run_from_audio import run_from_audio from wafl.runners.run_web_interface import run_app @@ -21,6 +22,9 @@ def print_help(): print("> wafl run-audio: Run a voice-powered version of the chatbot") print("> wafl run-server: Run a webserver version of the chatbot") print("> wafl run-tests: Run the tests in testcases.txt") + print( + "> wafl run-action : Run the action from actions.yaml" + ) print() @@ -56,6 +60,16 @@ def process_cli(): run_testcases() remove_preprocessed("/") + elif command == "run-action": + if len(arguments) > 2: + action_name = arguments[2] + + else: + print("Please provide the action name as the second argument.") + return + + run_action(action_name) + elif command == "help": print_help() diff --git a/wafl/connectors/bridges/llm_chitchat_answer_bridge.py b/wafl/connectors/bridges/llm_chitchat_answer_bridge.py index 3acb086b..88449c68 100644 --- a/wafl/connectors/bridges/llm_chitchat_answer_bridge.py +++ b/wafl/connectors/bridges/llm_chitchat_answer_bridge.py @@ -15,19 +15,6 @@ async def get_answer(self, text: str, dialogue: str, query: str) -> str: return await self._connector.generate(prompt) async def _get_answer_prompt(self, text, rules_text, dialogue=None): - if rules_text: - rules_to_use = f"I want you to follow these rules:\n{rules_text.strip()}\n" - pattern = "\nuser: " - if pattern in dialogue: - last_user_position = dialogue.rfind(pattern) - before_user_dialogue, after_user_dialogue = ( - dialogue[:last_user_position], - dialogue[last_user_position + len(pattern) :], - ) - dialogue = f"{before_user_dialogue}\nuser: {rules_to_use}\nuser: {after_user_dialogue}" - else: - dialogue = f"user: {rules_to_use}\n{dialogue}" - prompt = f""" The following is a summary of a conversation. All the elements of the conversation are described briefly: diff --git a/wafl/connectors/factories/whisper_connector_factory.py b/wafl/connectors/factories/whisper_connector_factory.py index 8304adc8..5b4e1c2f 100644 --- a/wafl/connectors/factories/whisper_connector_factory.py +++ b/wafl/connectors/factories/whisper_connector_factory.py @@ -4,6 +4,4 @@ class WhisperConnectorFactory: @staticmethod def get_connector(config): - return RemoteWhisperConnector( - config.get_value("listener_model") - ) + return RemoteWhisperConnector(config.get_value("listener_model")) diff --git a/wafl/events/conversation_events.py b/wafl/events/conversation_events.py index 95dedb3b..cf38856b 100644 --- a/wafl/events/conversation_events.py +++ b/wafl/events/conversation_events.py @@ -21,6 +21,7 @@ def __init__( self._config = config self._knowledge = load_knowledge(config, logger) self._answerer = create_answerer(config, self._knowledge, interface, logger) + self._answerer._bridge._connector._cache = {} self._interface = interface self._logger = logger self._is_computing = False @@ -105,6 +106,11 @@ def is_computing(self): def reload_knowledge(self): self._knowledge = load_knowledge(self._config, self._logger) + def reset_discourse_memory(self): + self._answerer = create_answerer( + self._config, self._knowledge, self._interface, logger + ) + def _activation_word_in_text(self, activation_word, text): if f"[{normalized(activation_word)}]" in normalized(text): return True diff --git a/wafl/facts.py b/wafl/facts.py index 4c368210..23db2067 100644 --- a/wafl/facts.py +++ b/wafl/facts.py @@ -9,7 +9,6 @@ class Fact: is_interruption: bool = False source: str = None destination: str = None - knowledge_name: str = "/" def toJSON(self): return str(self) diff --git a/wafl/filter/base_filter.py b/wafl/filter/base_filter.py deleted file mode 100644 index 732dab10..00000000 --- a/wafl/filter/base_filter.py +++ /dev/null @@ -1,3 +0,0 @@ -class BaseAnswerFilter: - async def filter(self, dialogue_list, query_text) -> str: - raise NotImplementedError() diff --git a/wafl/interface/dummy_interface.py b/wafl/interface/dummy_interface.py index 1a8489bc..e651b2e1 100644 --- a/wafl/interface/dummy_interface.py +++ b/wafl/interface/dummy_interface.py @@ -5,32 +5,37 @@ from wafl.interface.base_interface import BaseInterface from wafl.interface.utils import not_good_enough +COLOR_START = "\033[94m" +COLOR_END = "\033[0m" + class DummyInterface(BaseInterface): - def __init__(self, to_utter=None, output_filter=None): + def __init__(self, to_utter=None, print_utterances=False): super().__init__() self._to_utter = to_utter self._bot_has_spoken = False self._dialogue = "" - self._output_filter = output_filter + self._print_utterances = print_utterances async def output(self, text: str, silent: bool = False): - if silent: - print(text) - return + if self._print_utterances: + if silent: + print(text) - if self._output_filter: - text = await self._output_filter.filter( - self.get_utterances_list_with_timestamp(), text - ) + else: + print(COLOR_START + "bot> " + text + COLOR_END) - self._dialogue += "bot: " + text + "\n" - self._utterances.append((time.time(), f"bot: {from_bot_to_user(text)}")) - self.bot_has_spoken(True) + if not silent: + self._dialogue += "bot: " + text + "\n" + self._utterances.append((time.time(), f"bot: {from_bot_to_user(text)}")) + self.bot_has_spoken(True) async def input(self) -> str: text = self._to_utter.pop(0).strip() text = self.__remove_activation_word_and_normalize(text) + if self._print_utterances: + print(COLOR_START + "user> " + text + COLOR_END) + while self._is_listening and not_good_enough(text): await self.output("I did not quite understand that") text = self._to_utter.pop(0) diff --git a/wafl/interface/queue_interface.py b/wafl/interface/queue_interface.py index 860e24af..08fdc247 100644 --- a/wafl/interface/queue_interface.py +++ b/wafl/interface/queue_interface.py @@ -5,23 +5,17 @@ class QueueInterface(BaseInterface): - def __init__(self, output_filter=None): + def __init__(self): super().__init__() self._bot_has_spoken = False self.input_queue = [] self.output_queue = [] - self._output_filter = output_filter async def output(self, text: str, silent: bool = False): if silent: self.output_queue.append({"text": text, "silent": True}) return - if self._output_filter: - text = await self._output_filter.filter( - self.get_utterances_list_with_timestamp(), text - ) - utterance = text self.output_queue.append({"text": utterance, "silent": False}) self._utterances.append((time.time(), f"bot: {text}")) diff --git a/wafl/interface/voice_interface.py b/wafl/interface/voice_interface.py index 44caea9a..46e53999 100644 --- a/wafl/interface/voice_interface.py +++ b/wafl/interface/voice_interface.py @@ -19,7 +19,7 @@ class VoiceInterface(BaseInterface): - def __init__(self, config, output_filter=None): + def __init__(self, config): super().__init__() self._sound_speaker = SoundFileSpeaker() self._activation_sound_filename = self.__get_activation_sound_from_config( @@ -28,7 +28,6 @@ def __init__(self, config, output_filter=None): self._deactivation_sound_filename = self.__get_deactivation_sound_from_config( config ) - self.listener_model_name = config.get_value("listener_model")["local_model"] self._speaker = FairSeqSpeaker(config) self._listener = WhisperListener(config) self._listener.set_timeout( @@ -42,7 +41,6 @@ def __init__(self, config, output_filter=None): ) self._bot_has_spoken = False self._utterances = [] - self._output_filter = output_filter async def add_hotwords_from_knowledge( self, knowledge: "Knowledge", max_num_words: int = 100, count_threshold: int = 5 @@ -66,11 +64,6 @@ async def output(self, text: str, silent: bool = False): if not text: return - if self._output_filter: - text = await self._output_filter.filter( - self.get_utterances_list_with_timestamp(), text - ) - self._listener.activate() text = from_bot_to_user(text) self._utterances.append((time.time(), f"bot: {text}")) diff --git a/wafl/knowledge/base_knowledge.py b/wafl/knowledge/base_knowledge.py index 04250dd5..0ad966cd 100644 --- a/wafl/knowledge/base_knowledge.py +++ b/wafl/knowledge/base_knowledge.py @@ -4,19 +4,16 @@ class BaseKnowledge: async def add(self, text): raise NotImplementedError() - async def add_rule(self, rule_text, knowledge_name=None): + async def add_rule(self, rule_text): raise NotImplementedError() - async def ask_for_facts(self, query, is_from_user=False, knowledge_name=None): + async def ask_for_facts(self, query, is_from_user=False): raise NotImplementedError() async def ask_for_facts_with_threshold( - self, query, is_from_user=False, knowledge_name=None, threshold=None + self, query, is_from_user=False, threshold=None ): raise NotImplementedError() - async def ask_for_rule_backward(self, query, knowledge_name=None, first_n=None): - raise NotImplementedError() - - async def has_better_match(self, query_text: str) -> bool: + async def ask_for_rule_backward(self, query, threshold=None): raise NotImplementedError() diff --git a/wafl/knowledge/single_file_knowledge.py b/wafl/knowledge/single_file_knowledge.py index 8d69f1d1..e2cc0621 100644 --- a/wafl/knowledge/single_file_knowledge.py +++ b/wafl/knowledge/single_file_knowledge.py @@ -6,16 +6,13 @@ from wafl.config import Configuration from wafl.facts import Fact -from wafl.simple_text_processing.normalize import normalized from wafl.knowledge.base_knowledge import BaseKnowledge from wafl.knowledge.utils import ( text_is_exact_string, get_first_cluster_of_rules, - filter_out_rules_that_are_too_dissimilar_to_query, ) from wafl.parsing.line_rules_parser import parse_rule_from_single_line from wafl.parsing.rules_parser import get_facts_and_rules_from_text -from wafl.extractors.dataclasses import Query from wafl.retriever.string_retriever import StringRetriever from wafl.retriever.dense_retriever import DenseRetriever from wafl.text_utils import clean_text_for_retrieval @@ -30,13 +27,11 @@ class SingleFileKnowledge(BaseKnowledge): _threshold_for_questions_from_user = 0.55 _threshold_for_questions_from_bot = 0.6 _threshold_for_questions_in_rules = 0.49 - _threshold_for_facts = 0.4 - _threshold_for_fact_rules = 0.22 - _threshold_for_fact_rules_for_creation = 0.1 - _threshold_for_partial_facts = 0.48 + _threshold_for_facts = 0.7 + _threshold_for_rules = 0.85 _max_rules_per_type = 3 - def __init__(self, config, rules_text=None, knowledge_name=None, logger=None): + def __init__(self, config, rules_text=None, logger=None): self._logger = logger self._facts_dict = {} self._rules_dict = {} @@ -45,17 +40,10 @@ def __init__(self, config, rules_text=None, knowledge_name=None, logger=None): "text_embedding_model", config, ) - self._rules_incomplete_retriever = DenseRetriever( - "text_embedding_model", config - ) - self._rules_fact_retriever = DenseRetriever("text_embedding_model", config) - self._rules_question_retriever = DenseRetriever("text_embedding_model", config) + self._rules_retriever = DenseRetriever("text_embedding_model", config) self._rules_string_retriever = StringRetriever() - knowledge_name = knowledge_name if knowledge_name else self.root_knowledge if rules_text: - facts_and_rules = get_facts_and_rules_from_text( - rules_text, knowledge_name=knowledge_name - ) + facts_and_rules = get_facts_and_rules_from_text(rules_text) self._facts_dict = { f"F{index}": value for index, value in enumerate(facts_and_rules["facts"]) @@ -73,9 +61,9 @@ def __init__(self, config, rules_text=None, knowledge_name=None, logger=None): if not loop or not loop.is_running(): asyncio.run(self._initialize_retrievers()) - async def add(self, text, knowledge_name="/"): + async def add(self, text): fact_index = f"F{len(self._facts_dict)}" - self._facts_dict[fact_index] = Fact(text=text, knowledge_name=knowledge_name) + self._facts_dict[fact_index] = Fact(text=text) await self._facts_retriever.add_text_and_index( clean_text_for_retrieval(text), fact_index ) @@ -83,30 +71,16 @@ async def add(self, text, knowledge_name="/"): clean_text_for_retrieval(text), fact_index ) - async def add_rule(self, rule_text, knowledge_name=None): - rule = parse_rule_from_single_line(rule_text, knowledge_name) + async def add_rule(self, rule_text): + rule = parse_rule_from_single_line(rule_text) index = str(len(self._rules_dict)) index = f"R{index}" self._rules_dict[index] = rule - await self._rules_fact_retriever.add_text_and_index( + await self._rules_retriever.add_text_and_index( clean_text_for_retrieval(rule.effect.text), index=index ) - async def has_better_match(self, query_text: str) -> bool: - if any(normalized(query_text).find(item) == 0 for item in ["yes", "no"]): - return False - - if any(normalized(query_text).find(item) != -1 for item in [" yes ", " no "]): - return False - - rules = await self.ask_for_rule_backward( - Query(text=f"The user says to the bot: '{query_text}.'", is_question=False) - ) - return any(rule.effect.is_interruption for rule in rules) - - async def ask_for_facts( - self, query, is_from_user=False, knowledge_name=None, threshold=None - ): + async def ask_for_facts(self, query, is_from_user=False, threshold=None): if query.is_question: indices_and_scores = await self._facts_retriever_for_questions.get_indices_and_scores_from_text( query.text @@ -136,7 +110,7 @@ async def ask_for_facts( ] async def ask_for_facts_with_threshold( - self, query, is_from_user=False, knowledge_name=None, threshold=None + self, query, is_from_user=False, threshold=None ): if query.is_question: indices_and_scores = await self._facts_retriever_for_questions.get_indices_and_scores_from_text( @@ -168,9 +142,9 @@ async def ask_for_facts_with_threshold( if item[1] > threshold ] - async def ask_for_rule_backward(self, query, knowledge_name=None, first_n=None): + async def ask_for_rule_backward(self, query, threshold=None): rules_and_scores = await self._ask_for_rule_backward_with_scores( - query, knowledge_name, first_n + query, threshold=threshold ) return get_first_cluster_of_rules(rules_and_scores) @@ -201,21 +175,9 @@ async def _initialize_retrievers(self): if text_is_exact_string(rule.effect.text): continue - if "{" in rule.effect.text: - await self._rules_incomplete_retriever.add_text_and_index( - clean_text_for_retrieval(rule.effect.text), index - ) - continue - - elif rule.effect.is_question: - await self._rules_question_retriever.add_text_and_index( - clean_text_for_retrieval(rule.effect.text), index - ) - - else: - await self._rules_fact_retriever.add_text_and_index( - clean_text_for_retrieval(rule.effect.text), index - ) + await self._rules_retriever.add_text_and_index( + clean_text_for_retrieval(rule.effect.text), index + ) for index, rule in self._rules_dict.items(): if not text_is_exact_string(rule.effect.text): @@ -238,9 +200,7 @@ async def create_from_list( return knowledge - async def _ask_for_rule_backward_with_scores( - self, query, knowledge_name=None, first_n=None - ): + async def _ask_for_rule_backward_with_scores(self, query, threshold=None): if text_is_exact_string(query.text): indices_and_scores = ( await self._rules_string_retriever.get_indices_and_scores_from_text( @@ -250,58 +210,21 @@ async def _ask_for_rule_backward_with_scores( return [(self._rules_dict[item[0]], item[1]) for item in indices_and_scores] indices_and_scores = ( - await self._rules_fact_retriever.get_indices_and_scores_from_text( - query.text - ) + await self._rules_retriever.get_indices_and_scores_from_text(query.text) ) - if not first_n: - fact_rules = [ - (self._rules_dict[item[0]], item[1]) - for item in indices_and_scores - if item[1] > self._threshold_for_fact_rules - ] - - else: - fact_rules = [ - (self._rules_dict[item[0]], item[1]) - for item in indices_and_scores - if item[1] > self._threshold_for_fact_rules_for_creation - ] - fact_rules = [item for item in sorted(fact_rules, key=lambda x: -x[1])][ - : self._max_rules_per_type - ] + if threshold == None: + threshold = self._threshold_for_rules - indices_and_scores = ( - await self._rules_question_retriever.get_indices_and_scores_from_text( - query.text - ) - ) - question_rules = [ + rules = [ (self._rules_dict[item[0]], item[1]) for item in indices_and_scores - if item[1] > self._threshold_for_questions_in_rules - ] - question_rules = [item for item in sorted(question_rules, key=lambda x: -x[1])][ - : self._max_rules_per_type + if item[1] > threshold ] - indices_and_scores = ( - await self._rules_incomplete_retriever.get_indices_and_scores_from_text( - query.text - ) - ) - incomplete_rules = [ - (self._rules_dict[item[0]], item[1]) - for item in indices_and_scores - if item[1] > self._threshold_for_partial_facts + rules = [item for item in sorted(rules, key=lambda x: -x[1])][ + : self._max_rules_per_type ] - incomplete_rules = [ - item for item in sorted(incomplete_rules, key=lambda x: -x[1]) - ][: self._max_rules_per_type] - rules_and_scores = fact_rules + question_rules + incomplete_rules - rules_and_scores = filter_out_rules_that_are_too_dissimilar_to_query( - query, rules_and_scores - ) + rules_and_scores = rules return rules_and_scores diff --git a/wafl/knowledge/utils.py b/wafl/knowledge/utils.py index 6928bb24..021a2b9e 100644 --- a/wafl/knowledge/utils.py +++ b/wafl/knowledge/utils.py @@ -42,20 +42,6 @@ def get_first_cluster_of_rules(rules_and_threshold): return rules -def filter_out_rules_that_are_too_dissimilar_to_query(query, rules_and_scores): - num_query_words = len(query.text.split()) - new_rules_and_scores = [] - for item in rules_and_scores: - rule = item[0] - num_rule_effect_words = len(rule.effect.text.split()) - if num_query_words < num_rule_effect_words / 3: - continue - - new_rules_and_scores.append(item) - - return new_rules_and_scores - - async def filter_out_rules_through_entailment(entailer, query, rules_and_scores): new_rules_and_scores = [] for rule, score in rules_and_scores: diff --git a/wafl/parsing/line_rules_parser.py b/wafl/parsing/line_rules_parser.py index a24c1845..8dbdb77d 100644 --- a/wafl/parsing/line_rules_parser.py +++ b/wafl/parsing/line_rules_parser.py @@ -3,7 +3,7 @@ from wafl.rules import Rule -def parse_rule_from_single_line(text, knowledge_name=None): +def parse_rule_from_single_line(text): if ":-" not in text: return None @@ -12,13 +12,11 @@ def parse_rule_from_single_line(text, knowledge_name=None): effect = Fact( text=effect_text.strip(), is_question=is_question(effect_text), - knowledge_name=knowledge_name, ) causes = [ Fact( text=item.strip(), is_question=is_question(item), - knowledge_name=knowledge_name, ) for item in causes_text.split(";") ] diff --git a/wafl/parsing/rules_parser.py b/wafl/parsing/rules_parser.py index d77a08ff..ceb9fc57 100644 --- a/wafl/parsing/rules_parser.py +++ b/wafl/parsing/rules_parser.py @@ -5,7 +5,7 @@ from wafl.simple_text_processing.deixis import from_user_to_bot -def get_facts_and_rules_from_text(text: str, knowledge_name=None): +def get_facts_and_rules_from_text(text: str): parsed_text_dict = yaml.safe_load(text) fact_strings = parsed_text_dict.get("facts", []) rules_list = parsed_text_dict.get("rules", {}) diff --git a/wafl/retriever/dense_retriever.py b/wafl/retriever/dense_retriever.py index d6285267..4bf3bb8c 100644 --- a/wafl/retriever/dense_retriever.py +++ b/wafl/retriever/dense_retriever.py @@ -28,9 +28,6 @@ async def add_text_and_index(self, text: str, index: str): async def get_indices_and_scores_from_text( self, text: str ) -> List[Tuple[str, float]]: - if not text or len(text) < self._threshold_length: - return [] - embeddings = await self._get_embeddings_from_text(text) return self._embeddings_model.similar_by_vector(embeddings, topn=5) diff --git a/wafl/rules.py b/wafl/rules.py index 76fb5a48..3e3d32f6 100644 --- a/wafl/rules.py +++ b/wafl/rules.py @@ -6,7 +6,6 @@ class Rule: effect: "Fact" causes: List["Fact"] - knowledge_name: str = "/" def toJSON(self): return str(self) diff --git a/wafl/run.py b/wafl/run.py index b0397e84..4f664ecb 100644 --- a/wafl/run.py +++ b/wafl/run.py @@ -52,3 +52,4 @@ def download_models(): import nltk nltk.download("averaged_perceptron_tagger") + diff --git a/wafl/runners/run_from_actions.py b/wafl/runners/run_from_actions.py new file mode 100644 index 00000000..c4868874 --- /dev/null +++ b/wafl/runners/run_from_actions.py @@ -0,0 +1,82 @@ +import asyncio + +import yaml + +from wafl.answerer.entailer import Entailer +from wafl.config import Configuration +from wafl.events.conversation_events import ConversationEvents +from wafl.interface.dummy_interface import DummyInterface +from wafl.logger.local_file_logger import LocalFileLogger + +_logger = LocalFileLogger() + +COLOR_YELLOW = "\033[93m" +COLOR_GREEN = "\033[92m" +COLOR_END = "\033[0m" + + +def get_action_list_and_expeted_list_from_yaml(filename, action_name): + actions = yaml.safe_load(open("actions.yaml")) + if action_name not in actions: + raise ValueError(f"Action {action_name} not found in actions.yaml") + + actions_list = [item["action"] for item in actions[action_name]] + expected_list = [item["expected"] for item in actions[action_name]] + return actions_list, expected_list + + +def predict_action(config, actions_list, expected_list): + interface = DummyInterface(to_utter=actions_list.copy(), print_utterances=True) + conversation_events = ConversationEvents( + config=config, + interface=interface, + logger=_logger, + ) + entailer = Entailer(config) + for expected in expected_list: + asyncio.run(conversation_events.process_next()) + last_utterance = interface.get_utterances_list()[-1] + + if not last_utterance: + raise ValueError("The agent did not say anything.") + + if expected and not asyncio.run( + entailer.left_entails_right( + last_utterance, + expected, + "\n".join(interface.get_utterances_list()[:-1]), + ) + ): + del entailer, conversation_events, interface + raise ValueError( + f"The utterance '{last_utterance}' does not entail '{expected}'." + ) + + +def run_action(action_name): + print(COLOR_GREEN + f"Running the action {action_name}\n" + COLOR_END) + actions_list, expected_list = get_action_list_and_expeted_list_from_yaml( + "actions.yaml", action_name + ) + config = Configuration.load_local_config() + num_retries = 10 + success = False + for _ in range(num_retries): + try: + predict_action(config, actions_list, expected_list) + success = True + break + + except (ValueError, SyntaxError) as e: + print(COLOR_YELLOW + str(e) + COLOR_END) + print(COLOR_GREEN + f"Retrying the action {action_name}..." + COLOR_END) + + if success: + print(COLOR_GREEN + f"Action {action_name} finished." + COLOR_END) + + else: + print(COLOR_YELLOW + f"Action {action_name} failed." + COLOR_END) + + +if __name__ == "__main__": + run_action("action_1_summarise_guardian") diff --git a/wafl/scheduler/web_loop.py b/wafl/scheduler/web_loop.py index 191bc5ae..f43a5a9e 100644 --- a/wafl/scheduler/web_loop.py +++ b/wafl/scheduler/web_loop.py @@ -47,6 +47,7 @@ async def reset_conversation(self): self._interface.deactivate() self._interface.activate() self._conversation_events.reload_knowledge() + self._conversation_events.reset_discourse_memory() await self._interface.output("Hello. How may I help you?") conversation = await self._get_conversation() return conversation diff --git a/wafl/templates/actions.yaml b/wafl/templates/actions.yaml new file mode 100644 index 00000000..8edeb6a8 --- /dev/null +++ b/wafl/templates/actions.yaml @@ -0,0 +1,7 @@ +action_1_summarise_guardian: + - + action: get the news from the guardian website + expected: the bot outputs a list of headlines + - + action: write the list of news headlines above as a text into the file summary_news.txt + expected: \ No newline at end of file diff --git a/wafl/templates/functions.py b/wafl/templates/functions.py index 4936c99b..0d001f89 100644 --- a/wafl/templates/functions.py +++ b/wafl/templates/functions.py @@ -1,7 +1,8 @@ import json +import html2text +import re import requests -from bs4 import BeautifulSoup from datetime import datetime, timedelta from wafl.exceptions import CloseConversation @@ -65,10 +66,21 @@ def check_weather_lat_long(latitude, longitude, day): def get_website(url): - response = requests.get(url) - soup = BeautifulSoup(response.content, "html.parser") - text = soup.get_text() - return text + text = requests.get(url).content.decode("utf-8") + h = html2text.HTML2Text() + h.ignore_links = True + return h.handle(text).strip()[:1000] + + +def get_guardian_headlines(): + url = "https://www.theguardian.com/uk" + text = requests.get(url).content.decode("utf-8") + pattern = re.compile(r"

    (.*?)

    ", re.MULTILINE) + matches = pattern.findall(text) + text = "-" + "\n-".join(matches) + h = html2text.HTML2Text() + h.ignore_links = True + return h.handle(text).strip() def get_time(): @@ -111,3 +123,10 @@ def get_shopping_list(): return "nothing" return ", ".join(db["shopping_list"]) + + +def write_to_file(filename, text): + with open(filename, "w") as file: + file.write(text) + + return f"File {filename} saved" diff --git a/wafl/testcases.py b/wafl/testcases.py index 19ee3f84..017ad0f8 100644 --- a/wafl/testcases.py +++ b/wafl/testcases.py @@ -1,5 +1,4 @@ -from fuzzywuzzy import fuzz -from wafl.events.utils import load_knowledge +from wafl.answerer.entailer import Entailer from wafl.simple_text_processing.deixis import from_user_to_bot, from_bot_to_user from wafl.exceptions import CloseConversation from wafl.events.conversation_events import ConversationEvents @@ -13,10 +12,10 @@ class ConversationTestCases: GREEN_COLOR_START = "\033[32m" COLOR_END = "\033[0m" - def __init__(self, config, text, code_path=None, logger=None): + def __init__(self, config, text, logger=None): + self._config = config self._testcase_data = get_user_and_bot_lines_from_text(text) - self._knowledge = load_knowledge(config, logger) - self._code_path = code_path if code_path else "/" + self._entailer = Entailer(config) async def test_single_case(self, name): if name not in self._testcase_data: @@ -26,12 +25,10 @@ async def test_single_case(self, name): test_lines = self._testcase_data[name]["lines"] is_negated = self._testcase_data[name]["negated"] interface = DummyInterface(user_lines) - conversation_events = ConversationEvents( - self._knowledge, interface=interface, code_path=self._code_path - ) + conversation_events = ConversationEvents(self._config, interface=interface) + await conversation_events._knowledge._initialize_retrievers() print(self.BLUE_COLOR_START + f"\nRunning test '{name}'." + self.COLOR_END) - continue_conversations = True while continue_conversations: try: @@ -42,14 +39,18 @@ async def test_single_case(self, name): is_consistent = True generated_lines = interface.get_utterances_list() + prior_dialogue = [] for test_line, generated_line in zip(test_lines, generated_lines): - test_line = self._apply_deixis(test_line) - if not await self._lhs_is_similar_to(generated_line, test_line): + if not await self._lhs_is_similar_to( + generated_line, test_line, prior_dialogue + ): print(f" [test_line] {test_line}") print(f" [predicted_line] {generated_line}") is_consistent = False break + prior_dialogue.append(generated_line) + if (is_consistent and not is_negated) or (not is_consistent and is_negated): print(self.GREEN_COLOR_START + " [Success]\n\n" + self.COLOR_END) return True @@ -63,7 +64,6 @@ async def test_single_case(self, name): async def run(self): to_return = True - for name in self._testcase_data: result = await self.test_single_case(name) if not result: @@ -71,15 +71,15 @@ async def run(self): return to_return - async def _lhs_is_similar_to(self, lhs, rhs): + async def _lhs_is_similar_to(self, lhs, rhs, prior_dialogue): lhs_name = lhs.split(":")[0].strip() rhs_name = rhs.split(":")[0].strip() if lhs_name != rhs_name: return False - lhs = ":".join(item.strip() for item in lhs.split(":")[1:]) - rhs = ":".join(item.strip() for item in rhs.split(":")[1:]) - return fuzz.ratio(lhs, rhs) > 80 + return await self._entailer.left_entails_right( + lhs, rhs, "\n".join(prior_dialogue) + ) def _apply_deixis(self, line): name = line.split(":")[0].strip() diff --git a/wafl/variables.py b/wafl/variables.py index 0a5126b3..84ebf205 100644 --- a/wafl/variables.py +++ b/wafl/variables.py @@ -1,4 +1,4 @@ def get_variables(): return { - "version": "0.0.80", + "version": "0.0.82", }