LangChain v0.3　その５ ~LangGraphの前に`Pydantic`~

# ここまで
LangChainを使って、色々と機能マシマシにしたい人、たくさんいますよね。
僕ものその一人。目的はここには書かないけど、LangChainって目的達成と合わせてプログラミングの理解にもいいと思います。
あ、今日のところは、ちゃんとclassについては理解しておいた方がいいと思う。

classの理解はこの講座がわかりやすかった。マジおすすめ。

https://www.udemy.com/course/python-blockchain/?srsltid=AfmBOoonwd3NLk2TJ0NMizEgqHc95TQIWYtg_ybnjx8tElqYNw7Si_0Z&couponCode=ST20MT111124B


# なぜここでPydantic？
ちょっと遠回りしましょう。というのも、僕も行き詰まったから。
急がば回れ

このPydanticが理解できていれば次にやろうとしているLangGraphがわかりやすい。


https://langchain-ai.github.io/langgraph/how-tos/state-model/?h=pydan

ほら、ここ↑にもPydanticが必須って書いてあるでしょ！
だからやるしかないのよ！

というのは雑なので、少し説明

LLMの入出力ステートレスじゃないですか。
そう、使って終わったらその情報は出力先に行ってしまって、過去の内容は保持していないっていう。
でもさ、チャットとか、これから使いたいLangChainって状態を保持しておかないと会話がちゃらんぽらん。
つまり、会話履歴だったり、何かの判断結果だったり、前提条件だったり、保持しておかなければならない。そのために**決まった書式**で残しておくと後で使いやすい。この保持するべき情報をLangChainでは`state`って呼んでます。
このstateには何でもかんでも入れちゃったら意味わかんないので、
- どの変数にはどんな型で
- デフォルト値は何で
- 上書きするのか、情報を追加するのか

そんなことをつらつらと定義します。そのために、`Pydantic`を使ったら便利だよってこと。


# 参考ページ
参考にしたのはこちらのページ

https://zenn.dev/nowa0402/articles/6b423af53274c2

基本的にはこのページをベースに理解を進めます。ありがたや


そしてこちらの書籍を買ったんですが、おすすめです。

https://gihyo.jp/book/2024/978-4-297-14530-9

245ページ目から250ページが次にやるLangGraph
これが目標なんですが、わかりやすかった！

# バージョン関連
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を使いました


# Pydanticを使いながら理解
知らなかったんだけど、Pydanticって標準ライブラリなんですね。

```python:ライブラリのインポート
from datetime import datetime
from pydantic import BaseModel, Field
from zoneinfo import ZoneInfo
```

型定義の方法は2種類あって、
1. 辞書型
1. なんやらJsonっぽいやつ

です。まずはJsonっぽい`BaseModel`を継承してつかいます。代わりに`TypedDict`でもできますが、入力した後の処理が変わります。`BaseModel`の場合はJsonの様に取り出しができますが、`TypedDict`だと辞書のように取り出します。それは後ほど。

# BaseModelを使った場合（Jsonっぽいやつ）

```python:型を宣言
class UserInfo(BaseModel):
    name: str = Field(..., description='name')
    age: int = Field(default=0, description='age')
    dt: datetime = Field(default=datetime.now(), description='input date')
```

何をやってるかっていうと`UserInfo`ってクラスで保持する情報の型定義します。
で、肝は`Field`と`BaseModel`

- `name`は文字型（str）
- `age`は整数型（int）
- `dt`はデータタイム型（datetime)

で指定しています。そして、`Field`では初期値を設定できます。ここのnameのところは`...`になってるのは「必須だけど、初期値は指定しないよ」ってことです。

わかると思うけど、初期値は`default=***`で設定します。
Fieldの設定をしない場合は空になるのですが、入力したらそこに値が入っていきますが、他の値は存在しません。
Fieldで設定した場合は初期値として何かしら入るのでより決まった形式になります。


ちなみに、次回、多分「その６」でやるLangGraphを使った処理ではField設定しなかった場合とした場合では結果が変わります。
試してみると面白いのでぜひお試しください。（今日はやめとく）



```python:情報を渡したインスタンスを作る
dt = datetime.now()

user_info = UserInfo(name="hogehoge", age=20, dt=dt)

print(user_info)
```

```output:output
name='hogehoge' age=20 dt=datetime.datetime(2024, 11, 12, 12, 42, 26, 817299)
```

一つ一つ取り出すにはこんな感じ
```python
print(user_info.name)
```

