diff --git a/.github/workflows/development-tests1.yml b/.github/workflows/development-tests1.yml index 71409201..f8fb0d3e 100644 --- a/.github/workflows/development-tests1.yml +++ b/.github/workflows/development-tests1.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install portaudio19-dev + sudo apt-get install portaudio19-dev python3-packaging python -m pip install --upgrade pip python -m pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi diff --git a/README.md b/README.md index 19d0e073..5a62028f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WAFL 0.0.70 [![Tests](https://github.com/fractalego/wafl/actions/workflows/development-tests1.yml/badge.svg)](https://github.com/fractalego/wafl/actions/workflows/development-tests1.yml)[![Docs](https://readthedocs.org/projects/wafl/badge/?version=latest)](https://wafl.readthedocs.io/en/latest/) +# WAFL 0.0.80 [![Tests](https://github.com/fractalego/wafl/actions/workflows/development-tests1.yml/badge.svg)](https://github.com/fractalego/wafl/actions/workflows/development-tests1.yml)[![Docs](https://readthedocs.org/projects/wafl/badge/?version=latest)](https://wafl.readthedocs.io/en/latest/) Introduction ============ @@ -46,16 +46,11 @@ Please see the examples in the following chapters. ## 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 +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. +A docker image can be used to run it as in the following: ```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. diff --git a/datasets/create_rules_dataset.py b/datasets/create_rules_dataset.py new file mode 100644 index 00000000..c711745a --- /dev/null +++ b/datasets/create_rules_dataset.py @@ -0,0 +1,45 @@ +import asyncio +import pandas as pd + +from wafl.config import Configuration +from wafl.connectors.remote.remote_llm_connector import RemoteLLMConnector + + +def get_prompt(df, theme): + prompt = "" + for _, row in df.sample(9).iterrows(): + prompt += ( + f""" + +Create a plausible dialogue about the theme \"{row["Theme"]}\" based on the following summary and rules. + +The rules are as follows: +{row["Rules"]} + +The conversation goes as follows: +{row["Conversation"]} + + """.strip() + + "\n\n" + ) + + return ( + prompt + + f'\nCreate plausible dialogue about the theme "{theme}" based on the following summary and rules.\n\nThe rules are as follows:\n' + ) + + +if __name__ == "__main__": + config = Configuration.load_local_config() + remote_llm_connector = RemoteLLMConnector( + config.get_value("llm_model"), last_strings=[""] + ) + + df = pd.read_csv("data/complex_instructions.csv") + theme = "playing a song that the user likes" + prompt = get_prompt(df, theme) + print( + asyncio.run( + remote_llm_connector.predict(prompt, temperature=0.5, num_tokens=1500) + ) + ) diff --git a/datasets/train_llm_on_rules_dataset.py b/datasets/train_llm_on_rules_dataset.py new file mode 100644 index 00000000..251bbe9d --- /dev/null +++ b/datasets/train_llm_on_rules_dataset.py @@ -0,0 +1,122 @@ +import random + +import pandas as pd +from datasets import Dataset +from transformers import ( + AutoTokenizer, + AutoModelForCausalLM, + TrainingArguments, + Trainer, + DataCollatorForLanguageModeling, +) + +model_name_or_path = "mistralai/Mistral-7B-Instruct-v0.1" +max_length = 1024 + 512 + + +def get_prompts(df): + prompts = [] + for _, row in df.sample(frac=1).iterrows(): + memory = "" + if memory == "": + memory = "The user has no memory." + + current_rule = row["Rules"] + rules = df.sample(random.choice([1, 2]))["Rules"].tolist() + [current_rule] + random.shuffle(rules) + rules = "\n".join(rules) + prompt = ( + f""" +The user is talking with a chatbot about the theme \"{row["Theme"]}\" based on the following summary. + +{memory} + + +The rules are as follows: + +{rules} + + +The conversation goes as follows: +{row["Conversation"]} + """.strip() + + "\n\n" + ) + prompts.append(prompt) + + return prompts + + +def preprocess_function(sample): + model_inputs = tokenizer( + sample["prompt"], + return_tensors="pt", + max_length=max_length, + padding="max_length", + ) + labels = tokenizer( + sample["prompt"], + return_tensors="pt", + max_length=max_length, + padding="max_length", + ) + + model_inputs["labels"] = labels["input_ids"] + return model_inputs + + +def model_init(): + model = AutoModelForCausalLM.from_pretrained(model_name_or_path) + parameters = model.parameters() + for parameter in parameters: + parameter.requires_grad = False + + model.model.enable_input_require_grads() + model.lm_head.training = True + for index in range(len(model.model.layers)): + model.model.layers[index].self_attn.k_proj.training = True + + return model + + +def create_dataset_from_file(filepath): + df = pd.read_csv(filepath) + prompts = get_prompts(df) + return Dataset.from_dict({"prompt": prompts}) + + +if __name__ == "__main__": + tokenizer = AutoTokenizer.from_pretrained(model_name_or_path) + tokenizer.pad_token = tokenizer.eos_token + dataset = create_dataset_from_file("data/complex_instructions.csv") + train_dataset = dataset.map( + preprocess_function, batched=True, batch_size=1, num_proc=4 + ) + data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False) + learning_rate = 1e-6 + output_dir_name = f"checkpoint_lr{learning_rate}" + training_args = TrainingArguments( + output_dir=output_dir_name, + per_device_train_batch_size=1, + per_device_eval_batch_size=1, + evaluation_strategy="steps", + use_cpu=True, + learning_rate=learning_rate, + num_train_epochs=2, + logging_steps=200, + eval_steps=200, + save_total_limit=1, + ) + model = model_init() + trainer = Trainer( + model=model, + args=training_args, + tokenizer=tokenizer, + data_collator=data_collator, + train_dataset=train_dataset, + ) + trainer.train() + trainer.save_model("wafl-mistral") + model = trainer.model + model.push_to_hub("fractalego/wafl-mistral") + tokenizer.push_to_hub("fractalego/wafl-mistral") diff --git a/documentation/build/doctrees/directory_structure.doctree b/documentation/build/doctrees/directory_structure.doctree deleted file mode 100644 index 6f17a3b2..00000000 Binary files a/documentation/build/doctrees/directory_structure.doctree and /dev/null differ diff --git a/documentation/build/doctrees/environment.pickle b/documentation/build/doctrees/environment.pickle index f93f325a..b3797a50 100644 Binary files a/documentation/build/doctrees/environment.pickle and b/documentation/build/doctrees/environment.pickle differ diff --git a/documentation/build/doctrees/examples.doctree b/documentation/build/doctrees/examples.doctree index bd9579a5..1640e068 100644 Binary files a/documentation/build/doctrees/examples.doctree and b/documentation/build/doctrees/examples.doctree differ diff --git a/documentation/build/doctrees/index.doctree b/documentation/build/doctrees/index.doctree index 6a397c8e..f358705e 100644 Binary files a/documentation/build/doctrees/index.doctree and b/documentation/build/doctrees/index.doctree differ diff --git a/documentation/build/doctrees/introduction.doctree b/documentation/build/doctrees/introduction.doctree index 35b82c28..793935e7 100644 Binary files a/documentation/build/doctrees/introduction.doctree and b/documentation/build/doctrees/introduction.doctree differ diff --git a/documentation/build/doctrees/license.doctree b/documentation/build/doctrees/license.doctree index a9ef0d9c..e0daffcd 100644 Binary files a/documentation/build/doctrees/license.doctree and b/documentation/build/doctrees/license.doctree differ diff --git a/documentation/build/doctrees/rules.doctree b/documentation/build/doctrees/rules.doctree deleted file mode 100644 index ad3a0350..00000000 Binary files a/documentation/build/doctrees/rules.doctree and /dev/null differ diff --git a/documentation/build/doctrees/rules_and_backtracking.doctree b/documentation/build/doctrees/rules_and_backtracking.doctree deleted file mode 100644 index 220b51a9..00000000 Binary files a/documentation/build/doctrees/rules_and_backtracking.doctree and /dev/null differ diff --git a/documentation/build/doctrees/wafl_init.doctree b/documentation/build/doctrees/wafl_init.doctree deleted file mode 100644 index f4eb3481..00000000 Binary files a/documentation/build/doctrees/wafl_init.doctree and /dev/null differ diff --git a/documentation/build/html/_sources/directory_structure.rst.txt b/documentation/build/html/_sources/directory_structure.rst.txt deleted file mode 100644 index 37167e75..00000000 --- a/documentation/build/html/_sources/directory_structure.rst.txt +++ /dev/null @@ -1,43 +0,0 @@ -Directory structure -=================== - -Consider the following directory structure in your project - -.. code-block:: bash - - . - ├── functions.py - ├── wafl.rules # (1) - │ - ├── facts - │ ├── functions.py - │ └── wafl.rules - ├── greetings - │ ├── wafl.rules # (2) - │ ├── functions.py - │ └── facts - │ ├── wafl.rules - │ └── functions.py - └── interruptions - ├── functions.py - └── wafl.rules - -The rules can be organised into a nested directory structure. -Each directory must contain `rules.wafl` and `functions.py`. -The first one contains the rules as explained in `the rules section `_. - -The directories to be used for inference must be used within the `wafl.rules` (1) file according to the following syntax - -.. code-block:: text - - #using facts - #using greetings - #using interruptions - -The keyword `#using ` includes the specified folder in the inference tree. -Only the rules and facts that are included will be part of the inference. -For example, the keyword `#using facts` within greetings/ (2) will not include the folder above it. -Inference in a subfolder is limited the the rules and facts that are part of that folder or below it. - -For more complete example, you can have a look at the (still early) project in -`wafl_home `_. \ No newline at end of file diff --git a/documentation/build/html/_sources/examples.rst.txt b/documentation/build/html/_sources/examples.rst.txt index 802b35dc..0872bb1b 100644 --- a/documentation/build/html/_sources/examples.rst.txt +++ b/documentation/build/html/_sources/examples.rst.txt @@ -4,5 +4,7 @@ Examples .. toctree:: :maxdepth: 2 - wafl_init - directory_structure \ No newline at end of file + simple_rule + rule_with_examples + rules_with_execute_command + rules_with_remember_command \ No newline at end of file diff --git a/documentation/build/html/_sources/index.rst.txt b/documentation/build/html/_sources/index.rst.txt index d2f4b8bd..431643d7 100644 --- a/documentation/build/html/_sources/index.rst.txt +++ b/documentation/build/html/_sources/index.rst.txt @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to WAFL's 0.0.45 documentation! +Welcome to WAFL's 0.0.80 documentation! ======================================= .. toctree:: @@ -12,9 +12,10 @@ Welcome to WAFL's 0.0.45 documentation! introduction installation + initialization + configuration running_WAFL - query_processing_pipeline - rules + facts_and_rules examples license diff --git a/documentation/build/html/_sources/introduction.rst.txt b/documentation/build/html/_sources/introduction.rst.txt index a1a6c0e9..7450893e 100644 --- a/documentation/build/html/_sources/introduction.rst.txt +++ b/documentation/build/html/_sources/introduction.rst.txt @@ -5,8 +5,7 @@ WAFL is a framework for personal agents. It integrates Large language models, speech recognition and text to speech. This framework combines Large Language Models and rules to create a predictable behavior. -Specifically, instead of organising the work of an LLM into a chain of thoughts, -WAFL intends to organise its behavior into inference trees. +A set of rules is used to define the behavior of the agent, supporting function calling and a working memory. WAFL is a work in progress. The current version requires the user to specify the rules to follow. diff --git a/documentation/build/html/_sources/license.rst.txt b/documentation/build/html/_sources/license.rst.txt index 572eafe6..9b6910aa 100644 --- a/documentation/build/html/_sources/license.rst.txt +++ b/documentation/build/html/_sources/license.rst.txt @@ -3,7 +3,7 @@ License This software is licensed under the MIT License: -Copyright (c) 2023 alberto.cetoli@fractalego.io +Copyright (c) 2024 alberto.cetoli@fractalego.io 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: diff --git a/documentation/build/html/_sources/rules.rst.txt b/documentation/build/html/_sources/rules.rst.txt deleted file mode 100644 index 86b1dba4..00000000 --- a/documentation/build/html/_sources/rules.rst.txt +++ /dev/null @@ -1,11 +0,0 @@ -Rules -===== - -Examples -======== - -.. toctree:: - :maxdepth: 2 - - writing_the_rules - rules_and_backtracking \ No newline at end of file diff --git a/documentation/build/html/_sources/rules_and_backtracking.rst.txt b/documentation/build/html/_sources/rules_and_backtracking.rst.txt deleted file mode 100644 index ee79e6e7..00000000 --- a/documentation/build/html/_sources/rules_and_backtracking.rst.txt +++ /dev/null @@ -1,28 +0,0 @@ -Rules and backtracking -====================== - -An input that satisfies a trigger condition will make the most relevant rule go through the list of actions within. -However, if one of the actions fails then the next most relevant rule will be activated, and so on until there are -no more relevant rules. - -For example, if there are two rules - -.. code-block:: text - - The user is feeling well - the user's name is John - SAY Happy you are feeling well, John - - The user is feeling ok - the user's name is Jane - SAY Happy you are feeling well, Jane - -and the user says: "My name is Jane and I am feeling well". -The first rule will be activated first, until the condition "the user's name is John" returns False. -Then the second rule will be activated because "feeling ok" is very similar to "feeling well". -The end result is that the bot will say "Happy you are feeling well, Jane" - -The backtracking of rules applies recursively. -In the end, a rule returns True only if all its actions return True. -Otherwise, execution is truncated and the next relevant rule starts a new inference branch. - diff --git a/documentation/build/html/_sources/wafl_init.rst.txt b/documentation/build/html/_sources/wafl_init.rst.txt deleted file mode 100644 index 070713d9..00000000 --- a/documentation/build/html/_sources/wafl_init.rst.txt +++ /dev/null @@ -1,67 +0,0 @@ -Initialization --------------- - -This command initialises WAFL's work environment - -.. code-block:: bash - - $ wafl init - -It creates a set of files that can be used to the interface side of WAFL. - -.. code-block:: bash - - $ ls - config.json - events.py - functions.py - rules.wafl - start_llm.sh - testcases.txt - -- The `config.json` file contains the configuration. - -.. code-block:: text - - { - "allow_interruptions": true, - "waking_up_word": "computer", - "waking_up_sound": true, - "deactivate_sound": true, - "improvise_tasks": true, - ... - } - -These settings regulate the following: - - * The "allow_interruptions" allows the user to create rules with the highest priority. - For example, the user might want a rule to be triggered in the middle of a conversation. - - * "waking_up_word" is the name of the bot, used to wake up the system in the "run-audio" mode. - - * "waking_up_sound" and "deactivate_sound" are played to signal the system is up or is back to idle. - - * "improvise_tasks" allows the system to create its own rules to accomplish a goal. - - -- The rules.wafl file contains the rules that guide the chatbot. - The rules are written in the WAFL language, with a trigger condition on top and a list of actions below. - -.. code-block:: text - - The user says "bring yourself online" - SAY Hello there! - -This rule is activated when the user says "bring yourself online", and the action is for the machine to say "Hello there!". - - -- The `functions.py` file contains the functions that can be used in the `rules.wafl` file. - -- The `events.py` file contains the event generation functions. - For example, there is a function that returns the time and one that returns the data. - These functions are executed every minute and may be used to activate one of the rules. - -- `start_llm.sh` is a script that starts the LLM locally. - It simply starts a docker container with the LLM image. - -- The `testcases.txt` file contains the test cases that can be used to test the LLM. diff --git a/documentation/build/html/directory_structure.html b/documentation/build/html/directory_structure.html deleted file mode 100644 index 57674d11..00000000 --- a/documentation/build/html/directory_structure.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - Directory structure — WAFL documentation - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Directory structure

-

Consider the following directory structure in your project

-
.
-├── functions.py
-├── wafl.rules            # (1)
-│
-├── facts
-│       ├── functions.py
-│       └── wafl.rules
-├── greetings
-│       ├── wafl.rules    # (2)
-│       ├── functions.py
-│       └── facts
-│               ├── wafl.rules
-│               └── functions.py
-└── interruptions
-        ├── functions.py
-        └── wafl.rules
-
-
-

The rules can be organised into a nested directory structure. -Each directory must contain rules.wafl and functions.py. -The first one contains the rules as explained in the rules section.

-

The directories to be used for inference must be used within the wafl.rules (1) file according to the following syntax

-
#using facts
-#using greetings
-#using interruptions
-
-
-

The keyword #using <FOLDER_NAME> includes the specified folder in the inference tree. -Only the rules and facts that are included will be part of the inference. -For example, the keyword #using facts within greetings/ (2) will not include the folder above it. -Inference in a subfolder is limited the the rules and facts that are part of that folder or below it.

-

For more complete example, you can have a look at the (still early) project in -wafl_home.

-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/examples.html b/documentation/build/html/examples.html index bf67886c..c5e07594 100644 --- a/documentation/build/html/examples.html +++ b/documentation/build/html/examples.html @@ -17,8 +17,8 @@ - - + + @@ -44,13 +44,15 @@
  • Introduction
  • Installation
  • +
  • Initialization
  • +
  • Configuration
  • Running WAFL
  • -
  • Query processing pipeline
  • -
  • Rules
  • -
  • Examples
  • +
  • The rules.yaml file
  • Examples
  • License
  • @@ -84,8 +86,13 @@

    Examples

    @@ -94,8 +101,8 @@

    Examples - - + +
    diff --git a/documentation/build/html/genindex.html b/documentation/build/html/genindex.html index ffda0e73..c7713889 100644 --- a/documentation/build/html/genindex.html +++ b/documentation/build/html/genindex.html @@ -41,10 +41,10 @@ diff --git a/documentation/build/html/index.html b/documentation/build/html/index.html index 91e8034e..56d3b8f9 100644 --- a/documentation/build/html/index.html +++ b/documentation/build/html/index.html @@ -4,7 +4,7 @@ - Welcome to WAFL’s 0.0.45 documentation! — WAFL documentation + Welcome to WAFL’s 0.0.80 documentation! — WAFL documentation - - - - - - - - - - - - -
    - - -
    - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/documentation/build/html/rules_and_backtracking.html b/documentation/build/html/rules_and_backtracking.html deleted file mode 100644 index b4483b41..00000000 --- a/documentation/build/html/rules_and_backtracking.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - Rules and backtracking — WAFL documentation - - - - - - - - - - - - - - - -
    - - -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -

    Rules and backtracking

    -

    An input that satisfies a trigger condition will make the most relevant rule go through the list of actions within. -However, if one of the actions fails then the next most relevant rule will be activated, and so on until there are -no more relevant rules.

    -

    For example, if there are two rules

    -
    The user is feeling well
    -    the user's name is John
    -    SAY Happy you are feeling well, John
    -
    -The user is feeling ok
    -    the user's name is Jane
    -    SAY Happy you are feeling well, Jane
    -
    -
    -

    and the user says: “My name is Jane and I am feeling well”. -The first rule will be activated first, until the condition “the user’s name is John” returns False. -Then the second rule will be activated because “feeling ok” is very similar to “feeling well”. -The end result is that the bot will say “Happy you are feeling well, Jane”

    -

    The backtracking of rules applies recursively. -In the end, a rule returns True only if all its actions return True. -Otherwise, execution is truncated and the next relevant rule starts a new inference branch.

    -
    - - -
    -
    - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/documentation/build/html/running_WAFL.html b/documentation/build/html/running_WAFL.html index 9184caa7..30bd180c 100644 --- a/documentation/build/html/running_WAFL.html +++ b/documentation/build/html/running_WAFL.html @@ -17,8 +17,8 @@ - - + + @@ -44,6 +44,8 @@ @@ -112,8 +112,8 @@

    $ wafl run-tests - - + +
    diff --git a/documentation/build/html/search.html b/documentation/build/html/search.html index 4ddfc698..785dd13b 100644 --- a/documentation/build/html/search.html +++ b/documentation/build/html/search.html @@ -44,10 +44,10 @@ diff --git a/documentation/build/html/searchindex.js b/documentation/build/html/searchindex.js index 8039ff3d..e8b86b8d 100644 --- a/documentation/build/html/searchindex.js +++ b/documentation/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["automatic_rules_creation", "directory_structure", "examples", "index", "installation", "introduction", "license", "query_processing_pipeline", "rules", "rules_and_backtracking", "running_WAFL", "wafl_init", "writing_the_rules"], "filenames": ["automatic_rules_creation.rst", "directory_structure.rst", "examples.rst", "index.rst", "installation.rst", "introduction.rst", "license.rst", "query_processing_pipeline.rst", "rules.rst", "rules_and_backtracking.rst", "running_WAFL.rst", "wafl_init.rst", "writing_the_rules.rst"], "titles": ["Automatic rules creation", "Directory structure", "Examples", "Welcome to WAFL\u2019s 0.0.45 documentation!", "Installation", "Introduction", "License", "Query processing pipeline", "Rules", "Rules and backtracking", "Running WAFL", "Initialization", "Writing the rules"], "terms": {"consid": [1, 12], "follow": [1, 4, 5, 6, 7, 11, 12], "your": [1, 4, 5, 12], "project": 1, "function": [1, 11, 12], "py": [1, 11, 12], "wafl": [1, 4, 5, 7, 11, 12], "rule": [1, 3, 4, 5, 7, 10, 11], "1": [1, 4, 12], "fact": [1, 12], "greet": [1, 12], "2": [1, 12], "interrupt": 1, "The": [1, 4, 5, 6, 7, 9, 10, 11, 12], "can": [1, 4, 10, 11, 12], "organis": [1, 5], "nest": 1, "each": [1, 12], "must": 1, "contain": [1, 10, 11, 12], "first": [1, 4, 9, 12], "one": [1, 9, 11, 12], "explain": 1, "section": 1, "us": [1, 4, 5, 6, 10, 11, 12], "infer": [1, 5, 7, 9, 12], "within": [1, 9, 12], "file": [1, 4, 6, 10, 11, 12], "accord": [1, 7, 12], "syntax": [1, 12], "keyword": 1, "folder_nam": 1, "includ": [1, 6, 12], "specifi": [1, 5], "folder": 1, "tree": [1, 5], "onli": [1, 9, 12], "ar": [1, 7, 9, 10, 11, 12], "part": [1, 4], "For": [1, 9, 11, 12], "exampl": [1, 3, 4, 9, 10, 11, 12], "abov": [1, 6, 12], "subfold": 1, "i": [1, 4, 5, 6, 7, 9, 10, 11, 12], "limit": [1, 6], "below": [1, 11, 12], "more": [1, 9], "complet": 1, "you": [1, 4, 9, 10, 12], "have": [1, 4], "look": 1, "still": 1, "earli": 1, "wafl_hom": [1, 12], "run": [3, 4, 11], "init": [4, 11], "directori": [2, 3], "structur": [2, 3], "introduct": 3, "instal": 3, "interfac": [3, 11], "side": [3, 11, 12], "llm": [3, 5, 7, 11, 12], "need": 3, "gpu": 3, "audio": [3, 11], "server": [3, 4], "cli": 3, "test": [3, 11], "queri": 3, "process": 3, "pipelin": 3, "sai": [3, 8, 9, 11], "command": [3, 4, 8, 10, 11], "rememb": [3, 8], "retriev": [3, 8], "ask": [3, 8], "question": [3, 8], "gener": [3, 8, 11], "text": [3, 5, 8], "item": [3, 8], "wise": [3, 8], "execut": [3, 8, 9, 10, 11], "trigger": [3, 8, 9, 11], "anoth": [3, 8], "code": [3, 8], "entail": [3, 8], "backtrack": [3, 8], "licens": 3, "index": 3, "modul": 3, "search": 3, "page": 3, "In": [4, 9, 12], "thi": [4, 5, 6, 10, 11, 12], "version": [4, 5, 12], "built": 4, "two": [4, 9, 12], "system": [4, 10, 11, 12], "both": 4, "same": 4, "machin": [4, 11], "local": [4, 10, 11], "access": 4, "microphon": 4, "speaker": 4, "To": 4, "sudo": 4, "apt": 4, "get": 4, "portaudio19": 4, "dev": 4, "ffmpeg": 4, "pip": 4, "after": 4, "requir": [4, 5], "initi": [2, 3, 4], "which": [4, 10], "creat": [4, 5, 11, 12], "config": [4, 10, 11], "json": [4, 10, 11], "edit": 4, "chang": [4, 10], "default": [4, 10], "set": [4, 11, 12], "A": [4, 6, 12], "standard": 4, "also": 4, "pleas": 4, "see": 4, "chapter": 4, "second": [4, 9, 12], "from": [4, 6, 7, 12], "last": [], "larg": 5, "languag": [4, 5, 11], "model": [4, 5, 12], "conveni": [], "speed": [], "docker": [4, 11], "imag": [4, 11], "script": 11, "p8080": 4, "8080": 4, "env": 4, "nvidia_disable_requir": 4, "all": [4, 6, 9, 10, 12], "fractalego": [4, 6], "latest": 4, "ha": [4, 12], "fill": [4, 12], "ip": 4, "address": 4, "localhost": 4, "altern": 4, "clone": 4, "repositori": [4, 12], "framework": 5, "home": [], "assist": [], "It": [5, 10, 11], "design": [], "combin": 5, "predict": 5, "behavior": 5, "specif": 5, "instead": 5, "work": [5, 10, 11], "an": [5, 6, 9], "chain": 5, "thought": 5, "intend": 5, "its": [5, 9, 11], "progress": 5, "current": 5, "user": [4, 5, 7, 9, 10, 11, 12], "while": [5, 12], "readi": 5, "plai": [5, 11], "might": [5, 11], "product": 5, "depend": 5, "case": [4, 5, 11, 12], "softwar": 6, "under": 6, "mit": 6, "copyright": 6, "c": 6, "2023": 6, "alberto": 6, "cetoli": 6, "io": 6, "permiss": 6, "herebi": 6, "grant": 6, "free": 6, "charg": 6, "ani": [6, 12], "person": [5, 6], "obtain": 6, "copi": 6, "associ": 6, "document": [6, 10], "deal": 6, "without": [6, 12], "restrict": 6, "right": [6, 12], "modifi": 6, "merg": 6, "publish": 6, "distribut": 6, "sublicens": 6, "sell": 6, "permit": 6, "whom": 6, "furnish": 6, "do": [6, 12], "so": [6, 9], "subject": 6, "condit": [6, 9, 11, 12], "notic": [6, 12], "shall": 6, "substanti": 6, "portion": 6, "THE": 6, "provid": 6, "AS": 6, "warranti": 6, "OF": 6, "kind": 6, "express": 6, "OR": 6, "impli": 6, "BUT": 6, "NOT": 6, "TO": 6, "merchant": 6, "fit": 6, "FOR": 6, "particular": 6, "purpos": [6, 10], "AND": 6, "noninfring": 6, "IN": 6, "NO": 6, "event": [6, 11], "author": 6, "holder": 6, "BE": 6, "liabl": 6, "claim": 6, "damag": 6, "other": [6, 12], "liabil": 6, "whether": 6, "action": [4, 6, 9, 11, 12], "contract": 6, "tort": 6, "otherwis": [6, 9, 12], "aris": 6, "out": 6, "connect": 6, "WITH": 6, "try": 7, "answer": 7, "": [7, 9, 11, 12], "diagram": 7, "repli": [7, 12], "flow": 7, "bot": [7, 9, 11, 12], "divid": 7, "three": 7, "main": 7, "branch": [7, 9], "direct": 7, "thei": [7, 12], "written": [7, 11, 12], "creation": 7, "accomplish": [7, 11], "input": [9, 12], "satisfi": 9, "make": [9, 12], "most": 9, "relev": 9, "go": 9, "through": [9, 12], "list": [9, 11, 12], "howev": [4, 9, 12], "fail": 9, "next": [9, 12], "activ": [9, 10, 11, 12], "until": 9, "feel": 9, "well": 9, "name": [9, 10, 11, 12], "john": [9, 12], "happi": [9, 12], "ok": 9, "jane": 9, "my": [9, 12], "am": [9, 12], "return": [9, 10, 11, 12], "fals": [9, 12], "Then": 9, "becaus": 9, "veri": 9, "similar": [9, 12], "end": [9, 12], "result": [9, 10], "appli": 9, "recurs": 9, "true": [9, 11, 12], "truncat": 9, "start": [9, 11], "new": [9, 12], "few": 10, "how": [10, 12], "There": [10, 12], "four": 10, "mode": [10, 11], "loop": 10, "wait": 10, "speak": 10, "word": 10, "defin": [10, 12], "comput": [10, 11, 12], "whatev": 10, "want": [4, 10, 11, 12], "web": 10, "listen": 10, "http": 10, "request": 10, "port": 10, "8889": 10, "act": 10, "chatbot": [10, 11], "line": [10, 12], "doe": [10, 12], "webserv": 10, "testcas": [10, 11], "txt": [10, 11], "what": 12, "l": 11, "start_llm": 11, "sh": 11, "configur": [4, 11], "allow_interrupt": 11, "allow": 11, "convers": 11, "waking_up_word": 11, "wake": 11, "up": 11, "waking_up_sound": 11, "sound": [], "when": [11, 12], "woken": [], "deactivate_sound": 11, "listener_model": [], "openai": [], "whisper": [], "tini": [], "en": [], "speech": 5, "recognit": 5, "support": [], "listener_hotword_logp": [], "8": [], "threshold": [], "log": [], "probabl": [], "hotword": [], "listener_volume_threshold": [], "0": [], "6": [], "volum": [], "listener_silence_timeout": [], "7": [], "silenc": [], "timeout": [], "model_host": [], "127": [], "host": [], "model_port": [], "guid": 11, "top": 11, "bring": 11, "yourself": 11, "onlin": 11, "hello": [11, 12], "time": 11, "data": 11, "These": 11, "everi": 11, "minut": 11, "mai": 11, "simpli": 11, "format": 12, "3": 12, "indent": 12, "number": 12, "space": 12, "valu": 12, "If": 12, "stop": 12, "demo": 12, "found": 12, "encount": 12, "singl": 12, "actor": 12, "One": 12, "simpl": 12, "9": 12, "type": 12, "someth": 12, "append": 12, "like": 12, "user_qu": 12, "about": 12, "themselv": 12, "tall": 12, "turn": 12, "output": 12, "At": 12, "cycl": 12, "element": 12, "memori": 12, "select": 12, "embed": 12, "displai": 12, "would": 12, "item1": 12, "item2": 12, "item3": 12, "mark": 12, "variabl": 12, "later": 12, "ye": 12, "No": [4, 12], "truth": 12, "never": 12, "fashion": 12, "italian_nam": 12, "italian": 12, "differ": 12, "hand": 12, "statement": 12, "done": 12, "python": 12, "call": 12, "def": 12, "print": 12, "argument": 12, "f": 12, "avail": 12, "prior": 12, "date": 12, "todai": 12, "continu": 12, "fly": 12, "descript": 12, "bracket": 12, "stdout": 12, "oper": 12, "rh": 12, "lh": 12, "lsh": 12, "write": [3, 8], "deploy": 4, "instanc": 4, "multi": 4, "setup": 4, "benefit": 4, "dedic": 4, "agent": 5, "integr": 5, "initialis": 11, "environ": 11, "improvise_task": 11, "regul": 11, "highest": 11, "prioriti": 11, "middl": 11, "signal": 11, "back": 11, "idl": 11, "own": 11, "goal": 11}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"automat": 0, "rule": [0, 8, 9, 12], "creation": 0, "directori": 1, "structur": 1, "exampl": [2, 8], "welcom": 3, "wafl": [3, 10], "": 3, "0": 3, "45": 3, "document": 3, "content": 3, "indic": 3, "tabl": 3, "instal": 4, "interfac": 4, "side": 4, "llm": 4, "need": 4, "gpu": 4, "introduct": 5, "licens": 6, "queri": 7, "process": 7, "pipelin": 7, "backtrack": 9, "run": 10, "audio": 10, "server": 10, "cli": 10, "test": 10, "init": [], "sai": 12, "command": 12, "rememb": 12, "retriev": 12, "ask": 12, "question": 12, "gener": 12, "text": 12, "item": 12, "wise": 12, "execut": 12, "trigger": 12, "anoth": 12, "code": 12, "entail": 12, "initi": 11, "write": 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": {"Automatic rules creation": [[0, "automatic-rules-creation"]], "Directory structure": [[1, "directory-structure"]], "Examples": [[2, "examples"], [8, "examples"]], "Welcome to WAFL\u2019s 0.0.45 documentation!": [[3, "welcome-to-wafl-s-0-0-45-documentation"]], "Contents:": [[3, null]], "Indices and tables": [[3, "indices-and-tables"]], "License": [[6, "license"]], "Query processing pipeline": [[7, "query-processing-pipeline"]], "Rules": [[8, "rules"]], "Rules and backtracking": [[9, "rules-and-backtracking"]], "Running WAFL": [[10, "running-wafl"]], "$ wafl run-audio": [[10, "wafl-run-audio"]], "$ wafl run-server": [[10, "wafl-run-server"]], "$ wafl run-cli": [[10, "wafl-run-cli"]], "$ wafl run-tests": [[10, "wafl-run-tests"]], "Installation": [[4, "installation"]], "Interface side": [[4, "interface-side"]], "LLM side (needs a GPU)": [[4, "llm-side-needs-a-gpu"]], "Introduction": [[5, "introduction"]], "Initialization": [[11, "initialization"]], "Writing the rules": [[12, "writing-the-rules"]], "SAY command": [[12, "say-command"]], "REMEMBER command": [[12, "remember-command"]], "RETRIEVE command": [[12, "retrieve-command"]], "Asking a question": [[12, "asking-a-question"]], "Generate a text": [[12, "generate-a-text"]], "Item-wise execution": [[12, "item-wise-execution"]], "Triggering of another rule": [[12, "triggering-of-another-rule"]], "Code execution": [[12, "code-execution"]], "Entailment": [[12, "entailment"]]}, "indexentries": {}}) \ No newline at end of file +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 diff --git a/documentation/build/html/wafl_init.html b/documentation/build/html/wafl_init.html deleted file mode 100644 index d5223345..00000000 --- a/documentation/build/html/wafl_init.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - Initialization — WAFL documentation - - - - - - - - - - - - - - - -
    - - -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -

    Initialization

    -

    This command initialises WAFL’s work environment

    -
    $ wafl init
    -
    -
    -

    It creates a set of files that can be used to the interface side of WAFL.

    -
    $ ls
    -config.json
    -events.py
    -functions.py
    -rules.wafl
    -start_llm.sh
    -testcases.txt
    -
    -
    -
      -
    • The config.json file contains the configuration.

    • -
    -
    {
    -  "allow_interruptions": true,
    -  "waking_up_word": "computer",
    -  "waking_up_sound": true,
    -  "deactivate_sound": true,
    -  "improvise_tasks": true,
    -  ...
    -}
    -
    -
    -

    These settings regulate the following:

    -
    -
      -
    • The “allow_interruptions” allows the user to create rules with the highest priority. -For example, the user might want a rule to be triggered in the middle of a conversation.

    • -
    • “waking_up_word” is the name of the bot, used to wake up the system in the “run-audio” mode.

    • -
    • “waking_up_sound” and “deactivate_sound” are played to signal the system is up or is back to idle.

    • -
    • “improvise_tasks” allows the system to create its own rules to accomplish a goal.

    • -
    -
    -
      -
    • The rules.wafl file contains the rules that guide the chatbot. -The rules are written in the WAFL language, with a trigger condition on top and a list of actions below.

    • -
    -
    The user says "bring yourself online"
    -  SAY Hello there!
    -
    -
    -

    This rule is activated when the user says “bring yourself online”, and the action is for the machine to say “Hello there!”.

    -
      -
    • The functions.py file contains the functions that can be used in the rules.wafl file.

    • -
    • The events.py file contains the event generation functions. -For example, there is a function that returns the time and one that returns the data. -These functions are executed every minute and may be used to activate one of the rules.

    • -
    • start_llm.sh is a script that starts the LLM locally. -It simply starts a docker container with the LLM image.

    • -
    • The testcases.txt file contains the test cases that can be used to test the LLM.

    • -
    -
    - - -
    -
    - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/documentation/source/_static/arbiter.png b/documentation/source/_static/arbiter.png deleted file mode 100644 index eef82e7a..00000000 Binary files a/documentation/source/_static/arbiter.png and /dev/null differ diff --git a/documentation/source/automatic_rules_creation.rst b/documentation/source/automatic_rules_creation.rst deleted file mode 100644 index 4eb3d871..00000000 --- a/documentation/source/automatic_rules_creation.rst +++ /dev/null @@ -1,3 +0,0 @@ -Automatic rules creation -======================== - diff --git a/documentation/source/configuration.rst b/documentation/source/configuration.rst new file mode 100644 index 00000000..b8b0f9e2 --- /dev/null +++ b/documentation/source/configuration.rst @@ -0,0 +1,64 @@ +Configuration +-------------- + +The file `config.json` contains some parameters for the chatbot and the url to connect to for the backend. + +A typical configuration file looks like this: + +.. code-block:: text + + { + "waking_up_word": "computer", + "waking_up_sound": true, + "deactivate_sound": true, + "rules": "rules.yaml", + "functions": "functions.py", + "llm_model": { + "model_host": "localhost", + "model_port": 8080 + }, + "listener_model": { + "model_host": "localhost", + "model_port": 8080, + "listener_hotword_logp": -8, + "listener_volume_threshold": 0.6, + "listener_silence_timeout": 0.7 + }, + "speaker_model": { + "model_host": "localhost", + "model_port": 8080 + }, + "text_embedding_model": { + "model_host": "localhost", + "model_port": 8080 + } + } + + +These settings regulate the following: + + * "waking_up_word" is the name of the bot, used to wake up the system in the "run-audio" mode. + + * "waking_up_sound" and "deactivate_sound" are played to signal the system is up or is back to idle. + + * "rules" is the file containing the facts and rules that guide the chatbot. The default is "rules.yaml". + + * "functions" is the file containing the functions that can be used in the rules. The default is "functions.py". + + * "llm_model" is the configuration to connect to the LLM model in the backend. The default is "localhost:8080". + + * "listener_model" is the configuration to connect to the listener model in the backend. The default is "localhost:8080". + + - The listener model is used to detect the wake-up word. + The similarity threshold for the detection can be set with the "listener_hotword_logp" parameter. + + - The "listener_volume_threshold" parameter is used to set the volume threshold for any conversation. + Any word uttered with a volume below this threshold is ignored. + + - The "listener_silence_timeout" parameter is used to set the silence timeout for any conversation. + If no word is uttered for a time longer than this timeout, the conversation is considered finished. + + * "speaker_model" is the configuration to connect to the speaker model in the backend. The default is "localhost:8080". + + * "text_embedding_model" is the configuration to connect to the text embedding model in the backend. The default is "localhost:8080". + diff --git a/documentation/source/directory_structure.rst b/documentation/source/directory_structure.rst deleted file mode 100644 index 37167e75..00000000 --- a/documentation/source/directory_structure.rst +++ /dev/null @@ -1,43 +0,0 @@ -Directory structure -=================== - -Consider the following directory structure in your project - -.. code-block:: bash - - . - ├── functions.py - ├── wafl.rules # (1) - │ - ├── facts - │ ├── functions.py - │ └── wafl.rules - ├── greetings - │ ├── wafl.rules # (2) - │ ├── functions.py - │ └── facts - │ ├── wafl.rules - │ └── functions.py - └── interruptions - ├── functions.py - └── wafl.rules - -The rules can be organised into a nested directory structure. -Each directory must contain `rules.wafl` and `functions.py`. -The first one contains the rules as explained in `the rules section `_. - -The directories to be used for inference must be used within the `wafl.rules` (1) file according to the following syntax - -.. code-block:: text - - #using facts - #using greetings - #using interruptions - -The keyword `#using ` includes the specified folder in the inference tree. -Only the rules and facts that are included will be part of the inference. -For example, the keyword `#using facts` within greetings/ (2) will not include the folder above it. -Inference in a subfolder is limited the the rules and facts that are part of that folder or below it. - -For more complete example, you can have a look at the (still early) project in -`wafl_home `_. \ No newline at end of file diff --git a/documentation/source/examples.rst b/documentation/source/examples.rst index 802b35dc..0872bb1b 100644 --- a/documentation/source/examples.rst +++ b/documentation/source/examples.rst @@ -4,5 +4,7 @@ Examples .. toctree:: :maxdepth: 2 - wafl_init - directory_structure \ No newline at end of file + simple_rule + rule_with_examples + rules_with_execute_command + rules_with_remember_command \ No newline at end of file diff --git a/documentation/source/facts_and_rules.rst b/documentation/source/facts_and_rules.rst new file mode 100644 index 00000000..a3874e89 --- /dev/null +++ b/documentation/source/facts_and_rules.rst @@ -0,0 +1,35 @@ +The rules.yaml file +=================== + +The language model can be guided through a list of rules and facts that are retrieved during the conversation. + +This file is written in YAML format as in the following: + +.. code-block:: yaml + + facts: + - This bot is doing well + - This bot is called Computer + + rules: + - the user wants to know the time: + - output "The time is get_time()". + + +Facts +----- +These are simple facts that are retrieved during the conversation. +For examples, if the user asks "How are you?", the bot will retrieve "this bot is doing well" and add it to its prompt, +eventually generating an answer like "I am fine" or "I am doing well". + +Rules +----- + +Rules are composed of a condition and a list of actions. +The condition is a simple trigger that is matched against the user input. +In the example above, the condition is "the user wants to know the time". +The condition is matched against the user input, and if it matches, the whole rule is added to the prompt of the language model. + +The actions are a set of items that the bot will execute in order. +In the example above, the bot should output "The time is get_time()" if it thinks that the user wants to know the time. + diff --git a/documentation/source/index.rst b/documentation/source/index.rst index d2f4b8bd..431643d7 100644 --- a/documentation/source/index.rst +++ b/documentation/source/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to WAFL's 0.0.45 documentation! +Welcome to WAFL's 0.0.80 documentation! ======================================= .. toctree:: @@ -12,9 +12,10 @@ Welcome to WAFL's 0.0.45 documentation! introduction installation + initialization + configuration running_WAFL - query_processing_pipeline - rules + facts_and_rules examples license diff --git a/documentation/source/initialization.rst b/documentation/source/initialization.rst new file mode 100644 index 00000000..0876e4d7 --- /dev/null +++ b/documentation/source/initialization.rst @@ -0,0 +1,43 @@ +Initialization +-------------- + +This command initialises WAFL's work environment + +.. code-block:: bash + + $ wafl init + +It creates a set of files that can be used to the interface side of WAFL. + +.. code-block:: bash + + $ ls + config.json + db.json + functions.py + main.py + requirements.txt + rules.yaml + secrets.json + start_llm.sh + testcases.txt + +- the `config.json` file contains some parameters for the chatbot and the url to connect to for the backend. + +- the `db.json` file is an auxiliary file that contains some information about the chatbot's state. + It is a json file that can be edited manually. + +- The `functions.py` file contains the functions that can be used in the `rules.yaml` file. + +- `main.py` is an auxiliary script that can be used to start a webserver locally to test the chatbot. + +- The `requirements.txt` file contains the python packages needed to run the functions in `functions.py`. + +- The `rules.yaml` file contains the facts and rules used to guide the conversation with the chatbot. + +- The `secrets.json` may contain credentials that are needed to run the the functions in `functions.py`. + +- `start_llm.sh` is a script that starts the LLM locally. + It simply starts a docker container with the LLM image. + +- The `testcases.txt` file contains the test cases that can be used to test the LLM. diff --git a/documentation/source/introduction.rst b/documentation/source/introduction.rst index a1a6c0e9..7450893e 100644 --- a/documentation/source/introduction.rst +++ b/documentation/source/introduction.rst @@ -5,8 +5,7 @@ WAFL is a framework for personal agents. It integrates Large language models, speech recognition and text to speech. This framework combines Large Language Models and rules to create a predictable behavior. -Specifically, instead of organising the work of an LLM into a chain of thoughts, -WAFL intends to organise its behavior into inference trees. +A set of rules is used to define the behavior of the agent, supporting function calling and a working memory. WAFL is a work in progress. The current version requires the user to specify the rules to follow. diff --git a/documentation/source/license.rst b/documentation/source/license.rst index 572eafe6..9b6910aa 100644 --- a/documentation/source/license.rst +++ b/documentation/source/license.rst @@ -3,7 +3,7 @@ License This software is licensed under the MIT License: -Copyright (c) 2023 alberto.cetoli@fractalego.io +Copyright (c) 2024 alberto.cetoli@fractalego.io 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: diff --git a/documentation/source/query_processing_pipeline.rst b/documentation/source/query_processing_pipeline.rst deleted file mode 100644 index 3497680e..00000000 --- a/documentation/source/query_processing_pipeline.rst +++ /dev/null @@ -1,16 +0,0 @@ -Query processing pipeline -========================= - -WAFL will try to answer the user's query as in the following diagram - -.. image:: _static/arbiter.png - :alt: WAFL pipeline - :align: center - - -The reply flow from the bot is divided into three main branches: - -* Direct reply from the LLM -* Inference according to the rules as they are written -* Rule creation if there are no rules to accomplish the user's query - diff --git a/documentation/source/rule_with_examples.rst b/documentation/source/rule_with_examples.rst new file mode 100644 index 00000000..e43c6f95 --- /dev/null +++ b/documentation/source/rule_with_examples.rst @@ -0,0 +1,24 @@ +Rule with examples +================== + +Some examples can be added to the rules to make the required output clearer to the bot. +Effectively, each rule is list of suggestions that go into the prompt for the language model. + +Consider the following rule: + +.. code-block:: yaml + + - the user wants to compute some math operation in Python: + - Think of the python code that solves the math problem and assigns the result to the variable "result" + - For example "what is the result of 2 + 2?" should output "result = 2 + 2" + - Another example "what is the square root of 2?" should output "import math;result = math.sqrt(2)" + - output exactly the following "PYTHON CODE THAT SOLVES THE PROBLEM" + +When requested to compute the square of pi, the rule above will be used to generate the following code: + +.. code-block:: python + + import math;result = math.pi**2 + + +Notice that the format of the output is the same as the one specified in the rule. diff --git a/documentation/source/rules.rst b/documentation/source/rules.rst deleted file mode 100644 index 86b1dba4..00000000 --- a/documentation/source/rules.rst +++ /dev/null @@ -1,11 +0,0 @@ -Rules -===== - -Examples -======== - -.. toctree:: - :maxdepth: 2 - - writing_the_rules - rules_and_backtracking \ No newline at end of file diff --git a/documentation/source/rules_and_backtracking.rst b/documentation/source/rules_and_backtracking.rst deleted file mode 100644 index ee79e6e7..00000000 --- a/documentation/source/rules_and_backtracking.rst +++ /dev/null @@ -1,28 +0,0 @@ -Rules and backtracking -====================== - -An input that satisfies a trigger condition will make the most relevant rule go through the list of actions within. -However, if one of the actions fails then the next most relevant rule will be activated, and so on until there are -no more relevant rules. - -For example, if there are two rules - -.. code-block:: text - - The user is feeling well - the user's name is John - SAY Happy you are feeling well, John - - The user is feeling ok - the user's name is Jane - SAY Happy you are feeling well, Jane - -and the user says: "My name is Jane and I am feeling well". -The first rule will be activated first, until the condition "the user's name is John" returns False. -Then the second rule will be activated because "feeling ok" is very similar to "feeling well". -The end result is that the bot will say "Happy you are feeling well, Jane" - -The backtracking of rules applies recursively. -In the end, a rule returns True only if all its actions return True. -Otherwise, execution is truncated and the next relevant rule starts a new inference branch. - diff --git a/documentation/source/rules_with_execute_command.rst b/documentation/source/rules_with_execute_command.rst new file mode 100644 index 00000000..484438fe --- /dev/null +++ b/documentation/source/rules_with_execute_command.rst @@ -0,0 +1,52 @@ +Rule with execute command +========================= + +There are two special tags that can be used in the rules: and . +The tag is used to execute a python command on the host machine. + +Some examples can be added to the rules to make the required output clearer to the bot. +Effectively, each rule is list of suggestions that go into the prompt for the language model. + +Consider the following rule: + +.. code-block:: yaml + + - the user wants to compute some math operation: + - Think of the python code that solves the math problem and assigns the result to the variable "result" + - For example "what is the result of 2 + 2?" should output "result = 2 + 2" + - Another example "what is the square root of 2?" should output "import math;result = math.sqrt(2)" + - output exactly the following "result = PYTHON CODE THAT SOLVES THE PROBLEM" + + +When requested to compute the square of pi, the rule above will be used to generate the following text: + +.. code-block:: python + + import math;result = math.pi**2 + +However the user will not see this text. +Everything that is between the and tags will be executed as python code and substituted with the value of the variable "result". + + +Local functions +--------------- + +A list of Python functions can be specified within the file "functions.py". +These functions can be used in the rules to generate the desired output. + +For example, the following rule will output the current date: + +.. code-block:: yaml + + - the user wants to know today's date: + - output "The date is get_date()". + + +As long as the function "get_date" is defined in the file "functions.py". + +.. code-block:: python + + def get_date(): + return datetime.datetime.now().strftime("%Y-%m-%d") + +In this case the bot will substitute the tag get_date() with the string output of the function "get_date()". \ No newline at end of file diff --git a/documentation/source/rules_with_remember_command.rst b/documentation/source/rules_with_remember_command.rst new file mode 100644 index 00000000..f3fc53fa --- /dev/null +++ b/documentation/source/rules_with_remember_command.rst @@ -0,0 +1,21 @@ +Rule with remember command +========================== + +There are two special tags that can be used in the rules: and . +The tag is used to remember an intermediate result that can be used to generate the final output. + +Consider the following rule: + +.. code-block:: yaml + + - the user wants to summarise a website: + - you'll need the website url to summarise + - output exactly " The website content is get_website('WEBSITE_URL') ". + - summarise the website content given what you remember + - output the summary + + +When requested to summarise a website, the rule will check if the website url is provided. +Then it will execute the python code in the tag and remember the result. +The result is added to the language model's prompt through the tag. +Finally, it will summarise the website content - inserted to the prompt in the prior step - and output the summary. diff --git a/documentation/source/simple_rule.rst b/documentation/source/simple_rule.rst new file mode 100644 index 00000000..cc6f855c --- /dev/null +++ b/documentation/source/simple_rule.rst @@ -0,0 +1,23 @@ +Simple rule +=========== + +This is a simple rule that shows how the rule engine works. + +.. code-block:: yaml + + - the user says "hello": + - the bot reply "Howdy" + +The rule engine will match the user input with the rule and execute the actions. +In this case the user input must be similar to "hello" and the bot will reply "Howdy" to that. + +A rule can have multiple actions, for example: + +.. code-block:: yaml + + - the user says "hello": + - the bot reply "Howdy" + - the bot reply "How are you?" + +The rule engine will execute all the actions in order. + diff --git a/documentation/source/wafl_init.rst b/documentation/source/wafl_init.rst deleted file mode 100644 index 070713d9..00000000 --- a/documentation/source/wafl_init.rst +++ /dev/null @@ -1,67 +0,0 @@ -Initialization --------------- - -This command initialises WAFL's work environment - -.. code-block:: bash - - $ wafl init - -It creates a set of files that can be used to the interface side of WAFL. - -.. code-block:: bash - - $ ls - config.json - events.py - functions.py - rules.wafl - start_llm.sh - testcases.txt - -- The `config.json` file contains the configuration. - -.. code-block:: text - - { - "allow_interruptions": true, - "waking_up_word": "computer", - "waking_up_sound": true, - "deactivate_sound": true, - "improvise_tasks": true, - ... - } - -These settings regulate the following: - - * The "allow_interruptions" allows the user to create rules with the highest priority. - For example, the user might want a rule to be triggered in the middle of a conversation. - - * "waking_up_word" is the name of the bot, used to wake up the system in the "run-audio" mode. - - * "waking_up_sound" and "deactivate_sound" are played to signal the system is up or is back to idle. - - * "improvise_tasks" allows the system to create its own rules to accomplish a goal. - - -- The rules.wafl file contains the rules that guide the chatbot. - The rules are written in the WAFL language, with a trigger condition on top and a list of actions below. - -.. code-block:: text - - The user says "bring yourself online" - SAY Hello there! - -This rule is activated when the user says "bring yourself online", and the action is for the machine to say "Hello there!". - - -- The `functions.py` file contains the functions that can be used in the `rules.wafl` file. - -- The `events.py` file contains the event generation functions. - For example, there is a function that returns the time and one that returns the data. - These functions are executed every minute and may be used to activate one of the rules. - -- `start_llm.sh` is a script that starts the LLM locally. - It simply starts a docker container with the LLM image. - -- The `testcases.txt` file contains the test cases that can be used to test the LLM. diff --git a/documentation/source/writing_the_rules.rst b/documentation/source/writing_the_rules.rst deleted file mode 100644 index a7ee7ed4..00000000 --- a/documentation/source/writing_the_rules.rst +++ /dev/null @@ -1,261 +0,0 @@ -Writing the rules -================= - -The file rules.wafl contains the rules used by the system. -Each rule is in the following format - -.. code-block:: text - - Trigger condition - action 1 - action 2 - action 3 - ... - -Notice that the trigger condition has no indentation, while the actions are indented by any number spaces. -Each action returns a true or false value. -If that value is false, the rule stops executing and the next rule is triggered. -A demo of the rules can be found in the repository `wafl_home `_. - -A rule ends when the next rule is encountered (a new trigger condition is found). -The trigger condition can be a single fact. For example - -.. code-block:: text - - This bot's name is "computer" - -There are two actors in the system: "the user" and "the bot". -One simple rule example can be - -.. code-block:: text - - The user asks what is this bot's name - SAY Hello, my name is Computer - -The rule above will be triggered when the user asks what is this bot's name. -There are 9 types of actions: -**SAY**, -**REMEMBER**, -**RETRIEVE**, -**asking a question**, -**generate a text**, -**item-wise execution**, -**triggering of another rule**, -**code execution**, -**entailment**. - - -SAY command ------------ - -This command has two uses: - -1) This command will make the bot say something. For example the rule above will make the bot say "Hello, my name is computer". - -2) This command will append to the return value of a rule. For example consider the following set - -.. code-block:: text - - "how is the user like" - user_qualities = the user asks about themselves - SAY {user_qualities} - - The user asks about themselves - SAY The user is tall - - -When the user asks "how am I like", the first rule will be triggered. -In turn, the first rule will trigger the second one. -The output of the second rule will fill the value for user_qualities. - -At the end of the inference cycle the bot will reply - -.. code-block:: text - - bot: The user is tall - -REMEMBER command ----------------- - -This command will make the bot remember something. -for example the rule below will make the bot remember the user's name. - -.. code-block:: text - - The user says their name is John - REMEMBER The user's name is John - -RETRIEVE command ----------------- - -This command will retrieve a list of elements from memory. -The retrieved items are selected through embedding similarity -for example the rule below will retrieve a list of elements and display it. - -.. code-block:: text - - "how is the user like" - user_qualities = RETRIEVE how is the user like - SAY {user_qualities} - - -The output would be the list of all the retrieved items - -.. code-block:: text - - bot: [item1, item2, item3, ...] - -Asking a question ------------------ - -Typing a question (with or without question mark) will return a variable. -This variable can be used later in the rule -For example the rule below will make the bot ask the user's name. - -.. code-block:: text - - The user says their name - name = what is the user's name? - REMEMBER The user's name is {name} - -Yes/No questions return a truth condition. -For example by using the rule below, the bot will ask the user if they want to remember their name. - -.. code-block:: text - - The user says their name - name = what is the user's name? - Do you want to remember the user's name? - REMEMBER The user's name is {name} - -If the user says "no", the rule will stop executing and the REMEMBER command will never be used - - -Generate a text ----------------- - -A text can be generated in a similar fashion as when asking questions - -.. code-block:: text - - The user says their name - name = what is the user's name? - italian_name = the italian version of {name} is - SAY The italian version of {name} is {italian_name} - -The text will be generated by the line "the italian version of {name}" according to the LLM model. -The only difference with asking question is that the text on the right hand side of `=` is a statement -and not a question. - - -Item-wise execution -------------------- - -A list of items in a `variable` can be executed item-wise through the command `{[variable]}` - -.. code-block:: text - - "how is the user like" - user_qualities = RETRIEVE how is the user like - SAY {[user_qualities]} - - -The output would be the retrieved items line by line - -.. code-block:: text - - bot: item1 - bot: item2 - bot: item3 - bot: ... - - -Triggering of another rule --------------------------- - -A rule can trigger another rule as follows - -.. code-block:: text - - The user says their name - name = what is the user's name? - the name if the user is {name} - - The name of the user is John - SAY Hello John! - -In this case the second rule is triggered if the user says their name is John. - -Code execution --------------- - -The code execution is done by using the python syntax. -A function defined in the file `functions.py` can be called from the rule. - - -For example, the file `rules.wafl` contains the following rule - -.. code-block:: text - - The user says their name - name = what is the user's name? - greet({name}) - - -and the file `functions.py` contains the following function - -.. code-block:: python - - def greet(name): - print("Hello", name) - -When the user says their name, the bot will greet the user by calling the function greet with the user's name as argument. -However print() does not activate the SAY command. -From the `functions.py` file, a rule can be triggered by using the syntax `"% ... %"` - -.. code-block:: python - - def greet(name): - "% SAY Hello %" - f"% SAY your name is {name} %" - -The first line will make the bot say "Hello". The second line will make the bot say "your name is John" if the user's name is John. - -The syntax `"% ... %"`, can be used to trigger a rule, to generate a text, to ask a question, to remember something, or any other action available in the rules file. -For example the prior function can be written as follows - -.. code-block:: python - - def greet(name): - "% SAY Hello %" - "% SAY your name is {name} %" - date = "% what is the date today? %" - "% SAY today is {date} %" - while "% Do you want to continue? %": - "% SAY I am happy to continue %" - - -*Creating functions on-the-fly* - -The system can create a function from a description by including the description within the <...> brackets - -.. code-block:: text - - The user says their name - name = what is the user's name? - greet({name}) < print on STDOUT the value of the input argument > - - -Entailment ----------- - -The entailment is done by using the :- operator. if RHS entails LHS, then LSH :- RHS is true, otherwise it is false. -For example the rule below will stop at the second line if the user's name is not John. - -.. code-block:: text - - The user says their name - name = what is the user's name? - The user's name is John :- The user's name is {name} - SAY Your name is John! - diff --git a/requirements.txt b/requirements.txt index 060ef80b..da1a90d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,6 @@ flask[async]==2.0.1 flask-cors==3.0.10 flask_dropzone==1.6.0 -accelerate==0.13.2 -bitsandbytes==0.35.3 -torch==2.0.0 -optimum==1.8.5 -transformers==4.28.1 nltk==3.6.2 gensim==4.3.1 sklearn==0.0 @@ -21,6 +16,5 @@ sphinx==6.1.3 sphinx-rtd-theme==1.2.0 bluepy==1.3.0 einops==0.6.1 -sentence_transformers==2.2.2 -fairseq==0.12.2 -g2p-en==2.1.0 \ No newline at end of file +g2p-en==2.1.0 +pyyaml==6.0.1 \ No newline at end of file diff --git a/setup.py b/setup.py index a56fe68e..e4b1e407 100644 --- a/setup.py +++ b/setup.py @@ -19,38 +19,37 @@ "wafl.connectors", "wafl.connectors.bridges", "wafl.connectors.factories", - "wafl.connectors.local", "wafl.connectors.remote", "wafl.events", "wafl.extractors", + "wafl.filter", "wafl.inference", "wafl.interface", "wafl.knowledge", "wafl.listener", "wafl.logger", "wafl.parsing", - "wafl.policy", "wafl.retriever", "wafl.runners", - "wafl.speaker", "wafl.scheduler", "wafl.simple_text_processing", + "wafl.speaker", ], package_data={ - "wafl": ["templates/*", "templates/**/*", "sounds/*", "models/*", "frontend/*", "data/*.csv"], + "wafl": [ + "templates/*", + "sounds/*", + "models/*", + "frontend/*", + ], }, install_requires=[ "flask[async]==2.0.1", "flask-cors==3.0.10", "flask_dropzone==1.6.0", "werkzeug==2.1.2", - "accelerate==0.13.2", - "bitsandbytes==0.35.3", - "torch==2.0.0", - "optimum==1.8.5", - "transformers==4.28.1", "nltk==3.6.2", - "gensim==4.0.1", + "gensim==4.3.1", "sklearn==0.0", "python-Levenshtein==0.12.2", "wave==0.0.2", @@ -61,9 +60,8 @@ "word2number==1.1", "aiohttp==3.8.4", "einops==0.6.1", - "sentence_transformers==2.2.2", - "fairseq==0.12.2", "g2p-en==2.1.0", + "pyyaml==6.0.1", ], classifiers=[ "License :: OSI Approved :: MIT License", diff --git a/tests/backward_import/functions.py b/tests/backward_import/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/backward_import/rules.wafl b/tests/backward_import/rules.wafl deleted file mode 100644 index 9e689d5b..00000000 --- a/tests/backward_import/rules.wafl +++ /dev/null @@ -1 +0,0 @@ -the color of the sky is green \ No newline at end of file diff --git a/tests/config.json b/tests/config.json index 37358326..2dfa3b8c 100644 --- a/tests/config.json +++ b/tests/config.json @@ -3,10 +3,12 @@ "waking_up_word": "computer", "waking_up_sound": true, "deactivate_sound": true, - "improvise_tasks": true, + "improvise_tasks": false, + "rules": "rules.yaml", + "functions": "functions.py", "llm_model": { "model_is_local": false, - "local_model": "mosaicml/mpt-7b-instruct", + "local_model": "mistralai/Mistral-7B-Instruct-v0.1", "remote_model": { "model_host": "localhost", "model_port": 8080 @@ -14,7 +16,7 @@ }, "listener_model": { "model_is_local": false, - "local_model": "fractalego/personal-whisper-medium.en-model", + "local_model": "fractalego/personal-whisper-distilled-model", "remote_model": { "model_host": "localhost", "model_port": 8080 @@ -33,23 +35,11 @@ }, "entailment_model": { "model_is_local": false, - "local_model": "MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "local_model": "MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli" }, "text_embedding_model": { "model_is_local": false, - "local_model": "msmarco-distilbert-base-v3", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } - }, - "qa_embedding_model": { - "model_is_local": false, - "local_model": "multi-qa-distilbert-dot-v1", + "local_model": "TaylorAI/gte-tiny", "remote_model": { "model_host": "localhost", "model_port": 8080 diff --git a/tests/events.py b/tests/events.py deleted file mode 100644 index 9fc743ae..00000000 --- a/tests/events.py +++ /dev/null @@ -1,28 +0,0 @@ -from datetime import datetime - - -def return_five_past_seven(): - return "the time is 7,05" - - -def get_date(): - now = datetime.now() - return "Today's date is " + now.strftime("%A %d %B %Y") - - -def get_clock(): - now = datetime.now() - minutes = int(now.strftime("%M")) - hour = int(now.strftime("%H")) - return f"The time is {hour}, {minutes} " - - -def get_time_in_natural_language(): - now = datetime.now() - minutes = int(now.strftime("%M")) - hour = int(now.strftime("%H")) - if minutes <= 30: - return f"The time is {minutes} past {hour}" - - else: - return f"The time is {60 - minutes} to {hour + 1}" diff --git a/tests/functions.py b/tests/functions.py index f175736f..596cda0c 100644 --- a/tests/functions.py +++ b/tests/functions.py @@ -4,7 +4,6 @@ from fuzzywuzzy import process from word2number import w2n from wafl.exceptions import CloseConversation, InterruptTask -from preprocess_test_functions import b, c _logger = logging.getLogger(__file__) diff --git a/tests/greetings/facts/functions.py b/tests/greetings/facts/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/greetings/facts/rules.wafl b/tests/greetings/facts/rules.wafl deleted file mode 100644 index fee95d68..00000000 --- a/tests/greetings/facts/rules.wafl +++ /dev/null @@ -1 +0,0 @@ -the sun is shiny \ No newline at end of file diff --git a/tests/greetings/functions.py b/tests/greetings/functions.py deleted file mode 100644 index 22bb9608..00000000 --- a/tests/greetings/functions.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_time(): - return "NOW" diff --git a/tests/greetings/rules.wafl b/tests/greetings/rules.wafl deleted file mode 100644 index 103f53b1..00000000 --- a/tests/greetings/rules.wafl +++ /dev/null @@ -1,20 +0,0 @@ -#using facts -#using rules -#using ../backward_import - -this bot is doing well - -person = who does the user want to greet back? - SAY Hello, {person}! - -the user wants to know the time - time = get_time() - SAY the time is {time}! - - -the user is italian - something needs to be said in italian - -the user's name has five letters - result = what is the color of the sky? - say The sky is {result} \ No newline at end of file diff --git a/tests/greetings/rules/functions.py b/tests/greetings/rules/functions.py deleted file mode 100644 index 7b114ffd..00000000 --- a/tests/greetings/rules/functions.py +++ /dev/null @@ -1,2 +0,0 @@ -def say_something_in_italian(): - "% SAY Ciao! %" diff --git a/tests/greetings/rules/rules.wafl b/tests/greetings/rules/rules.wafl deleted file mode 100644 index 1a13f4be..00000000 --- a/tests/greetings/rules/rules.wafl +++ /dev/null @@ -1,2 +0,0 @@ -something needs to be said in italian - say_something_in_italian() \ No newline at end of file diff --git a/tests/local_config.json b/tests/local_config.json index d7548499..33b9212a 100644 --- a/tests/local_config.json +++ b/tests/local_config.json @@ -3,10 +3,12 @@ "waking_up_word": "computer", "waking_up_sound": true, "deactivate_sound": true, - "improvise_tasks": true, + "improvise_tasks": false, + "rules": "rules.yaml", + "functions": "functions.py", "llm_model": { "model_is_local": true, - "local_model": "mosaicml/mpt-7b-instruct", + "local_model": "mistralai/Mistral-7B-Instruct-v0.1", "remote_model": { "model_host": "localhost", "model_port": 8080 @@ -14,7 +16,7 @@ }, "listener_model": { "model_is_local": true, - "local_model": "fractalego/personal-whisper-medium.en-model", + "local_model": "fractalego/personal-whisper-distilled-model", "remote_model": { "model_host": "localhost", "model_port": 8080 @@ -33,23 +35,11 @@ }, "entailment_model": { "model_is_local": true, - "local_model": "MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "local_model": "MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli" }, "text_embedding_model": { "model_is_local": true, - "local_model": "msmarco-distilbert-base-v3", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } - }, - "qa_embedding_model": { - "model_is_local": true, - "local_model": "multi-qa-distilbert-dot-v1", + "local_model": "TaylorAI/gte-tiny", "remote_model": { "model_host": "localhost", "model_port": 8080 diff --git a/tests/preprocess_test_functions.py b/tests/preprocess_test_functions.py deleted file mode 100644 index 5e496917..00000000 --- a/tests/preprocess_test_functions.py +++ /dev/null @@ -1,10 +0,0 @@ -def a(): - pass - - -def b(): - "% SAY Hello %" - - -def c(): - b() diff --git a/tests/rules.wafl b/tests/rules.wafl deleted file mode 100644 index fba9b3ff..00000000 --- a/tests/rules.wafl +++ /dev/null @@ -1,4 +0,0 @@ -#using greetings - -The user greets - keyboard_interrupt() \ No newline at end of file diff --git a/tests/rules.yaml b/tests/rules.yaml new file mode 100644 index 00000000..e4c45e85 --- /dev/null +++ b/tests/rules.yaml @@ -0,0 +1,49 @@ +facts: + - This bot is doing well + - This bot is called Computer + - The user lives in Eltham, London + +rules: + - the user wants to compute some math operation: + - Think of the python code that solves the math problem and assigns the result to the variable "result" + - For example "what is the result of 2 + 2?" should output "result = 2 + 2" + - Another example "what is the square root of 2?" should output "import math;result = math.sqrt(2)" + - output exactly the following "result = PYTHON CODE THAT SOLVES THE PROBLEM" + + - the user wants to know the time: + - output "The time is get_time()". + + - the user wants to know today's date: + - output "The date is get_date()". + + - the user wants to know today's day of the week: + - output "The day of the week is get_day()". + + - the user wants to know the weather today: + - output "check_today_weather()". + + - the user wants to know the weather tomorrow: + - output "check_tomorrow_weather()". + + - the user wants to summarise a website: + - you'll need the website url to summarise + - output exactly " The website content is get_website('WEBSITE_URL') ". + - summarise the website content given what you remember + - output the summary + + - the user wants to know what is in the shopping list: + - output "get_shopping_list()". + + - the user wants to add something to the shopping list: + - The task here is to add the item to the shopping list using a python function + - example "add milk to the shopping list" should output "add_to_shopping_list(['milk'])" + - output "add_to_shopping_list(ITEMS_TO_ADD)". + + - the user wants to remove something to the shopping list: + - The task here is to remove the item from the shopping list using a python function + - example "remove milk from the shopping list" should output "remove_from_shopping_list(['milk'])" + - output "remove_from_shopping_list(ITEMS_TO_REMOVE)". + + - the user asks something about cities, capitals, countries, buildings, famous people, bars, restaurants, rivers, mountains, lakes, seas, oceans, planets, stars, galaxies: + - say that you are just improvising the answer + - say what you think answer the question \ No newline at end of file diff --git a/tests/test__explaination_happens_when_answer_is_false.py b/tests/test__explaination_happens_when_answer_is_false.py deleted file mode 100644 index d3daef58..00000000 --- a/tests/test__explaination_happens_when_answer_is_false.py +++ /dev/null @@ -1,48 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.exceptions import CloseConversation -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger - -_logger = LocalFileLogger() - -_wafl_greetings = """ -This bot is here to answer the user - -INTERRUPTION the user wants to know the time - time = get_time() - SAY the time is {time} - -INTERRUPTION the user says to shut up - close_conversation() - -INTERRUPTION the user wants to quit the task - close_task() - -""".strip() - - -class TestInterruptions(TestCase): - def test_time_shut_up_does_not_interrupt_if_it_contraddicts_facts(self): - interface = DummyInterface(["Hello", "shut up"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), - interface=interface, - code_path="/", - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - return False - - assert True diff --git a/tests/test_arbiter_answerer.py b/tests/test_arbiter_answerer.py deleted file mode 100644 index 15ad7472..00000000 --- a/tests/test_arbiter_answerer.py +++ /dev/null @@ -1,96 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.answerer.arbiter_answerer import ArbiterAnswerer -from wafl.config import Configuration -from wafl.events.answerer_creator import create_answerer -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.policy.answerer_policy import AnswerPolicy - -_path = os.path.dirname(__file__) - -_wafl_rules = """ - -This bot name is Computer -This bot is doing well - -""".strip() - - -class TestArbiterAnswerer(TestCase): - def test_generated_answer_from_conversation(self): - interface = DummyInterface(["what the color of the sky?"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_rules), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - expected = "blue" - print(interface.get_utterances_list()) - self.assertIn(expected.lower(), interface.get_utterances_list()[-1].lower()) - - def test_generated_answer_from_conversation2(self): - interface = DummyInterface(["what is the capital of Italy?"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_rules), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - expected = "rome" - self.assertIn(expected.lower(), interface.get_utterances_list()[-1].lower()) - - def test_generated_answer_from_conversation3(self): - interface = DummyInterface( - ["what is the capital of Italy .", "how tall is Micheal Jordan"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_rules), - interface=interface, - ) - asyncio.run(interface.output("Please say computer to activate me.")) - asyncio.run(interface.output("What can I do for you?")) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "6" - self.assertIn(expected, interface.get_utterances_list()[-1]) - - def test_fact_answer(self): - interface = DummyInterface(["What is the name of this bot"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_rules), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - expected = "computer" - print(interface.get_utterances_list()) - self.assertIn(expected.lower(), interface.get_utterances_list()[-1].lower()) - - def test_chitchat(self): - interface = DummyInterface(["good good"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_rules), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - expected = "good good" - self.assertIn(expected.lower(), interface.get_utterances_list()[-1].lower()) - - def test__conversation_input_returns_chitchat_for_trivial_input(self): - interface = DummyInterface(["uhm what"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, ""), interface=interface - ) - asyncio.run(interface.output("say hello")) - asyncio.run(conversation_events.process_next()) - expected = "hello" - self.assertIn(expected, interface.get_utterances_list()[-1]) diff --git a/tests/test_closing_conversation.py b/tests/test_closing_conversation.py index e2f0e9f9..2e9fef85 100644 --- a/tests/test_closing_conversation.py +++ b/tests/test_closing_conversation.py @@ -6,82 +6,27 @@ from wafl.events.conversation_events import ConversationEvents from wafl.exceptions import CloseConversation from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge wafl_example = """ -_close - close_conversation() - -INTERRUPTION the user says good bye - SAY good bye! - _close - -INTERRUPTION the user says shut up - SAY ok - _close - -INTERRUPTION the user asks this bot to be silent - _close - -INTERRUPTION the user says: "thank you" - _close +rules: + - the user thanks the bot: + - The intention of the user is to close the conversation + - You must answer the user by writing "close_conversation()" """ class TestInterruptionsToCloseConversation(TestCase): - def test__good_bye_closes_conversation(self): - interface = DummyInterface( - to_utter=[ - "Goodbye.", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - self.assertTrue(True) - return - - self.assertTrue(False) - def test__thank_you_closes_conversation(self): interface = DummyInterface( to_utter=[ - "Thank you", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - self.assertTrue(True) - return - - self.assertTrue(False) - - def test__thanks_closes_conversation(self): - interface = DummyInterface( - to_utter=[ - "Thanks.", + "thank you", ] ) config = Configuration.load_local_config() + config.set_value("rules", wafl_example) conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), + config=config, interface=interface, - code_path="/", ) try: asyncio.run(conversation_events.process_next()) diff --git a/tests/test_commonsense_entailment.py b/tests/test_commonsense_entailment.py deleted file mode 100644 index fb886029..00000000 --- a/tests/test_commonsense_entailment.py +++ /dev/null @@ -1,71 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface - -wafl_example = """ - -item = what does the user want to add to the shopping list? - The user adds {item} to a list :- the user adds something to a grocery list - SAY {item} will be added - -item = what does the user want to add to the shopping list? - ! The user adds {item} to a list :- the user adds something to a grocery list - SAY {item} is not a shopping item - -""" - - -class TestCommonSense(TestCase): - def test__sentences_can_filter_items_positive(self): - interface = DummyInterface( - to_utter=[ - "Please add apples to the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Apples will be added" - assert interface.get_utterances_list()[-1] == expected - - def test__sentences_can_filter_items_positive2(self): - interface = DummyInterface( - to_utter=[ - "Please add bananas to the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Bananas will be added" - assert interface.get_utterances_list()[-1] == expected - - def test__sentences_can_filter_items_negative(self): - interface = DummyInterface( - to_utter=[ - "Please add skyscrapers to the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Skyscrapers is not a shopping item" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_config.py b/tests/test_config.py index 87308b2b..b559117e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,7 +13,7 @@ def test__listener_accepts_threshold_for_hotword_logp(self): interface = VoiceInterface(config) self.assertEqual( interface._listener._hotword_threshold, - config.get_value("listener_hotword_logp"), + config.get_value("listener_model")["listener_hotword_logp"], ) def test__listener_accepts_threshold_for_volume(self): @@ -21,12 +21,13 @@ def test__listener_accepts_threshold_for_volume(self): interface = VoiceInterface(config) self.assertEqual( interface._listener._volume_threshold, - config.get_value("listener_volume_threshold"), + config.get_value("listener_model")["listener_volume_threshold"], ) def test__listener_accepts_silence_timeout(self): config = Configuration.load_local_config() interface = VoiceInterface(config) self.assertEqual( - interface._listener._timeout, config.get_value("listener_silence_timeout") + interface._listener._timeout, + config.get_value("listener_model")["listener_silence_timeout"], ) diff --git a/tests/test_connection.py b/tests/test_connection.py index 60cb9a7c..dd2df36e 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -10,8 +10,6 @@ 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.connectors.bridges.llm_qa_bridge import LLMQABridge -from wafl.extractors.entailer import Entailer from wafl.listener.whisper_listener import WhisperListener from wafl.speaker.fairseq_speaker import FairSeqSpeaker @@ -19,26 +17,9 @@ class TestConnection(TestCase): - def test__connection_to_generative_model_hostname_is_active(self): - config = Configuration.load_local_config() - LLMQABridge(config) - - def test__connection_to_generative_model_hostname_answer_a_question_correctly(self): - config = Configuration.load_local_config() - connector = LLMQABridge(config) - answer_text = asyncio.run( - connector.get_answer( - text="The bot remembers: The sky is blue", - dialogue="", - query="what color is the sky?", - ) - ) - expected = "blue" - self.assertEqual(expected, answer_text) - def test__connection_to_generative_model_can_generate_text(self): config = Configuration.load_local_config() - connector = RemoteLLMConnector(config) + connector = RemoteLLMConnector(config.get_value("llm_model")) prediction = asyncio.run( connector.predict( 'Generate a full paragraph based on this chapter title "The first contact". ' @@ -49,7 +30,7 @@ def test__connection_to_generative_model_can_generate_text(self): def test__connection_to_generative_model_can_generate_text_within_tags(self): config = Configuration.load_local_config() - connector = RemoteLLMConnector(config) + connector = RemoteLLMConnector(config.get_value("llm_model")) connector._num_prediction_tokens = 200 text = 'Generate a full paragraph based on this chapter title " The First Contact". The theme of the paragraph is space opera. Include the characters "Alberto" and "Maria". Write at least three sentences.' prompt = f""" @@ -65,7 +46,7 @@ def test__connection_to_generative_model_can_generate_text_within_tags(self): def test__connection_to_generative_model_can_generate_a_python_list(self): config = Configuration.load_local_config() - connector = RemoteLLMConnector(config) + connector = RemoteLLMConnector(config.get_value("llm_model")) connector._num_prediction_tokens = 200 prompt = "Generate a Python 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)) @@ -76,8 +57,9 @@ 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 Python list of 4 chapters names for a space opera book. The output needs to be a python list of strings: " + 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): @@ -86,14 +68,6 @@ def test__chit_chat_bridge_can_run_locally(self): answer = asyncio.run(dialogue_bridge.get_answer("", "", "bot: hello")) assert len(answer) > 0 - def test__entailment_local_connector(self): - premise = "The user says 'hello.'." - hypothesis = "The user is greeting" - config = Configuration.load_from_filename("local_config.json") - entailer = Entailer(config) - prediction = asyncio.run(entailer.get_relation(premise, hypothesis)) - self.assertTrue(prediction["entailment"] > 0.95) - def test__listener_local_connector(self): config = Configuration.load_from_filename("local_config.json") listener = WhisperListener(config) diff --git a/tests/test_conversation.py b/tests/test_conversation.py deleted file mode 100644 index 98f3460b..00000000 --- a/tests/test_conversation.py +++ /dev/null @@ -1,278 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger - -_logger = LocalFileLogger() - -_wafl_example = """ - -The user greets - username = What is the user's name - SAY hello to you, {username}! - -The user says they can swim - username = What is the user's name - the user is called {username} - -color = What is the user's hair color - username = What is the user's name - {username} has {color} hair - -the user wants to register to the newsletter - email = what is the user's email - REMEMBER the user's email is {email} - SAY {email} has been added to the newsletter - -This bot name is Fractalego - -the user is very happy - -The user's name is Bob - -Bob has black hair - -""".strip() - -_wafl_greetings = """ -The user greets - SAY Hello there! - username = What is the user's name - REMEMBER the user is called {username} - REMEMBER the user's name is {username} - SAY Nice to meet you, {username}! - -The user wants to join the club - Is the user good enough to join? - SAY Welcome to the club! - -""".strip() - -_wafl_how_are_you = """ - -This bot name is Computer -This bot is doing well - -""".strip() - - -_wafl_dialogue_variable = """ - -The user wants this bot to say something about the user - reply = Given this dialogue {_dialogue} say something about the user - SAY {reply} - -""".strip() - - -class TestConversation(TestCase): - def test__single_utterance(self): - interface = DummyInterface() - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - assert interface.get_utterances_list()[0] == "bot: " + utterance - - def test__say_command(self): - interface = DummyInterface() - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example, logger=_logger), - interface=interface, - logger=_logger, - ) - input_from_user = "hello!".capitalize() - asyncio.run(conversation_events._process_query(input_from_user)) - expected = "bot: Hello to you, bob!" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test_input_during_inference(self): - interface = DummyInterface( - to_utter=["Can I register to the newsletter?", "test@example.com"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Test@example.com has been added to the newsletter" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test__remember_command(self): - interface = DummyInterface( - to_utter=[ - "Can I register to the newsletter?", - "test@example.com", - "What is the email of the user", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example, logger=_logger), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "test@example.com" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() - - def test__knowledge_insertion(self): - interface = DummyInterface( - to_utter=["the user's mother is called Ada", "How is the user's mum called"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "ada" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() - - def test__greeting(self): - interface = DummyInterface(["My name is Albert", "What is my name"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, "", logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "albert" - assert expected in interface.get_utterances_list()[-1].lower() - - def test__greeting_with_alberto_as_name(self): - interface = DummyInterface(["My name is Albert0", "What is my name"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings, logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "albert0" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() - - def test__yes(self): - interface = DummyInterface(["My name is Ada", "am I called Ada"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings, logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert ( - "yes" in interface.get_utterances_list()[-1].lower() - or "ada" in interface.get_utterances_list()[-1].lower() - ) - - def test__no(self): - interface = DummyInterface(["My name is Albert", "Is my name Bob?"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings, logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert ( - "no" in interface.get_utterances_list()[-1].lower() - or "albert" in interface.get_utterances_list()[-1].lower() - ) - - def test__yes_no_questions_from_bot_with_answer_yes(self): - interface = DummyInterface(["I want to join the club", "yes"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings, logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - assert interface.get_utterances_list()[-1] == "bot: Welcome to the club!" - - def test__yes_no_questions_from_bot_with_answer_no(self): - interface = DummyInterface(["I want to join the club", "no"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings, logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - assert ( - interface.get_utterances_list()[-2] == "bot: are you good enough to join?" - ) - - def test__hello_and_username(self): - interface = DummyInterface(["Hello", "Albert"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings, logger=_logger), - interface=interface, - logger=_logger, - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == "bot: Nice to meet you, albert!" - - def test__how_are_you(self): - interface = DummyInterface(["How are you?"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_how_are_you, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - assert "doing well" in interface.get_utterances_list()[-1] - - def test__dialogue_variable_works(self): - interface = DummyInterface( - ["my name is Albert0 and I am a carpenter", "say something about me"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_dialogue_variable, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "albert0" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py deleted file mode 100644 index 8d8ea6bc..00000000 --- a/tests/test_dependencies.py +++ /dev/null @@ -1,172 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.project_knowledge import ProjectKnowledge - - -wafl_dependency = """ -#using greetings - -The user greets - name = what is the name of the person that is greeting - the user wants {name} to greet back - -the user's name is alberto - the user is italian - -""".strip() - - -class TestDependencies(TestCase): - def test__knowledge_dependencies_are_populated(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - knowledge = ProjectKnowledge(tmp_filename) - expected = { - "/": ["/greetings"], - "/greetings": ["/facts", "/rules", "/../backward_import"], - "/greetings/../backward_import": [], - "/greetings/facts": [], - "/greetings/rules": [], - } - self.assertEqual(expected, knowledge._dependency_dict) - - def test__knowledge_dictionary_is_populated(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, tmp_filename) - expected = [ - "/", - "/greetings", - "/greetings/facts", - "/greetings/rules", - "/greetings/../backward_import", - ] - self.assertEqual(expected, list(knowledge._knowledge_dict.keys())) - - def test__rules_are_called_from_dependency_list(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - interface = DummyInterface(to_utter=["Hello", "albert"]) - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, tmp_filename) - conversation_events = ConversationEvents( - knowledge, interface=interface, code_path=knowledge.get_dependencies_list() - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Hello, albert!" - assert interface.get_utterances_list()[-1] == expected - - def test__facts_are_answered_from_dependency_list_one_level_deep(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - interface = DummyInterface( - to_utter=[ - "How are you doing", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - ProjectKnowledge(config, tmp_filename), interface=interface - ) - asyncio.run(conversation_events.process_next()) - expected = "well" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1] - - def test__facts_are_answered_from_dependency_list_two_levels_deep(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - interface = DummyInterface( - to_utter=[ - "how is the sun", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - ProjectKnowledge(config, tmp_filename), interface=interface - ) - asyncio.run(conversation_events.process_next()) - expected = "shin" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1] - - def test__functions_can_be_called_from_a_dependency(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - interface = DummyInterface( - to_utter=[ - "What time is it", - ] - ) - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, tmp_filename) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: The time is now!" - assert interface.get_utterances_list()[-1] == expected - - def test__functions_can_be_called_from_a_2_level_deep_dependency(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - interface = DummyInterface( - to_utter=[ - "My name is Alberto", - ] - ) - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, tmp_filename) - conversation_events = ConversationEvents( - knowledge=knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - expected = "bot: Ciao!" - assert interface.get_utterances_list()[-1] == expected - - def test__rules_can_be_imported_using_prior_folders(self): - tmp_filename = "test.wafl" - with open(tmp_filename, "w") as file: - file.write(wafl_dependency) - - interface = DummyInterface( - to_utter=[ - "My name is Maria", - ] - ) - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, tmp_filename) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - expected = "bot: The sky is green" - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py deleted file mode 100644 index 8fd6622f..00000000 --- a/tests/test_edge_cases.py +++ /dev/null @@ -1,162 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger - -_logger = LocalFileLogger() -_wafl_greetings = """ - -# Simple initial facts -This bot name is Computer - -""".strip() - -_tube_line_rules = """ - -# Simple initial facts -This bot is doing well - -This bot is called Computer - - -# Greetings commands -The user says "bring yourself online" - SAY Hello there! - -the user asks "How are you" - SAY All is good thanks - - -# Time commands -the user asks for the time - time = get_time() - SAY the time is {time} - - -# Shopping list - -item = what does the user want to add to the shopping list? - add_shopping_list(item) - SAY {item} has been added to the list - ! _ask_another_item - items = get_shopping_list_in_english() - SAY the shopping list now contains: {items} - - -_ask_another_item - does the user want to add another item - item = what do you want to add to the shopping list - add_shopping_list(item) - SAY {item} has been added to the list - _ask_another_item - -the user wants to delete the shopping list - Do you want to delete the current shopping list - reset_shopping_list() - SAY The shopping list has been deleted - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -"What should I buy" - the user wants to know what is in the shopping list - - -the user wants to add something - item = what does the user want to add? - list_name = which list? - the user wants to add {item} to {list_name} - -# Check for trains -the user wants to check if a line is running -### TESTING COMMENTS - line_name = which line do you want to check? - check_tfl_line(line_name) - -linename = which line is running? - SAY {linename} - normname = normalize_name(linename) - check_tfl_line(normname) - -is the overground running? - check_tfl_line("overground") - - -# End the events -_close - close_conversation() - -the user believes they don't need anything else - SAY ok - _close - -the user says good bye - SAY good bye! - _close - -the user says shut up - SAY ok - _close - -the user asks this bot to be silent - _close - -the user says: "thank you this is all" - _close - -# Interruptions - - -the user does not want to continue the task - ! Do you want to continue - close_task() - -the user wants to stop - ! Do you want to continue - close_task()) - -""".strip() - - -class TestEdgeCases(TestCase): - def test__double_lower_case_questions_are_answered_correctly(self): - interface = DummyInterface(["is the jubile line running"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _tube_line_rules), - interface=interface, - code_path="/", - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - assert "asks:" not in interface.get_utterances_list()[0] - - def test__clause_does_not_return_unknown(self): - interface = DummyInterface(["is the jubili line running"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _tube_line_rules), - interface=interface, - code_path="/", - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - assert "unknown" not in interface.get_utterances_list()[-1] - - def test__no_answer_if_retrieval_is_too_sparse(self): - interface = DummyInterface(["I will i"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _tube_line_rules), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert "unknown" not in interface.get_utterances_list()[-1] diff --git a/tests/test_empty_input.py b/tests/test_empty_input.py deleted file mode 100644 index a22f38f5..00000000 --- a/tests/test_empty_input.py +++ /dev/null @@ -1,51 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_wafl_greetings = """ - -The user says their name - SAY Hello there! - username = What is the user's name - SAY Nice to meet you, {username}! - -""".strip() - -_wafl_greetings2 = """ - -The user says hi or hello - SAY Hello there! - -""".strip() - - -class TestEmptyInput(TestCase): - def test_hello_and_username(self): - interface = DummyInterface(["Hello", "My name is Albert"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), interface=interface - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "nice to meet you" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() - - def test_empty_input_does_nothing(self): - interface = DummyInterface(["computer"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings2), interface=interface - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - conversation_events.process_next(activation_word="computer") - assert interface.get_utterances_list() != ["bot: Hello there!"] diff --git a/tests/test_entailment.py b/tests/test_entailment.py deleted file mode 100644 index 2ffe68a1..00000000 --- a/tests/test_entailment.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.extractors.entailer import Entailer - -_path = os.path.dirname(__file__) - - -class TestEntailment(TestCase): - def test_fact_entailment(self): - premise = "The user says 'hello.'." - hypothesis = "The user is greeting" - - config = Configuration.load_local_config() - entailer = Entailer(config) - prediction = asyncio.run(entailer.get_relation(premise, hypothesis)) - self.assertTrue(prediction["entailment"] > 0.95) - - def test_question_entailment(self): - premise = "The user says 'What time is the train leaving.'" - hypothesis = "The user inquires about transport time tables" - - config = Configuration.load_local_config() - entailer = Entailer(config) - prediction = asyncio.run(entailer.get_relation(premise, hypothesis)) - print(prediction) - self.assertTrue(prediction["entailment"] > 0.95) - - def test_entailment_method(self): - premise = "The user says 'my name is John.'." - hypothesis = "The user says their name" - - config = Configuration.load_local_config() - entailer = Entailer(config) - self.assertEqual(asyncio.run(entailer.entails(premise, hypothesis)), "True") diff --git a/tests/test_entailment_in_rules.py b/tests/test_entailment_in_rules.py deleted file mode 100644 index 45d4aaad..00000000 --- a/tests/test_entailment_in_rules.py +++ /dev/null @@ -1,32 +0,0 @@ -import asyncio -import os - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from unittest import TestCase - -_path = os.path.dirname(__file__) -_wafl_greetings = """ - -The user wants to buy something - item = what does the user want to buy - the user wants to buy a fruit:-the user wants to buy {item} - SAY You want to buy fruit! - -""".strip() - - -class TestEntailmentInRules(TestCase): - def test__entailment_in_rule_returns_true(self): - interface = DummyInterface(["I want to buy apples"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: You want to buy fruit!" - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_events.py b/tests/test_events.py deleted file mode 100644 index 05edce65..00000000 --- a/tests/test_events.py +++ /dev/null @@ -1,87 +0,0 @@ -import asyncio -import os - -from datetime import datetime -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.generated_events import GeneratedEvents -from wafl.events.events_from_function_list import EventsCreatorFromFunctionList -from wafl.events.events_from_module_name import EventsCreatorFromModuleName -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger - -_path = os.path.dirname(__file__) -_logger = LocalFileLogger() - -_wafl_example = """ -It is 7,05 - SAY Hello there! -""".strip() - - -def function_that_returns_time(): - now = datetime.now() - minutes = int(now.strftime("%M")) - hour = int(now.strftime("%H")) - return f"The time is {hour}:{minutes}" - - -def return_five_past_seven(): - return "the time is 7,05" - - -def return_four_past_seven(): - return "the time is 7,04" - - -class TestEvents(TestCase): - def test__events_correctly_uses_argument_functions(self): - events_creator = EventsCreatorFromFunctionList([function_that_returns_time]) - expected = function_that_returns_time() - predicted = events_creator.get()[0] - self.assertEqual(expected, predicted) - - def test__events_can_trigger_rule(self): - interface = DummyInterface() - config = Configuration.load_local_config() - generated_events = GeneratedEvents( - config, - SingleFileKnowledge(config, _wafl_example, logger=_logger), - events=EventsCreatorFromFunctionList([return_five_past_seven]), - interface=interface, - logger=_logger, - ) - asyncio.run(generated_events.process_next()) - - expected = "bot: Hello there!" - assert interface.get_utterances_list()[-1] == expected - - def test__events_does_not_trigger_rule(self): - interface = DummyInterface() - config = Configuration.load_local_config() - generated_events = GeneratedEvents( - config, - SingleFileKnowledge(config, _wafl_example, logger=_logger), - events=EventsCreatorFromFunctionList([return_four_past_seven]), - interface=interface, - logger=_logger, - ) - asyncio.run(generated_events.process_next()) - expected = [] - assert interface.get_utterances_list() == expected - - def test__events_functions_can_be_loaded_from_file(self): - interface = DummyInterface() - config = Configuration.load_local_config() - generated_events = GeneratedEvents( - config, - SingleFileKnowledge(config, _wafl_example, logger=_logger), - events=EventsCreatorFromModuleName("events"), - interface=interface, - logger=_logger, - ) - asyncio.run(generated_events.process_next()) - expected = ["bot: Hello there!"] - assert interface.get_utterances_list() == expected diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py deleted file mode 100644 index a1a1e8e4..00000000 --- a/tests/test_exceptions.py +++ /dev/null @@ -1,36 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.exceptions import CloseConversation -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_wafl_greetings = """ - -The user says good bye - close_conversation() - -""".strip() - - -class TestExceptions(TestCase): - def test_runtime_warning_escapes_python_space(self): - interface = DummyInterface(["Good bye!"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), - interface=interface, - code_path="/", - ) - utterance = "Welcome to the website. How may I help you?" - interface.output(utterance) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - return - - assert False diff --git a/tests/test_executables.py b/tests/test_executables.py deleted file mode 100644 index c66b5613..00000000 --- a/tests/test_executables.py +++ /dev/null @@ -1,201 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -wafl_example = """ -Speed is space over time - - -the user wants to register to the newsletter - email = what is the user's email - REMEMBER the user's email is {email} - newsletter_name = dummy_add_email(email) - dummy_log_email(email) - SAY {email} has been added to the newsletter '{newsletter_name}' - -item = what does the user want to add to the shopping list? - shopping_list.append(item) - SAY {item} has been added to the list - -item = what does the user want to remove from the shopping list? - shopping_list.remove(item) - SAY {item} has been removed from the list - -item = what does the user want to add to the test list? - ! equal(item, "batteries") - REMEMBER the user wants to add {item} - shopping_list.append(item) - SAY {item} has been added to the list - -item = what does the user want to add to the test list? - equal(item, "batteries") - REMEMBER the user wants to add {item} - SAY {item} cannot be added to the list - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -the user asks for the time - time = get_time() - SAY the time is {time} - -The user wants to say something - sentence = What does the user want to say - say_text(sentence) - - -the user says "please define speed": - testing_fact_from_python_space() - SAY Test complete - -""" - - -class TestExecutables(TestCase): - def test_executables(self): - interface = DummyInterface(to_utter=["test@example.com"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - input_from_user = "Can I register to the newsletter?".capitalize() - asyncio.run(conversation_events._process_query(input_from_user)) - expected = ( - "bot: Test@example.com has been added to the newsletter 'fake_newsletter'" - ) - assert interface.get_utterances_list()[-1] == expected - - def test_add_to_list(self): - interface = DummyInterface(to_utter=["Please add apples to the shopping list"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Apples has been added to the list" - assert interface.get_utterances_list()[-1] == expected - - def test_remove_from_list(self): - interface = DummyInterface( - to_utter=[ - "Please add apples to the shopping list", - "Please delete apples from the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "bot: Apples has been removed from the list" - assert interface.get_utterances_list()[-1] == expected - - def test_list_the_items(self): - interface = DummyInterface( - to_utter=[ - "Please add apples to the shopping list", - "Please add bananas to the shopping list", - "What's in the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "bot: The shopping list contains: apples, bananas" - expected2 = "bot: The shopping list contains: bananas, apples" - assert ( - interface.get_utterances_list()[-1] == expected - or interface.get_utterances_list()[-1] == expected2 - ) - - def test_list_the_items2(self): - interface = DummyInterface( - to_utter=[ - "Please add apples to the shopping list", - "Please add bananas to the shopping list", - "What does the shopping list contain", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "bot: The shopping list contains: apples, bananas" - expected2 = "bot: The shopping list contains: bananas, apples" - assert ( - interface.get_utterances_list()[-1] == expected - or interface.get_utterances_list()[-1] == expected2 - ) - - def test_question_activates_inference(self): - interface = DummyInterface(to_utter=["What time is it?"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "The time is" - assert expected in interface.get_utterances_list()[-1] - - def test_negation(self): - interface = DummyInterface(to_utter=["add batteries to the test list"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Batteries cannot be added to the list" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test_say_command_in_functions(self): - interface = DummyInterface(to_utter=["I want to say 'this is a test'"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: This is a test" - assert interface.get_utterances_list()[-1].lower() == expected.lower() - - def test__facts_work_in_python_space(self): - interface = DummyInterface(to_utter=["Please define speed"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Test complete" - assert interface.get_utterances_list()[-1].lower() == expected.lower() diff --git a/tests/test_facts.py b/tests/test_facts.py new file mode 100644 index 00000000..691e4522 --- /dev/null +++ b/tests/test_facts.py @@ -0,0 +1,31 @@ +import asyncio + +from unittest import TestCase + +from wafl.config import Configuration +from wafl.events.conversation_events import ConversationEvents +from wafl.interface.dummy_interface import DummyInterface + +wafl_example = """ +facts: + - the bots name is "Bob" +""" + + +class TestFacts(TestCase): + def test__facts_are_retrieved(self): + interface = DummyInterface( + to_utter=[ + "what is your name", + ] + ) + config = Configuration.load_local_config() + config.set_value("rules", wafl_example) + conversation_events = ConversationEvents( + config=config, + interface=interface, + ) + asyncio.run(conversation_events.process_next()) + print(interface.get_utterances_list()) + expected = "bob" + self.assertIn(expected, interface.get_utterances_list()[-1].lower()) diff --git a/tests/test_generation.py b/tests/test_generation.py deleted file mode 100644 index 0d96416e..00000000 --- a/tests/test_generation.py +++ /dev/null @@ -1,50 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) - - -wafl_rules = """ -the user is Italian - -The user says hello - result = the user is Italian - SAY {result} - -The user says their name - name = what is the user's name? - first_letter = Return the first letter of the word {name} - SAY {first_letter} - SAY The first letter of your name is {first_letter} -""".strip() - - -class TestGeneration(TestCase): - def test__language_model_returns_first_letter_of_name(self): - interface = DummyInterface(["My name is alberto"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_rules), interface=interface, code_path="/" - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: The first letter of your name is a" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test__does_not_generate_if_it_is_not_instructions(self): - interface = DummyInterface(["Hello"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_rules), interface=interface, code_path="/" - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Yes" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_greetings.py b/tests/test_greetings.py deleted file mode 100644 index e2e1db2f..00000000 --- a/tests/test_greetings.py +++ /dev/null @@ -1,92 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_wafl_greetings = """ -# Simple initial facts -This bot name is Computer - - -# Greetings commands -The user greets - ! the user has introduced themselves - SAY Hello there! - username = What is the user's name - REMEMBER the user is called {username} - REMEMBER the user's name is {username} - REMEMBER the user introduced themselves - SAY Nice to meet you, {username}! - - -# Time commands -the user asks for the time - time = get_time() - SAY the time is {time} - - -# voice bootstrapping -the user wants to record their voice - do you want to record your voice? - record_utterances() - the user wants to close the events - -# End the events -the user wants to close the events - SAY good bye! - close_conversation() - -the user believes they don't need anything else - the user wants to close the events - -the user wishes good bye - the user wants to end the events - -the user says shut up - the user wants to end the events - - -# Interruptions - -the user says thank you - Does the user want to terminate the events? - the user wants to close the events - -the user does not want to continue the task - Do you want to continue - close_task() - -the user wants to stop - Do you want to continue - close_task() - -""".strip() - - -class TestGreetings(TestCase): - def test_hello_and_username(self): - interface = DummyInterface(["Hello", "Albert"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), interface=interface - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - assert interface.get_utterances_list()[-1] == "bot: Nice to meet you, albert!" - - def test_hello_and_username2(self): - interface = DummyInterface(["Hello", "bob"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), interface=interface - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == "bot: Nice to meet you, bob!" diff --git a/tests/test_inference.py b/tests/test_inference.py deleted file mode 100644 index 6ee4b726..00000000 --- a/tests/test_inference.py +++ /dev/null @@ -1,160 +0,0 @@ -import asyncio -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.narrator import Narrator -from wafl.events.task_memory import TaskMemory -from wafl.inference.backward_inference import BackwardInference -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.extractors.dataclasses import Query -from wafl.logger.local_file_logger import LocalFileLogger -from wafl.policy.answerer_policy import AnswerPolicy - -_logger = LocalFileLogger() - -wafl_example = """ - -The user greets - username = What is the user's name - SAY hello to you, {username}! - -The user says they can swim - username = What is the user's name - USER is called {username} - -color = What is the user's hair color - username = What is the user's name - {username} has {color} hair - -{person} has a {type} tree in the garden - person = what is the user's name - house_address = what is {person}'s address - type = what is the tree type at {house_address} - -This bot name is Fractalego - -the user is very happy - -The user's name is Bob - -Bob has black hair - -Bob's address is 42 Flinch road - -42 Flinch road has a peach tree in the garden - -""".strip() - - -class TestInference(TestCase): - def test__simple_question(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - ) - query = Query(text="What is this bot's name", is_question=True, variable="name") - answer = asyncio.run(inference.compute(query, depth=1)) - expected = "fractalego" - assert answer.text.lower() == expected - assert answer.variable == query.variable - - def test__fact_check_true(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - ) - query = Query( - text="The user is in a good mood", is_question=False, variable="name" - ) - answer = asyncio.run(inference.compute(query, depth=1)) - assert answer.is_true() - - def test__fact_check_false(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - ) - query = Query(text="The user is sad", is_question=False, variable="name") - answer = asyncio.run(inference.compute(query, depth=1)) - assert answer.is_false() - - def test__simple_rule(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - ) - query = Query(text="The user says hello!", is_question=False, variable="name") - answer = asyncio.run(inference.compute(query, depth=1)) - assert answer.is_true() - - def test__forward_substitution(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - ) - query = Query( - text="The user says: I can swim", is_question=False, variable="name" - ) - answer = asyncio.run(inference.compute(query, depth=1)) - assert answer.is_true() - - def test__backward_substitution(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - ) - query = Query( - text="The user says: I have black hair", is_question=False, variable="name" - ) - answer = asyncio.run(inference.compute(query, depth=1)) - assert answer.is_true() - - def test__forward_substution_2(self): - interface = DummyInterface() - config = Configuration.load_local_config() - inference = BackwardInference( - config, - SingleFileKnowledge(config, wafl_example), - interface, - Narrator(interface), - logger=_logger, - ) - policy = AnswerPolicy(interface) - query = Query( - text="What type of tree is there at Bob's house", - is_question=True, - variable="name", - ) - task_memory = TaskMemory() - answer = asyncio.run( - inference._look_for_answer_in_rules( - query, task_memory, "/", policy, 0, False - ) - ) - expected = "peach tree" - assert answer.text == expected diff --git a/tests/test_interruptions.py b/tests/test_interruptions.py deleted file mode 100644 index 56c006c0..00000000 --- a/tests/test_interruptions.py +++ /dev/null @@ -1,90 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.exceptions import CloseConversation -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_wafl_greetings = """ -This bot is here to answer the user unless asked to be silent - -The user says hi - SAY Hello there! - username = What is the user's name - SAY Nice to meet you, {username}! - -INTERRUPTION the user wants to know the time - time = get_time() - SAY the time is {time} - -INTERRUPTION the user says to shut up - close_conversation() - -INTERRUPTION the user wants to stop the task - close_task() - -""".strip() - - -class TestInterruptions(TestCase): - def test_time_request_does_not_interrupt(self): - interface = DummyInterface(["Hello", "Albert"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), interface=interface - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == "bot: Nice to meet you, albert!" - - def test_time_request_does_interrupt(self): - interface = DummyInterface(["Hello", "what's the time?", "Albert"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), - interface=interface, - code_path="/", - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert "The time is" in interface.get_utterances_list()[-4] - assert interface.get_utterances_list()[-1] == "bot: Nice to meet you, albert!" - - def test_time_shut_up_does_interrupt(self): - interface = DummyInterface(["Hello", "shut up"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), - interface=interface, - code_path="/", - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - return - - assert False - - def test_task_interrupt_task_does_interrupt(self): - interface = DummyInterface(["Hello", "I want to stop the task"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), - interface=interface, - code_path="/", - ) - utterance = "Welcome to the website. How may I help you?" - asyncio.run(interface.output(utterance)) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == "bot: Task interrupted" diff --git a/tests/test_knowledge.py b/tests/test_knowledge.py deleted file mode 100644 index 01c83b8d..00000000 --- a/tests/test_knowledge.py +++ /dev/null @@ -1,40 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.extractors.dataclasses import Query - -wafl_example = """ - -The user says hello - name = What is the user's name - SAY Hello, {name}! - -item = what does the user want to add to the shopping list? - reset_shopping_list() - shopping_list.append(item) - SAY {item} has been added to the list - ! _ask_another_item - -_ask_another_item - does the user want to add another item - item = what do you want to add to the shopping list - SAY {item} has been added to the list - _ask_another_item - -""" - - -class TestKnowledge(TestCase): - def test_exact_string(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, wafl_example) - rules = asyncio.run( - knowledge.ask_for_rule_backward( - Query(text="_ask_another_item", is_question=False) - ) - ) - expected = "_ask_another_item" - assert rules[0].effect.text == expected diff --git a/tests/test_list_type.py b/tests/test_list_type.py deleted file mode 100644 index b2e2059d..00000000 --- a/tests/test_list_type.py +++ /dev/null @@ -1,46 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) - -_rules = """ - -the user wants to write a book - theme = what is the book about? - list_of_chapters = Generate a bullet list of 4 chapters names for a book. The theme of the book is {theme}. - SAY [{list_of_chapters}] - chapter_texts = Generate a full paragraph based on this chapter title "[{list_of_chapters}]". Include the characters "Alberto" and "Maria". Write at least three sentences. - SAY [{chapter_texts}] - -""" - - -class TestListType(TestCase): - def test__rule_line_can_return_list(self): - interface = DummyInterface( - [ - "I want to write a book", - "space opera", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _rules), interface=interface, code_path="/" - ) - asyncio.run(conversation_events.process_next()) - [print(item) for item in interface.get_utterances_list()] - assert "alberto" in interface.get_utterances_list()[-1].lower() - assert "alberto" in interface.get_utterances_list()[-2].lower() - assert "alberto" in interface.get_utterances_list()[-3].lower() - assert "alberto" in interface.get_utterances_list()[-4].lower() - assert "maria" in interface.get_utterances_list()[-1].lower() - assert "maria" in interface.get_utterances_list()[-2].lower() - assert "maria" in interface.get_utterances_list()[-3].lower() - assert "maria" in interface.get_utterances_list()[-4].lower() diff --git a/tests/test_lists.py b/tests/test_lists.py deleted file mode 100644 index af0af57a..00000000 --- a/tests/test_lists.py +++ /dev/null @@ -1,190 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) - -_rules = """ - -item = what does the user want to add to the shopping list? - add_shopping_list(item) - SAY {item} has been added to the list - ! _ask_another_item - -item = what does the user want to add to the shopping list? - add_shopping_list(item) - !{item} can be part of a shopping list - SAY Please speak more clearly - -item = what does the user want to remove from the shopping list? - remove_from_shopping_list(item) - items = get_shopping_list_in_english() - -_ask_another_item - does the user want to add another item - item = what do you want to add to the shopping list - add_shopping_list(item) - SAY {item} has been added to the list - _ask_another_item - -_ask_another_item - does the user want to add another item - item = what do you want to add to the shopping list - !{item} can be part of a shopping list - SAY {item} cannot be part of a shopping list - _ask_another_item - -"remove an item from the shopping list" - item = What do you want to remove? - remove_from_shopping_list(item) - items = get_shopping_list_in_english() - -the user wants to delete the shopping list - Do you want to delete the current shopping list - reset_shopping_list() - SAY The shopping list has been deleted - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -"What should I buy" - the user wants to know what is in the shopping list - -""" - -_lists_in_functions_rules = """ -item = what does the user want to add to the shopping list? - add_shopping_list_as_function(item) - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -the user wants to delete the shopping list - Do you want to delete the current shopping list - reset_shopping_list() - SAY The shopping list has been deleted -""" - - -class TestLists(TestCase): - def test__second_rule_is_not_run_if_prior_clause_fails(self): - interface = DummyInterface( - [ - "add apples to the shopping list", - "no", - "remove apples from the shopping list", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _rules), interface=interface, code_path="/" - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - output = "\n".join(interface.get_utterances_list()) - assert output.count("Do you want to remove apples from the shopping list") == 1 - - def test__add_item_to_list_as_function(self): - interface = DummyInterface( - [ - "Please delete the shopping list", - "yes", - "add apples to the shopping list", - "yes", - "strawberries", - "yes", - "bananas", - "no", - "what is in the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _lists_in_functions_rules), - interface=interface, - code_path="/", - ) - while asyncio.run(conversation_events.process_next()): - pass - - assert ( - interface.get_utterances_list()[-1] - == "bot: The shopping list contains: apples, bananas, strawberries" - ) - - def test__yes_please_means_yes(self): - interface = DummyInterface( - [ - "Please delete the shopping list", - "yes please", - "add apples to the shopping list", - "yes please", - "strawberries", - "no", - "what is in the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _lists_in_functions_rules), - interface=interface, - code_path="/", - ) - while asyncio.run(conversation_events.process_next()): - pass - - assert ( - interface.get_utterances_list()[-1] - == "bot: The shopping list contains: apples, strawberries" - ) - - def test__yes_I_do_means_yes(self): - interface = DummyInterface( - [ - "Please delete the shopping list", - "yes I do", - "add apples to the shopping list", - "yes I do", - "strawberries", - "no", - "what is in the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _lists_in_functions_rules), - interface=interface, - code_path="/", - ) - while asyncio.run(conversation_events.process_next()): - pass - - assert ( - interface.get_utterances_list()[-1] - == "bot: The shopping list contains: apples, strawberries" - ) - - def test__hotword_is_ignored_in_instructions(self): - interface = DummyInterface( - [ - "computer add apples to the shopping list", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _rules), interface=interface, code_path="/" - ) - hotword = "Computer" - asyncio.run(conversation_events.process_next(activation_word=hotword)) - expected = "bot: apples has been added to the list" - self.assertEqual(interface.get_utterances_list()[-3].lower(), expected) diff --git a/tests/test_natural_language_in_functions.py b/tests/test_natural_language_in_functions.py deleted file mode 100644 index 8bfe9f19..00000000 --- a/tests/test_natural_language_in_functions.py +++ /dev/null @@ -1,105 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - - -wafl_example = """ - -item = what does the user want to add to the shopping list? - reset_shopping_list() - shopping_list.append(item) - SAY {item} has been added to the list - append_until_user_is_done() - - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -the user wants this bot to say hello twice - say_twice() -""" - -_tube_line_rules = """ -linename = which line is running? - SAY RUNNING - normname = normalize_name(linename) - SAY {normname} - check_tfl_line(normname) - -is the overground running? - check_tfl_line("overground") - -is the dlr running? - check_tfl_line("dlr") - -""".strip() - - -class TestLanguageInFunctions(TestCase): - def test_executables(self): - interface = DummyInterface( - to_utter=[ - "Add apples to the shopping list", - "yes", - "Please add bananas to the shopping list", - "no", - "What's in the shopping list", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "bot: The shopping list contains: apples, bananas" - assert interface.get_utterances_list()[-1] == expected - - def test_say_twice_in_python_space(self): - interface = DummyInterface( - to_utter=[ - "Please say hello twice", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = [ - "bot: Please say: 'hello'", - "bot: Your input is recorded", - "bot: Please say: 'hello'", - "bot: Your input is recorded", - ] - - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[1] == expected[0] - assert interface.get_utterances_list()[2] == expected[1] - assert interface.get_utterances_list()[3] == expected[2] - assert interface.get_utterances_list()[4] == expected[3] - - def test_double_functions(self): - interface = DummyInterface(["Is the victoria line running"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _tube_line_rules), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert ( - interface.get_utterances_list()[-1] - == "bot: The victoria line is running normally" - ) diff --git a/tests/test_negations.py b/tests/test_negations.py deleted file mode 100644 index ed4566ff..00000000 --- a/tests/test_negations.py +++ /dev/null @@ -1,64 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -wafl_example = """ - -the user wants to know what is in the shopping list - ERASE MEMORY - Does the user want to see the shopping list - SAY So you do want to see it! - -""" - - -class TestNegations(TestCase): - def test_simple_yes(self): - interface = DummyInterface( - to_utter=[ - "What's in the shopping list", - "yes", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), interface=interface - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: So you do want to see it!" - assert interface.get_utterances_list()[-1] == expected - - def test_simple_no(self): - interface = DummyInterface( - to_utter=[ - "I want to know what the shopping list contains", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), interface=interface - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: do you want to see the shopping list" - assert interface.get_utterances_list()[-2] == expected - - def test_no_thanks(self): - interface = DummyInterface( - to_utter=[ - "I want to know what the shopping list contains", - "no thanks", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), interface=interface - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: do you want to see the shopping list" - assert interface.get_utterances_list()[-2] == expected diff --git a/tests/test_new_tests.py b/tests/test_new_tests.py deleted file mode 100644 index 82c91dca..00000000 --- a/tests/test_new_tests.py +++ /dev/null @@ -1,9 +0,0 @@ -import os - -from unittest import TestCase - -_path = os.path.dirname(__file__) - - -class TestNew(TestCase): - pass diff --git a/tests/test_parsing.py b/tests/test_parsing.py deleted file mode 100644 index fe7dd282..00000000 --- a/tests/test_parsing.py +++ /dev/null @@ -1,159 +0,0 @@ -import asyncio -from unittest import TestCase - -from wafl.config import Configuration -from wafl.facts import Fact -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.parsing.rules_parser import get_facts_and_rules_from_text, get_dependency_list -from wafl.extractors.dataclasses import Query -from wafl.rules import Rule - -wafl_example = """ -#using lists, tfl -#using weather - -USER greets - USER is called {username} - SAY hello to you, {username}! -USER says their name - USER is called {username} - - nice to meet you {username} - -BOT name is Fractalego -the user is happy - -""".strip() - - -class TestParsing(TestCase): - def test__rules_parsing(self): - facts_and_rules = get_facts_and_rules_from_text(wafl_example) - expected = str( - [ - Rule( - effect=Fact( - text="USER greets", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - causes=[ - Fact( - text="USER is called {username}", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - Fact( - text="SAY hello to you, {username}!", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - ], - knowledge_name=None, - ), - Rule( - effect=Fact( - text="USER says their name", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - causes=[ - Fact( - text="USER is called {username}", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - Fact( - text="nice to meet you {username}", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - ], - knowledge_name=None, - ), - ] - ) - assert str(facts_and_rules["rules"]) == expected - - def test__fact_parsing(self): - facts_and_rules = get_facts_and_rules_from_text(wafl_example) - expected = str( - [ - Fact( - text="BOT name is Fractalego", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - Fact( - text="the user is happy", - is_question=False, - variable=None, - is_interruption=False, - source=None, - destination=None, - knowledge_name=None, - ), - ] - ) - assert str(facts_and_rules["facts"]) == expected - - def test__knowledge_facts(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, wafl_example) - expected = str(Fact(text="the user is happy", is_question=False)) - facts = asyncio.run( - knowledge.ask_for_facts(Query("how is the user", is_question=True)) - ) - assert str(facts[0]) == expected - - def test__knowledge_rules(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, wafl_example) - expected = str( - Rule( - effect=Fact(text="USER greets", is_question=False), - causes=[ - Fact(text="USER is called {username}", is_question=False), - Fact(text="SAY hello to you, {username}!", is_question=False), - ], - ) - ) - rules = asyncio.run( - knowledge.ask_for_rule_backward( - Query("the user greets you", is_question=False) - ) - ) - assert str(rules[0]) == expected - - def test__dependency_list_is_extracted(self): - dependency_list = get_dependency_list(wafl_example) - expected = ["lists", "tfl", "weather"] - self.assertEqual(dependency_list, expected) diff --git a/tests/test_policy.py b/tests/test_policy.py deleted file mode 100644 index b2019e5a..00000000 --- a/tests/test_policy.py +++ /dev/null @@ -1,50 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) -wafl_example = """ - -The user asks about the shopping list - SAY So you do want to see it! - -The user wants to see the todo list - SAY No list here! - -""" - - -class TestPolicy(TestCase): - def test__information_is_repeated(self): - interface = DummyInterface( - to_utter=["What's in the shopping list", "say it again"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), interface=interface - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "to see it" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() - - def test__policy_can_steer_conversation(self): - interface = DummyInterface( - to_utter=["What's in the shopping list", "I meant the todo list"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), interface=interface - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "no list here!" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1].lower() diff --git a/tests/test_preprocessing.py b/tests/test_preprocessing.py deleted file mode 100644 index a4f2569d..00000000 --- a/tests/test_preprocessing.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.parsing.preprocess import ( - create_preprocessed, - remove_preprocessed, - import_module, - get_all_functions_names, -) - -wafl_example = """ - -the user says hello - c() - -""" - - -class TestPreprocessing(TestCase): - def test__preprocessing_has_all_functions_names(self): - predicted = get_all_functions_names("preprocess_test_functions") - expected = ["a", "b", "c"] - assert predicted == expected - - def test__preprocessing_runs(self): - create_preprocessed( - "/", "preprocess_test_functions", "preprocess_test_functions.py" - ) - remove_preprocessed("/", "preprocess_test_functions.py") - - def test__import_preprocessed_module(self): - create_preprocessed( - "/", "preprocess_test_functions", "preprocess_test_functions.py" - ) - import_module("/", "", "preprocess_test_functions") - remove_preprocessed("/", "preprocess_test_functions.py") - - def test__functions_can_call_another_function(self): - interface = DummyInterface( - to_utter=[ - "Hello", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Hello" - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_question_in_rule_trigger.py b/tests/test_question_in_rule_trigger.py deleted file mode 100644 index f73e4dab..00000000 --- a/tests/test_question_in_rule_trigger.py +++ /dev/null @@ -1,96 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.exceptions import CloseConversation -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - - -wafl_example = """ -This bot is here to answer the user. - -_close - close_conversation() - -INTERRUPTION the user says good bye - SAY good bye! - _close - -INTERRUPTION the user says shut up - SAY ok - _close - -INTERRUPTION the user asks this bot to be silent - _close - -INTERRUPTION the user says: "thank you" - _close -""" - - -class TestLanguageInFunctions(TestCase): - def test__good_bye_closes_conversation(self): - interface = DummyInterface( - to_utter=[ - "Goodbye.", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - self.assertTrue(True) - return - - self.assertTrue(False) - - def test__thank_you_closes_conversation(self): - interface = DummyInterface( - to_utter=[ - "Thank you.", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - self.assertTrue(True) - return - - self.assertTrue(False) - - def test__thanks_closes_conversation(self): - interface = DummyInterface( - to_utter=[ - "Thanks.", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - try: - asyncio.run(conversation_events.process_next()) - - except CloseConversation: - self.assertTrue(True) - return - - self.assertTrue(False) diff --git a/tests/test_questions.py b/tests/test_questions.py deleted file mode 100644 index 8b63f7fc..00000000 --- a/tests/test_questions.py +++ /dev/null @@ -1,52 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.events.narrator import Narrator -from wafl.simple_text_processing.questions import is_question -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.extractors.extractor import Extractor -from wafl.extractors.dataclasses import Query - -_wafl_example = """ -The user greets - SAY Hello there! - username = Are you fine? - SAY I am glad you are fine! - -""" - - -class TestQuestions(TestCase): - def test_are_question(self): - utterance = "Are you interested in this platter of fish" - assert is_question(utterance) - - def test_am_question(self): - utterance = "Am I interested in this platter of fish" - assert is_question(utterance) - - def test_yes_qa(self): - query = Query(text="Is the user satisfied with this", is_question=True) - user_answer = ( - "When asked 'is the user satisfied with this', the user says: 'yes I am.'" - ) - config = Configuration.load_local_config() - qa = Extractor(config, Narrator(DummyInterface)) - answer = asyncio.run(qa.extract(query, user_answer)) - - assert answer.is_true() - - def test_yes_or_no_questions_only_accept_positive_or_negative_replies(self): - interface = DummyInterface(["Hello", "Blue sky", "yes"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: I am glad you are fine!" - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_questions_in_prior_conversation.py b/tests/test_questions_in_prior_conversation.py deleted file mode 100644 index 046e4552..00000000 --- a/tests/test_questions_in_prior_conversation.py +++ /dev/null @@ -1,51 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger - -_path = os.path.dirname(__file__) - -_logger = LocalFileLogger() - -_wafl_example = """ - -The user greets - SAY the weather is very cold - SAY the temperature today is 0 celsius - -""".strip() - - -class TestAnswerInConversation(TestCase): - def test__temperature_is_remembered(self): - interface = DummyInterface(["Hello!", "What is the temperature today?"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "0" - print(interface.get_utterances_list()) - assert expected in interface.get_utterances_list()[-1] - - def test__random_name_is_remembered(self): - interface = DummyInterface(["My name is Albert", "What is my name"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example), interface=interface - ) - utterance = "Welcome to the website. How may I help you?" - interface.output(utterance) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "albert" - assert expected in interface.get_utterances_list()[-1].lower() diff --git a/tests/test_questions_to_statements.py b/tests/test_questions_to_statements.py deleted file mode 100644 index 446568ed..00000000 --- a/tests/test_questions_to_statements.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.narrator import Narrator -from wafl.simple_text_processing.questions import get_sentence_from_yn_question -from wafl.interface.dummy_interface import DummyInterface -from wafl.extractors.dataclasses import Query -from wafl.extractors.extractor import Extractor - -_path = os.path.dirname(__file__) - - -class TestQuestionsToStatements(TestCase): - def test__yn_question_is_translated_to_sentence1(self): - text = "is this bot doing well?" - prediction = get_sentence_from_yn_question(text) - expected = "this bot is doing well" - self.assertEqual(expected, prediction) - - def test__yn_question_is_translated_to_sentence2(self): - text = "are all the systems nominal?" - prediction = get_sentence_from_yn_question(text) - expected = "all the systems are nominal" - self.assertEqual(expected, prediction) - - def test__yn_question_is_translated_to_sentence3(self): - text = "Did Bob sell the house?" - prediction = get_sentence_from_yn_question(text) - expected = "Bob did sell the house" - self.assertEqual(expected, prediction) - - def test__yn_question_is_translated_to_sentence4(self): - text = "Did he sell the house?" - prediction = get_sentence_from_yn_question(text) - expected = "he did sell the house" - self.assertEqual(expected, prediction) - - def test__yn_questions_use_entailer_for_positive_answers(self): - text = "This bot is doing well" - query = Query("is this bot ok?", is_question=True) - config = Configuration.load_local_config() - qa = Extractor(config, Narrator(DummyInterface)) - prediction = asyncio.run(qa.extract(query, text)) - self.assertEqual("yes", prediction.text.lower()) diff --git a/tests/test_recursion_in_python_space.py b/tests/test_recursion_in_python_space.py deleted file mode 100644 index f676f85c..00000000 --- a/tests/test_recursion_in_python_space.py +++ /dev/null @@ -1,39 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_rules = """ -"Is the {linename} running?" - linename = which line do you want to check? - normname = normalize_name2(linename) - SAY {normname} - -""".strip() - -_path = os.path.dirname(__file__) - - -class TestRecursion(TestCase): - def test__recursion_when_normalizing_names(self): - interface = DummyInterface( - [ - "what line is running", - "the jubilee line", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _rules), interface=interface, code_path="/" - ) - - while asyncio.run(conversation_events.process_next()): - pass - - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == "bot: Jubilee" diff --git a/tests/test_reminders.py b/tests/test_reminders.py deleted file mode 100644 index 2f504260..00000000 --- a/tests/test_reminders.py +++ /dev/null @@ -1,74 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.events.generated_events import GeneratedEvents -from wafl.events.events_from_module_name import EventsCreatorFromModuleName -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) -_wafl_example = """ -the user wants to set an alarm at a specific time - time = At what time? - REMEMBER the time is {time} :- SAY Hello there!; SAY This rule was created - SAY An alarm was created for {time} - -the user wants to set an alarm in minutes from now - minutes_from_now = how many minutes from now? do not use the word 'minute' - time = get_time_in_future(minutes_from_now) - REMEMBER the time is {time} :- SAY Hello there!; SAY This rule was created - SAY An alarm was created in {minutes_from_now} -""" - - -class TestReminders(TestCase): - def test__time_reminder_can_be_set(self): - interface = DummyInterface() - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example) - generated_events = GeneratedEvents( - config, - knowledge, - events=EventsCreatorFromModuleName("events"), - interface=interface, - ) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - ) - input_from_user = "I want an alarm for 7,05" - asyncio.run(conversation_events._process_query(input_from_user)) - expected = "bot: An alarm was created for 7:05" - assert interface.get_utterances_list()[-1] == expected - - asyncio.run(generated_events.process_next()) - expected = ["bot: Hello there!", "bot: This rule was created"] - assert interface.get_utterances_list()[-2:] == expected - - def test__time_reminder_can_be_set_1_minute_from_now(self): - interface = DummyInterface() - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example) - generated_events = GeneratedEvents( - config, - knowledge, - events=EventsCreatorFromModuleName("events"), - interface=interface, - ) - conversation_events = ConversationEvents( - knowledge, interface=interface, code_path="/" - ) - input_from_user = "I want to set an alarm in one minute" - asyncio.run(conversation_events._process_query(input_from_user)) - expected = "bot: An alarm was created in 1 minute" - assert interface.get_utterances_list()[-1] == expected - - while not asyncio.run(generated_events.process_next()): - pass - - expected = ["bot: Hello there!", "bot: This rule was created"] - assert interface.get_utterances_list()[-2:] == expected diff --git a/tests/test_retrieval.py b/tests/test_retrieval.py deleted file mode 100644 index fb2e3c30..00000000 --- a/tests/test_retrieval.py +++ /dev/null @@ -1,98 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger -from wafl.retriever.string_retriever import StringRetriever -from wafl.retriever.dense_retriever import DenseRetriever - -_logger = LocalFileLogger() -_wafl_remember_rules = """ -the user is tall - -the user has brown hair - -the user has green eyes - - -the user wants the bot to remember something - sentence = What piece of information do you want me to remember? - REMEMBER sentence - SAY I will remember that {sentence} - - -"how is the user like" - user_qualities = RETRIEVE how is the user like - SAY {user_qualities} - -""" - - -class TestRetrieval(TestCase): - def test_retrieval(self): - config = Configuration.load_local_config() - retriever = DenseRetriever("text_embedding_model", config) - sentences = ["this is a test", "the food is hot on the table"] - for index, sentence in enumerate(sentences): - asyncio.run(retriever.add_text_and_index(sentence, str(index))) - - query = "the food is warm" - expected = "1" - predicted = asyncio.run(retriever.get_indices_and_scores_from_text(query)) - assert predicted[0][0] == expected - - def test_retrieval_from_rules(self): - config = Configuration.load_local_config() - interface = DummyInterface(to_utter=["tell me how the user is like"]) - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_remember_rules, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert len(interface.get_utterances_list()) == 4 - - def test_exact_string_retrieval(self): - retriever = StringRetriever() - sentences = [ - "this is a test", - "the food is hot on the table", - "_this is an exact string", - ] - for index, sentence in enumerate(sentences): - asyncio.run(retriever.add_text_and_index(sentence, str(index))) - - query = "_this is an exact string" - expected = "2" - predicted = asyncio.run(retriever.get_indices_and_scores_from_text(query)) - assert predicted[0][0] == expected - - def test_short_text_retrieves_nothing(self): - config = Configuration.load_local_config() - retriever = DenseRetriever("msmarco-distilbert-base-v3", config) - sentences = ["The user greets"] - for index, sentence in enumerate(sentences): - asyncio.run(retriever.add_text_and_index(sentence, str(index))) - - query = "O uh" - expected = [] - predicted = asyncio.run(retriever.get_indices_and_scores_from_text(query)) - assert predicted == expected - - def test_input_during_inference(self): - interface = DummyInterface(to_utter=["Please remember that my name is Alberto"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_remember_rules, logger=_logger), - interface=interface, - logger=_logger, - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: I will remember that that your name is alberto" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_rules.py b/tests/test_rules.py new file mode 100644 index 00000000..619e3cd9 --- /dev/null +++ b/tests/test_rules.py @@ -0,0 +1,50 @@ +import asyncio + +from unittest import TestCase + +from wafl.config import Configuration +from wafl.events.conversation_events import ConversationEvents +from wafl.interface.dummy_interface import DummyInterface + +wafl_example = """ +rules: + - the user says "my name is bob": + - You must answer the user by writing "the horse is tall" + + - the user says their name: + - reply casually to the conversation" +""" + + +class TestRules(TestCase): + def test__rules_can_be_triggered(self): + interface = DummyInterface( + to_utter=[ + "my name is bob", + ] + ) + config = Configuration.load_local_config() + config.set_value("rules", wafl_example) + conversation_events = ConversationEvents( + config=config, + interface=interface, + ) + asyncio.run(conversation_events.process_next()) + expected = "bot: the horse is tall" + self.assertEqual(expected, interface.get_utterances_list()[-1]) + + def test__rules_are_not_always_triggered(self): + interface = DummyInterface( + to_utter=[ + "my name is Frank", + ] + ) + config = Configuration.load_local_config() + config.set_value("rules", wafl_example) + conversation_events = ConversationEvents( + config=config, + interface=interface, + ) + asyncio.run(conversation_events.process_next()) + unexpected = "bot: the horse is tall" + self.assertNotEqual(unexpected, interface.get_utterances_list()[-1]) diff --git a/tests/test_rules_creation.py b/tests/test_rules_creation.py deleted file mode 100644 index 73838aaf..00000000 --- a/tests/test_rules_creation.py +++ /dev/null @@ -1,37 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) -_wafl_example = """ -the user wants to create a new rule - REMEMBER the user says "hello" :- SAY Hello there!; SAY This rule was created - SAY A rule was created -""" - - -class TestRulesCreation(TestCase): - def test__one_line_rule_can_be_created(self): - interface = DummyInterface() - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example), - interface=interface, - ) - input_from_user = "I need you to create a new rule" - asyncio.run(conversation_events._process_query(input_from_user)) - expected = "bot: A rule was created" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - interface.reset_history() - input_from_user = "Hello" - asyncio.run(conversation_events._process_query(input_from_user)) - expected = ["bot: Hello there!", "bot: This rule was created"] - assert interface.get_utterances_list()[-2:] == expected diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py deleted file mode 100644 index 0e5cc44e..00000000 --- a/tests/test_scheduler.py +++ /dev/null @@ -1,153 +0,0 @@ -import asyncio -import os - -from unittest import TestCase -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.events.generated_events import GeneratedEvents -from wafl.events.events_from_function_list import EventsCreatorFromFunctionList -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.project_knowledge import ProjectKnowledge -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger -from wafl.scheduler.conversation_loop import ConversationLoop -from wafl.scheduler.generated_event_loop import GeneratedEventLoop -from wafl.scheduler.scheduler import Scheduler - -_path = os.path.dirname(__file__) -_logger = LocalFileLogger() - - -def return_four_past_seven(): - return "The time is 7,04" - - -_wafl_example = """ -It is 7,04 - SAY It's 4 past seven! - keyboard_interrupt() - -the user greets - keyboard_interrupt() -""".strip() - - -class TestScheduler(TestCase): - def test__conversation_loop_can_run(self): - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl", logger=_logger) - interface = DummyInterface(["hello!"]) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - config=config, - logger=_logger, - ) - conversation_loop = ConversationLoop( - interface, - conversation_events, - _logger, - activation_word="", - max_misses=3, - ) - asyncio.run(conversation_loop.run()) - expected = ["user: hello !", "bot: Good bye!"] - self.assertEqual(expected, interface.get_utterances_list()) - - def test__scheduler_can_run_with_conversation_loop(self): - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl", logger=_logger) - interface = DummyInterface(["hello!"]) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - config=config, - logger=_logger, - ) - conversation_loop = ConversationLoop( - interface, - conversation_events, - _logger, - activation_word="", - ) - scheduler = Scheduler([conversation_loop]) - scheduler.run() - expected = ["user: hello !", "bot: Good bye!"] - self.assertEqual(expected, interface.get_utterances_list()) - - async def test__scheduler_can_run_with_conversation_and_event_loop__no_trigger_from_events( - self, - ): - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl", logger=_logger) - interface = DummyInterface(["hello!"]) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - config=config, - logger=_logger, - ) - conversation_loop = ConversationLoop( - interface, - conversation_events, - _logger, - activation_word="", - ) - event_loop = GeneratedEventLoop( - interface, - GeneratedEvents( - knowledge, - events=EventsCreatorFromFunctionList([return_four_past_seven]), - interface=interface, - code_path=knowledge.get_dependencies_list(), - logger=_logger, - ), - logger=_logger, - ) - async with asyncio.timeout(3): - scheduler = Scheduler([conversation_loop, event_loop]) - scheduler.run() - expected = ["user: hello !", "bot: Good bye!"] - - self.assertEqual(expected, interface.get_utterances_list()) - - async def test__scheduler_can_run_with_conversation_and_event_loop__does_trigger_from_events( - self, - ): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example, logger=_logger) - interface = DummyInterface(["hello!"]) - conversation_events = ConversationEvents( - knowledge, - interface=interface, - config=config, - code_path="/", - logger=_logger, - ) - conversation_loop = ConversationLoop( - interface, - conversation_events, - _logger, - activation_word="", - ) - event_loop = GeneratedEventLoop( - interface, - GeneratedEvents( - config, - knowledge, - events=EventsCreatorFromFunctionList([return_four_past_seven]), - code_path="/", - interface=interface, - logger=_logger, - ), - logger=_logger, - ) - async with asyncio.timeout(3): - scheduler = Scheduler([event_loop, conversation_loop]) - scheduler.run() - expected = ["bot: It's 4 past seven!", "user: hello !", "bot: Good bye!"] - - self.assertEqual(expected, interface.get_utterances_list()) diff --git a/tests/test_shopping_example.py b/tests/test_shopping_example.py deleted file mode 100644 index b9a99c37..00000000 --- a/tests/test_shopping_example.py +++ /dev/null @@ -1,94 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.extractors.dataclasses import Query - -_path = os.path.dirname(__file__) - -_rules = """ - -item = what does the user want to add to the shopping list? - add_shopping_list(item) - SAY {item} has been added to the list - ! _ask_another_item - -item = what does the user want to add to the shopping list? - add_shopping_list(item) - !{item} can be part of a shopping list - SAY Please speak more clearly - -item = what does the user want to remove from the shopping list? - remove_from_shopping_list(item) - items = get_shopping_list_in_english() - -_ask_another_item - does the user want to add another item - item = what do you want to add to the shopping list - add_shopping_list(item) - SAY {item} has been added to the list - _ask_another_item - -_ask_another_item - does the user want to add another item - item = what do you want to add to the shopping list - !{item} can be part of a shopping list - SAY {item} cannot be part of a shopping list - _ask_another_item - -"remove an item from the shopping list" - item = What do you want to remove? - remove_from_shopping_list(item) - items = get_shopping_list_in_english() - -the user wants to delete the shopping list - Do you want to delete the current shopping list - reset_shopping_list() - SAY The shopping list has been deleted - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -"What should I buy" - the user wants to know what is in the shopping list - -""" - - -class TestShoppingList(TestCase): - def test_no_activation(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _rules) - results = asyncio.run( - knowledge.ask_for_rule_backward( - Query( - text="The user says: 'remove apples from the shopping list.'", - is_question=False, - ) - ) - ) - assert len(results) == 2 - - def test_second_rule_is_not_run_if_prior_clause_fails(self): - interface = DummyInterface( - [ - "add apples to the shopping list", - "no", - "remove apples from the shopping list", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _rules), interface=interface, code_path="/" - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - output = "\n".join(interface.get_utterances_list()) - assert output.count("Do you want to remove apples from the shopping list") == 1 diff --git a/tests/test_task_creation.py b/tests/test_task_creation.py deleted file mode 100644 index 39879846..00000000 --- a/tests/test_task_creation.py +++ /dev/null @@ -1,171 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.connectors.bridges.llm_code_creator_bridge import LLMCodeCreatorBridge -from wafl.connectors.bridges.llm_task_creator_bridge import LLMTaskCreatorBridge -from wafl.events.conversation_events import ConversationEvents -from wafl.extractors.code_creator import CodeCreator -from wafl.extractors.task_creator import TaskCreator -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.project_knowledge import ProjectKnowledge -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_wafl_example = """ - -The user wants to know the road to somewhere - SAY Looking now - -The user wants to know the weather - SAY The temperature is going to be between 19 and 22 - SAY The probability of rain is 95% - -""".strip() - - -class TestTaskCreation(TestCase): - def test__task_creation_connector(self): - config = Configuration.load_local_config() - connector = LLMTaskCreatorBridge(config) - task = "the user wants to go swimming in the sea" - triggers = "\n".join( - [ - "- the user wants to know the road to somewhere", - "- the user wants to say hello", - ] - ) - prediction = asyncio.run(connector.get_answer("", triggers, task)) - expected = """ -the user wants to go swimming in the sea - location = where do you want to go? - result = Answer the following question. How to get to {location}? - SAY This is the road to {location}: {result} - """.strip() - print(prediction) - assert expected == prediction - - def test__task_creation1(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example) - task_creator = TaskCreator(config, knowledge) - task = "the user wants to go swimming in the sea at brighton beach" - answer = asyncio.run(task_creator.extract(task)) - expected = """ -the user wants to go swimming in the sea at brighton beach - road_to_brighton_beach = the user wants to know the road to brighton beach - weather = the user wants to know the weather - result = Answer the following question given this road and weather: {road_to_brighton_beach}. How to get to brighton beach? - SAY {result} - """.strip() - print(answer.text) - assert expected == answer.text.strip() - - def test__task_creation2(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example) - task_creator = TaskCreator(config, knowledge) - task = "the user wants to know if they need and umbrella" - answer = asyncio.run(task_creator.extract(task)) - expected = """ -the user wants to know if they need an umbrella - weather_forecast = the user wants to know the weather - result = Answer the following question given this forecast: {weather_forecast}. Do you need an umbrella? - SAY {result} - """.strip() - print(answer.text) - assert expected == answer.text.strip() - - def test__task_creation3(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example) - task_creator = TaskCreator(config, knowledge) - task = "the user wants to go from London to Manchester" - answer = asyncio.run(task_creator.extract(task)) - expected = """ -the user wants to go from London to Manchester - road_to_manchester = the user wants to know the road to Manchester - result = Answer the following question given this road: {road_to_manchester}. How to get to Manchester? - SAY {result} - """.strip() - print(answer.text) - assert expected == answer.text.strip() - - def test__task_is_created_from_conversation(self): - interface = DummyInterface(to_utter=["Tell me if I need an umbrella"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - ProjectKnowledge.create_from_string( - config, _wafl_example, knowledge_name="/" - ), - interface=interface, - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: The probability of rain is 95%. you should bring an umbrella." - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test__code_creation(self): - config = Configuration.load_local_config() - connector = LLMCodeCreatorBridge(config) - task = "connect to 'localhost:port' and return the data as json" - function_shape = "json_data = connect_to_localhost(port)" - prediction = asyncio.run(connector.get_answer("", function_shape, task)) - expected = """ -def connect_to_localhost(port): - import socket - - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect((socket.gethostname(), port)) - return s.recv(1024).decode('utf-8') - """.strip() - print(prediction) - assert expected == prediction - - def test__task_creation_with_function(self): - config = Configuration.load_local_config() - knowledge = SingleFileKnowledge(config, _wafl_example) - code_creator = CodeCreator(config, knowledge) - task = "folders_list = list_subfolders('/var/') " - answer = asyncio.run(code_creator.extract(task)) - expected = """ -def list_subfolders(folder_name): - import os - - subfolders = [] - for subfolder in os.listdir(folder_name): - if os.path.isdir(os.path.join(folder_name, subfolder)): - subfolders.append(subfolder) - return subfolders - """.strip() - print(answer.text) - assert expected == answer.text.strip() - - def test__task_is_created_from_conversation_with_code(self): - interface = DummyInterface(to_utter=["Please list of the subfolders of /var"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - ProjectKnowledge.create_from_string( - config, _wafl_example, knowledge_name="/" - ), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert "bot: tmp" in [item.lower() for item in interface.get_utterances_list()] - assert "bot: lib" in [item.lower() for item in interface.get_utterances_list()] - - def test__math_task_is_created_from_conversation(self): - interface = DummyInterface(to_utter=["Multiply 100 and 43"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - ProjectKnowledge.create_from_string( - config, _wafl_example, knowledge_name="/" - ), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - assert "4300" in interface.get_utterances_list()[-1] diff --git a/tests/test_task_extraction.py b/tests/test_task_extraction.py deleted file mode 100644 index 1be7edcc..00000000 --- a/tests/test_task_extraction.py +++ /dev/null @@ -1,71 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.extractors.task_extractor import TaskExtractor -from wafl.interface.dummy_interface import DummyInterface - - -class TestTaskExtraction(TestCase): - def test__dialogue_extractor1(self): - interface = DummyInterface() - interface._utterances = [ - [0, "user: what time is it"], - ] - config = Configuration.load_local_config() - task_extractor = TaskExtractor(config, interface) - prediction = asyncio.run(task_extractor.extract("")).text - expected = "wants to know the time" - print(prediction) - assert expected in prediction - - def test__dialogue_extractor2(self): - interface = DummyInterface() - interface._utterances = [ - [0, "user: hello what time is it"], - [1, "bot: hello there"], - [2, "bot: The time is 20 past 13"], - [3, "user: what is in the shopping list"], - ] - config = Configuration.load_local_config() - task_extractor = TaskExtractor(config, interface) - prediction = asyncio.run(task_extractor.extract("")).text - expected = "the user wants to know what is in the shopping list" - assert expected == prediction - - def test__dialogue_extractor3(self): - interface = DummyInterface() - interface._utterances = [ - [0, "user: hello what time is it"], - [1, "bot: hello there"], - [2, "bot: The time is 20 past 13"], - [3, "user: what is in the shopping list"], - [4, "bot: the shopping list contains milk, bread, and eggs"], - [5, "user: is the circle line running"], - [6, "bot: yes, it is running normally"], - [7, "user: what about the Jubilee line"], - ] - config = Configuration.load_local_config() - task_extractor = TaskExtractor(config, interface) - prediction = asyncio.run(task_extractor.extract("")).text - expected = "the user wants to know if the Jubilee line is running" - print(prediction) - assert expected == prediction - - def test__no_task_present_is_predicted_as_unknown(self): - interface = DummyInterface() - interface._utterances = [ - [0, "user: hello what time is it"], - [1, "bot: hello there"], - [2, "bot: The time is 20 past 13"], - [3, "user: what is in the shopping list"], - [4, "bot: the shopping list contains milk, bread, and eggs"], - [5, "user: you"], - ] - config = Configuration.load_local_config() - task_extractor = TaskExtractor(config, interface) - prediction = asyncio.run(task_extractor.extract("")).text - expected = "unknown" - print(prediction) - assert expected == prediction diff --git a/tests/test_task_lists.py b/tests/test_task_lists.py deleted file mode 100644 index 7edfa966..00000000 --- a/tests/test_task_lists.py +++ /dev/null @@ -1,37 +0,0 @@ -import asyncio -import os - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge - -_path = os.path.dirname(__file__) -wafl_example = """ - -The user asks about the weather - SAY the sun is shining - -The user wants delete something - SAY Item removed - -""" - - -class TestTaskList(TestCase): - def test__double_command_is_executed(self): - interface = DummyInterface( - to_utter=["tell me about the weather and then delete the apples"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), interface=interface - ) - asyncio.run(conversation_events.process_next()) - expected = "the sun is shining" - assert expected in interface.get_utterances_list()[-2].lower() - - expected = "item removed" - assert expected in interface.get_utterances_list()[-1].lower() diff --git a/tests/test_task_memory.py b/tests/test_task_memory.py deleted file mode 100644 index 4dce3001..00000000 --- a/tests/test_task_memory.py +++ /dev/null @@ -1,220 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.events.task_memory import TaskMemory -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.logger.local_file_logger import LocalFileLogger - -wafl_example = """ - -The user says or wants to say hello - name = What is the user's name - SAY Hello, {name}! - -item = what does the user want to add to the shopping list? - reset_shopping_list() - shopping_list.append(item) - SAY {item} has been added to the list - ! _ask_another_item - -_ask_another_item - does the user want to add another item - item = what do you want to add - SAY {item} has been added to the shopping list - _ask_another_item - -""" - -memory_example = """ - -The user says hello - name = What is the user's name - SAY Hello, {name}! - -the user wants to know what is in the shopping list - SAY the shopping list contains: nothing - -item = what does the user want to add to the shopping list? - reset_shopping_list() - shopping_list.append(item) - SAY {item} has been added to the shopping list - -the user wants to add something - item = what does the user want to add? - list_name = which list does the user want to add things to? - add {item} to {list_name} - -""" - - -class TestTaskMemory(TestCase): - def test__task_memory_class(self): - task_memory = TaskMemory() - task_memory.add_question("What is the color of Bob's dress") - task_memory.add_answer("Red") - task_memory.add_question("Who is talking") - prediction = task_memory.get_discussion().strip() - expected = """ -Q: What is the color of Bob's dress -A: Red -Q: Who is talking -A: - """.strip() - assert prediction == expected - - def test__executables(self): - interface = DummyInterface( - to_utter=[ - "Hello, my name is Bob", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - logger=LocalFileLogger(), - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Hello, bob!" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test__hello_does_not_get_into_task_memory(self): - interface = DummyInterface(to_utter=["hello", "Albert"]) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Hello, albert!" - assert interface.get_utterances_list()[-1] == expected - - def test__task_memory_does_not_propagate_down_for_depth2(self): - interface = DummyInterface( - to_utter=[ - "Add apples to the shopping list", - "yes", - "bananas", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Bananas has been added to the shopping list" - assert interface.get_utterances_list()[-3] == expected - - def test__task_memory_does_not_propagate_down_for_depth3(self): - interface = DummyInterface( - to_utter=[ - "Add apples to the shopping list", - "yes", - "pineapple", - "yes", - "bananas", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Bananas has been added to the shopping list" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-3] == expected - - def test__task_memory_works_for_yes_questions(self): - interface = DummyInterface( - to_utter=[ - "Add apples to the shopping list", - "yes bananas", - "no", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, wafl_example), - interface=interface, - code_path="/", - logger=LocalFileLogger(), - ) - asyncio.run(conversation_events.process_next()) - expected = "bot: Bananas has been added to the shopping list" - assert interface.get_utterances_list()[-3] == expected - - def test__prior_list_name_is_remembered(self): - interface = DummyInterface( - to_utter=[ - "Add apples to the shopping list", - "add bananas as well", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, memory_example), - interface=interface, - code_path="/", - logger=LocalFileLogger(), - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "bot: Bananas has been added to the shopping list" - print(interface.get_utterances_list()) - assert interface.get_utterances_list()[-1] == expected - - def test__prior_list_name_is_remembered_second_time(self): - interface = DummyInterface( - to_utter=[ - "add tangerines to the shopping list", - "add bananas to the shopping list", - "ok now add apples", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, memory_example), - interface=interface, - code_path="/", - logger=LocalFileLogger(), - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - print(interface.get_utterances_list()) - expected = "bot: Apples has been added to the shopping list" - assert interface.get_utterances_list()[-1] == expected - - def test__prior_list_name_is_remembered_second_time_for_coffee_filters(self): - interface = DummyInterface( - to_utter=[ - "What's in the shopping list?", - "ok add apples.", - "add coffee filters", - ] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, memory_example), - interface=interface, - code_path="/", - logger=LocalFileLogger(), - ) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - asyncio.run(conversation_events.process_next()) - expected = "bot: Coffee filters has been added to the shopping list" - assert interface.get_utterances_list()[-1] == expected diff --git a/tests/test_testcases.py b/tests/test_testcases.py deleted file mode 100644 index 131338d3..00000000 --- a/tests/test_testcases.py +++ /dev/null @@ -1,104 +0,0 @@ -import asyncio - -from unittest import TestCase - -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.parsing.testcase_parser import get_user_and_bot_lines_from_text -from wafl.testcases import ConversationTestCases - -_wafl_greetings = """ - -The user greets - SAY Hello there! - username = What is the user's name - REMEMBER the user is called {username} - REMEMBER the user's name is {username} - SAY Nice to meet you, {username}! - -""".strip() - -_test_case_greetings = """ - -test the greetings work - user: Hello - bot: Hello there! - bot: What is your name - user: Bob - bot: Nice to meet you, bob! - -! test the greetings uses the correct name - user: Hello - bot: Hello there! - bot: What is your name - user: Bob - bot: Nice to meet you, unknown! - - -""".strip() - - -class TestConversationalTestCases(TestCase): - def test_that_test_case_is_parsed_correctly_with_user_and_bot_present(self): - dialogue_data = get_user_and_bot_lines_from_text(_test_case_greetings) - assert list(dialogue_data["test the greetings work"].keys()) == [ - "bot_lines", - "user_lines", - "lines", - "negated", - ] - - def test_that_test_case_is_parsed_correctly_with_correct_dialogues(self): - dialogue_data = get_user_and_bot_lines_from_text(_test_case_greetings) - predicted_for_user = ["Hello", "Bob"] - predicted_for_bot = [ - "Hello there!", - "What is your name", - "Nice to meet you, bob!", - ] - assert ( - dialogue_data["test the greetings work"]["user_lines"] == predicted_for_user - ) - assert ( - dialogue_data["test the greetings work"]["bot_lines"] == predicted_for_bot - ) - - def test_greeting_goes_as_planned(self): - dialogue_data = get_user_and_bot_lines_from_text(_test_case_greetings) - interface = DummyInterface( - dialogue_data["test the greetings work"]["user_lines"] - ) - config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_greetings), interface=interface - ) - asyncio.run(conversation_events.process_next()) - assert [ - item.replace("bot: ", "") - for item in interface.get_utterances_list() - if "bot:" in item - ] == dialogue_data["test the greetings work"]["bot_lines"] - - def test_conversation_testcase_single_test_success(self): - config = Configuration.load_local_config() - testcase = ConversationTestCases( - config, _test_case_greetings, SingleFileKnowledge(config, _wafl_greetings) - ) - assert testcase.test_single_case("test the greetings work") - - def test_conversation_testcase_single_test_failure(self): - new_test_case = _test_case_greetings.replace("Bob", "Albert") - config = Configuration.load_local_config() - testcase = ConversationTestCases( - config, new_test_case, SingleFileKnowledge(config, _wafl_greetings) - ) - assert not asyncio.run(testcase.test_single_case("test the greetings work")) - - def test_conversation_testcase_run_all(self): - config = Configuration.load_local_config() - testcase = ConversationTestCases( - config, _test_case_greetings, SingleFileKnowledge(config, _wafl_greetings) - ) - assert asyncio.run(testcase.run()) diff --git a/tests/test_voice.py b/tests/test_voice.py index 675ca421..a4abbd12 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -8,66 +8,48 @@ from wafl.interface.voice_interface import VoiceInterface from wafl.events.conversation_events import ConversationEvents from wafl.interface.dummy_interface import DummyInterface -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge from wafl.listener.whisper_listener import WhisperListener _wafl_example = """ - -the user says their name - SAY nice to meet you! - -the user name is Jane - +facts: + - This bot is doing well + +rules: + - the user's name is Jane: + - write "I hear you" """.strip() _path = os.path.dirname(__file__) class TestVoice(TestCase): - def test_activation(self): + def test__activation(self): interface = DummyInterface(to_utter=["computer", "my name is Jane"]) config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example), interface=interface - ) + config.set_value("rules", _wafl_example) + conversation_events = ConversationEvents(config=config, interface=interface) interface.activate() asyncio.run(conversation_events.process_next(activation_word="computer")) asyncio.run(conversation_events.process_next(activation_word="computer")) - assert len(interface.get_utterances_list()) == 3 + assert interface.get_utterances_list()[-1] == "bot: I hear you" - def test_no_activation(self): + def test__no_activation(self): interface = DummyInterface(to_utter=["my name is bob"]) config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example), interface=interface - ) + conversation_events = ConversationEvents(config=config, interface=interface) interface.deactivate() asyncio.run(conversation_events.process_next(activation_word="computer")) assert len(interface.get_utterances_list()) == 1 - def test_computer_name_is_removed_after_activation(self): + def test__computer_name_is_removed_after_activation(self): interface = DummyInterface(to_utter=["[computer] computer my name is bob"]) config = Configuration.load_local_config() - conversation_events = ConversationEvents( - SingleFileKnowledge(config, _wafl_example), interface=interface - ) + conversation_events = ConversationEvents(config=config, interface=interface) interface.deactivate() asyncio.run(conversation_events.process_next(activation_word="computer")) - print(interface.get_utterances_list()) assert interface.get_utterances_list()[-1].count("computer") == 0 - def test_hotwords_as_input(self): - config = Configuration.load_local_config() - interface = VoiceInterface(config) - asyncio.run( - interface.add_hotwords_from_knowledge( - SingleFileKnowledge(config, _wafl_example), count_threshold=1 - ) - ) - expected = ["jane", "name is", "is jane", "says", "says their", "their name"] - assert interface._listener._hotwords == expected - - def test_sound_file_is_translated_correctly(self): + def test__sound_file_is_translated_correctly(self): f = wave.open(os.path.join(_path, "data/1002.wav"), "rb") waveform = np.frombuffer(f.readframes(f.getnframes()), dtype=np.int16) / 32768 config = Configuration.load_local_config() @@ -77,7 +59,7 @@ def test_sound_file_is_translated_correctly(self): expected = "DELETE BATTERIES FROM THE GROCERY LIST" assert result == expected - def test_random_sounds_are_excluded(self): + def test__random_sounds_are_excluded(self): f = wave.open(os.path.join(_path, "data/random_sounds.wav"), "rb") waveform = np.frombuffer(f.readframes(f.getnframes()), dtype=np.int16) / 32768 config = Configuration.load_local_config() @@ -86,10 +68,13 @@ def test_random_sounds_are_excluded(self): expected = "[unclear]" assert result == expected - def test_voice_interface_receives_config(self): + 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") + 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") diff --git a/todo.txt b/todo.txt index 94f347c9..9808b652 100644 --- a/todo.txt +++ b/todo.txt @@ -1,10 +1,194 @@ ### TODO -* make system faster to load locally. Why does it load functions.py 4 times? why the long wait? -* add a small wafl_home to the init, with folders and everything -* rule creation for "hello" is terrible. Revise all rule creation +* 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 +/* nicer UI? +/ * New icons +/ * darker left bar +/* 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) + + +* redeploy locally and on the server +* new version on github + +* add rules for + / shopping lists + trains and music + + +* add yaml like in the github issue +* test testcases work (only local entailer) + +/* update wafl init: It should create the project the modern way. + +* use deci-lm +* make sure system works with audio too + +* aggregate rules into a tree using a rule builder (like in the old system) + * perhaps one use-case is for diary entries: what is my diary for next week requires today's date first + + + + + +/* I am not sure the system is cancelling code that has been executed. Check the whole pipeline of prior_functions + +/* when an import throws an exception, add import to the code and try again +/ * if the import does not exist, return the code as is without substitution + +/* don't use replicas, use a beam decoder where and are pushed upwards. (this means no sampling - perhaps there is a better way) +/ * do it in the local llm connector first +/ * use sequence_bias in generate() together with epsilon_cutoff +/ (for example if the token is not likely its prob should not be increased) + DOESN'T WORK: It needs to use beam search, but I want to keep sampling with temperature + +/ * ALTERNATIVELY increase the number of replicas to 6? + + +/* quantize the model to 4 bits + TOO SLOW on 3090 +/* merge remote and local llm connector. Both should derive from the same class with common functions +/**** make it so the computer does not repeat! reset conversation when the bot repeats itself + +/* only one rule at the time!! +/ * if a rule is executed, it is then consumed + +* bug: the system kept executing "The bot predicts:" + +**** what to do with conversational collapse? + - the system just repeats the last utterance + - how much is 2+2, what is the real name of bon jovi, how tall is mt everest + - the collapse is due to NUMBER being returing unknown (execute becomes more likely after one prior ) + - the system is also more likely to return unknown after one unknown. Select the answer that has no unknowns? + +* solve math expression execute (import do not work in eval and exec needs a print on stdout) +* add errors when loading config file (add log to stderr) +* add a memory that the execute command was called/not called. + +* no more than one rule (two rules it already gets confused) +* better ui +* better documentation +* better tests +# the dimension of the sentence emn model 384 should be in config + + + +* multi-step interactions for function execution are still hard. + - perhaps the rules need to stay in the prior discourse for longer + - the same rule appears after the first execution, therefore the bot thinks it has already executed it + - user: follow rules... + user: compute stuff + bot: answer + + user: compute stuff + bot: answer + user: follow rules... (this was at the beginning in the prior discourse) + user: do it again + + maybe you need a tag for user: follow rule. Maybe a superuser tag that is removed from the output (but stays in the interface)? + + + +#### keep prior rules for a couple of turns +#### log execute in green and memory in blue +#### keep the temp low 0.2 (otherwise it doesn't follow multi-point +# rules well) + +/* Put the answer of in the facts and re-compute the answer +/* fine-tune the llm to follow the rules +/ - create dataset of about 50 examples +/ - fine tune only last layer + This does not work + + +------------------------------------------------ + + +* use perplexity in arbiter to determine what to do + - use a perplexity budget? + + +* the answer filter is too fickle + - wrong transcriptions + - code is transcribed as prior text + - It needs examples!! => CREATE A LIST OF EXAMPLES TO BE RETRIEVED FOR THE FILTER + - examples: code -> code + - "this is what i came up with:" -> same + +* add commands to navigate the conversational tree + - go back (?) + - "I am asking you that question" + + +/* why is <|EOS|> added in the code generated by asking: "write a function in python" + - the error was in wafl_llm, every # was replaced with <|EOS|> (legacy from MPT) + +/* write a function in C++ does not trigger the rule about writing in a language different from python + - this is because the rules cannot specify when not to be activated: the entailer blocks the retrieval + write a function in c++ does not entail write a function that is not in python + +* write a function in python does not work + - the system stops at what is the name of the function, there is no reply after that + - this is because the system fails at "what is the goal of the function" + * IMPLEMENT A ASK USER, ASK BOT, GENERATE, VERIFY, otherwise it's just a string assignment + + - can you code does not retrieve anything + - the system creates a new rule, even if a good rule already exists + + +/* what is the weather like/what about tomorrow -> every new query gets a reply about the weather + +/* items are not updated in the web interface: +/ - new utterances by the bot are not added to the list +/ - they only appear after the user has typed something +/ - should you wait to update list? +/ - should you yield all conversations in the conversations events and then say them all at once? + +/* add a filter dialogue answerer on top of everything (top-answerer) !!!! +/ - add filter ability to all interfaces +/ - add filter to web interface when it runs from command line +------------------------------------------------ +* remember: entailment is related to mutual information. +* If the system generates a rule that has a question implying the trigger -> the answer to that question is unknown + + +* make it answer: who is the mayor of london, who was the first james bond + * find a way to re-load the knowledges from the answer bridges while the system is running + * make it so the system does not repeat questions that are in the trigger + * slim down the corpus task_creator.csv: + - do not repeat instructions for every item, just use instructions at the beginning. + +* solve issue about intermediate item taking the value of the prior textarea instead of "typing..." + +* implement notebook style web interface +/ * will you need to remove user: bot: from utterances? + * change the interface so you can navigate it + * allow for web components to write output + * test with output from matplotlib + +/* use temperature in generate: the system tends to repeat itself and is terrible. +/* avoid newline in textarea after pressing enter +/* sometimes (when I say "nothing/no/...") the conversation stops. + there is no answer and all the next replies are the queries themselves + + +/* why does the weather not work? + +/* make system faster to load locally. Why does it load functions.py 4 times? why the long wait? +/* add a small wafl_home to the init, with folders and everything /* make stand-alone connectors (no need for server-side) /* modify config for all connectors diff --git a/wafl/answerer/answerer_implementation.py b/wafl/answerer/answerer_implementation.py new file mode 100644 index 00000000..0bd2d446 --- /dev/null +++ b/wafl/answerer/answerer_implementation.py @@ -0,0 +1,18 @@ +def get_last_bot_utterances(dialogue_items, num_utterances): + utterances = [] + for item in reversed(dialogue_items): + if item[1].startswith("bot:"): + utterances.append(item[1].replace("bot:", "").strip()) + + if len(utterances) == num_utterances: + break + + return utterances + + +def get_last_user_utterance(dialogue_items): + for item in reversed(dialogue_items): + if item[1].startswith("user:"): + return item[1].replace("user:", "").strip() + + return "" diff --git a/wafl/answerer/arbiter_answerer.py b/wafl/answerer/arbiter_answerer.py deleted file mode 100644 index 80c97e07..00000000 --- a/wafl/answerer/arbiter_answerer.py +++ /dev/null @@ -1,130 +0,0 @@ -from wafl.answerer.base_answerer import BaseAnswerer -from wafl.answerer.dialogue_answerer import DialogueAnswerer -from wafl.answerer.inference_answerer import InferenceAnswerer -from wafl.config import Configuration -from wafl.events.narrator import Narrator -from wafl.extractors.entailer import Entailer -from wafl.extractors.task_extractor import TaskExtractor -from wafl.inference.backward_inference import BackwardInference -from wafl.inference.utils import answer_is_informative -from wafl.extractors.dataclasses import Answer, Query - - -class ArbiterAnswerer(BaseAnswerer): - def __init__(self, config, answerers_dict, knowledge, interface, logger): - self._answerers_dict = answerers_dict - self._narrator = Narrator(interface) - self._interface = interface - self._logger = logger - self._entailer = Entailer(config, logger) - self._knowledge = knowledge - self._task_extractor = TaskExtractor(config, interface) - self._config = Configuration.load_local_config() - - async def answer(self, query_text, policy): - simple_task = f"The user says: {query_text.capitalize()}" - task = await self._task_extractor.extract(simple_task) - if not task.is_neutral() and await self._knowledge.ask_for_rule_backward( - Query.create_from_text(task.text), - knowledge_name="/", - ): - simple_task += ". There is a rule for that request." - - elif await self._knowledge.ask_for_rule_backward( - Query.create_from_text(simple_task), - knowledge_name="/", - ): - simple_task += ". There is a rule for that request." - - else: - simple_task += ". There is no rule for that request." - - score = 1 - keys_and_scores = [] - for key in self._answerers_dict.keys(): - if len(self._answerers_dict) > 1: - score = await self._entailer.entails( - simple_task, - key, - return_threshold=True, - threshold=0.5, - ) - - keys_and_scores.append((key, score)) - - keys_and_scores = sorted(keys_and_scores, key=lambda x: -x[1]) - all_answers = [] - for key, _ in keys_and_scores: - answerer = self._answerers_dict[key] - answer = await answerer.answer(query_text, policy) - all_answers.append(answer) - if answer_is_informative(answer) and not answer.is_false(): - return answer - - if any(answer.is_false() for answer in all_answers): - return Answer(text="False") - - return Answer(text="Unknown") - - @staticmethod - def create_answerer(config, knowledge, interface, code_path, logger): - narrator = Narrator(interface) - return ArbiterAnswerer( - config, - { - "The user greets and there is no rule for that query": DialogueAnswerer( - config, knowledge, interface, logger - ), - "The user speaks about themselves and there is no rule for that query": DialogueAnswerer( - config, knowledge, interface, logger - ), - "The user makes small talk and there is no rule for that query": DialogueAnswerer( - config, knowledge, interface, logger - ), - "The user gives an order or request and there is a rule for that query": InferenceAnswerer( - config, - interface, - BackwardInference( - config, - knowledge, - interface, - narrator, - code_path, - logger=logger, - generate_rules=False, - ), - logger, - ), - "The user gives an order or request and there is no rule for that query": InferenceAnswerer( - config, - interface, - BackwardInference( - config, - knowledge, - interface, - narrator, - code_path, - logger=logger, - generate_rules=True, - ), - logger, - ), - "The user gives a command and and there is no rule for that query": InferenceAnswerer( - config, - interface, - BackwardInference( - config, - knowledge, - interface, - narrator, - code_path, - logger=logger, - generate_rules=True, - ), - logger, - ), - }, - knowledge, - interface, - logger, - ) diff --git a/wafl/answerer/base_answerer.py b/wafl/answerer/base_answerer.py index e1489d70..a999dd0f 100644 --- a/wafl/answerer/base_answerer.py +++ b/wafl/answerer/base_answerer.py @@ -1,3 +1,3 @@ class BaseAnswerer: - async def answer(self, query_text, policy): + async def answer(self, query_text: str) -> "Answer": raise NotImplementedError diff --git a/wafl/answerer/dialogue_answerer.py b/wafl/answerer/dialogue_answerer.py index efe43d9c..53dce11b 100644 --- a/wafl/answerer/dialogue_answerer.py +++ b/wafl/answerer/dialogue_answerer.py @@ -1,31 +1,40 @@ +import re import time +import traceback +from importlib import import_module +from inspect import getmembers, isfunction + +from wafl.answerer.answerer_implementation import ( + get_last_bot_utterances, + get_last_user_utterance, +) from wafl.answerer.base_answerer import BaseAnswerer from wafl.connectors.bridges.llm_chitchat_answer_bridge import LLMChitChatAnswerBridge +from wafl.exceptions import CloseConversation from wafl.extractors.dataclasses import Query, Answer -from wafl.inference.utils import cluster_facts +from wafl.simple_text_processing.questions import is_question class DialogueAnswerer(BaseAnswerer): - def __init__(self, config, knowledge, interface, logger): + def __init__(self, config, knowledge, interface, code_path, logger): self._bridge = LLMChitChatAnswerBridge(config) self._knowledge = knowledge self._logger = logger self._interface = interface - self._max_num_past_utterances = 7 + self._max_num_past_utterances = 5 + self._max_num_past_utterances_for_facts = 5 + self._max_num_past_utterances_for_rules = 0 + self._prior_facts_with_timestamp = [] + self._init_python_module(code_path.replace(".py", "")) + self._max_predictions = 3 - async def answer(self, query_text, policy): - print(__name__) + 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) - facts_and_thresholds = await self._knowledge.ask_for_facts_with_threshold( - query, is_from_user=True, knowledge_name="/", threshold=0.5 - ) - texts = cluster_facts(facts_and_thresholds) - for text in texts[::-1]: - await self._interface.add_fact(f"The bot remembers: {text}") + rules_texts = await self._get_relevant_rules(query) dialogue = self._interface.get_utterances_list_with_timestamp()[ -self._max_num_past_utterances : @@ -37,20 +46,153 @@ async def answer(self, query_text, policy): if not dialogue: dialogue = [(time.time(), f"user: {query_text}")] - facts = self._interface.get_facts_and_timestamp() - dialogue_items = dialogue + facts + dialogue_items = dialogue dialogue_items = sorted(dialogue_items, key=lambda x: x[0]) + 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] - dialogue_items = "\n".join(dialogue_items) - answer_text = await self._bridge.get_answer( - text="", - dialogue=dialogue_items, - query=query_text, + conversational_timestamp = len(dialogue_items) + facts = await self._get_relevant_facts( + query, + has_prior_rules=bool(rules_texts), + conversational_timestamp=conversational_timestamp, ) + dialogue_items = "\n".join(dialogue_items) + + for _ in range(self._max_predictions): + original_answer_text = await self._bridge.get_answer( + text=facts, + dialogue=dialogue_items, + query=rules_texts, + ) + await self._interface.add_fact(f"The bot predicts: {original_answer_text}") + ( + answer_text, + memories, + ) = await self._substitute_memory_in_answer_and_get_memories_if_present( + await self._substitute_results_in_answer(original_answer_text) + ) + if answer_text in last_bot_utterances: + dialogue_items = last_user_utterance + continue + + if not memories: + break + + facts += "\n" + "\n".join(memories) + dialogue_items += f"\nbot: {original_answer_text}" + if self._logger: self._logger.write(f"Answer within dialogue: The answer is {answer_text}") - if await policy.accept(answer_text): - return Answer.create_from_text(answer_text) + return Answer.create_from_text(answer_text) + + async def _get_relevant_facts( + self, query, has_prior_rules, conversational_timestamp + ): + memory = "\n".join([item[0] for item in self._prior_facts_with_timestamp]) + self._prior_facts_with_timestamp = [ + item + for item in self._prior_facts_with_timestamp + if item[1] + > 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 + ) + if facts_and_thresholds: + 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]) + + else: + if is_question(query.text) and not has_prior_rules: + memory += ( + f"\nThe answer to {query.text} is not in the knowledge base." + "The bot can answer the question while informing the user that the answer was not retrieved" + ) + + if has_prior_rules: + memory += f"\nThe bot tries to answer {query.text} following the rules from the user." + + 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) + + def _init_python_module(self, module_name): + self._module = import_module(module_name) + self._functions = [item[0] for item in getmembers(self._module, isfunction)] + + async def _substitute_results_in_answer(self, answer_text): + matches = re.finditer(r"(.*?)", answer_text, re.DOTALL) + for match in matches: + to_execute = match.group(1) + result = await self._run_code(to_execute) + answer_text = answer_text.replace(match.group(0), result) + + return answer_text + + async def _substitute_memory_in_answer_and_get_memories_if_present( + self, answer_text + ): + matches = re.finditer(r"(.*?)", answer_text, re.DOTALL) + memories = [] + for match in matches: + to_execute = match.group(1) + answer_text = answer_text.replace(match.group(0), "") + memories.append(to_execute) + + return answer_text, memories + + async def _run_code(self, to_execute): + result = None + for _ in range(3): + try: + if any(item + "(" in to_execute for item in self._functions): + result = eval(f"self._module.{to_execute}") + break + + else: + ldict = {} + exec(to_execute, globals(), ldict) + if "result" in ldict: + result = str(ldict["result"]) + break + + except NameError as e: + match = re.search(r"\'(\w+)\' is not defined", str(e)) + if match: + to_import = match.group(1) + to_execute = f"import {to_import}\n{to_execute}" + + except CloseConversation as e: + raise e + + except Exception as e: + result = ( + f'Error while executing\n\n"""python\n{to_execute}\n"""\n\n{str(e)}' + ) + traceback.print_exc() + break + + if not result: + result = f'\n"""python\n{to_execute}\n"""' - return Answer.create_neutral() + return result diff --git a/wafl/answerer/fact_answerer.py b/wafl/answerer/fact_answerer.py deleted file mode 100644 index 266bfaeb..00000000 --- a/wafl/answerer/fact_answerer.py +++ /dev/null @@ -1,45 +0,0 @@ -from wafl.answerer.base_answerer import BaseAnswerer -from wafl.extractors.dataclasses import Query -from wafl.extractors.extractor import Extractor -from wafl.inference.utils import cluster_facts - - -class FactAnswerer(BaseAnswerer): - def __init__(self, knowledge, narrator, logger): - self._knowledge = knowledge - self._logger = logger - self._narrator = narrator - self._extractor = Extractor(narrator, logger) - - async def answer(self, query_text, policy): - if self._logger: - self._logger.write(f"Fact Answerer: the query is {query_text}") - - query = Query.create_from_text(query_text) - facts_and_thresholds = await self._knowledge.ask_for_facts_with_threshold( - query, is_from_user=True, knowledge_name="/" - ) - texts = cluster_facts(facts_and_thresholds) - for text in texts: - if self._logger: - self._logger.write(f"Answer within facts: The query is {query_text}") - self._logger.write(f"Answer within facts: The context is {text}") - - text = self._narrator.get_context_for_facts(text) - answer = await self._extractor.extract(query, text) - if self._logger: - self._logger.write(f"Answer within facts: The answer is {answer.text}") - - if await policy.accept(answer.text): - return answer - - text = self._narrator.summarize_dialogue() - if self._logger: - self._logger.write( - f"SimpleAnswerer: The context is {text}", self._logger.level.INFO - ) - self._logger.write( - f"SimpleAnswerer: The query is {query_text}", self._logger.level.INFO - ) - - return await self._extractor.extract(Query(query_text, is_question=True), text) diff --git a/wafl/answerer/generated_answerer.py b/wafl/answerer/generated_answerer.py deleted file mode 100644 index 2b37d14b..00000000 --- a/wafl/answerer/generated_answerer.py +++ /dev/null @@ -1,29 +0,0 @@ -from wafl.answerer.base_answerer import BaseAnswerer -from wafl.connectors.bridges.llm_generated_answer_bridge import LLMGeneratedAnswerBridge -from wafl.extractors.dataclasses import Answer -from wafl.extractors.entailer import Entailer - - -class GeneratedAnswerer(BaseAnswerer): - def __init__(self, narrator, logger): - self._logger = logger - self._narrator = narrator - self._connector = LLMGeneratedAnswerBridge() - self._entailer = Entailer(logger) - - async def answer(self, query_text, policy): - if self._logger: - self._logger.write(f"Generated Answerer: the query is {query_text}") - - answer_text = await self._connector.get_answer( - text="", dialogue=None, query=query_text - ) - if self._logger: - self._logger.write(f"Generated Answerer: the answer is {answer_text}") - - if await self._entailer.is_neutral( - self._narrator.summarize_dialogue(), answer_text - ) and await policy.accept(answer_text): - return Answer(text=answer_text) - - return Answer.create_neutral() diff --git a/wafl/answerer/inference_answerer.py b/wafl/answerer/inference_answerer.py deleted file mode 100644 index 48172697..00000000 --- a/wafl/answerer/inference_answerer.py +++ /dev/null @@ -1,96 +0,0 @@ -from wafl.answerer.base_answerer import BaseAnswerer -from wafl.events.narrator import Narrator -from wafl.events.task_memory import TaskMemory -from wafl.extractors.entailer import Entailer -from wafl.extractors.task_extractor import TaskExtractor -from wafl.simple_text_processing.questions import is_question -from wafl.extractors.dataclasses import Query, Answer -from wafl.extractors.extractor import Extractor - - -class InferenceAnswerer(BaseAnswerer): - def __init__(self, config, interface, inference, logger): - self._qa = Extractor(config, logger) - self._logger = logger - self._narrator = Narrator(interface) - self._interface = interface - self._inference = inference - self._task_extractor = TaskExtractor(config, interface) - self._entailer = Entailer(config, logger) - - async def answer(self, query_text, policy): - prior_conversation = self._narrator.summarize_dialogue() - if self._logger: - self._logger.write( - f"InferenceAnswerer: The context is {prior_conversation}", - self._logger.level.INFO, - ) - self._logger.write( - f"InferenceAnswerer: The query is {query_text}", self._logger.level.INFO - ) - - simple_task = f"The user says: '{query_text.capitalize()}'" - task_answer = await self._task_extractor.extract(simple_task) - if task_answer.is_neutral(): - task = simple_task - - else: - task = task_answer.text - - task_texts = split_tasks(task) - answers = [] - for task_text in task_texts: - result = await self._entailer.entails( - simple_task, task_text, return_threshold=True, threshold=0.6 - ) - if not result: - task_text = simple_task - - await self._interface.add_choice( - f"The bot understands the task to be '{task_text}'" - ) - - answers.append( - await get_answer_using_text( - self._inference, - self._interface, - task_text, - prior_conversation, - policy, - ) - ) - - if len(answers) > 1: - return perform_and(answers) - - if not answers: - return Answer.create_neutral() - - return answers[0] - - -def split_tasks(task_text): - return [item.strip() for item in task_text.split("|") if item] - - -def perform_and(answers): - result = all([answer.is_true() for answer in answers]) - if result: - return Answer.create_true() - - if any([answer.is_neutral() for answer in answers]): - return Answer.create_neutral() - - return Answer.create_false() - - -async def get_answer_using_text( - inference, interface, task_text, prior_conversation, policy -): - working_memory = TaskMemory() - working_memory.add_story(prior_conversation) - working_memory.add_story(task_text) - query = Query(text=task_text, is_question=is_question(task_text), variable="name") - interface.bot_has_spoken(False) - answer = await inference.compute(query, working_memory, policy=policy) - return answer diff --git a/wafl/answerer/list_answerer.py b/wafl/answerer/list_answerer.py deleted file mode 100644 index 83c46414..00000000 --- a/wafl/answerer/list_answerer.py +++ /dev/null @@ -1,21 +0,0 @@ -from wafl.answerer.base_answerer import BaseAnswerer -from wafl.events.narrator import Narrator -from wafl.inference.utils import answer_is_informative -from wafl.extractors.dataclasses import Answer - - -class ListAnswerer(BaseAnswerer): - def __init__(self, answerers_list, interface, logger): - self._answerers_list = answerers_list - self._narrator = Narrator(interface) - self._logger = logger - - async def answer(self, query_text, policy): - all_answers = [] - for answerer in self._answerers_list: - answer = await answerer.answer(query_text, policy) - all_answers.append(answer) - if answer_is_informative(answer): - return answer - - return Answer(text="Unknown") diff --git a/wafl/answerer/simple_answerer.py b/wafl/answerer/simple_answerer.py deleted file mode 100644 index a928173b..00000000 --- a/wafl/answerer/simple_answerer.py +++ /dev/null @@ -1,32 +0,0 @@ -from wafl.answerer.base_answerer import BaseAnswerer -from wafl.extractors.dataclasses import Query, Answer -from wafl.extractors.extractor import Extractor -from wafl.simple_text_processing.questions import is_question - - -class SimpleAnswerer(BaseAnswerer): - def __init__(self, narrator, logger): - self._extractor = Extractor(narrator, logger) - self._logger = logger - self._narrator = narrator - - async def answer(self, query_text, policy): - if not is_question(query_text): - return Answer(text="unknown") - - text = self._narrator.summarize_dialogue() - if self._logger: - self._logger.write( - f"SimpleAnswerer: The context is {text}", self._logger.level.INFO - ) - self._logger.write( - f"SimpleAnswerer: The query is {query_text}", self._logger.level.INFO - ) - - answer = await self._extractor.extract( - Query(query_text, is_question=True), text - ) - if await policy.accept(answer.text): - return answer - - return Answer.create_neutral() diff --git a/wafl/answerer/utils.py b/wafl/answerer/utils.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/command_line.py b/wafl/command_line.py index e9db3bbf..87b81ada 100644 --- a/wafl/command_line.py +++ b/wafl/command_line.py @@ -10,7 +10,7 @@ download_models, ) from wafl.runners.run_from_audio import run_from_audio -from wafl.runners.run_web_interface import run_server +from wafl.runners.run_web_interface import run_app def print_help(): @@ -19,6 +19,7 @@ def print_help(): print("> wafl init: Initialize the current folder") print("> wafl run-cli: Run a cli version of the chatbot") 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() @@ -48,7 +49,7 @@ def process_cli(): remove_preprocessed("/") elif command == "run-server": - run_server() + run_app() remove_preprocessed("/") elif command == "run-tests": @@ -60,7 +61,8 @@ def process_cli(): else: print("Unknown argument.\n") - print_help() + else: + print_help() def main(): diff --git a/wafl/config.py b/wafl/config.py index 88212e56..45c00c8e 100644 --- a/wafl/config.py +++ b/wafl/config.py @@ -6,28 +6,10 @@ def create_initial_files(): - _events_template = open(os.path.join(_path, "templates/events.py")) - _config_template = open(os.path.join(_path, "templates/config.json")) - _testcases_template = open(os.path.join(_path, "templates/testcases.txt")) - _docker_start = open(os.path.join(_path, "templates/start_llm.sh")) - _sample_project_dir = os.path.join(_path, "templates/sample_project/") - + _sample_project_dir = os.path.join(_path, "templates/") print("+ Initializing ... ", end="") - shutil.copytree(_sample_project_dir, "./", dirs_exist_ok=True) - with open("config.json", "w") as file: - file.write(_config_template.read()) - - with open("testcases.txt", "w") as file: - file.write(_testcases_template.read()) - - with open("events.py", "w") as file: - file.write(_testcases_template.read()) - - with open("start_llm.sh", "w") as file: - file.write(_docker_start.read()) - if not os.path.exists("logs/"): os.mkdir("logs/") diff --git a/wafl/connectors/base_llm_connector.py b/wafl/connectors/base_llm_connector.py new file mode 100644 index 00000000..806164e4 --- /dev/null +++ b/wafl/connectors/base_llm_connector.py @@ -0,0 +1,71 @@ +import logging +import re + + +from wafl.connectors.utils import select_best_answer + +_system_logger = logging.getLogger(__file__) + +model = None +tokenizer = None + + +class BaseLLMConnector: + _max_tries = 3 + _max_reply_length = 500 + _num_prediction_tokens = 200 + _cache = {} + + def __init__(self, last_strings=None): + if not last_strings: + self._last_strings = [ + "\nuser", + "\nbot", + "<|EOS|>", + "", + "\n", + "", + ] + + else: + self._last_strings = last_strings + + async def predict(self, prompt: str) -> [str]: + raise NotImplementedError + + async def generate(self, prompt: str) -> str: + if prompt in self._cache: + return self._cache[prompt] + + text = prompt + start = len(text) + while ( + all(item not in text[start:] for item in self._last_strings) + and len(text) < start + self._max_reply_length + ): + text += select_best_answer(await self.predict(text), self._last_strings) + + end_set = set() + for item in self._last_strings: + if "" in item or "" in item: + continue + + end_set.add(text.find(item, start)) + + if -1 in end_set: + end_set.remove(-1) + + end = len(text) + if end_set: + end = min(end_set) + + candidate_answer = text[start:end].split("bot: ")[-1].strip() + candidate_answer = re.sub(r"(.*)<\|.*\|>", r"\1", candidate_answer).strip() + + if prompt not in self._cache: + self._cache[prompt] = candidate_answer + + if not candidate_answer: + candidate_answer = "unknown" + + return candidate_answer diff --git a/wafl/connectors/bridges/llm_chitchat_answer_bridge.py b/wafl/connectors/bridges/llm_chitchat_answer_bridge.py index de9bf813..3acb086b 100644 --- a/wafl/connectors/bridges/llm_chitchat_answer_bridge.py +++ b/wafl/connectors/bridges/llm_chitchat_answer_bridge.py @@ -1,10 +1,6 @@ -import asyncio import os -import re -from wafl.connectors.bridges.bridge_implementation import load_knowledge_from_file from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory -from wafl.extractors.dataclasses import Query _path = os.path.dirname(__file__) @@ -14,47 +10,38 @@ def __init__(self, config): self._connector = LLMConnectorFactory.get_connector(config) self._config = config - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if loop and loop.is_running(): - self._knowledge = None - - else: - self._knowledge = asyncio.run( - load_knowledge_from_file("dialogues", self._config) - ) - async def get_answer(self, text: str, dialogue: str, query: str) -> str: prompt = await self._get_answer_prompt(text, query, dialogue) return await self._connector.generate(prompt) - async def _get_answer_prompt(self, text, query, dialogue=None): - if not self._knowledge: - self._knowledge = await load_knowledge_from_file("dialogues", self._config) - - retrieved_dialogues = await self._knowledge.ask_for_facts( - Query.create_from_text(dialogue), threshold=0.1 - ) - retrieved_dialogues = "\n\n\n".join( - [item.text for item in retrieved_dialogues][:3] - ) - dialogue = re.sub(r"bot:(.*)\n", r"bot: \1<|EOS|>\n", dialogue) - prompt = f""" -The user and the bot talk. -The bot ends every utterance line with <|EOS|>. -This bot answers are short and to the point. Do not use more than one sentence to reply. -The bot should not repeat itself. Every reply should be different from the previous ones. -some examples are as follows: + 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}" - -{retrieved_dialogues} - - -In the dialogue below a user is speaking to a bot: + prompt = f""" +The following is a summary of a conversation. All the elements of the conversation are described briefly: + +A user is chatting with a bot. The chat is happening through a web interface. The user is typing the messages and the bot is replying. +{text.strip()} + + + +Create a plausible dialogue based on the aforementioned summary and rules. +Do not repeat yourself. Be friendly but not too servile. +Wrap any code or html you output in the with the markdown syntax for code blocks (i.e. use triple backticks ```) unless it is between tags. + + +This is the dialogue: {dialogue} bot: """.strip() diff --git a/wafl/connectors/bridges/llm_code_creator_bridge.py b/wafl/connectors/bridges/llm_code_creator_bridge.py deleted file mode 100644 index 739f5d50..00000000 --- a/wafl/connectors/bridges/llm_code_creator_bridge.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio -import os - -from wafl.connectors.bridges.bridge_implementation import load_knowledge_from_file -from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory -from wafl.extractors.dataclasses import Query - -_path = os.path.dirname(__file__) - - -class LLMCodeCreatorBridge: - def __init__(self, config): - self._connector = LLMConnectorFactory.get_connector(config) - self._config = config - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if loop and loop.is_running(): - self._knowledge = None - - else: - self._knowledge = asyncio.run( - load_knowledge_from_file("code_creator", self._config) - ) - - async def get_answer(self, text: str, dialogue: str, query: str) -> str: - prompt = await self._get_answer_prompt(text, query, dialogue) - return await self._connector.generate(prompt) - - async def _get_answer_prompt(self, text: str, task: str, function_name: str = None): - if self._knowledge is None: - self._knowledge = await load_knowledge_from_file( - "code_creator", self._config - ) - - function_name = function_name.split("=")[-1] - retrieved_items = await self._knowledge.ask_for_facts( - Query.create_from_text(task), threshold=0.0 - ) - retrieved_items = "\n\n\n".join( - [item.text for item in retrieved_items][::-1][:5] - ) - prompt = f""" -{retrieved_items} - -The code needs to accomplish the following task: {task} - -The function with arguments and output needs to be exactly as in the following. Keep the same names and argument number: -{function_name} - -Create a python code that returns the user's request. Import within the function the relevant modules: - """.strip() - - return prompt diff --git a/wafl/connectors/bridges/llm_generated_answer_bridge.py b/wafl/connectors/bridges/llm_generated_answer_bridge.py deleted file mode 100644 index 7981233e..00000000 --- a/wafl/connectors/bridges/llm_generated_answer_bridge.py +++ /dev/null @@ -1,19 +0,0 @@ -from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory - - -class LLMGeneratedAnswerBridge: - def __init__(self, config=None): - self._connector = LLMConnectorFactory.get_connector(config) - - async def get_answer(self, text: str, dialogue: str, query: str) -> str: - prompt = await self._get_answer_prompt(text, query, dialogue) - return await self._connector.generate(prompt) - - async def _get_answer_prompt(self, text, query, dialogue=None): - prompt = ( - f"The user looks for an answer to the question:\n" - f"Q: {query}\n" - f"A: I believe" - ) - - return prompt diff --git a/wafl/connectors/bridges/llm_prompt_predictor_bridge.py b/wafl/connectors/bridges/llm_prompt_predictor_bridge.py deleted file mode 100644 index ea46a9f4..00000000 --- a/wafl/connectors/bridges/llm_prompt_predictor_bridge.py +++ /dev/null @@ -1,23 +0,0 @@ -from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory - - -class LLMPromptPredictorBridge: - def __init__(self, config): - self._connector = LLMConnectorFactory.get_connector(config) - - async def get_answer(self, text: str, dialogue: str, query: str) -> str: - prompt = await self._get_answer_prompt(text, query, dialogue) - return await self._connector.generate(prompt) - - async def _get_answer_prompt(self, text, query, dialogue=None): - for _ in range(5): - text = text.replace(" ", " ") - - prompt = f""" - -Complete the following task and add <|EOS|> at the end: {text} - - - """.strip() - - return prompt diff --git a/wafl/connectors/bridges/llm_qa_bridge.py b/wafl/connectors/bridges/llm_qa_bridge.py deleted file mode 100644 index c7ac1b5f..00000000 --- a/wafl/connectors/bridges/llm_qa_bridge.py +++ /dev/null @@ -1,75 +0,0 @@ -import asyncio -import os - -from wafl.connectors.bridges.bridge_implementation import load_knowledge_from_file -from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory -from wafl.extractors.dataclasses import Query - -_path = os.path.dirname(__file__) - - -class LLMQABridge: - def __init__(self, config): - self._connector = LLMConnectorFactory.get_connector(config) - self._config = config - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if loop and loop.is_running(): - self._knowledge = None - self._adversarial_knowledge = None - - else: - self._knowledge = asyncio.run(load_knowledge_from_file("qa", self._config)) - self._adversarial_knowledge = asyncio.run( - load_knowledge_from_file("qa_adversarial", self._config) - ) - - async def get_answer(self, text: str, dialogue: str, query: str) -> str: - prompt = await self._get_answer_prompt(text, query, dialogue) - return await self._connector.generate(prompt) - - async def _get_answer_prompt(self, text, query, dialogue=None): - if not self._knowledge: - self._knowledge = await load_knowledge_from_file("qa", self._config) - - if not self._adversarial_knowledge: - self._adversarial_knowledge = await load_knowledge_from_file( - "qa_adversarial", self._config - ) - - text = text.strip() - text = text.replace("\\'", "'") - query = query.strip() - retrieved_items = await self._knowledge.ask_for_facts_with_threshold( - Query.create_from_text(f"{text} Q:{query}"), threshold=0.0 - ) - retrieved_adversarial_items = ( - await self._adversarial_knowledge.ask_for_facts_with_threshold( - Query.create_from_text(f"{text} Q:{query}"), - threshold=0.0, - ) - ) - all_items_and_scores = sorted( - retrieved_items[:5] + retrieved_adversarial_items[:5], key=lambda x: x[1] - ) - prompt = ( - "\n\n\n".join([item[0].text for item in all_items_and_scores]) + "\n\n\n" - ) - - prompt += ( - "Below a user and a bot discuss a story. The user is talking to the bot.\n" - ) - prompt += "If the answer is *not* in the story the answer is 'unknown'.\n\n" - prompt += " " + text + " \n\n" - prompt += "The first person asks questions about the story and the second answers them:\n" - if dialogue: - dialogue = dialogue.strip() - prompt += dialogue + "\n" - - prompt += "Q: " + query + "\n" - prompt += "A:" - return prompt diff --git a/wafl/connectors/bridges/llm_task_creator_bridge.py b/wafl/connectors/bridges/llm_task_creator_bridge.py deleted file mode 100644 index f14da88c..00000000 --- a/wafl/connectors/bridges/llm_task_creator_bridge.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio -import os - -from wafl.connectors.bridges.bridge_implementation import load_knowledge_from_file -from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory -from wafl.extractors.dataclasses import Query - -_path = os.path.dirname(__file__) - - -class LLMTaskCreatorBridge: - def __init__(self, config): - self._connector = LLMConnectorFactory.get_connector(config) - self._config = config - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if loop and loop.is_running(): - self._knowledge = None - - else: - self._knowledge = asyncio.run( - load_knowledge_from_file("task_creator", self._config) - ) - - async def get_answer(self, text: str, dialogue: str, query: str) -> str: - prompt = await self._get_answer_prompt(text, query, dialogue) - return await self._connector.generate(prompt) - - async def _get_answer_prompt(self, text: str, task: str, triggers: str = None): - if self._knowledge is None: - self._knowledge = await load_knowledge_from_file( - "task_creator", self._config - ) - - retrieved_items = await self._knowledge.ask_for_facts( - Query.create_from_text(task), threshold=0.0 - ) - retrieved_items = "\n\n\n".join( - [item.text for item in retrieved_items][::-1][:5] - ) - prompt = f"""" + "\n" -{retrieved_items} - - -The intention of the user is the following: {task} - -The system has rules that are triggered by the following sentences -{triggers} - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {{...}}. -Use the least steps: - """.strip() - - return prompt diff --git a/wafl/connectors/bridges/llm_task_extractor_bridge.py b/wafl/connectors/bridges/llm_task_extractor_bridge.py deleted file mode 100644 index 69fb60cf..00000000 --- a/wafl/connectors/bridges/llm_task_extractor_bridge.py +++ /dev/null @@ -1,81 +0,0 @@ -import asyncio -import os - -from wafl.connectors.bridges.bridge_implementation import load_knowledge_from_file -from wafl.connectors.factories.llm_connector_factory import LLMConnectorFactory -from wafl.extractors.dataclasses import Query - -_path = os.path.dirname(__file__) - - -class LLMTaskExtractorBridge: - def __init__(self, config=None): - self._connector = LLMConnectorFactory.get_connector(config) - self._config = config - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if loop and loop.is_running(): - self._knowledge = None - self._adversarial_knowledge = None - - else: - self._knowledge = asyncio.run( - load_knowledge_from_file("task_extractor", self._config) - ) - self._adversarial_knowledge = asyncio.run( - load_knowledge_from_file("task_extractor_adversarial", self._config) - ) - - async def get_answer(self, text: str, dialogue: str, query: str) -> str: - prompt = await self._get_answer_prompt(text, query, dialogue) - return await self._connector.generate(prompt) - - async def _get_answer_prompt(self, text: str, query: str, dialogue: str = None): - if self._knowledge is None: - self._knowledge = await load_knowledge_from_file( - "task_extractor", self._config - ) - - if self._adversarial_knowledge is None: - self._adversarial_knowledge = await load_knowledge_from_file( - "task_extractor_adversarial", self._config - ) - - retrieved_items = await self._knowledge.ask_for_facts_with_threshold( - Query.create_from_text(dialogue), threshold=0.0 - ) - retrieved_adversarial_items = ( - await self._adversarial_knowledge.ask_for_facts_with_threshold( - Query.create_from_text(dialogue), threshold=0.0 - ) - ) - all_items_and_scores = sorted( - retrieved_items[:5] + retrieved_adversarial_items[:5], key=lambda x: x[1] - ) - retrieved_string = ( - "\n\n\n".join([item[0].text for item in all_items_and_scores]) + "\n\n\n" - ) - - prompt = f""" -The task is to extract the user's intention from the last statement from the user. -Prior statements only provide context and should not be used to determine the user's intention. -Be as specific as possible. -If the last statement has multiple intentions, separate them with a pipe character "|". -After the task is extracted, end the text with <|EOS|>. -Some examples are below. - - -{retrieved_string} - - -The following conversation is taking place: -{dialogue} - -Say the user's intention in the last utterance: - """.strip() - - return prompt diff --git a/wafl/connectors/factories/entailment_connector_factory.py b/wafl/connectors/factories/entailment_connector_factory.py deleted file mode 100644 index 0e1fb063..00000000 --- a/wafl/connectors/factories/entailment_connector_factory.py +++ /dev/null @@ -1,11 +0,0 @@ -from wafl.connectors.local.local_entailment_connector import LocalEntailmentConnector -from wafl.connectors.remote.remote_entailment_connector import RemoteEntailmentConnector - - -class EntailmentConnectorFactory: - @staticmethod - def get_connector(config): - if config.get_value("entailment_model")["model_is_local"]: - return LocalEntailmentConnector(config.get_value("entailment_model")) - - return RemoteEntailmentConnector(config.get_value("entailment_model")) diff --git a/wafl/connectors/factories/llm_connector_factory.py b/wafl/connectors/factories/llm_connector_factory.py index b04c5dee..2d1c8714 100644 --- a/wafl/connectors/factories/llm_connector_factory.py +++ b/wafl/connectors/factories/llm_connector_factory.py @@ -1,11 +1,7 @@ -from wafl.connectors.local.local_llm_connector import LocalLLMConnector from wafl.connectors.remote.remote_llm_connector import RemoteLLMConnector class LLMConnectorFactory: @staticmethod def get_connector(config): - if config.get_value("llm_model")["model_is_local"]: - return LocalLLMConnector(config.get_value("llm_model")) - return RemoteLLMConnector(config.get_value("llm_model")) diff --git a/wafl/connectors/factories/sentence_embedder_connector_factory.py b/wafl/connectors/factories/sentence_embedder_connector_factory.py index 08925aa7..7ab1a708 100644 --- a/wafl/connectors/factories/sentence_embedder_connector_factory.py +++ b/wafl/connectors/factories/sentence_embedder_connector_factory.py @@ -1,6 +1,3 @@ -from wafl.connectors.local.local_sentence_embedder_connector import ( - LocalSentenceEmbedderConnector, -) from wafl.connectors.remote.remote_sentence_embedder_connector import ( RemoteSentenceEmbedderConnector, ) @@ -9,9 +6,4 @@ class SentenceEmbedderConnectorFactory: @staticmethod def get_connector(model_name, config): - if config.get_value(model_name)["model_is_local"]: - return LocalSentenceEmbedderConnector( - config.get_value(model_name)["local_model"] - ) - return RemoteSentenceEmbedderConnector(config.get_value(model_name)) diff --git a/wafl/connectors/factories/speaker_connector_factory.py b/wafl/connectors/factories/speaker_connector_factory.py index bad8fbe7..578497eb 100644 --- a/wafl/connectors/factories/speaker_connector_factory.py +++ b/wafl/connectors/factories/speaker_connector_factory.py @@ -1,11 +1,7 @@ -from wafl.connectors.local.local_speaker_connector import LocalSpeakerConnector from wafl.connectors.remote.remote_speaker_connector import RemoteSpeakerConnector class SpeakerConnectorFactory: @staticmethod def get_connector(config): - if config.get_value("speaker_model")["model_is_local"]: - return LocalSpeakerConnector(config.get_value("speaker_model")) - - return RemoteSpeakerConnector(config.get_value("speaker_model")["remote_model"]) + return RemoteSpeakerConnector(config.get_value("speaker_model")) diff --git a/wafl/connectors/factories/whisper_connector_factory.py b/wafl/connectors/factories/whisper_connector_factory.py index 84571b9f..8304adc8 100644 --- a/wafl/connectors/factories/whisper_connector_factory.py +++ b/wafl/connectors/factories/whisper_connector_factory.py @@ -1,13 +1,9 @@ -from wafl.connectors.local.local_whisper_connector import LocalWhisperConnector from wafl.connectors.remote.remote_whisper_connector import RemoteWhisperConnector class WhisperConnectorFactory: @staticmethod def get_connector(config): - if config.get_value("listener_model")["model_is_local"]: - return LocalWhisperConnector(config.get_value("listener_model")) - return RemoteWhisperConnector( - config.get_value("listener_model")["remote_model"] + config.get_value("listener_model") ) diff --git a/wafl/connectors/local/local_entailment_connector.py b/wafl/connectors/local/local_entailment_connector.py deleted file mode 100644 index 083e2996..00000000 --- a/wafl/connectors/local/local_entailment_connector.py +++ /dev/null @@ -1,40 +0,0 @@ -import logging - -import torch - -from transformers import AutoTokenizer -from transformers import AutoModelForSequenceClassification -from typing import Dict - -_system_logger = logging.getLogger(__file__) - -device = "cuda" if torch.cuda.is_available() else "cpu" - -model = None -tokenizer = None - - -class LocalEntailmentConnector: - def __init__(self, config): - global model, tokenizer - if not model: - model_name = config["local_model"] - _system_logger.info(f"Loading model {model_name} locally.") - model = AutoModelForSequenceClassification.from_pretrained(model_name) - model.to(device) - _system_logger.info(f"The model is loaded.") - - if not tokenizer: - tokenizer = AutoTokenizer.from_pretrained(model_name) - - self._cache = {} - - async def predict(self, premise: str, hypothesis: str) -> Dict[str, float]: - input_ids = tokenizer( - premise, hypothesis, truncation=True, return_tensors="pt" - ).input_ids.to(device) - output = model(input_ids) - prediction = torch.softmax(output["logits"], -1)[0] - label_names = ["entailment", "neutral", "contradiction"] - answer = {name: float(pred) for pred, name in zip(prediction, label_names)} - return answer diff --git a/wafl/connectors/local/local_llm_connector.py b/wafl/connectors/local/local_llm_connector.py deleted file mode 100644 index e9ea9997..00000000 --- a/wafl/connectors/local/local_llm_connector.py +++ /dev/null @@ -1,119 +0,0 @@ -import logging -import time -import re -import torch - -from transformers import AutoTokenizer, AutoModelForCausalLM -from transformers import StoppingCriteria - -_system_logger = logging.getLogger(__file__) - -device = "cuda" if torch.cuda.is_available() else "cpu" -model = None -tokenizer = None - -class LocalLLMConnector: - _max_tries = 3 - _max_reply_length = 500 - _num_prediction_tokens = 200 - _cache = {} - - def __init__(self, config): - global model, tokenizer - if not model: - _system_logger.info(f"Loading model {config['local_model']} locally.") - model = AutoModelForCausalLM.from_pretrained( - config["local_model"], - init_device=device, - trust_remote_code=True, - torch_dtype=torch.half, - ) - _system_logger.info(f"The model is loaded.") - - if not tokenizer: - tokenizer = AutoTokenizer.from_pretrained(config["local_model"]) - - self._stop_at_eos = StopAtEOS(tokenizer) - - async def predict(self, prompt: str) -> str: - input_ids = tokenizer.encode( - prompt, return_tensors="pt", truncation=True, max_length=1008 - ).to(device) - with torch.no_grad(): - num_beams = 1 - num_tokens = 200 - output = model.generate( - input_ids, - max_new_tokens=num_tokens, - num_beams=num_beams, - num_return_sequences=1, - pad_token_id=tokenizer.eos_token_id, - eos_token_id=tokenizer.eos_token_id, - use_cache=True, - stopping_criteria=[self._stop_at_eos], - ) - output_ids = list(output[0][input_ids.shape[1] :]) - if tokenizer.eos_token_id in output_ids: - output_ids = output_ids[: output_ids.index(tokenizer.eos_token_id)] - - answer = tokenizer.decode(output_ids) - answer = re.sub(r"(.*)<\|.*\|>(.*)", r"\1<|EOS|>", answer) - answer = re.sub(r"(.*)#", r"\1<|EOS|>", answer) - - return answer - - async def generate(self, prompt: str) -> str: - print(__name__) - start_time = time.time() - if prompt in self._cache: - print(time.time() - start_time) - return self._cache[prompt] - - text = prompt - start = len(text) - while ( - all( - item not in text[start:] - for item in ["<|EOS|>", "user:", "\nThe bot", "bot:"] - ) - and len(text) < start + self._max_reply_length - ): - text += await self.predict(text) - - end_set = set() - end_set.add(text.find("\nuser:", start)) - end_set.add(text.find("\nbot:", start)) - end_set.add(text.find("<|EOS|>", start)) - end_set.add(text.find("\nThe bot", start)) - if -1 in end_set: - end_set.remove(-1) - - end = len(text) - if end_set: - end = min(end_set) - - candidate_answer = text[start:end].split("bot: ")[-1].strip() - candidate_answer = re.sub(r"(.*)<\|.*\|>", r"\1", candidate_answer).strip() - - if prompt not in self._cache: - self._cache[prompt] = candidate_answer - - print(time.time() - start_time) - if not candidate_answer: - candidate_answer = "unknown" - - return candidate_answer - - -class StopAtEOS(StoppingCriteria): - def __init__(self, tokenizer, last_string="<|EOS|>"): - self._tokenizer = tokenizer - self._last_string = last_string - - def __call__( - self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs - ) -> bool: - generated_text = self._tokenizer.decode(input_ids[0], skip_special_tokens=True) - return generated_text.rfind(self._last_string) == len(generated_text) - len( - self._last_string - ) diff --git a/wafl/connectors/local/local_sentence_embedder_connector.py b/wafl/connectors/local/local_sentence_embedder_connector.py deleted file mode 100644 index 67ec2adf..00000000 --- a/wafl/connectors/local/local_sentence_embedder_connector.py +++ /dev/null @@ -1,21 +0,0 @@ -import torch.cuda -from sentence_transformers import SentenceTransformer -from typing import Dict, List - -_device = "cuda" if torch.cuda.is_available() else "cpu" - -models_dict = {} - -class LocalSentenceEmbedderConnector: - _max_tries = 3 - - def __init__(self, model_name): - self._model_name = model_name - - global models_dict - if model_name not in models_dict: - models_dict[model_name] = SentenceTransformer(model_name, device=_device) - - async def predict(self, text: str) -> Dict[str, List[float]]: - vector = models_dict[self._model_name].encode(text, show_progress_bar=False) - return {"embedding": vector} diff --git a/wafl/connectors/local/local_speaker_connector.py b/wafl/connectors/local/local_speaker_connector.py deleted file mode 100644 index 83ad9b13..00000000 --- a/wafl/connectors/local/local_speaker_connector.py +++ /dev/null @@ -1,34 +0,0 @@ -import torch - -from typing import Dict -from fairseq.checkpoint_utils import load_model_ensemble_and_task_from_hf_hub -from fairseq.models.text_to_speech.hub_interface import TTSHubInterface - -_device = "cuda" if torch.cuda.is_available() else "cpu" - - -class LocalSpeakerConnector: - def __init__(self, config): - model_name = config["local_model"] - global model - models, cfg, self._task = load_model_ensemble_and_task_from_hf_hub( - model_name, - arg_overrides={"vocoder": "hifigan", "fp16": False}, - ) - self._model = models[0] - TTSHubInterface.update_cfg_with_data_cfg(cfg, self._task.data_cfg) - self._generator = self._task.build_generator(models, cfg) - self._generator.model.to(_device) - self._generator.vocoder.model.to(_device) - - async def predict(self, text: str) -> Dict[str, float]: - sample = TTSHubInterface.get_model_input(self._task, text) - sample["net_input"]["src_tokens"] = sample["net_input"]["src_tokens"].to( - _device - ) - with torch.no_grad(): - wav, rate = TTSHubInterface.get_prediction( - self._task, self._model, self._generator, sample - ) - wav = wav.cpu().numpy().tobytes() - return {"wav": wav, "rate": rate} diff --git a/wafl/connectors/local/local_whisper_connector.py b/wafl/connectors/local/local_whisper_connector.py deleted file mode 100644 index e68cab9d..00000000 --- a/wafl/connectors/local/local_whisper_connector.py +++ /dev/null @@ -1,78 +0,0 @@ -import torch -from transformers import WhisperProcessor, WhisperForConditionalGeneration -from typing import Dict - -_device = "cuda" if torch.cuda.is_available() else "cpu" - - -class LocalWhisperConnector: - _max_tries = 3 - - def __init__(self, config): - model_name = config["local_model"] - global model, processor - self.model = WhisperForConditionalGeneration.from_pretrained(model_name) - self.model.to(_device) - self.processor = WhisperProcessor.from_pretrained(model_name) - self._starting_tokens = self.processor.tokenizer.convert_tokens_to_ids( - ["<|startoftranscript|>", "<|notimestamps|>"] - ) - self._ending_tokens = self.processor.tokenizer.convert_tokens_to_ids( - ["<|endoftext|>"] - ) - - async def predict(self, waveform, hotword=None) -> Dict[str, float]: - num_beams = 3 - num_tokens = 15 - input_features = self.processor( - audio=waveform, return_tensors="pt", sampling_rate=16_000 - ).input_features - hotword_tokens = None - if hotword: - hotword_tokens = torch.tensor( - [ - item - for item in self.processor.tokenizer.encode(f" {hotword}") - if item not in set(self._ending_tokens + self._starting_tokens) - ], - dtype=torch.int, - ).unsqueeze(0) - - output = self.model.generate( - input_features.to(_device), - num_beams=num_beams, - return_dict_in_generate=True, - output_scores=True, - max_length=num_tokens, - ) - transcription = self.processor.batch_decode( - output.sequences, skip_special_tokens=True - )[0] - score = output.sequences_scores - logp = None - if hotword_tokens is not None: - logp = self.compute_logp(hotword_tokens, input_features) - - return { - "transcription": transcription, - "score": score, - "logp": logp, - } - - def compute_logp(self, hotword_tokens, input_features): - input_ids = torch.tensor([self._starting_tokens]).cuda() - for _ in range(hotword_tokens.shape[1]): - logits = self.model( - input_features.to(_device), - decoder_input_ids=input_ids, - ).logits - new_token = torch.argmax(logits, dim=-1) - new_token = torch.tensor([[new_token[:, -1]]]).cuda() - input_ids = torch.cat([input_ids, new_token], dim=-1) - - logprobs = torch.log(torch.softmax(logits, dim=-1)) - sum_logp = 0 - for logp, index in zip(logprobs[0][1:], hotword_tokens): - sum_logp += logp[int(index)] - - return sum_logp diff --git a/wafl/connectors/remote/remote_entailment_connector.py b/wafl/connectors/remote/remote_entailment_connector.py deleted file mode 100644 index 40e8e300..00000000 --- a/wafl/connectors/remote/remote_entailment_connector.py +++ /dev/null @@ -1,68 +0,0 @@ -import aiohttp -import asyncio -import json - -from typing import Dict - - -class RemoteEntailmentConnector: - _max_tries = 3 - - def __init__(self, config): - self._server_url = ( - f"https://{config['remote_model']['model_host']}:" - f"{config['remote_model']['model_port']}/predictions/entailment" - ) - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if (not loop or (loop and not loop.is_running())) and not asyncio.run( - self.check_connection() - ): - raise RuntimeError("Cannot connect a running Entailment Model.") - - self._cache = {} - - async def predict(self, premise: str, hypothesis: str) -> Dict[str, float]: - if (premise, hypothesis) in self._cache: - return self._cache[(premise, hypothesis)] - - payload = {"premise": premise, "hypothesis": hypothesis} - for _ in range(self._max_tries): - async with aiohttp.ClientSession( - connector=aiohttp.TCPConnector(ssl=False) - ) as session: - async with session.post(self._server_url, json=payload) as response: - data = await response.text() - prediction = json.loads(data) - label_names = ["entailment", "neutral", "contradiction"] - answer = { - name: float(pred) for pred, name in zip(prediction, label_names) - } - if (premise, hypothesis) not in self._cache: - self._cache[(premise, hypothesis)] = answer - - return answer - - return "UNKNOWN" - - async def check_connection(self): - payload = {"premise": "hello", "hypothesis": "a greeting"} - try: - async with aiohttp.ClientSession( - conn_timeout=3, connector=aiohttp.TCPConnector(ssl=False) - ) as session: - async with session.post(self._server_url, json=payload) as response: - await response.text() - return True - - except aiohttp.client.InvalidURL: - print() - print("Is the entailment server running?") - print("Please run 'bash start-llm.sh' (see docs for explanation).") - print() - - return False diff --git a/wafl/connectors/remote/remote_llm_answer_policy_connector.py b/wafl/connectors/remote/remote_llm_answer_policy_connector.py deleted file mode 100644 index 7a7caa56..00000000 --- a/wafl/connectors/remote/remote_llm_answer_policy_connector.py +++ /dev/null @@ -1,75 +0,0 @@ -from wafl.connectors.remote.remote_llm_connector import RemoteLLMConnector - - -class RemoteLLMAnswerPolicyConnector(RemoteLLMConnector): - def __init__(self, config=None): - super().__init__(config) - - async def _get_answer_prompt(self, text, query, dialogue: str = None): - prompt = f""" -The following conversation is taking place: -user: I have a toy car - -Is the next item fit to continue the conversation? -Bot: The weather looks rainy - -Please answer Yes or No: n<|EOS|> - - -The following conversation is taking place: -bot: I have a toy car -user: what - -Is the next item fit to continue the conversation? -bot: I have a toy car - -Please answer Yes or No: y<|EOS|> - - -The following conversation is taking place: -user: I am hungry - -Is the next item fit to continue the conversation? -bot: This is the menu - -Please answer Yes or No: y<|EOS|> - - -The following conversation is taking place: -user: blah blah - -Is the next item fit to continue the conversation? -bot: I don't understand - -Please answer Yes or No: y<|EOS|> - - -The following conversation is taking place: -user: blah blah - -Is the next item fit to continue the conversation? -bot: the bot understands that the user says blah blah - -Please answer Yes or No: y<|EOS|> - - -The following conversation is taking place: -the bot remembers: the user's name is John -user: is my name Jane - -Is the next item fit to continue the conversation? -bot: no - -Please answer Yes or No: y<|EOS|> - - -The following conversation is taking place: -{dialogue} - -Is the next item fit to continue the conversation? -bot: {query} - -Please answer Yes or No: - """.strip() - - return prompt diff --git a/wafl/connectors/remote/remote_llm_chitchat_answer_connector.py b/wafl/connectors/remote/remote_llm_chitchat_answer_connector.py deleted file mode 100644 index 89624de2..00000000 --- a/wafl/connectors/remote/remote_llm_chitchat_answer_connector.py +++ /dev/null @@ -1,55 +0,0 @@ -import asyncio -import os -import re - -from wafl.connectors.remote.remote_llm_connector import RemoteLLMConnector -from wafl.extractors.dataclasses import Query - -_path = os.path.dirname(__file__) - - -class RemoteLLMChitChatAnswerConnector(RemoteLLMConnector): - def __init__(self, config=None): - super().__init__(config) - - try: - loop = asyncio.get_running_loop() - - except RuntimeError: - loop = None - - if loop and loop.is_running(): - self._knowledge = None - - else: - self._knowledge = asyncio.run( - self._load_knowledge_from_file("dialogues", _path) - ) - - async def _get_answer_prompt(self, text, query, dialogue=None): - if not self._knowledge: - self._knowledge = await self._load_knowledge_from_file("dialogues", _path) - - retrieved_dialogues = await self._knowledge.ask_for_facts( - Query.create_from_text(dialogue), threshold=0.1 - ) - retrieved_dialogues = "\n\n\n".join( - [item.text for item in retrieved_dialogues][:3] - ) - dialogue = re.sub(r"bot:(.*)\n", r"bot: \1<|EOS|>\n", dialogue) - prompt = f""" -The user and the bot talk. -The bot ends every utterance line with <|EOS|>. -This bot answers are short and to the point. Do not use more than one sentence to reply. -The bot should not repeat itself. Every reply should be different from the previous ones. -some examples are as follows: - - -{retrieved_dialogues} - - -In the dialogue below a user is speaking to a bot: -{dialogue} -bot: - """.strip() - return prompt diff --git a/wafl/connectors/remote/remote_llm_connector.py b/wafl/connectors/remote/remote_llm_connector.py index 418d53f7..d232df7d 100644 --- a/wafl/connectors/remote/remote_llm_connector.py +++ b/wafl/connectors/remote/remote_llm_connector.py @@ -1,24 +1,20 @@ import aiohttp import asyncio -import csv -import os -import joblib -import time -import re -from wafl.config import Configuration -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge +from wafl.connectors.base_llm_connector import BaseLLMConnector -class RemoteLLMConnector: +class RemoteLLMConnector(BaseLLMConnector): _max_tries = 3 - _max_reply_length = 500 + _max_reply_length = 1024 _num_prediction_tokens = 200 _cache = {} + _num_replicas = 10 - def __init__(self, config): - host = config["remote_model"]["model_host"] - port = config["remote_model"]["model_port"] + def __init__(self, config, last_strings=None): + super().__init__(last_strings) + host = config["model_host"] + port = config["model_port"] self._server_url = f"https://{host}:{port}/predictions/bot" try: @@ -32,11 +28,19 @@ def __init__(self, config): ): raise RuntimeError("Cannot connect a running LLM.") - async def predict(self, prompt: str) -> str: + async def predict(self, prompt: str, temperature=None, num_tokens=None) -> [str]: + if not temperature: + temperature = 0.5 + + if not num_tokens: + num_tokens = self._num_prediction_tokens + payload = { "data": prompt, - "num_beams": 1, - "num_tokens": self._num_prediction_tokens, + "temperature": temperature, + "num_tokens": num_tokens, + "last_strings": self._last_strings, + "num_replicas": self._num_replicas, } for _ in range(self._max_tries): @@ -45,57 +49,18 @@ async def predict(self, prompt: str) -> str: ) as session: async with session.post(self._server_url, json=payload) as response: answer = await response.text() - if not answer: - answer = "<|EOS|>" - - return answer - - return "UNKNOWN" - - async def generate(self, prompt: str) -> str: - print(__name__) - start_time = time.time() - if prompt in self._cache: - print(time.time() - start_time) - return self._cache[prompt] - - text = prompt - start = len(text) - while ( - all( - item not in text[start:] - for item in ["<|EOS|>", "user:", "\nThe bot", "bot:"] - ) - and len(text) < start + self._max_reply_length - ): - text += await self.predict(text) + return answer.split("<||>") - end_set = set() - end_set.add(text.find("\nuser:", start)) - end_set.add(text.find("\nbot:", start)) - end_set.add(text.find("<|EOS|>", start)) - end_set.add(text.find("\nThe bot", start)) - if -1 in end_set: - end_set.remove(-1) - - end = len(text) - if end_set: - end = min(end_set) - - candidate_answer = text[start:end].split("bot: ")[-1].strip() - candidate_answer = re.sub(r"(.*)<\|.*\|>", r"\1", candidate_answer).strip() - - if prompt not in self._cache: - self._cache[prompt] = candidate_answer - - print(time.time() - start_time) - if not candidate_answer: - candidate_answer = "unknown" - - return candidate_answer + return [""] async def check_connection(self): - payload = {"data": "test", "num_beams": 1, "num_tokens": 5} + payload = { + "data": "test", + "temperature": 0.6, + "num_tokens": 1, + "last_strings": self._last_strings, + "num_replicas": self._num_replicas, + } try: async with aiohttp.ClientSession( conn_timeout=3, connector=aiohttp.TCPConnector(ssl=False) diff --git a/wafl/connectors/remote/remote_sentence_embedder_connector.py b/wafl/connectors/remote/remote_sentence_embedder_connector.py index c76eb339..ae71e6fe 100644 --- a/wafl/connectors/remote/remote_sentence_embedder_connector.py +++ b/wafl/connectors/remote/remote_sentence_embedder_connector.py @@ -10,11 +10,9 @@ class RemoteSentenceEmbedderConnector: _max_tries = 3 def __init__(self, config): - host = config["remote_model"]["model_host"] - port = config["remote_model"]["model_port"] - model_name = config["local_model"] + host = config["model_host"] + port = config["model_port"] - self._model_name = model_name self._server_url = f"https://{host}:" f"{port}/predictions/sentence_embedder" try: loop = asyncio.get_running_loop() @@ -28,7 +26,7 @@ def __init__(self, config): raise RuntimeError("Cannot connect a running Entailment Model.") async def predict(self, text: str) -> Dict[str, List[float]]: - payload = {"text": text, "model_name": self._model_name} + payload = {"text": text} for _ in range(self._max_tries): async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(ssl=False) @@ -42,7 +40,7 @@ async def predict(self, text: str) -> Dict[str, List[float]]: return {"embedding": [0.0]} async def check_connection(self): - payload = {"text": "test", "model_name": "model_name"} + payload = {"text": "test"} try: async with aiohttp.ClientSession( conn_timeout=3, connector=aiohttp.TCPConnector(ssl=False) diff --git a/wafl/connectors/utils.py b/wafl/connectors/utils.py new file mode 100644 index 00000000..6bab650d --- /dev/null +++ b/wafl/connectors/utils.py @@ -0,0 +1,9 @@ +def select_best_answer(answers, last_strings): + special_words = ( + last_strings + + ["", "", "result ="] + + ["", "", "", ""] + ) + return sorted( + answers, key=lambda x: sum([x.count(word) for word in special_words]) + )[-1] diff --git a/wafl/data/code_creator.csv b/wafl/data/code_creator.csv deleted file mode 100644 index a74a32b4..00000000 --- a/wafl/data/code_creator.csv +++ /dev/null @@ -1,24 +0,0 @@ -"The code needs to accomplish the following task: Return the nth Fibonacci number - -The function with arguments and output needs to be exactly as in the following. Keep the same names and argument number: -n_th_number = fibonacci(2) - -Create a python code that returns the user's request. Import within the function the relevant modules: -def fibonacci(number_to_return): - if number_to_return == 0: - return 0 - - return number_to_return * fibonacci(number_to_return – 1)<|EOS|>" -"The code needs to accomplish the following task: list all files in a folder - -The function with arguments and output needs to be exactly as in the following. Keep the same names and argument number: -files = list_all_files(""./"") - -Create a python code that returns the user's request. Import within the function the relevant modules: -def list_all_files(folder_name): - import os - - files_list = [] - for file in os.listdir(folder_name): - files_list.append(file) - return files_list<|EOS|>" diff --git a/wafl/data/dialogues.csv b/wafl/data/dialogues.csv deleted file mode 100644 index 3bc410fa..00000000 --- a/wafl/data/dialogues.csv +++ /dev/null @@ -1,55 +0,0 @@ -"In the dialogue below a user is speaking to a bot: -User: hello -bot: hello<|EOS|> -" -"In the dialogue below a user is speaking to a bot: -user: you -bot: what?<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: what -bot: what?<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: what is the colour of the sky -bot: I believe it is blue on a good day<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: who was the lead actor in superman (1978) -bot: I believe it was Christopher Reeve<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: what is the color of the sun -The bot remembers: the sun is bright yellow -bot: The sun is bright yellow<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: what is the height of my truck -The bot remembers: The user is Italian -The bot remembers: The user has a one bedroom flat -The bot remembers: The user's truck is 8ft -bot: The user's truck is 8ft<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: My flat is a one bedroom -bot: nice to know<|EOS|> -user: is my flat a 2 bedroom -bot: no, it is a 1 bedroom<|EOS|>" -"In the dialogue below a user is speaking to a bot: -the bot remembers: The user is tall -the bot remembers: The user's name is John -user: is my name Jane -bot: no, your name is John<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: My address is 11 Coulton rd -bot: good to know<|EOS|> -user: what is my address -the bot remembers: the user's name is John -bot: Your address is 11 Coulton rd<|EOS|>" -"In the dialogue below a user is speaking to a bot: -bot: Welcome to the website. How may I help you?<|EOS|> -user: My name is John -bot: Hello john, how may I help you?<|EOS|> -user: Is my name Jane? -bot: No, your name is John<|EOS|>" -"In the dialogue below a user is speaking to a bot: -user: the user 's mother is called Ada -bot: Hello to you, bob!<|EOS|> -user: How is the user 's mum called? -The bot remembers: This bot name is fractalego. -The bot remembers: The user 's name is bob. -bot: the user’s mum is called Ada<|EOS|>" diff --git a/wafl/data/qa.csv b/wafl/data/qa.csv deleted file mode 100644 index 51c79da7..00000000 --- a/wafl/data/qa.csv +++ /dev/null @@ -1,56 +0,0 @@ -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The user says 'find me a restaurant' - -The first person asks questions about the story and the second answers them: -Q: What is the user asking the bot? -A: to find a restaurant<|EOS|>" -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The user says 'I live at 1km from here' - -The first person asks questions about the story and the second answers them: -Q: How far from here? -A: 1 kilometer<|EOS|>" -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The user says 'hello'. The bot answers 'hello there'. The bot remembers: The sun is shiny - -The first person asks questions about the story and the second answers them: -Q: How is the sun? -A: shiny<|EOS|>" -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The user says 'hello'. The bot answers 'hello there'. When asked the user's name the user replies: I am John - -The first person asks questions about the story and the second answers them: -Q: What is the user's name? -A: John<|EOS|>" -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The user says 'hello'. The bot answers 'hello there'. When the bot asks who is president, the user replies JFK - -The first person asks questions about the story and the second answers them: -Q: Who is the president? -A: JFK<|EOS|>" -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The user says 'hello'. The bot answers 'hello there'. When the bot asks what the user is reading, the user replies a book - -The first person asks questions about the story and the second answers them: -Q: What is the user reading? -A: a book<|EOS|>" -"Below a user and a bot discuss a story. The user is talking to the bot. -If the answer is *not* in the story the answer is 'unknown'. - - The bot remembers: The earth is round. - -The first person asks questions about the story and the second answers them: -Q: What shape is the earth? -A: round<|EOS|>" diff --git a/wafl/data/task_creator.csv b/wafl/data/task_creator.csv deleted file mode 100644 index 192c49a6..00000000 --- a/wafl/data/task_creator.csv +++ /dev/null @@ -1,181 +0,0 @@ -"The intention of the user is the following: the user wants to know where to eat at a restaurant - -The system has rules that are triggered by the following sentences -- the user wants to know where something is - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to eat at a restaurant - food_type = what type of food do you want? - restaurant_position = the user wants to know where a {food_type} restaurant is - result = Format this restaurant position {restaurant_position} in a convenient way to explain where it is. - SAY The restaurant you want could be at {result}<|EOS|>" -"The intention of the user is the following: the user wants to go somewhere - -The system has rules that are triggered by the following sentences -- the user wants to know where something is - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to go somewhere - location = where do you want to go? - result = Answer the following question. How to get to {location}? - SAY This is the road to {location}: {result}<|EOS|>" -"The intention of the user is the following: the user wants to know if it is going to rain - -The system has rules that are triggered by the following sentences -- the user says ""Hello"" -- the user wants to know what the weather is like -- the user wants to know the time - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to know if it is going to rain - weather_forecast = the user wants to know the weather today - result = Answer the following question given this forecast: {weather_forecast}. Is it going to rain? - SAY {result}<|EOS|>" -"The intention of the user is the following: the user wants to repeat the first thing that they said - -The system has rules that are triggered by the following sentences -- The user needs to go - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to repeat the first thing that they said - first_sentence = What did the user say first? - SAY {first_sentence}<|EOS|>" -"The intention of the user is the following: the user wants to multiply 2 and 3 - -The system has rules that are triggered by the following sentences -- The user wants to know what is in the shopping list - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to multiply 2 and 3 - result = multiply(2, 3) - SAY {result}<|EOS|>" -"The intention of the user is the following: the user wants to list all the files in a folder - -The system has rules that are triggered by the following sentences -- the user wants to add something to the shopping list - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to list all the files in a folder - folder_name = which folder do you want to list? - files_list = list_folder({folder_name}) - SAY {files_list}<|EOS|> -" -"The intention of the user is the following: the user wants to get the nth fibonacci number - -The system has rules that are triggered by the following sentences -- the user wants to greet this bot - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to get the nth fibonacci number - index = what is the index of the fibonacci number you want? - fn_th_number = fibonacci({index}) - SAY {files_list}<|EOS|>" -"The intention of the user is the following: the user wants to know where to buy legos - -The system has rules that are triggered by the following sentences -- The user wants to know the road to somewhere -- the user wants to add something to the shopping list - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to know where to buy legos - road_to_lego_shop = the user wants to know the road to the nearest lego shop - result = Answer the following question given this road: {road_to_lego_shop}. How to get to the lego shop? - SAY {result}<|EOS|>" -"The intention of the user is the following: the user wants to know if they need snow boots - -The system has rules that are triggered by the following sentences -- the user says ""hello"" -- the user says ""what is the weather"" - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to know if they need snow boots - weather_forecast = the user says ""what is the weather"" - result = Answer the following question given this forecast: {weather_forecast}. Does the user need snow boots? - SAY {result}<|EOS|>" -"The intention of the user is the following: the user wants to know how the third planet from the sun is called - -The system has rules that are triggered by the following sentences -- the user says ""hello"" -- the user says ""what is the weather"" - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to know how the third planet from the sun is called - answer = how is the third planet from the sun called - SAY The third planet from the sun is called {answer}<|EOS|>" -"The intention of the user is the following: the user wants to know where to go hiking in Edinburgh - -The system has rules that are triggered by the following sentences -- The user wants to know the road to somewhere -- the user wants to add something to the shopping list - -Create a new rule to answer the user. -The first line is the rule trigger. -The following lines are the steps to accomplish the task. -These lines cannot contain the trigger (no recursive tasks are allowed). -A Python function can be added with instructions in English within <...>. -The result of a query can be used within another query by using brackets {...}. -Use the least steps: -the user wants to know where to go hiking in Edinburgh - road_to_scotland_hike = the user wants to know the road to a good hiking location in Edinburgh - result = Answer the following question given this road: {road_to_scotland_hike}. How to get to the hiking location in Edinburgh? - SAY {result}<|EOS|>" diff --git a/wafl/data/task_extractor.csv b/wafl/data/task_extractor.csv deleted file mode 100644 index 6ce02930..00000000 --- a/wafl/data/task_extractor.csv +++ /dev/null @@ -1,88 +0,0 @@ -"The following conversation is taking place: -user: I want to drive the car - -Say the user's intention in the last utterance: the user wants to drive the car<|EOS|>" -"The following conversation is taking place: -user: I want to see my car -bot: Your car is in the garage -user: I want to drive it - -Say the user's intention in the last utterance: the user wants to drive the car<|EOS|>" -"The following conversation is taking place: -user: I want to drive the car to London - -Say the user's intention in the last utterance: the user wants to drive the car to London<|EOS|>" -"The following conversation is taking place: -user: Can I have french fries - -Say the user's intention in the last utterance: the user asks if they can have french fries<|EOS|> -" -"The following conversation is taking place: -user: Add oranges to the shopping list - -Say the user's intention in the last utterance: the user wants to add oranges to the shopping list<|EOS|>" -"The following conversation is taking place: -user: Hello -bot: hello -user: what is in the shopping list -bot: the shopping list contains apples, kiwis -user: right, add oranges - -Say the user's intention in the last utterance: the user wants to add oranges to the shopping list<|EOS|>" -"The following conversation is taking place: -user: what is the weather and what is the temperature - -Say the user's intention in the last utterance: the user wants to know the weather | the user wants to know the temperature<|EOS|>" -"The following conversation is taking place: -user: remember that I am an engineer - -Say the user's intention in the last utterance: the user wants the bot to remember that they are an engineer<|EOS|>" -"The following conversation is taking place: -user: I want to drive the car to London -bot: here are some routes for you to choose from -bot: london to paris -bot: london to rome -user: find me a good restaurant and order a pizza - -Say the user's intention in the last utterance: the user wants this bot to find a good restaurant | the user wants this bot to order a pizza<|EOS|>" -"The following conversation is taking place: -user: what is the weather like -bot: it is sunny -user: what is the time and how long is it before 12 - -Say the user's intention in the last utterance: the user wants to know the time | the user to know how long it is before 12<|EOS|>" -"The following conversation is taking place: -user: tell me what time it is and then what is the weather tomorrow - -Say the user's intention in the last utterance: the user wants to know the time | the user wants to know the weather tomorrow<|EOS|>" -"The following conversation is taking place: -user: what is the weather like -bot: it is sunny -user: what music is playing - -Say the user's intention in the last utterance: the user wants to know which music is playing<|EOS|>" -"The following conversation is taking place: -user: what is the the weather like -user: what is the time - -Say the user's intention in the last utterance: the user wants to know the time<|EOS|>" -"The following conversation is taking place: -user: tell me about the weather. - -Say the user's intention in the last utterance: the user wants to know what the weather is like<|EOS|>" -"The following conversation is taking place: -user: add paper -user: add scissors -user: add stone - -Say the user's intention in the last utterance: the user wants to add stone<|EOS|>" -"The following conversation is taking place: -user: list all the files in a folder - -Say the user's intention in the last utterance: the user wants list all the files in a folder<|EOS|>" -"The following conversation is taking place: -user: list the first four files in the alphabet folder -bot: sure the list is a, b, c, d -user: sorry I meant the numbers folder - -Say the user's intention in the last utterance: the user wants this bot to list the first four files in the numbers folder<|EOS|>" diff --git a/wafl/events/answerer_creator.py b/wafl/events/answerer_creator.py index 0fa6505b..53eacdbd 100644 --- a/wafl/events/answerer_creator.py +++ b/wafl/events/answerer_creator.py @@ -1,14 +1,7 @@ -from wafl.answerer.arbiter_answerer import ArbiterAnswerer -from wafl.answerer.list_answerer import ListAnswerer +from wafl.answerer.dialogue_answerer import DialogueAnswerer -def create_answerer(config, knowledge, interface, code_path, logger): - return ListAnswerer( - [ - ArbiterAnswerer.create_answerer( - config, knowledge, interface, code_path, logger - ), - ], - interface, - logger, +def create_answerer(config, knowledge, interface, logger): + return DialogueAnswerer( + config, knowledge, interface, config.get_value("functions"), logger ) diff --git a/wafl/events/conversation_events.py b/wafl/events/conversation_events.py index 4ec0d226..95dedb3b 100644 --- a/wafl/events/conversation_events.py +++ b/wafl/events/conversation_events.py @@ -2,10 +2,9 @@ import re from wafl.events.answerer_creator import create_answerer -from wafl.policy.answerer_policy import AnswerPolicy from wafl.simple_text_processing.normalize import normalized from wafl.config import Configuration -from wafl.events.utils import input_is_valid, remove_text_between_brackets +from wafl.events.utils import input_is_valid, load_knowledge from wafl.simple_text_processing.questions import is_question from wafl.exceptions import InterruptTask @@ -15,22 +14,16 @@ class ConversationEvents: def __init__( self, - knowledge: "BaseKnowledge", + config: "Configuration", interface: "BaseInterface", - code_path=None, - config=None, logger=None, ): - if not config: - config = Configuration.load_local_config() - - self._answerer = create_answerer( - config, knowledge, interface, code_path, logger - ) - self._knowledge = knowledge + self._config = config + self._knowledge = load_knowledge(config, logger) + self._answerer = create_answerer(config, self._knowledge, interface, logger) self._interface = interface - self._policy = AnswerPolicy(config, interface, logger) self._logger = logger + self._is_computing = False if logger: self._logger.set_depth(0) @@ -38,20 +31,19 @@ async def output(self, text: str): await self._interface.output(text) async def _process_query(self, text: str): + self._is_computing = True self._interface.bot_has_spoken(False) - if not input_is_valid(text): + self._is_computing = False return False text_is_question = is_question(text) - self._policy.improvise = False try: - answer = await self._answerer.answer( - remove_text_between_brackets(text), policy=self._policy - ) + answer = await self._answerer.answer(text) except InterruptTask: await self._interface.output("Task interrupted") + self._is_computing = False return False if not self._interface.bot_has_spoken(): @@ -66,8 +58,7 @@ async def _process_query(self, text: str): if ( not text_is_question - and answer.is_false() - and not self._interface.bot_has_spoken() + and self._interface.get_utterances_list()[-1].find("user:") == 0 ): await self._interface.output("I don't know what to reply") @@ -78,6 +69,7 @@ async def _process_query(self, text: str): ): await self._interface.output("Yes") + self._is_computing = False return answer async def process_next(self, activation_word: str = "") -> bool: @@ -107,6 +99,12 @@ async def process_next(self, activation_word: str = "") -> bool: return False + def is_computing(self): + return self._is_computing + + def reload_knowledge(self): + self._knowledge = load_knowledge(self._config, self._logger) + def _activation_word_in_text(self, activation_word, text): if f"[{normalized(activation_word)}]" in normalized(text): return True diff --git a/wafl/events/generated_events.py b/wafl/events/generated_events.py deleted file mode 100644 index dace15c7..00000000 --- a/wafl/events/generated_events.py +++ /dev/null @@ -1,54 +0,0 @@ -import os - -from wafl.events.narrator import Narrator -from wafl.simple_text_processing.questions import is_question -from wafl.extractors.dataclasses import Query -from wafl.inference.backward_inference import BackwardInference -from wafl.exceptions import InterruptTask - -os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" - - -class GeneratedEvents: - def __init__( - self, - config, - knowledge: "BaseKnowledge", - interface: "BaseInterface", - events: "BaseEventCreator", - code_path=None, - logger=None, - ): - self._inference = BackwardInference( - config, - knowledge, - interface, - Narrator(interface), - code_path, - logger=logger, - generate_rules=False, - ) - self._knowledge = knowledge - self._interface = interface - self._events = events - - async def output(self, text: str): - await self._interface.output(text) - - async def _process_query(self, text: str): - query = Query(text=text, is_question=is_question(text), variable="name") - return await self._inference.compute(query) - - async def process_next(self, activation_word: str = "") -> bool: - events = self._events.get() - for event in events: - try: - answer = await self._process_query(event) - if answer.is_true(): - return True - - except InterruptTask: - await self._interface.output("Task interrupted") - return False - - return False diff --git a/wafl/events/utils.py b/wafl/events/utils.py index dc99d3bc..52734ea2 100644 --- a/wafl/events/utils.py +++ b/wafl/events/utils.py @@ -1,5 +1,6 @@ import re +from wafl.knowledge.single_file_knowledge import SingleFileKnowledge from wafl.simple_text_processing.normalize import normalized @@ -15,3 +16,16 @@ def input_is_valid(text): def remove_text_between_brackets(text: str) -> str: return re.sub(r"(\[.*?\])", "", text) + + +def load_knowledge(config, logger): + if ".yaml" in config.get_value("rules") and not any( + item in config.get_value("rules") for item in [" ", "\n"] + ): + with open(config.get_value("rules")) as file: + rules_txt = file.read() + + else: + rules_txt = config.get_value("rules") + + return SingleFileKnowledge(config, rules_txt, logger=logger) diff --git a/wafl/extractors/code_creator.py b/wafl/extractors/code_creator.py deleted file mode 100644 index 94a38a51..00000000 --- a/wafl/extractors/code_creator.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging -import os - -from wafl.connectors.bridges.llm_code_creator_bridge import LLMCodeCreatorBridge -from wafl.extractors.dataclasses import Answer -from wafl.extractors.utils import get_function_description, get_code - -_path = os.path.dirname(__file__) -_logger = logging.getLogger(__file__) - - -class CodeCreator: - def __init__(self, config, knowledge, logger=None): - self._connector = LLMCodeCreatorBridge(config) - self._knowledge = knowledge - - async def extract(self, function_and_task: str) -> Answer: - print(__name__) - function_and_task = function_and_task.strip() - function_shape = get_code(function_and_task) - task = get_function_description(function_and_task) - prediction = await self._connector.get_answer( - "", function_shape.strip(), task.strip() - ) - return Answer(text=prediction.strip()) diff --git a/wafl/extractors/entailer.py b/wafl/extractors/entailer.py deleted file mode 100644 index 718d305d..00000000 --- a/wafl/extractors/entailer.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import Dict, Union - -from wafl.connectors.factories.entailment_connector_factory import ( - EntailmentConnectorFactory, -) - - -class Entailer: - def __init__(self, config, logger=None): - self._logger = logger - self._connector = EntailmentConnectorFactory.get_connector(config) - self._cache = {} - - async def get_relation(self, premise: str, hypothesis: str) -> Dict[str, float]: - return await self._connector.predict(premise, hypothesis) - - async def entails( - self, - premise: str, - hypothesis: str, - threshold=0.75, - contradiction_threshold=0.65, - return_threshold=False, - ) -> Union[str, float]: - if self._logger: - self._logger.write("Starting entailment procedure.") - - arguments = ( - premise, - hypothesis, - threshold, - contradiction_threshold, - return_threshold, - ) - if arguments in self._cache: - return self._cache[arguments] - - prediction = await self.get_relation(premise, hypothesis) - if prediction["entailment"] > threshold: - if self._logger: - self._logger.write(f"Entailment: The premise is {premise}") - self._logger.write(f"Entailment: The hypothesis is {hypothesis}") - self._logger.write(f"Entailment: The results are {str(prediction)}") - - if return_threshold: - self._cache[arguments] = prediction["entailment"] - return prediction["entailment"] - - self._cache[arguments] = "True" - return "True" - - if prediction["contradiction"] < contradiction_threshold: - premise = self._add_presuppositions_to_premise(premise) - prediction = await self.get_relation(premise, hypothesis) - - if self._logger: - self._logger.write(f"Entailment: The premise is {premise}") - self._logger.write(f"Entailment: The hypothesis is {hypothesis}") - self._logger.write(f"Entailment: The results are {str(prediction)}") - - if prediction["entailment"] > threshold: - if return_threshold: - self._cache[arguments] = prediction["entailment"] - return prediction["entailment"] - - self._cache[arguments] = "True" - return "True" - - if return_threshold: - self._cache[arguments] = 0 - return 0 - - if prediction["neutral"] > threshold: - self._cache[arguments] = "Unknown" - return "Unknown" - - self._cache[arguments] = "False" - return "False" - - async def is_neutral( - self, - premise: str, - hypothesis: str, - threshold=0.75, - ) -> Union[str, float]: - if self._logger: - self._logger.write("Starting entailment neutral check.") - - prediction = await self.get_relation(premise, hypothesis) - if prediction["neutral"] > threshold: - if self._logger: - self._logger.write(f"Entailment: The premise is {premise}") - self._logger.write(f"Entailment: The hypothesis is {hypothesis}") - self._logger.write(f"Entailment: The results are {str(prediction)}") - - return True - - if self._logger: - self._logger.write(f"Entailment: The premise is {premise}") - self._logger.write(f"Entailment: The hypothesis is {hypothesis}") - self._logger.write(f"Entailment: The results are {str(prediction)}") - - if prediction["neutral"] > threshold: - return True - - return False - - def _add_presuppositions_to_premise(self, premise): - premise = premise.replace("user says:", "user says to this bot:") - premise = premise.replace("user asks:", "user asks to this bot:") - return premise diff --git a/wafl/extractors/extractor.py b/wafl/extractors/extractor.py deleted file mode 100644 index 183c0269..00000000 --- a/wafl/extractors/extractor.py +++ /dev/null @@ -1,115 +0,0 @@ -import logging -import os - -from wafl.connectors.bridges.llm_qa_bridge import LLMQABridge -from wafl.simple_text_processing.questions import ( - is_question, - is_yes_no_question, - get_sentence_from_yn_question, -) -from wafl.simple_text_processing.normalize import normalized -from wafl.extractors.dataclasses import Answer -from wafl.extractors.entailer import Entailer - -_path = os.path.dirname(__file__) -_logger = logging.getLogger(__file__) - - -class Extractor: - def __init__(self, config, narrator, logger=None): - self._entailer = Entailer(config, logger) - self._qa = LLMQABridge(config) - self._narrator = narrator - self._logger = logger - self._entailer_to_qa_mapping = { - "True": "Yes", - "False": "No", - "Unknown": "Unknown", - } - - async def extract(self, query: "Query", text: str, task_memory=None): - if self._logger: - self._logger.write(f"Extractor: the query is {query}") - self._logger.write(f"Extractor: the text is {text}") - - query_text = query.text.strip() - if query.is_question and not is_yes_no_question(query_text): - answer = await self._answer_question( - query_text, query.variable, text, task_memory - ) - return answer - - if query.is_question and is_yes_no_question(query_text): - query_text = get_sentence_from_yn_question(query_text) - if "the user says: 'yes" in text.lower(): - return Answer(text="Yes") - - return await self._check_fact( - text, query_text, query.variable, threshold=0.5 - ) - - return await self._check_fact(query_text, text, query.variable, threshold=0.5) - - async def _answer_question(self, query_text, variable_name, text: str, task_memory): - answer = await self._get_answer(text, "", query_text) - if answer and answer[-1] in [".", ",", "!"]: - answer = answer[:-1] - - return Answer(text=answer, variable=variable_name) - - async def _check_fact(self, query_text, text, variable_name, threshold): - if not is_question(text): - query_context = self._narrator.get_relevant_fact_context(text, query_text) - answer = await self._entailer.entails( - query_context, text, threshold=threshold - ) - return Answer( - text=self._entailer_to_qa_mapping[answer], variable=variable_name - ) - - answer_text = normalized(await self._qa.get_answer(query_text, "", text)) - if answer_text != "unknown" and answer_text != "no": - return Answer(text="Yes", variable=variable_name) - - return Answer(text="No", variable=variable_name) - - async def _get_answer(self, story, dialogue_text, query_text): - query_text = _clean_query_text(query_text) - if self._logger: - self._logger.write(f"Extractor: answering the query {query_text}") - - answer_text = normalized( - await self._qa.get_answer(story, dialogue_text, query_text) - ) - - if self._logger: - self._logger.write(f"Extractor: the answer is {answer_text}") - - if not answer_text: - answer_text = "unknown" - - return answer_text - - def get_entailer(self): - return self._entailer - - -def _split_events(text): - return text.split("; ") - - -def _clean_events(text): - text = text.strip() - text = text.replace(" 's ", "'s ") - text = text.replace(".'", "'") - text = text.replace('."', '"') - if text and text[-1] == ".": - text = text[:-1] - - return text - - -def _clean_query_text(text): - text = text.replace(".'?", "?'") - text = text.replace("'?", "?'") - return text diff --git a/wafl/extractors/prompt_predictor.py b/wafl/extractors/prompt_predictor.py deleted file mode 100644 index 18c8c4e6..00000000 --- a/wafl/extractors/prompt_predictor.py +++ /dev/null @@ -1,19 +0,0 @@ -import logging -import os - -from wafl.connectors.bridges.llm_prompt_predictor_bridge import LLMPromptPredictorBridge -from wafl.extractors.dataclasses import Answer - -_path = os.path.dirname(__file__) -_logger = logging.getLogger(__file__) - - -class PromptPredictor: - def __init__(self, config, logger=None): - self._model = LLMPromptPredictorBridge(config) - self._closing_tag = "" - - async def predict(self, prompt: str): - prediction = await self._model.get_answer(prompt, "", "") - prediction = prediction.replace(self._closing_tag, "").strip() - return Answer(text=prediction.strip()) diff --git a/wafl/extractors/task_creator.py b/wafl/extractors/task_creator.py deleted file mode 100644 index 141fef71..00000000 --- a/wafl/extractors/task_creator.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging -import os - -from wafl.connectors.bridges.llm_task_creator_bridge import LLMTaskCreatorBridge -from wafl.extractors.dataclasses import Answer, Query - -_path = os.path.dirname(__file__) -_logger = logging.getLogger(__file__) - - -class TaskCreator: - def __init__(self, config, knowledge, logger=None): - self._connector = LLMTaskCreatorBridge(config) - self._knowledge = knowledge - self._logger = logger - - async def extract(self, task: str) -> Answer: - print(__name__) - if self._logger: - self._logger.write("TaskCreator: asking for rules") - - rules = await self._knowledge.ask_for_rule_backward( - Query.create_from_text(task), knowledge_name="/", first_n=15 - ) - rules = [rule for rule in rules if not rule.effect.is_interruption] - if self._logger: - self._logger.write("Rules found") - for rule in rules: - self._logger.write(str(rule)) - - triggers = "\n".join(["- " + item.effect.text for item in rules]) - prediction = await self._connector.get_answer("", triggers, task) - return Answer(text=prediction.strip()) diff --git a/wafl/extractors/task_extractor.py b/wafl/extractors/task_extractor.py deleted file mode 100644 index d173f59b..00000000 --- a/wafl/extractors/task_extractor.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import os - -from wafl.connectors.bridges.llm_task_extractor_bridge import LLMTaskExtractorBridge -from wafl.extractors.dataclasses import Answer - -_path = os.path.dirname(__file__) -_logger = logging.getLogger(__file__) - - -class TaskExtractor: - def __init__(self, config, interface, logger=None): - self._interface = interface - self._connector = LLMTaskExtractorBridge(config) - self._max_num_past_utterances = 3 - self._to_ignore = ["yes", "no", "ok", "okay", "sure", "nope", "yep", "you"] - - async def extract(self, query: str) -> Answer: - print(__name__) - dialogue = "\n".join( - self._interface.get_utterances_list()[-self._max_num_past_utterances :] - ) - if not dialogue: - dialogue = query - - if self._ignore_utterances(dialogue): - return Answer.create_neutral() - - prediction = await self._connector.get_answer( - "", - dialogue, - query, - ) - return Answer(text=prediction.strip()) - - def _ignore_utterances(self, dialogue: str) -> bool: - utterance = dialogue.split("\n")[-1].split("user:")[-1].strip() - if utterance.lower() in self._to_ignore: - return True - - return False diff --git a/wafl/filter/base_filter.py b/wafl/filter/base_filter.py new file mode 100644 index 00000000..732dab10 --- /dev/null +++ b/wafl/filter/base_filter.py @@ -0,0 +1,3 @@ +class BaseAnswerFilter: + async def filter(self, dialogue_list, query_text) -> str: + raise NotImplementedError() diff --git a/wafl/frontend/index.html b/wafl/frontend/index.html index d33a3d59..bc7283f1 100644 --- a/wafl/frontend/index.html +++ b/wafl/frontend/index.html @@ -2,102 +2,41 @@ WAFL frontend - + + - + +
    -
    + aria-label="Sidebar" +> +
    +
    +
    @@ -106,34 +45,17 @@
    -
    +    
         
    - -
    - -
    - 👍 -
    -
    - 👎 -
    -
    +
    - \ No newline at end of file + diff --git a/wafl/frontend/selector.html b/wafl/frontend/selector.html new file mode 100644 index 00000000..15ad1ce6 --- /dev/null +++ b/wafl/frontend/selector.html @@ -0,0 +1,18 @@ + + + + WAFL frontend + + + + + + + +
    + Creating a new instance. This may take a few seconds... +
    + + \ No newline at end of file diff --git a/wafl/frontend/wafl.css b/wafl/frontend/wafl.css new file mode 100644 index 00000000..a3a69e78 --- /dev/null +++ b/wafl/frontend/wafl.css @@ -0,0 +1,129 @@ +body { + background: #f0f8ff; + color: black; + font-family: monospace; + font-size: 25px; + padding-left: 30px; + padding-right: 30px; + padding-bottom: 50px; +} + +#query{ + font-size: 20px; + margin: 20px; + min-height: 25px; + max-height: 50px; + width: 100%; + border-radius: 5px; + border-color: #7a7a9f; + border-width: 2px; + padding: 10px; + resize: none; +} + +pre { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +pre .dialogue { + width: 50vw; + height: 90vh; + display: flex; + padding: 10px; + margin-left: 8px; + flex-direction: column-reverse; +} + +pre .logs { + width: 35vw; + height: 90vh; + display: flex; + padding: 10px; + margin-left: 15px; + flex-direction: column-reverse; + background: white; + opacity: 50%; +} + +.controls { + position: fixed; + bottom: 0; + width: 100%; + padding: 10px 10px; + border-top: 1px solid #4e4a4a; +} + +#banner { + width: 100%; + padding: 10px 10px; +} + +#default-sidebar a{ + cursor: pointer; + text-align: left; +} + +.dialogue-row-user { + font-family: monospace; + font-size: 30px; + margin-top: 20px; + margin-bottom: 10px; + padding: 10px; + width: 80%; +} + +.dialogue-row-bot { + font-family: monospace; + font-size: 30px; + color: gray; + margin-top: 20px; + margin-bottom: 15px; + border-radius: 5px; + border-color: #7a7a9f; + border-width: 2px; + background-color: whitesmoke; + padding: 10px; +} + +.log-row { + font-size: 11px; + margin-left: 30px; + margin-top: 10px; + color: #2a2a2a; + margin-bottom: 10px; + padding: 10px; +} + + +.dialogue-row-comment { + font-family: monospace; + font-size: 30px; + margin-top: 20px; + margin-bottom: 15px; + border-radius: 5px; + border-color: #7a7a9f; + border-width: 2px; + padding: 10px; +} + +#code { + font-family: monospace; + color: #1d9d01; + background-color: #1f1d1d; + padding: 10px; + border-radius: 5px; + width: 100%; + resize: none; +} + +execute { + color: green; +} + +rememeber { + color: blue; +} \ No newline at end of file diff --git a/wafl/frontend/wafl.js b/wafl/frontend/wafl.js new file mode 100644 index 00000000..42bc3b6b --- /dev/null +++ b/wafl/frontend/wafl.js @@ -0,0 +1,8 @@ +$("textarea").on("keydown", function(e){ + if (e.keyCode == 13 && !e.shiftKey) + { + // prevent default behavior + e.preventDefault(); + return false; + } +}); \ No newline at end of file diff --git a/wafl/inference/__init__.py b/wafl/inference/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/inference/backward_inference.py b/wafl/inference/backward_inference.py deleted file mode 100644 index fa7b2129..00000000 --- a/wafl/inference/backward_inference.py +++ /dev/null @@ -1,843 +0,0 @@ -import asyncio -import logging -import traceback - -from wafl.config import Configuration -from wafl.extractors.code_creator import CodeCreator -from wafl.extractors.task_creator import TaskCreator -from wafl.extractors.task_extractor import TaskExtractor -from wafl.extractors.utils import get_code, get_function_description -from wafl.parsing.rules_parser import get_facts_and_rules_from_text -from wafl.simple_text_processing.questions import is_question, is_yes_no_question -from wafl.events.task_memory import TaskMemory -from wafl.simple_text_processing.deixis import from_bot_to_bot -from wafl.exceptions import InterruptTask, CloseConversation -from wafl.extractors.prompt_predictor import PromptPredictor -from wafl.inference.utils import ( - cluster_facts, - selected_answer, - project_answer, - text_has_new_task_memory_command, - text_has_say_command, - text_has_remember_command, - text_is_code, - check_negation, - apply_substitutions, - update_substitutions_from_answer, - add_function_arguments, - invert_answer, - text_has_assigmnent, - update_substitutions_from_results, - answer_is_informative, - text_is_text_generation_task, - escape_characters, - get_causes_list, - text_has_retrieve_command, - create_default_substitutions, -) -from wafl.simple_text_processing.normalize import normalized -from wafl.knowledge.utils import needs_substitutions -from wafl.parsing.preprocess import import_module, create_preprocessed -from wafl.extractors.extractor import Extractor -from wafl.extractors.dataclasses import Query, Answer -from inspect import getmembers, isfunction - -_logger = logging.getLogger(__name__) - - -class BackwardInference: - def __init__( - self, - config: "Configuration", - knowledge: "BaseKnowledge", - interface: "BaseInterface", - narrator: "Narrator", - module_names=None, - max_depth: int = 3, - generate_rules: bool = False, - logger=None, - ): - self._max_depth = max_depth - self._knowledge = knowledge - self._interface = interface - self._config = config - self._extractor = Extractor(config, narrator, logger) - self._prompt_predictor = PromptPredictor(config, logger) - self._task_extractor = TaskExtractor(config, interface) - self._task_creator = TaskCreator(config, knowledge, logger) - self._code_creator = CodeCreator(config, knowledge) - - self._narrator = narrator - self._logger = logger - self._generate_rules = generate_rules - self._module = {} - self._functions = {} - self._sentences_to_utter = [] - if module_names: - self._init_python_modules(module_names) - - async def get_inference_answer(self, text, policy, task_memory=TaskMemory()): - query = Query(text=text, is_question=is_question(text)) - knowledge_name = self._knowledge.root_knowledge - answer = await self._compute_recursively( - query, task_memory, knowledge_name, policy, depth=1 - ) - - if answer.is_true(): - return True - - if answer.is_false(): - return False - - return answer.text - - async def compute( - self, query, task_memory=None, policy=None, knowledge_name="/", depth=0 - ): - if query.is_neutral(): - return Answer.create_neutral() - - lock = asyncio.Lock() - await lock.acquire() - if not task_memory: - task_memory = TaskMemory() - - result = await self._compute_recursively( - query, task_memory, knowledge_name, policy, depth=depth - ) - lock.release() - return result - - async def _compute_recursively( - self, - query: "Query", - task_memory, - knowledge_name, - policy, - depth, - inverted_rule=False, - ): - self._log(f"The query is {query.text}", depth) - self._log(f"The depth is {depth}", depth) - self._log(f"The max depth is {self._max_depth}", depth) - - if depth == 0: - answer = await self._look_for_answer_in_rules( - query, task_memory, knowledge_name, policy, depth, inverted_rule - ) - if not answer: - answer = Answer.create_neutral() - - self._log("Answer found by executing the rules: " + answer.text, depth) - return answer - - if depth > self._max_depth: - self._log("Max inference depth has been reached: " + str(depth)) - return Answer(text="False") - - candidate_answers = [] - answer = await self._look_for_answer_in_entailment(query, knowledge_name, depth) - candidate_answers.append(answer) - if answer and answer_is_informative(answer): - self._log("Answer in entailment: " + answer.text, depth) - return answer - - if ":-" in query.text: - return selected_answer(candidate_answers, query.variable) - - answer = await self._look_for_answer_in_facts( - query, task_memory, knowledge_name, depth - ) - candidate_answers.append(answer) - if answer and not answer.is_neutral(): - self._log("Answers in facts: " + answer.text, depth) - return answer - - if depth > 0: - if text_has_new_task_memory_command(query.text): - task_memory.erase() - return await self._process_new_task_memory_command() - - answer = await self._look_for_answer_in_last_user_utterance( - query, task_memory, knowledge_name, depth - ) - candidate_answers.append(answer) - if answer and answer_is_informative(answer): - if not await self._answer_makes_sense( - query.text, answer.text, self._extractor.get_entailer() - ): - answer = Answer.create_neutral(variable=answer.variable) - - self._log("Answers in working memory: " + answer.text, depth) - return answer - - answer = await self._look_for_answer_in_task_memory( - query, task_memory, knowledge_name, depth - ) - candidate_answers.append(answer) - if answer and answer_is_informative(answer): - if not await self._answer_makes_sense( - query.text, answer.text, self._extractor.get_entailer() - ): - answer = Answer.create_neutral(variable=answer.variable) - - self._log("Answers in working memory: " + answer.text, depth) - return answer - - answer = await self._look_for_answer_by_asking_the_user( - query, task_memory, knowledge_name, policy, depth - ) - candidate_answers.append(answer) - if answer and answer_is_informative(answer): - if not await self._answer_makes_sense( - query.text, answer.text, self._extractor.get_entailer() - ): - answer = Answer.create_neutral(variable=answer.variable) - - self._log("Answers by asking the user: " + answer.text, depth) - return answer - - if depth > 0: - task_memory = TaskMemory() - if text_has_say_command(query.text): - answer = await self._process_say_command(query.text) - return answer - - elif text_has_remember_command(query.text): - return await self._process_remember_command(query.text, knowledge_name) - - elif text_is_code(query.text): - return await self._process_code( - query.text, "", knowledge_name, {}, policy - ) - - answer = await self._look_for_answer_in_rules( - query, task_memory, knowledge_name, policy, depth, inverted_rule - ) - candidate_answers.append(answer) - if answer and answer_is_informative(answer): - self._log("Answer found by executing the rules: " + answer.text, depth) - return answer - - return selected_answer(candidate_answers, query.variable) - - async def _look_for_answer_in_rules( - self, query, task_memory, query_knowledge_name, policy, depth, inverted_rule - ): - self._log(f"Looking for answers in rules") - rules = await self._knowledge.ask_for_rule_backward( - query, knowledge_name=query_knowledge_name - ) - if not rules and self._generate_rules: - self._log(f"Creating rules for the task: {query.text}", depth) - rules_answer = await self._task_creator.extract(query.text) - if not rules_answer.is_neutral(): - rules = get_facts_and_rules_from_text( - rules_answer.text, query_knowledge_name - )["rules"] - - for rule in rules: - await self._interface.add_choice(f"The bot created the rule:") - await self._interface.add_choice(str(rule)) - - for rule in rules: - index = 0 - substitutions = create_default_substitutions(self._interface) - rule_effect_text = rule.effect.text - knowledge_name = rule.knowledge_name - self._log(f"Trying rule with trigger: {rule_effect_text}", depth) - if is_question(rule_effect_text): - if not await self._validate_question_in_effects( - rule.effect, query.text, substitutions - ): - continue - - elif not needs_substitutions(rule.effect): - answer = await self._validate_fact_in_effects( - rule_effect_text, query, substitutions - ) - if answer.is_false(): - continue - - if ( - not rule.effect.is_interruption - and policy - and not await policy.accept(f"The bot understands '{rule_effect_text}'") - ): - continue - - await self._interface.add_choice( - f"The bot selected the rule with trigger {rule_effect_text}." - ) - - for cause in rule.causes: - cause_text = cause.text.strip() - self._log("clause: " + cause_text, depth) - original_cause_text = cause_text - if task_memory.is_in_prior_failed_clauses(original_cause_text): - self._log("This clause failed before", depth) - break - - code_description = "" - if text_is_code(cause_text): - code_description = get_function_description(cause_text) - cause_text = get_code(cause_text) - - cause_text = apply_substitutions(cause_text, substitutions) - if code_description: - apply_substitutions(code_description, substitutions) - - cause_text, invert_results = check_negation(cause_text) - cause_text_list = get_causes_list(cause_text) - - answers = [] - for cause_text in cause_text_list: - answer = await self._process_cause( - cause_text, - cause.is_question, - code_description, - knowledge_name, - substitutions, - policy, - task_memory, - depth, - invert_results, - ) - if type(answer) == list: - answers.extend(answer) - - else: - answers.append(answer) - - if len(answers) > 1: - answer = Answer.create_from_text( - str([item.text for item in answers])[1:-1] - ) - - else: - answer = answers[0] - - if answers: - answer.variable = answers[0].variable - - if invert_results: - answer = invert_answer(answer) - - if answer.is_false(): - task_memory.add_failed_clause(original_cause_text) - break - - if answer.variable: - update_substitutions_from_answer(answer, substitutions) - - if depth == 0: - await self._flush_interface_output() - - index += 1 - - if index == len(rule.causes): - answer = await self._validate_fact_in_effects( - rule_effect_text, query, substitutions - ) - if answer.is_neutral(): - return answer.create_true() - - if not answer.is_false(): - return answer - - if inverted_rule: - return Answer(text="False") - - async def _process_cause( - self, - cause_text, - cause_is_question, - code_description, - knowledge_name, - substitutions, - policy, - task_memory, - depth, - invert_results, - ): - if text_has_say_command(cause_text): - answer = await self._process_say_command(cause_text) - - elif text_has_retrieve_command(cause_text): - answer = await self._process_retrieve_command(cause_text) - - elif text_has_remember_command(cause_text): - answer = await self._process_remember_command(cause_text, knowledge_name) - - elif text_is_code(cause_text): - answer = await self._process_code( - cause_text, - code_description, - knowledge_name, - substitutions, - policy, - ) - - elif await text_is_text_generation_task( - cause_text, self._extractor.get_entailer() - ): - answer = await self._process_text_generation( - cause_text, knowledge_name, substitutions - ) - - else: - answer = await self._process_query( - cause_text, - cause_is_question, - task_memory, - knowledge_name, - policy, - depth, - inverted_rule=invert_results, - ) - if answer.is_neutral() and answer.variable: - answer = Answer.create_false() - - return answer - - async def _look_for_answer_in_facts( - self, query, task_memory, knowledge_name, depth - ): - self._log(f"Looking for answers in facts") - facts_and_thresholds = await self._knowledge.ask_for_facts_with_threshold( - query, is_from_user=depth == 0, knowledge_name=knowledge_name - ) - texts = cluster_facts(facts_and_thresholds) - for text in texts: - self._log(f"Answer within facts: The query is {query.text}") - self._log(f"Answer within facts: The context is {text}") - text = self._narrator.get_context_for_facts(text) - answer = await self._extractor.extract(query, text) - task_memory.add_story(text) - self._log(f"Answer within facts: The answer is {answer.text}") - return answer - - async def _look_for_answer_in_entailment(self, query, knowledge_name, depth): - self._log(f"Looking for answers in entailment") - if ":-" not in query.text: - return None - - hypothesis, premise = query.text.split(":-") - answer = await self._extractor.extract( - Query(text=premise, is_question=is_question(premise)), hypothesis - ) - return answer - - async def _look_for_answer_in_last_user_utterance( - self, query, task_memory, knowledge_name, depth - ): - self._log(f"Looking for answers in the user's last utterance") - if depth > 0 and task_memory.get_story() and query.is_question: - query.text = from_bot_to_bot(query.text) - user_utterances = [ - item.replace("user:", "The user says:") - for item in self._interface.get_utterances_list() - if "user:" in item - ] - if not user_utterances: - return None - - answer = await self._extractor.extract( - query, user_utterances[-1], task_memory - ) - if task_memory.text_is_in_prior_questions(answer.text): - answer.text = "unknown" - - if task_memory.text_is_in_prior_answers(answer.text): - answer.text = "unknown" - - task_memory.add_answer(answer.text) - - if not query.is_question: - return answer - - if normalized(answer.text) not in [ - "unknown", - "yes", - "no", - ]: - if answer.text[-1] == ".": - answer.text = answer.text[:-1] - - return answer - - async def _look_for_answer_in_task_memory( - self, query, task_memory, knowledge_name, depth - ): - self._log(f"Looking for answers in task memory") - if depth > 0 and task_memory.get_story() and query.is_question: - query.text = from_bot_to_bot(query.text) - answer = await self._extractor.extract( - query, task_memory.get_story(), task_memory - ) - if task_memory.text_is_in_prior_questions(answer.text): - answer.text = "unknown" - - if ( - not answer.is_true() - and not answer.is_false() - and task_memory.text_is_in_prior_answers(answer.text) - ): - answer.text = "unknown" - - task_memory.add_answer(answer.text) - - if not query.is_question: - return answer - - if answer.text[-1] == ".": - answer.text = answer.text[:-1] - - return answer - - async def _look_for_answer_by_asking_the_user( - self, query, task_memory, knowledge_name, policy, depth - ): - self._log(f"Looking for answers by asking the user") - if depth > 0 and query.is_question: - await self._flush_interface_output() - while True: - self._log(f"Asking the user: {query.text}") - await self._interface.output(query.text) - user_input_text = await self._interface.input() - self._log(f"The user replies: {user_input_text}") - if await self._query_has_better_match(query.text, user_input_text): - self._log(f"Found a better match for {user_input_text}", depth) - task_text = ( - await self._task_extractor.extract(user_input_text) - ).text - await self._interface.add_choice( - f"The bot tries to see if the new task can be '{task_text}'" - ) - await self._spin_up_another_inference_task( - user_input_text, - task_memory, - knowledge_name, - policy, - depth, - ) - await self._interface.add_choice( - f"The task '{task_text}' did not bring any result." - ) - - else: - break - - if normalized(user_input_text) == "yes": - user_answer = Answer(text="True") - - elif normalized(user_input_text) == "no": - user_answer = Answer(text="False") - - else: - story = ( - f"When asked '{query.text}', the user says: '{user_input_text}.'" - ) - query.text = from_bot_to_bot(query.text) - user_answer = await self._extractor.extract(query, story) - - self._log(f"The answer that is understood: {user_answer.text}") - - if is_yes_no_question(query.text): - user_answer = project_answer(user_answer, ["yes", "no"]) - if user_answer.text not in ["yes", "no"]: - await self._interface.output("Yes or No?") - user_answer = await self._look_for_answer_by_asking_the_user( - query, task_memory, knowledge_name, policy, depth - ) - - if user_answer.is_true(): - user_answer = Answer(text="True") - task_memory.add_story(story) - task_memory.add_question(query.text) - task_memory.add_answer("yes") - - elif user_answer.is_false(): - user_answer = Answer(text="False") - - else: - task_memory.add_story(story) - task_memory.add_question(query.text) - task_memory.add_answer(user_input_text) - - if not user_answer.is_neutral(): - if user_answer.text[-1] == ".": - user_answer.text = user_answer.text[:-1] - return user_answer - - async def _validate_question_in_effects(self, effect, query_text, substitutions): - answer = await self._extractor.extract(effect, query_text) - self._log("Validating question in the rule trigger.") - self._log(f"The query is {query_text}") - self._log(f"The answer is {answer.text}") - - if answer.is_false() or not answer_is_informative(answer): - return False - - if answer.variable: - update_substitutions_from_answer(answer, substitutions) - - return True - - async def _process_say_command(self, cause_text): - utterance = cause_text.strip()[3:].strip().capitalize() - self._log(f"Uttering: {utterance}") - self._sentences_to_utter.append(utterance) - answer = Answer.create_true() - return answer - - async def _process_remember_command(self, cause_text, knowledge_name): - utterance = cause_text[8:].strip() - if ":-" in utterance: - self._log( - f"Adding the following Rule to the knowledge name {knowledge_name}: {utterance}" - ) - await self._knowledge.add_rule(utterance, knowledge_name=knowledge_name) - - else: - self._log( - f"Adding the following Fact to the knowledge name {knowledge_name}: {utterance}" - ) - await self._knowledge.add(utterance, knowledge_name=knowledge_name) - - return Answer(text="True") - - async def _process_new_task_memory_command(self): - self._log(f"Erasing working memory") - return Answer(text="True") - - async def _validate_fact_in_effects(self, rule_effect_text, query, substitutions): - for key, value in substitutions.items(): - if key and value: - rule_effect_text = rule_effect_text.replace(key, value) - - answer = await self._extractor.extract(query, rule_effect_text) - self._log("Validating the statement in the rule trigger.") - self._log(f"The query is {rule_effect_text}") - self._log(f"The answer is {answer.text}") - return answer - - async def _process_code( - self, cause_text, code_description, knowledge_name, substitutions, policy - ): - variable = None - if "=" in cause_text: - variable, to_execute = cause_text.split("=") - variable = variable.strip() - to_execute = to_execute.strip() - - else: - to_execute = cause_text.strip() - - try: - if any( - item + "(" in to_execute for item in self._functions[knowledge_name] - ): - to_execute = add_function_arguments(to_execute) - - task_memory = ( - TaskMemory() - ) # task_memory is used as argument of the code in eval() - to_execute = escape_characters(to_execute) - self._log(f"Executing code: {to_execute}") - - if not code_description: - result = eval(f"self._module['{knowledge_name}'].{to_execute}") - if result is not None: - result = await result - - else: - generated_function_answer = await self._code_creator.extract( - cause_text + f" <{code_description}>" - ) - exec(generated_function_answer.text) - result = eval(to_execute) - - self._log(f"Execution result: {result}") - - except (CloseConversation, InterruptTask) as e: - _logger.warning(str(e)) - raise e - - except Exception as e: - traceback.print_exc() - _logger.warning(str(e)) - result = False - - if text_has_assigmnent(cause_text) and variable: - update_substitutions_from_results(result, variable, substitutions) - - if result != False: - answer = Answer(text=str(result)) - - else: - answer = Answer(text="False") - - return answer - - async def _process_text_generation(self, cause_text, knowledge_name, substitutions): - if "=" not in cause_text: - return Answer(text="False") - - position_of_assignment_operator = cause_text.find("=") - variable, prompt = ( - cause_text[:position_of_assignment_operator].strip(), - cause_text[position_of_assignment_operator + 1 :].strip(), - ) - variable = variable.strip() - prompt = prompt.strip() - answer = await self._prompt_predictor.predict(prompt) - answer.variable = variable - update_substitutions_from_results(answer.text, variable, substitutions) - return answer - - async def _process_query( - self, - cause_text, - cause_is_question, - task_memory, - knowledge_name, - policy, - depth, - inverted_rule, - ): - if "=" in cause_text: - variable, text = cause_text.split("=") - variable = variable.strip() - text = text.strip() - new_query = Query( - text=text, is_question=is_question(text), variable=variable - ) - - elif cause_is_question: - new_query = Query(text=cause_text, is_question=True) - - else: - new_query = Query(text=cause_text, is_question=False) - - additional_text = "" - if inverted_rule: - additional_text = "NOT " - - await self._interface.add_choice( - f"The bot tries the new query '{additional_text + new_query.text}'" - ) - answer = await self._compute_recursively( - new_query, task_memory, knowledge_name, policy, depth + 1, inverted_rule - ) - self._log(f"The answer to the query is {answer.text}", depth) - - if answer.variable and answer.is_neutral(): - return Answer(text="False", variable=answer.variable) - - if not new_query.variable: - await self._flush_interface_output() - - elif self._sentences_to_utter: - answer.text = "\n".join(self._sentences_to_utter) - self._sentences_to_utter = [] - - return answer - - def _log(self, text, depth=None): - if self._logger: - if depth: - self._logger.set_depth(depth) - - self._logger.write(f"BackwardInference: {text}", self._logger.level.INFO) - - def _init_python_modules(self, module_names): - if type(module_names) == str: - module_names = [module_names] - - for module_name in module_names: - create_preprocessed(module_name) - self._module[module_name] = import_module(module_name) - self._functions[module_name] = [ - item[0] for item in getmembers(self._module[module_name], isfunction) - ] - - async def _spin_up_another_inference_task( - self, input_text, task_memory, knowledge_name, policy, depth - ): - prior_conversation = self._narrator.summarize_dialogue() - working_memory = TaskMemory() - working_memory.add_story(prior_conversation) - query_text = f"The user says: '{input_text}.'" - working_memory.add_story(query_text) - query = Query( - text=query_text, - is_question=is_question(query_text), - variable="name", - ) - self._interface.bot_has_spoken(False) - self._log(f"Spinning up another inference task", depth) - await self._compute_recursively( - query, - task_memory, - knowledge_name, - policy, - depth, - ) - await self._flush_interface_output() - - async def _flush_interface_output(self): - for _ in range(len(self._sentences_to_utter)): - await self._interface.output(self._sentences_to_utter.pop(0)) - - async def _process_retrieve_command(self, cause_text): - cause_text = cause_text.replace("retrieve", "") - cause_text = cause_text.replace("RETRIEVE", "") - if "=" in cause_text: - variable, text = cause_text.split("=") - variable = variable.strip() - text = text.strip() - query = Query(text=text, is_question=is_question(text), variable=variable) - - else: - variable = None - query = Query(text=cause_text, is_question=is_question(cause_text)) - - facts = await self._knowledge.ask_for_facts_with_threshold(query, threshold=0.5) - return [Answer(text=item[0].text, variable=variable) for item in facts] - - async def _query_has_better_match( - self, - query: str, - user_answer: str, - ): - if is_question(user_answer): - return True - - if await self._knowledge.has_better_match(user_answer): - return True - - return False - - async def _answer_makes_sense( - self, query: str, user_answer: str, entailer: "Entailer" - ): - result = await entailer.entails( - f"bot: {query} user: {user_answer}", - "the user refuses to answer", - threshold=0.9, - return_threshold=True, - ) - if result: - return False - - result = await entailer.entails( - f"bot: {query} user: {user_answer}", - "The answer gives no information", - threshold=0.9, - return_threshold=True, - ) - if result: - return False - - return True diff --git a/wafl/interface/base_interface.py b/wafl/interface/base_interface.py index 0a331301..18089c28 100644 --- a/wafl/interface/base_interface.py +++ b/wafl/interface/base_interface.py @@ -4,11 +4,12 @@ class BaseInterface: - def __init__(self): + def __init__(self, decorator=None): self._is_listening = True self._choices = [] self._facts = [] self._utterances = [] + self._decorator = decorator async def output(self, text: str, silent: bool = False): raise NotImplementedError @@ -58,3 +59,9 @@ def reset_history(self): self._utterances = [] self._choices = [] self._facts = [] + + def _decorate_reply(self, text: str) -> str: + if not self._decorator: + return text + + return self._decorator.extract(text, self._utterances) diff --git a/wafl/interface/command_line_interface.py b/wafl/interface/command_line_interface.py index 0ca207c6..d7a5f695 100644 --- a/wafl/interface/command_line_interface.py +++ b/wafl/interface/command_line_interface.py @@ -1,6 +1,6 @@ import time -from wafl.simple_text_processing.deixis import from_bot_to_user, from_user_to_bot +from wafl.simple_text_processing.deixis import from_bot_to_user from wafl.interface.base_interface import BaseInterface from wafl.interface.utils import not_good_enough @@ -24,10 +24,10 @@ async def output(self, text: str, silent: bool = False): self.bot_has_spoken(True) async def input(self) -> str: - text = from_user_to_bot(input("user> ")).strip() + text = input("user> ").strip() while not_good_enough(text): await self.output("I did not quite understand that") - text = from_user_to_bot(input("user> ")) + text = input("user> ") self._utterances.append((time.time(), f"user: {text}")) return text diff --git a/wafl/interface/dummy_interface.py b/wafl/interface/dummy_interface.py index 936a2fae..1a8489bc 100644 --- a/wafl/interface/dummy_interface.py +++ b/wafl/interface/dummy_interface.py @@ -1,23 +1,29 @@ import re import time -from wafl.simple_text_processing.deixis import from_bot_to_user, from_user_to_bot +from wafl.simple_text_processing.deixis import from_bot_to_user from wafl.interface.base_interface import BaseInterface from wafl.interface.utils import not_good_enough class DummyInterface(BaseInterface): - def __init__(self, to_utter=None): + def __init__(self, to_utter=None, output_filter=None): super().__init__() self._to_utter = to_utter self._bot_has_spoken = False self._dialogue = "" + self._output_filter = output_filter async def output(self, text: str, silent: bool = False): if silent: print(text) return + if self._output_filter: + text = await self._output_filter.filter( + self.get_utterances_list_with_timestamp(), text + ) + self._dialogue += "bot: " + text + "\n" self._utterances.append((time.time(), f"bot: {from_bot_to_user(text)}")) self.bot_has_spoken(True) @@ -27,10 +33,10 @@ async def input(self) -> str: text = self.__remove_activation_word_and_normalize(text) while self._is_listening and not_good_enough(text): await self.output("I did not quite understand that") - text = from_user_to_bot(self._to_utter.pop(0)) + text = self._to_utter.pop(0) self._dialogue += "user: " + text + "\n" - utterance = from_user_to_bot(text) + utterance = text self._utterances.append((time.time(), f"user: {utterance}")) return utterance diff --git a/wafl/interface/queue_interface.py b/wafl/interface/queue_interface.py index 08fdc247..860e24af 100644 --- a/wafl/interface/queue_interface.py +++ b/wafl/interface/queue_interface.py @@ -5,17 +5,23 @@ class QueueInterface(BaseInterface): - def __init__(self): + def __init__(self, output_filter=None): 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 959b596b..44caea9a 100644 --- a/wafl/interface/voice_interface.py +++ b/wafl/interface/voice_interface.py @@ -4,7 +4,7 @@ import time from wafl.events.utils import remove_text_between_brackets -from wafl.simple_text_processing.deixis import from_bot_to_user, from_user_to_bot +from wafl.simple_text_processing.deixis import from_bot_to_user from wafl.interface.base_interface import BaseInterface from wafl.interface.utils import get_most_common_words, not_good_enough from wafl.listener.whisper_listener import WhisperListener @@ -19,7 +19,7 @@ class VoiceInterface(BaseInterface): - def __init__(self, config): + def __init__(self, config, output_filter=None): super().__init__() self._sound_speaker = SoundFileSpeaker() self._activation_sound_filename = self.__get_activation_sound_from_config( @@ -28,18 +28,21 @@ def __init__(self, config): self._deactivation_sound_filename = self.__get_deactivation_sound_from_config( config ) - self._deny_sound_filename = self.__get_deny_sound_from_config(config) - - self.listener_model_name = config.get_value("listener_model") + self.listener_model_name = config.get_value("listener_model")["local_model"] self._speaker = FairSeqSpeaker(config) self._listener = WhisperListener(config) - self._listener.set_timeout(config.get_value("listener_silence_timeout")) + self._listener.set_timeout( + config.get_value("listener_model")["listener_silence_timeout"] + ) self._listener.set_volume_threshold( - config.get_value("listener_volume_threshold") + config.get_value("listener_model")["listener_volume_threshold"] + ) + self._listener.set_hotword_threshold( + config.get_value("listener_model")["listener_hotword_logp"] ) - self._listener.set_hotword_threshold(config.get_value("listener_hotword_logp")) 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 @@ -63,6 +66,11 @@ 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}")) @@ -86,11 +94,11 @@ async def input(self) -> str: text = text.lower().capitalize() print(COLOR_START + "user> " + text + COLOR_END) - utterance = remove_text_between_brackets(from_user_to_bot(text)) + utterance = remove_text_between_brackets(text) if utterance.strip(): self._utterances.append((time.time(), f"user: {text}")) - return from_user_to_bot(text) + return text def bot_has_spoken(self, to_set: bool = None): if to_set != None: @@ -108,9 +116,6 @@ def deactivate(self): self._sound_speaker.speak(self._deactivation_sound_filename) super().deactivate() - def play_deny_sound(self): - self._sound_speaker.speak(self._deny_sound_filename) - def __get_activation_sound_from_config(self, config): if config.get_value("waking_up_sound"): return os.path.join(_path, "../sounds/activation.wav") @@ -123,12 +128,6 @@ def __get_deactivation_sound_from_config(self, config): return None - def __get_deny_sound_from_config(self, config): - if config.get_value("deny_sound"): - return os.path.join(_path, "../sounds/deny.wav") - - return None - def __remove_activation_word_and_normalize(self, text): activation_word = re.sub(r"\[(.*)\].*", r"\1", text) text = re.sub( diff --git a/wafl/knowledge/project_knowledge.py b/wafl/knowledge/project_knowledge.py deleted file mode 100644 index b70658c4..00000000 --- a/wafl/knowledge/project_knowledge.py +++ /dev/null @@ -1,183 +0,0 @@ -from typing import Dict, List -from wafl.knowledge.base_knowledge import BaseKnowledge -from wafl.knowledge.single_file_knowledge import SingleFileKnowledge -from wafl.knowledge.utils import get_first_cluster_of_rules -from wafl.parsing.rules_parser import get_dependency_list - - -class ProjectKnowledge(BaseKnowledge): - def __init__(self, config, rules_filename=None, logger=None): - self._config = config - self._logger = logger - self._dependency_dict = {} - self._knowledge_dict = {} - self.rules_filename = None - if rules_filename: - self._knowledge_dict = self._populate_knowledge_structure( - rules_filename, self._dependency_dict - ) - self.rules_filename = rules_filename - - async def add(self, text, knowledge_name=None): - if not knowledge_name: - knowledge_name = self.root_knowledge - - await self._knowledge_dict[knowledge_name].add( - text, knowledge_name=knowledge_name - ) - - async def add_rule(self, text, knowledge_name=None): - if not knowledge_name: - knowledge_name = self.root_knowledge - - await self._knowledge_dict[knowledge_name].add_rule( - text, knowledge_name=knowledge_name - ) - - async def ask_for_facts(self, query, is_from_user=False, knowledge_name=None): - if not knowledge_name: - knowledge_name = self.root_knowledge - - to_return = [] - for name in self._knowledge_dict.keys(): - if name in self._dependency_dict[knowledge_name]: - if self._logger: - self._logger.write(f"Project Knowledge: Asking for facts in {name}") - - to_return.extend( - await self._knowledge_dict[name].ask_for_facts(query, is_from_user) - ) - - return to_return - - async def ask_for_facts_with_threshold( - self, query, is_from_user=False, knowledge_name=None, threshold=None - ): - if not knowledge_name: - knowledge_name = self.root_knowledge - - to_return = [] - for name in self._knowledge_dict.keys(): - if name in self._get_all_dependency_names(knowledge_name): - if self._logger: - self._logger.write(f"Project Knowledge: Asking for facts in {name}") - - to_return.extend( - await self._knowledge_dict[name].ask_for_facts_with_threshold( - query, is_from_user, threshold=threshold - ) - ) - - return sorted(to_return, key=lambda x: -x[1]) - - async def ask_for_rule_backward(self, query, knowledge_name=None, first_n=None): - if not knowledge_name: - knowledge_name = self.root_knowledge - - rules_and_scores_list = [] - - for name in self._knowledge_dict.keys(): - if name in self._get_all_dependency_names(knowledge_name): - if self._logger: - self._logger.write(f"Project Knowledge: Asking for rules in {name}") - - rules_and_scores_list.extend( - await self._knowledge_dict[name]._ask_for_rule_backward_with_scores( - query, knowledge_name=name, first_n=first_n - ) - ) - - rules_and_scores_list = sorted(rules_and_scores_list, key=lambda x: -x[1]) - rules = get_first_cluster_of_rules(rules_and_scores_list)[:first_n] - return rules - - async def has_better_match( - self, query_text: str, knowledge_name: str = None - ) -> bool: - if not knowledge_name: - knowledge_name = self.root_knowledge - - result_list = [] - - for name in self._knowledge_dict.keys(): - if self._logger: - self._logger.write( - f"Project Knowledge: Asking for better match in {name}" - ) - - if name in self._get_all_dependency_names(knowledge_name): - result_list.append( - await self._knowledge_dict[name].has_better_match(query_text) - ) - - return any(result_list) - - def reload_rules(self, rules_filename: str): - self._knowledge_dict = self._populate_knowledge_structure( - rules_filename, self._dependency_dict - ) - - async def reinitialize_all_retrievers(self): - for knowledge in self._knowledge_dict.values(): - await knowledge._initialize_retrievers() - - def get_dependencies_list(self): - return self._get_all_dependency_names(self.root_knowledge) - - @staticmethod - def create_from_string( - config: "Configuration", rules_text: str, knowledge_name: str - ) -> "ProjectKnowledge": - knowledge = ProjectKnowledge(config) - knowledge._knowledge_dict[knowledge_name] = SingleFileKnowledge( - config, rules_text - ) - return knowledge - - def _populate_knowledge_structure( - self, filename: str, dependency_dict: Dict[str, List[str]] - ) -> Dict[str, SingleFileKnowledge]: - knowledge_structure = {} - with open(filename) as file: - text = file.read() - - name = _get_module_name_from_filename(filename) - knowledge_structure[name] = SingleFileKnowledge( - self._config, text, knowledge_name=name, logger=self._logger - ) - dependencies = get_dependency_list(text) - dependency_dict.setdefault(name, []) - dependency_dict[name].extend( - [self.root_knowledge + item for item in dependencies] - ) - for dependency_name in dependencies: - knowledge_structure.update( - self._populate_knowledge_structure( - f".{name}/{dependency_name}/rules.wafl", dependency_dict - ) - ) - - return knowledge_structure - - def _get_all_dependency_names(self, knowledge_name): - all_dependencies = [knowledge_name] - old_len = 0 - while len(all_dependencies) != old_len: - for dependency in all_dependencies: - if dependency in self._dependency_dict: - new_dependency_names = [ - dependency + item for item in self._dependency_dict[dependency] - ] - new_dependency_names = [ - item.replace("//", "/") for item in new_dependency_names - ] - all_dependencies.extend(new_dependency_names) - - old_len = len(all_dependencies) - - return all_dependencies - - -def _get_module_name_from_filename(filename): - filename = filename.replace("//", "/") - return "/" + "/".join(filename.split("/")[1:-1]) diff --git a/wafl/knowledge/single_file_knowledge.py b/wafl/knowledge/single_file_knowledge.py index a8c2dccb..8d69f1d1 100644 --- a/wafl/knowledge/single_file_knowledge.py +++ b/wafl/knowledge/single_file_knowledge.py @@ -10,14 +10,11 @@ from wafl.knowledge.base_knowledge import BaseKnowledge from wafl.knowledge.utils import ( text_is_exact_string, - rules_are_too_different, get_first_cluster_of_rules, filter_out_rules_that_are_too_dissimilar_to_query, - filter_out_rules_through_entailment, ) 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.entailer import Entailer from wafl.extractors.dataclasses import Query from wafl.retriever.string_retriever import StringRetriever from wafl.retriever.dense_retriever import DenseRetriever @@ -45,10 +42,9 @@ def __init__(self, config, rules_text=None, knowledge_name=None, logger=None): self._rules_dict = {} self._facts_retriever = DenseRetriever("text_embedding_model", config) self._facts_retriever_for_questions = DenseRetriever( - "qa_embedding_model", + "text_embedding_model", config, ) - self._entailer = Entailer(config, logger) self._rules_incomplete_retriever = DenseRetriever( "text_embedding_model", config ) @@ -305,19 +301,7 @@ async def _ask_for_rule_backward_with_scores( ][: self._max_rules_per_type] rules_and_scores = fact_rules + question_rules + incomplete_rules - rules = [item[0] for item in sorted(rules_and_scores, key=lambda x: -x[1])] - if not first_n and await rules_are_too_different( - self._rules_fact_retriever, rules - ): - return [] - rules_and_scores = filter_out_rules_that_are_too_dissimilar_to_query( query, rules_and_scores ) - - if not first_n: - rules_and_scores = await filter_out_rules_through_entailment( - self._entailer, query, rules_and_scores - ) - return rules_and_scores diff --git a/wafl/knowledge/utils.py b/wafl/knowledge/utils.py index f877e82a..6928bb24 100644 --- a/wafl/knowledge/utils.py +++ b/wafl/knowledge/utils.py @@ -16,6 +16,8 @@ async def rules_are_too_different(retriever, rules): if dot_products and min(dot_products) < 0.39: return False + return True + def get_first_cluster_of_rules(rules_and_threshold): if not rules_and_threshold: diff --git a/wafl/listener/whisper_listener.py b/wafl/listener/whisper_listener.py index 1572d919..4fbe75ff 100644 --- a/wafl/listener/whisper_listener.py +++ b/wafl/listener/whisper_listener.py @@ -4,12 +4,9 @@ import pyaudio import time import numpy as np -import torch.cuda from wafl.connectors.factories.whisper_connector_factory import WhisperConnectorFactory -device = "cuda" if torch.cuda.is_available() else "cpu" - class WhisperListener: _chunk = 1024 diff --git a/wafl/parsing/preprocess.py b/wafl/parsing/preprocess.py index 522d3bca..5a9db911 100644 --- a/wafl/parsing/preprocess.py +++ b/wafl/parsing/preprocess.py @@ -1,3 +1,4 @@ +import functools import importlib import os import re @@ -36,6 +37,7 @@ def clean_module_name(module_name): return return_path +@functools.lru_cache def create_preprocessed( module: str, functions_standard_name=_functions_standard_name, diff --git a/wafl/parsing/rules_parser.py b/wafl/parsing/rules_parser.py index b1d2c47a..d77a08ff 100644 --- a/wafl/parsing/rules_parser.py +++ b/wafl/parsing/rules_parser.py @@ -1,97 +1,30 @@ -from wafl.simple_text_processing.questions import is_question -from wafl.simple_text_processing.deixis import from_user_to_bot +import yaml + from wafl.facts import Fact -from wafl.parsing.utils import ( - get_lines_stripped_from_comments, - is_quoted_text, - text_has_interruption, - clean_text, - concatenate_slashes_into_one_single_line, -) from wafl.rules import Rule - - -def get_dependency_list(text: str): - _command_name = "#using" - dependency_list = [] - - for line in text.split("\n"): - line = line.strip() - if _command_name in line: - dependency_list.extend( - [item.strip() for item in line[len(_command_name) :].split(",")] - ) - - return dependency_list +from wafl.simple_text_processing.deixis import from_user_to_bot def get_facts_and_rules_from_text(text: str, knowledge_name=None): - text = concatenate_slashes_into_one_single_line(text) - lines = get_lines_stripped_from_comments(text) - lines.extend(["LAST"]) + parsed_text_dict = yaml.safe_load(text) + fact_strings = parsed_text_dict.get("facts", []) + rules_list = parsed_text_dict.get("rules", {}) facts = [] - rules = [] - - rule_length = 0 - current_fact = "" - causes = [] - for line in lines: - separation = line.find(line.strip()) - if separation > 0: - rule_length += 1 - text = line.strip() - causes.append( - Fact( - text=text, - is_question=is_question(text), - knowledge_name=knowledge_name, - ) + for text in fact_strings: + facts.append( + Fact( + text=from_user_to_bot(text), ) + ) - else: - text = line.strip() - if not text: - continue - - if current_fact: - if rule_length == 0: - facts.append(current_fact) - - else: - rules.append( - Rule( - effect=current_fact, - causes=causes, - knowledge_name=knowledge_name, - ) - ) - - causes = [] - rule_length = 0 - - if "=" in text: - sentence_is_question = True - variable, text = text.split("=") - text = text.strip() - variable = variable.strip() - - else: - sentence_is_question = False - variable = None - if is_quoted_text(text): - text = "The user says: " + from_user_to_bot(text) - - is_interruption = text_has_interruption(text) - if is_interruption: - text = clean_text(text) - - current_fact = Fact( - text=from_user_to_bot(text), - is_question=sentence_is_question, - variable=variable, - is_interruption=is_interruption, - knowledge_name=knowledge_name, + rules = [] + for rule_dict in rules_list: + rules.append( + Rule( + effect=Fact(text=list(rule_dict.keys())[0]), + causes=[Fact(item) for item in list(rule_dict.values())[0]], ) + ) return {"facts": facts, "rules": rules} diff --git a/wafl/policy/__init__.py b/wafl/policy/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/policy/answerer_policy.py b/wafl/policy/answerer_policy.py deleted file mode 100644 index 63d80cac..00000000 --- a/wafl/policy/answerer_policy.py +++ /dev/null @@ -1,11 +0,0 @@ -from wafl.connectors.remote.remote_llm_answer_policy_connector import ( - RemoteLLMAnswerPolicyConnector, -) - - -class AnswerPolicy: - def __init__(self, config, interface, logger=None): - self._logger = logger - - async def accept(self, result: str): - return True diff --git a/wafl/retriever/dense_retriever.py b/wafl/retriever/dense_retriever.py index b12ff63d..d6285267 100644 --- a/wafl/retriever/dense_retriever.py +++ b/wafl/retriever/dense_retriever.py @@ -18,7 +18,7 @@ def __init__(self, model_name, config): self._connector = SentenceEmbedderConnectorFactory.get_connector( model_name, config ) - self._embeddings_model = KeyedVectors(768) + self._embeddings_model = KeyedVectors(384) async def add_text_and_index(self, text: str, index: str): embeddings = await self._get_embeddings_from_text(text) diff --git a/wafl/rules.py b/wafl/rules.py index 2124c8d3..76fb5a48 100644 --- a/wafl/rules.py +++ b/wafl/rules.py @@ -14,6 +14,13 @@ def toJSON(self): def __str__(self): rule_str = self.effect.text for cause in self.causes: - rule_str += "\n " + cause.text + try: + rule_str += "\n " + cause.text + + except TypeError as e: + print(f"Error in rule:'''\n{rule_str}'''") + print("Perhaps the YAML file is not well formatted.") + print() + raise e return rule_str diff --git a/wafl/run.py b/wafl/run.py index 0b6555c8..b0397e84 100644 --- a/wafl/run.py +++ b/wafl/run.py @@ -4,7 +4,6 @@ from wafl.exceptions import CloseConversation from wafl.events.conversation_events import ConversationEvents from wafl.interface.command_line_interface import CommandLineInterface -from wafl.knowledge.project_knowledge import ProjectKnowledge from wafl.logger.local_file_logger import LocalFileLogger from wafl.testcases import ConversationTestCases from wafl.variables import get_variables @@ -21,11 +20,9 @@ def print_incipit(): def run_from_command_line(): interface = CommandLineInterface() config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl", logger=_logger) conversation_events = ConversationEvents( - knowledge, + config=config, interface=interface, - code_path=knowledge.get_dependencies_list(), logger=_logger, ) asyncio.run(interface.output("Hello. How may I help you?")) @@ -42,13 +39,10 @@ def run_from_command_line(): def run_testcases(): print("Running the testcases in testcases.txt\n") config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl") test_cases_text = open("testcases.txt").read() testcases = ConversationTestCases( config, test_cases_text, - knowledge, - code_path=knowledge.get_dependencies_list(), logger=_logger, ) asyncio.run(testcases.run()) diff --git a/wafl/runners/routes.py b/wafl/runners/routes.py new file mode 100644 index 00000000..169bfabc --- /dev/null +++ b/wafl/runners/routes.py @@ -0,0 +1,127 @@ +import asyncio +import os +import random +import sys +import threading + +from flask import Flask, render_template, redirect, url_for +from flask_cors import CORS +from wafl.config import Configuration +from wafl.events.conversation_events import ConversationEvents +from wafl.interface.queue_interface import QueueInterface +from wafl.knowledge.single_file_knowledge import SingleFileKnowledge +from wafl.logger.local_file_logger import LocalFileLogger +from wafl.scheduler.conversation_loop import ConversationLoop +from wafl.scheduler.scheduler import Scheduler +from wafl.scheduler.web_loop import WebLoop + +_path = os.path.dirname(__file__) +_logger = LocalFileLogger() +app = Flask( + __name__, + static_url_path="", + static_folder=os.path.join(_path, "../frontend/"), + template_folder=os.path.join(_path, "../frontend/"), +) +CORS(app) + + +@app.route("/create_new_instance", methods=["POST"]) +def create_new_instance(): + conversation_id = random.randint(0, sys.maxsize) + result = create_scheduler_and_webserver_loop(conversation_id) + add_new_rules(app, conversation_id, result["web_server_loop"]) + thread = threading.Thread(target=result["scheduler"].run) + thread.start() + return redirect(url_for(f"index_{conversation_id}")) + + +@app.route("/") +async def index(): + return render_template("selector.html") + + +def get_app(): + return app + + +def create_scheduler_and_webserver_loop(conversation_id): + config = Configuration.load_local_config() + interface = QueueInterface() + interface.activate() + conversation_events = ConversationEvents( + config=config, + interface=interface, + logger=_logger, + ) + conversation_loop = ConversationLoop( + interface, + conversation_events, + _logger, + activation_word="", + max_misses=-1, + deactivate_on_closed_conversation=False, + ) + asyncio.run(interface.output("Hello. How may I help you?")) + web_loop = WebLoop(interface, conversation_id, conversation_events) + return { + "scheduler": Scheduler([conversation_loop, web_loop]), + "web_server_loop": web_loop, + } + + +def add_new_rules(app, conversation_id, web_server_loop): + app.add_url_rule( + f"/{conversation_id}/", + f"index_{conversation_id}", + web_server_loop.index, + methods=["GET"], + ) + app.add_url_rule( + f"/{conversation_id}/reset_conversation", + f"reset_conversation_{conversation_id}", + web_server_loop.reset_conversation, + methods=["POST"], + ) + app.add_url_rule( + f"/{conversation_id}/reload_rules", + f"reload_rules_{conversation_id}", + web_server_loop.reload_rules, + methods=["POST"], + ) + app.add_url_rule( + f"/{conversation_id}/check_new_messages", + f"check_new_messages_{conversation_id}", + web_server_loop.check_for_new_messages, + methods=["POST"], + ) + app.add_url_rule( + f"/{conversation_id}/load_messages", + f"load_messages_{conversation_id}", + web_server_loop.load_messages, + methods=["POST", "GET"], + ) + app.add_url_rule( + f"/{conversation_id}/input", + f"input_{conversation_id}", + web_server_loop.handle_input, + methods=["POST", "GET"], + ) + app.add_url_rule( + f"/{conversation_id}/output", + f"output_{conversation_id}", + web_server_loop.handle_output, + methods=["POST"], + ) + app.add_url_rule( + f"/{conversation_id}/thumbs_up", + f"thumbs_up_{conversation_id}", + web_server_loop.thumbs_up, + methods=["POST"], + ) + app.add_url_rule( + f"/{conversation_id}/thumbs_down", + f"thumbs_down_{conversation_id}", + web_server_loop.thumbs_down, + methods=["POST"], + ) diff --git a/wafl/runners/run_from_audio.py b/wafl/runners/run_from_audio.py index fc11e0f9..ddd5fa53 100644 --- a/wafl/runners/run_from_audio.py +++ b/wafl/runners/run_from_audio.py @@ -1,12 +1,8 @@ from wafl.config import Configuration from wafl.events.conversation_events import ConversationEvents -from wafl.events.events_from_module_name import EventsCreatorFromModuleName -from wafl.events.generated_events import GeneratedEvents from wafl.interface.voice_interface import VoiceInterface -from wafl.knowledge.project_knowledge import ProjectKnowledge from wafl.logger.local_file_logger import LocalFileLogger from wafl.scheduler.conversation_loop import ConversationLoop -from wafl.scheduler.generated_event_loop import GeneratedEventLoop from wafl.scheduler.scheduler import Scheduler _logger = LocalFileLogger() @@ -14,13 +10,10 @@ def run_from_audio(): config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl", logger=_logger) interface = VoiceInterface(config) conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), config=config, + interface=interface, logger=_logger, ) conversation_loop = ConversationLoop( @@ -29,18 +22,7 @@ def run_from_audio(): _logger, activation_word=config.get_value("waking_up_word"), ) - generated_events = GeneratedEvents( - config, - knowledge, - events=EventsCreatorFromModuleName("events"), - interface=interface, - ) - events_loop = GeneratedEventLoop( - interface, - generated_events, - logger=_logger, - ) - scheduler = Scheduler([conversation_loop, events_loop]) + scheduler = Scheduler([conversation_loop]) scheduler.run() diff --git a/wafl/runners/run_web_interface.py b/wafl/runners/run_web_interface.py index 54ad98fd..35814571 100644 --- a/wafl/runners/run_web_interface.py +++ b/wafl/runners/run_web_interface.py @@ -1,40 +1,11 @@ -import asyncio +from wafl.runners.routes import get_app -from wafl.config import Configuration -from wafl.events.conversation_events import ConversationEvents -from wafl.interface.queue_interface import QueueInterface -from wafl.knowledge.project_knowledge import ProjectKnowledge -from wafl.logger.local_file_logger import LocalFileLogger -from wafl.scheduler.conversation_loop import ConversationLoop -from wafl.scheduler.scheduler import Scheduler -from wafl.scheduler.web_loop import WebLoop +app = get_app() -_logger = LocalFileLogger() - -def run_server(): - interface = QueueInterface() - config = Configuration.load_local_config() - knowledge = ProjectKnowledge(config, "rules.wafl", logger=_logger) - interface.activate() - conversation_events = ConversationEvents( - knowledge, - interface=interface, - code_path=knowledge.get_dependencies_list(), - logger=_logger, - ) - conversation_loop = ConversationLoop( - interface, - conversation_events, - _logger, - activation_word="", - max_misses=-1, - ) - asyncio.run(interface.output("Hello. How may I help you?")) - web_loop = WebLoop(interface, knowledge) - scheduler = Scheduler([conversation_loop, web_loop]) - asyncio.run(scheduler.run()) +def run_app(): + app.run(host="0.0.0.0", port=8889) if __name__ == "__main__": - run_server() + run_app() diff --git a/wafl/connectors/local/__init__.py b/wafl/scheduler/__init__.py similarity index 100% rename from wafl/connectors/local/__init__.py rename to wafl/scheduler/__init__.py diff --git a/wafl/scheduler/conversation_loop.py b/wafl/scheduler/conversation_loop.py index 94554fa1..3b9dfe09 100644 --- a/wafl/scheduler/conversation_loop.py +++ b/wafl/scheduler/conversation_loop.py @@ -1,18 +1,26 @@ import asyncio import random +import traceback from wafl.exceptions import CloseConversation class ConversationLoop: def __init__( - self, interface, conversation, logger, activation_word="", max_misses=3 + self, + interface, + conversation, + logger, + activation_word="", + max_misses=3, + deactivate_on_closed_conversation=True, ): self._interface = interface self._conversation_events = conversation self._logger = logger self._activation_word = activation_word self._max_misses = max_misses + self._deactivate_on_closed_conversation = deactivate_on_closed_conversation async def run(self): await self._say_initial_greetings() @@ -87,7 +95,12 @@ async def _main_loop(self): self._logger.write( f"Closing the conversation", log_level=self._logger.level.INFO ) - self._interface.deactivate() + if self._deactivate_on_closed_conversation: + self._interface.deactivate() + + else: + await self._interface.output("I am not sure what to reply.") + continue except Exception as e: @@ -96,4 +109,8 @@ async def _main_loop(self): ) self._logger.write(str(e), log_level=self._logger.level.ERROR) print("Error in conversation loop. Closing the conversation.") + await self._interface.output( + "Error in the conversation loop. Marking this conversation as a failure." + ) print(str(e)) + traceback.print_stack() diff --git a/wafl/scheduler/web_interface_implementation.py b/wafl/scheduler/web_interface_implementation.py new file mode 100644 index 00000000..bec8e520 --- /dev/null +++ b/wafl/scheduler/web_interface_implementation.py @@ -0,0 +1,28 @@ +import re + + +def _change_code_wrapper(text): + pattern = r"```.*?\n(.*?)```" + num_rows = text.count("\n") - 1 + + def replace_code(match): + code = match.group(1) + return f'' + + return re.sub(pattern, replace_code, text, flags=re.DOTALL) + + +def get_html_from_dialogue_item( + text, +): + if text.find("bot:") == 0: + return ( + f"
    " + _change_code_wrapper(text[4:]).strip() + "
    " + ) + + return ( + "
    " + + text[5:].strip() + + "
    " + ) diff --git a/wafl/scheduler/web_loop.py b/wafl/scheduler/web_loop.py index 53afd5be..191bc5ae 100644 --- a/wafl/scheduler/web_loop.py +++ b/wafl/scheduler/web_loop.py @@ -1,120 +1,119 @@ import asyncio -import logging import os -import threading -from flask import Flask, render_template, request, jsonify -from flask_cors import CORS +from flask import render_template, request, jsonify from wafl.interface.queue_interface import QueueInterface from wafl.logger.history_logger import HistoryLogger +from wafl.scheduler.web_interface_implementation import ( + get_html_from_dialogue_item, +) _path = os.path.dirname(__file__) -app = Flask( - __name__, - static_url_path="", - static_folder=os.path.join(_path, "../frontend/"), - template_folder=os.path.join(_path, "../frontend/"), -) -app.config.from_object(__name__) -CORS(app, resources={r"/*": {"origins": "*"}}) -app.config["async_mode"] = "asyncio" -log = logging.getLogger("werkzeug") -log.setLevel(logging.WARNING) class WebLoop: - def __init__(self, interface: QueueInterface, knowledge: "ProjectKnowledge"): + def __init__( + self, + interface: QueueInterface, + conversation_id: int, + conversation_events: "ConversationEvents", + ): self._interface = interface - self._knowledge = knowledge - self._rules_filename = knowledge.rules_filename - self._hystory_logger = HistoryLogger(self._interface) + self._history_logger = HistoryLogger(self._interface) + self._conversation_id = conversation_id + self._conversation_events = conversation_events + self._prior_dialogue_items = "" + + async def index(self): + return render_template("index.html", conversation_id=self._conversation_id) + + async def handle_input(self): + query = request.form["query"] + self._interface.input_queue.append(query) + return f""" + + """.strip() + + async def reset_conversation(self): + self._interface.reset_history() + self._interface.deactivate() + self._interface.activate() + self._conversation_events.reload_knowledge() + await self._interface.output("Hello. How may I help you?") + conversation = await self._get_conversation() + return conversation - async def run(self): - @app.route("/input", methods=["POST"]) - async def handle_input(): - query = request.form["query"] - self._interface.input_queue.append(query) + async def reload_rules(self): + async with asyncio.Lock(): + print("Not implemented yet") + + return "" + + async def check_for_new_messages(self): + conversation = await self._get_conversation() + if conversation != self._prior_dialogue_items: + self._prior_dialogue_items = conversation return f""" - -
    -
    - """.strip() - - @app.route("/reset_conversation", methods=["POST"]) - async def reset_conversation(): - self._interface.reset_history() - await self._interface.output("Hello. How may I help you?") - conversation = await self._get_conversation() - return conversation - - @app.route("/reload_rules", methods=["POST"]) - async def reload_rules(): - async with asyncio.Lock(): - self._knowledge.reload_rules(self._rules_filename) - await self._knowledge.reinitialize_all_retrievers() - print("Rules reloaded") - - return "" - - @app.route("/load_messages", methods=["POST"]) - async def load_messages(): - conversation = await self._get_conversation() - return conversation - - @app.route("/output") - async def handle_output(): - if not self._interface.output_queue: - return jsonify({"text": "", "silent": False}) - - output = self._interface.output_queue.pop(0) - return jsonify(output) - - @app.route("/thumbs_up", methods=["POST"]) - async def thumbs_up(): - self._hystory_logger.write("thumbs_up") - return jsonify("") - - @app.route("/thumbs_down", methods=["POST"]) - async def thumbs_down(): - self._hystory_logger.write("thumbs_down") - return jsonify("") - - @app.route("/") - async def index(): - return render_template("index.html") - - def run_app(): - app.run(host="0.0.0.0", port=8889) - - thread = threading.Thread(target=run_app) - thread.start() +
    """ + + else: + self._prior_dialogue_items = conversation + return "
    " + + async def load_messages(self): + conversation = await self._get_conversation() + return conversation + + async def handle_output(self): + if not self._interface.output_queue: + return jsonify({"text": "", "silent": False}) + + output = self._interface.output_queue.pop(0) + return jsonify(output) + + async def thumbs_up(self): + self._history_logger.write("thumbs_up") + return jsonify("") + + async def thumbs_down(self): + self._history_logger.write("thumbs_down") + return jsonify("") + + async def run(self): + print(f"New web server instance {self._conversation_id} running!") + return async def _get_conversation(self): - dialogue = self._interface.get_utterances_list_with_timestamp() - dialogue = [ - ( - item[0], - "
    " - + item[1] - + "
    ", + dialogue_items = self._interface.get_utterances_list_with_timestamp() + dialogue = [] + for index, item in enumerate(dialogue_items): + dialogue.append( + ( + item[0], + get_html_from_dialogue_item( + item[1], + ), + ) ) - for item in dialogue - ] + choices = self._interface.get_choices_and_timestamp() choices = [ ( item[0], - "
    " - + item[1] - + "
    ", + "
    " + item[1] + "
    ", ) for item in choices ] @@ -122,9 +121,7 @@ async def _get_conversation(self): facts = [ ( item[0], - "
    " - + item[1] - + "
    ", + "
    " + item[1] + "
    ", ) for item in facts ] @@ -134,10 +131,12 @@ async def _get_conversation(self): dialogue_items = dialogue dialogue_items = sorted(dialogue_items, key=lambda x: x[0])[::-1] dialogue_items = [item[1] for item in dialogue_items] - conversation = "
    " + conversation = ( + "
    " + ) conversation += "".join(dialogue_items) conversation += "
    " - conversation += "
    " + conversation += "
    " conversation += "".join(choices_and_facts) conversation += "
    " return conversation diff --git a/wafl/templates/config.json b/wafl/templates/config.json index d7548499..4bb4fa90 100644 --- a/wafl/templates/config.json +++ b/wafl/templates/config.json @@ -1,58 +1,26 @@ { - "allow_interruptions": true, "waking_up_word": "computer", "waking_up_sound": true, "deactivate_sound": true, - "improvise_tasks": true, + "rules": "rules.yaml", + "functions": "functions.py", "llm_model": { - "model_is_local": true, - "local_model": "mosaicml/mpt-7b-instruct", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "model_host": "localhost", + "model_port": 8080 }, "listener_model": { - "model_is_local": true, - "local_model": "fractalego/personal-whisper-medium.en-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": true, - "local_model": "facebook/fastspeech2-en-ljspeech", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } - }, - "entailment_model": { - "model_is_local": true, - "local_model": "MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "model_host": "localhost", + "model_port": 8080 }, "text_embedding_model": { - "model_is_local": true, - "local_model": "msmarco-distilbert-base-v3", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } - }, - "qa_embedding_model": { - "model_is_local": true, - "local_model": "multi-qa-distilbert-dot-v1", - "remote_model": { - "model_host": "localhost", - "model_port": 8080 - } + "model_host": "localhost", + "model_port": 8080 } } diff --git a/wafl/templates/db.json b/wafl/templates/db.json new file mode 100644 index 00000000..7bf6f19c --- /dev/null +++ b/wafl/templates/db.json @@ -0,0 +1,5 @@ +{ + "shopping_list": [], + "latitude": 51.5074, + "longitude": 0.1272 +} \ No newline at end of file diff --git a/wafl/templates/events.py b/wafl/templates/events.py deleted file mode 100644 index 69d3114c..00000000 --- a/wafl/templates/events.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import datetime - - -def get_date(): - now = datetime.now() - return "Today's date is " + now.strftime("%A %d %B %Y") - - -def get_clock(): - now = datetime.now() - minutes = int(now.strftime("%M")) - hour = int(now.strftime("%H")) - return f"The time is {hour}, {minutes} " - - -def get_time_in_natural_language(): - now = datetime.now() - minutes = int(now.strftime("%M")) - hour = int(now.strftime("%H")) - if minutes <= 30: - return f"The time is {minutes} past {hour}" - - else: - return f"The time is {60 - minutes} to {hour + 1}" diff --git a/wafl/templates/functions.py b/wafl/templates/functions.py new file mode 100644 index 00000000..4936c99b --- /dev/null +++ b/wafl/templates/functions.py @@ -0,0 +1,113 @@ +import json +import requests + +from bs4 import BeautifulSoup +from datetime import datetime, timedelta +from wafl.exceptions import CloseConversation + +_db_filename = "db.json" + + +def close_conversation(): + raise CloseConversation() + + +def check_today_weather(): + today = datetime.now().strftime("%Y-%m-%d") + with open(_db_filename) as file: + db = json.load(file) + latitude = db["latitude"] + longitude = db["longitude"] + + return check_weather_lat_long(latitude, longitude, today) + + +def check_tomorrow_weather(): + today = datetime.now() + with open(_db_filename) as file: + db = json.load(file) + latitude = db["latitude"] + longitude = db["longitude"] + + tomorrow = (today + timedelta(days=1)).strftime("%Y-%m-%d") + return check_weather_lat_long(latitude, longitude, tomorrow) + + +def check_weather_lat_long(latitude, longitude, day): + secrets = json.load(open("secrets.json")) + result = requests.get( + f"https://rgw.5878-e94b1c46.eu-gb.apiconnect.appdomain.cloud/metoffice/production/v0/forecasts/point/daily?excludeParameterMetadata=true&includeLocationName=true&latitude={latitude}&longitude={longitude}", + headers={ + "X-IBM-Client-Id": secrets["key"], + "X-IBM-Client-Secret": secrets["secret"], + }, + ) + data = result.json() + if "features" not in data: + return "There is a connection error to the weather API. Please try later. " + to_say = "" + for item in data["features"][0]["properties"]["timeSeries"]: + if day in item["time"]: + to_say += f"The temperature should be between {int(item['dayLowerBoundMaxTemp'])} and {int(item['dayUpperBoundMaxTemp'])}. " + if item["dayProbabilityOfPrecipitation"] != 0: + to_say += f"The probability of rain is {item['dayProbabilityOfPrecipitation']} percent. " + + else: + to_say += "There is no probability of rain. " + + if item["dayProbabilityOfSnow"] != 0: + to_say += f"The probability of snow is {item['dayProbabilityOfSnow']} percent. " + + else: + to_say += "There is no probability of snow. " + + return to_say + + +def get_website(url): + response = requests.get(url) + soup = BeautifulSoup(response.content, "html.parser") + text = soup.get_text() + return text + + +def get_time(): + return datetime.now().strftime("%H:%M") + + +def get_date(): + return datetime.now().strftime("%Y-%m-%d") + + +def get_day(): + return datetime.now().strftime("%A") + + +def add_to_shopping_list(list_of_items_to_add): + db = json.load(open(_db_filename)) + for item in list_of_items_to_add: + if item not in db["shopping_list"]: + db["shopping_list"].append(item) + + json.dump(db, open(_db_filename, "w")) + + return "Item added" + + +def remove_shopping_list(list_of_items_to_remove): + db = json.load(open(_db_filename)) + for item in list_of_items_to_remove: + if item in db["shopping_list"]: + db["shopping_list"].remove(item) + + json.dump(db, open(_db_filename, "w")) + + return "Item removed" + + +def get_shopping_list(): + db = json.load(open(_db_filename)) + if db["shopping_list"] == []: + return "nothing" + + return ", ".join(db["shopping_list"]) diff --git a/wafl/templates/main.py b/wafl/templates/main.py new file mode 100644 index 00000000..22feffd3 --- /dev/null +++ b/wafl/templates/main.py @@ -0,0 +1,4 @@ +from wafl.runners.run_web_interface import app + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8889) diff --git a/wafl/templates/requirements.txt b/wafl/templates/requirements.txt new file mode 100644 index 00000000..ec9ce87f --- /dev/null +++ b/wafl/templates/requirements.txt @@ -0,0 +1 @@ +bs4==0.0.1 diff --git a/wafl/templates/rules.yaml b/wafl/templates/rules.yaml new file mode 100644 index 00000000..78eee512 --- /dev/null +++ b/wafl/templates/rules.yaml @@ -0,0 +1,48 @@ +facts: + - This bot is doing well + - This bot is called Computer + +rules: + - the user wants to compute some math operation: + - Think of the python code that solves the math problem and assigns the result to the variable "result" + - For example "what is the result of 2 + 2?" should output "result = 2 + 2" + - Another example "what is the square root of 2?" should output "import math;result = math.sqrt(2)" + - output exactly the following "result = PYTHON CODE THAT SOLVES THE PROBLEM" + + - the user wants to know the time: + - output "The time is get_time()". + + - the user wants to know today's date: + - output "The date is get_date()". + + - the user wants to know today's day of the week: + - output "The day of the week is get_day()". + + - the user wants to know the weather today: + - output "check_today_weather()". + + - the user wants to know the weather tomorrow: + - output "check_tomorrow_weather()". + + - the user wants to summarise a website: + - you'll need the website url to summarise + - output exactly " The website content is get_website('WEBSITE_URL') ". + - summarise the website content given what you remember + - output the summary + + - the user wants to know what is in the shopping list: + - output "get_shopping_list()". + + - the user wants to add something to the shopping list: + - The task here is to add the item to the shopping list using a python function + - example "add milk to the shopping list" should output "add_to_shopping_list(['milk'])" + - output "add_to_shopping_list(ITEMS_TO_ADD)". + + - the user wants to remove something to the shopping list: + - The task here is to remove the item from the shopping list using a python function + - example "remove milk from the shopping list" should output "remove_from_shopping_list(['milk'])" + - output "remove_from_shopping_list(ITEMS_TO_REMOVE)". + + - the user asks something about cities, capitals, countries, buildings, famous people, bars, restaurants, rivers, mountains, lakes, seas, oceans, planets, stars, galaxies: + - say that you are just improvising the answer + - say what you think answer the question \ No newline at end of file diff --git a/wafl/templates/sample_project/coding/functions.py b/wafl/templates/sample_project/coding/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/templates/sample_project/coding/rules.wafl b/wafl/templates/sample_project/coding/rules.wafl deleted file mode 100644 index b4a936bc..00000000 --- a/wafl/templates/sample_project/coding/rules.wafl +++ /dev/null @@ -1,11 +0,0 @@ -This bot can answer coding questions. - -Create a function in Python - language = what language is the function in? - name = what is the name of the function? - goal = what is the goal of the function? - function = Write a {language} code called {name} that accomplishes the following: {goal} - SAY This is what I could come up with: - SAY - SAY {function} - SAY diff --git a/wafl/templates/sample_project/facts/functions.py b/wafl/templates/sample_project/facts/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/templates/sample_project/facts/rules.wafl b/wafl/templates/sample_project/facts/rules.wafl deleted file mode 100644 index ce348911..00000000 --- a/wafl/templates/sample_project/facts/rules.wafl +++ /dev/null @@ -1,6 +0,0 @@ -# Simple initial facts - -This bot is doing well -This bot is called Computer -This bot is doing okay. -This bot is here to answer the user unless asked to be silent. diff --git a/wafl/templates/sample_project/functions.py b/wafl/templates/sample_project/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/templates/sample_project/greetings/functions.py b/wafl/templates/sample_project/greetings/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/templates/sample_project/greetings/rules.wafl b/wafl/templates/sample_project/greetings/rules.wafl deleted file mode 100644 index 2ba9bdf2..00000000 --- a/wafl/templates/sample_project/greetings/rules.wafl +++ /dev/null @@ -1,9 +0,0 @@ -# Greetings commands -"bring yourself online" - SAY Hello there! - -"Hello" - ! This bot has greeted the user - SAY Hello there! - REMEMBER This bot has greeted the user - diff --git a/wafl/templates/sample_project/interruptions/functions.py b/wafl/templates/sample_project/interruptions/functions.py deleted file mode 100644 index 959fada6..00000000 --- a/wafl/templates/sample_project/interruptions/functions.py +++ /dev/null @@ -1,9 +0,0 @@ -from wafl.exceptions import CloseConversation, InterruptTask - - -def close_conversation(): - raise CloseConversation - - -def close_task(): - raise InterruptTask diff --git a/wafl/templates/sample_project/interruptions/rules.wafl b/wafl/templates/sample_project/interruptions/rules.wafl deleted file mode 100644 index 359d3737..00000000 --- a/wafl/templates/sample_project/interruptions/rules.wafl +++ /dev/null @@ -1,33 +0,0 @@ -# End the conversation - -_close - close_conversation() - -INTERRUPTION the user believes they don't need anything else - SAY ok - _close - -INTERRUPTION the user says "good bye" - SAY good bye! - _close - -INTERRUPTION the user says to shut up - SAY ok - _close - -INTERRUPTION the user asks this bot to be silent - _close - -INTERRUPTION the user says: "thank you" - _close - -INTERRUPTION the user says: "thank you this is all" - _close - -INTERRUPTION the user wants to stop - ! Do you want to continue? - close_task() - -INTERRUPTION "nevermind" - close_task() - diff --git a/wafl/templates/sample_project/reminders/functions.py b/wafl/templates/sample_project/reminders/functions.py deleted file mode 100644 index 4ae8dfde..00000000 --- a/wafl/templates/sample_project/reminders/functions.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import datetime, timedelta -from word2number import w2n - - -def get_integer_from_string(text): - words = text.split() - for word in words: - number = w2n.word_to_num(word) - if number is not None: - return number - - return None - - -def get_time_in_future(minutes_from_now): - num_minutes = get_integer_from_string(minutes_from_now) - if not num_minutes: - return False - - now = datetime.now() - final_time = now + timedelta(minutes=num_minutes) - return final_time.strftime("%H, %M") diff --git a/wafl/templates/sample_project/reminders/interruptions/__wafl_functions.py b/wafl/templates/sample_project/reminders/interruptions/__wafl_functions.py deleted file mode 100644 index 4bfcd009..00000000 --- a/wafl/templates/sample_project/reminders/interruptions/__wafl_functions.py +++ /dev/null @@ -1,9 +0,0 @@ -from wafl.exceptions import CloseConversation, InterruptTask - - -def close_conversation(inference, task_memory): - raise CloseConversation - - -def close_task(inference, task_memory): - raise InterruptTask diff --git a/wafl/templates/sample_project/reminders/interruptions/functions.py b/wafl/templates/sample_project/reminders/interruptions/functions.py deleted file mode 100644 index 959fada6..00000000 --- a/wafl/templates/sample_project/reminders/interruptions/functions.py +++ /dev/null @@ -1,9 +0,0 @@ -from wafl.exceptions import CloseConversation, InterruptTask - - -def close_conversation(): - raise CloseConversation - - -def close_task(): - raise InterruptTask diff --git a/wafl/templates/sample_project/reminders/interruptions/rules.wafl b/wafl/templates/sample_project/reminders/interruptions/rules.wafl deleted file mode 100644 index 359d3737..00000000 --- a/wafl/templates/sample_project/reminders/interruptions/rules.wafl +++ /dev/null @@ -1,33 +0,0 @@ -# End the conversation - -_close - close_conversation() - -INTERRUPTION the user believes they don't need anything else - SAY ok - _close - -INTERRUPTION the user says "good bye" - SAY good bye! - _close - -INTERRUPTION the user says to shut up - SAY ok - _close - -INTERRUPTION the user asks this bot to be silent - _close - -INTERRUPTION the user says: "thank you" - _close - -INTERRUPTION the user says: "thank you this is all" - _close - -INTERRUPTION the user wants to stop - ! Do you want to continue? - close_task() - -INTERRUPTION "nevermind" - close_task() - diff --git a/wafl/templates/sample_project/reminders/rules.wafl b/wafl/templates/sample_project/reminders/rules.wafl deleted file mode 100644 index 4a6524f1..00000000 --- a/wafl/templates/sample_project/reminders/rules.wafl +++ /dev/null @@ -1,16 +0,0 @@ -the user wants to set an alarm at a specific time - time = At what time? - ERASE MEMORY - sentence = what do you want to be reminded of? - REMEMBER the time is {time} :- SAY {sentence} - SAY An alarm was created for {time} - -the user wants to set an alarm in minutes from now - minutes_from_now = how many minutes from now? do not use the word 'minute' - time = get_time_in_future(minutes_from_now) - ERASE MEMORY - sentence = what do you want to be reminded of? - REMEMBER the time is {time} :- SAY {sentence} - SAY An alarm was created in {minutes_from_now} minutes - SAY The alarm time is therefore {time} - SAY I will say the sentence {sentence} diff --git a/wafl/templates/sample_project/rules.wafl b/wafl/templates/sample_project/rules.wafl deleted file mode 100644 index 4dbb4eb5..00000000 --- a/wafl/templates/sample_project/rules.wafl +++ /dev/null @@ -1,9 +0,0 @@ -#using coding -#using facts -#using greetings -#using time-queries -#using shopping-list -#using weather -#using writing -#using interruptions -#using reminders diff --git a/wafl/templates/sample_project/shopping-list/functions.py b/wafl/templates/sample_project/shopping-list/functions.py deleted file mode 100644 index e58c2e99..00000000 --- a/wafl/templates/sample_project/shopping-list/functions.py +++ /dev/null @@ -1,84 +0,0 @@ -import json - -from fuzzywuzzy import process - - -def get_shopping_list_in_english(): - shopping_list = json.load(open("shopping_list.json")) - if not shopping_list: - return "Nothing" - - return ", ".join(shopping_list) - - -def add_shopping_list(item): - if "shopping" in item.lower(): - return False - - if "add " in item.lower(): - item = item.lower().replace("add ", "") - - if not { - f"% The user adds {item} to a list :- the user adds something to a grocery list %" - }: - if not {f"% Do you really want to add {item}?%"}: - return False - - shopping_list = json.load(open("shopping_list.json")) - if " and " in item: - items_to_add = item.split(" and ") - shopping_list.extend(items_to_add) - - else: - shopping_list.append(item) - - json.dump(shopping_list, open("shopping_list.json", "w")) - f"% SAY {item} has been added to the shopping list%" - return True - - -def remove_from_shopping_list(item): - shopping_list = json.load(open("shopping_list.json")) - if not shopping_list: - "% SAY the shopping list is already empty.%" - return False - - extracted, score = process.extract(item, shopping_list, limit=1)[0] - if score < 60: - f"% SAY I did not quite get the item to remove %" - return False - - if not {f"% Do you want to remove {extracted} from the shopping list? %"}: - return False - - shopping_list.remove(extracted) - json.dump(shopping_list, open("shopping_list.json", "w")) - return True - - -def remove_first_item_from_shopping_list(): - shopping_list = json.load(open("shopping_list.json")) - if not shopping_list: - "% SAY the shopping list is already empty.%" - return False - - shopping_list.pop(0) - json.dump(shopping_list, open("shopping_list.json", "w")) - return True - - -def remove_last_item_from_shopping_list(): - shopping_list = json.load(open("shopping_list.json")) - if not shopping_list: - "% SAY the shopping list is already empty.%" - return False - - shopping_list.pop(-1) - json.dump(shopping_list, open("shopping_list.json", "w")) - return True - - -def reset_shopping_list(): - shopping_list = [] - json.dump(shopping_list, open("shopping_list.json", "w")) - return True diff --git a/wafl/templates/sample_project/shopping-list/interruptions/__wafl_functions.py b/wafl/templates/sample_project/shopping-list/interruptions/__wafl_functions.py deleted file mode 100644 index 5c0483d7..00000000 --- a/wafl/templates/sample_project/shopping-list/interruptions/__wafl_functions.py +++ /dev/null @@ -1,9 +0,0 @@ -from wafl.exceptions import CloseConversation, InterruptTask - - -async def close_conversation(inference, policy, task_memory): - raise CloseConversation - - -async def close_task(inference, policy, task_memory): - raise InterruptTask diff --git a/wafl/templates/sample_project/shopping-list/interruptions/functions.py b/wafl/templates/sample_project/shopping-list/interruptions/functions.py deleted file mode 100644 index 959fada6..00000000 --- a/wafl/templates/sample_project/shopping-list/interruptions/functions.py +++ /dev/null @@ -1,9 +0,0 @@ -from wafl.exceptions import CloseConversation, InterruptTask - - -def close_conversation(): - raise CloseConversation - - -def close_task(): - raise InterruptTask diff --git a/wafl/templates/sample_project/shopping-list/interruptions/rules.wafl b/wafl/templates/sample_project/shopping-list/interruptions/rules.wafl deleted file mode 100644 index e9309e99..00000000 --- a/wafl/templates/sample_project/shopping-list/interruptions/rules.wafl +++ /dev/null @@ -1,32 +0,0 @@ -# End the conversation - -_close - close_conversation() - -INTERRUPTION the user believes they don't need anything else - SAY ok - _close - -INTERRUPTION the user says "good bye" - SAY good bye! - _close - -INTERRUPTION the user says to shut up - SAY ok - _close - -INTERRUPTION the user asks this bot to be silent - _close - -INTERRUPTION the user says: "thank you" - _close - -INTERRUPTION the user says: "thank you this is all" - _close - -INTERRUPTION the user wants to stop - ! Do you want to continue? - close_task() - -INTERRUPTION "nevermind" - close_task() diff --git a/wafl/templates/sample_project/shopping-list/rules.wafl b/wafl/templates/sample_project/shopping-list/rules.wafl deleted file mode 100644 index 20bb84b1..00000000 --- a/wafl/templates/sample_project/shopping-list/rules.wafl +++ /dev/null @@ -1,55 +0,0 @@ -#using interruptions - -# Shopping list - -There is only one list, the shopping list - - -item = what does the user want to add to the shopping list? - add_shopping_list(item) - -item = what does the user want to remove from the shopping list? - remove_from_shopping_list(item) - -"remove or delete an item from the shopping list" - item = What do you want to remove? - remove_from_shopping_list(item) - SAY item removed - -the user wants to delete the shopping list - Do you want to delete the current shopping list - reset_shopping_list() - SAY The shopping list has been deleted - -the user wants to know what is in the shopping list - items = get_shopping_list_in_english() - SAY The shopping list contains: {items} - -the user wants to add something - item = what does the user want to add? - list_name = which list? - the user wants to add {item} to {list_name} - -the user wants to remove something - item = what does the user want to remove? - list_name = which list? - the user wants to remove {item} from {list_name} - -"What should I buy" - the user wants to know what is in the shopping list - -the user wants to remove the first item in the shopping list - remove_first_item_from_shopping_list() - SAY item removed - -the user wants to remove the first item - list_name = which list? - the user wants to remove the first item in {list_name} - -the user wants to remove the last item in the shopping list - remove_last_item_from_shopping_list() - SAY item removed - -the user wants to remove the last item - list_name = which list? - the user wants to remove the last item in {list_name} diff --git a/wafl/templates/sample_project/time-queries/functions.py b/wafl/templates/sample_project/time-queries/functions.py deleted file mode 100644 index fe58699c..00000000 --- a/wafl/templates/sample_project/time-queries/functions.py +++ /dev/null @@ -1,17 +0,0 @@ -from datetime import datetime, timedelta - - -def get_date(): - now = datetime.now() - return now.strftime("%A %d %B %Y") - - -def get_time(): - now = datetime.now() - minutes = int(now.strftime("%M")) - hour = int(now.strftime("%H")) - if minutes <= 30: - return f"{minutes} past {hour}" - - else: - return f"{60 - minutes} to {hour + 1}" diff --git a/wafl/templates/sample_project/time-queries/rules.wafl b/wafl/templates/sample_project/time-queries/rules.wafl deleted file mode 100644 index b1b1167a..00000000 --- a/wafl/templates/sample_project/time-queries/rules.wafl +++ /dev/null @@ -1,8 +0,0 @@ -# Time commands -"what time is it now" - time = get_time() - SAY the time is {time} - -"What day is it today" - date = get_date() - SAY Today is {date} diff --git a/wafl/templates/sample_project/weather/functions.py b/wafl/templates/sample_project/weather/functions.py deleted file mode 100644 index 2af23a30..00000000 --- a/wafl/templates/sample_project/weather/functions.py +++ /dev/null @@ -1,43 +0,0 @@ -import json -import requests - -from datetime import datetime, timedelta - -# These are the coordinate of central London. Please change at your convenience -latitude = "51.5074" -longitude = "0.1272" - - -def check_today_weather(): - today = datetime.now().strftime("%Y-%m-%d") - check_weather_lat_long(latitude, longitude, today) - - -def check_tomorrow_weather(): - today = datetime.now() - tomorrow = (today + timedelta(days=1)).strftime("%Y-%m-%d") - check_weather_lat_long(latitude, longitude, tomorrow) - - -def check_weather_lat_long(latitude, longitude, day): - secrets = json.load(open("secrets.json")) - result = requests.get( - f"https://rgw.5878-e94b1c46.eu-gb.apiconnect.appdomain.cloud/metoffice/production/v0/forecasts/point/daily?excludeParameterMetadata=true&includeLocationName=true&latitude={latitude}&longitude={longitude}", - headers={ - "X-IBM-Client-Id": secrets["key"], - "X-IBM-Client-Secret": secrets["secret"], - }, - ) - data = result.json() - if "features" not in data: - "% SAY There is a connection error to the weather API. Please try later. %" - return False - - for item in data["features"][0]["properties"]["timeSeries"]: - if day in item["time"]: - f"% SAY The temperature should be between {int(item['dayLowerBoundMaxTemp'])} and {int(item['dayUpperBoundMaxTemp'])}. %" - if item["dayProbabilityOfPrecipitation"] != 0: - f"% SAY The probability of rain is {item['dayProbabilityOfPrecipitation']} percent. %" - - if item["dayProbabilityOfSnow"] != 0: - f"% SAY The probability of snow is {item['dayProbabilityOfSnow']} percent. %" diff --git a/wafl/templates/sample_project/weather/rules.wafl b/wafl/templates/sample_project/weather/rules.wafl deleted file mode 100644 index 7fe2847f..00000000 --- a/wafl/templates/sample_project/weather/rules.wafl +++ /dev/null @@ -1,13 +0,0 @@ -# Weather - -"How is the weather" - check_today_weather() - -"How is the weather today" - check_today_weather() - -"What is the weather forecast" - check_today_weather() - -"How is the weather tomorrow" - check_tomorrow_weather() diff --git a/wafl/templates/sample_project/writing/__wafl_functions.py b/wafl/templates/sample_project/writing/__wafl_functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/templates/sample_project/writing/functions.py b/wafl/templates/sample_project/writing/functions.py deleted file mode 100644 index e69de29b..00000000 diff --git a/wafl/templates/sample_project/writing/rules.wafl b/wafl/templates/sample_project/writing/rules.wafl deleted file mode 100644 index 763f7dc2..00000000 --- a/wafl/templates/sample_project/writing/rules.wafl +++ /dev/null @@ -1,7 +0,0 @@ -This bot can answer coding questions. - -Write a paragraph - goal = what do you want to write about? - paragraph = Write a paragraph that accomplishes the following: {goal} - SAY This is what I could come up with: - SAY {paragraph} diff --git a/wafl/templates/secrets.json b/wafl/templates/secrets.json new file mode 100644 index 00000000..b6115765 --- /dev/null +++ b/wafl/templates/secrets.json @@ -0,0 +1,4 @@ +{ + "key": "", + "secret": "" +} diff --git a/wafl/templates/start_llm.sh b/wafl/templates/start_llm.sh index 5e162007..054340e5 100644 --- a/wafl/templates/start_llm.sh +++ b/wafl/templates/start_llm.sh @@ -1,3 +1,3 @@ echo "Starting the Docker instance." echo "It might take a few minutes for the model to load." -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 diff --git a/wafl/testcases.py b/wafl/testcases.py index 9bec5d51..19ee3f84 100644 --- a/wafl/testcases.py +++ b/wafl/testcases.py @@ -1,11 +1,10 @@ -import asyncio - +from fuzzywuzzy import fuzz +from wafl.events.utils import load_knowledge 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 from wafl.interface.dummy_interface import DummyInterface from wafl.parsing.testcase_parser import get_user_and_bot_lines_from_text -from wafl.extractors.entailer import Entailer class ConversationTestCases: @@ -14,10 +13,9 @@ class ConversationTestCases: GREEN_COLOR_START = "\033[32m" COLOR_END = "\033[0m" - def __init__(self, config, text, knowledge, code_path=None, logger=None): + def __init__(self, config, text, code_path=None, logger=None): self._testcase_data = get_user_and_bot_lines_from_text(text) - self._knowledge = knowledge - self._entailer = Entailer(config, logger) + self._knowledge = load_knowledge(config, logger) self._code_path = code_path if code_path else "/" async def test_single_case(self, name): @@ -46,7 +44,7 @@ async def test_single_case(self, name): generated_lines = interface.get_utterances_list() for test_line, generated_line in zip(test_lines, generated_lines): test_line = self._apply_deixis(test_line) - if not await self._lhs_entails_rhs(generated_line, test_line): + if not await self._lhs_is_similar_to(generated_line, test_line): print(f" [test_line] {test_line}") print(f" [predicted_line] {generated_line}") is_consistent = False @@ -73,7 +71,7 @@ async def run(self): return to_return - async def _lhs_entails_rhs(self, lhs, rhs): + async def _lhs_is_similar_to(self, lhs, rhs): lhs_name = lhs.split(":")[0].strip() rhs_name = rhs.split(":")[0].strip() if lhs_name != rhs_name: @@ -81,7 +79,7 @@ async def _lhs_entails_rhs(self, lhs, rhs): lhs = ":".join(item.strip() for item in lhs.split(":")[1:]) rhs = ":".join(item.strip() for item in rhs.split(":")[1:]) - return await self._entailer.entails(lhs, rhs) == "True" + return fuzz.ratio(lhs, rhs) > 80 def _apply_deixis(self, line): name = line.split(":")[0].strip() diff --git a/wafl/variables.py b/wafl/variables.py index 161fa5be..0a5126b3 100644 --- a/wafl/variables.py +++ b/wafl/variables.py @@ -1,4 +1,4 @@ def get_variables(): return { - "version": "0.0.70", + "version": "0.0.80", }