Repository: facebookresearch/ParlAI

Issue:
Cannot have multi-turn conversation with agent via interactive_web.py script
Cannot have multi-turn conversation with agent via interactive_web.py script

**Reproduction steps**
Run interacive_web.py with some model file 
```
python3 ../ParlAI/parlai/scripts/interactive_web.py -mf ~/models/model_file 
```
communicate with the agent, send any two consecutive messages as shown below.
```
curl -XPOST "http://localhost:8080/interact" "Content-Type: text" -d "Hello"
```
**Expected behavior**
No exception should be thrown, agent should be available for multi-turn conversation.

The second message produces the following stacktrace
```
Exception happened during processing of request from ('127.0.0.1', 55372)
Traceback (most recent call last):
  File "/Users/johnsmith/miniconda3/envs/rasa-env/lib/python3.6/socketserver.py", line 320, in _handle_request_noblock
    self.process_request(request, client_address)
  File "/Users/johnsmith/miniconda3/envs/rasa-env/lib/python3.6/socketserver.py", line 351, in process_request
    self.finish_request(request, client_address)
  File "/Users/johnsmith/miniconda3/envs/rasa-env/lib/python3.6/socketserver.py", line 364, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "/Users/johnsmith/miniconda3/envs/rasa-env/lib/python3.6/socketserver.py", line 724, in __init__
    self.handle()
  File "/Users/johnsmith/miniconda3/envs/rasa-env/lib/python3.6/http/server.py", line 418, in handle
    self.handle_one_request()
  File "/Users/johnsmith/miniconda3/envs/rasa-env/lib/python3.6/http/server.py", line 406, in handle_one_request
    method()
  File "/Users/johnsmith/ParlAI/parlai/scripts/interactive_web.py", line 155, in do_POST
    SHARED.get('opt'), body.decode('utf-8')
  File "/Users/johnsmith/ParlAI/parlai/scripts/interactive_web.py", line 139, in interactive_running
    SHARED['agent'].observe(reply)
  File "/Users/johnsmith/ParlAI/parlai/core/torch_agent.py", line 1545, in observe
    reply = self.last_reply(use_reply=self.opt.get('use_reply', 'label'))
  File "/Users/johnsmith/ParlAI/parlai/core/torch_agent.py", line 1515, in last_reply
    or self.observation['episode_done']
KeyError: 'episode_done'
--------------------------

After second message.
```

### Suggestion
The following edit could be made to prevent this issue.
```
self.observation['episode_done'] --> 'episode_done' in self.observation
```
### Extra
Making the change above allows multi-turn conversation, but now there is the question of some functionality terminating the conversation from the client-side i.e.
```
curl -XPOST "http://localhost:8080/interact" "Content-Type: text" -d "[DONE]"
```
such that user application or human has some option to terminate multi-turn conversations. 
If this should be moved to functionality, just let me know!

Thank you 🙏 

Discussion:

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9fbfaa87..bfd1fb04 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -11,6 +11,13 @@ standard_cpu36: &standard_cpu36
   docker:
     - image: circleci/python:3.6.5-node
 
+osx_cpu37: &osx_cpu37
+  macos:
+    xcode: "10.1.0"
+  environment:
+    PYTHON: 3.7.1
+    HOMEBREW_NO_AUTO_UPDATE: 1
+
 gpu: &gpu
   environment:
     CUDA_VERSION: "10.0"
@@ -57,6 +64,12 @@ installtorchgpu: &installtorchgpu
       pip3 install --progress-bar off pytorch-pretrained-bert
       pip3 install --progress-bar off torchtext
 
+installtorchcpuosx: &installtorchcpuosx
+  run:
+    name: Install torch CPU and dependencies
+    command: |
+      pip3 install --progress-bar off https://download.pytorch.org/whl/cpu/torch-1.0.0-cp37-none-macosx_10_7_x86_64.whl
+
 installtorchcpu36: &installtorchcpu36
   run:
     name: Install torch CPU and dependencies
@@ -106,6 +119,19 @@ jobs:
           name: Data tests
           command: python setup.py test -s tests.suites.datatests -v
 
