LangChain v0.3　その７ ~ChatBot１「愛のメモリ〜」~

# 今日の目的
今日の目的は`ChatBot`です。memoryです。愛のメモリーは松崎しげるさんです。熱い歌に胸キュンです。暑い見かけに僕と同じおっさんを感じます。（かっこいいけど）
`ChatBot`として機能するには過去の会話履歴の記録が必須です。その機能を作っていきます。
といっても一部はLangGraphの中でもやっていたんですけどね。
プログラム界隈は複数のことが入り混じっているので、色々並行して理解を進めていかなければいけないので、完璧な理解なんぞ、天才のみがやることじゃないかって思ってます。笑

目標
- 過去の履歴を参照して回答する
<br>
<br>
<br>

# ここまでの経過
プロンプトテンプレートや、LCEL、LangGraphなどをやってきたので、基礎はできてきていると思います。
いきなりここにきちゃった方は過去の記事を参照くださいませ。

<br>
<br>
<br>

# バージョン関連
Python 3.10.8
langchain==0.3.7
python-dotenv
langchain-openai==0.2.5
langgraph>0.2.27
langchain-core

※LLMのAPIはAzureOpenAIのgpt-4o-miniを使いました

<br>
<br>
<br>

# ライブラリのインポートと環境変数の設定
これはいつも通り

```python:ライブラリ
import os
from dotenv import load_dotenv
from langchain_openai import AzureChatOpenAI, AzureOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, trim_messages
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, END, MessagesState, StateGraph
```

```python:環境変数
load_dotenv('.env')

os.environ["LANGCHAIN_TRACING_V2"]="true"
os.environ["LANGCHAIN_ENDPOINT"]="https://api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"]=os.getenv("LANG_SMITH_API")
os.environ["LANGCHAIN_PROJECT"]="langchain_test"
```

```python:モデルの準備
model = AzureChatOpenAI(
    azure_deployment='gpt-4o-mini',
    azure_endpoint=os.getenv("END_POINT"),
    api_version=os.getenv("API_VERSION"),
    api_key=os.getenv("API_KEY"),
    max_tokens=150
)
```
<br>
<br>
<br>



# まずは素のLLMを確認
```python
model.invoke([HumanMessage(content="僕の名前は池駄賃です。")])
# AIMessage(content='池駄賃さん、こんにちは！お話ししたいこと・・・)
```

```python
model.invoke([HumanMessage(content="僕の名前を知ってる？")])
# AIMessage(content='ごめんなさい、あなたの名前はわかりません。・・・)
```
はい。単にモデルを呼び出しただけでは会話履歴が反映されません。
これをステートレスと言ったりするらしい。📝

ということで、Memory機能を追加していきましょう。
<br>
<br>
<br>

# Memory機能を追加してChatBot化
ということで過去の会話履歴を参照できる様にしてChatBot化しましょう。

```python:グラフを作る
def call_model(state: MessagesState):
    response = model.invoke(state["messages"])
    return {"messages": response}

workflow1 = StateGraph(state_schema=MessagesState)
workflow1.add_node("model", call_model)
workflow1.add_edge(START, "model")
workflow1.add_edge("model", END)

memory = MemorySaver()
graph1 = workflow1.compile(checkpointer=memory)


########### graphの可視化
from IPython.display import Image, display

try:
    display(Image(graph1.get_graph().draw_mermaid_png()))
except Exception:
    pass
```
![output.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655320/0b75738a-2ea9-c8c1-7fad-d58e2c9ee074.jpeg)

すごーくシンプルなグラフを組みました。
肝はLangGraphの`MemorySaver`を使うところですね。グラフをコンパイルするときに渡してあげればいいみたい。


<br>
<br>
<br>


さて、瀬戸内で大活躍した村上水軍になったつもりで試してみましょう。

```python:最初の会話
query = "僕は海賊の村上です。瀬戸内で暴れてます。"
input_messages = [HumanMessage(query)]
output = graph1.invoke({"messages": input_messages}, config)
output
# {'messages': [
#     HumanMessage(content='僕は海賊の村上です。瀬戸内で暴れてます。'・・・), 
#     AIMessage(content='村上さん、海賊としての冒険は楽しそうですね！・・・）
# ]
```

