Closed5

LangChainで作ったChain/AgentをLangServeでAPI化する

kun432kun432

CLIが必要になりそう&普段使わないpoetryを使うようなので、今回はdevcontainerを使う。

.devcontainer/.devcontainer.json
{
	"name": "Python 3",
	"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm",
	"features": {
		"ghcr.io/devcontainers-contrib/features/poetry:2": {}
	}
}

コンテナで開いて以降はコンテナ内の作業。

パッケージのインストール。extraは今回は全てを指定。

$ pip install "langserve[all]"

LangChainのCLIを使うと、LangServeに対応したプロジェクトディレクトリを作成してくれるらしいので、あわせてインストール。

$ pip install langchain-cli
$ pip freeze | egrep "lang(chain|serve)"
langchain-cli==0.0.24
langchain-core==0.2.5
langserve==0.2.2

LangChain CLIでプロジェクトを作成。

$ langchain app new my-app

対話モードになる。パッケージをここで追加できるようだけども、ENTERで一旦スキップ。

What package would you like to add? (leave blank to skip): 

プロジェクトディレクトリが作成された

Success! Created a new LangChain app under "./my-app"!


Next, enter your new app directory by running:

    cd ./my-app

Then add templates with commands like:

    langchain app add extraction-openai-functions
    langchain app add git+ssh://git@github.com/efriis/simple-pirate.git

プロジェクトディレクトリの中身はこんな感じ。

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

4 directories, 8 files

my-app/app/server.pyがAPIサーバのスクリプト。ここのadd_routesにrunnablesを書けばいいらしい。そこはとりあえず後回し。

./my-app/app/server.py
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from langserve import add_routes

app = FastAPI()


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


# 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)

パッケージを追加する場合はpoetryを使う。langchain-openaiを追加する。

$ cd my-app    # プロジェクトディレクトリに移動
$ poetry add langchain-openai

OpenAIのAPIキーを環境変数にセット。実際にやる場合は.envあたりを使うのが良いと思う。

$ export OPENAI_API_KEY="sk-..."

あとは以下で起動するだけだけども、コードはまだ何も書いてないのでエラーになる。

$ poetry run langchain serve

ということでチュートリアルのサンプルコードを反映する。

./my-app/app/server.py
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from langserve import add_routes

# langchainをインポート
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI

app = FastAPI()


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

# モデルやプロンプトなどの定義
model = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_template("{topic}についてのジョークを考えて。")

# /jokeでchainを実行する
add_routes(
    app,
    prompt | model,  # LCELでチェーンを書く
    path="/joke"
)

if __name__ == "__main__":
    import uvicorn

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

では起動する。ポートを変更する場合などは以下のようにして変更できる。

$ poetry run langchain serve --port 8100
INFO:     Will watch for changes in these directories: ['/workspaces/langserve-test/my-app']
INFO:     Uvicorn running on http://127.0.0.1:8100 (Press CTRL+C to quit)
INFO:     Started reloader process [13607] using StatReload
INFO:     Started server process [13616]
INFO:     Waiting for application startup.

 __          ___      .__   __.   _______      _______. _______ .______     ____    ____  _______
|  |        /   \     |  \ |  |  /  _____|    /       ||   ____||   _  \    \   \  /   / |   ____|
|  |       /  ^  \    |   \|  | |  |  __     |   (----`|  |__   |  |_)  |    \   \/   /  |  |__
|  |      /  /_\  \   |  . `  | |  | |_ |     \   \    |   __|  |      /      \      /   |   __|
|  `----./  _____  \  |  |\   | |  |__| | .----)   |   |  |____ |  |\  \----.  \    /    |  |____
|_______/__/     \__\ |__| \__|  \______| |_______/    |_______|| _| `._____|   \__/     |_______|

LANGSERVE: Playground for chain "/joke/" is live at:
LANGSERVE:  │
LANGSERVE:  └──> /joke/playground/
LANGSERVE:
LANGSERVE: See all available routes at /docs/

INFO:     Application startup complete.

こんな感じで起動した。

ということでとりあえずシンプルにcurlで試してみる。

$ curl -X POST 'http://localhost:8000/joke/invoke' \
    -H 'Content-Type: application/json' \
    --data-raw '{
        "input": {
            "topic": "猫"
        }
    }' | jq -r .
{
  "output": {
    "content": "もちろん、猫についてのジョークを考えました:\n\nどうして猫はインターネットが好きなのか知ってる?\nだって、そこには\"キャット\"フィッシュがたくさんいるからさ!\n\nどうでしょうか?気に入っていただけたら嬉しいです。",
    "additional_kwargs": {},
    "response_metadata": {
      "token_usage": {
        "completion_tokens": 64,
        "prompt_tokens": 16,
        "total_tokens": 80
      },
      "model_name": "gpt-4o",
      "system_fingerprint": "fp_319be4768e",
      "finish_reason": "stop",
      "logprobs": null
    },
    "type": "ai",
    "name": null,
    "id": "run-f0ce5e48-5901-43b3-80fc-b178be4d85dc-0",
    "example": false,
    "tool_calls": [],
    "invalid_tool_calls": [],
    "usage_metadata": {
      "input_tokens": 16,
      "output_tokens": 64,
      "total_tokens": 80
    }
  },
  "metadata": {
    "run_id": "43f32b90-4ba6-4d8b-b116-8b567c686987",
    "feedback_tokens": []
  }
}

/runnable名/がエンドポイントのベースになって、invokestream とかで叩けるっぽい。

pythonでinvoke。

import requests 

response = requests.post(
    "http://localhost:8100/joke/invoke",
    json={'input': {'topic': '猫'}}
)
print(response.json()["output"]["content"])

langserveはクライアントライブラリとしても使用できる。streamの例。

from langserve import RemoteRunnable
import asyncio

joke_chain = RemoteRunnable("http://localhost:8100/joke/")

async def jokes(topic: str):
    async for msg in joke_chain.astream({"topic": topic}):
        print(msg.content, end="", flush=True)

asyncio.run(jokes("猫"))
kun432kun432

/runnable名/playgroundにアクセスすると、チャットUIで簡単に試せる。ただし、よくあるplaygroundのような自由度はなく、runnableの入力スキーマが辞書であること、かつ、以下のどちらかの条件を満たす必要がある。

  • 1つのキーがあり、そのキーの値はチャットメッセージのリストでなければならない。
  • 2つのキーがあり、1つはメッセージのリスト、もう1つは最新のメッセージでなければならない。

playground_type="chat"をapp_routesに設定する必要がある、と書いてあるが、特に指定しなくてもアクセスできた。

このスクラップは2024/06/12にクローズされました