Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cloud RunとFastAPIで、ChatBotをミニマムスタートしよう / Getting start ChatBot with FastAPI and Cloud RUN

attakei
August 29, 2020

Cloud RunとFastAPIで、ChatBotをミニマムスタートしよう / Getting start ChatBot with FastAPI and Cloud RUN

attakei

August 29, 2020
Tweet

More Decks by attakei

Other Decks in Programming

Transcript

  1. ngrok こんな感じに動く $ ngrok http 8000 Session Status online Account

    Kazuya Takei (Plan: Free) Update update available (version 2.3.35, Ctrl-U to update) Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://8e1d754c956e.ngrok.io -> http://localhost:8000 Forwarding https://8e1d754c956e.ngrok.io -> http://localhost:8000
  2. 最小の構成 ミニマムな main.py from fastapi import FastAPI app = FastAPI()

    @app.get("/") async def hello_world(): return {"msg": "Hello, World!"}
  3. 最小の構成 サーバーを起動する 動作確認 $ pip install fastapi uvicorn $ uvicorn

    main:app INFO: Uvicorn running on http://127.0.0 INFO: Started reloader process [2719205 INFO: Started server process [2719212] INFO: Waiting for application startup. INFO: Application startup complete. INFO: 127.0.0.1:46564 - "GET / HTTP/1.1 $ http localhost:8000 HTTP/1.1 200 OK content-length: 23 content-type: application/json { "msg": "Hello, World!" }
  4. 最小の構成との比較 Flask Fast API ※見た目の関係で、意図的にコードを省いています from flask import Flask app

    = Flask(__name__) @app.route('/') def hello_world(): return json.dumps({ "msg":"Hello, World!" }) from fastapi import FastAPI app = FastAPI() @app.get("/") async def hello_world(): return {"msg": "Hello, World!"}
  5. 機能サンプル ページネーション用のクエリパラメータ from fastapi import FastAPI app = FastAPI() @app.get("/items/")

    async def get_items(p: int = 1): """ ``/items/`` => ``p`` = 1 ``/items/?p=3`` => ``p`` = 3 ``/items/?p=three`` => バリデーションエラー """ return {"msg": "Hello World"}
  6. 機能サンプル $ http localhost:8000/items/ p==3 HTTP/1.1 200 OK { "msg":

    "Hello World" } $ http localhost:8000/items/ p==three HTTP/1.1 422 Unprocessable Entity { "detail": [ { "loc": [ "query", "p" ], "msg": "value is not a valid int "type": "type_error.integer" } ] }
  7. pyproject.toml 重要なところだけ抜粋 [tool.poetry] name = "chatbot" [tool.poetry.dependencies] python = "^3.8"

    # 本体 fastapi = "^0.60.1" # POSTリクエスト時にformdataを受け付けるのに必要 python-multipart = "^0.0.5" # ASGIサーバー uvicorn = "^0.11.7"
  8. chatbot.py ルーティング関数のデフォルト引数が Form の場合、 POST リクエストから取る from fastapi import FastAPI,

    Form app = FastAPI() @app.post("/slash-commands/hello") async def hello(text: str = Form(...), user_name: str = Form(...)): return { "text": f"Hello {user_name}, I recieved `{text}`" }
  9. 動きを確認 $ http --form localhost:8000/slash-commands/hello text=Example user_name=attakei HTTP/1.1 200 OK

    content-length: 46 content-type: application/json date: Fri, 28 Aug 2020 18:14:23 GMT server: uvicorn { "text": "Hello attakei, I recieved `Example`" }
  10. よりChatOpsっぽいコマンド /dig を作ります ※ dig = DNSの問い合わせをするツール Slack上のコマンドイメージ $ dig

    example.com ;; QUESTION SECTION: ;example.com. IN A ;; ANSWER SECTION: example.com. 48418 IN A 93.184.216.34 /dig example.com
  11. (1)を拡張して作る、 /dig # import を省略 # dns-pythonを使います app = FastAPI()

    @app.post("/slash-commands/dig") async def dig(text: str = Form("")): # DNSPythonを使ってDNS情報を取得する name = dns.name.from_text(text) query = dns.message.make_query(name, dns.rdatatype.A) msg = dns.query.udp(query, "8.8.8.8") return { "text": f"Answer for `A` of `{name}`\n```{msg}```" }
  12. 試してみる $ http --form localhost:8000/slash-commands/dig text=example.com HTTP/1.1 200 OK content-length:

    207 content-type: application/json date: Fri, 28 Aug 2020 18:18:19 GMT server: uvicorn { "text": "Answer for `A` of `example.com.`\n```id 31266\nopcode QUERY\nrcode NOERROR }
  13. 試してみる Answer for `A` of `example.com.` ```id 31266 opcode QUERY

    rcode NOERROR flags QR RD RA ;QUESTION example.com. IN A ;ANSWER example.com. 20986 IN A 93.184.216.34 ;AUTHORITY ;ADDITIONAL```
  14. 3秒しか猶予がない # import を省略 app = FastAPI() @app.post("/slash-commands/dig") async def

    dig(text: str = Form("")): name = dns.name.from_text(text) query = dns.message.make_query(name, dns.rdatatype.A) # もし、ここで想定外の時間がかかったら? msg = dns.query.udp(query, "8.8.8.8") return { "text": f"Answer for `A` of `{name}`\n```{msg}```" }
  15. Background Task機能 # import を省略 app = FastAPI() async def

    query_dns(name: str, url: str): name = dns.name.from_text(name) query = dns.message.make_query(name, dns.rdatatype.A) await asyncio.sleep(5) # 時間のかかる仮定 msg = dns.query.udp(query, "8.8.8.8") slack = slackweb.Slack(url) slack.notify(text=f"Answer for `A` of `{name}`\n```{msg}```") @app.post("/slash-commands/dig") async def dig( bg: BackgroundTasks, text: str = Form(""), response_url: str = Form("") ): # 直接処理せずに、移譲する bg.add_task(query_dns, text, response_url) # 空のレスポンスを返せば、まずは何もしない return Response()
  16. Cloud Run向けDockerfile(例) # ------------------ # Build stage # ------------------ FROM

    python:3.8-slim as buildenv RUN mkdir -p /build/chatbot COPY chatbot/* /build/chatbot/ COPY poetry.lock pyproject.toml /build/ WORKDIR /build RUN poetry build # ------------------ # Running stage # ------------------ FROM python:3.8-slim COPY --from=buildenv /build/dist/chatbot-*-py3-none-any.whl / RUN pip install /chatbot-*-py3-none-any.whl CMD ["uvicorn", "chatbot:app", "--port", "8080"]