【LangServe】2024年12月時点での書き方
以前こちらの記事で書いたのですが、色々バージョンアップしていました
立ち上げ
python3.12を想定しています
$ langchain app new .
以下のようなファイルができています
.
├── Dockerfile
├── README.md
├── app
│ ├── __init__.py
│ ├── __pycache__
│ └── server.py
├── packages
│ └── README.md
└── pyproject.toml
さて、この時点でpyproject.tomlを見てみると、いろいろおかしいことに気づきます
[tool.poetry]
name = "langchain_test"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
packages = [
{ include = "app" },
]
[tool.poetry.dependencies]
python = "^3.11"
uvicorn = "^0.23.2"
langserve = {extras = ["server"], version = ">=0.0.30"}
pydantic = "<2"
[tool.poetry.group.dev.dependencies]
langchain-cli = ">=0.0.15"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
まずpython3.12で立ち上げたはずなのに、勝手にpython3.11を指定しています。
なので、3.12を使うよう書き換えてやりましょう。
[tool.poetry.dependencies]
python = "^3.12" #書き換え
% poetry env use python3.12
% poetry env info
Virtualenv
Python: 3.12.7
Implementation: CPython
・・・
# 3.12であることが確認できたら、パッケージをアップデートする
% poetry update
ローカルでの動作確認
さて、この時点でローカルで立ち上げてもエラーが発生します
% poetry run langchain serve
# 以下のようなエラー
・・・
ImportError: cannot import name 'VerifyTypes' from 'httpx._types'
理由はlangserveのREADMEに書かれている通り、pydanticのバージョンがあっていないからです。
Versions of LangServe <= 0.2.0, will not generate OpenAPI docs properly when using Pydantic V2 as Fast API does not support mixing pydantic v1 and v2 namespaces. See section below for more details. Either upgrade to LangServe>=0.3.0 or downgrade Pydantic to pydantic 1.
※ そういえばpython3.13でpydanticの不一致が解消されたような気がします
対処法として、langserveのバージョンを0.3以上にすることを明記し、pydanticのバージョン指定を消します
[tool.poetry.dependencies]
python = "^3.12"
uvicorn = "^0.23.2"
langserve = {extras = ["server"], version = ">=0.3"} # 書き換え
- pydantic = "<2" # 消去
再度poetry update
を実行して、langchainを立ち上げてみましょう。
今度は問題なくswaggerが立ち上がったはずです。
LLMの導入
% poetry add langchain-openai langchain
% poetry add python-dotenv
OPENAI_API_KEY="sk-*******"
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from langserve import add_routes
from fastapi.middleware.cors import CORSMiddleware
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from dotenv import load_dotenv
load_dotenv()
app = FastAPI()
# 追加
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def redirect_root_to_docs():
return RedirectResponse("/docs")
# Edit this to add the chain you want to add
template = """
あなたはお笑い芸人です。ユーザーのメッセージに対して、面白い返答を返すようにしてください。
お題:{user_message}
"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(model="gpt-4o-mini")
add_routes(
app,
prompt | llm,
path="/chat",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
準備ができたら、langchainを立ち上げてください
/chatルートに色々できているはずです。
Dockerfileの編集とコンテナ作成
Dockerfileはlangserve立ち上げ時にデフォルトで作成されますが、内容が色々間違っているので修正します。
FROM python:3.12-slim # python3.11から書き換え
RUN pip install poetry==1.8.4 # 実際に使用しているpoetryのバージョンに合わせる
poetryのバージョンは、以下のコマンドで確認できます
% pip show poetry
「Build Image」でビルドできたら、コンテナで動かしてみましょう
GCPへデプロイ
以下のようなコマンド実行ファイルを作成すると簡単です。
#!/bin/bash
PROJECT_ID="ここにPROJECT_IDを入れる"
REGION="asia-northeast2"
SERVICE_NAME="何か適当に名前をつける"
echo "Building and pushing Docker image..."
gcloud builds submit --tag gcr.io/${PROJECT_ID}/testfunc
echo "Deploying to Cloud Run..."
gcloud run deploy ${SERVICE_NAME} \
--image gcr.io/${PROJECT_ID}/testfunc \
--platform managed \
--region ${REGION} \
--allow-unauthenticated \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 10 \
--port 8080 \
--timeout 300 \
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID},OPENAI_API_KEY=${OPENAI_API_KEY}"
echo "Deployment completed. Checking service URL..."
gcloud run services describe ${SERVICE_NAME} \
--platform managed \
--region ${REGION} \
--format='value(status.url)'
% zsh deploy.sh
フロントの画面
メッセージアプリにしました。
VueとReactはTailwindCssとdaisyuiを使っています。
Vue
クリックして展開
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { SystemMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
type Message = SystemMessage | HumanMessage | AIMessage
const chatHistory = reactive<Message[]>([
new SystemMessage('あなたは人工知能です'),
new AIMessage('何かお題をください'),
])
const humanNewMessage = ref('')
const isComposing = ref(false) // 日本語入力中かどうか
// キーボードの入力を監視
const handleKeyDown = async(e: KeyboardEvent) => {
// Shift + Enterの場合は改行を許可
if (e.shiftKey && e.key === 'Enter') {
return
}
// 通常のEnterの場合
if (e.key === 'Enter') {
// 日本語入力中は送信しない
if (isComposing.value) {
return
}
e.preventDefault() // デフォルトの改行を防ぐ
// メッセージが空の場合は送信しない
if (!humanNewMessage.value.trim()) {
return
}
chatHistory.push(new HumanMessage(humanNewMessage.value))
// メッセージを送信
await handleSendMessage(humanNewMessage.value)
humanNewMessage.value = '' // 入力欄をクリア
}
}
const streamingAiMessage = ref('')
const handleSendMessage = async(message:string) => {
const response = await fetch('http://127.0.0.1:8000/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
input: {user_message: message},
config: {},
kwargs: {}
}),
})
if (!response.ok) throw new Error('Network response was not ok')
const reader = response.body?.getReader()
if (!reader) throw new Error('Reader not available')
while (true) {
const { done, value } = await reader.read()
if (done) break
// 受信したチャンクをデコード
const chunk = new TextDecoder().decode(value)
if(chunk.startsWith('event: end'))break
if(!chunk.startsWith('event: data'))continue
// SSEフォーマットからデータ部分を抽出
const lines = chunk.split('\n').filter(line => line.trim() !== '')
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.slice(6) // 'data: ' の部分を除去
const jsonData = JSON.parse(jsonStr)
if (jsonData.content) {
streamingAiMessage.value += jsonData.content
}
} catch (e) {
console.error('Failed to parse JSON:', e)
}
}
}
}
chatHistory.push(new AIMessage(streamingAiMessage.value))
streamingAiMessage.value = ''
}
</script>
<template>
<div class="h-screen p-4">
<div class="flex flex-col w-[500px] h-full">
<div class=" flex-1" >
<div v-for="message in chatHistory" :key="message.id">
<div v-if="message.getType()!=='system'" class=" chat" :class=" message.getType() === 'ai' ? 'chat-start' : 'chat-end'">
<div class="chat-bubble">
{{ message.content }}
</div>
</div>
</div>
<div v-if=" streamingAiMessage">
<div class=" chat chat-start">
<div class="chat-bubble">
{{ streamingAiMessage }}
</div>
</div>
</div>
</div>
<div class="flex items-center">
<textarea class=" textarea textarea-bordered flex-1"
v-model="humanNewMessage"
@keydown="handleKeyDown"
@compositionstart="isComposing = true"
@compositionend="isComposing = false"
></textarea>
</div>
</div>
</div>
</template>
Discussion