```output:output
'hogehoge'
```


<<おまけ>>`model_dump`関数を使えば辞書化
```python
print(user_info.model_dump())
```

```output:output
{'name': 'hogehoge', 'age': 20, 'dt': datetime.datetime(2024, 11, 12, 12, 42, 26, 817299)}
```
今は`BaseModel`を使っているので、Jsonチックに取り扱いできますが、`model_dump()`関数を使うと辞書型に変換されます。


### ちょっと意地悪する

```python:年齢にstrを入れてみる
user_info2 = UserInfo(name="1243", age='20', dt='2024-11-10')
user_info2
```

```output:output
UserInfo(name='1243', age=20, dt=datetime.datetime(2024, 11, 10, 0, 0))
```
変換された。すげぇ





```python:nameにintを入れてみた
UserInfo(name=1243, age=20, dt=datetime.datetime(2024, 11, 10, 0, 0))
```

```ouput:output
ValidationError: 1 validation error for UserInfo
name
  Input should be a valid string [type=string_type, input_value=1243, input_type=int]
```
エラー吐いた。よくわからん。（気にしない）
ま、サービス作るなら例外処理をしろって事かな。


```python:nameだけ指定
input = UserInfo(name='ikedachin')
input
```

```ouput:output
UserInfo(name='ikedachin', age=0, dt=datetime.datetime(2024, 11, 11, 20, 55, 42, 616638))
```
ageとdtにはデフォルト値


# TypedDictを継承した場合（辞書型）
```python
from datetime import datetime
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
```
`typing_extentions`はPython3.11以降使えるようになったPydanticを以前のバージョンでも使えるようにしたライブラリらしい。これはありがたい。

```python:継承するクラスはTypedDict
class UserInfoDict(TypedDict):
    name: str = Field(..., description='name')
    age: int = Field(default=0, description='age')
    dt: datetime = Field(default=datetime.now(), description='input date')
```

```python:nameだけ指定してみる
user_info_dict = UserInfoDict(name='ikedachin_dict')
user_info_dict
```

```output:output
{'name': 'ikedachin_dict'}
```
あり？デフォルト値は効かないみたい。

```python:キーを見てみる
print(user_info_dict.keys())
```

```output:output
dict_keys(['name'])
```
確かに、初期設定したキーが存在しない。


# そして、langchian_coreのPydanticを使ってみる
いよいよです。はい
行ってみよう

```python:ライブラリインポート
from datetime import datetime
from langchain_core.pydantic.v1 import BaseModel
```

```python:型宣言
class State1(BaseModel):
    name : str = Field(..., description='名前')
    age: int = Field(default=0, description='年齢')
    info: str = Field(default='', description='情報')
```

```python:使ってみる
user_info = State1(name='ageage')
print(user_info)
```
```output:output
State1(name='ageage', age=8, info=FieldInfo(annotation=NoneType, required=False, default='', description='情報'))
```
なんや色々と増えとる。初期設定の情報も取得できそうね。

```python
print(user_info.name)
print(user_info.name.description)
```

```output:output
'ageage'
'名前'
```


## LangGraphに向けて練習
LangGraphは関数をノードとして、ノードとノードをエッヂで繋ぐ。
そのノードの練習です。

```python
def test_basemodel(state: State1): # BaseModelを継承したクラスで定義したstateを使う
    name_info = state.name
    age_info = state.age
    state.info = f'{name_info}_{str(age_info)}' # name_ageの形にしてinfoに入力
    return state
```

こうすると決まった形式で取り出せるし、情報を追記できる。**これが狙い**っす。

```python:test_basemodelにuser_infoを渡してみる
user_info2 = test_basemodel(user_info)
print(user_info2)
```

```output:output
name='ageage' age=8 info='ageage_8'
```


なんとなくわかってきましたよね
`state`ってやつに情報を持たせたい
そしてその`state`の情報を受け取って、新しい情報に更新したり、追加したり。
そしてまた`state`を返り値として返す事で情報をつないでいくんですね。

その`state`には情報を適当に持たせるのではなくて、情報ごとに整理したい。
そのための型定義なんでしょう。この`state`は小さなデータベースだなって感じました。

さて、次はこの`state`を引継ぎながら動かすLangGraphをやってみましょう。


# まとめ 
Pydanticというかっこいいライブラリの使い方を勉強しました。
上手に使えばちっちゃなデータベースの様なものができる。僕はそう理解しました。

ではでは