LangChain v0.3　その14　~LangMemでAgentを理解する１~

# 1. きっかけ

https://generative-agents.connpass.com/event/346713/

このイベントに参加したんです。昼休みだし。昼休みはいつも自席でパン食べてるし参加しやすい。
それに、 **LangMemってなんや！** って気になったし。
これだけのきっかけだったのですが、会話の中から記憶を追加していくって使う場面が簡単に想定できたんです。wkwk

僕、自転車のロードレースの中継をしながらしゃべったりするんですけど、レース時間が長いので序盤のレース展開とか簡単に忘れちゃう（ぉい！）。そして思い出せない。（加齢）
そこで、このLangMemを使うと解決でき・・・そうじゃん！
わくわくしちゃう

<br>

**わくわくはモチベーション**でしょ！！

<br>

で、`LangMem`は`Langgraph`の`Prebuilt`のなかの`Agent`の一つであったりします。

<br>

https://langchain-ai.github.io/langgraph/prebuilt/#available-libraries

<br>

`Agent`のページから`LangMem`に行くと`GitHub`に行っちゃいますが、以下から行くとドキュメントに行けます。こっちから行くとドキュメントにいって、あっちから行くとGitHubってめんどくさい。もう少しわかりやすくしてくれると良いのにね！笑

<br>

https://langchain-ai.github.io/langmem/

<br><br>


# 2. バージョン情報

:::note
バージョン情報
Python==3.11.9
langchain==0.3.19
python-dotenv
langchain-openai==0.3.7
langgraph==0.3.2
langchain-core==0.3.40
langchain-community==0.3.18
:::

実はいろいろあって、Pythonのバージョンをあげちゃいました。
そして、このLangmemをやるためにはlanggraphのバージョンも変更する必要があり、いろいろとこれまでのバージョンと変わっていますのでご注意ください。

<br>

実はイベントの時にも主催のGenerativeAgentsの大嶋さんもおっしゃられていたのですが、イベントの日は最新バージョンでは動きませんでした。なんとその日にアップデートしたlanggraphがダメだったという。www

<br>

https://pypi.org/project/langgraph/#history

Pypiを見ると、アップデートに苦労したみたいデス。
そして、使いたい`langgraph-prebuild`が「だめだからおすすめしないよ！」ってなってますね。
そして、0.3.1でもまともに動かず、0.3.2になるとちゃんと使えるようになりました。
ちょっと忙しくて、放置してたらどんどんアップデートされたので無駄な努力をしなくて済んだので良かった。
それにしても、大嶋さん、YoutubeLive当日にアップデートがあって使えなくなるとか引きが強い。しかし、焦りも、きょどりもない！カコエエ
（見習いたい）

<br><br>

# 3. 今回はここをやってみよう

https://langchain-ai.github.io/langmem/guides/use_tools_in_custom_agent/#example-using-openai-api

ざっとドキュメントに目を通してみて（読んではない）、ここをやってみることにしましょう。

理由は
- LangChain、LangGraphをここまでやってきているならわかるはず
- 慣れてるOpenAIだし

ドキュメント通りでは動かなかったので、ちょっと改造してみました。

<br><br>


# 4. プログラム
langmemは`pip install langmem==0.3.2`でインストールしちゃいます。
はい、僕はpip信者です。でも、イベントの中で`uv`使っててちょっとよさげって思った。

<br>


## 4-1. ライブラリインポート
```Python:ライブラリインポート
import os
from typing import Any, Dict, List
from dotenv import load_dotenv

from langchain_openai import AzureOpenAI, AzureChatOpenAI
from langgraph.store.memory import InMemoryStore
from langmem import create_manage_memory_tool, create_search_memory_tool
```

<br>

## 4-2. 環境変数の設定
```Python
load_dotenv()

os.environ['AZURE_OPENAI_API_KEY'] = os.getenv('API_KEY')
os.environ['AZURE_OPENAI_ENDPOINT'] = os.getenv('END_POINT')
```
今回は環境変数にAPIキーやエンドポイントを設定します。
これは環境変数に入れることでしか対応できません。ライブラリのクラスを書き換えればできると思いますが、あんま、メリットないので、サクッと環境変数に入れておきます。

