OpenAI Agent SDKでポケモンパーティ記事を分析する


# はじめに
以下の記事を昨日投稿しました。

https://qiita.com/tetsujin_081/items/1836b210f05389c76d32

AIエージェントを用いて、サイト運営を自動化した内容を概要的に書かせていただきました。
今回は、AIエージェント部分に絞って、どう実装したかを書きます。

# AIエージェントの構成
以下の流れになります
 - インプットは、パーティ記事のURLのみ
 - テキスト取得と整理をするエージェント
 - ポケモンのテキスト情報をJSON化するエージェント
 - id情報を照合するエージェント

![AIフロー図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724198/19ff346d-e738-4c84-aafd-a779fe2fa917.png)



## テキスト取得と整理をするエージェント
エージェント本体はこちら。
 - アウトプット形式を```pydantic```の```BaseModel```指定しています
 - ツールとして、関数を渡すことで、AIが使ってくれます
 - モデルは```gpt-4.1-mini```としてます（試した限りで最低限動く、かつ一番安いもの）
```py
# 記事から情報を整理するエージェント
article_abstract_agent = Agent(
    name="gutserver",
    instructions="""
    #命令
    あなたはポケモンのパーティをまとめる担当です。

    入力文のURLからテキストを取得し、
    それをもとに、パーティの概要と、6体のポケモンに関して情報を整理してください。

    #条件：
    日本語説明は、パーティ全体としての概要をまとめてください。

    first_pokemon、second_pokemon、third_pokemon、forth_pokemon、fifth_pokemon、sixth_pokemon、にはそれぞれの情報に以下を含めてください
     - ポケモン名
     - アイテム（持ち物）
     - せいかく
     - とくせい
     - せいべつ
     - HP努力値（略記：H）
     - こうげき努力値（略記：A）個体値に指定があれば記載してください
     - ぼうぎょ努力値（略記：B）
     - とくこう努力値（略記：C）
     - とくぼう努力値（略記：D）
     - すばやさ努力値（略記：S）個体値に指定があれば記載してください
     - わざ1
     - わざ2
     - わざ3
     - わざ4
     - テラスタイプ
     - 日本語説明（100文字以内で簡潔にまとめる）
     - 英語説明（200文字以内で簡潔にまとめる）
    """,
    tools=[
        GetArticleTextTool, #ツールを指定
    ],
    model="gpt-4.1-mini",
    output_type=ArticleOutput #アウトプット形式を指定
)
```

アウトプット形式です。構造化するだけでなく、ある程度制約をかけることができます。
以下の例では、最大文字数をかけています。（詳しくはhttps://docs.pydantic.dev/latest/)

これを指定しておくだけで、AIがこの形式で適切に入れてくれます。
ただ、「```min_length=0```を入れると空文字が出力される」など、不安定な部分も感じます。（そのため、```min_length=0```は指定していない） 
```py
from pydantic import BaseModel, Field

class ArticleOutput(BaseModel):
    japanese_title: str = Field(max_length=100)
    english_title: str = Field(max_length=200)
    japanese_explanation: str = Field(max_length=100)
    english_explanation: str = Field(max_length=200)
    season: int
    rank: int
    first_pokemon: str
    second_pokemon: str
    third_pokemon: str
    forth_pokemon: str
    fifth_pokemon: str
    sixth_pokemon: str
```

ツール部分です。
以下はWebスクレイピングのコードですが、関数として渡しておくと、この関数を使ってAIがスクレイピングしてくれます。

:::note info
AI自体にもWebを検索する機能はありますが、一部サイト（はてなブログなど）へのアクセスがブロックされ、参照できないことがあります。そのため独自にスクレイピング関数をツールとして与えて検索できるようにしてます。
:::

