🐕‍🦺

【LangServe】ローカル開発からGCPへのデプロイまで

2024/06/20に公開

こちらの公式ドキュメントの解説です。
https://python.langchain.com/v0.2/docs/langserve/

最終的にGCPにデプロイします。

公式通りにやっても、うまくいきません
(というより、公式はデプロイ周りの解説がものすごい適当です)

私の手元でデプロイまでやりましたので、備忘録を兼ねて記事にします。

アプリケーション立ち上げ〜デプロイまで

まずはAI関係なく、langchain開発環境で立ち上げたアプリケーションで、簡単なフロントサイドを作成し、デプロイまでやりましょう。

アプリケーション立ち上げ

terminal
# アプリケーション用のフォルダを作成。
# フォルダに移動後
% langchain app new .

フォルダとファイル構成は以下のようになっているはずです。

.
├── Dockerfile
├── README.md
├── app
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-312.pyc
│   │   └── server.cpython-312.pyc
│   └── server.py
├── packages
│   └── README.md
└── pyproject.toml

メインで触るのは、app/server.pyです。
そしてDockerがデフォルトで付いてくるのが特徴です。
(ちなみに私はDockerをほとんど使ったことがなく、だいぶ苦戦しました)

開発サーバー起動

langchain-cliでサーバー起動はlangchain serveを使います。

% langchain serve

ちなみに最初の状態では失敗します。
これは、app/server.pyのadd_routes(app, NotImplemented)の部分が無効なためで、この部分をコメントアウトすると、問題なく起動します

app/server.py
# add_routes(app, NotImplemented)

試しにブラウザでhttp://127.0.0.1:8000/docsにアクセスしてみましょう。

swaggerUIが開くはずです。

webページを作成

fastAPIでwebページを作成してみましょう

HTMLを返すにはJinja2を使います。

ルートディレクトリにtemplates/index.htmlというファイルを作成してください。

templates/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <h1>Hello, world!</h1>
  </body>
</html>

app/server.pyは以下のように/indexを追加します

app/server.py
from fastapi import FastAPI, Request # 追加
from fastapi.responses import RedirectResponse # 追加
from fastapi.templating import Jinja2Templates
# from langserve import add_routes

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/")
async def redirect_root_to_docs():
    return RedirectResponse("/docs")

# 追加
@app.get("/index")
async def index(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})


# Edit this to add the chain you want to add
# add_routes(app, NotImplemented)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

http://127.0.0.1:8000/indexにアクセスして、HTMLのページが表示されることを確認しましょう。

余談ですが、fastAPIではクエリパラメーターを渡してページに表示させることができます。
以下の例ではクエリパラメーターにname=aaaを渡したときに、aaaと表示させるようにしています

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <h1>Hello, world!</h1>
    <div>{{name}}</div>
  </body>
</html>
server.py
・・・
@app.get("/index")
async def index(request: Request, name: str = ''):
    return templates.TemplateResponse("index.html", {'name':name, "request": request})
・・・

Dockerイメージ作成

dockerイメージを作成するにあたり、Dockerfileに少し修正が必要です。

まずpythonは執筆時点での最新バージョンの3.12に設定します。

また、webページに./templateを書いたので、これもDocker Image作成時にコピーするようにします。

# FROM python:3.11-slim
FROM python:3.12-slim

RUN pip install poetry==1.6.1

RUN poetry config virtualenvs.create false

WORKDIR /code

COPY ./pyproject.toml ./README.md ./poetry.lock* ./

COPY ./package[s] ./packages

RUN poetry install  --no-interaction --no-ansi --no-root

COPY ./app ./app
# 追加
COPY ./templates ./templates

RUN poetry install --no-interaction --no-ansi

EXPOSE 8080

CMD exec uvicorn app.server:app --host 0.0.0.0 --port 8080

修正したら、Dockerfile上で右クリックし、Build imageをクリックしましょう

これで、Dockerイメージファイルができますので、latestの上で右クリックしてRunをクリックしましょう。

