Perplexity AIとInstructorを利用した構造化出力サンプルプログラム

# 既存投稿一覧ページへのリンク
[一覧ページ](https://qiita.com/o_chan_t/items/fc759695acfaef8df2cc)

## その他・記事まとめ
[Unity関連 / Qiita](https://qiita.com/o_chan_t/items/0b5ae1e7c45f106082b8)

# Perplexity APIの基本形

```python:PerplexityAPI.py
import requests
import os
import configparser

# 1. 取得したAPIキーをここに入力
config = configparser.ConfigParser()
config.read('../APIConfig/config.ini', encoding='utf-8')
api_key = config['perplexity']['api_key']

model_select = {
    "debug": "llama-3.1-sonar-small-128k-online",
    "normal": "llama-3.1-sonar-large-128k-online",
    "sonor_pro": "sonar-pro",
    "sonor_reasoning_pro": "sonar-reasoning-pro",
    "sonor_reasoning": "sonar-reasoning",
}

if not api_key:
    print("Error: PERPLEXITY_API_KEY環境変数が設定されていません。")
else:
    url = "https://api.perplexity.ai/chat/completions"
    payload = {
        "model": model_select["normal"],
        "messages": [
            {"role": "system", "content": "日本語で完結に回答して下さい。"},
            {"role": "user", "content": "本日の注目ニュースを教えて下さい。"}
        ],
        "max_tokens": 4096,
        "temperature": 0.7
    }
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()
        response_data = response.json()
        print(response_data)
    except requests.exceptions.RequestException as e:
        print(f"エラーが発生しました: {e}")
        if e.response is not None:
            print("詳細:", e.response.text)
```

```
{'id': 'd39403ea-d450-4757-8ae4-04de56a47c68', 'model': 'llama-3.1-sonar-large-128k-online', 'created': 1748060301, 'usage': {'prompt_tokens': 23, 'completion_tokens': 384, 'total_tokens': 407, 'search_context_size': 'low'}, 'citations': ['https://www.youtube.com/watch?v=460uq2ajCL4', 'https://toyokeizai.net/articles/-/879916', 'https://www.youtube.com/watch?v=xXD7qqIK4cw', 'https://www.kyoiku-press.com/post-date/2025/05/24/', 'https://www.youtube.com/user/ANNnewsCH'], 'object': 'chat.completion', 'choices': [{'index': 0, 'finish_reason': 'stop', 'message': {'role': 'assistant', 'content': '2025年5月24日の注目ニュースは以下の通りです。\n\n## 経済ニュース\n- 日本とEUでは長期金利が上昇しており、注視が必要です。この影響で、ニューヨーク証券取引所のダウ平均株価が同日約800ドル以上下落しています[2|.\n- 日米の超長期金利と金利差の推移、 以及日本国債イールドカーブの変化が注目されています。特に、4月2日から5月21日までの日本国債利回りの変化や、主要国30年金利 の変化幅も分析されています[1].\n\n## 天気ニュース\n- 日本の西日本地域、特に九州や四国、中国地方では激しい雷雨のおそれが あり、土砂災害に警戒が必要です。暖かく湿った空気が流れ込むため、雨雲が発達し、一時的に雷を伴った激しい雨が予想されています[3].\n\n## 社会ニュース\n- 東京の警視庁は、闇バイトで逮捕された10代の約6割が「先輩に誘われた」ことを明らかにし、女子高校生に対して注意喚起を行っています[5].\n\n## 教育ニュース\n- アントレプレナーシップ教育が注目されており、現代社会での技 術革新や社会の急速な変化に対応するために、自ら考え行動する力が求められています。教育現場での多様な価値観の認識と、新たな価値を創造する姿勢の重要性が強調されています[4].'}, 'delta': {'role': 'assistant', 'content': ''}}]}
```

# 久しぶりにLangChainを⋯

ChatGPTの情報は多いけれど、Perplexity AIの情報はあまり見つからなかったのでメモ書きです。
Perplexityさんは中々思い通りに構造化データにしてくれないので、下記のベースプログラムを元に色々と検証していくことにします。

## 基本形

```python:PerplexityAI_structured_output_sample.py
"""
Perplexity AI APIを使用した構造化データ抽出プログラム
特徴：
1. 設定ファイルからのAPIキー読み込み
2. モデル選択機能の実装
3. Pydanticを活用した型安全な出力構造
4. 生レスポンス情報の取得機能
5. トークン使用量の可視化
"""

from openai import OpenAI
import instructor
from pydantic import BaseModel

import configparser

# 1. 取得したAPIキーをここに入力
config = configparser.ConfigParser()
config.read('../APIConfig/config.ini', encoding='utf-8')
api_key = config['perplexity']['api_key']

modeli_select = {
    "debug": "llama-3.1-sonar-small-128k-online",
    "normal": "llama-3.1-sonar-large-128k-online",
    "high": "llama-3.1-sonar-huge-128k-online",
    "debug_chat": "llama-3.1-sonar-small-128k-chat",
    "normal_chat": "llama-3.1-sonar-large-128k-chat",
}

client = instructor.from_perplexity(
    OpenAI(
        api_key=api_key,
        base_url="https://api.perplexity.ai"
        )
    )

# 出力構造を定義
class User(BaseModel):
    name: str
    age: int

# 構造化出力の実行
completion = client.chat.completions.create(
    model=modeli_select["debug"],
    response_model=User,
    messages=[{
        "role": "user", 
        "content": "山田太郎さんは25歳です。"
    }]
)

# 構造化されたデータを取得
user = completion  # 通常の構造化データ
raw_response = completion._raw_response  # 生レスポンス

# 構造化データの表示
print(f"名前: {user.name}, 年齢: {user.age}歳")

# 生レスポンスの表示
print("\n=== 生レスポンス ===")
print(f"モデル: {raw_response.model}")
print(f"生成ID: {raw_response.id}")
print(f"使用トークン数: {raw_response.usage}")
print(f"生レスポンス: {raw_response}")
```

```
名前: 山田太郎, 年齢: 56歳

=== 生レスポンス ===
モデル: llama-3.1-sonar-small-128k-online
生成ID: ee370ad7-030a-407a-bc59-c172b296b64e
使用トークン数: CompletionUsage(completion_tokens=15, prompt_tokens=8, total_tokens=23, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0))
生レスポンス: ChatCompletion(id='ee370ad7-030a-407a-bc59-c172b296b64e', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='{"name": "山田太郎", "age": 56}', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None), delta={'role': 'assistant', 'content': ''})], created=1747887973, model='llama-3.1-sonar-small-128k-online', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=15, prompt_tokens=8, total_tokens=23, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)), citations=['https://dexall.co.jp/articles/?p=1562', 'https://www.youtube.com/watch?v=3XXn2f6ucgM', 'https://ja.wikipedia.org/wiki/%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E_(%E6%AD%8C%E6%89%8B)', 'https://note.com/nijimorikokoro/n/nb8bc9e45b6c2', 'https://www.youtube.com/watch?v=DBqbqJP8qwE'])
```

## 基本形(role: systemを追加)

```python:PerplexityAI_structured_output_sample2.py
# 構造化出力の実行（systemメッセージ追加）
completion = client.chat.completions.create(
    model=modeli_select["debug"],
    response_model=User,
    messages=[
        {
            "role": "system",
            "content": "ユーザーが入力した文章から、名前と年齢を抽出してください。"
        },
        {
            "role": "user", 
            "content": "山田太郎さんは25歳です。"
        }
    ]
)
```

```
モデル: llama-3.1-sonar-small-128k-online
生成ID: ff64d64f-3c91-4835-85dd-c7e567334284
使用トークン数: CompletionUsage(completion_tokens=16, prompt_tokens=30, total_tokens=46, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0))
生レスポンス: ChatCompletion(id='ff64d64f-3c91-4835-85dd-c7e567334284', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='{ "name": "山田太郎", "age": 25 }', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None), delta={'role': 'assistant', 'content': ''})], created=1747889222, model='llama-3.1-sonar-small-128k-online', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=16, prompt_tokens=30, total_tokens=46, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)), citations=['https://dexall.co.jp/articles/?p=1562', 'https://www.youtube.com/watch?v=3XXn2f6ucgM', 'https://ja.wikipedia.org/wiki/%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E_(%E6%AD%8C%E6%89%8B)', 'https://note.com/nijimorikokoro/n/nb8bc9e45b6c2', 'https://bunshun.jp/articles/-/66637?page=2'])
```

## 基本形3(出力構造を少しだけ複雑に)

```python:PerplexityAI_structured_output_sample3.py
from pydantic import Field
from typing import Literal
# 出力構造を定義
class User(BaseModel):
    first_name: str = Field(..., min_length=1, max_length=20, pattern=r"^[^\d]+$")
    last_name: str = Field(..., min_length=1, max_length=20, pattern=r"^[^\d]+$")
    age: int = Field(..., gt=0, lt=150, description="年齢は0より大きく150未満")
    age_group: Literal["child", "teen", "adult", "senior"]

# 構造化出力の実行（systemメッセージ追加）
completion = client.chat.completions.create(
    model=modeli_select["debug"],
    response_model=User,
    messages=[
        {
            "role": "system",
            "content": f'''
    以下の要件に従ってユーザー入力から情報を抽出してください：

    ### 名前抽出ルール
    1. 姓と名を分割（例：「山田太郎」→ last_name="山田", first_name="太郎"）
    2. 数字を含まず1-20文字
    3. 敬称（さん/様など）は除去

    ### 年齢処理
    1. 0 < age < 150 の整数
    2. 年齢グループ分類：
    - child: 0-12歳
    - teen: 13-19歳
    - adult: 20-64歳
    - senior: 65-149歳

    ### バリデーション
    - 入力に複数情報が含まれる場合は最初の該当データを採用
    - 矛盾する情報がある場合は例外を発生
    '''.strip()
        },
        {
            "role": "user", 
            "content": "山田太郎さんは25歳です。"
        }
    ]
)

# 構造化されたデータを取得
user = completion  # 通常の構造化データ
raw_response = completion._raw_response  # 生レスポンス

# 構造化データの表示
print(type(user))
print(f"{user}")

# 生レスポンスの表示
print("\n=== 生レスポンス ===")
print(f"モデル: {raw_response.model}")
print(f"生成ID: {raw_response.id}")
print(f"使用トークン数: {raw_response.usage}")
print(f"生レスポンス: {raw_response}")

```

```
<class '__main__.User'>
first_name='太郎' last_name='山田' age=25 age_group='teen'

=== 生レスポンス ===
モデル: llama-3.1-sonar-small-128k-online
生成ID: 56b3ac43-abb4-45aa-a086-c8c534e7b2cf
使用トークン数: CompletionUsage(completion_tokens=31, prompt_tokens=220, total_tokens=251, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0))
生レスポンス: ChatCompletion(id='56b3ac43-abb4-45aa-a086-c8c534e7b2cf', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='{ "first_name": "太郎", "last_name": "山田", "age": 25, "age_group": "teen" }', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None), delta={'role': 'assistant', 'content': ''})], created=1747889823, model='llama-3.1-sonar-small-128k-online', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=31, prompt_tokens=220, total_tokens=251, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)), citations=['https://dexall.co.jp/articles/?p=1562', 'https://www.youtube.com/watch?v=3XXn2f6ucgM', 'https://ja.wikipedia.org/wiki/%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E_(%E6%AD%8C%E6%89%8B)', 'https://note.com/nijimorikokoro/n/nb8bc9e45b6c2', 'https://bunshun.jp/articles/-/66637?page=2'])
```

```text:"content": "吾輩は猫である(llama-3.1-sonar-small-128k-online)"
<class '__main__.User'>
first_name='吾輩' last_name='猫' age=1 age_group='child'

=== 生レスポンス ===
モデル: llama-3.1-sonar-small-128k-online
生成ID: 8dd3fbb9-7d4a-4fe4-866c-f20e8aa46456
使用トークン数: CompletionUsage(completion_tokens=69, prompt_tokens=912, total_tokens=981, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0))
生レスポンス: ChatCompletion(id='8dd3fbb9-7d4a-4fe4-866c-f20e8aa46456', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='{"first_name":"吾輩","last_name":"猫","age":1,"age_group":"child"}', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None), delta={'role': 'assistant', 'content': ''})], created=1747889956, model='llama-3.1-sonar-small-128k-online', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=69, prompt_tokens=912, total_tokens=981, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)), citations=['https://json-schema.org/learn/miscellaneous-examples', 'https://www.qatouch.com/blog/validating-json-schema/', 'https://json-schema.org/draft/2020-12/json-schema-validation', 'https://zuplo.com/blog/2024/07/19/verify-json-schema', 'https://jweiler.com/json/schema/2020/07/03/json-schema-for-net-derived-class.html'])
```

```text:"content": "吾輩は猫である(llama-3.1-sonar-large-128k-online)"
<class '__main__.User'>
first_name='なし' last_name='なし' age=149 age_group='senior'

=== 生レスポンス ===
モデル: llama-3.1-sonar-large-128k-online
生成ID: 1a726419-607d-45ac-9198-0874e65b0cf9
使用トークン数: CompletionUsage(completion_tokens=58, prompt_tokens=528, total_tokens=586, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0))
生レスポンス: ChatCompletion(id='1a726419-607d-45ac-9198-0874e65b0cf9', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='{"first_name": "なし", "last_name": "なし", "age": 149, "age_group": "senior"}', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None), delta={'role': 'assistant', 'content': ''})], created=1747890044, model='llama-3.1-sonar-large-128k-online', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=58, prompt_tokens=528, total_tokens=586, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)), citations=['https://www.syncfusion.com/blogs/post/using-json-schema-for-json-validation/amp', 'https://zuplo.com/blog/2024/07/19/verify-json-schema', 'https://json-schema.org/draft/2020-12/json-schema-validation', 'https://linkml.io/linkml/intro/tutorial03.html', 'https://github.com/json-schema-org/json-schema-spec/wiki/Scope-of-JSON-Schema-Validation'])
```


## 基本形4(出力構造をもう少しだけ複雑に)

```python:PerplexityAI_structured_output_sample4.py
from pydantic import Field
from pydantic import field_validator
from typing import Optional, Literal

# 出力構造を定義
class Address(BaseModel):
    prefecture: str = Field(..., description="都道府県名")
    city: str = Field(..., description="市区町村名")
    street: Optional[str] = Field(None, description="番地・建物名")

class User(BaseModel):
    first_name: str = Field(..., min_length=1, max_length=20, pattern=r"^[^\d]+$")
    last_name: str = Field(..., min_length=1, max_length=20, pattern=r"^[^\d]+$")
    age: int = Field(..., gt=0, lt=150, description="年齢は0より大きく150未満")
    age_group: Literal["child", "teen", "adult", "senior"]  # Literal型で、指定された値以外は許可しません
    address: Optional[Address] = None
    @field_validator('age_group')   # age_groupフィールドの値を検証・変換するためのメソッド
    @classmethod
    def validate_age_group(cls, v, values):
        if 'age' in values.data:
            age = values.data['age']
            if age < 13:
                return "child"
            elif age < 20:
                return "teen"
            elif age < 65:
                return "adult"
            else:
                return "senior"
        return v

# 構造化出力の実行（systemメッセージ追加）
completion = client.chat.completions.create(
    model=modeli_select["debug"],
    response_model=User,
    messages = [
        {
            "role": "system",
            "content": '''
    以下の要件に従ってユーザー入力から情報を抽出してください：

    ## 名前抽出ルール
    1. 姓と名を分割（例：「山田太郎」→ last_name="山田", first_name="太郎"）
    2. 数字を含まず1〜20文字であること
    3. 敬称（さん、様など）は除去すること

    ## 年齢処理
    1. 0 < age < 150 の整数のみ抽出すること
    2. 年齢に応じて age_group を自動分類すること
    - child: 0〜12歳
    - teen: 13〜19歳
    - adult: 20〜64歳
    - senior: 65〜149歳

    ## 住所抽出ルール
    1. 住所要素を以下の形式で分割
    - prefecture: 都道府県名（例：「東京都」）
    - city: 市区町村名（例：「新宿区」）
    - street: 番地・建物名（例：「西新宿2-8-1」）
    2. 部分的な情報しかない場合は可能な範囲で抽出（例：「大阪市北区」→ prefecture="大阪府", city="大阪市北区"）

    ## バリデーション
    - 入力に複数の情報が含まれる場合は、最初に該当したデータのみを採用すること
    - 矛盾する情報がある場合は例外を発生させること
    '''.strip()
        },
        {
            "role": "user",
            "content": "山田太郎さんは25歳です。東京都新宿区西新宿2-8-1に住んでいます。"
        }
    ]
)

# 構造化されたデータを取得
user = completion  # 通常の構造化データ
raw_response = completion._raw_response  # 生レスポンス

# 構造化データの表示
print(type(user))
print(f"{user}")

# 生レスポンスの表示
print("\n=== 生レスポンス ===")
print(f"モデル: {raw_response.model}")
print(f"生成ID: {raw_response.id}")
print(f"使用トークン数: {raw_response.usage}")
print(f"生レスポンス: {raw_response}")
```

```
<class '__main__.User'>
first_name='太郎' last_name='山田' age=25 age_group='adult' address=Address(prefecture='東京都', city='新宿区', street='西新宿2丁目8-1')

=== 生レスポンス ===
モデル: llama-3.1-sonar-small-128k-online
生成ID: 59bec0a5-f802-4251-84b9-d7608f82e377
使用トークン数: CompletionUsage(completion_tokens=65, prompt_tokens=387, total_tokens=452, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0))
生レスポンス: ChatCompletion(id='59bec0a5-f802-4251-84b9-d7608f82e377', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='{ "first_name": "太郎", "last_name": "山田", "age": 25, "age_group": "adult", "address": { "prefecture": "東京都", "city": "新宿区", "street": "西新宿2丁目8-1" } }', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None), delta={'role': 'assistant', 'content': ''})], created=1747903828, model='llama-3.1-sonar-small-128k-online', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=65, prompt_tokens=387, total_tokens=452, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)), citations=['https://www.navitime.co.jp/address/131040700020000800001/%E6%9D%B1%E4%BA%AC%E9%83%BD%E6%96%B0%E5%AE%BF%E5%8C%BA%E8%A5%BF%E6%96%B0%E5%AE%BF2%E4%B8%81%E7%9B%AE8-1/', 'https://www.navitime.co.jp/poi?spot=02300-1009192', 'https://www.zaimu.metro.tokyo.lg.jp/tochousha/goannai', 'https://www.metro.tokyo.lg.jp/about/kotsuannai', 'https://www.its-mo.com/addr/13/104/070/002/00008/00001/'])
```

# あとがき

instructorについての理解が足りなかった。。。
perplexity aiでも上手くいくけれど、chat GPTの方が構造化するのが得意なLLMということですね。

LLMはいいとこ取りで柔軟に使っていくことにします。