+  unittests_osx:
+    <<: *osx_cpu37
+    working_directory: ~/ParlAI
+    steps:
+      - checkout
+      - <<: *fixgit
+      - <<: *setup
+      - <<: *installdeps
+      - <<: *installtorchcpuosx
+      - run:
+          name: Unit tests (OSX)
+          command: python setup.py test -s tests.suites.unittests -v
+
   unittests_36:
     <<: *standard_cpu36
     working_directory: ~/ParlAI
@@ -302,6 +328,7 @@ workflows:
       - unittests_gpu
       - unittests_37
       - unittests_36
+      - unittests_osx
       - mturk_tests:
           filters:
             branches:
@@ -333,6 +360,7 @@ workflows:
       - unittests_36
       - unittests_37
       - unittests_gpu
+      - unittests_osx
       - mturk_tests
       - nightly_gpu_tests
       - test_website
diff --git a/parlai/core/agents.py b/parlai/core/agents.py
index 2578c979..658103e3 100644
--- a/parlai/core/agents.py
+++ b/parlai/core/agents.py
@@ -495,6 +495,12 @@ def load_agent_module(opt):
                         "previously: {} )]".format(k, v, new_opt.get(k, None))
                     )
                 new_opt[k] = v
+
+        model_class = get_agent_module(new_opt['model'])
+
+        if hasattr(model_class, 'upgrade_opt'):
+            new_opt = model_class.upgrade_opt(new_opt)
+
         # add model arguments to new_opt if they aren't in new_opt already
         for k, v in opt.items():
             if k not in new_opt:
@@ -512,7 +518,6 @@ def load_agent_module(opt):
                 'is correct. This may manifest as a shape mismatch later '
                 'on.'.format(old_dict_file, new_opt['dict_file'])
             )
-        model_class = get_agent_module(new_opt['model'])
 
         # check for model version
         if hasattr(model_class, 'model_version'):
@@ -537,9 +542,6 @@ def load_agent_module(opt):
                         m.format(m='modelname', v=curr_version, c='ModelAgent')
                     )
 
-        if hasattr(model_class, 'upgrade_opt'):
-            new_opt = model_class.upgrade_opt(new_opt)
-
         # if we want to load weights from --init-model, compare opts with
         # loaded ones
         compare_init_model_opts(opt, new_opt)
diff --git a/parlai/core/torch_agent.py b/parlai/core/torch_agent.py
index 952d42b0..7009c21e 100644
--- a/parlai/core/torch_agent.py
+++ b/parlai/core/torch_agent.py
@@ -720,14 +720,7 @@ class TorchAgent(ABC, Agent):
         label_truncate = opt.get('label_truncate') or opt['truncate']
         self.label_truncate = label_truncate if label_truncate >= 0 else None
         # stores up to hist_utt past observations within current dialog
-        self.history = self.history_class()(
-            opt,
-            maxlen=self.text_truncate,
-            size=self.histsz,
-            p1_token=self.P1_TOKEN,
-            p2_token=self.P2_TOKEN,
-            dict_agent=self.dict,
-        )
+        self.history = self.build_history()
 
         self.is_training = False  # track whether model is training
         self.rank_candidates = opt['rank_candidates']
@@ -735,6 +728,17 @@ class TorchAgent(ABC, Agent):
         # set interactive mode or not according to options.
         self.set_interactive_mode(opt['interactive_mode'], shared)
 
+    def build_history(self):
+        """Return the constructed history object."""
+        return self.history_class()(
+            self.opt,
+            maxlen=self.text_truncate,
+            size=self.histsz,
+            p1_token=self.P1_TOKEN,
+            p2_token=self.P2_TOKEN,
+            dict_agent=self.dict,
+        )
+
     def build_dictionary(self):
         """
         Return the constructed dictionary, which will be set to self.dict.
@@ -1706,7 +1710,7 @@ class TorchAgent(ABC, Agent):
         pass
 
     def set_interactive_mode(self, mode, shared):
-        """ Set interactive mode on or off."""
+        """Set interactive mode on or off."""
         # Base class is a no-op.
         pass
 
diff --git a/parlai/scripts/interactive_web.py b/parlai/scripts/interactive_web.py
index c51199c3..6782c08f 100644
--- a/parlai/scripts/interactive_web.py
+++ b/parlai/scripts/interactive_web.py
@@ -134,8 +134,7 @@ WEB_HTML = """
 
 class MyHandler(BaseHTTPRequestHandler):
     def interactive_running(self, opt, reply_text):
