parlaiとweb apiのように会話したい



いろんなbotを作って遊んでいるが、pariaiがやはり凄いので、もっと遊びたい。
parlai(Blenderbot2)にはインタラクティブなインタフェースが用意されている。

``` 
Enter Your Message: What is your hobby?
/opt/conda/lib/python3.10/site-packages/parlai/core/torch_generator_agent.py:1728: UserWarning: __floordiv__ is deprecated, and its behavior will change in a future version of pytorch. It currently rounds toward 0 (like the 'trunc' function NOT 'floor'). This results in incorrect rounding for negative values. To keep the current behavior, use torch.div(a, b, rounding_mode='trunc'), or for actual floor division, use torch.div(a, b, rounding_mode='floor').
  hyp_ids = best_idxs // voc_size
[BlenderBot2Fid]: I like to play video games. What about you? _POTENTIALLY_UNSAFE__
Enter Your Message: Who are you?
[BlenderBot2Fid]: Hi, I'm a computer programmer. What do you like to do in your spare time?
Enter Your Message: Who is the Prime Minister of Japan?
[BlenderBot2Fid]: I'm not sure, but I do know that Shinzo Abe is the prime minister of Japan.
Enter Your Message: Do you have any recommended manga?
[BlenderBot2Fid]: I don't have any recommendations, but there are a lot of anime and manga out there.
```

コマンドで遊ぶ分には良いが、ほかアプリと連携させるには不便なので、Web経由でparlaiとやり取りできるようにした。
まず、サーバ用のプログラムを用意。
parlaiのインタラクティブに起動しつつ、pexpectで"Enter Your Message:"のプロンプトが来るまで待ち受けて入力を受け付ける。
応答には余計な文字列が含まれているので、'[BlenderBot2Fid]'の文字列を基準に必要なものだけ削って返す。
今後、入出力をいろいろ増やして遊びたいので、文字列ではなくjsonで応答できるようにする。
なお、--search_serverとあるようにserarch_serverを分離している。
(というか、同じ環境でsearch_serverも動かそうとするとpipがdependency resolver does not currently～を吐くのは自分だけ？無視しても動くけれど、気持ち悪いので分離)
safe_interactiveを使わない場合に初回応答時に出てくる「_POTENTIALLY_UNSAFE__」のメッセージも、ここで除外している。

``` pyton
#!/opt/conda/bin/python
# -*- coding: utf-8 -*-
import socket, pickle, pexpect, sys

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((socket.gethostname(), 54320))
s.listen(5)

p = pexpect.spawn('parlai interactive --model-file zoo:blenderbot2/blenderbot2_400M/model --search_server 10.100.0.95:10187')
p.expect("Enter Your Message:", timeout=300)
print("Ready to respond")

while True:
    csocket, address = s.accept()
    req=pickle.loads(csocket.recv(1024))
    p.sendline(req["msg"])
    print("input: " + req["msg"])
    p.expect("Enter Your Message:", timeout=300)
    out=p.before.decode('utf-8')
    out=out[ out.find('[BlenderBot2Fid]') +28:-12].replace("_POTENTIALLY_UNSAFE__","")
    print("output: " + out)
    csocket.send(pickle.dumps([out]))
    csocket.close()

``` 
そしてcgi。
上記プロセスに接続して応答を受け取る。

``` pyton
#!/opt/conda/bin/python
# -*- coding: utf-8 -*-
import cgi, socket, pickle, json, sys

params = cgi.FieldStorage()
print('Content-Type: text/html; charset=UTF-8\n')

if 'req' not in params: print("リクエスト「req」がありません")
else:
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((socket.gethostname(), 54320))
  s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  sendpacket=pickle.dumps( {"msg":params['req'].value.replace("\n","。").replace("\t","")} )
  s.send(sendpacket)
  # print(str(params),file=sys.stderr)
  print(pickle.loads(s.recv(1024))[0])
``` 

Dockerfileは以下。

``` Dockerfile
FROM jupyter/datascience-notebook
USER root

ENV PYTHONUNBUFFERED True
ENV LANG C.UTF-8
ENV LANGUAGE en_US:

RUN pip install torch parlai transformers faiss-gpu
RUN pip install fairseq

RUN cp /etc/localtime /etc/localtime.org
RUN ln -sf  /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN echo "Asia/Tokyo" >/etc/timezone

WORKDIR /app
COPY app /app
CMD /app/boot.sh
``` 

あまり行儀が良くないが、1コンテナ上でいろいろ動かしている。
server.pyは、先のparlaiを起動しているヤツ。