<br>



## 4-3. tools実行関数
toolsの実行関数を作ります。
そう、Langmemはtoolsとして実装します。


```Python:toolsを実行する関数
def execute_tool(tools_by_name: Dict[str, Any], tool_call: Dict[str, Any]) -> str:
    """Execute a tool call and return the result"""
    tool_name = tool_call["name"]

    if tool_name not in tools_by_name:
        return f"Error: Tool {tool_name} not found"
    else:
        # print(tool_call)
        pass

    tool = tools_by_name[tool_name]
    # print(tool)
    try:
        result = tool.invoke(tool_call["args"])
        return str(result)
    except Exception as e:
        return f"Error executing {tool_name}: {str(e)}"
```
ほぼ、ドキュメントのコピペです。受け渡されるtool_callingとtool_calling結果で関数を実行するための関数です。
keyはtool名、valueは関数のtoolsの辞書を受けとって、実行すべきtoolsを受け取ることで実行できるようになっています。このtoolsにはメモリーとして保持すべき内容を保存するtoolとメモリーから情報を引き出すtoolを実装することになります。
他のtoolsも利用可能な汎用的な関数ですね。
変更したのは引数の`tool_call`に`'function'`がない渡し方をしたので、僕の関数からは取り除いています。

<br>



## 4-4. Agent実行関数
```Python:Agent実行関数
def run_agent(tools: List[Any], user_input: str, max_steps: int = 5) -> str:
    """Run a simple agent loop that can use tools"""
    
    # for Args of execute_tool function
    tools_by_name = {tool.name: tool for tool in tools}

    # Convert tools to OpenAI's format
    openai_tools = [
        {
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.tool_call_schema.model_json_schema(),
            },
        }
        for tool in tools
    ]


    # Setup
    llm = AzureChatOpenAI(
        model="gpt-4o-mini",
        azure_endpoint=os.getenv("END_POINT"),
        api_version=os.getenv("API_VERSION"),
        api_key=os.getenv("API_KEY")
    )
    llm_with_tools = llm.bind_tools(openai_tools)

    # Prepare 1st Messages
    messages = [{"role": "user", "content": user_input}]
    
    # REACT loop    
    for _ in range(max_steps):
        response = llm_with_tools.invoke(messages)
        tool_calls = response.tool_calls

        if not tool_calls:
            # No more tools to call, return the final response
            # print('no tools')
            return response.content

        # append result to messages
        messages.append(
            {"role": "assistant", "content": response.content, "tool_calls": tool_calls}
        )

        # calling and execute tools
        for tool_call in tool_calls:
            # print(tool_call)
            tool_result = execute_tool(tools_by_name, tool_call)
            # append tool call result in tool cooling format
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call["id"],
                    "content": tool_result,
                }
            )

    return "Reached maximum number of steps"
```
おおむねコピペです。
流れをざっくり解説すると、

1. `execute_tool`関数の引数のtools_by_nameを用意
1. `tool`sをOpenAIフォーマットに作り直し
1. GPT-4o-miniを用意して、さらに、`tools`をバインド
1. 最初のmessagesを用意
1. 決めた回数（今回は5回）、`messages`をバインドモデルに入力（出力は`messages`に追加）
1. 出力に`tool_calling`されていたら`execute_tool`を実行
1. `tool_calling`がなければ、モデルの出力をそのまま出力


という流れ。



<br>

## 4-5. toolsの用意
```Python:memory_toolsの用意
# Set up memory store and tools
store = InMemoryStore(
            index={
                "dims": 1536,
                "embed": "azure_openai:text-embedding-ada-002",
            }
        )
memory_tools = [
    create_manage_memory_tool(namespace="memories", store=store),
    create_search_memory_tool(namespace="memories", store=store),
]
```
storeに、InMemoryStoreを用います。コンピュータのメモリにベクトルストアを作る感じのようです。ここで、embeddingモデルを呼ぶんですが、ここでapi_keyなどを渡す方法を見つけられませんでした。
`4-2`でapi_keyなどを環境変数に入れておいた理由はこれです。

<br>