-        reply = {}
-        reply['text'] = reply_text
+        reply = {'episode_done': False, 'text': reply_text}
         SHARED['agent'].observe(reply)
         model_res = SHARED['agent'].act()
         return model_res
diff --git a/parlai/zoo/model_list.py b/parlai/zoo/model_list.py
index 3e112c1a..756d27ff 100644
--- a/parlai/zoo/model_list.py
+++ b/parlai/zoo/model_list.py
@@ -16,6 +16,31 @@ automatically, e.g.:
 
    python examples/interactive.py --model-file
        "zoo:wikipedia_2016-12-21/tfidf_retriever/drqa_docs"
+
+
+There are a number of guidelines you should follow in the zoo:
+
+- You should choose the best directory name as possible. An input of
+  ``zoo:PROJECTNAME/MODELNAME/FILENAME`` will attempt to use a build script from
+  parlai/zoo/PROJECTNAME/MODELNAME.py.
+- You should include all of the following fields:
+    * title: the name of the entry:
+    * id: corresponds to PROJECTNAME
+    * description: describe the entry in reasonable detail. It should be at least
+      a couple sentences.
+    * example: an example command to chat with or evaluate the model
+    * result: the expected output from running the model. You are strongly encouraged
+      to make a nightly test which verifies this result.
+    * external_website: if applicable, an external website related to the zoo to
+      link to.
+    * project: if applicable, a link to the project folder. You must have either
+      external_website or project.
+    * example2 and result2 (optional): additional examples to run.
+
+- As much as possible, you should try to include two examples: one to generate
+  some key metrics (e.g. from a paper) and one to actually chat with the model
+  using interactive.py. Both should strongly attempt to minimize mandatory
+  command line flags.
 """
 
 model_list = [
@@ -34,6 +59,10 @@ model_list = [
         "example": (
             "python -m parlai.scripts.interactive -mf zoo:convai2/kvmemnn/model"
         ),
+        "result": (
+            "Enter Your Message: Hi, what do you think of peanuts?\n"
+            "there was a kid in the school system my mum works for with a severe peanut allergy"
+        ),
     },
     {
         "title": "Seq2Seq ConvAI2 model",
@@ -50,15 +79,10 @@ model_list = [
             "python -m parlai.scripts.interactive -mf "
             "zoo:convai2/seq2seq/convai2_self_seq2seq_model -m legacy:seq2seq:0"
         ),
-    },
-    {
-        "title": "Seq2Seq Twitter model",
-        "id": "twitter",
-        "path": "zoo:twitter/seq2seq/twitter_seq2seq_model",
-        "agent": "legacy:seq2seq:0",
-        "task": "twitter",
-        "description": ("Seq2Seq conversational model trained on the Twitter task"),
-        "result": "{'exs': 10405, 'accuracy': 0.001538, 'f1': 0.07537, 'bleu': 0.002304, 'loss': 3.93, 'ppl': 50.9}",  # noqa: E501
+        "result": (
+            "Enter Your Message: Hi, what do you think of peanuts?\n"
+            "[Seq2Seq]: i don't have any , but i do not have a favorite ."
+        ),
     },
     {
         "title": "DrQA SQuAD model",
@@ -66,8 +90,14 @@ model_list = [
         "path": "zoo:drqa/squad/model",
         "agent": "drqa",
         "task": "squad",
-        "description": "drqa reader trained on SQuAD",
+        "description": "DrQA Reader trained on SQuAD",
+        "external_website": "https://github.com/facebookresearch/DrQA",
+        "example": (
+            "python -m parlai.scripts.eval_model -mf zoo:drqa/squad/model -t squad "
+            "-dt test"
+        ),
         "result": (
+            # TODO: this differs slightly from the actual results as of 2019-07-23
             "{'exs': 10570, 'accuracy': 0.6886, 'f1': 0.7821, 'hits@1': 0.689, 'hits@5': 0.689, 'hits@10': 0.689, 'hits@100': 0.689, 'bleu': 0.1364, 'train_loss': 0}"  # noqa: E501
         ),
     },
@@ -76,6 +106,7 @@ model_list = [
         "id": "wikipedia_2016-12-21",
         "path": "zoo:wikipedia_2016-12-21/tfidf_retriever/drqa_docs",
         "agent": "tfidf_retriever",
+        "external_website": "https://github.com/facebookresearch/DrQA",
         "task": "wikipedia:full",
         "example": (
             "python -m parlai.scripts.interactive --model tfidf_retriever "
@@ -108,7 +139,10 @@ model_list = [
         "description": (
             "Retrieval over Wikipedia dump, used for DrQA on the open squad " "dataset."
         ),
-        "example": "python -m parlai.scripts.interactive --model tfidf_retriever -mf zoo:wikipedia_full/tfidf_retriever/model",  # noqa: E501
+        "example": (
+            "python -m parlai.scripts.interactive --model tfidf_retriever -mf "
+            "zoo:wikipedia_full/tfidf_retriever/model"
+        ),
         "result": (
             """
             Enter Your Message: Yann LeCun