``` bash
[root@ip-10-100-0-11 parlai]# cat ./app/boot.sh
nohup /app/server.py >server.log 2>&1 &

exec /app/app.py
[root@ip-10-100-0-11 parlai]#
[root@ip-10-100-0-11 parlai]# cat ./app/app.py
#!/opt/conda/bin/python
# -*- coding: utf-8 -*-
import os, http.server, socketserver

PORT = 80
os.chdir('/app')

class handler(http.server.CGIHTTPRequestHandler):
   cgi_directories = ["/cgi-bin"]
httpd = socketserver.TCPServer(("0.0.0.0", PORT), handler)
httpd.server_name = "test"
httpd.server_port = PORT
httpd.serve_forever()
[root@ip-10-100-0-11 parlai]#
``` 

なお、zoo:blenderbot2/blenderbot2_400M/modelの初回起動時に13GB程度のデータダウンロードが走るが、サーバが重いのか16時間ほどを要した。
コンテナ削除で一緒に消えると悲しいので、別途ダウンロードしてローカルに保存しておくのが無難。
容量が容量なのでimageに含めたくはないし、以下のスクリプトで大きめのファイルを予め/parlai/data以下にダウンロードしておき、/opt/conda/lib/python3.10/site-packages/data以下をまるごとマウントする。

``` bash
function parlai_setfile() {
  dname=`dirname ${2}`
  if [ ! -e /parlai/${2} ]; then
    mkdir -p /parlai/${dname}/
    wget -P /parlai/${dname}/ ${1}
  fi
}

parlai_setfile http://parl.ai/downloads/_models/blenderbot2/blenderbot2_400M/model.tgz data/models/blenderbot2/blenderbot2_400M/model.tgz
parlai_setfile https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json data/gpt2/encoder.json
parlai_setfile http://parl.ai/downloads/_models/hallucination/bart_rag_token/model.tgz data/models/hallucination/bart_rag_token/model.tgz
parlai_setfile https://dl.fbaipublicfiles.com/dpr/wikipedia_split/psgs_w100.tsv.gz data/models/hallucination/wiki_passages/psgs_w100.tsv.gz
parlai_setfile https://dl.fbaipublicfiles.com/dpr/checkpoint/retriver/multiset/hf_bert_base.cp data/models/hallucination/multiset_dpr/hf_bert_base.cp
parlai_setfile http://parl.ai/downloads/_models/blenderbot2/memory_decoder/model.tgz  data/models/blenderbot2/memory_decoder/model.tgz
parlai_setfile http://parl.ai/downloads/_models/blenderbot2/query_generator/model.tgz data/models/blenderbot2/query_generator/model.tgz
parlai_setfile http://dl.fbaipublicfiles.com/fairseq/models/bart.large.tar.gz data/models/bart/bart.large.tar.gz

```

上記でダウンロードしたファイルを、docker run時に「-v /parlai/data:/opt/conda/lib/python3.10/site-packages/data」でマウントすればOK


これで、web経由でやりとりできる。RTX 3070でも1秒程度で応答帰ってくるので上々。

http://IP/cgi-bin/parlai.py?req=are you happy?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421201/c6e3e984-371b-ea2e-3be1-4bcf25aea26f.png)

http://IP/cgi-bin/parlai.py?req=Do you have a recommended movie?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421201/835d2d4a-ee1a-4461-52cd-c006c301eac9.png)

以下と連携すれば、安上がりな日本語botにならないかな？
https://qiita.com/hajime_y/items/f1ca8a93d96dc3976ba1

と思ってやってみた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421201/b25edc29-6a74-ca6c-fe66-a03e4b4371c4.png)
2つ目の書き込みには「映画」という単語が出てきていないのに、「インターセプター」という単語からか、あるいは会話の流れからか、それが映画であると認識しているの凄い。
なんJコーパス学習させて頭おかしくなったずんだもん(中身Rinna-1b)と会話されて、その履歴で学習しなおせば、あっちも少しマシになるだろうか？

一応、ImageServerの方のDockerfileも載せておく。
[SearchEngine](https://github.com/JulesGM/ParlAI_SearchEngine)はgitからとってきて置いておく必要あり。

```Dockerfile
FROM jupyter/datascience-notebook
USER root

ENV PYTHONUNBUFFERED True
ENV LANG C.UTF-8
ENV LANGUAGE en_US:

RUN pip install torch

RUN cp /etc/localtime /etc/localtime.org
RUN ln -sf  /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN echo "Asia/Tokyo" >/etc/timezone

COPY SearchEngine /SearchEngine
WORKDIR /SearchEngine
RUN pip install -r requirements.txt
CMD exec python /SearchEngine/search_server.py serve --host 0.0.0.0:80
```