そして、ここでlangmemの関数が二つ出てきます。
create_manage_memory_tool: メモリーに書き込んだり、アップデートしたり、消したりするツール
create_search_memory_tool: 検索してメモリーから情報を取り出すツール

この二つをtoolsとして用います。
namespaceは適当に決めて良いと思いますが、のちにstoreから検索するときにnamespaceを渡すので、整合は取りましょう。


<br>


## 4-6. 実行
```Python
result = run_agent(
    tools=memory_tools,
    user_input="僕はフィドルを弾くのが好きですが、他人の誹謗中傷を聞くのは嫌いなので、覚えておいてください。",
)
print(result)

# フィドルを弾くのが好きで、他人の誹謗中傷を聞くのが嫌いということを覚えました。何か他に覚えておいてほしいことがあれば教えてください！
```
フィドルを弾くのが好きだけど、誹謗中傷を聞くのは嫌だよ。覚えておいてね！って入力しました。
その出力として、「覚えといたよ～」と帰ってきましたね。

<br>


## 4-6. storeの確認
```Python
print(store.search(("memories",)))

# [
#     Item(
#         namespace=['memories'], 
#         key='2a983e27-5a06-435f-b0ac-2d9dab09030f', 
#         value={'content': 'ユーザーはフィドルを弾くのが好きです。'}, 
#         created_at='2025-03-04T20:49:19.439791+00:00', 
#         updated_at='2025-03-04T20:49:19.439791+00:00', score=None
#     ), 
#     Item(
#         namespace=['memories'], 
#         key='d638dfe8-2ac2-41ce-bcdc-1aa1e24855fd', 
#         value={'content': 'ユーザーは他人の誹謗中傷を聞くのが嫌いです。'}, 
#         created_at='2025-03-04T20:49:19.490007+00:00', 
#         updated_at='2025-03-04T20:49:19.490007+00:00', 
#         score=None
#     )
# ]
```


## 4-6.メモリーから情報を取得しながら回答
### 4-6-1. 嫌いなことを聞いてみる
```Python:メモリーから情報を取得
response = run_agent(
        tools=memory_tools,
        user_input='ところで、僕が嫌いなのはなんだっけ？'
    )
print(response)
# あなたが嫌いなのは、他人の誹謗中傷を聞くことです。
```

そうだよ。みんな、SNSでの誹謗中傷はやめようね！

<br>



### 4-6-2. 好きなことを聞いてみる
```Python:メモリーから情報を取得
response = run_agent(
        tools=memory_tools,
        user_input='僕の好きなことはなんだっけ？'
    )
print(response)
# 'あなたが好きなことは、フィドルを弾くことです。また、他人の誹謗中傷を聞くのが嫌いなことも覚えています。他に何か好きなことがあれば教えてください！'
```

うまく弾けないけど、フィドルはたのしいんだよ。

<br>

### 4-6-3. 情報を追加する
```Python:会話を加える
response = run_agent(
        tools=memory_tools,
        user_input="今日は家に帰ったらフィドルの練習しなきゃ。Mason's Apronの練習にしよう"
    )
print(response)
# フィドルの練習、楽しんでくださいね！Mason's Apronは素敵な曲ですね。頑張ってください！
```

そうそう、いい曲なんだよ。


<br>

### 4-6-4. さらに質問する
```Python:付け加えた情報から回答させる
response = run_agent(
        tools=memory_tools,
        user_input="家に帰ったら何を練習するんだっけ？"
    )
print(response)
# 家に帰ったら、Mason's Apronをフィドルの練習をする予定です。楽しんでくださいね！
```

おおお、これは便利！
記憶力ゼロのぼくには常備したいくらいだ。ｗ


<br>



# 5. おわりに
InMemoryStoreとtoolsを組み合わせて会話内容から必要な内容を一時メモリに保存しておく方法をためしてみました。こうすると、モデルに入力するトークン数を抑えたり、messagesに入れておく会話回数を小さくしたりすることができるのではないでしょうか。
また、ベクトルストアを自然な対話の中から構築することができるので、**俺のベクターストア**が自然とできることになります。

それにしてもわくわくするな。これ

とはいえ、ひと様のプログラムのコピペなんで、いまいち理解が薄い気がする。
（ほんとは言語モデル開発が止まっているので再開したい。）