@@ -217,15 +251,6 @@ model_list = [
         "'hits@10': 0.994, 'hits@100': 1.0, 'bleu': 0.7255, 'lr': 5e-05, 'num_updates': 15050,"  # noqa: E501
         "'examples': 6623, 'loss': 5307.0, 'mean_loss': 0.8013, 'mean_rank': 1.599, 'train_accuracy': 0}",  # noqa: E501
     },
-    {
-        "title": "Legacy Seq2Seq Twitter model",
-        "id": "twitter",
-        "path": "zoo:twitter/seq2seq/twitter_seq2seq_model",
-        "agent": "legacy:seq2seq:0",
-        "task": "twitter",
-        "description": ("Generic conversational model trained on the twitter task"),
-        "result": "{'exs': 10405, 'accuracy': 0.001538, 'f1': 0.07537, 'bleu': 0.002304, 'loss': 3.93, 'ppl': 50.9}",  # noqa: E501
-    },
     {
         "title": "Controllable Dialogue ConvAI2 model",
         "id": "controllable_dialogue",
@@ -233,7 +258,17 @@ model_list = [
         "agent": "projects.controllable_dialogue.controllable_seq2seq.controllable_seq2seq:ControllableSeq2seqAgent",  # noqa: E501
         "task": "convai2",
         "project": "https://github.com/facebookresearch/ParlAI/tree/master/projects/controllable_dialogue",
-        "example": "python -m parlai.scripts.eval_model --model projects.controllable_dialogue.controllable_seq2seq.controllable_seq2seq:ControllableSeq2seqAgent --task projects.controllable_dialogue.tasks.agents -mf zoo:controllable_dialogue/convai2_finetuned_baseline",
+        "example": (
+            "python -m parlai.scripts.eval_model --model "
+            "projects.controllable_dialogue.controllable_seq2seq.controllable_seq2seq:"
+            "ControllableSeq2seqAgent --task "
+            "projects.controllable_dialogue.tasks.agents "
+            "-mf zoo:controllable_dialogue/convai2_finetuned_baseline"
+        ),
+        "result": (
+            "{'exs': 7801, 'accuracy': 0.0006409, 'f1': 0.1702, 'bleu': 0.005205, "
+            "'token_acc': 0.3949, 'loss': 3.129, 'ppl': 22.86}"
+        ),
         "description": ("Seq2Seq model with control trained on ConvAI2"),
     },
     {
@@ -250,8 +285,11 @@ model_list = [
             "python examples/eval_model.py -t personality_captions "
             "-mf zoo:personality_captions/transresnet/model --num-test-labels 5 -dt test"
         ),
-        "result": "{'exs': 10000, 'accuracy': 0.5113, 'f1': 0.5951, 'hits@1': 0.511, 'hits@5': 0.816, "  # noqa: E501
-        "'hits@10': 0.903, 'hits@100': 0.998, 'bleu': 0.4999, 'hits@1/100': 1.0, 'loss': -0.002, 'med_rank': 1.0}",  # noqa: E501
+        "result": (
+            "{'exs': 10000, 'accuracy': 0.5113, 'f1': 0.5951, 'hits@1': 0.511, "
+            "'hits@5': 0.816, 'hits@10': 0.903, 'hits@100': 0.998, 'bleu': 0.4999, "
+            "'hits@1/100': 1.0, 'loss': -0.002, 'med_rank': 1.0}"
+        ),
     },
     {
         "title": "Poly-Encoder Transformer ConvAI2 Model",
@@ -259,16 +297,14 @@ model_list = [
         "path": "zoo:pretrained_transformers/model_poly",
         "agent": "transformer/polyencoder",  # noqa: E501
         "task": "convai2",
+        "project": "https://github.com/facebookresearch/ParlAI/tree/master/projects/polyencoder/",
         "description": (
             "Polyencoder pretrained on Reddit and fine-tuned on ConvAI2 scoring 89+ hits@1/20. See the pretrained_transformers directory for a list of other available pretrained transformers"
         ),
         "example": (
-            "python examples/interactive.py -mf zoo:pretrained_transformers/model_poly/model -t convai2"
+            "python examples/interactive.py -mf "
+            "zoo:pretrained_transformers/model_poly/model -t convai2"
         ),
-        # "python examples/interactive.py -m transformer/polyencoder "
-        # "-mf zoo:pretrained_transformers/model_poly/model --encode-candidate-vecs true "
-        # "--eval-candidates fixed  "
-        # "--fixed-candidates-path data/models/pretrained_transformers/convai_trainset_cands.txt"
         "result": (
             "hi how are you doing ?\n"
             "[Polyencoder]: i am alright . i am back from the library .\n"
@@ -311,6 +347,7 @@ model_list = [
         "path": "zoo:self_feeding/model",
         "agent": "projects.self_feeding.self_feeding_agent:SelfFeedingAgent",
         "task": "self_feeding:all:train",
+        "project": "https://github.com/facebookresearch/ParlAI/tree/master/projects/self_feeding",
         "description": (
             "The self-feeding chatbot of Hancock, et al., 2019 "
             "(https://arxiv.org/abs/1901.05415). This model learns from is mistakes "
diff --git a/parlai/zoo/twitter/__init__.py b/parlai/zoo/twitter/__init__.py
deleted file mode 100644
index 240697e3..00000000
--- a/parlai/zoo/twitter/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright (c) Facebook, Inc. and its affiliates.
-# This source code is licensed under the MIT license found in the
-# LICENSE file in the root directory of this source tree.
diff --git a/parlai/zoo/twitter/dict.py b/parlai/zoo/twitter/dict.py
deleted file mode 100644
index 0b4d2365..00000000
--- a/parlai/zoo/twitter/dict.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright (c) Facebook, Inc. and its affiliates.
-# This source code is licensed under the MIT license found in the
-# LICENSE file in the root directory of this source tree.
-
-from parlai.core.params import ParlaiParser
-from parlai.scripts.build_dict import setup_args, build_dict as main_build_dict
-
-import os
-
-
-def download(datapath):
-    DICT_PATH = os.path.join(datapath, 'models', 'twitter', 'dict', 'dict_30k')
-    # don't actually download--build it
-    parser = setup_args(ParlaiParser())
-    # first build on standard train and validation
-    parser.set_defaults(
-        task='twitter', dict_lower=True, dict_file=DICT_PATH, dict_maxtokens=30000
-    )
-    opt = parser.parse_args(args='')
-    return main_build_dict(opt)
diff --git a/parlai/zoo/twitter/seq2seq.py b/parlai/zoo/twitter/seq2seq.py
deleted file mode 100644
index 7740efa8..00000000
--- a/parlai/zoo/twitter/seq2seq.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright (c) Facebook, Inc. and its affiliates.
-# This source code is licensed under the MIT license found in the
-# LICENSE file in the root directory of this source tree.
-"""Pretrained seq2seq model on twitter with 30k vocab size."""
-
-from parlai.core.build_data import download_models
-
-
-def download(datapath):
-    opt = {'datapath': datapath, 'model_type': 'seq2seq'}
-    fnames = ['twitter_seq2seq_model.tgz']
-    download_models(opt, fnames, 'twitter', version='v1.0', use_model_type=True)
diff --git a/projects/polyencoder/README.md b/projects/polyencoder/README.md
new file mode 100644
index 00000000..2d9f2037
--- /dev/null
+++ b/projects/polyencoder/README.md
@@ -0,0 +1,5 @@
+# Polyencoder
+
+Paper: https://arxiv.org/abs/1905.01969
+
+This is a placeholder for the polyencoder project source code.


Test patch:
diff --git a/tests/test_zootasks.py b/tests/test_zootasks.py
index bcf1ab3f..a3da6ffe 100644
--- a/tests/test_zootasks.py
+++ b/tests/test_zootasks.py
@@ -4,27 +4,61 @@
 # This source code is licensed under the MIT license found in the
 # LICENSE file in the root directory of this source tree.
 
+"""
+Test common developer mistakes in the model zoo and task list.
+
+Mostly just ensures the docs will output nicely.
+"""
+
 import os
 import unittest
 import parlai.core.testing_utils as testing_utils
+from parlai.zoo.model_list import model_list
+from parlai.tasks.task_list import task_list
+
+ZOO_EXCEPTIONS = {"fasttext_cc_vectors", "fasttext_vectors", "glove_vectors", "bert"}
 
 
 class TestZooAndTasks(unittest.TestCase):
     """Make sure the package is alive."""
 
-    def test_zoolist_types(self):
-        from parlai.zoo.model_list import model_list
+    def _assertZooString(self, member, container, animal_name=None):
+        msg = f'Missing or empty {member} in parlai.zoo.model_list'
+        if animal_name:
+            msg += f' [{animal_name}]'
+        self.assertIn(member, container, msg=msg)
+        self.assertTrue(container[member], msg=msg)
+
+    def test_zoolist_fields(self):
+        """Ensure zoo entries conform to style standards."""
+
+        for animal in model_list:
+            self._assertZooString('title', animal)
+            name = animal['title']
+            # every task must at least contain these
+            for key in ['id', 'task', 'description', 'example', 'result']:
+                self._assertZooString(key, animal, name)
+
+            # if there's a second example there should be a second result
+            if 'example2' in animal:
+                self._assertZooString('result2', animal, name)
+
+            # every entry needs a project page or a website
+            self.assertTrue(
+                ("project" in animal) or ("external_website" in animal),
+                f"Zoo entry ({name}) should contain either project or external_website",
+            )
 
+    def test_zoolist_types(self):
+        """Ensure no type errors in the model zoo."""
         self._check_types(model_list, 'Zoo')
 
     def test_tasklist_types(self):
-        from parlai.tasks.task_list import task_list
-
+        """Ensure no type errors in the task list."""
         self._check_types(task_list, 'Task')
 
     def test_tasklist(self):
-        from parlai.tasks.task_list import task_list
-
+        """Check the task list for issues."""
         self._check_directory(
             "task_list",
             task_list,
@@ -34,14 +68,9 @@ class TestZooAndTasks(unittest.TestCase):
         )
 
     def test_zoolist(self):
-        from parlai.zoo.model_list import model_list
-
+        """Check the zoo list for issues."""
         self._check_directory(
-            "model_list",
-            model_list,
-            "parlai/zoo",
-            "id",
-            ignore=["fasttext_cc_vectors", "fasttext_vectors", "glove_vectors", "bert"],
+            "model_list", model_list, "parlai/zoo", "id", ignore=ZOO_EXCEPTIONS
         )
 
     def _check_directory(self, listname, thing_list, thing_dir, thing_key, ignore=None):