ただし、今の状態ではブラウザでhttp://0.0.0.0:8080にアクセスしても開けません。

理由の一つはjinja2ライブラリがインストールされていないため、一つはCORS設定のためです。

まずjinja2ライブラリのインストールは、pipではなくpoetryを使います。
poetryはpython版のnpmやyarnみたいなものです。

% poetry add jinja2

そうすると、pyproject.tomlにjinja2が追加されます
(javascriptのpackage.jsonみたいなものと考えれば良いでしょう)

次にCORSは以下の通り設定します

server.py
app = FastAPI()
templates = Jinja2Templates(directory="templates")

# 追加
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://127.0.0.1:8080", "http://0.0.0.0:8080"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

ここまでできたら、再度Docker imageをビルドし、実行してみましょう。

http://0.0.0.0:8080にアクセスして開けるようになっているはずです。

gcloudにビルド、デプロイ

最後にgcloudにデプロイします。

Google cloudで何かしらプロジェクトを作成してください。
(今回作成したプロジェクト名はmicro-avenue-423705-f0というIDでした)

ちなみにデプロイするには課金が必要です。

今回のようなシンプルなアプリでも一回のデプロイで1円ほどかかります。
失敗しまくると数十円くらいかかってしまうので注意しましょう。

Google cloudの用意が整ったら、以下のようにDockerイメージをプッシュし、Cloud Runにデプロイしましょう
(この例で「micro-avenue-423705-f0」はproject-id、「langserve-app」は好きな名前で付けられる関数名です)

# Dockerイメージのビルドとプッシュ
gcloud builds submit --tag gcr.io/micro-avenue-423705-f0/langserve-app

# Cloud Runへのデプロイ
gcloud run deploy langserve-app \
    --image gcr.io/micro-avenue-423705-f0/langserve-app \
    --platform managed \
    --region asia-northeast2 \
    --allow-unauthenticated

ちなみにregionのasia-northeast2は大阪です。東京はasia-northeast1を指定してください

※ Cloud Runへのデプロイの仕方はこちら
https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service?hl=ja

大喜利アプリにしてみる

ここからAIを組み込んでいきます

ライブラリの導入

今回のプロンプトとモデルにはChatPromptTemplateとChatOpenAIモデルを用います。

server.py
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langserve import add_routes

繰り返しになりますが、pipではなくpoetryで追加してください。

% poetry add langchain-openai
# (他も必要に応じて追加)

langchainのバージョンでエラーが出る場合

2024年7月13日に改めて作成したところ、langchainが要求するpydanticのバージョンが違うためエラーがでました。

Because no versions of langchain-core match >0.2.17,<0.2.18 || >0.2.18,<0.3.0
 and langchain-core (0.2.17) depends on pydantic (>=2.7.4,<3.0.0), langchain-core (>=0.2.17,<0.2.18 || >0.2.18,<0.3.0) requires pydantic (>=2.7.4,<3.0.0).

だからといってpydanticのバージョンを2に上げてしまうと、今度はfastapiが要求するpydanticのバージョンと合わなくなり、swaggerにルーティングが正確に表示されなくなってしまいます。

⚠️ Using pydantic 2.8.2. OpenAPI docs for invoke, batch, stream, stream_log endpoints will not be generated. API endpoints and playground should work as expected. If you need to see the docs, you can downgrade to pydantic 1. For example, pip install pydantic==1.10.13See https://github.com/tiangolo/fastapi/issues/10360 for details.

対処法として、7月13日時点ではlangchainの最新バージョンは0.2.7ですが、これを0.2.5を指定することでエラーが起きなくなります。

langchain = "^0.2.5"
langchain-community = "^0.2.5"

環境変数の設定

pythonではpython-dotenvというライブラリを使います。

server.py
from dotenv import load_dotenv
load_dotenv()

.envファイルは以下のように書きます

OPENAI_API_KEY = **************

LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=**************
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com

ルートの追加