```python:過去の会話の内容を聞いてみる
query = "ところで、僕の名前はなに？"
input_messages = [HumanMessage(query)]
output = graph1.invoke({"messages": input_messages}, config)
output
# {'messages': [
#     HumanMessage(content='僕は海賊の村上です。瀬戸内で暴れてます。'・・・), 
#     AIMessage(content='村上さん、海賊としての冒険は楽しそうですね！・・・),
#     HumanMessage(content='ところで、僕の名前はなに？'・・・), 
#     AIMessage(content='あなたの名前は「村上」ですよ！海賊としての・・・),
# ]
```
ということで、ちゃんと会話の履歴を参照しながら回答することができました。
結構簡単だったな。w
ま、前回やったLangGraphでも会話履歴は残せてましたね。

<br>
<br>
<b>

# PromptTemplateを使う場合
システムプロンプトを定義するような場合ですね。これも普通に使えばいいだけです。
LLMに海賊になりきってもらいましょう！

```python:プロンプトテンプレートの用意
prompt = ChatPromptTemplate.from_messages([
        ("system", "あなたは海賊です。最大限努力して海賊として回答してください。",),
        MessagesPlaceholder(variable_name="messages"), 
        # MessagePlaceholderクラスのところを入力変数'messages'で置き換える
    ])
```

```python:グラフの定義
workflow2 = StateGraph(state_schema=MessagesState)


def call_model(state: MessagesState):
    chain = prompt | model
    response = chain.invoke(state)
    return {"messages": response}


workflow2.add_node("model", call_model)
workflow2.add_edge(START, "model")
workflow2.add_edge("model", END)

memory = MemorySaver()
graph2 = workflow2.compile(checkpointer=memory)
```

<br>
<br>
<br>

プロンプトテンプレートを使うときにはthread_idを定義できます。つまり、会話を分けることができるってことですよね。idは適当に作ってます。



```python:configの用意
config = {"configurable": {"thread_id": "abc345"}}
```

```python:最初の会話
query = "まいど～！ぼくのなまえは池駄賃だよ"

input_messages = [HumanMessage(query)]
output = graph2.invoke({"messages": input_messages}, config)
output
# {'messages': [
#     HumanMessage(content='まいど～！ぼくのなまえは池駄賃だよ'・・・),
#     AIMessage(content='まいど！池駄賃、海賊仲間よ！お前の名前、海の荒波の・・・),
# ]
```
こやつ、システムプロンプトによって海賊になりきっとる！笑


<br>
<br>
<br>


```python:会話を追加
query = "LangChainを使いこなしたくて勉強中なんだ。海賊船でも勉強できるかな？"

input_messages = [HumanMessage(query)]
output = graph2.invoke({"messages": input_messages}, config)

query = "ところで、ぼくの名前は何だっけ？"
input_messages = [HumanMessage(query)]
output = graph2.invoke({"messages": input_messages}, config)
output
# {'messages': [
#     HumanMessage(content='まいど～！ぼくのなまえは池駄賃だよ'・・・),
#     AIMessage(content='まいど！池駄賃、海賊仲間よ！お前の名前、海の荒波の・・・),
#     HumanMessage(content='LangChainを使いこなしたくて勉強中なんだ。・・・),
#     AIMessage(content='アーッハッハッ！もちろんじゃ、池駄賃！海賊船でも勉強は・・・)
#     HumanMessage(content='ところで、ぼくの名前は何だっけ？'・・・),
#     AIMessage(content='おお、池駄賃！お前の名前は池駄賃じゃ！忘れるわけがないわい。・・・),
# ]
```

勤勉そうな海賊さんだ！笑
しっかり、池駄賃の名前を覚えていましたね

<br>
<br>
<br>


# 終わりに
会話履歴を参照することで、ちゃんとchatbotとして機能しました。
前回のLangGraphでやった方法と合わせると実現方法はいくつかありそうですね。

どの方法がいいかは都度考えていく必要がありそうですが、会話の履歴はリスト形式でモデルに渡すといい様です。（今回のstateの"message"ですね）

長くなりそうなので、一旦ここで区切ります。
次回、会話履歴のtrimmingについてやってみましょう。