```py
import requests
from bs4 import BeautifulSoup

@function_tool  
async def GetArticleTextTool(url: str) -> str:
    """ポケモンのパーティ編成を紹介した記事のテキストデータを取得します。

    Args:
        url: ポケモンのパーティ編成を紹介した記事のURL
    """
    res=requests.get(url)
    res.encoding='utf-8'
    soup=BeautifulSoup(res.text,'html.parser')
    # 記事のテキストデータを返す。
    return soup.get_text()
```



[S30 5位の記事](https://wdp5ubyhqeatx9u.hatenablog.com/entry/2025/06/01/230551)を分析すると
以下のような出力になります。
```py
japanese_title='S30 最終5位 対面ミライ白バド構築' 
english_title='S30 Final 5th Rank Opponent Mirai White Bado Team' 
japanese_explanation='この構築は、ミライドン・白馬バドレックス・ランドロスを軸にし、高耐久と技範囲をトリックルーム下で活かす対面構築です。ウーラオスの襷やオオニューラのエレキシードで幅広い相手に対応し、カイリューは特定の厳' 
english_explanation='This team focuses on utilizing Miraidon, White Horse B2 (White Bado) and Landorus as core Pokémon, leveraging high durability and wide moveset under Trick Room for positional advantage. Urshifu with a' 
season=30 
rank=5 
first_pokemon='ミライドン\nアイテム：突撃チョッキ\n性格：ひかえめ\n特性：ハドロンエンジン\n性別：不明\n努力値：HP220／C116／D148／S20／B4\n個体値：記載なし\nわざ1：イナズマドライブ\nわざ2：流星群\nわざ3：パラボラチャージ\nわざ4：ボルトチェンジ\nテラスタイプ：電気\n説明：チョッキで耐久を高め、トリックルーム運用のミライドン。パラボラチャージが見えない回復として強力。' 
second_pokemon='白馬バドレックス\nアイテム：食べ残し\n性格：腕白\n特性：人馬一体\n性別：不明\n努力値：HP252／A20／B100／D132／S4\n個体値：記載なし\nわざ1：ブリザードランス\nわざ2：やどりぎのたね\nわざ3：アンコール\nわざ4：トリックルーム\nテラスタイプ：水\n説明：構築の軸。トリックルーム下で白バドレックスがスイーパーとして活躍。' 
third_pokemon='ランドロス\nアイテム：ゴツゴツメット\n性格：のんき\n特性：いかく\n性別：不明\n努力値：HP252／A4／B252\n個体値：記載なし\nわざ1：じしん\nわざ2：とんぼがえり\nわざ3：ステルスロック\nわざ4：挑発\nテラスタイプ：炎\n説明：物理耐久が高く、ランドロスは物理受けクッションとして優秀。挑発やステロで対面操作も可能。' 
forth_pokemon='ウーラオス\nアイテム：気合の襷\n性格：いじっぱり\n特性：不可視のこぶし\n性別：不明\n努力値：HP76／A252／B116／S60\n個体値：記載なし\nわざ1：水流連打\nわざ2：インファイト\nわざ3：アクアジェット\nわざ4：剣の舞\nテラスタイプ：ステラ\n説明：襷持ちにより安定した初手アタッカー。剣舞アクアジェットで崩し性能が高い。' 
fifth_pokemon='オオニューラ\nアイテム：エレキシード\n性格：いじっぱり\n特性：かるわざ\n性別：不明\n努力値：HP60／A236／D100／S212\n個体値：記載なし\nわざ1：インファイト\nわざ2：アクロバット\nわざ3：剣の舞\nわざ4：挑発\nテラスタイプ：飛行\n説明：毒びし対策・グライオン対面最強。選出画面での圧力が強力。' 
sixth_pokemon='カイリュー\nアイテム：おんみつまんと\n性格：いじっぱり\n特性：マルチスケイル\n性別：不明\n努力値：HP244／A236／B4／D4／S20\n個体値：記載なし\nわざ1：神速\nわざ2：地震\nわざ3：竜の舞\nわざ4：羽休め\nテラスタイプ：ノーマル\n説明：ホウオウ系統やテツノワダチに強く、状況に応じて活躍。'
```

```first_pokemon```だけ表示したい場合は以下になりますね。

```py
abstract_result = await Runner.run(article_abstract_agent, article_url)
print(abstract_result.final_output.first_pokemon)

# 出力
# ミライドン\nアイテム：突撃チョッキ\n性格：ひかえめ\n特性：ハドロンエンジン\n性別：不明\n努力値：HP220／C116／D148／S20／B4\n個体値：記載なし\nわざ1：イナズマドライブ\nわざ2：流星群\nわざ3：パラボラチャージ\nわざ4：ボルトチェンジ\nテラスタイプ：電気\n説明：チョッキで耐久を高め、トリックルーム運用のミライドン。パラボラチャージが見えない回復として強力。

response_json = party_request(request_url, abstract_result.final_output....)
# party_request関数でWebアプリに登録
```
ここまでで、パーティ情報の情報と、6体のポケモンのテキストに分割できています。
パーティ情報はこの時点で、Webアプリケーションに送信します。


次は、ポケモンを整理するエージェントです。

## ポケモンのテキスト情報をJSON化するエージェント
ポケモン1体の情報をインプットにAPIで送信できるようにJSON化します。
 - アウトプット形式を```pydantic```の```BaseModel```指定しています（設定項目は省略）
 - ツールとしてAIエージェントを指定しています（先ほどは関数を指定していました）
 - モデルは```gpt-4.1-mini```としてます（試した限りで最低限動く、かつ一番安いもの）

 
```py
pokemon_adjustment_agent = Agent(
    name="pokemon_info",
    instructions="""
    #命令
    ポケモンの情報をまとめてください。
    #条件
    - 以下はidで表示してください。不明な場合は1にすること。
    　- ポケモン
      - アイテム（持ち物）
      - せいかく
      - とくせい
      - わざ
      - テラスタイプ
    - せいべつは、不明なら0、オスなら2、メスなら3にしてください
    - 個体値は指定がなければ31にしてください。
    """,
    tools=[
        pokemon_id_agent.as_tool(
            tool_name="pokemon_id_searcher",
            tool_description="ポケモン名からidを特定してください。",
        ),
        item_id_agent.as_tool(
            tool_name="item_id_searcher",
            tool_description="アイテム（持ち物）からidを特定してください。",
        ),
        nature_id_agent.as_tool(
            tool_name="nature_id_searcher",
            tool_description="性格からidを特定してください。",
        ),
        move_id_agent.as_tool(
            tool_name="move_id_searcher",
            tool_description="わざからidを特定してください。",
        ),
        ability_id_agent.as_tool(
            tool_name="ability_id_searcher",
            tool_description="とくせいからidを特定してください。",
        ),
        terastal_type_id_agent.as_tool(
            tool_name="type_id_searcher",
            tool_description="テラスタイプからidを特定してください。",
        ),

    ],
    model="gpt-4.1-mini",
    output_type=PokemonOutput
)
```

アウトプット形式は以下で指定しています。
```py
class PokemonOutput(BaseModel):
    pokemon_id: int
    item_id: int
    ability_id: int
    ....
    move_id_1: int
    move_id_2: int
    move_id_3: int
    move_id_4: int
```


ポケモンidを検索するエージェントです。

```py
pokemon_id_agent = Agent(
    name="pokemon_id_searcher",
    instructions="""
    #命令
    ポケモンの名前からidを特定してください。
    #条件
    idは公式の図鑑番号ではありません。
    答えのみ簡潔に答えてください。
    """,
    tools=[
        FileSearchTool(
            vector_store_ids=["vs_******ポケモンidのデータを指定*****"],
        ),
    ],
    model="gpt-4.1-mini"
)
```
アプリケーション側のデータ（ポケモン）は以下ですが、
![スクリーンショット 2025-06-09 20.19.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724198/2f02b7d0-fe37-4768-b913-9f4d7e306a15.png)

```FileSearchTool``` のベクターストアには以下のような、アプリケーションでのid、日本語、英語のみをjsonを保存
``` pokemon.json
[
    {"id":1,"japanese_name":"フシギダネ","japanese_forme_name":"","english_name":"Bulbasaur","english_forme_name":""},
    {"id":2,"japanese_name":"フシギソウ","japanese_forme_name":"","english_name":"Ivysaur","english_forme_name":""},
    {"id":3,"japanese_name":"フシギバナ","japanese_forme_name":"","english_name":"Venusaur","english_forme_name":""},
    ....
    {"id":1252,"japanese_name":"モモワロウ","japanese_forme_name":"","english_name":"Pecharunt","english_forme_name":""}
]
```
openAIのダッシュボードのStorageにファイルをアップロードするだけで、
簡単にベクターデータ（ID）が生成されます。

![スクリーンショット 2025-06-09 22.46.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724198/d77d88a2-b3a1-49e3-93b8-fd0f6db34de0.png)

わざidエージェントも同様です。
```py
move_id_agent = Agent(
    name="move_id_searcher",
    instructions="""
    #命令
    わざからidを特定してください。
    #条件
    idは公式の番号ではありません。
    答えのみ簡潔に答えてください。
    """,
    tools=[
        FileSearchTool(
            vector_store_ids=["vs_******わざidのデータを指定*****"],
        ),
    ],
    model="gpt-4.1-mini"
)
```


アプリケーション側のデータ（わざ）は以下ですが、
![スクリーンショット 2025-06-09 22.50.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724198/479e5328-2492-4186-ae00-e6dbc25109e2.png)


```FileSearchTool``` のベクターストアには以下のような、アプリケーションでのid、日本語、英語のみをjsonを保存
``` move.json
[
    {"id":1,"japanese_name":"あくまのキッス","english_name":"Lovely Kiss"},
    {"id":2,"japanese_name":"あなをほる","english_name":"Dig"},
    {"id":3,"japanese_name":"あばれる","english_name":"Thrash"},
    ....
    {"id":920,"japanese_name":"レイジングブル","english_name":"Raging Bull"}
]
```

アイテムidエージェントも同様です。
```py
item_id_agent = Agent(
    name="item_id_searcher",
    instructions="""
    #命令
    アイテム（持ち物）からidを特定してください。
    #条件
    idは公式の番号ではありません。
    答えのみ簡潔に答えてください。
    """,
    tools=[
        FileSearchTool(
            vector_store_ids=["vs_******アイテムidのデータを指定*****"],
        ),
    ],
    model="gpt-4.1-mini"
)
```
アプリケーション側のデータ（もちもの）は以下ですが、
![スクリーンショット 2025-06-09 20.22.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724198/a5764689-982b-4b5e-a5dc-af732ec43fe4.png)

```FileSearchTool``` のベクターストアには以下のような、アプリケーションでのid、日本語、英語のみをjsonを保存

``` item.json
[
    {"id":1,"japanese_name":"とくせいガード","english_name":"Ability Shield"},
    {"id":2,"japanese_name":"きゅうこん","english_name":"Absorb Bulb"},
    {"id":3,"japanese_name":"だいこんごうだま","english_name":"Adamant Crystal"},
    {"id":4,"japanese_name":"こんごうだま","english_name":"Adamant Orb"},
    {"id":5,"japanese_name":"バンジのみ","english_name":"Aguav Berry"},
    {"id":6,"japanese_name":"ふうせん","english_name":"Air Balloon"},
    ....
]
```


## 結果
以下の画面を作ることができました。
AIで登録した部分と、あらかじめWebアプリに登録していた情報を組み合わせて表示しています。

![スクリーンショット 2025-06-09 22.11.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724198/17f440a8-1f18-4e6e-bdcf-45c975dc880f.png)

idで保存しているので以下の動きをしているわけです。
 - AIエージェントはidで保存する
 - Webアプリはid情報から紐づく情報も表示