Sonar APIの結果出力をJSON形式で取得する
はじめに
Perplexityから新モデルとしてSonarとSonar Proがリリースされました。
Perplexityは以前からAPIを提供していましたが、
精度が良くないとの声が多く、触っていませんでした。
今回Sonarがリリースされたので
試してみることにしました。
試してみる
公式ドキュメントを元に試してみます。[1]
import requests
from dotenv import load_dotenv
import os
import json
load_dotenv()
PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions"
PERPLEXITY_API_KEY = os.environ["PERPLEXITY_API_KEY"]
system_prompt = """
You operate as a “Horse Racing Information Assistant”.
Your main task is to collect user-supported information on the web about your target races.
Answer in Japanese.
"""
your_prompt = """
2025年2月23日に実施されるフェブラリーSについて
上のレースにはガイアフォースという馬が出走します。
ガイアフォースの次の情報を教えてください。
- 馬名
- レースの馬番
- 騎手名
- 調教師名
- ポジティブな意見 (リスト形式)
- ネガティブな意見 (リスト形式)
"""
payload = {
"model": "sonar-pro",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": your_prompt},
],
"max_tokens": 4096,
"temperature": 0.2,
}
headers = {
"Authorization": f"Bearer {PERPLEXITY_API_KEY}",
"Content-Type": "application/json",
}
response = requests.request("POST", PERPLEXITY_API_URL, json=payload, headers=headers)
body = json.loads(response.text)
回答の文章は次のようになりました。[2]
body["choices"][0]["message"]["content"]
ガイアフォースの情報は以下の通りです:
- 馬名: ガイアフォース
- レースの馬番: 15番[4]
- 騎手名: 長岡 禎仁[5]
- 調教師名: 杉山晴紀(栗東)[2][5]
- ポジティブな意見:
- 昨年のフェブラリーステークスで2着と好成績を残している[3]
- 東京のマイルコースが合うとされている[9]
- 追い切りでは躍動感のある動きを見せている[1]
- 調教師は馬の状態に自信を持っている[1]
- 外枠を引いたことで囲まれることがなく、有利と考えられている[3]
- ネガティブな意見:
- 初速が遅く、前に行きづらい傾向がある[9]
- 6歳馬となり、年齢的な衰えの可能性がある
- 昨年と異なる調整過程を取っているため、仕上がりに不安がある[10]
- 外枠を引いたことで、スタート後のポジション取りが難しくなる可能性がある
引用元も取得できます。
body["citations"]
# ['https://www.radionikkei.jp/keiba_article/news/c_215.html',
# 'https://umanity.jp/sp/racedata/db/horse_top.php?code=2019104476',
# 'https://umatoku.hochi.co.jp/articles/20250222-OHT1T51240.html',
# 'https://s.keibabook.co.jp/cyuou/odds/5/202501040811',
# 'https://www.keibalab.jp/db/horse/2019104476/',
# 'https://db.netkeiba.com/horse/ped/2019104476/',
# 'https://www.sponichi.co.jp/gamble/news/2025/02/23/kiji/20250222s00004048383000c.html',
# 'https://s.keibabook.co.jp/cyuou/seiseki/202501040811',
# 'https://www.radionikkei.jp/keiba_article/news/s_600.html',
# 'https://umanity.jp/racedata/race_newsdet.php?nid=11841697']
LangChainとの連携
アプリケーションに組み込むことを考えると
JSON形式で取得したいです。
PerplexityのAPIのドキュメントを読むとresponse_format
を指定することで
JSON形式で取得できると記載されていましたが、
Tier-3以上のメンバーしか利用できないとのことでした。
Tier-3以上のメンバーになるためには
$500の課金が必要です。
JSON形式の出力のためだけに課金するのは高いと感じたため、
LangChainを利用して、
Sonar APIの結果をJSON形式で取得することを試みます。
LangChainにはChatPerplexity
クラスがあるのでこれを活用します。
実際に動かしたコードは次のようになりました。
from pydantic import BaseModel, Field
from langchain_community.chat_models import ChatPerplexity
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from typing import List
class HorseInfo(BaseModel):
horse_name: str = Field(..., description="馬名")
horse_number: int = Field(..., description="レースの馬番")
jockey: str = Field(..., description="騎手名")
trainer: str = Field(..., description="調教師名")
positive_opinions: List[str] = Field(..., description="ポジティブな意見")
negative_opinions: List[str] = Field(..., description="ネガティブな意見")
prompt_template = """
レース日: {race_date}
レース名: {race_name}
上のレースに出走するガイアフォースの次の情報を教えてください。
- 馬名
- レースの馬番
- 騎手名
- 調教師名
- ポジティブな意見 (リスト形式)
- ネガティブな意見 (リスト形式)
JSON形式で返却してください。
keyとvalueの組み合わせは以下の通りです。
"horse_name": 馬名,
"horse_number": レースの馬番,
"jockey": "騎手名",
"trainer": "調教師名",
"positive_opinions": ["ポジティブな意見1", "ポジティブな意見2"],
"negative_opinions": ["ネガティブな意見1", "ネガティブな意見2"],
"""
pydantic_parser = PydanticOutputParser(pydantic_object=HorseInfo)
prompt = PromptTemplate(
template=prompt_template,
input_variables=["race_date", "race_name"],
partial_variables={
"format_instructions": pydantic_parser.get_format_instructions()
},
)
llm = ChatPerplexity(
model="sonar-pro",
temperature=0.2,
pplx_api_key=PERPLEXITY_API_KEY,
max_tokens=4096,
)
chain = prompt | llm
inputs = {
"race_date": "2025-02-23",
"race_name": "フェブラリーS",
}
response = chain.invoke(inputs)
# AIMessage(content='{\n "horse_name": "ガイアフォース",\n "horse_number": 14,\n "jockey": "長岡禎仁",\n "trainer": "杉山晴紀",\n "positive_opinions": [\n "追い切りで躍動感のある動きを見せた[1]",\n "昨年の2着馬で、リベンジに挑む[5]",\n "具合が良く、寒い時期の歩様の硬さもない[5]",\n "調教師は展開さえ噛み合えば十分勝ち負けになると考えている[3]",\n "東京のマイルコースが合う[3]"\n ],\n "negative_opinions": [\n "初速がなかなかつかない馬で、位置取りは前に行けない可能性がある[3]",\n "勝ち星からはかなり離れている[1]"\n ]\n}', additional_kwargs={'citations': ['https://www.radionikkei.jp/keiba_article/news/c_215.html', 'https://www.keibalab.jp/db/race/202502230511/syutsuba.html', 'https://www.radionikkei.jp/keiba_article/news/s_600.html', 'https://s.keibabook.co.jp/cyuou/odds/4/202501040811', 'https://hochi.news/articles/20250222-OHT1T51240.html', 'https://race.netkeiba.com/special/index.html?id=0019', 'https://db.netkeiba.com/horse/2019104476/', 'https://ja.wikipedia.org/wiki/%E3%82%AC%E3%82%A4%E3%82%A2%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B9', 'https://race.netkeiba.com/race/shutuba.html?race_id=202505010811', 'https://www.youtube.com/watch?v=JqIirPcSPyY']}, response_metadata={}, id='run-781d7321-b706-4c62-a816-c19073b5a8c8-0')
contentの内部はJSON形式の文字列になっているのでjson.loads
で変換します。
json.loads(response.content)
{'horse_name': 'ガイアフォース',
'horse_number': 15,
'jockey': '長岡禎仁',
'trainer': '杉山晴紀',
'positive_opinions': [
'昨年のフェブラリーステークスで2着と好成績を残している[1][4]',
'東京のマイルコースが合うと評価されている[4]',
'調教の動きが良く、躍動感のある動きを見せている[2]',
'輸送に慣れており、物おじしなくなっている[3]',
'外枠でも巻き返しが期待できる[3]',
'状態面に自信があり、具合が良い[1]',
'広いコースで伸び伸び走れることが強み[4]',
'展開さえ噛み合えば十分勝ち負けになると期待されている[4]'
],
'negative_opinions': [
'初速がなかなかつかない馬で、位置取りが前に行きづらい[4]',
'外枠からのスタートとなる[7]',
'前走のチャンピオンズCでは外枠に泣いた[3]'
]
}
pydantic_parser
で定義したJSON形式になっています。
引用元は次のように取得できます。
response.additional_kwargs["citations"]
# ['https://umatoku.hochi.co.jp/articles/20250222-OHT1T51240.html',
# 'https://www.radionikkei.jp/keiba_article/news/c_215.html',
# 'https://www.sponichi.co.jp/gamble/news/2025/02/23/kiji/20250222s00004048383000c.html',
# 'https://www.radionikkei.jp/keiba_article/news/s_600.html',
# 'https://news.netkeiba.com/?pid=news_view&no=289894',
# 'https://www.youtube.com/watch?v=bJAMWvB82ZM',
# 'https://www.youtube.com/watch?v=AoEKM4v3WPM',
# 'https://ja.wikipedia.org/wiki/%E3%82%AC%E3%82%A4%E3%82%A2%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B9',
# 'https://tospo-keiba.jp/breaking_news/55433',
# 'https://db.netkeiba.com/horse/2019104476/']
注意点
with_structured_outputを使用すると引用元が取得できない
Pydanticのクラスをレスポンスとして受け取るための他の方法では
with_structured_output
メソッドがあります。
この場合は次のような記述になります。
llm = ChatPerplexity(
model="sonar-pro",
temperature=0.2,
pplx_api_key=PERPLEXITY_API_KEY,
max_tokens=4096,
).with_structured_output(schema=HorseInfo)
しかしながら、この方法だとresponse
のmodel_extra dict
に格納されている引用元が取得できません。
バージョン管理
上記に関連しますが、ChatPerplexityクラスがwith_structured_output
に対応したのはlangchain-communityの0.3.18で最新版になります。
LangChainは開発スピードが速いので、バージョンに気をつけましょう。
終わりに
LangChain経由でPerplexityのAPIの返答をJSON形式で受け取る方法を記載しました。
LangChainを今回初めて本格的に触りました。
他に良い方法があるかもしれないので、
コメントでご指摘いただけると幸いです。
参考
文章中に触れたURL以外には次の記事を参考にしました。
- [LangChain] with_structured_output を使用して、Pydanticのクラスをレスポンスとして受け取る
- LangChain
with_structured_output
メソッドによる構造化データ抽出
-
OpenAI公式のライブラリ経由でも使用できます。https://docs.perplexity.ai/guides/getting-started#make-your-api-call ↩︎
-
表示している文章は加工しています。実際には次のように改行コードが入ります。
'ガイアフォースの情報は以下の通りです:\n\n- 馬名: ガイアフォース\n- レースの馬番: 15番[4]\n- 騎手名: 長岡 禎仁[5]\n- 調教師名: 杉山晴紀(栗東)[2][5]\n\nポジティブな意見:\n- 昨年のフェブラリーステークスで2着と好成績を残している[3]\n- 東京のマイルコースが合うとされている[9]\n- 追い切りでは躍動感のある動きを見せている[1]\n- 調教師は馬の状態に自信を持っている[1]\n- 外枠を引いたことで囲まれることがなく、有利と考えられている[3]\n\nネガティブな意見:\n- 初速が遅く、前に行きづらい傾向がある[9]\n- 6歳馬となり、年齢的な衰えの可能性がある\n- 昨年と異なる調整過程を取っているため、仕上がりに不安がある[10]\n- 外枠を引いたことで、スタート後のポジション取りが難しくなる可能性がある' ↩︎
Discussion