通常のfastAPIのルート追加(@app.get('/'))と異なり、langserveではadd_routesを使ってルートを追加することができます。

以下のように/postルートを追加してみましょう。

model = ChatOpenAI(model="gpt-3.5-turbo-0125")
prompt = ChatPromptTemplate.from_template("あなたはお笑い芸人です。お題に対して、大喜利の答えを返してください。お題:{topic}")
add_routes(
    app,
    prompt | model,
    path="/post",
)

さて、/postルートを追加したことで、どのようにルーティングが反映されたか見てみましょう。

ローカルサーバーの起動はpoetryを使って起動してください(langchain serveだけだとライブラリがありませんとエラーが出ます)

% poetry run langchain serve

起動したらローカルホストhttp://0.0.0.0:8000/docsにアクセスしましょう。
(docsはルーティングをswaggerで確認できる特殊ルートです)

以下のように、/postでいろいろなルートが追加されていることがわかります。

この中で使うのは/post/invokeです。

以下のようなrequest bodyを渡せばよいことがわかります。

ちなみに inputにtopicというフィールドがあるのは、promptに
"あなたはお笑い芸人です。お題に対して、大喜利の答えを返してください。お題:{topic}"
と作成したからです。

そして、成功したら以下のようなレスポンスが返ってくることがわかります。

try it outで何か試してみると良いでしょう。

フロントサイド

サーバーサイドのAPIを作成できましたので、フロントサイドを簡単に作ってみます。

お題を投げるように、フォームを作成します
(フレームワークやライブラリなしでHTMLやjavascript書いたの何年ぶりだろう)

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <h1>大喜利アプリ</h1>

    <form method="post" action="/post">
      <input type="text" name="topic" placeholder="お題を与えてください">

      <button>送信</button>
    </form>

    <div>
      <p id="answer">ここに回答</p>
    </div>

    <script>
      const answer = document.getElementById('answer');
      const form = document.querySelector('form');

      form.addEventListener('submit', async (event) => {
        event.preventDefault();

        const formData = new FormData(form);
        const response = await fetch('/post/invoke', {
          method: 'POST',
          body: JSON.stringify({
            "input": {
              "topic": formData.get('topic')
            },
            "config": {},
            "kwargs": {}
          }),
          headers: {
            'Content-Type': 'application/json'
          }
        });

        const data = await response.json();
        answer.textContent = data.output.content;
      });
    </script>
  </body>
</html>

ここまでできたら、フロントサイドからお題を投げて、回答を得られることを確認してみましょう。

こんな感じで回答が得られるはずです。

Dockerの作成とデプロイ

先ほどと同じ手順でDocker Imageを作成し、デプロイしましょう。

Docker Imageを作成する際に.envを追加してやる必要があります。

Dockerfile
# FROM python:3.11-slim
FROM python:3.12-slim

RUN pip install poetry==1.6.1

RUN poetry config virtualenvs.create false

WORKDIR /code

COPY ./pyproject.toml ./README.md ./poetry.lock* ./

COPY ./package[s] ./packages

RUN poetry install  --no-interaction --no-ansi --no-root

COPY ./app ./app

COPY ./templates ./templates

COPY .env .env # 追加

RUN poetry install --no-interaction --no-ansi

EXPOSE 8080

CMD exec uvicorn app.server:app --host 0.0.0.0 --port 8080

あとは先ほどと同じように、ローカルでDocker Imageを作成とDocker Containerの稼働確認。

ローカルで稼働が確認できたら、GCPへDockerイメージのビルドとプッシュをしましょう。

# Dockerイメージのビルドとプッシュ
gcloud builds submit --tag gcr.io/micro-avenue-423705-f0/langserve-app

# Cloud Runへのデプロイ
gcloud run deploy langserve-app \
    --image gcr.io/micro-avenue-423705-f0/langserve-app \
    --platform managed \
    --region asia-northeast2 \
    --allow-unauthenticated

デプロイできました!

Discussion