Closed24

LlamaIndexモジュールガイドを試してみる: Querying

kun432kun432

Understanding: Querying
https://docs.llamaindex.ai/en/stable/understanding/querying/querying.html

Modules Guides: Querying
https://docs.llamaindex.ai/en/stable/module_guides/querying/querying.html

ここまでやってきた流れだと、インデックス作成後は以下のようにしていた。

query_engine = index.as_query_engine()
response = query_engine.query("イクイノックスの主な勝ち鞍は?")
print(response)

インデックスに対してas_query_engine()でクエリエンジンオブジェクトを作成して、それに対してqueryでクエリを渡す、という形。

実際には、クエリは3つのステージで構成されている。

  1. Retrieval(検索)
    • クエリに最も関連性の高いドキュメントをインデックスから返す
    • 最も一般的な検索は"top-k"セマンティック検索だが、他にも多くの検索戦略がある
  2. Postprocessing(後処理)
    • 検索されたノードをリランク、整形、フィルタする
    • 例えば、特定のメタデータが付加されているものだけを選択する等
  3. Response syntheis(応答合成)
    • クエリ、最も関連性の高いデータ、プロンプトを合成して、LLMに送信、レスポンスを得る

上記をローレベルAPIで書くと以下のような感じになる。

from llama_index import get_response_synthesizer
from llama_index.retrievers import VectorIndexRetriever
from llama_index.query_engine import RetrieverQueryEngine
from llama_index.postprocessor import SimilarityPostprocessor

retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10,
)

response_synthesizer = get_response_synthesizer(response_mode="refine")

query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.8)],
)

response = query_engine.query("イクイノックスの主なGI勝ち鞍は?")
print(response)
イクイノックスの主なGI勝ち鞍は、天皇賞(秋)、有馬記念、そしてドバイシーマクラシックです。

個々の内容については後述するが、ざっくりこういう感じなんだろうなということがわかる。

  • VectorIndexRetrieverで、ベクトルインデックスからベクトル検索を行い、関連性の高いドキュメント上位10件を抽出
  • response_synthesizerで、"refine"モード(検索されたチャンクを順に渡して回答を洗練させるプロンプトを実行する)
  • RetrieverQueryEngineで、node_postprocessorsを関連付けて、retrieverの検索結果の関連性スコアが0.8以上に限定し、response_synthesizerに渡す
  • query_engineにqueryメソッドでクエリを渡す

Queryingのコンポーネントには上記も含めて、以下のようなものがある。

  • Query Engine
  • Chat Engine
  • Data Agents
  • Retriever
  • Response Synthesizer
  • Routers
  • Node Postprocessor
  • Structured Outputs

とりあえず、Query Engine、Chat Engine、Data Agentsは高レベルAPIだと思うので、

  • Retriever
  • Response Synthesizer
  • Node Postprocessor

あたりからまずは見ていってから、Query Engine、Chat Engineを見る。

Data AgentsとRoutersはエージェント的なやつなのでまとめて。

Structure Outputsは適当なところで。

kun432kun432

Retriever

https://docs.llamaindex.ai/en/stable/module_guides/querying/retriever/root.html

インデックスから関連性の高いNodeを検索するのがRetriever。高レベルAPIだと、インデックスにas_retriever()でretrieverオブジェクトを作成して、.retrieve()でクエリを渡す。

retriever = index.as_retriever(
    similarity_top_k=10,
)
nodes = retriever.retrieve("ドウデュースの主な勝ち鞍は?")

結果はNodeで返される。

for n in nodes:
    print("Score: {}\nText: {}".format(n.get_score(), n.get_content()[:50].replace("\n","") + "..."))
Score: 0.8710878825048197
Text: 鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の5...
Score: 0.8586540345620574
Text: まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りを...
Score: 0.8566146252006821
Text: 鞍上には武豊が復帰した。鞍上横山和生と本レースがラストランとなっており、レースを牽引して尚直線粘る...
Score: 0.8541785521180076
Text: さらに、ダービー馬による有馬記念制覇はオルフェーヴル以来10年ぶり9頭目で、三冠馬以外ではハクチカラ...
Score: 0.8489308913433957
Text: その後夏は治療と休養にあて、秋初戦として10月29日に東京競馬場で開催される天皇賞(秋)に出走。当日...
Score: 0.847351191459949
Text: また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となっ...
Score: 0.8472923877327061
Text: ドウデュース(欧字名:Do Deuce、2019年5月7日 - )は、日本の競走馬。主な勝ち鞍は20...
Score: 0.8425653266200219
Text: レースでは道中後方からじっくり運び、最後の直線は外からメンバー最速となる上がり (競馬)3ハロン33...
Score: 0.8407498004580377
Text: 1番人気に推されると、レースは直線でガイアフォースとの追い比べをクビ差制してデビュー勝ちを果たした。...
Score: 0.8374629379221759
Text: == 血統表 ==母ダストアンドダイヤモンズはアメリカで重賞2勝を挙げ、2012年のGI・ブリーダ...

オプションを渡すこともできるが、どのオプションが使えるかは使用しているインデックスにもよる。例えばSummaryIndexなどはretriever_modeで振る舞いが変わるが、VectorIndexの場合はretriever_mode はない。

https://docs.llamaindex.ai/en/stable/module_guides/querying/retriever/retriever_modes.html

要はインデックスとRetrieverはある意味セットの関係で、特定のインデックスの場合には、細かな振る舞いの違いで異なるRetrieverが実装されているということみたい(でretriever_modeでこれを切り替えることができる)

低レベルAPIの場合は以下。VectorIndexRetrieverを使用して明示的にIndexを指定している。

from llama_index.retrievers import VectorIndexRetriever

retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10,
)

nodes = retriever.retrieve("ドウデュースの主な勝ち鞍は?")

https://docs.llamaindex.ai/en/stable/api_reference/query/retrievers/vector_store.html

高レベルAPIだとas_retrieverで適切なretieverが選択されるが、こちらだと明示的に選択する必要がある。

Retrieverはいろいろある。インデックスやDocument/Nodeの構成などによってチョイスすることになる感じ。細かくはみてられないのと、いくつか気になるものがあるので、それは別途。

https://docs.llamaindex.ai/en/stable/module_guides/querying/retriever/retrievers.html

kun432kun432

Response Synthesizer

Response Synthesizerは、クエリ、retrieverで得られたデータ、プロンプトを合成して、LLMに送信、レスポンスを得る。

retriever.retrieve()で返されたNodeを、Response Synthesizerに渡す。以下すでにretrieverでNodeが返されている前提。

from llama_index.response_synthesizers import (
    ResponseMode,
    get_response_synthesizer,
)

response_synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.COMPACT
)

response = response_synthesizer.synthesize(
    "ドウデュースの主な勝ち鞍は?",
    nodes=nodes
)

print(response)
朝日杯フューチュリティステークス、東京優駿、有馬記念

as_query_engine()でResponse Synthesizerを指定することもできる。

from llama_index.response_synthesizers import (
    ResponseMode,
    get_response_synthesizer,
)

response_synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.COMPACT
)

query_engine = index.as_query_engine(response_synthesizer=response_synthesizer)
response = query_engine.query("ドウデュースの主な勝ち鞍は?")

Response Mode

response_modeにより、どのプロンプトテンプレートを使用するか?(retieverの結果をプロンプトにどう埋め込むか、LLMにどう問い合わせるか?)が変わる。

https://docs.llamaindex.ai/en/stable/module_guides/querying/response_synthesizers/root.html#configuring-the-response-mode

  • refine

    • 特徴: 各テキストチャンクを順番に処理し、答えを作成・改善
    • メリット: 詳細な回答に適している
    • 使用プロンプト: text_qa_template, refine_template
  • compact

    • 特徴: refineに似ているが、チャンクを事前に結合し、LLMコールを減らす
    • メリット: LLMコールが少ない
    • 使用プロンプト: text_qa_template, refine_template
  • tree_summarize

    • 特徴: チャンクをsummary_templateプロンプトで問い合わせ、一つの答えにまとめる
    • メリット: 要約に適している
    • 使用プロンプト: summary_template
  • simple_summarize

    • 特徴: テキストチャンクを切り詰め、一つのLLMプロンプトに収める
    • メリット: 速い要約が可能だが、詳細が失われる可能性あり
    • SummaryIndex用
  • no_text

    • 特徴: LLMに送ることなく、ノードを取得するのみ
    • メリット: ノードの検査が可能
  • accumulate

    • 特徴: クエリを各テキストチャンクに適用し、回答を配列に蓄積
    • メリット: 同じクエリを別々のテキストチャンクに対して実行する際に適している
  • compact_accumulate

    • 特徴: accumulateと同じだが、LLMプロンプトをcompactと同様に結合
    • メリット: LLMコールを減らすことができる

こちらもインデックスによって選べるもの・選べないものがある点に注意。

Structured Answer Filtering

refine / compact のときは structured_answer_filteringオプションが使える。これは、特にrefineの場合に意味が出てくると思うのだけど、

  • retrievalで渡されたNodeが質問と関連があるか?をチェックする。
  • 関連がなければ、そのNodeはrefineの対象としない

というものらしく、Function Callingを使ってやっているらしい。

from llama_index.response_synthesizers import (
    ResponseMode,
    get_response_synthesizer,
)

response_synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.REFINE,     # refineモードで有効
    structured_answer_filtering=True,     # ここ
    verbose=True     # Trueにするとstructured_answer_filteringの結果が標準出力に出力される
)

response = response_synthesizer.synthesize(
    "ドウデュースの主な勝ち鞍を教えて。",
    nodes=nodes
)

print(response)

set_global_handler("simple")を有効にして、どのようなやり取りが行われているのかを見てみる。

最初のNodeに対するQAプロンプト。

** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍を教えて。
Answer: 
**************************************************
** Response: **
assistant: None
**************************************************

通常ならResponseにLLMからの回答が入るのだけど、上記のようにNoneとなっていて、かわりに次に以下のように表示される

Function call: StructuredRefineResponse with args: {
  "answer": "ドウデュースの主な勝ち鞍は、2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。",
  "query_satisfied": true
}
> Refine context: file_path: data/ドウデュース.txt

ここでFunction Callingを使って、どうやら回答の生成+その回答がクエリに関連しているかを判定しているらしい。Function Callingのプロンプトはコードだとこの辺。

https://github.com/run-llama/llama_index/blob/1d861a9440cdc9e1b2335320a7a2bf26667b90b2/llama_index/response_synthesizers/refine.py#L21-L35

今回のケースは初回かつTrueだったので、ここの回答内容が次のNodeの判定に引き継がれる、すなわちrefineされていく。

** Messages: **
user: You are an expert Q&A system that strictly operates in two modes when refining existing answers:
1. **Rewrite** an original answer using the new context.
2. **Repeat** the original answer if the new context isn't useful.
Never reference the original answer or context directly in your answer.
When in doubt, just repeat the original answer.New Context: file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
Query: ドウデュースの主な勝ち鞍を教えて。
Original Answer: ドウデュースの主な勝ち鞍は、2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。
New Answer: 

次のNodeのOriginal Answerに反映されているのがわかる。

逆にFalseとなっている場合はこう。

** Messages: **
user: You are an expert Q&A system that strictly operates in two modes when refining existing answers:
1. **Rewrite** an original answer using the new context.
2. **Repeat** the original answer if the new context isn't useful.
Never reference the original answer or context directly in your answer.
When in doubt, just repeat the original answer.New Context: file_path: data/ドウデュース.txt

鞍上には武豊が復帰した。
鞍上横山和生と本レースがラストランとなっており、レースを牽引して尚直線粘るタイトルホルダーと、二番手から先に仕掛けていた鞍上クリストフ・ルメールのスターズオンアースを抜き去り、先頭でゴール板を駆け抜けて見事復活の勝利を挙げた。武豊は2017年のキタサンブラック以来、6年ぶりの制覇となった。また、54歳9カ月10日での有馬記念勝利は最年長勝利記録であり、同時に自身が持つJRA・GI最年長勝利記録(54歳19日)を更新。ドリームジャーニーやオルフェーヴル、ブラストワンピースに騎乗し歴代最多の有馬記念4勝を挙げている池添謙一に並ぶ勝利数となった。馬番5番での優勝は、1970年のスピードシンボリ、1972年のイシノヒカルに次いで、51年ぶり3度目となった。
Query: ドウデュースの主な勝ち鞍を教えて。
Original Answer: ドウデュースの主な勝ち鞍は、GIを含む重賞3勝目です。
New Answer: 
**************************************************
** Response: **
assistant: None
**************************************************


Function call: StructuredRefineResponse with args: {
  "answer": "ドウデュースの主な勝ち鞍は、GIを含む重賞3勝目です。",
  "query_satisfied": false
}
> Refine context: file_path: data/ドウデュース.txt

これの1つ前のNodeまでにrefineされた回答、元のクエリ、今回のNodeの情報を踏まえて、生成した回答のチェックがfalseとなっているので、このNodeのコンテキストはrefineには使用されていない、ということになる様子。(ユーザープロンプトにもあるとおり合致しない場合は回答はそのまま繰り返される)

最初見た時意味があんまりわからなかったのだけど、以下のnotebookで動きが理解できた。

https://docs.llamaindex.ai/en/stable/examples/response_synthesizers/structured_refine.html

以前も書いたけど、refineの精度についてはちょっと懐疑的で、Nodeを順にチェックしていくのはいいんだけど、Nodeの数が多ければおそらく最初に出てきたものは徐々に薄れたり、後から出てきたNodeによって逆にノイズが乗っかったりする可能性がありそう。ただstructured_answer_filteringを使うことで、このノイズを多少なりとも軽減できるのかもしれない。とはいえ、Nodeごとに個々にLLMへの問い合わせは発生するのでコストやレスポンス時間の観点で難しいかなと思ったりはする。

Tree Summarize

Tree Index的な要約をするresponse synthesizer、ってことなのかな?

まるっとテキストを渡すみたい。

from llama_index.response_synthesizers import TreeSummarize

tree_summarizer = TreeSummarize(verbose=True)
response = await tree_summarizer.aget_response("イクイノックスの主な勝ち鞍を教えて", [documents[0].text])

print(response)

まず複数のチャンクに分割されて、ここに回答が生成される。

3 text chunks after repacking

** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information from multiple sources is below.
---------------------
4月14日、ロンジンワールドベストレースホースランキング(2023年1月1日から4月9日までの世界の主要レースを対象)が発表され、イクイノックスは2着馬に3馬身1/2差をつけコースレコードで逃げ切ったドバイシーマクラシックの内容が高く評価され、レーティングは129ポンドで第1位となった。(snip)
---------------------
Given the information from multiple sources and not prior knowledge, answer the query.
Query: イクイノックスの主な勝ち鞍を教えて
Answer: 
**************************************************
** Response: **
assistant: イクイノックスの主な勝ち鞍は、ドバイシーマクラシック、宝塚記念、天皇賞(秋)、ジャパンカップです。
**************************************************

最後に各チャンクごとの内容から最終回答が生成される。

1 text chunks after repacking

** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information from multiple sources is below.
---------------------
イクイノックスの主な勝ち鞍は、2022年・2023年の天皇賞(秋)連覇、2022年の有馬記念、2023年のドバイシーマクラシック、宝塚記念、ジャパンカップです。

イクイノックスの主な勝ち鞍は、ドバイシーマクラシック、宝塚記念、天皇賞(秋)、ジャパンカップです。

イクイノックスの主な勝ち鞍は、ジャパンカップ、天皇賞(春)、宝塚記念、有馬記念、安田記念、マイルチャンピオンシップなどです。
---------------------
Given the information from multiple sources and not prior knowledge, answer the query.
Query: イクイノックスの主な勝ち鞍を教えて
Answer: 
**************************************************
** Response: **
assistant: イクイノックスの主な勝ち鞍は、2022年・2023年の天皇賞(秋)連覇、2022年の有馬記念、ドバイシーマクラシック、宝塚記念、ジャパンカップ、天皇賞(春)、有馬記念、安田記念、マイルチャンピオンシップなどです。
**************************************************

ま、回答は間違ってるんだけども。

TreeIndexが上から辿るのに対して、こちらは末端ごとの結果を集約する、って感じかな?

もはやresponse synthesizer単体の機能としてはどこまでやるねん、という気もしなくもないけど、あくまでも入力はテキストだから、まあそうかという感。

Tree SummarizeのStructured Outputのnotebookもあるけど、これはStructured Outputのところで確認する。

https://docs.llamaindex.ai/en/stable/examples/response_synthesizers/custom_prompt_synthesizer.html

Response Synthesizerで指定できるオプション

https://docs.llamaindex.ai/en/stable/module_guides/querying/response_synthesizers/response_synthesizers.html

kun432kun432

Node Postprocessor

https://docs.llamaindex.ai/en/stable/module_guides/querying/node_postprocessors/root.html

Node Postprocessorは、Retrieverの結果を後処理してからResponse Synthesizerに渡す。何かしら整形したり、フィルタしたりというところ。

以下のようなretrieverから得たNodeがあるとする。

from llama_index.retrievers import VectorIndexRetriever

retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10,
)

nodes = retriever.retrieve("ドウデュースの主な勝ち鞍は?")

for n in nodes:
    print("Score: {}\nText: {}".format(n.get_score(), n.get_content()[:50].replace("\n","") + "..."))
Score: 0.8711197622628892
Text: 鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の5...
Score: 0.8586644710637568
Text: まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りを...
Score: 0.856530004414721
Text: 鞍上には武豊が復帰した。鞍上横山和生と本レースがラストランとなっており、レースを牽引して尚直線粘る...
Score: 0.8543041231544019
Text: さらに、ダービー馬による有馬記念制覇はオルフェーヴル以来10年ぶり9頭目で、三冠馬以外ではハクチカラ...
Score: 0.8489308913433957
Text: その後夏は治療と休養にあて、秋初戦として10月29日に東京競馬場で開催される天皇賞(秋)に出走。当日...
Score: 0.8472844128737264
Text: また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となっ...
Score: 0.8472790958966984
Text: ドウデュース(欧字名:Do Deuce、2019年5月7日 - )は、日本の競走馬。主な勝ち鞍は20...
Score: 0.8425653266200219
Text: レースでは道中後方からじっくり運び、最後の直線は外からメンバー最速となる上がり (競馬)3ハロン33...
Score: 0.8385146201009842
Text: 1番人気に推されると、レースは直線でガイアフォースとの追い比べをクビ差制してデビュー勝ちを果たした。...
Score: 0.8376079308869422
Text: == 血統表 ==母ダストアンドダイヤモンズはアメリカで重賞2勝を挙げ、2012年のGI・ブリーダ...

これに対して、Node Postprocessorを使って、関連性スコア0.85以上というフィルタを掛ける。

from llama_index.postprocessor import SimilarityPostprocessor

processor = SimilarityPostprocessor(similarity_cutoff=0.85)
filtered_nodes = processor.postprocess_nodes(nodes)

for n in filtered_nodes:
    print("Score: {}\nText: {}".format(n.get_score(), n.get_content()[:50].replace("\n","") + "..."))

以下のように、スコアしきい値以上のものでフィルタされている。

Score: 0.8711197622628892
Text: 鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の5...
Score: 0.8586644710637568
Text: まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りを...
Score: 0.856530004414721
Text: 鞍上には武豊が復帰した。鞍上横山和生と本レースがラストランとなっており、レースを牽引して尚直線粘る...
Score: 0.8543041231544019
Text: さらに、ダービー馬による有馬記念制覇はオルフェーヴル以来10年ぶり9頭目で、三冠馬以外ではハクチカラ...

さらにCohereのRerankをNode Postprocessorとして使ってみる。

!pip install cohere
from llama_index.postprocessor import CohereRerank

reranker = CohereRerank(api_key=os.environ["COHERE_API_KEY"], top_n=5)
reranked_nodes = reranker.postprocess_nodes(filtered_nodes, query_str="ドウデュースの主な勝ち鞍は?")

for n in reranked_nodes:
    print("Score: {}\nText: {}".format(n.get_score(), n.get_content()[:50].replace("\n","") + "..."))
Score: 0.97257143
Text: 鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の5...
Score: 0.03926875
Text: さらに、ダービー馬による有馬記念制覇はオルフェーヴル以来10年ぶり9頭目で、三冠馬以外ではハクチカラ...
Score: 0.03442355
Text: まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りを...
Score: 0.027220903
Text: 鞍上には武豊が復帰した。鞍上横山和生と本レースがラストランとなっており、レースを牽引して尚直線粘る...

スコアと順位が変わっているのがわかる。

なお、上記はNodesに対して直接Node Postprocessorを適用した形だけど、Query Engineでもできる。

query_engine = index.as_query_engine(
    similarity_top_k=10,
    node_postprocessors=[
        SimilarityPostprocessor(similarity_cutoff=0.8),
        CohereRerank(api_key=os.environ["COHERE_API_KEY"], top_n=5),
    ]
)

response = query_engine.query("ドウデュースの主な勝ち鞍を教えて。")

Node Postprocessorで使えるモジュール

https://docs.llamaindex.ai/en/stable/module_guides/querying/node_postprocessors/node_postprocessors.html

  • SimilarityPostprocessor
    • 関連性スコアしきい値を設定してフィルタする
  • KeywordNodePostprocessor
    • 必須/除外キーワードが含まれているか、フィルタする
  • MetadataReplacementPostProcessor
    • 指定のメタデータがあればテキストを置き換える
  • LongContextReorder
    • 長文コンテキストの場合に、順番を入れ替えて、最初と最後に重要なコンテキストを配置する
  • SentenceEmbeddingOptimizer
    • クエリと関連のない文章を取り除くことでトークン消費を抑える
  • CohereRerank
    • Cohere ReRankを使用してNodeの順位をリランクする
  • SentenceTransformerRerank
    • sentence-transformerを使用してNodeの順位をリランクする
  • LLM Rerank
    • LLMを使用してNodeの順位をリランクする
  • FixedRecencyPostprocessor
    • 日付で順位をソートする。より最新のものを優先するとか。メタデータに日付のフィールドが必要。
  • EmbeddingRecencyPostprocessor
    • 日付で順位をソートして古いものを削除した後、Embeddingの類似性で順位をソートする。
  • TimeWeightedPostprocessor
    • Node抽出にかかった時間でリランクする。あまり使用されていないNodeの順位を下げるとか。
  • PIINodePostprocessor
    • PII (Personal Identifiable Information)などセキュリティ的に好ましくない情報を除外する。
  • PrevNextNodePostprocessor
  • Node間のリレーションに基づいて、Nodeを選択する。例えば、時系列にNodeが並んでいる場合に、最も関連性のあるNodeの前後を拾う、みたいな。
  • AutoPrevNextNodePostprocessor
    • PrevNextNodePostprocessorの判断をLLMに行わせるパターン
kun432kun432

Query Engine

https://docs.llamaindex.ai/en/stable/module_guides/deploying/query_engine/root.html

冒頭に記載した通り、Query Engineは以下の低レベルAPIをラップしてハイレベルAPIとして提供しているものと認識している。

  • Retrieval(検索)
  • Postprocessing(後処理)
  • Response syntheis(応答合成)

Starter Tutorialにもあるとおり、最小で書くとこんな感じ。

from llama_index import VectorStoreIndex, SimpleDirectoryReader

documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)

query_engine = index.as_query_engine()
response = query_engine.query("ドウデュースの主な勝ち鞍を教えて。")
print(response)

作成したインデックスに対して、as_query_engine()でクエリエンジンを初期化してquery()メソッドでクエリを叩くだけ。

ストリーミングレスポンスも可能。ただし対応しているLLMのみ。

https://docs.llamaindex.ai/en/stable/module_guides/deploying/query_engine/streaming.html#streaming-response

query_engine = index.as_query_engine(streaming=True)
streaming_response = query_engine.query("ドウデュースの主な勝ち鞍は?")
streaming_response.print_response_stream()

実際にはストリーミングで出力される。

ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。

わかりやすくするとこう。

query_engine = index.as_query_engine(streaming=True)
streaming_response = query_engine.query("ドウデュースの主な勝ち鞍は?")

for text in streaming_response.response_gen:
    print(text)
    pass
ド
ウ
デ
ュ
ース
の
主
な
勝
ち
鞍
は
、
日
本
ダ
ービ
ー
、
朝
日
杯
フ
ュ
ーチ
ュ
リ
テ
ィ
ス
テ
ー
ク
ス
、
京
都
記
念
です
。

繰り返しになるけど、結局のところは、Query Engineは、Retriever/Node Postprocessor/Response syntheizerの組み合わせのラッパーだと思うので、指定できるオプション、例えばresponse_modeもそれらに準じることになると思うし、たくさんあるQuery Engineモジュールもそれぞれ独自のRetriever/Node Postprocessor/Response syntheizerを組み合わせたり、ってことだと思うので、全部を細かく見ることはしない(ただし全部が全部そうではなさそう、Query Engine単体として実装されているものもある様子、まじかよ・・・)

いくつか興味があるのはこのあたりかなぁ。

  • JSON Query Engine
  • Pandas Query Engine
  • Router Query Engine
  • Structured Hierarchical Retrieval
  • Recursive Retriever + QueryEngine
  • Joint QA Summary Query Engine
  • Sub Question Query Engine
  • Multi-Step Query Engine
  • Self Correcting Query Engines - Evaluation & Retry
  • CitationQueryEngine
  • Ensemble Query Engine Guide
  • FLARE Query Engine
  • Query Transformations

まあこの辺はおいおいやることにする。

kun432kun432

Chat Engine

https://docs.llamaindex.ai/en/stable/module_guides/deploying/chat_engines/root.html

Query Engineのチャット版。Query Engineが一問一答で完結するのに対し、Chat Engineはマルチターンの会話になる、つまり過去〜直前までの会話履歴に意味が出てくるのが違い。

コードとしてはas_query_engine()as_chat_engine()になり、queryメソッドがchatメソッドになるだけ。

chat_engine = index.as_chat_engine()
response = chat_engine.chat("ドウデュースの主な勝ち鞍は?")
print(response)

こんな感じでインデックスを作る。set_global_handler("simple")でプロンプト・レスポンスを確認する。

from llama_index import VectorStoreIndex, SimpleDirectoryReader, ServiceContext
from llama_index.text_splitter import SentenceSplitter
from llama_index.embeddings import OpenAIEmbedding
from llama_index.llms import OpenAI
from llama_index import set_global_handler

set_global_handler("simple")

llm = OpenAI(model="gpt-3.5-turbo", temperarture=0)
embed_model = OpenAIEmbedding()
node_parser = SentenceSplitter(chunk_size=400, chunk_overlap=20)

service_context = ServiceContext.from_defaults(
  llm=llm,
  embed_model=embed_model,
  node_parser=node_parser,
)

documents = SimpleDirectoryReader("data").load_data()

index = VectorStoreIndex.from_documents(documents, service_context=service_context)

Chat Engineで問い合わせしてみる。

chat_engine = index.as_chat_engine()

response = chat_engine.chat("ドウデュースの主な勝ち鞍について教えて。")
print(f"### 回答出力 ###\n\n{response}")
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: None
**************************************************


** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍は何ですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
**************************************************


** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
**************************************************


### 回答出力 ###

ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。

1回目のクエリとレスポンスが行われていることがわかる。あと、なんかChat Engineの場合は"tool"が使われてる様子だけど、それは置いておく。

もう一度クエリを送ってみる。・

response = chat_engine.chat("へぇ、すごいですね。")
print(f"### 回答出力 ###\n\n{response}")
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
user: へぇ、すごいですね。
**************************************************
** Response: **
assistant: はい、ドウデュースはこれらのレースで優れたパフォーマンスを見せました。彼の能力と才能は素晴らしいものであり、多くの人々に感銘を与えています。
**************************************************


### 回答出力 ###

はい、ドウデュースはこれらのレースで優れたパフォーマンスを見せました。彼の能力と才能は素晴らしいものであり、多くの人々に感銘を与えています。

2回目のリクエストでは前回の会話も含めたプロンプトを送信しているのがわかる。

Chat Engineの使い方

順番に見ていく

https://docs.llamaindex.ai/en/stable/module_guides/deploying/chat_engines/usage_pattern.html

会話履歴の消去

会話履歴を消去する

chat_engine.reset()

続けてクエリを送ってみる。

chat_engine = index.as_chat_engine()

response = chat_engine.chat("レース内容について詳しく教えて")
print(f"### 回答出力 ###\n\n{response}")
** Messages: **
user: レース内容について詳しく教えて
**************************************************
** Response: **
assistant: もちろんです!どのレースの内容について詳しく知りたいですか?例えば、F1レースやオリンピックの陸上競技、自転車レースなど、具体的な競技やイベントを教えてください。
**************************************************


### 回答出力 ###

もちろんです!どのレースの内容について詳しく知りたいですか?例えば、F1レースやオリンピックの陸上競技、自転車レースなど、具体的な競技やイベントを教えてください。

プロンプトに直前の会話履歴がないため、前回と話が繋がっていない。あと、今回は"tool"というのが見えないけど、おそらくクエリの内容がインデックスと関係するかどうか?がまず最初に判断されていて、関連する場合だけインデックス検索されているのではないか?という気がするなー。詳細はおいおい。

インタラクティブな対話

Colaboratoryで試すとこんな感じ。

chat_engine = index.as_chat_engine()

chat_engine.chat_repl()

日本語の場合は、変換確定時のENTERキーで送信されちゃうのでちょっと注意が必要。

chat_mode

Query Engineのresponse_modeと同じように、Chat Engineにもchat_modeというものがあり、挙動が変わる。

  • best(デフォルトはこれっぽい
    • Query Engineを「ツール」化し、ReAct Data AgentやOpenAI Data Agentで使用する。
    • OpenAI Data Agentは、OpenAIのFunction Callingを使うため、gpt-3.5-turbo / gpt-4が必要
  • condense_question
    • チャットの履歴を見て、インデックスのクエリになるようにユーザーメッセージを書き直す。
    • Query Engineからの応答を読み込んだ後、応答を返す。
  • context
    • すべてのユーザーメッセージを使用してインデックスからNodeを取得する。
    • 取得したテキストはシステムプロンプトに挿入されるため、Chat Engineは自然に応答するか、Query Engineからのコンテキストを使用することができる。
  • condense_plus_context
    • condense_questioncontextの組み合わせ。
    • チャットの履歴を見て、インデックスの検索クエリになるようにユーザーメッセージを書き直す。
    • 検索されたテキストはシステムプロンプトに挿入され、Chat Engineは自然に応答するか、Query Engineからのコンテキストを使用することができる。
  • simple
    • LLMと直接チャットするシンプルなもの
    • Query Engineは関係ない。
  • react
    • bestと同じ
    • ReAct Data Agentを強制する
  • openai
    • bestと同じ
    • OpenAI Data Agentを強制する

モードごとに呼び出されるChat Engineモジュールやそれに付随するプロンプトが変わるのは、Query Engineと同じ。

Note: you can access different chat engines by specifying the chat_mode as a kwarg. condense_question corresponds to CondenseQuestionChatEngine, react corresponds to ReActChatEngine, context corresponds to a ContextChatEngine.

以降で、各モードの動きをざっと見てみる。set_global_handler("simple")verbose=Trueをつけてプロンプトやレスポンスの入出力を見ながら。

from llama_index import set_global_handler

set_global_handler("simple")

chat_modes = ["best", "condense_question", "context", "condense_plus_context", "simple", "react", "openai"]
messages = ["ドウデュースの主な勝ち鞍について教えて。", "へえ、すごいですね", "そのうちGI勝利はいくつですか?"]

for chat_mode in chat_modes:
    print("### MODE: {}\n".format(chat_mode.upper()))
    
    chat_engine = index.as_chat_engine(chat_mode=chat_mode, verbose=True)
    chat_engine.reset()

    for i, message in enumerate(messages, start=1):
        print("#### {} 回目\n".format(i))

        response = chat_engine.chat(message)
        print("\n⇒ 回答: {}\n".format(response.response.replace("\n","")))

出力がデカくてZennのコメントに収まり切らなくなりそうなので、各モードごとにコメントを分けて記載する。

kun432kun432

chat_mode: best

  • best(デフォルトはこれっぽい)
    • Query Engineを「ツール」化し、ReAct Data AgentやOpenAI Data Agentで使用する。
    • OpenAI Data Agentは、OpenAIのFunction Callingを使うため、gpt-3.5-turbo / gpt-4が必要
出力内容
### MODE: BEST

#### 1 回目

Added user message to memory: ドウデュースの主な勝ち鞍について教えて。
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: None
**************************************************


=== Calling Function ===
Calling function: query_engine_tool with args: {
  "input": "ドウデュースの主な勝ち鞍は何ですか?"
}
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍は何ですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
**************************************************


Got output: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
========================

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。

#### 2 回目

Added user message to memory: へえ、すごいですね
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
user: へえ、すごいですね
**************************************************
** Response: **
assistant: はい、ドウデュースはこれらのレースで素晴らしいパフォーマンスを見せました。日本ダービーは日本の最も権威ある競馬の一つであり、朝日杯フューチュリティステークスも若い馬たちの競争で非常に重要なレースです。また、京都記念は重賞レースでの3勝目となります。ドウデュースはこれらのレースで優れた能力を発揮し、多くの競馬ファンを魅了しました。
**************************************************



⇒ 回答: はい、ドウデュースはこれらのレースで素晴らしいパフォーマンスを見せました。日本ダービーは日本の最も権威ある競馬の一つであり、朝日杯フューチュリティステークスも若い馬たちの競争で非常に重要なレースです。また、京都記念は重賞レースでの3勝目となります。ドウデュースはこれらのレースで優れた能力を発揮し、多くの競馬ファンを魅了しました。

#### 3 回目

Added user message to memory: そのうちGI勝利はいくつですか?
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
user: へえ、すごいですね
assistant: はい、ドウデュースはこれらのレースで素晴らしいパフォーマンスを見せました。日本ダービーは日本の最も権威ある競馬の一つであり、朝日杯フューチュリティステークスも若い馬たちの競争で非常に重要なレースです。また、京都記念は重賞レースでの3勝目となります。ドウデュースはこれらのレースで優れた能力を発揮し、多くの競馬ファンを魅了しました。
user: そのうちGI勝利はいくつですか?
**************************************************
** Response: **
assistant: None
**************************************************


=== Calling Function ===
Calling function: query_engine_tool with args: {
  "input": "ドウデュースのGI勝利はいくつですか?"
}
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となった。


=== 3歳(2022年) ===
3歳初戦として、弥生賞ディープインパクト記念に出走。単勝オッズ2.2倍の1番人気に推された。道中は勝ち馬アスクビクターモアを見る形で追走。残り800メートル過ぎに後方からロジハービンが一気に進出したため、いったんポジションを下げる。そこから立て直し、ゴール前では勝ち馬を懸命に追い上げたがクビ差届かず2着に。デビューからの連勝は3でストップした。
続いて、4月17日に行われた皐月賞に出走。1番人気には推されたものの単勝のオッズは3.9倍で、これは1984年のグレード制導入以降、1990年アイネスフウジンの4.1倍に次ぐ皐月賞1番人気の低支持率オッズであった。

file_path: data/ドウデュース.txt

1番人気に推されると、レースは直線でガイアフォースとの追い比べをクビ差制してデビュー勝ちを果たした。
次走はリステッド競走のアイビーステークスを選択。2番人気に推され、レースでは追い比べから抜け出すと、最後は追い込んできたグランシエロをクビ差凌いで優勝、デビュー2連勝とした。
続いて朝日杯フューチュリティステークスに出走。重賞勝ち馬セリフォスやジオグリフをはじめとした自身と同じ無敗馬が多く顔を揃える中、3番人気に支持される。レースでは直線で外に出すと、先に抜け出していたセリフォスを半馬身差で差し切り優勝、無傷3連勝でGI初制覇を果たした。鞍上の武豊はこの競走22回目の挑戦で初制覇となり、日本の中央競馬 (JRA) の平地GI完全制覇までホープフルステークスを残すのみとした。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースのGI勝利はいくつですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースのGI勝利は1回です。
**************************************************


Got output: ドウデュースのGI勝利は1回です。
========================

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
user: へえ、すごいですね
assistant: はい、ドウデュースはこれらのレースで素晴らしいパフォーマンスを見せました。日本ダービーは日本の最も権威ある競馬の一つであり、朝日杯フューチュリティステークスも若い馬たちの競争で非常に重要なレースです。また、京都記念は重賞レースでの3勝目となります。ドウデュースはこれらのレースで優れた能力を発揮し、多くの競馬ファンを魅了しました。
user: そのうちGI勝利はいくつですか?
assistant: None
tool: ドウデュースのGI勝利は1回です。
**************************************************
** Response: **
assistant: ドウデュースのGI勝利は1回です。
**************************************************



⇒ 回答: ドウデュースのGI勝利は1回です。
  • インデックスを検索するQuery EngineはToolとなっていて、Function Callingでクエリの内容を踏まえて呼び出される。
    • クエリの内容がQuery Engineを呼ぶ必要がなければ、呼び出されない。
  • クエリとレスポンスおよびTool実行結果が、会話履歴として保持され、以降のクエリでも使用される
  • 検索結果はUser messageとして付与される。

納得の挙動。Function Calling使って必要がなときだけインデックス検索している。

kun432kun432

chat_mode: condense_question

  • condense_question
    • チャットの履歴を見て、インデックスのクエリになるようにユーザーメッセージを書き直す。
    • Query Engineからの応答を読み込んだ後、応答を返す。
出力内容
### MODE: CONDENSE_QUESTION

#### 1 回目

** Messages: **
user: Given a conversation (between Human and Assistant) and a follow up message from Human, rewrite the message to be a standalone question that captures all relevant context from the conversation.

<Chat History>


<Follow Up Message>
ドウデュースの主な勝ち鞍について教えて。

<Standalone question>

**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は何ですか?
**************************************************


Querying with: ドウデュースの主な勝ち鞍は何ですか?
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍は何ですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。

#### 2 回目

** Messages: **
user: Given a conversation (between Human and Assistant) and a follow up message from Human, rewrite the message to be a standalone question that captures all relevant context from the conversation.

<Chat History>
user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。

<Follow Up Message>
へえ、すごいですね

<Standalone question>

**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は何ですか?
**************************************************


Querying with: ドウデュースの主な勝ち鞍は何ですか?
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍は何ですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。

#### 3 回目

** Messages: **
user: Given a conversation (between Human and Assistant) and a follow up message from Human, rewrite the message to be a standalone question that captures all relevant context from the conversation.

<Chat History>
user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
user: へえ、すごいですね
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。

<Follow Up Message>
そのうちGI勝利はいくつですか?

<Standalone question>

**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍のうち、GI勝利はいくつありますか?
**************************************************


Querying with: ドウデュースの主な勝ち鞍のうち、GI勝利はいくつありますか?
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となった。


=== 3歳(2022年) ===
3歳初戦として、弥生賞ディープインパクト記念に出走。単勝オッズ2.2倍の1番人気に推された。道中は勝ち馬アスクビクターモアを見る形で追走。残り800メートル過ぎに後方からロジハービンが一気に進出したため、いったんポジションを下げる。そこから立て直し、ゴール前では勝ち馬を懸命に追い上げたがクビ差届かず2着に。デビューからの連勝は3でストップした。
続いて、4月17日に行われた皐月賞に出走。1番人気には推されたものの単勝のオッズは3.9倍で、これは1984年のグレード制導入以降、1990年アイネスフウジンの4.1倍に次ぐ皐月賞1番人気の低支持率オッズであった。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍のうち、GI勝利はいくつありますか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍のうち、GI勝利は1つあります。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍のうち、GI勝利は1つあります。

2回目のクエリは1回目のクエリで置き換えられているようにみえる。会話履歴の内容を踏まえて常にインデックス検索するようなクエリにする、って感じなのかな?イマイチユースケースがピンとこない。

kun432kun432

chat_mode: context

  • context
    • すべてのユーザーメッセージを使用してインデックスからNodeを取得する。
    • 取得したテキストはシステムプロンプトに挿入されるため、Chat Engineは自然に応答するか、Query Engineからのコンテキストを使用することができる。
出力内容
### MODE: CONTEXT

#### 1 回目

** Messages: **
system: 
Context information is below.
--------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
--------------------

user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は以下の通りです。

1. 日本ダービー:2013年に優勝し、キズナ以来となるダービー6勝目を達成しました。また、史上初の50代でのダービージョッキーとしても名誉を得ました。

2. 朝日杯フューチュリティステークス:日本ダービーを制した前年の勝ち馬として、1994年のナリタブライアン以来28年ぶりにこのレースを制しました。

3. 京都記念:日本ダービーを制した馬が勝利するのは、1948年のマツミドリ以来75年ぶりとなりました。

これらの勝利は、ドウデュースのキャリアにおいて特に輝かしい成果となっています。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍は以下の通りです。1. 日本ダービー:2013年に優勝し、キズナ以来となるダービー6勝目を達成しました。また、史上初の50代でのダービージョッキーとしても名誉を得ました。2. 朝日杯フューチュリティステークス:日本ダービーを制した前年の勝ち馬として、1994年のナリタブライアン以来28年ぶりにこのレースを制しました。3. 京都記念:日本ダービーを制した馬が勝利するのは、1948年のマツミドリ以来75年ぶりとなりました。これらの勝利は、ドウデュースのキャリアにおいて特に輝かしい成果となっています。

#### 2 回目

** Messages: **
system: 
Context information is below.
--------------------
file_path: data/イクイノックス.txt

すごく強かったです。リバティにとって素晴らしい経験ができたので、今後に必ず生きてくると思います」とイクイノックスを称賛した。

イクイノックスの今後について、同馬を所有するシルクレーシングの米本昌史代表は「まずは馬の様子を見て無事を確認して。いろいろな評価をいただけると思うので、これから考えたい」とし、「有馬記念も、ここ(ジャパンカップ)が最後ということも含めて、全てが選択肢になると思います」と話した。
その後、11月28日にノーザンファーム天栄に放牧に出して、協議を行った結果、11月29日にジャパンカップを最後に有馬記念には出走せず、現役を引退することを決断し、11月30日に現役を引退することがシルクレーシングより正式に発表された。

file_path: data/イクイノックス.txt

イクイノックスは世界一の馬ですから。世界のみんなが彼の競馬を見たかったと思う。イクイノックスの強さを見せられました」、「イクイノックスは全部を持っている馬。スタートからいいポジションが取れるし、その後冷静に走れて、いい脚で伸びてくれる。スタミナもある。完璧な馬です」とイクイノックスを称賛した。また、「彼はまだ強く、タフになれる。この秋はイクイノックスのピークに持っていけると思う」とさらに強くなる可能性があるとした。
天皇賞(秋)を勝利したことにより、JRA総獲得賞金は12億5269万2000円となり、コントレイルを抜いて歴代10位となった。さらに、ドバイシーマクラシックの分(約4億6000万円)も含めると総獲得賞金は17億1158万2100円となり、オルフェーヴルを抜いて歴代6位となった。
--------------------

user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は以下の通りです。

1. 日本ダービー:2013年に優勝し、キズナ以来となるダービー6勝目を達成しました。また、史上初の50代でのダービージョッキーとしても名誉を得ました。

2. 朝日杯フューチュリティステークス:日本ダービーを制した前年の勝ち馬として、1994年のナリタブライアン以来28年ぶりにこのレースを制しました。

3. 京都記念:日本ダービーを制した馬が勝利するのは、1948年のマツミドリ以来75年ぶりとなりました。

これらの勝利は、ドウデュースのキャリアにおいて特に輝かしい成果となっています。
user: へえ、すごいですね
**************************************************
** Response: **
assistant: はい、ドウデュースは非常に優れた競走馬であり、その成績は非常に素晴らしいものです。特に日本ダービーの6勝は、その偉業を象徴しています。また、朝日杯フューチュリティステークスや京都記念など、他の重要なレースでも勝利を収めています。ドウデュースの能力と才能は、競馬界で高く評価されています。
**************************************************



⇒ 回答: はい、ドウデュースは非常に優れた競走馬であり、その成績は非常に素晴らしいものです。特に日本ダービーの6勝は、その偉業を象徴しています。また、朝日杯フューチュリティステークスや京都記念など、他の重要なレースでも勝利を収めています。ドウデュースの能力と才能は、競馬界で高く評価されています。

#### 3 回目

** Messages: **
system: 
Context information is below.
--------------------
file_path: data/イクイノックス.txt

GI6連勝はグレード制が導入された1984年以降では、テイエムオペラオー、ロードカナロアのGI競走の連勝記録に並んだ。芝平地の古馬GⅠ6勝は日本馬歴代最多タイ。木村哲也調教師は本競走初制覇。また、クリストフ・ルメールは武豊と並ぶジャパンカップ最多4勝目。そのほかにも、3着に5番人気のスターズオンアースが入ったため、3連単のオッズが11.3倍となったのは、第40回を制したアーモンドアイと2着馬コントレイル、3着馬デアリングタクトの13.4倍を更新し、歴代JRA・GIにおける最低額配当となった他、イクイノックス、リバティアイランドのワイドのオッズが1.3倍となり、2000年の天皇賞(春)を制したテイエムオペラオーと2着馬ラスカルスズカ、3着馬ナリタトップロードなどと並び、ワイドの低額配当タイの記録となった。

file_path: data/イクイノックス.txt

キャリア5戦での天皇賞(秋)制覇は史上最短、前年のホープフルステークスから続いていた平地GI競走の1番人気の連敗記録を16連敗で止めるなど、記録ずくめの勝利となった。キタサンブラック産駒はGI初制覇で、史上4組目の天皇賞(秋)父子制覇を達成した。レース後、インタビューでルメールは「春はアンラッキーだったけど、今日は本当のイクイノックスを見せることができた」「今回が彼の最初のGIですが、これが最後ではない。改めてこれからもGI取れると思います」とコメントした。

次走として有馬記念に出走すると表明した。ファン投票でも多くの票を集め、第1回中間発表、第2回中間発表で共に3位の票数を獲得し、最終結果発表でも294,688票を集め3位となった。

12月25日、予定通り有馬記念に出走。単勝2.3倍の1番人気に推された。
--------------------

user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は以下の通りです。

1. 日本ダービー:2013年に優勝し、キズナ以来となるダービー6勝目を達成しました。また、史上初の50代でのダービージョッキーとしても名誉を得ました。

2. 朝日杯フューチュリティステークス:日本ダービーを制した前年の勝ち馬として、1994年のナリタブライアン以来28年ぶりにこのレースを制しました。

3. 京都記念:日本ダービーを制した馬が勝利するのは、1948年のマツミドリ以来75年ぶりとなりました。

これらの勝利は、ドウデュースのキャリアにおいて特に輝かしい成果となっています。
user: へえ、すごいですね
assistant: はい、ドウデュースは非常に優れた競走馬であり、その成績は非常に素晴らしいものです。特に日本ダービーの6勝は、その偉業を象徴しています。また、朝日杯フューチュリティステークスや京都記念など、他の重要なレースでも勝利を収めています。ドウデュースの能力と才能は、競馬界で高く評価されています。
user: そのうちGI勝利はいくつですか?
**************************************************
** Response: **
assistant: ドウデュースのGI勝利は、現時点では確認されていません。彼は日本ダービーなどの重要なレースで優勝しましたが、GI競走での勝利はまだありません。ただし、GI競走においても常に上位に食い込む力を持っており、将来的にはGI勝利を達成する可能性もあります。
**************************************************



⇒ 回答: ドウデュースのGI勝利は、現時点では確認されていません。彼は日本ダービーなどの重要なレースで優勝しましたが、GI競走での勝利はまだありません。ただし、GI競走においても常に上位に食い込む力を持っており、将来的にはGI勝利を達成する可能性もあります。

なるほど、クエリの内容がインデックス検索不要な場合でも常にインデックス検索して、かつ、関係なくてもその結果をプロンプトに含めるって感じか。でインデックス検索結果はsystem messageを常に書き換える・user messageを書き換えない、つまり「自然」ということなんだろう。1回目の検索結果は1回目の回答に含まれているのでOKってことなんだろうけど、今回のケースだと関係ないものを拾ってきてるので、応答にノイズが載る可能性はあるかな。

condense_questionもそうだったけど、Function Callingは、クエリがインデックスに関連するかどうか・インデックス検索すべきかどうかというところの判断に使われているのだね。

kun432kun432

chat_mode: condense_plus_context

  • condense_plus_context
    • condense_questioncontextの組み合わせ。
    • チャットの履歴を見て、インデックスの検索クエリになるようにユーザーメッセージを書き直す。
    • 検索されたテキストはシステムプロンプトに挿入され、Chat Engineは自然に応答するか、Query Engineからのコンテキストを使用することができる。
出力内容
### MODE: CONDENSE_PLUS_CONTEXT

#### 1 回目

Condensed question: ドウデュースの主な勝ち鞍について教えて。
Context: file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
** Messages: **
system: 
  The following is a friendly conversation between a user and an AI assistant.
  The assistant is talkative and provides lots of specific details from its context.
  If the assistant does not know the answer to a question, it truthfully says it
  does not know.

  Here are the relevant documents for the context:

  file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。

  Instruction: Based on the above documents, provide a detailed answer for the user question below.
  Answer "don't know" if not present in the document.
  
user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、以下の通りです:

1. 日本ダービー:ドウデュースは2013年に日本ダービーを制しました。この勝利により、鞍上の武豊騎手は史上初の50代でのダービージョッキーとなりました。

2. 朝日杯フューチュリティステークス:ドウデュースは朝日杯フューチュリティステークスでも勝利しています。

3. 京都記念:ドウデュースは日本ダービーを制した後、京都記念でも勝利しました。この勝利は、日本ダービー勝利馬が京都記念を制するのは、1948年のマツミドリ以来75年ぶりの快挙となりました。

また、ドウデュースはこれらの重賞勝利に加えて、GIを含む重賞3勝を挙げています。

以上がドウデュースの主な勝ち鞍です。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍は、以下の通りです:1. 日本ダービー:ドウデュースは2013年に日本ダービーを制しました。この勝利により、鞍上の武豊騎手は史上初の50代でのダービージョッキーとなりました。2. 朝日杯フューチュリティステークス:ドウデュースは朝日杯フューチュリティステークスでも勝利しています。3. 京都記念:ドウデュースは日本ダービーを制した後、京都記念でも勝利しました。この勝利は、日本ダービー勝利馬が京都記念を制するのは、1948年のマツミドリ以来75年ぶりの快挙となりました。また、ドウデュースはこれらの重賞勝利に加えて、GIを含む重賞3勝を挙げています。以上がドウデュースの主な勝ち鞍です。

#### 2 回目

** Messages: **
user: 
  Given the following conversation between a user and an AI assistant and a follow up question from user,
  rephrase the follow up question to be a standalone question.

  Chat History:
  user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、以下の通りです:

1. 日本ダービー:ドウデュースは2013年に日本ダービーを制しました。この勝利により、鞍上の武豊騎手は史上初の50代でのダービージョッキーとなりました。

2. 朝日杯フューチュリティステークス:ドウデュースは朝日杯フューチュリティステークスでも勝利しています。

3. 京都記念:ドウデュースは日本ダービーを制した後、京都記念でも勝利しました。この勝利は、日本ダービー勝利馬が京都記念を制するのは、1948年のマツミドリ以来75年ぶりの快挙となりました。

また、ドウデュースはこれらの重賞勝利に加えて、GIを含む重賞3勝を挙げています。

以上がドウデュースの主な勝ち鞍です。
  Follow Up Input: へえ、すごいですね
  Standalone question:
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は何ですか?
**************************************************


Condensed question: ドウデュースの主な勝ち鞍は何ですか?
Context: file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
** Messages: **
system: 
  The following is a friendly conversation between a user and an AI assistant.
  The assistant is talkative and provides lots of specific details from its context.
  If the assistant does not know the answer to a question, it truthfully says it
  does not know.

  Here are the relevant documents for the context:

  file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。

  Instruction: Based on the above documents, provide a detailed answer for the user question below.
  Answer "don't know" if not present in the document.
  
user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、以下の通りです:

1. 日本ダービー:ドウデュースは2013年に日本ダービーを制しました。この勝利により、鞍上の武豊騎手は史上初の50代でのダービージョッキーとなりました。

2. 朝日杯フューチュリティステークス:ドウデュースは朝日杯フューチュリティステークスでも勝利しています。

3. 京都記念:ドウデュースは日本ダービーを制した後、京都記念でも勝利しました。この勝利は、日本ダービー勝利馬が京都記念を制するのは、1948年のマツミドリ以来75年ぶりの快挙となりました。

また、ドウデュースはこれらの重賞勝利に加えて、GIを含む重賞3勝を挙げています。

以上がドウデュースの主な勝ち鞍です。
user: へえ、すごいですね
**************************************************
** Response: **
assistant: はい、ドウデュースは非常に優れた競走馬であり、その成績は素晴らしいものです。特に日本ダービーの勝利は、歴代最多勝利数を更新し、史上初の50代でのダービージョッキーとなった鞍上の武豊騎手にとっても名誉なことでした。また、朝日杯フューチュリティステークスや京都記念でも勝利し、重賞3勝を挙げています。

競馬界では、このような優れた成績を収める競走馬は非常に尊敬されます。ドウデュースの成功は、騎手や調教師、オーナーなど、多くの人々の努力と協力の結果です。彼らの熱意と才能が、ドウデュースの成功につながったのです。

ドウデュースの勝利は、競馬ファンにとっても喜ばしい出来事であり、競馬界における偉大な記録の一つとなりました。彼の活躍は、競馬の魅力と競走馬の可能性を示すものとして、多くの人々に感銘を与えました。
**************************************************



⇒ 回答: はい、ドウデュースは非常に優れた競走馬であり、その成績は素晴らしいものです。特に日本ダービーの勝利は、歴代最多勝利数を更新し、史上初の50代でのダービージョッキーとなった鞍上の武豊騎手にとっても名誉なことでした。また、朝日杯フューチュリティステークスや京都記念でも勝利し、重賞3勝を挙げています。競馬界では、このような優れた成績を収める競走馬は非常に尊敬されます。ドウデュースの成功は、騎手や調教師、オーナーなど、多くの人々の努力と協力の結果です。彼らの熱意と才能が、ドウデュースの成功につながったのです。ドウデュースの勝利は、競馬ファンにとっても喜ばしい出来事であり、競馬界における偉大な記録の一つとなりました。彼の活躍は、競馬の魅力と競走馬の可能性を示すものとして、多くの人々に感銘を与えました。

#### 3 回目

** Messages: **
user: 
  Given the following conversation between a user and an AI assistant and a follow up question from user,
  rephrase the follow up question to be a standalone question.

  Chat History:
  user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、以下の通りです:

1. 日本ダービー:ドウデュースは2013年に日本ダービーを制しました。この勝利により、鞍上の武豊騎手は史上初の50代でのダービージョッキーとなりました。

2. 朝日杯フューチュリティステークス:ドウデュースは朝日杯フューチュリティステークスでも勝利しています。

3. 京都記念:ドウデュースは日本ダービーを制した後、京都記念でも勝利しました。この勝利は、日本ダービー勝利馬が京都記念を制するのは、1948年のマツミドリ以来75年ぶりの快挙となりました。

また、ドウデュースはこれらの重賞勝利に加えて、GIを含む重賞3勝を挙げています。

以上がドウデュースの主な勝ち鞍です。
user: へえ、すごいですね
assistant: はい、ドウデュースは非常に優れた競走馬であり、その成績は素晴らしいものです。特に日本ダービーの勝利は、歴代最多勝利数を更新し、史上初の50代でのダービージョッキーとなった鞍上の武豊騎手にとっても名誉なことでした。また、朝日杯フューチュリティステークスや京都記念でも勝利し、重賞3勝を挙げています。

競馬界では、このような優れた成績を収める競走馬は非常に尊敬されます。ドウデュースの成功は、騎手や調教師、オーナーなど、多くの人々の努力と協力の結果です。彼らの熱意と才能が、ドウデュースの成功につながったのです。

ドウデュースの勝利は、競馬ファンにとっても喜ばしい出来事であり、競馬界における偉大な記録の一つとなりました。彼の活躍は、競馬の魅力と競走馬の可能性を示すものとして、多くの人々に感銘を与えました。
  Follow Up Input: そのうちGI勝利はいくつですか?
  Standalone question:
**************************************************
** Response: **
assistant: ドウデュースはGIを含む重賞でいくつ勝利していますか?
**************************************************


Condensed question: ドウデュースはGIを含む重賞でいくつ勝利していますか?
Context: file_path: data/ドウデュース.txt

1番人気に推されると、レースは直線でガイアフォースとの追い比べをクビ差制してデビュー勝ちを果たした。
次走はリステッド競走のアイビーステークスを選択。2番人気に推され、レースでは追い比べから抜け出すと、最後は追い込んできたグランシエロをクビ差凌いで優勝、デビュー2連勝とした。
続いて朝日杯フューチュリティステークスに出走。重賞勝ち馬セリフォスやジオグリフをはじめとした自身と同じ無敗馬が多く顔を揃える中、3番人気に支持される。レースでは直線で外に出すと、先に抜け出していたセリフォスを半馬身差で差し切り優勝、無傷3連勝でGI初制覇を果たした。鞍上の武豊はこの競走22回目の挑戦で初制覇となり、日本の中央競馬 (JRA) の平地GI完全制覇までホープフルステークスを残すのみとした。

file_path: data/ドウデュース.txt

また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となった。


=== 3歳(2022年) ===
3歳初戦として、弥生賞ディープインパクト記念に出走。単勝オッズ2.2倍の1番人気に推された。道中は勝ち馬アスクビクターモアを見る形で追走。残り800メートル過ぎに後方からロジハービンが一気に進出したため、いったんポジションを下げる。そこから立て直し、ゴール前では勝ち馬を懸命に追い上げたがクビ差届かず2着に。デビューからの連勝は3でストップした。
続いて、4月17日に行われた皐月賞に出走。1番人気には推されたものの単勝のオッズは3.9倍で、これは1984年のグレード制導入以降、1990年アイネスフウジンの4.1倍に次ぐ皐月賞1番人気の低支持率オッズであった。
** Messages: **
system: 
  The following is a friendly conversation between a user and an AI assistant.
  The assistant is talkative and provides lots of specific details from its context.
  If the assistant does not know the answer to a question, it truthfully says it
  does not know.

  Here are the relevant documents for the context:

  file_path: data/ドウデュース.txt

1番人気に推されると、レースは直線でガイアフォースとの追い比べをクビ差制してデビュー勝ちを果たした。
次走はリステッド競走のアイビーステークスを選択。2番人気に推され、レースでは追い比べから抜け出すと、最後は追い込んできたグランシエロをクビ差凌いで優勝、デビュー2連勝とした。
続いて朝日杯フューチュリティステークスに出走。重賞勝ち馬セリフォスやジオグリフをはじめとした自身と同じ無敗馬が多く顔を揃える中、3番人気に支持される。レースでは直線で外に出すと、先に抜け出していたセリフォスを半馬身差で差し切り優勝、無傷3連勝でGI初制覇を果たした。鞍上の武豊はこの競走22回目の挑戦で初制覇となり、日本の中央競馬 (JRA) の平地GI完全制覇までホープフルステークスを残すのみとした。

file_path: data/ドウデュース.txt

また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となった。


=== 3歳(2022年) ===
3歳初戦として、弥生賞ディープインパクト記念に出走。単勝オッズ2.2倍の1番人気に推された。道中は勝ち馬アスクビクターモアを見る形で追走。残り800メートル過ぎに後方からロジハービンが一気に進出したため、いったんポジションを下げる。そこから立て直し、ゴール前では勝ち馬を懸命に追い上げたがクビ差届かず2着に。デビューからの連勝は3でストップした。
続いて、4月17日に行われた皐月賞に出走。1番人気には推されたものの単勝のオッズは3.9倍で、これは1984年のグレード制導入以降、1990年アイネスフウジンの4.1倍に次ぐ皐月賞1番人気の低支持率オッズであった。

  Instruction: Based on the above documents, provide a detailed answer for the user question below.
  Answer "don't know" if not present in the document.
  
user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、以下の通りです:

1. 日本ダービー:ドウデュースは2013年に日本ダービーを制しました。この勝利により、鞍上の武豊騎手は史上初の50代でのダービージョッキーとなりました。

2. 朝日杯フューチュリティステークス:ドウデュースは朝日杯フューチュリティステークスでも勝利しています。

3. 京都記念:ドウデュースは日本ダービーを制した後、京都記念でも勝利しました。この勝利は、日本ダービー勝利馬が京都記念を制するのは、1948年のマツミドリ以来75年ぶりの快挙となりました。

また、ドウデュースはこれらの重賞勝利に加えて、GIを含む重賞3勝を挙げています。

以上がドウデュースの主な勝ち鞍です。
user: へえ、すごいですね
assistant: はい、ドウデュースは非常に優れた競走馬であり、その成績は素晴らしいものです。特に日本ダービーの勝利は、歴代最多勝利数を更新し、史上初の50代でのダービージョッキーとなった鞍上の武豊騎手にとっても名誉なことでした。また、朝日杯フューチュリティステークスや京都記念でも勝利し、重賞3勝を挙げています。

競馬界では、このような優れた成績を収める競走馬は非常に尊敬されます。ドウデュースの成功は、騎手や調教師、オーナーなど、多くの人々の努力と協力の結果です。彼らの熱意と才能が、ドウデュースの成功につながったのです。

ドウデュースの勝利は、競馬ファンにとっても喜ばしい出来事であり、競馬界における偉大な記録の一つとなりました。彼の活躍は、競馬の魅力と競走馬の可能性を示すものとして、多くの人々に感銘を与えました。
user: そのうちGI勝利はいくつですか?
**************************************************
** Response: **
assistant: ドウデュースのGI勝利は、朝日杯フューチュリティステークスが初めてのGI制覇となります。このレースでは、無敗馬たちが顔を揃える中、3番人気に支持されながらも見事に勝利しました。この勝利は、ドウデュースの無傷の3連勝であり、GI初制覇となりました。

したがって、ドウデュースのGI勝利は1つです。
**************************************************



⇒ 回答: ドウデュースのGI勝利は、朝日杯フューチュリティステークスが初めてのGI制覇となります。このレースでは、無敗馬たちが顔を揃える中、3番人気に支持されながらも見事に勝利しました。この勝利は、ドウデュースの無傷の3連勝であり、GI初制覇となりました。したがって、ドウデュースのGI勝利は1つです。

condense_questionとは違って、インデックス検索用にクエリが書き直されるものの、会話履歴としては元のクエリがそのまま維持されるので、会話として違和感はない。やっぱりcondense_questionの使い所がピンとこないな。

kun432kun432

chat_mode: simple

  • simple
    • LLMと直接チャットするシンプルなもの
    • Query Engineは関係ない。
出力内容
### MODE: SIMPLE

#### 1 回目

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: ドウデュースは、競走馬の名前ではなく、テニスの用語です。テニスの試合で、スコアが40-40になった状態を「デュース」と呼びます。その後のポイントを取ったプレーヤーが「アドバンテージ」と呼ばれる有利な状態になります。

したがって、「ドウデュースの主な勝ち鞍」という表現は、テニスの試合には適用されません。テニスの試合では、プレーヤーがセットや試合を制するためにポイントを獲得する必要があります。

もし、他の競技やスポーツに関する情報をお求めであれば、具体的な競技やスポーツの名前を教えていただければ、お手伝いできるかもしれません。
**************************************************



⇒ 回答: ドウデュースは、競走馬の名前ではなく、テニスの用語です。テニスの試合で、スコアが40-40になった状態を「デュース」と呼びます。その後のポイントを取ったプレーヤーが「アドバンテージ」と呼ばれる有利な状態になります。したがって、「ドウデュースの主な勝ち鞍」という表現は、テニスの試合には適用されません。テニスの試合では、プレーヤーがセットや試合を制するためにポイントを獲得する必要があります。もし、他の競技やスポーツに関する情報をお求めであれば、具体的な競技やスポーツの名前を教えていただければ、お手伝いできるかもしれません。

#### 2 回目

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースは、競走馬の名前ではなく、テニスの用語です。テニスの試合で、スコアが40-40になった状態を「デュース」と呼びます。その後のポイントを取ったプレーヤーが「アドバンテージ」と呼ばれる有利な状態になります。

したがって、「ドウデュースの主な勝ち鞍」という表現は、テニスの試合には適用されません。テニスの試合では、プレーヤーがセットや試合を制するためにポイントを獲得する必要があります。

もし、他の競技やスポーツに関する情報をお求めであれば、具体的な競技やスポーツの名前を教えていただければ、お手伝いできるかもしれません。
user: へえ、すごいですね
**************************************************
** Response: **
assistant: ありがとうございます!テニスは世界中で人気のあるスポーツであり、プロの試合や大会も盛んに行われています。競技のルールや戦略、プレーヤーの技術など、テニスには魅力的な要素がたくさんあります。もしテニスに興味があるのであれば、ぜひ試合を観戦したり、自分でプレーしてみることをおすすめします!
**************************************************



⇒ 回答: ありがとうございます!テニスは世界中で人気のあるスポーツであり、プロの試合や大会も盛んに行われています。競技のルールや戦略、プレーヤーの技術など、テニスには魅力的な要素がたくさんあります。もしテニスに興味があるのであれば、ぜひ試合を観戦したり、自分でプレーしてみることをおすすめします!

#### 3 回目

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースは、競走馬の名前ではなく、テニスの用語です。テニスの試合で、スコアが40-40になった状態を「デュース」と呼びます。その後のポイントを取ったプレーヤーが「アドバンテージ」と呼ばれる有利な状態になります。

したがって、「ドウデュースの主な勝ち鞍」という表現は、テニスの試合には適用されません。テニスの試合では、プレーヤーがセットや試合を制するためにポイントを獲得する必要があります。

もし、他の競技やスポーツに関する情報をお求めであれば、具体的な競技やスポーツの名前を教えていただければ、お手伝いできるかもしれません。
user: へえ、すごいですね
assistant: ありがとうございます!テニスは世界中で人気のあるスポーツであり、プロの試合や大会も盛んに行われています。競技のルールや戦略、プレーヤーの技術など、テニスには魅力的な要素がたくさんあります。もしテニスに興味があるのであれば、ぜひ試合を観戦したり、自分でプレーしてみることをおすすめします!
user: そのうちGI勝利はいくつですか?
**************************************************
** Response: **
assistant: 申し訳ありませんが、私は具体的な競走馬やテニスプレーヤーの情報を持っていません。また、私はリアルタイムの情報を提供することはできません。競走馬のGI勝利の数やテニスプレーヤーの成績については、公式の競馬やテニスのウェブサイトや専門のスポーツニュースサイトなどで確認することをおすすめします。そちらで最新の情報を入手することができます。申し訳ありませんが、お手伝いできずに残念です。
**************************************************



⇒ 回答: 申し訳ありませんが、私は具体的な競走馬やテニスプレーヤーの情報を持っていません。また、私はリアルタイムの情報を提供することはできません。競走馬のGI勝利の数やテニスプレーヤーの成績については、公式の競馬やテニスのウェブサイトや専門のスポーツニュースサイトなどで確認することをおすすめします。そちらで最新の情報を入手することができます。申し訳ありませんが、お手伝いできずに残念です。

これは完全にインデックス検索せずにLLMとやりとりするだけ。会話履歴だけは管理されている。

kun432kun432

chat_mode: react

  • react
    • bestと同じ
    • ReAct Data Agentを強制する
出力内容
### MODE: REACT

#### 1 回目

** Messages: **
system: 
You are designed to help with a variety of tasks, from answering questions     to providing summaries to other types of analyses.

## Tools
You have access to a wide variety of tools. You are responsible for using
the tools in any sequence you deem appropriate to complete the task at hand.
This may require breaking the task into subtasks and using different tools
to complete each subtask.

You have access to the following tools:
> Tool Name: query_engine_tool
Tool Description: Useful for running a natural language query
against a knowledge base and get back a natural language response.

Tool Args: {'title': 'DefaultToolFnSchema', 'description': 'Default tool function Schema.', 'type': 'object', 'properties': {'input': {'title': 'Input', 'type': 'string'}}, 'required': ['input']}


## Output Format
To answer the question, please use the following format.

```
Thought: I need to use a tool to help me answer the question.
Action: tool name (one of query_engine_tool) if using a tool.
Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {"input": "hello world", "num_beams": 5})
```

Please ALWAYS start with a Thought.

Please use a valid JSON format for the Action Input. Do NOT do this {'input': 'hello world', 'num_beams': 5}.

If this format is used, the user will respond in the following format:

```
Observation: tool response
```

You should keep repeating the above format until you have enough information
to answer the question without using any more tools. At that point, you MUST respond
in the one of the following two formats:

```
Thought: I can answer without using any more tools.
Answer: [your answer here]
```

```
Thought: I cannot answer the question with the provided tools.
Answer: Sorry, I cannot answer your query.
```

## Current Conversation
Below is the current conversation consisting of interleaving human and assistant messages.


user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: Thought: I need to use a tool to help me answer the question.
Action: query_engine_tool
Action Input: {"input": "ドウデュースの主な勝ち鞍は何ですか?"}
**************************************************


Thought: I need to use a tool to help me answer the question.
Action: query_engine_tool
Action Input: {'input': 'ドウデュースの主な勝ち鞍は何ですか?'}
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍は何ですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
**************************************************


Observation: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
** Messages: **
system: 
You are designed to help with a variety of tasks, from answering questions     to providing summaries to other types of analyses.

## Tools
You have access to a wide variety of tools. You are responsible for using
the tools in any sequence you deem appropriate to complete the task at hand.
This may require breaking the task into subtasks and using different tools
to complete each subtask.

You have access to the following tools:
> Tool Name: query_engine_tool
Tool Description: Useful for running a natural language query
against a knowledge base and get back a natural language response.

Tool Args: {'title': 'DefaultToolFnSchema', 'description': 'Default tool function Schema.', 'type': 'object', 'properties': {'input': {'title': 'Input', 'type': 'string'}}, 'required': ['input']}


## Output Format
To answer the question, please use the following format.

```
Thought: I need to use a tool to help me answer the question.
Action: tool name (one of query_engine_tool) if using a tool.
Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {"input": "hello world", "num_beams": 5})
```

Please ALWAYS start with a Thought.

Please use a valid JSON format for the Action Input. Do NOT do this {'input': 'hello world', 'num_beams': 5}.

If this format is used, the user will respond in the following format:

```
Observation: tool response
```

You should keep repeating the above format until you have enough information
to answer the question without using any more tools. At that point, you MUST respond
in the one of the following two formats:

```
Thought: I can answer without using any more tools.
Answer: [your answer here]
```

```
Thought: I cannot answer the question with the provided tools.
Answer: Sorry, I cannot answer your query.
```

## Current Conversation
Below is the current conversation consisting of interleaving human and assistant messages.


user: ドウデュースの主な勝ち鞍について教えて。
assistant: Thought: I need to use a tool to help me answer the question.
Action: query_engine_tool
Action Input: {'input': 'ドウデュースの主な勝ち鞍は何ですか?'}
user: Observation: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
**************************************************
** Response: **
assistant: Thought: I can answer without using any more tools.
Answer: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
**************************************************


Thought: I can answer without using any more tools.
Response: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。

⇒ 回答: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。

#### 2 回目

** Messages: **
system: 
You are designed to help with a variety of tasks, from answering questions     to providing summaries to other types of analyses.

## Tools
You have access to a wide variety of tools. You are responsible for using
the tools in any sequence you deem appropriate to complete the task at hand.
This may require breaking the task into subtasks and using different tools
to complete each subtask.

You have access to the following tools:
> Tool Name: query_engine_tool
Tool Description: Useful for running a natural language query
against a knowledge base and get back a natural language response.

Tool Args: {'title': 'DefaultToolFnSchema', 'description': 'Default tool function Schema.', 'type': 'object', 'properties': {'input': {'title': 'Input', 'type': 'string'}}, 'required': ['input']}


## Output Format
To answer the question, please use the following format.

```
Thought: I need to use a tool to help me answer the question.
Action: tool name (one of query_engine_tool) if using a tool.
Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {"input": "hello world", "num_beams": 5})
```

Please ALWAYS start with a Thought.

Please use a valid JSON format for the Action Input. Do NOT do this {'input': 'hello world', 'num_beams': 5}.

If this format is used, the user will respond in the following format:

```
Observation: tool response
```

You should keep repeating the above format until you have enough information
to answer the question without using any more tools. At that point, you MUST respond
in the one of the following two formats:

```
Thought: I can answer without using any more tools.
Answer: [your answer here]
```

```
Thought: I cannot answer the question with the provided tools.
Answer: Sorry, I cannot answer your query.
```

## Current Conversation
Below is the current conversation consisting of interleaving human and assistant messages.


user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
user: へえ、すごいですね
**************************************************
** Response: **
assistant: Thought: I can answer without using any more tools.
Answer: Yes, Doudeuse's achievements are indeed impressive. Winning races like the Japanese Derby, Asahi Hai Futurity Stakes, and the Kyoto Kinen is a testament to the horse's talent and success.
**************************************************


Thought: I can answer without using any more tools.
Response: Yes, Doudeuse's achievements are indeed impressive. Winning races like the Japanese Derby, Asahi Hai Futurity Stakes, and the Kyoto Kinen is a testament to the horse's talent and success.

⇒ 回答: Yes, Doudeuse's achievements are indeed impressive. Winning races like the Japanese Derby, Asahi Hai Futurity Stakes, and the Kyoto Kinen is a testament to the horse's talent and success.

#### 3 回目

** Messages: **
system: 
You are designed to help with a variety of tasks, from answering questions     to providing summaries to other types of analyses.

## Tools
You have access to a wide variety of tools. You are responsible for using
the tools in any sequence you deem appropriate to complete the task at hand.
This may require breaking the task into subtasks and using different tools
to complete each subtask.

You have access to the following tools:
> Tool Name: query_engine_tool
Tool Description: Useful for running a natural language query
against a knowledge base and get back a natural language response.

Tool Args: {'title': 'DefaultToolFnSchema', 'description': 'Default tool function Schema.', 'type': 'object', 'properties': {'input': {'title': 'Input', 'type': 'string'}}, 'required': ['input']}


## Output Format
To answer the question, please use the following format.

```
Thought: I need to use a tool to help me answer the question.
Action: tool name (one of query_engine_tool) if using a tool.
Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {"input": "hello world", "num_beams": 5})
```

Please ALWAYS start with a Thought.

Please use a valid JSON format for the Action Input. Do NOT do this {'input': 'hello world', 'num_beams': 5}.

If this format is used, the user will respond in the following format:

```
Observation: tool response
```

You should keep repeating the above format until you have enough information
to answer the question without using any more tools. At that point, you MUST respond
in the one of the following two formats:

```
Thought: I can answer without using any more tools.
Answer: [your answer here]
```

```
Thought: I cannot answer the question with the provided tools.
Answer: Sorry, I cannot answer your query.
```

## Current Conversation
Below is the current conversation consisting of interleaving human and assistant messages.


user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
user: へえ、すごいですね
assistant: Yes, Doudeuse's achievements are indeed impressive. Winning races like the Japanese Derby, Asahi Hai Futurity Stakes, and the Kyoto Kinen is a testament to the horse's talent and success.
user: そのうちGI勝利はいくつですか?
**************************************************
** Response: **
assistant: Thought: I need to use a tool to help me answer the question.
Action: query_engine_tool
Action Input: {"input": "DoudeuseのGI勝利数はいくつですか?"}
**************************************************


Thought: I need to use a tool to help me answer the question.
Action: query_engine_tool
Action Input: {'input': 'DoudeuseのGI勝利数はいくつですか?'}
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となった。


=== 3歳(2022年) ===
3歳初戦として、弥生賞ディープインパクト記念に出走。単勝オッズ2.2倍の1番人気に推された。道中は勝ち馬アスクビクターモアを見る形で追走。残り800メートル過ぎに後方からロジハービンが一気に進出したため、いったんポジションを下げる。そこから立て直し、ゴール前では勝ち馬を懸命に追い上げたがクビ差届かず2着に。デビューからの連勝は3でストップした。
続いて、4月17日に行われた皐月賞に出走。1番人気には推されたものの単勝のオッズは3.9倍で、これは1984年のグレード制導入以降、1990年アイネスフウジンの4.1倍に次ぐ皐月賞1番人気の低支持率オッズであった。

file_path: data/イクイノックス.txt

GI6連勝はグレード制が導入された1984年以降では、テイエムオペラオー、ロードカナロアのGI競走の連勝記録に並んだ。芝平地の古馬GⅠ6勝は日本馬歴代最多タイ。木村哲也調教師は本競走初制覇。また、クリストフ・ルメールは武豊と並ぶジャパンカップ最多4勝目。そのほかにも、3着に5番人気のスターズオンアースが入ったため、3連単のオッズが11.3倍となったのは、第40回を制したアーモンドアイと2着馬コントレイル、3着馬デアリングタクトの13.4倍を更新し、歴代JRA・GIにおける最低額配当となった他、イクイノックス、リバティアイランドのワイドのオッズが1.3倍となり、2000年の天皇賞(春)を制したテイエムオペラオーと2着馬ラスカルスズカ、3着馬ナリタトップロードなどと並び、ワイドの低額配当タイの記録となった。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: DoudeuseのGI勝利数はいくつですか?
Answer: 
**************************************************
** Response: **
assistant: DoudeuseのGI勝利数は1です。
**************************************************


Observation: DoudeuseのGI勝利数は1です。
** Messages: **
system: 
You are designed to help with a variety of tasks, from answering questions     to providing summaries to other types of analyses.

## Tools
You have access to a wide variety of tools. You are responsible for using
the tools in any sequence you deem appropriate to complete the task at hand.
This may require breaking the task into subtasks and using different tools
to complete each subtask.

You have access to the following tools:
> Tool Name: query_engine_tool
Tool Description: Useful for running a natural language query
against a knowledge base and get back a natural language response.

Tool Args: {'title': 'DefaultToolFnSchema', 'description': 'Default tool function Schema.', 'type': 'object', 'properties': {'input': {'title': 'Input', 'type': 'string'}}, 'required': ['input']}


## Output Format
To answer the question, please use the following format.

```
Thought: I need to use a tool to help me answer the question.
Action: tool name (one of query_engine_tool) if using a tool.
Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {"input": "hello world", "num_beams": 5})
```

Please ALWAYS start with a Thought.

Please use a valid JSON format for the Action Input. Do NOT do this {'input': 'hello world', 'num_beams': 5}.

If this format is used, the user will respond in the following format:

```
Observation: tool response
```

You should keep repeating the above format until you have enough information
to answer the question without using any more tools. At that point, you MUST respond
in the one of the following two formats:

```
Thought: I can answer without using any more tools.
Answer: [your answer here]
```

```
Thought: I cannot answer the question with the provided tools.
Answer: Sorry, I cannot answer your query.
```

## Current Conversation
Below is the current conversation consisting of interleaving human and assistant messages.


user: ドウデュースの主な勝ち鞍について教えて。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目の京都記念です。
user: へえ、すごいですね
assistant: Yes, Doudeuse's achievements are indeed impressive. Winning races like the Japanese Derby, Asahi Hai Futurity Stakes, and the Kyoto Kinen is a testament to the horse's talent and success.
user: そのうちGI勝利はいくつですか?
assistant: Thought: I need to use a tool to help me answer the question.
Action: query_engine_tool
Action Input: {'input': 'DoudeuseのGI勝利数はいくつですか?'}
user: Observation: DoudeuseのGI勝利数は1です。
**************************************************
** Response: **
assistant: Thought: I can answer without using any more tools.
Answer: DoudeuseのGI勝利数は1です。
**************************************************


Thought: I can answer without using any more tools.
Response: DoudeuseのGI勝利数は1です。

⇒ 回答: DoudeuseのGI勝利数は1です。

bestと仕組みは同じだけど、ReActフレームワークを強制的に使うというもの。

そういえばbestはReAct or OpenAI Agentのどちらかを必要ならツールとして使う、というものだったけど、ReActになっていなかったな。こういうケースを考えると強制化したいユースケースはあるかもしれない。

kun432kun432

chat_mode: openai

  • openai
    • bestと同じ
    • OpenAI Data Agentを強制する
出力内容
### MODE: OPENAI

#### 1 回目

Added user message to memory: ドウデュースの主な勝ち鞍について教えて。
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
**************************************************
** Response: **
assistant: None
**************************************************


=== Calling Function ===
Calling function: query_engine_tool with args: {
  "input": "ドウデュースの主な勝ち鞍は何ですか?"
}
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。
6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。
次走として凱旋門賞への出走を表明した。

file_path: data/ドウデュース.txt

まずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。
次走は3月25日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースの主な勝ち鞍は何ですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
**************************************************


Got output: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
========================

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
**************************************************
** Response: **
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および京都記念です。
**************************************************



⇒ 回答: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および京都記念です。

#### 2 回目

Added user message to memory: へえ、すごいですね
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および京都記念です。
user: へえ、すごいですね
**************************************************
** Response: **
assistant: はい、ドウデュースはこれらのレースで優れたパフォーマンスを見せました。これらのレースは日本の競馬界で非常に重要なレースであり、優れた競走馬が集まる競争です。ドウデュースがこれらのレースで勝利を収めたことは、その能力と実績の証です。
**************************************************



⇒ 回答: はい、ドウデュースはこれらのレースで優れたパフォーマンスを見せました。これらのレースは日本の競馬界で非常に重要なレースであり、優れた競走馬が集まる競争です。ドウデュースがこれらのレースで勝利を収めたことは、その能力と実績の証です。

#### 3 回目

Added user message to memory: そのうちGI勝利はいくつですか?
** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および京都記念です。
user: へえ、すごいですね
assistant: はい、ドウデュースはこれらのレースで優れたパフォーマンスを見せました。これらのレースは日本の競馬界で非常に重要なレースであり、優れた競走馬が集まる競争です。ドウデュースがこれらのレースで勝利を収めたことは、その能力と実績の証です。
user: そのうちGI勝利はいくつですか?
**************************************************
** Response: **
assistant: None
**************************************************


=== Calling Function ===
Calling function: query_engine_tool with args: {
  "input": "ドウデュースのGI勝利はいくつですか?"
}
** Messages: **
system: You are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
user: Context information is below.
---------------------
file_path: data/ドウデュース.txt

また馬主である松島及びキーファーズにとっては初の単独所有馬によるGI勝利、並びに国内GI初制覇となった。


=== 3歳(2022年) ===
3歳初戦として、弥生賞ディープインパクト記念に出走。単勝オッズ2.2倍の1番人気に推された。道中は勝ち馬アスクビクターモアを見る形で追走。残り800メートル過ぎに後方からロジハービンが一気に進出したため、いったんポジションを下げる。そこから立て直し、ゴール前では勝ち馬を懸命に追い上げたがクビ差届かず2着に。デビューからの連勝は3でストップした。
続いて、4月17日に行われた皐月賞に出走。1番人気には推されたものの単勝のオッズは3.9倍で、これは1984年のグレード制導入以降、1990年アイネスフウジンの4.1倍に次ぐ皐月賞1番人気の低支持率オッズであった。

file_path: data/ドウデュース.txt

1番人気に推されると、レースは直線でガイアフォースとの追い比べをクビ差制してデビュー勝ちを果たした。
次走はリステッド競走のアイビーステークスを選択。2番人気に推され、レースでは追い比べから抜け出すと、最後は追い込んできたグランシエロをクビ差凌いで優勝、デビュー2連勝とした。
続いて朝日杯フューチュリティステークスに出走。重賞勝ち馬セリフォスやジオグリフをはじめとした自身と同じ無敗馬が多く顔を揃える中、3番人気に支持される。レースでは直線で外に出すと、先に抜け出していたセリフォスを半馬身差で差し切り優勝、無傷3連勝でGI初制覇を果たした。鞍上の武豊はこの競走22回目の挑戦で初制覇となり、日本の中央競馬 (JRA) の平地GI完全制覇までホープフルステークスを残すのみとした。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ドウデュースのGI勝利はいくつですか?
Answer: 
**************************************************
** Response: **
assistant: ドウデュースのGI勝利は1回です。
**************************************************


Got output: ドウデュースのGI勝利は1回です。
========================

** Messages: **
user: ドウデュースの主な勝ち鞍について教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および重賞3勝目である京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、および京都記念です。
user: へえ、すごいですね
assistant: はい、ドウデュースはこれらのレースで優れたパフォーマンスを見せました。これらのレースは日本の競馬界で非常に重要なレースであり、優れた競走馬が集まる競争です。ドウデュースがこれらのレースで勝利を収めたことは、その能力と実績の証です。
user: そのうちGI勝利はいくつですか?
assistant: None
tool: ドウデュースのGI勝利は1回です。
**************************************************
** Response: **
assistant: ドウデュースのGI勝利は1回です。
**************************************************


⇒ 回答: ドウデュースのGI勝利は1回です。

OpenAI Data Agentを強制化、つまり(必要なケースでは)Function Callingをかならず使うってことだね。

kun432kun432

Chat Engineの低レベルAPI

Query Engine同様、Chat Engineも低レベルAPIを使って書くことができる。高レベルAPIの場合は、

Note: While the high-level API optimizes for ease-of-use, it does NOT expose full range of configurability.

ということなので、各コンポーネントを細かく制御したい場合はこちらということになる。chat_mode: "condense_question"の場合を書いてみる。chat_mode: "condense_question"が使用しているChat Engine、CondenseQuestionChatEngineのコードはこれ。

https://github.com/run-llama/llama_index/blob/06b0e390fe1d2fab7c41ff63f6505810c15f8684/llama_index/chat_engine/condense_question.py

Query Engineも含めて、プロンプトを全部日本語に置き換えてみた。

from llama_index.prompts import PromptTemplate, ChatPromptTemplate
from llama_index.llms import ChatMessage, MessageRole
from llama_index.chat_engine.condense_question import CondenseQuestionChatEngine
from llama_index.query_engine import RetrieverQueryEngine

custom_text_qa_system_prompt = ChatMessage(
    content="""\
あなたは世界中で信頼されているQAシステムです。
事前知識ではなく、常に提供されたコンテキスト情報を使用してクエリに回答してください
従うべきいくつかのルール:
1. 回答内で指定されたコンテキストを直接参照しないでください。
2. 「コンテキストに基づいて、...」や「コンテキスト情報は...」、またはそれに類するような記述は避けてください。""",
    role=MessageRole.SYSTEM,
)

custom_text_qa_prompt_template_messages = [
    custom_text_qa_system_prompt,
    ChatMessage(
        content="""コンテキスト情報は以下のとおりです。
---------------------
{context_str}
---------------------
事前知識ではなくコンテキスト情報を考慮して、クエリに答えます。
Query: {query_str}
Answer: """,
        role=MessageRole.USER,
    ),
]

custom_chat_text_qa_prompt = ChatPromptTemplate(message_templates=custom_text_qa_prompt_template_messages)

# Indexは事前に作成済みとする
query_engine = index.as_query_engine(
    text_qa_template=custom_chat_text_qa_prompt,
)

custom_condense_question_prompt = PromptTemplate(
    """\
以下のHumanとAssistantの会話履歴(Chat History)と、それに続く、人からのメッセージ(Follow Up Message)に基づいて,
メッセージを、会話に関連するすべての文脈をとらえた独立した質問(Standalone question)に書き換えてください。

<Chat History>
{chat_history}

<Follow Up Message>
{question}

<Standalone question>
"""
)

custom_chat_history = [
    ChatMessage(
        role=MessageRole.USER,
        content="こんにちは、今日は競馬についていろいろ語りましょう。",
    ),
    ChatMessage(role=MessageRole.ASSISTANT, content="わかりました。いいですね。では始めましょう。"),
]


chat_engine = CondenseQuestionChatEngine.from_defaults(
    query_engine=query_engine,
    condense_question_prompt=custom_condense_question_prompt,
    chat_history=custom_chat_history,
    verbose=False,
)

chat_engine.chat_repl()

結果、ちょっと間違っとる。

APIリファレンスはここ
https://docs.llamaindex.ai/en/stable/api_reference/query/chat_engines.html

今回使用したCondenseQuestionChatEnginefrom_defaults()を見るとわかるように、Chat Engineは、Query Engineをラップして、さらに会話履歴やメモリなどをチャットに必要なコンポーネントを追加したものということだな。

https://docs.llamaindex.ai/en/stable/api_reference/query/chat_engines/condense_question_chat_engine.html

Chat Engineのストリーミングレスポンス

chatメソッドではなくstream_chatメソッドを使えばよいだけ。

streaming_response = chat_engine.stream_chat("ドウデュースの主な勝ち鞍は?")

for token in streaming_response.response_gen:
    print(token, end="\n")
ド
ウ
デ
ュ
ース
の
主
な
勝
ち
鞍
は
、
日
本
ダ
ービ
ー
、
京
都
記
念
、
朝
日
杯
フ
ュ
ーチ
ュ
リ
テ
ィ
ス
テ
ー
ク
ス
です
。

ChatEngineのモジュール

https://docs.llamaindex.ai/en/stable/module_guides/deploying/chat_engines/modules.html

  • ReAct Chat Engine
  • OpenAI Chat Engine
  • Condense Question Chat Engine
  • Context Chat Engine
  • Context Plus Condense Chat Engine
  • Simple Chat Engine

説明やコードを見ていると、ReAct Chat EngineとOpenAI Chat Engineの2つはChatEngineとして実装しているというよりも、Data AgentにChat Engineのインタフェースをつけたような感じに見える。

チャット履歴の永続化(Chat Stores)

https://docs.llamaindex.ai/en/stable/module_guides/storing/chat_stores.html

Storingのところで少し触れたけども、会話履歴の永続化のためのStorageオブジェクトが2種類ある。

  • SimpleChatStore
    • オンメモリで会話履歴を保持
    • JSONファイルで永続化(インポート・エクスポート)可能
  • RedisChatStore
    • その名の通りRedisで会話履歴を保持

SimpleChatStoreを使ってみる。

from llama_index.storage.chat_store import SimpleChatStore
from llama_index.memory import ChatMemoryBuffer

chat_store = SimpleChatStore()

chat_memory = ChatMemoryBuffer.from_defaults(
    token_limit=3000,
    chat_store=chat_store,
    chat_store_key="user1",
)

chat_engine = index.as_chat_engine(memory=chat_memory)
chat_engine.chat_repl()

会話してみる。

===== Entering Chat REPL =====
Type "exit" to exit.

Human: ドウデュースの主な勝ち鞍を教えて。
Assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。

Human: 朝日杯フューチュリティステークスで騎乗していた騎手は誰?
Assistant: ドウデュースの朝日杯フューチュリティステークスでの騎手は戸崎圭太騎手でした。

Human: 朝日杯フューチュリティステークスは何番人気だった?
Assistant: ドウデュースの朝日杯フューチュリティステークスの人気は3番でした。

Human: exit

ファイルにエクスポートする

chat_store.persist(persist_path="chat_store.json")

中身はこんな感じ。ちょっとUnicodeデコードしたりゴニョゴニョしてるので、実際とはちょっと違いがあるけども。

{
    "store": {
        "chat_history": [
            {
                "role": "user",
                "content": "ドウデュースの主な勝ち鞍を教えて。",
                "additional_kwargs": {}
            },
            {
                "role": "assistant",
                "content": null,
                "additional_kwargs": {
                    "tool_calls": [
                        {
                            "id": "call_mCPlIwTE1jGVQCWK1mjqUEN6",
                            "function": {
                                "arguments": {
                                    "input": "ドウデュースの主な勝ち鞍は何ですか?"
                                },
                                "name": "query_engine_tool"
                            },
                            "type": "function"
                        }
                    ]
                }
            },
            {
                "role": "tool",
                "content": "ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。",
                "additional_kwargs": {
                    "name": "query_engine_tool",
                    "tool_call_id": "call_mCPlIwTE1jGVQCWK1mjqUEN6"
                }
            },
            {
                "role": "assistant",
                "content": "ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。",
                "additional_kwargs": {}
            },
            {
                "role": "user",
                "content": "朝日杯フューチュリティステークスで騎乗していた騎手は誰?",
                "additional_kwargs": {}
            },
            {
                "role": "assistant",
                "content": null,
                "additional_kwargs": {
                    "tool_calls": [
                        {
                            "id": "call_VDasPIQ0K0kMs8nhwYVf3v6l",
                            "function": {
                                "arguments": {
                                    "input": "ドウデュースの朝日杯フューチュリティステークスでの騎手は誰でしたか?"
                                },
                                "name": "query_engine_tool"
                            },
                            "type": "function"
                        }
                    ]
                }
            },
            {
                "role": "tool",
                "content": "戸崎圭太",
                "additional_kwargs": {
                    "name": "query_engine_tool",
                    "tool_call_id": "call_VDasPIQ0K0kMs8nhwYVf3v6l"
                }
            },
            {
                "role": "assistant",
                "content": "ドウデュースの朝日杯フューチュリティステークスでの騎手は戸崎圭太騎手でした。",
                "additional_kwargs": {}
            },
            {
                "role": "user",
                "content": "朝日杯フューチュリティステークスは何番人気だった?",
                "additional_kwargs": {}
            },
            {
                "role": "assistant",
                "content": null,
                "additional_kwargs": {
                    "tool_calls": [
                        {
                            "id": "call_te4UM7uSnxiplZ4guU3JIOEO",
                            "function": {
                                "arguments": {
                                    "input": "ドウデュースの朝日杯フューチュリティステークスの人気は何番でしたか?"
                                },
                                "name": "query_engine_tool"
                            },
                            "type": "function"
                        }
                    ]
                }
            },
            {
                "role": "tool",
                "content": "ドウデュースの朝日杯フューチュリティステークスの人気は3番でした。",
                "additional_kwargs": {
                    "name": "query_engine_tool",
                    "tool_call_id": "call_te4UM7uSnxiplZ4guU3JIOEO"
                }
            },
            {
                "role": "assistant",
                "content": "ドウデュースの朝日杯フューチュリティステークスの人気は3番でした。",
                "additional_kwargs": {}
            }
        ]
    },
    "class_name": "SimpleChatStore"
}

呼び出す場合はこう。一応事前にチャット履歴を消して試してみる。

chat_engine.reset()
loaded_chat_store = SimpleChatStore.from_persist_path(
    persist_path="chat_store.json"
)

chat_memory = ChatMemoryBuffer.from_defaults(
    token_limit=3000,
    chat_store=loaded_chat_store,
    chat_store_key="user1",
)

chat_engine = index.as_chat_engine(memory=chat_memory)

メモリの中身を見てみる。

for i in chat_engine.memory.get_all():
    print(i)

会話が読み出されているのがわかる。

user: ドウデュースの主な勝ち鞍を教えて。
assistant: None
tool: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
assistant: ドウデュースの主な勝ち鞍は、日本ダービー、朝日杯フューチュリティステークス、京都記念です。
user: 朝日杯フューチュリティステークスで騎乗していた騎手は誰?
assistant: None
tool: 戸崎圭太
assistant: ドウデュースの朝日杯フューチュリティステークスでの騎手は戸崎圭太騎手でした。
user: 朝日杯フューチュリティステークスは何番人気だった?
assistant: None
tool: ドウデュースの朝日杯フューチュリティステークスの人気は3番でした。
assistant: ドウデュースの朝日杯フューチュリティステークスの人気は3番でした。

続けて会話してみる。

response = chat_engine.chat("へぇ、すごいですね")
print(response)

ちゃんと続いている。

はい、ドウデュースは朝日杯フューチュリティステークスでの活躍が素晴らしかったです。人気のある競走で好成績を収めることは、競走馬にとって大きな成果です。ドウデュースの才能と騎手の技術が光りましたね。

LangChainに比べるとメモリのオプションが少ないのはちょっと残念なところ。

LangChainのToolとしてLlamaIndexは使えるので、いっそメモリについてはLangChain側のものを使うという方法もある。ただ学習コストは倍になるのがね。。。

https://github.com/run-llama/llama_index/blob/main/examples/langchain_demo/LangchainDemo.ipynb

kun432kun432

Data Agents

https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/root.html

いわゆる「エージェント」。「エージェント」の定義は難しいなと思っているけど、以下の公式ブログの記事もほうがわかりやすそう。

https://blog.llamaindex.ai/data-agents-eed797d7972f

データエージェントはLLMを搭載したナレッジワーカーであり、データに対して様々なタスクを "読み取り "と "書き込み "の両方の機能でインテリジェントに実行することができます。以下のようなことが可能です:

  • 非構造化データ、半構造化データ、構造化データなど、さまざまなタイプのデータに対して、自動検索を実行。
  • 任意の外部サービスAPIを構造化された方法で呼び出す。彼らはレスポンスを即座に処理するか、将来の使用のためにこのデータをインデックス化/キャッシュすることができる。
  • 会話履歴の保存
  • 上記のすべてを駆使して、単純なデータ・タスクから複雑なデータ・タスクまでこなす。


© 2023 Jerry Liu. LlamaIndex is released under the MIT license

データエージェントの構築には、以下のコアコンポーネントが必要である:

  • 推論ループ
  • ツールの抽象化

データエージェントは、APIのセット、または対話するためのツールを提供される。これらのAPIは、世界に関する情報を返したり、状態を変更するアクションを実行することができます。各ツールはリクエスト/レスポンス・インターフェースを公開する。リクエストは構造化されたパラメータのセットであり、レスポンスはどのような形式でもよい(少なくとも概念的には、ほとんどの場合、ここでのレスポンスは何らかの形式のテキスト文字列である)。

入力タスクが与えられると、データエージェントは推論ループを使用して、どのツールをどの順序で使用するか、各ツールを呼び出すパラメータを決定する。ループ」は概念的に非常に単純なもの(1ステップのツール選択プロセス)でも、複雑なもの(各ステップで多数のツールが選択される多ステップの選択プロセス)でも構わない。


© 2023 Jerry Liu. LlamaIndex is released under the MIT license

個人的には「推論ループ」はこういう感じか。

  • 自動で必要な検索を実行する
  • 自動で必要なAPIを呼び出す
  • 自動で上記の結果を処理する
  • 上記のツール群から、必要なツールを自動で選択、目的を達成するまで自動で繰り返し処理を行う。

で、例えばQuery Engineを使った検索だったり、外部APIだったり、はたまた別のエージェントだったり、というような、個々のタスクを抽象化したものが「ツール」と。

特定の小さな「入出力」を組み合わせて、大きな意味での「入出力」を得る、それらを自律的・自動的に行うのが「エージェント」ってことなんだろうなと思う。

Data Agentの使用パターン

https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/usage_pattern.html

ReActエージェントでツールを使う場合の例。ツールは2つの整数を乗算するというもの。

from llama_index.tools import FunctionTool
from llama_index.llms import OpenAI
from llama_index.agent import ReActAgent

# ツールで使用する関数の定義
def multiply(a: int, b: int) -> int:
    """Multiple two integers and returns the result integer"""
    return a * b

# 関数をツールとして登録
multiply_tool = FunctionTool.from_defaults(fn=multiply)

# LLM初期化
llm = OpenAI(model="gpt-3.5-turbo")

# ReAct エージェントを初期化、上記のツールを渡す
agent = ReActAgent.from_tools([multiply_tool], llm=llm, verbose=True)

ではクエリ。agentオブジェクトに対して、chat()だとChat Engine、query()だとQuery Engineの動きになるらしい。今回はquery()で。

response = agent.query("2123 かける 215123 は?")
print("回答: ", response)

結果。ReActで、ツールを選択して、ツールの定義に従って実行、結果を観察・判断して回答を返しているのがわかる。

Thought: I need to use the multiply tool to calculate the product of 2123 and 215123.
Action: multiply
Action Input: {'a': 2123, 'b': 215123}
Observation: 456706129
Thought: I have the result of multiplying 2123 by 215123.
Response: The product of 2123 and 215123 is 456706129.
回答:  The product of 2123 and 215123 is 456706129.

ツールとして定義されていないクエリを渡してみる。

response = agent.query("2123 たす 215123 は?")
print("回答: ", response)
Thought: The user wants to add 2123 and 215123.
Action: multiply
Action Input: {'a': 2123, 'b': 215123}
Observation: 456706129
Thought: I have the result of adding 2123 and 215123.
Response: The sum of 2123 and 215123 is 217246.
回答:  The sum of 2123 and 215123 is 217246.

ツールの選択は間違っているが、ReActによりツールの結果は使用せずにLLMで生成した結果を返している(と思われる)

ツールの定義

Module Guides: Tools
https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/tools/root.html#

ツールはAPIインタフェース定義と考えれば良い。ツールの定義は以下のような方法があるみたい。

FunctionTool

上でも書いた通り、関数をそのままツールとして定義して使う。

from llama_index.tools import FunctionTool
from llama_index.llms import OpenAI
from llama_index.agent import ReActAgent

def multiply(a: int, b: int) -> int:
    """Multiple two integers and returns the result integer"""
    return a * b

multiply_tool = FunctionTool.from_defaults(fn=multiply)

llm = OpenAI(model="gpt-3.5-turbo")

agent = ReActAgent.from_tools([multiply_tool], llm=llm, verbose=True)

response = agent.query("2123 ひく 215123 は?")
print("回答: ", response)

ちなみに、ツールを直接実行することもできる。

multiply_tool.call(2,4)
ToolOutput(content='8', tool_name='multiply', raw_input={'args': (2, 4), 'kwargs': {}}, raw_output=8)

ツールのメタデータを見てみる。

multiply_tool.metadata

関数の引数・戻り値の定義や、関数内のdocstringが返ってくる。

ToolMetadata(description='multiply(a: int, b: int) -> int\nMultiple two integers and returns the result integer.', name='multiply', fn_schema=<class 'pydantic.main.multiply'>)

OpenAIのFunction Callingの定義として出力することもできる

multiply_tool.metadata.to_openai_tool()
{
  'type': 'function',
  'function': {
    'name': 'multiply',
    'description': 'multiply(a: int, b: int) -> int\nMultiple two integers and returns the result integer.',
    'parameters': {
      'title': 'multiply',
      'type': 'object',
      'properties': {
        'a': {'title': 'A', 'type': 'integer'},
        'b': {'title': 'B', 'type': 'integer'}
      },
      'required': ['a', 'b']
    }
  }
}

エージェントはこれを使って、どのツールを使うかを判別している様子。

https://blog.llamaindex.ai/building-better-tools-for-llm-agents-f8c5a6714f11

We can see that the docstring describing how to use the Tool get passed to the Agent. Additionally, the parameters, type info and function name are passed along to give the Agent a strong idea on how it can use this function. All of this information is essentially acting as the prompt for how the agent understands the tool.

なので、関数の説明や型ヒントをきちんと設定しておくのが良さそう。

QueryEngineTool

Query Engineで作成したインデックス検索をツールとして使う。

複数のインデックスからそれぞれのクエリエンジンを作成。

from llama_index import VectorStoreIndex, ServiceContext
from llama_index.readers.file.flat_reader import FlatReader

from llama_index.text_splitter import SentenceSplitter
from llama_index.embeddings import OpenAIEmbedding
from llama_index.llms import OpenAI
from llama_index import set_global_handler

llm = OpenAI(model="gpt-3.5-turbo", temperarture=0)
embed_model = OpenAIEmbedding()
node_parser = SentenceSplitter(chunk_size=400, chunk_overlap=20)

service_context = ServiceContext.from_defaults(
  llm=llm,
  embed_model=embed_model,
  node_parser=node_parser,
)

reader = FlatReader()

docs_d = reader.load_data(Path("data/ドウデュース.txt"))
docs_e = reader.load_data(Path("data/イクイノックス.txt"))

index_d = VectorStoreIndex.from_documents(docs_d, service_context=service_context)
index_e = VectorStoreIndex.from_documents(docs_e, service_context=service_context)

query_engine_d = index_d.as_query_engine(similarity_top_k=5)
query_engine_e = index_e.as_query_engine(similarity_top_k=5)

それぞれのクエリエンジンをツールとして登録したReActエージェントを作成。

from llama_index.agent import ReActAgent
from llama_index.tools import QueryEngineTool, ToolMetadata
from llama_index.llms import OpenAI

query_engine_tools = [
    QueryEngineTool(
        query_engine=query_engine_d,
        metadata=ToolMetadata(
            name="query_engine_doduece",
            description="Provides information in Japanese about a race horse named 'Do Duece(ドウデュース)'"
            "Use a detailed plain text question as input to the tool.",
        ),
    ),
    QueryEngineTool(
        query_engine=query_engine_e,
        metadata=ToolMetadata(
            name="query_engine_equinox",
            description="Provides information in Japanese about a race horse named 'Equinox(イクイノックス)'"
            "Use a detailed plain text question as input to the tool.",
        ),
    ),
]

llm = OpenAI(model="gpt-3.5-turbo", temperature=0)

agent = ReActAgent.from_tools(query_engine_tools, llm=llm, verbose=True)

ではクエリ。

response = agent.query("ドウデュースの主な勝ち鞍は?")
Thought: I need to use a tool to help me answer the question.
Action: query_engine_doduece
Action Input: {'input': 'ドウデュースの主な勝ち鞍は?'}
Observation: ドウデュースの主な勝ち鞍は、日本ダービー、有馬記念、京都記念などです。
Thought: I can answer without using any more tools.
Response: ドウデュースの主な勝ち鞍は、日本ダービー、有馬記念、京都記念などです。
response = agent.query("イクイノックスの主な勝ち鞍は?")
Thought: I need to use a tool to help me answer the question.
Action: query_engine_equinox
Action Input: {'input': 'イクイノックスの主な勝ち鞍は?'}
Observation: イクイノックスの主な勝ち鞍は、天皇賞(秋)と有馬記念です。
Thought: I can answer without using any more tools.
Response: イクイノックスの主な勝ち鞍は、天皇賞(秋)と有馬記念です。

クエリに応じてそれぞれのツールが呼び出されて回答が生成されているのがわかる。

他のエージェントをツールとして使う。

最初に作成した、掛け算エージェントをツールとして組み込んでみる。

from llama_index.agent import ReActAgent
from llama_index.tools import FunctionTool, QueryEngineTool, ToolMetadata
from llama_index.llms import OpenAI


def multiply(a: int, b: int) -> int:
    """Multiple two integers and returns the result integer"""
    return a * b


multiply_tool = FunctionTool.from_defaults(fn=multiply)

llm = OpenAI(model="gpt-3.5-turbo", temperature=0)

multiply_agent = ReActAgent.from_tools([multiply_tool], llm=llm, verbose=True)

tools = [
    QueryEngineTool(
        query_engine=query_engine_d,
        metadata=ToolMetadata(
            name="query_engine_doduece",
            description="Provides information in Japanese about a race horse named 'Do Duece(ドウデュース)'"
            "Use a detailed plain text question as input to the tool.",
        ),
    ),
    QueryEngineTool(
        query_engine=query_engine_e,
        metadata=ToolMetadata(
            name="query_engine_equinox",
            description="Provides information in Japanese about a race horse named 'Equinox(イクイノックス)'"
            "Use a detailed plain text question as input to the tool.",
        ),
    ),
    QueryEngineTool(
        query_engine=multiply_agent,
        metadata=ToolMetadata(
            name="multiply_agent",
            description="Multiple two integers and returns the result integer."
        ),
    ),]


agent = ReActAgent.from_tools(tools, llm=llm, verbose=True)

そもそも掛け算エージェントは掛け算ツールをエージェント化したものなので、ツール自体をそのまま追加すればいいのだけど、エージェントをツールとして呼び出すためにあえて上記のように書いた。なお、

A nifty feature of our agents is that since they inherit from BaseQueryEngine, you can easily define other agents as tools through our QueryEngineTool.

らしいので、エージェントはQueryEngineToolとしてツールに登録する模様。

ではクエリ。

response = agent.query("ドウデュースの主な勝ち鞍は?")
Thought: I need to use a tool to help me answer the question.
Action: query_engine_doduece
Action Input: {'input': 'ドウデュースの主な勝ち鞍は?'}
Observation: ドウデュースの主な勝ち鞍は、日本ダービー、有馬記念、京都記念などです。
Thought: The query_engine_doduece tool provided the following information about Do Duece's major victories: Japanese Derby, Arima Kinen, Kyoto Kinen, and more.
Response: ドウデュースの主な勝ち鞍は、日本ダービー、有馬記念、京都記念などです。
response = agent.query("イクイノックスの主な勝ち鞍は?")
Thought: I need to use a tool to help me answer the question.
Action: query_engine_equinox
Action Input: {'input': 'イクイノックスの主な勝ち鞍は?'}
Observation: イクイノックスの主な勝ち鞍は、天皇賞(秋)と有馬記念です。
Thought: I can answer without using any more tools.
Response: イクイノックスの主な勝ち鞍は、天皇賞(秋)と有馬記念です。

ここまでは先程と同じ。掛け算エージェントを呼んでみる。

response = agent.query("2156 かける 256 は?")
Thought: I can use the multiply_agent tool to multiply 2156 by 256.
Action: multiply_agent
Action Input: {'input': '2156, 256'}
Thought: I need to multiply the numbers 2156 and 256.
Action: multiply
Action Input: {'a': 2156, 'b': 256}
Observation: 551936
Thought: I have multiplied 2156 and 256 and the result is 551936.
Response: The product of 2156 and 256 is 551936.
Observation: The product of 2156 and 256 is 551936.
Thought: I can answer without using any more tools.
Response: The product of 2156 and 256 is 551936.

元エージェント⇒掛け算エージェント⇒掛け算ツール、と呼び出されて、回答が生成されているのがわかる。

カスタムなツールを作成する

https://blog.llamaindex.ai/building-better-tools-for-llm-agents-f8c5a6714f11

上記の内容を写経してみる。Wolfram Alphaに数学的・科学的なクエリを投げて結果をえるツールの例。

こんな感じでTool Specを作成する。

from llama_index.tools.tool_spec.base import BaseToolSpec
from typing import Optional
import urllib

# Wolfram Alpha Short Answers APIのエンドポイントを定義
QUERY_URL_TMPL = "http://api.wolframalpha.com/v1/result?appid={app_id}&i={query}"

# BaseToolSpecを継承して、Wolfram Alpha用のToolSpecを定義
class WolframAlphaToolSpec(BaseToolSpec):

    # LLMに公開する関数名を定義
    spec_functions = ["wolfram_alpha_query"]

    # コンストラクタ。Wolfram AlphaのAPIキーで初期化する。
    def __init__(self, app_id: Optional[str] = None) -> None:
        """Initialize with parameters."""
        self.token = app_id
  
    # エージェントから呼ばれる関数の定義。多分docstringは英語の方がいいとは思う。
    def wolfram_alpha_query(self, query: str) -> str:
        """
        数学的・科学的な問題について、Wolfram Alphaにクエリを送信する。
        
        入力例:
            "(7 * 12 ^ 10) / 321"
            "1ポンドのいちごのカロリー"
  
        引数:
            query (str): Wolfram Alphaにに渡されるクエリ文字列
  
        """
        response = requests.get(QUERY_URL_TMPL.format(app_id=self.token, query=urllib.parse.quote_plus(query)))
        return response.text

ToolSpecからToolを作成する。

# Tool SpecからToolインスタンス初期化
wolfram_spec = WolframAlphaToolSpec(app_id="XXXXXXXXXX")

# Tool SpecをToolのリストに変換。今回は1つだけ。
tools = wolfram_spec.to_tool_list()

# ToolをOpenAI Function Callingの定義で出力して確認
for i in tools:
    print(i.metadata.to_openai_tool())
{
  'type': 'function',
  'function': {
    'name': 'wolfram_alpha_query',
    'description': 'wolfram_alpha_query(query: str) -> str\n\n        数学的・科学的な問題について、Wolfram Alphaにクエリを送信する。\n\n        入力例:\n            "(7 * 12 ^ 10) / 321"\n            "1ポンドのいちごのカロリー"\n\n        引数:\n            query (str): Wolfram Alphaにに渡されるクエリ文字列\n\n        ',
    'parameters': {
      'title': 'wolfram_alpha_query',
      'type': 'object',
      'properties': {
        'query': {
          'title': 'Query',
          'type': 'string'
        }
      },
      'required': ['query']
    }
  }
}

上記のツールを使って、エージェントを定義。

from llama_index.agent import ReActAgent
from llama_index.llms import OpenAI

agent = ReActAgent.from_tools(tools, llm=llm, verbose=True)

クエリを投げてみる

print(agent.query('(7 * 12 ^ 10) / 321 は?'))
Thought: The user wants to know the result of the expression (7 * 12 ^ 10) / 321.
Action: wolfram_alpha_query
Action Input: {'query': '(7 * 12 ^ 10) / 321'}
Observation: 144473849856/107
Thought: I have the result of the expression.
Response: The result of (7 * 12 ^ 10) / 321 is approximately 1,350,000,000.
The result of (7 * 12 ^ 10) / 321 is approximately 1,350,000,000.
print(agent.query("1ポンドのいちごのカロリー"))
Thought: The user is asking for the calorie content of 1 pound of strawberries. I can use the Wolfram Alpha tool to find this information.
Action: wolfram_alpha_query
Action Input: {'query': '1 pound of strawberries calorie'}
Observation: about 145 dietary Calories
Thought: I have obtained the answer to the user's question.
Response: The calorie content of 1 pound of strawberries is approximately 145 dietary Calories.
The calorie content of 1 pound of strawberries is approximately 145 dietary Calories.

良いツールの書き方については上で紹介したブログ記事を参照のこと。

kun432kun432

LlamaHubのツールを使う

https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/tools/llamahub_tools_guide.html

https://blog.llamaindex.ai/data-agents-eed797d7972f

ドキュメントを見る限り、LLamaHubを使ったエージェントツールの使い方には2種類ある。

  1. OnDemandLoaderTool
  2. LoadAndSearchToolSpec

それぞれ見てみる。

OnDemandLoaderTool

OnDemandLoaderToolの話をする前に、まず最初にLlamaHubで提供されているのは以下の4つ。

  1. Data Loaders
  • いわゆるReader。色々なデータソースからデータのロードを行う。
  1. Agent Tools
  • エージェントで使用するツール。
  1. Llama Packs
  • LlamaIndexを使ったRAGアプリケーションそのもの。
  1. Llama Datasets
  • 評価やテストなどに使えるデータセット。

LLamaHubのツールを利用するとなれば、Agent Toolsのことかと思うけど、OnDemandLoaderToolはData Loadersをツールに変換するためのものらしい。内部ではこういうことが行われる。


Source: Data Agents | by Jerry Liu | LlamaIndex Blog
© 2023 Jerry Liu. LlamaIndex is released under the MIT license

LlamaHubのWikipedia Loaderを使ってみる。サンプルコードが色々足りない感があるので、ミニマムで必要なコードをミニマムで書いてみた。

!pip install wikipedia
from llama_hub.wikipedia.base import WikipediaReader
from llama_index.tools.ondemand_loader_tool import OnDemandLoaderTool
from llama_index.llms import OpenAI
from llama_index.agent import OpenAIAgent

reader = WikipediaReader()

tool = OnDemandLoaderTool.from_defaults(
    reader,
    name="Wikipedia_Search",
    description="A tool for loading data and querying articles from Wikipedia",
)

llm = OpenAI(model="gpt-3.5-turbo", temperature=0)

agent = OpenAIAgent.from_tools([tool], llm=llm, verbose=True)

クエリ

print(agent.query("日本の競走馬、ドウデュースの主な勝ち鞍を教えて"))
Added user message to memory: 日本の競走馬、ドウデュースの主な勝ち鞍を教えて
=== Calling Function ===
Calling function: Wikipedia_Search with args: {
  "pages": ["ドウデュース"]
}
Got output: Error: Missing query_str in kwargs with parameter name: query_str
========================

=== Calling Function ===
Calling function: Wikipedia_Search with args: {
  "pages": ["ドウデュース"],
  "query_str": "競走成績"
}
Got output: Do Deuce had a successful racing career. As a two-year-old, he won three races, including the Ivy Stakes and the Asahi Hai Futurity Stakes. In his three-year-old season, he finished second in the Grade 2 Yayoi Sho and third in the Satsuki Sho before winning the Tokyo Yushun. As a four-year-old, he won the Kyoto Kinen and the Arima Kinen. He also competed in races in France, finishing fourth in the Prix Niel and 19th in the Prix de l'Arc de Triomphe. Overall, Do Deuce had a strong racing record.
========================

ドウデュースは、競走馬として成功したキャリアを持っています。2歳の時には、アイビーステークスや朝日杯フューチュリティステークスなど3つのレースに勝利しました。3歳のシーズンでは、Grade 2の弥生賞で2着、皐月賞で3着となり、東京優駿を制しました。4歳の時には、京都記念や有馬記念などを制覇しました。また、フランスのレースにも参戦し、プリ・ニエルで4着、凱旋門賞で19着となりました。総じて、ドウデュースは強力な競走成績を持っています。

イメージとしては、Readerを使ってデータをロード、OnDemandLoaderToolでインデックスとクエリエンジンを作成して、ツールとしての検索インタフェースを用意する、みたいなのをアドホックにやる感じかなと思った。

LoadAndSearchToolSpec

LoadAndSearchToolSpecは、既存のツールを入力としてつかい、tool specとして2つの関数、loadsearchを返す。loadはインデックスを返し、searchはクエリを受け取ってインデックスを検索する。


Source: Data Agents | by Jerry Liu | LlamaIndex Blog
© 2023 Jerry Liu. LlamaIndex is released under the MIT license

とりあえずこれ見ても全然意味がわからないw なんとなくだけど、tool specが複数の関数を返すことでエージェントがより細かく制御しやすくなる、のかなぁ???

でサンプルに従ってコードを書いてみたんだけど・・・

from llama_hub.tools.wikipedia.base import WikipediaToolSpec
from llama_index.tools.tool_spec.load_and_search.base import LoadAndSearchToolSpec
from llama_index.agent import OpenAIAgent

# Wikipedia用tool specからtool specインスタンスを初期化
wiki_spec = WikipediaToolSpec()

# tool specインスタンスからtoolを取り出し。loadとsearchのうちsearchだけを取り出しているように見える。
tool = wiki_spec.to_tool_list()[1]

# LoadAndSearchToolSpecでtoolをリスト化して、エージェントを作成
agent = OpenAIAgent.from_tools(
    LoadAndSearchToolSpec.from_defaults(tool).to_tool_list(), verbose=True
)
agent.query("ドウデュースの主な勝ち鞍を教えて")
Added user message to memory: ドウデュースの主な勝ち鞍を教えて
=== Calling Function ===
Calling function: search_data with args: {
  "query": "ドウデュース"
}
Got output: Error: 'str' object has no attribute 'get_doc_id'
========================

=== Calling Function ===
Calling function: search_data with args: {
  "query": "ドウデュース",
  "lang": "ja"
}
Got output: Error: 'str' object has no attribute 'get_doc_id'
========================

(snip)

=== Calling Function ===
Calling function: search_data with args: {
  "query": "ドウデュース",
  "lang": "ja"
}
Got output: Error: 'str' object has no attribute 'get_doc_id'
========================

Response(response='None', source_nodes=[], metadata=None)

んー、ちゃんと動かない・・・そもそも、カスタムなツール作ってたときのコードのイメージでWikipedia Tool Specの中身を覗いてみると、

tools = wiki_spec.to_tool_list()

for i in tools:
    print(i.metadata.to_openai_tool())
{
  'type': 'function',
  'function': {
    'name': 'load_data',
    'description': "load_data(page: str, lang: str = 'en', **load_kwargs: Dict[str, Any]) -> str\n\n        Retrieve a Wikipedia page. Useful for learning about a particular concept that isn't private information.\n\n        Args:\n            page (str): Title of the page to read.\n            lang (str): Language of Wikipedia to read. (default: English)\n        ",
    'parameters': {
      'title': 'load_data',
      'type': 'object',
      'properties': {
        'page': {'title': 'Page','type': 'string'},
        'lang': {'title': 'Lang', 'default': 'en', 'type': 'string'},
        'load_kwargs': {'title': 'Load Kwargs', 'type': 'object'}
      },
      'required': ['page', 'load_kwargs']
    }
  }
}
{
  'type': 'function',
  'function': {
    'name': 'search_data',
    'description': "search_data(query: str, lang: str = 'en') -> str\n\n        Search Wikipedia for a page related to the given query.\n        Use this tool when `load_data` returns no results.\n\n        Args:\n            query (str): the string to search for\n        ",
    'parameters': {
      'title': 'search_data',
      'type': 'object',
      'properties': {
        'query': {'title': 'Query', 'type': 'string'},
        'lang': {'title': 'Lang', 'default': 'en', 'type': 'string'}
      },
      'required': ['query']
    }
  }
}

って感じなので、

from llama_hub.tools.wikipedia.base import WikipediaToolSpec
from llama_index.agent import OpenAIAgent

wiki_spec = WikipediaToolSpec()
tools = wiki_spec.to_tool_list()

agent = OpenAIAgent.from_tools(tools, verbose=True)

だけでよいと思うんだよなー。クエリもちゃんと動いたし。

agent.query("日本の競走馬、ドウデュースの主な勝ち鞍を教えて")
Added user message to memory: 日本の競走馬、ドウデュースの主な勝ち鞍を教えて
=== Calling Function ===
Calling function: search_data with args: {
  "query": "ドウデュース"
}
Got output: Do Deuce (Japanese: ドウデュース, foaled 7 May 2019) is a Japanese Thoroughbred racehorse. He was the best two-year-old colt in Japan in 2021 when he was undefeated in three races including the Ivy Stakes and the Asahi Hai Futurity Stakes. (snip)

(snip)

== Pedigree ==
Do Deuce in inbred 4 × 4 to Lyphard, meaning that this stallion appears twice in the fourth generation of his pedigree.


== References ==
========================
Response(response='ドウデュースは、日本の競走馬であり、2019年に生まれました。彼は2021年に日本の最優秀2歳牡馬となり、アイビーステークスや朝日杯フューチュリティステークスなど3つのレースで無敗の成績を収めました。彼の2年目のキャンペーンのハイライトは、東京優駿でエクイノックスを破って優勝したことです。2023年の4歳シーズンには、2月の京都記念と年末の有馬記念を制しました。\n\n参考文献: Wikipedia', source_nodes=[], metadata=None)

気になったのは、上のverboseでは少し省略してしまっているけど、該当のページをまるっと渡して回答生成してるっぽく見えてて、チャンク化&インデックス化したデータから検索されている気がしない。これまさにドキュメントに書いてある状態だよなぁと思うのだけども。

This is helpful for any API endpoint that will by default return large volumes of data - for instance our WikipediaToolSpec will by default return entire Wikipedia pages, which will easily overflow most LLM context windows.

なので現状、LoadAndSearchToolSpecが何をやってくれるものなのか、ちょっとわかってない。

日本語がうまく動かないのかなと思って英語にしてみたりもしたけど、ダメだった。Wikipedia Toolが変わってしまったのかもしれないし、他のツールでは動作するのかもしれないけど、ちょっと追いかける気にはならなかったので、この辺で諦め。。。

あと、wikipediaの検索言語をデフォルトで"ja"にしてほしいんだけど、そのパラメータの渡し方も分からなかった。

kun432kun432

エージェントのモジュール

https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/modules.html

  • OpenAI Agent
    • Function Callingを使う
  • OpenAI Assistant Agent
    • Assitatnt APIを使う
  • ReAct Agent
    • ReActフレームワークを使う
  • LLM Compiler Agent
    • LLMCompilerを使ったエージェント。Function Callingを並列実行するらしい。
  • カスタムでエージェントを作成
  • エージェントを低レベルAPIで実装

あたりが紹介されているけど、今回は割愛。

kun432kun432

Routers

https://docs.llamaindex.ai/en/stable/module_guides/querying/router/root.html

名前の通り、Routerは、ユーザークエリを受け取って、複数の処理へルーティングする。イメージとしては、Function Callingで複数の関数を用意するようなケース、ああいう感じの抽象化モジュールと思っている。

  • 複数の中から1つを選択することもできるし、複数を選択して結果を統合することもできる
  • ルーティングの対象は、データソース、クエリエンジン、リトリーバーなどいろいろ

ちょっと今の自分の理解だとかなりレイヤー高めの概念かなぁ、OpenAI AgentやReAct Agentならまだしも。

ここはスキップ。

kun432kun432

Structured Output

https://docs.llamaindex.ai/en/stable/module_guides/querying/structured_outputs/structured_outputs.html

出力を構造化する。特にLLMの出力結果を別のアプリケーションに渡す際などはとても重要になってくる。

以下のモジュールが紹介されている。

  • Output Parsers
    • LLM Text Completion APIの前後で動作するモジュール
    • LLM Function Calling APIでは使用されない(Function Calling API自体が構造化出力できるため)
  • Pydantic Programs
    • 入力プロンプトをPydanticオブジェクトで表される構造化出力にマップする汎用モジュール
    • Function Calling か Text Completion API+Output Parsers が使用される。
    • クエリーエンジンと統合可能
  • Pre-defined Pydantic Programs
    • 入力を特定の出力型(データフレームなど)にマップするPydanticプログラムをあらかじめ定義する

こういう感じで動作する

Text Completion APIの場合


© 2023 Jerry Liu. LlamaIndex is released under the MIT license

上にある通り、LLMの入出力前後でPydantic Output Parserがフォーマットの調整を行う。LLMへの入力も、元の入力+プロンプトから見れば出力というわけね。

Function Callingを使う場合


© 2023 Jerry Liu. LlamaIndex is released under the MIT license

Function CallingはArgumentsを使えばそのまま出力固定(JSON)になるのだけど、そういう使い方ではなさそう。まあ実際にやりながら。

Pydantic Program

https://docs.llamaindex.ai/en/stable/module_guides/querying/structured_outputs/pydantic_program.html

Pydanticを使って、入力文字列を構造化Pydanticオブジェクトに変換する抽象化がLLamaIndexにおけるPydantic Programというものらしい。汎用的に使えるので色々なところで利用ができる。

Pydantic Programには3つの種類がある。

  • LLM Text Completion Pydantic Programs
    • Text Completion APIと出力構文解析により、入力テキストをユーザー指定の構造化オブジェクトに変換する
    • LLM Function Calling Pydantic Program
      • Function Calling APIにより、入力テキストをユーザー指定の構造化オブジェクトに変換する
  • Prepackaged Pydantic Programs
    • 予め定義した構造化オブジェクトに変換する

LLM Text Completion Pydantic Program

https://docs.llamaindex.ai/en/stable/examples/output_parsing/llm_program.html

LLMTextCompletionProgramを使って、Pydanticのクラス定義をLLMの出力に強制する。

Pydanticでデータモデルを定義する。ここでは、Songクラスと、Albumクラスを定義している。

from pydantic import BaseModel
from typing import List

class Song(BaseModel):
    """Data model for a song."""

    title: str
    length_seconds: int


class Album(BaseModel):
    """Data model for an album."""

    name: str
    artist: str
    songs: List[Song]

LLMTextCompletionProgramに上記のデータモデル定義をoutput_clsで渡してやる。

from llama_index.program import LLMTextCompletionProgram

prompt_template_str = """\
アーティストと曲のリストを含む、アルバムの例を生成してください。\
映画 {movie_name} をヒントとして使ってください。
"""

program = LLMTextCompletionProgram.from_defaults(
    output_cls=Album,
    prompt_template_str=prompt_template_str,
    verbose=True,
)

output = program(movie_name="侍戦隊シンケンジャー 銀幕版 天下分け目の戦")
output
Album(name='侍戦隊シンケンジャー 銀幕版 天下分け目の戦', artist='Various Artists', songs=[Song(title='侍戦隊シンケンジャー 銀幕版 天下分け目の戦', length_seconds=240), Song(title='勇者たちの誓い', length_seconds=210), Song(title='戦国の風', length_seconds=180), Song(title='魂の剣', length_seconds=195), Song(title='炎の心', length_seconds=220)])

PydanticOutputParserをOutput Parserとして書くと、上記は以下と同じ。

from llama_index.output_parsers import PydanticOutputParser
from llama_index.program import LLMTextCompletionProgram

program = LLMTextCompletionProgram.from_defaults(
    output_parser=PydanticOutputParser(output_cls=Album),
    prompt_template_str=prompt_template_str,
    verbose=True,
)

output = program(movie_name="侍戦隊シンケンジャー 銀幕版 天下分け目の戦")
output

ちなみにこのときどんなプロンプトが投げられているのかというとこんな感じ。

user: アーティストと曲のリストを含む、アルバムの例を生成してください。映画 侍戦隊シンケンジャー 銀幕版 天下分け目の戦 をヒントとして使ってください。



Here's a JSON schema to follow:
{{"title": "Album", "description": "Data model for an album.", "type": "object", "properties": {{"name": {{"title": "Name", "type": "string"}}, "artist": {{"title": "Artist", "type": "string"}}, "songs": {{"title": "Songs", "type": "array", "items": {{"$ref": "#/definitions/Song"}}}}}}, "required": ["name", "artist", "songs"], "definitions": {{"Song": {{"title": "Song", "description": "Data model for a song.", "type": "object", "properties": {{"title": {{"title": "Title", "type": "string"}}, "length_seconds": {{"title": "Length Seconds", "type": "integer"}}}}, "required": ["title", "length_seconds"]}}}}}}

Output a valid JSON object but do not repeat the schema.

で勝ってきたレスポンスはこう。

assistant: {
  "name": "侍戦隊シンケンジャー 銀幕版 天下分け目の戦",
  "artist": "Various Artists",
  "songs": [
    {
      "title": "侍戦隊シンケンジャー 銀幕版 天下分け目の戦",
      "length_seconds": 240
    },
    {
      "title": "勇者たちの誓い",
      "length_seconds": 210
    },
    {
      "title": "戦国の風",
      "length_seconds": 180
    },
    {
      "title": "魂の剣",
      "length_seconds": 195
    },
    {
      "title": "炎の心",
      "length_seconds": 220
    }
  ]
}

なるほど、確かにこの図の通り、LLMの処理の入出力それぞれでPydanticのデータモデル定義が使われているのがわかる。


© 2023 Jerry Liu. LlamaIndex is released under the MIT license

でこのOutputParserをカスタムで書くこともできる。

from llama_index.types import BaseOutputParser
from pydantic import BaseModel
from typing import List

class Song(BaseModel):
    """Data model for a song."""

    title: str
    length_seconds: int


class Album(BaseModel):
    """Data model for an album."""

    name: str
    artist: str
    songs: List[Song]


class CustomAlbumOutputParser(BaseOutputParser):
    """Custom Album output parser.

    Assume first line is name and artist.

    Assume each subsequent line is the song.

    """

    def __init__(self, verbose: bool = False):
        self.verbose = verbose

    def parse(self, output: str) -> Album:
        """Parse output."""
        if self.verbose:
            print(f"> Raw output: {output}")
        lines = output.split("\n")
        name, artist = lines[0].split(",")
        songs = []
        for i in range(1, len(lines)):
            title, length_seconds = lines[i].split(",")
            songs.append(Song(title=title, length_seconds=length_seconds))

        return Album(name=name, artist=artist, songs=songs)

ではカスタムなOutputParserを使って出力を制御してみる。

prompt_template_str = """\
アーティストと曲のリストを含む、アルバムの例を生成してください。\
映画 {movie_name} をヒントとして使ってください。\

以下のフォーマットで回答を返す。
最初の行: <album_name>,<album_artist> 
2行目以降: <song_title>,<song_length_seconds>
"""

program = LLMTextCompletionProgram.from_defaults(
    output_parser=CustomAlbumOutputParser(verbose=True),
    output_cls=Album,
    prompt_template_str=prompt_template_str,
    verbose=True,
)

output = program(movie_name="侍戦隊シンケンジャー 銀幕版 天下分け目の戦")
output
> Raw output: 侍戦隊シンケンジャー 銀幕版 天下分け目の戦,侍戦隊シンケンジャー
天下分け目の戦,240
勇者の証,210
炎神戦隊ゴーオンジャー BGM,180
炎神戦隊ゴーオンジャー,200
炎神戦隊ゴーオンジャー エンディングテーマ,190
炎神戦隊ゴーオンジャー インストゥルメンタル,180
炎神戦隊ゴーオンジャー インストゥルメンタル2,170
炎神戦隊ゴーオンジャー インストゥルメンタル3,160
炎神戦隊ゴーオンジャー インストゥルメンタル4,150
炎神戦隊ゴーオンジャー インストゥルメンタル5,140
Album(name='侍戦隊シンケンジャー 銀幕版 天下分け目の戦', artist='侍戦隊シンケンジャー', songs=[Song(title='天下分け目の戦', length_seconds=240), Song(title='勇者の証', length_seconds=210), Song(title='炎神戦隊ゴーオンジャー BGM', length_seconds=180), Song(title='炎神戦隊ゴーオンジャー', length_seconds=200), Song(title='炎神戦隊ゴーオンジャー エンディングテーマ', length_seconds=190), Song(title='炎神戦隊ゴーオンジャー インストゥルメンタル', length_seconds=180), Song(title='炎神戦隊ゴーオンジャー インストゥルメンタル2', length_seconds=170), Song(title='炎神戦隊ゴーオンジャー インストゥルメンタル3', length_seconds=160), Song(title='炎神戦隊ゴーオンジャー インストゥルメンタル4', length_seconds=150), Song(title='炎神戦隊ゴーオンジャー インストゥルメンタル5', length_seconds=140)])

ちなみにこの場合は入力時はプロンプトでフォーマットを指定しているし、出力のパースも自前で最後にPydanticものデータモデルを適合してるだけになるね。

LLM Function Calling Pydantic Programs

https://docs.llamaindex.ai/en/stable/examples/output_parsing/openai_pydantic_program.html

OpenAIPydanticProgramを使うとFunction Callingを使って出力を制御できる。上と同じAlbumクラスの例。

from pydantic import BaseModel
from typing import List
from llama_index.program import OpenAIPydanticProgram


class Song(BaseModel):
    title: str
    length_seconds: int


class Album(BaseModel):
    name: str
    artist: str
    songs: List[Song]


prompt_template_str = """\
アーティストと曲のリストを含む、アルバムの例を生成してください。\
映画 {movie_name} をヒントとして使ってください。
"""
program = OpenAIPydanticProgram.from_defaults(
    output_cls=Album,
    prompt_template_str=prompt_template_str,
    verbose=True
)

データモデルの定義にdocstringを指定しない場合はdescrptionで説明を追加する必要がある

output = program(
    movie_name="仮面ライダーW FOREVER AtoZ/運命のガイアメモリ",
    description="アルバムのデータモデル"
)

Fuction CallingでArgumentsが生成されていることがわかる。

Function call: Album with args: {
  "name": "映画 仮面ライダーW FOREVER AtoZ/運命のガイアメモリ",
  "artist": "Various Artists",
  "songs": [
    {
      "title": "W-B-X 〜W-Boiled Extreme〜",
      "length_seconds": 251
    },
    {
      "title": "Finger on the Trigger",
      "length_seconds": 252
    },
    {
      "title": "W-B-X 〜W-Boiled Extreme〜 (TVサイズ)",
      "length_seconds": 92
    },
    {
      "title": "Finger on the Trigger (TVサイズ)",
      "length_seconds": 92
    },
    {
      "title": "W-B-X 〜W-Boiled Extreme〜 (Instrumental)",
      "length_seconds": 251
    },
    {
      "title": "Finger on the Trigger (Instrumental)",
      "length_seconds": 252
    }
  ]
}
output
Album(name='映画 仮面ライダーW FOREVER AtoZ/運命のガイアメモリ', artist='Various Artists', songs=[Song(title='W-B-X 〜W-Boiled Extreme〜', length_seconds=251), Song(title='Finger on the Trigger', length_seconds=252), Song(title='W-B-X 〜W-Boiled Extreme〜 (TVサイズ)', length_seconds=92), Song(title='Finger on the Trigger (TVサイズ)', length_seconds=92), Song(title='W-B-X 〜W-Boiled Extreme〜 (Instrumental)', length_seconds=251), Song(title='Finger on the Trigger (Instrumental)', length_seconds=252)])

データモデルにdocstringを設定する場合はdescriptionは不要。

from pydantic import BaseModel
from typing import List
from llama_index.program import OpenAIPydanticProgram

class Song(BaseModel):
    """
    曲のデータモデル
    """
    title: str
    length_seconds: int


class Album(BaseModel):
    """
    アルバムのデータモデル
    """
    name: str
    artist: str
    songs: List[Song]

prompt_template_str = """\
アーティストと曲のリストを含む、アルバムの例を生成してください。\
映画 {movie_name} をヒントとして使ってください。
"""
program = OpenAIPydanticProgram.from_defaults(
    output_cls=Album, prompt_template_str=prompt_template_str, verbose=True
)

output = program(
    movie_name="仮面ライダーW FOREVER AtoZ/運命のガイアメモリ"
)

データモデルのdocstring定義、もしくはdescriptionの指定がなければ、以下のようにエラーになる。つまりFunction Calling定義のdescrption=プロンプトに使用されているということね。

ValueError: Must provide description for your Pydantic Model. Either provide a docstring or add `description=<your_description>` to the method. Required to convert Pydantic Model to OpenAI Function.

実際のリクエストの内容はこんな感じ。

{
    'messages': [
        {
            'role': 'user',
            'content': 'アーティストと曲のリストを含む、アルバムの例を生成してください。映画 仮面ライダーW FOREVER AtoZ/運命のガイアメモリ をヒントとして使ってください。\n'
        }
    ],
    'model': 'gpt-3.5-turbo-0613',
    'stream': False,
    'temperature': 0.1,
    'tool_choice': {
        'type': 'function',
        'function': {
            'name': 'Album'
        }
    },
    'tools': [
        {
            'type': 'function',
            'function': {
                'name': 'Album',
                'description': 'アルバムのデータモデル',
                'parameters': {
                    'title': 'Album',
                    'description': 'アルバムのデータモデル',
                    'type': 'object',
                    'properties': {
                        'name': {
                            'title': 'Name',
                            'type': 'string'
                        },
                        'artist': {
                            'title': 'Artist',
                            'type': 'string'
                        },
                        'songs': {
                            'title': 'Songs',
                            'type': 'array',
                            'items': {
                                '$ref': '#/definitions/Song'
                            }
                        }
                    },
                    'required': ['name', 'artist', 'songs'],
                    'definitions': {
                        'Song': {
                            'title': 'Song',
                            'description': '曲のデータモデル',
                            'type': 'object',
                            'properties': {
                                'title': {
                                    'title': 'Title',
                                    'type': 'string'
                                },
                                'length_seconds': {
                                    'title': 'Length Seconds',
                                    'type': 'integer'
                                }
                            },
                            'required': ['title', 'length_seconds']
                        }
                    }
                }
            }
        }
    ]
}

なるほど、Function Callの定義でPydanticクラスが使用されて、結果もFunctio CallのArgumentsをPydanticクラスを通して出力制御するということか。


© 2023 Jerry Liu. LlamaIndex is released under the MIT license

並列Function Calling

Function Callingは並列実行できる。

https://platform.openai.com/docs/guides/function-calling/parallel-function-calling

ただし1106以降のモデルを使う必要がある。上にある通りLlamaIndexはデフォルトだと0613を使っているようなので、LLMの定義も必要になる。

from pydantic import BaseModel
from typing import List
from llama_index.program import OpenAIPydanticProgram
from llama_index.llms import OpenAI

class Song(BaseModel):
    """
    曲のデータモデル
    """
    title: str
    length_seconds: int


class Album(BaseModel):
    """
    アルバムのデータモデル
    """
    name: str
    artist: str
    songs: List[Song]

prompt_template_str = """\
アーティストと曲のリストを含む、3つのアルバムの例を生成してください。\
それぞれのテーマは、仮面ライダーW、侍戦隊シンケンジャー、 ウルトラマンゼロ、です。
"""

program = OpenAIPydanticProgram.from_defaults(
    output_cls=Album,
    llm=OpenAI(model="gpt-3.5-turbo-1106"),      # 1106以降のモデルを指定
    prompt_template_str=prompt_template_str,
    allow_multiple=True,                            # allow_multiple=Trueで並列処理を有効化
    verbose=True
)

output = program()
output
Function call: Album with args: {"name": "仮面ライダーW", "artist": "仮面ライダーW", "songs": [{"title": "W-B-X", "length_seconds": 240}, {"title": "Cyclone Effect", "length_seconds": 210}, {"title": "Finger on the Trigger", "length_seconds": 195}]}
Function call: Album with args: {"name": "侍戦隊シンケンジャー", "artist": "侍戦隊シンケンジャー", "songs": [{"title": "Samurai Sentai Shinkenger", "length_seconds": 220}, {"title": "Uramasa", "length_seconds": 190}, {"title": "Shirokuji", "length_seconds": 205}]}
Function call: Album with args: {"name": "ウルトラマンゼロ", "artist": "ウルトラマンゼロ", "songs": [{"title": "Go! Fight! Ultraman Zero!!", "length_seconds": 230}, {"title": "Ultra Zero Fight", "length_seconds": 215}, {"title": "Believe in Zero", "length_seconds": 200}]}
output
[
  Album(name='仮面ライダーW', artist='仮面ライダーW', songs=[Song(title='W-B-X', length_seconds=240), Song(title='Cyclone Effect', length_seconds=210), Song(title='Finger on the Trigger', length_seconds=195)]),
  Album(name='侍戦隊シンケンジャー', artist='侍戦隊シンケンジャー', songs=[Song(title='Samurai Sentai Shinkenger', length_seconds=220), Song(title='Uramasa', length_seconds=190), Song(title='Shirokuji', length_seconds=205)]),
  Album(name='ウルトラマンゼロ', artist='ウルトラマンゼロ', songs=[Song(title='Go! Fight! Ultraman Zero!!', length_seconds=230), Song(title='Ultra Zero Fight', length_seconds=215), Song(title='Believe in Zero', length_seconds=200)])
]

ストリーミング

Function Callingってストリーミングできたのか、全然知らなかった。stream_list()を使う。

output = program.stream_list()

for obj in output:
    print(obj.json(indent=2))

なるほど、ストリーミングといってもFunctionごとって感じなのね。

その他

ちなみにLlamaIndexを使わずに、Function Callingを使ってStrutured Outputを行うライブラリが紹介されている。

https://github.com/jxnl/instructor

kun432kun432

Prepackaged Pydantic Programs

Prepackaged Pydantic Programsは、

入力を特定の出力型(データフレームなど)にマップするPydanticプログラムをあらかじめ定義する

ということだったけど、イマイチピンときてない。ドキュメントで紹介されているのはPandasのデータフレームを使った例だった。

https://docs.llamaindex.ai/en/stable/examples/output_parsing/df_program.html

https://docs.llamaindex.ai/en/stable/examples/output_parsing/evaporate_program.html

さらっとだけ触ってみる。

from llama_index.program import OpenAIPydanticProgram, DataFrame

prompt_template_str = """\
以下のクエリを構造化データに展開してください。:

{input_str}.

列名の集合と行の集合の両方を抽出してください。
"""

program = OpenAIPydanticProgram.from_defaults(
    output_cls=DataFrame,
    prompt_template_str=prompt_template_str,
    verbose=True,
)

ではクエリを渡してみる。

input_str = """\
僕の名前はジョンで、25歳です。ニューヨーク バスケットボールが好きです。\
彼の名前は マイクで30歳。サンフランシスコに住んでいて、野球をするのが好きです。\
サラは20歳でロサンゼルス在住。テニスが好きです。\
彼女の名前はメアリーで35歳。シカゴに住んでいる。\
"""

response_obj = program(input_str=input_str)

なるほど。。。

Function call: DataFrame with args: {
  "columns": [
    {"column_name": "名前", "column_desc": "名前"},
    {"column_name": "年齢", "column_desc": "年齢"},
    {"column_name": "居住地", "column_desc": "居住地"},
    {"column_name": "趣味", "column_desc": "趣味"}
  ],
  "rows": [
    {"row_values": ["ジョン", 25, "ニューヨーク", "バスケットボール"]},
    {"row_values": ["マイク", 30, "サンフランシスコ", "野球"]},
    {"row_values": ["サラ", 20, "ロサンゼルス", "テニス"]},
    {"row_values": ["メアリー", 35, "シカゴ", ""]}
  ]
}
DataFrame(description=None, columns=[DataFrameColumn(column_name='名前', column_desc='名前'), DataFrameColumn(column_name='年齢', column_desc='年齢'), DataFrameColumn(column_name='居住地', column_desc='居住地'), DataFrameColumn(column_name='趣味', column_desc='趣味')], rows=[DataFrameRow(row_values=['ジョン', 25, 'ニューヨーク', 'バスケットボール']), DataFrameRow(row_values=['マイク', 30, 'サンフランシスコ', '野球']), DataFrameRow(row_values=['サラ', 20, 'ロサンゼルス', 'テニス']), DataFrameRow(row_values=['メアリー', 35, 'シカゴ', ''])])

これをPandasデータフレームに変換

response_obj.to_df()

リクエストの中身。なるほど、DataFrameオブジェクトを使うと、データフレームの定義が渡されるのね。

{
    'messages': [
        {
            'role': 'user', 'content': '以下のクエリを構造化データに展開してください。:\n\n僕の名前はジョンで、25歳です。ニューヨーク バスケットボールが好きです。彼の名前は マイクで30歳。サンフランシスコに住んでいて、野球をするのが好きです。サラは20歳でロサンゼルス在住。テニスが好きです。彼女の名前はメアリーで35歳。シカゴに住んでいる。.\n\n列名の集合と行の集合の両方を抽出してください。\n'
        }
    ],
    'model': 'gpt-3.5-turbo-0613',
    'stream': False,
    'temperature': 0.1,
    'tool_choice': {
        'type': 'function',
        'function': {
            'name': 'DataFrame'
        }
    },
    'tools': [
        {
            'type': 'function',
            'function': {
                'name': 'DataFrame',
                'description': 'Data-frame class.\n\nConsists of a `rows` field which is a list of dictionaries,\nas well as a `columns` field which is a list of column names.',
                'parameters': {
                    'title': 'DataFrame',
                    'description': 'Data-frame class.\n\nConsists of a `rows` field which is a list of dictionaries,\nas well as a `columns` field which is a list of column names.',
                    'type': 'object',
                    'properties': {
                        'description': {
                            'title': 'Description',
                            'type': 'string'
                        },
                        'columns': {
                            'title': 'Columns',
                            'description': 'List of column names.',
                            'type': 'array',
                            'items': {'$ref': '#/definitions/DataFrameColumn'}
                        },
                        'rows': {
                            'title': 'Rows',
                            'description': 'List of DataFrameRow objects. Each DataFrameRow contains         valuesin order of the data frame column.',
                            'type': 'array',
                            'items': {'$ref': '#/definitions/DataFrameRow'}
                        }
                    },
                    'required': ['columns', 'rows'],
                    'definitions': {
                        'DataFrameColumn': {
                            'title': 'DataFrameColumn',
                            'description': 'Column in a DataFrame.',
                            'type': 'object',
                            'properties': {
                                'column_name': {
                                    'title': 'Column Name',
                                    'description': 'Column name.',
                                    'type': 'string'
                                },
                                'column_desc': {
                                    'title': 'Column Desc',
                                    'description': 'Column description.',
                                    'type': 'string'
                                }
                            },
                            'required': ['column_name', 'column_desc']
                        },
                        'DataFrameRow': {
                            'title': 'DataFrameRow',
                            'description': 'Row in a DataFrame.',
                            'type': 'object',
                            'properties': {
                                'row_values': {
                                    'title': 'Row Values',
                                    'description': 'List of row values, where each value corresponds to a row key.',
                                    'type': 'array',
                                    'items': {}
                                }
                            },
                            'required': ['row_values']
                        }
                    }
                }
            }
        }
    ]
}

DataFrameRowsOnlyを使うと列だけのデータフレームとして出力制御される。

from llama_index.program import OpenAIPydanticProgram, DataFrameRowsOnly

prompt_template_str = """\
以下のクエリを構造化データに展開してください:

{input_str}

カラム名: ['名前', '年齢', '都市', '好きなスポーツ']。
関数スキーマにない追加のパラメータを指定しないでください。
"""

program = OpenAIPydanticProgram.from_defaults(
    output_cls=DataFrameRowsOnly,
    prompt_template_str=prompt_template_str,
    verbose=True,
)

input_str = """\
僕の名前はジョンで、25歳です。ニューヨーク バスケットボールが好きです。\
彼の名前は マイクで30歳。サンフランシスコに住んでいて、野球をするのが好きです。\
サラは20歳でロサンゼルス在住。テニスが好きです。\
彼女の名前はメアリーで35歳。シカゴに住んでいる。\
"""

response_obj = program(input_str=input_str)
response_obj
Function call: DataFrameRowsOnly with args: {
  "rows": [
    {
      "row_values": ["ジョン", 25, "ニューヨーク", "バスケットボール"]
    },
    {
      "row_values": ["マイク", 30, "サンフランシスコ", "野球"]
    },
    {
      "row_values": ["サラ", 20, "ロサンゼルス", "テニス"]
    },
    {
      "row_values": ["メアリー", 35, "シカゴ", null]
    }
  ]
}
DataFrameRowsOnly(rows=[DataFrameRow(row_values=['ジョン', 25, 'ニューヨーク', 'バスケットボール']), DataFrameRow(row_values=['マイク', 30, 'サンフランシスコ', '野球']), DataFrameRow(row_values=['サラ', 20, 'ロサンゼルス', 'テニス']), DataFrameRow(row_values=['メアリー', 35, 'シカゴ', None])])

データフレームに変換はできるけど、列名は提議していないので存在しない。

response_obj.to_df()

DataFrame Programsを使う

OpenAIPydanticProgramで多くのパラメータを指定するかわりに、DFFullProgramDFRowsProgramを使ってオブジェクトをシンプルに作ることができる。

DFRowsProgramの場合

from llama_index.program import OpenAIPydanticProgram, DFRowsProgram
import pandas as pd

df = pd.DataFrame(
    {
        "Name": pd.Series(dtype="str"),
        "Age": pd.Series(dtype="int"),
        "City": pd.Series(dtype="str"),
        "Favorite Sport": pd.Series(dtype="str"),
    }
)

df_rows_program = DFRowsProgram.from_defaults(
    pydantic_program_cls=OpenAIPydanticProgram,
    df=df,
    verbose=True
)

input_str = """\
僕の名前はジョンで、25歳です。ニューヨーク バスケットボールが好きです。\
彼の名前は マイクで30歳。サンフランシスコに住んでいて、野球をするのが好きです。\
サラは20歳でロサンゼルス在住。テニスが好きです。\
彼女の名前はメアリーで35歳。シカゴに住んでいる。\
"""

result_obj = df_rows_program(
    input_str=input_str,
)
result_obj
Function call: DataFrameRowsOnly with args: {
  "rows": [
    {
      "row_values": ["ジョン", 25, "ニューヨーク", "バスケットボール"]
    },
    {
      "row_values": ["マイク", 30, "サンフランシスコ", "野球"]
    },
    {
      "row_values": ["サラ", 20, "ロサンゼルス", "テニス"]
    },
    {
      "row_values": ["メアリー", 35, "シカゴ", null]
    }
  ]
}
DataFrameRowsOnly(rows=[DataFrameRow(row_values=['ジョン', 25, 'ニューヨーク', 'バスケットボール']), DataFrameRow(row_values=['マイク', 30, 'サンフランシスコ', '野球']), DataFrameRow(row_values=['サラ', 20, 'ロサンゼルス', 'テニス']), DataFrameRow(row_values=['メアリー', 35, 'シカゴ', None])])

ただしDFRowsProgramなので列名はない。

result_obj.to_df()

その場合はデータフレームを渡せば列名もセットされる。

result_obj.to_df(existing_df=df)

DFFullProgramを使うパターン。

from llama_index.program import OpenAIPydanticProgram, DFFullProgram
import pandas as pd


df_full_program = DFFullProgram.from_defaults(
    pydantic_program_cls=OpenAIPydanticProgram,
)

input_str = """\
僕の名前はジョンで、25歳です。ニューヨーク バスケットボールが好きです。\
彼の名前は マイクで30歳。サンフランシスコに住んでいて、野球をするのが好きです。\
サラは20歳でロサンゼルス在住。テニスが好きです。\
彼女の名前はメアリーで35歳。シカゴに住んでいる。\
"""

result_obj = df_full_program(
    input_str=input_str,
)
result_obj
DataFrame(description=None, columns=[DataFrameColumn(column_name='Name', column_desc='Name of the person'), DataFrameColumn(column_name='Age', column_desc='Age of the person'), DataFrameColumn(column_name='Location', column_desc='City where the person lives'), DataFrameColumn(column_name='Hobby', column_desc='Hobby of the person')], rows=[DataFrameRow(row_values=['ジョン', 25, 'ニューヨーク', 'バスケットボール']), DataFrameRow(row_values=['マイク', 30, 'サンフランシスコ', '野球']), DataFrameRow(row_values=['サラ', 20, 'ロサンゼルス', 'テニス']), DataFrameRow(row_values=['メアリー', 35, 'シカゴ', None])])
result_obj.to_df()

DFFullProgramだと列名とかは自動作成される。データフレームの定義を渡すのはできないみたい。Dataframeと等価ってことかな。

Evaporate

https://docs.llamaindex.ai/en/stable/examples/output_parsing/evaporate_program.html

これらしい。

https://arxiv.org/abs/2304.09433

落合陽一風まとめ

Language Models Enable Simple Systems for Generating Structured Views of Heterogeneous Data Lakes

ひとことでまとめるとどんなものですか?

この研究では、異なる種類の文書から表形式のデータを自動的に作成するシステム「EVAPORATE」を紹介しています。このシステムは、大きな言語モデルを使って、さまざまなフォーマットのドキュメント(例えば、ウェブページやPDF)から情報を引き出し、整理して表にまとめます。

先行研究と比較してどの点がすごいのですか?

EVAPORATEは、従来の方法よりも汎用的で、異なる種類の文書を扱う際にも高い精度でデータを整理できます。特に、改良版の「EVAPORATE-CODE+」は、従来の最先端システムよりも優れた成果を示しました。これにより、多様な文書からの情報抽出や整理がより効率的になります。

技術や手法の重要な点はどこにありますか?

EVAPORATEには、主に2つの方法があります。「EVAPORATE-DIRECT」は、直接文書から情報を抽出する方法です。もう一つは「EVAPORATE-CODE」で、コードを作成して文書を処理する方法です。さらに、品質を向上させるための「EVAPORATE-CODE+」も提案されており、この方法では複数の関数を生成し、それらの結果を組み合わせています。

技術や手法はどのように有効だと判断されましたか?

「EVAPORATE-CODE+」は、平均してかなり高い精度を達成しました。これは、以前の方法「EVAPORATE-DIRECT」よりも大幅な改善を示しています。この結果から、言語モデルを使ったこの新しいシステムが、高品質かつ低コストで幅広く使える可能性があることが分かります。

何か議論や批判すべき点はありますか?

「EVAPORATE-DIRECT」は、特に大量のデータを扱う場合に非常に高いコストがかかります。一方で、「EVAPORATE-CODE」はコストは抑えられますが、品質が落ちる可能性があります。「EVAPORATE-CODE+」では、この問題を改善しようとしていますが、機械によって生成された関数を使うことによる新たな課題もあります。

次に読むべき論文・文献は?

次に読むべき文献としては、言語モデルを使ったデータ処理システムの設計や評価、様々なドメインへの適用に関する研究が良いでしょう。また、機械生成関数を用いた弱い監督の応用に関する最新の研究も参考になります。これらのトピックに関連する文献は、AIやデータマネジメントの分野の学術ジャーナルやカンファレンスの論文集に見られることが多いです。

ちょっと試してみたけどエラーになった。追っかける気もそんなにないので、ここはパス。

kun432kun432

Query EngineとPydantic Outputsを組み合わせる

https://docs.llamaindex.ai/en/stable/module_guides/querying/structured_outputs/query_engine.html

Pydantic Programなどを使って出力制御を色々やってきたけど、実際にクエリエンジンと組み合わせてどうつかうか。

index.as_query_engine()の裏でRetrieverQueryEngineが動いているけど、これがPydandicを通した構造化出力に対応している。他のOutput Parserとの違いはLLM APIコールが追加されることがないこと、らしい。おそらくFunction Callingを使うのだろうと思われる。

まずはざっくり。

いつも通りにインデックスを作成する。

from pathlib import Path
import requests

wiki_titles = ["ドウデュース", "イクイノックス"]
for title in wiki_titles:
    response = requests.get(
        "https://ja.wikipedia.org/w/api.php",
        params={
            "action": "query",
            "format": "json",
            "titles": title,
            "prop": "extracts",
            # 'exintro': True,
            "explaintext": True,
        },
    ).json()
    page = next(iter(response["query"]["pages"].values()))
    wiki_text = page["extract"]

    data_path = Path("data")
    if not data_path.exists():
        Path.mkdir(data_path)

    with open(data_path / f"{title}.txt", "w") as fp:
        fp.write(wiki_text)
from llama_index import VectorStoreIndex, SimpleDirectoryReader, ServiceContext
from llama_index.text_splitter import SentenceSplitter
from llama_index.embeddings import OpenAIEmbedding
from llama_index.llms import OpenAI

llm = OpenAI(model="gpt-4", temperarture=0.3)
embed_model = OpenAIEmbedding()
node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=20)

service_context = ServiceContext.from_defaults(
  llm=llm,
  embed_model=embed_model,
  node_parser=node_parser,
)

documents = SimpleDirectoryReader("data").load_data()

index = VectorStoreIndex.from_documents(documents, service_context=service_context)

Pydanticで競走馬のデータモデルを作成する。

from typing import List
from pydantic import BaseModel


class RaceHorseBiography(BaseModel):
    """Data model for a biography of a race horse."""

    name: str
    major_wins: List[str]
    primary_jockey: List[str]
    best_known_for: List[str]
    extra_info: str

クエリエンジンで検索。output_clsで定義したPydanticモデルを指定する。

query_engine = index.as_query_engine(
    output_cls=RaceHorseBiography,
    response_mode="refine"
)

response = query_engine.query("ドウデュースについて教えて。")

print(response.name)
print(response.major_wins)
print(response.primary_jockey)
print(response.best_known_for)
print(response.extra_info)
ドウデュース
['日本ダービー', '京都記念']
['武豊']
['日本ダービーと京都記念の勝利', 'ダービーレコードの更新', 'ロンジンワールドベストレースホースランキングで第15位']
ドウデュースは、日本ダービーと京都記念を勝利したことで知られています。特に、日本ダービーでは2分21秒9のダービーレコードを更新し、鞍上の武豊は史上初の50代でのダービージョッキーの名誉を得ました。また、朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン以来28年ぶりとなりました。さらに、国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」では、日本ダービーの勝利が評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位に位置づけられました。しかし、ドバイターフへの出走を予定していたものの、調教後に左前脚を負傷し、出走を取り消しました。その後は治療と休養を経て、天皇賞(秋)に出走しました。

RetrieverQueryEngineresponse_modeで、構造化出力に対応しているのは以下のモード。

  • refine
  • compact
  • tree_summarize
  • accumulate
  • compact_accumulate

モデルによって、OpenAIPydanitcProgamLLMTextCompletionProgramが使用される。refinetree_summarizeなど複数回LLMに問い合わせる場合、前回のレスポンスをPydanticで出力制御してJSONオブジェクト化したものを次のリクエストに渡すらしい。

ちょっとリクエストを見てみる。

{
    'messages': [
        {
            'role': 'system',
            'content': "You are an expert Q&A system that is trusted around the world.\nAlways answer the query using the provided context information, and not prior knowledge.\nSome rules to follow:\n1. Never directly reference the given context in your answer.\n2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines."
        },
        {
            'role': 'user',
            'content': 'Context information is below.\n---------------------\nfile_path: data/ドウデュース.txt\n\nまずまずのスタートを決めると、道中は馬群を見て最後方グループで待機し、向正面から外へ出て徐々に捲りをかけ、直線に入ると早々と馬群から抜け出して後続を突き放し、3馬身半差の快勝でGIを含む重賞3勝目を挙げた。日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなった。\n次走は325日にドバイのメイダン競馬場で行われるドバイターフとし、同月15日(現地時間同月14日)にメイダン競馬場に到着した。しかし出馬投票後の同月24日、調教後に左前肢跛行を発症しドバイターフへの出走を取り消した。友道は「調教後に左腕節に違和感を認め、競馬に向けて進めておりましたが、将来のある馬なのでここでは無理をせず、取り消すことを決断いたしました」と語った。\nその後夏は治療と休養にあて、秋初戦として1029日に東京競馬場で開催される天皇賞(秋)に出走。\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: ドウデュースについて教えて。\nAnswer: '
        }
    ],
    'model': 'gpt-4',
    'stream': False,
    'temperature': 0.1,
    'tool_choice': {
        'type': 'function',
        'function': {
            'name': 'RaceHorseBiography'
        }
    },
    'tools': [
        {
            'type': 'function',
            'function': {
                'name': 'RaceHorseBiography',
                'description': 'Data model for a biography of a race horse.',
                'parameters': {
                    'title': 'RaceHorseBiography',
                    'description': 'Data model for a biography of a race horse.',
                    'type': 'object',
                    'properties': {
                        'name': {
                            'title': 'Name',
                            'type': 'string'
                        },
                        'major_wins': {
                            'title': 'Major Wins',
                            'type': 'array',
                            'items': {
                                'type': 'string'
                            }
                        },
                        'primary_jockey': {
                            'title': 'Primary Jockey',
                            'type': 'array',
                            'items': {
                                'type': 'string'
                            }
                        },
                        'best_known_for': {
                            'title': 'Best Known For',
                            'type': 'array',
                            'items': {
                                'type': 'string'
                            }
                        },
                        'extra_info': {
                            'title': 'Extra Info',
                            'type': 'string'
                        }
                    },
                    'required': ['name', 'major_wins', 'primary_jockey', 'best_known_for', 'extra_info']
                }
            }
        }
    ]
}

refineによる2回目のプロンプト。プロンプト内にPydanticを通したJSONオブジェクトが埋め込まれている(実際にはUnicodeエンコードされているが、わかりやすくデコードしたものを入れてある)

{
    'messages': [
        {
            'role': 'user',
            'content': 'You are an expert Q&A system that strictly operates in two modes when refining existing answers:\n1. **Rewrite** an original answer using the new context.\n2. **Repeat** the original answer if the new context isn\'t useful.\nNever reference the original answer or context directly in your answer.\nWhen in doubt, just repeat the original answer.New Context: file_path: data/ドウデュース.txt\n\n2倍の3番人気でレースを迎えた。レースではスタートから後方に控え、前半1000メートルを589という早いペースの中落ち着いてレースを進め、直線に入ってスパート。鞍上武豊の右ムチに応え加速し先団を捉え、大外から伸びてくるイクイノックスをクビ差抑え込み優勝。朝日杯以来のGI制覇となり、勝ちタイムは2219のダービーレコード(コースレコードは2018年ジャパンカップでアーモンドアイが記録した2206)。鞍上の武は、歴代最多を更新する2013年のキズナ以来となるダービー6勝目、かつ歴代最年長、史上初の50代でのダービージョッキーの名誉となった。また朝日杯フューチュリティステークスの勝ち馬が日本ダービーを制したのは、1994年のナリタブライアン(前身である朝日杯3歳ステークスを勝利)以来28年ぶりとなった。\n6月10日に国際競馬統括機関連盟が発表した「ロンジンワールドベストレースホースランキング」において、ドウデュースは日本ダービーを勝利した功績を評価され、シャフリヤールやエンブレムロードと並ぶレーティング120で第15位タイに位置づけられた。\nQuery: ドウデュースについて教えて。\nOriginal Answer: {"name": "ドウデュース", "major_wins": ["日本ダービー", "京都記念"], "primary_jockey": [], "best_known_for": ["日本ダービーと京都記念の勝利"], "extra_info": "ドウデュースは、日本ダービーと京都記念を勝利したことで知られています。特に、日本ダービーを勝利した馬が京都記念を勝利するのは、1948年春の京都記念のマツミドリ以来75年ぶりとなったことが注目されています。しかし、ドバイターフへの出走を予定していたものの、調教後に左前肢跛行を発症し、出走を取り消しました。その後は治療と休養を経て、天皇賞(秋)に出走しました。"}\nNew Answer: '
        }
    ],
    'model': 'gpt-4',
    'stream': False,
    'temperature': 0.1,
    'tool_choice': {
        'type': 'function',
        'function': {
            'name': 'RaceHorseBiography'
        }
    },
    'tools': [
        {
            'type': 'function',
            'function': {
                'name': 'RaceHorseBiography',
                'description': 'Data model for a biography of a race horse.',
                'parameters': {
                    'title': 'RaceHorseBiography',
                    'description': 'Data model for a biography of a race horse.',
                    'type': 'object',
                    'properties': {
                        'name': {
                            'title': 'Name',
                            'type': 'string'
                        },
                        'major_wins': {
                            'title': 'Major Wins',
                            'type': 'array',
                            'items': {
                                'type': 'string'
                            }
                        },
                        'primary_jockey': {
                            'title': 'Primary Jockey',
                            'type': 'array',
                            'items': {
                                'type': 'string'
                            }
                        },
                        'best_known_for': {
                            'title': 'Best Known For',
                            'type': 'array',
                            'items': {
                                'type': 'string'
                            }
                        },
                        'extra_info': {
                            'title': 'Extra Info',
                            'type': 'string'
                        }
                    },
                    'required': ['name', 'major_wins', 'primary_jockey', 'best_known_for', 'extra_info']
                }
            }
        }
    ]
}

このあたりも参考に。

https://docs.llamaindex.ai/en/stable/examples/query_engine/pydantic_query_engine.html

https://docs.llamaindex.ai/en/stable/examples/response_synthesizers/pydantic_tree_summarize.html

Output Parsing Modules

https://docs.llamaindex.ai/en/stable/module_guides/querying/structured_outputs/output_parser.html

他のフレームワークのOutput Parserを使うこともできる。

  • Guardrails
  • LangChain
  • Guidance

Guardrailsあたりは実用的に使えそうな気がする。

kun432kun432

Query Pipeline

https://docs.llamaindex.ai/en/stable/module_guides/querying/pipeline/root.html

https://blog.llamaindex.ai/introducing-query-pipelines-025dc2bb0537

https://medium.com/p/dee5a2e9a797

つい最近追加されたやつ、泣

Query Pipelineは、クエリに関する抽象化モジュール。ここまで色々なモジュールを組み合わせてRAGのクエリパイプラインを作ってみたけども、それを「オーケストレーション」的にやろうというもの。

Query Pipelineを使うメリットは以下。

  • より少ないコード行数/ボイラープレートで一般的なクエリーワークフローを表現できる
    • 出力/入力間のコンバーター・ロジックを書いたり、各モジュールの引数の型付けを正確に把握したりする必要がなくなる。
  • 読みやすさの向上
    • 定型文の削減は、読みやすさの向上につながる。
  • エンドツーエンドの観測可能性
    • パイプライン全体にわたってコールバックの統合が行われるため(任意にネストされたDAGでも)、観測可能性の統合に煩わされることがなくなる。
  • (将来的な)簡単なシリアライゼーション
    • 宣言的なインターフェースにより、コア・コンポーネントを他のシステム上でより簡単にシリアライズ/再展開できる。
  • (将来的な)キャッシュ
    • このインターフェイスは、入力の再利用を可能にするキャッシュ・レイヤーをフードの下に構築することも可能にする。

Query Pipelineで使えるモジュール一覧

https://docs.llamaindex.ai/en/stable/module_guides/querying/pipeline/module_usage.html

以下に従ってやってみる。

https://docs.llamaindex.ai/en/stable/examples/pipeline/query_pipeline.html

上記では4つのパターンが紹介されている。

  1. プロンプトとLLMをチェーンさせる
  2. クエリ書き換え(プロンプト+LLM)と検索をチェーンさせる
  3. RAGのフルクエリパイプライン(クエリ書き換え、検索、リランキング、レスポンス合成)をチェーンさせる
  4. カスタムクエリーコンポーネントのセットアップ

順番にやってみる。

準備

トレースにArize Phoenixを使う。

!pip install -q llama-index arize-phoenix
from google.colab import userdata
import os

os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
import phoenix as px
import llama_index

px.launch_app()
llama_index.set_global_handler("arize_phoenix")

表示されたURLにアクセスする。

🌍 To view the Phoenix app in your browser, visit https://XXXXXXXX-colab.googleusercontent.com/

LLMへのリクエストが行われるとここに記録されていく。

1. プロンプトとLLMをチェーンさせる

まずはシンプルなワークフローとしてプロンプトとLLMをチェーンさせる。シーケンシャルなクエリパイプラインの場合はchainを使うと、左から右に処理されていって、次の処理に適したフォーマットに自動的に変換していく。

プロンプトテンプレートをLLMに渡すチェーン。

from llama_index.query_pipeline.query import QueryPipeline
from llama_index.llms import OpenAI
from llama_index.prompts import PromptTemplate

prompt_str = "{movie_name} に関係する映画名を生成してください。"
prompt_tmpl = PromptTemplate(prompt_str)

llm = OpenAI(model="gpt-3.5-turbo")

p = QueryPipeline(chain=[prompt_tmpl, llm], verbose=True)

クエリパイプラインの実行はrunで。

output = p.run(movie_name="仮面ライダーW")
> Running module 712ac40a-1a8f-453f-85e3-43fe4f81ecaa with input: 
movie_name: 仮面ライダーW

> Running module 34461f34-3853-44d9-a83e-72e91fd66ce1 with input: 
messages: 仮面ライダーW に関係する映画名を生成してください。
print(str(output))
assistant: 1. 仮面ライダーW FOREVER AtoZ/運命のガイアメモリ
2. 仮面ライダーW RETURNS 仮面ライダーアクセル/帰還のゲイツ
3. 仮面ライダーW RETURNS 仮面ライダーエターナル/永遠のメモリー
4. 仮面ライダーW RETURNS 仮面ライダーサイクロン/再生のガイアメモリ
5. 仮面ライダーW RETURNS 仮面ライダーアクセル/帰還のゲイツ
6. 仮面ライダーW RETURNS 仮面ライダーエターナル/永遠のメモリー
7. 仮面ライダーW RETURNS 仮面ライダーサイクロン/再生のガイアメモリ
8. 仮面ライダーW RETURNS 仮面ライダーアクセル/帰還のゲイツ
9. 仮面ライダーW RETURNS 仮面ライダーエターナル/永遠のメモリー
10. 仮面ライダーW RETURNS 仮面ライダーサイクロン/再生のガイアメモリ

Arize Phoenixを見るとこんな感じでクエリからプロンプトへの埋め込み、LLMへの問い合わせが行われているのがわかる。

これにPydanticOutputParserを使って出力フォーマット制御を追加してみる。

from typing import List
from pydantic import BaseModel, Field
from llama_index.output_parsers import PydanticOutputParser


class Movie(BaseModel):
    """1つの映画を表すオブジェクト"""
    name: str = Field(..., description="映画のタイトル")
    year: int = Field(..., description="映画の上映年")


class Movies(BaseModel):
    """映画のリストを表すオブジェクト"""
    movies: List[Movie] = Field(..., description="映画のリスト")

llm = OpenAI(model="gpt-3.5-turbo")
output_parser = PydanticOutputParser(Movies)

json_prompt_str = """\
{movie_name} に関係する映画名を生成してください。出力は以下のJSON形式で:
"""

json_prompt_str = output_parser.format(json_prompt_str)

ちなみにこういうプロンプトが設定される。ちなみにJSONスキーマ内の日本語は実際にはUnicodeエンコードされている。

{movie_name} に関係する映画名を生成してください。出力は以下のJSON形式で:



Here's a JSON schema to follow:
{{"title": "Movies", "description": "映画のリストを表すオブジェクト", "type": "object", "properties": {{"movies": {{"title": "Movies", "description": "映画のリスト", "type": "array", "items": {{"$ref": "#/definitions/Movie"}}}}}}, "required": ["movies"], "definitions": {{"Movie": {{"title": "Movie", "description": "1つの映画を表すオブジェクト", "type": "object", "properties": {{"name": {{"title": "Name", "description": "映画のタイトル", "type": "string"}}, "year": {{"title": "Year", "description": "映画の上映年", "type": "integer"}}}}, "required": ["name", "year"]}}}}}}

Output a valid JSON object but do not repeat the schema.

チェーンの最後にOutput Parserを追加して実行。

json_prompt_tmpl = PromptTemplate(json_prompt_str)

p = QueryPipeline(chain=[json_prompt_tmpl, llm, output_parser], verbose=True)
output = p.run(movie_name="仮面ライダーW")
> Running module 66a47d95-4c63-43dd-b428-1ba2e362ee4e with input: 
movie_name: 仮面ライダーW

> Running module ccd40fcb-f4c2-4d64-bd4c-f7ca232fee54 with input: 
messages: 仮面ライダーW に関係する映画名を生成してください。出力は以下のJSON形式で:



Here's a JSON schema to follow:
{"title": "Movies", "description": "\u6620\u753b\u306e\u30ea\u30b9\u30c8\u3092\u8868\u3059\u30aa\u30d6\u30b8\u30a7\u30af\u30...

> Running module 9ed5de10-d63b-42f4-98b2-a841f12251ff with input: 
input: assistant: {
  "movies": [
    {
      "name": "仮面ライダーW FOREVER AtoZ/運命のガイアメモリ",
      "year": 2010
    },
    {
      "name": "仮面ライダーW RETURNS 仮面ライダーアクセル",
      "year": 2011
    },
    {
      "name...
output
Movies(movies=[Movie(name='仮面ライダーW FOREVER AtoZ/運命のガイアメモリ', year=2010), Movie(name='仮面ライダーW RETURNS 仮面ライダーアクセル', year=2011), Movie(name='仮面ライダーW RETURNS 仮面ライダーエターナル', year=2011), Movie(name='仮面ライダーW RETURNS 仮面ライダーアクセル/仮面ライダーエターナル', year=2011), Movie(name='仮面ライダーW RETURNS 仮面ライダーサンバ', year=2011)])

LLMにストリーミングさせる場合、LLMをas_query_component(streamling=True)で指定する。ストリーミングさせつつ、LLMの出力結果を受け渡して再度LLMに出力させるようなチェーン。

prompt_str = "{movie_name} に関係する映画名を生成してください。"
prompt_tmpl = PromptTemplate(prompt_str)

# let's add some subsequent prompts for fun
prompt_str2 = """\
以下に文章があります:

{text}

各映画の概要を書き直してください。
"""
prompt_tmpl2 = PromptTemplate(prompt_str2)

llm = OpenAI(model="gpt-3.5-turbo")
llm_c = llm.as_query_component(streaming=True)

p = QueryPipeline(
    chain=[prompt_tmpl, llm_c, prompt_tmpl2, llm_c], verbose=True
)
output = p.run(movie_name="仮面ライダーW")
> Running module 52a13d6f-9059-497f-8468-a503103eae29 with input: 
movie_name: 仮面ライダーW

> Running module 73c824b1-7c1d-47ef-846a-207315478c21 with input: 
messages: 仮面ライダーW に関係する映画名を生成してください。

> Running module 26411af1-2924-4898-85a7-917838b0e540 with input: 
text: <generator object llm_chat_callback.<locals>.wrap.<locals>.wrapped_llm_chat.<locals>.wrapped_gen at 0x7c4f29e49070>

> Running module f6ab8d12-ca18-4e8f-b988-92d1b1c206d9 with input: 
messages: 以下に文章があります:

1. 仮面ライダーW: フォーエバー AtoZ/運命のガイアメモリ
2. 仮面ライダーW: リターンズ アクセル/エターナル
3. 仮面ライダーW: ゴーフォーサイクロン
4. 仮面ライダーW: サイクロンジョーカー
5. 仮面ライダーW: エクストリームサイクロン
6. 仮面ライダーW: サイクロンアクセル
7. 仮面ライダーW: ジョーカーエターナル
8. 仮面ライダ...
for o in output:
    print(o.delta, end="\n")
1
.
 
仮
面
ラ
イ
ダ
ー
W
:
 フ
ォ
ーエ
バ
ー
 A
to
Z
/
運
命
の
ガ
イ
ア
メ
モ
リ
 -
 
仮
面
ラ
イ
ダ
ー
W
が
、
新
た
な
敵
と
(snip)

Arize Phoenixでみてみると、クエリからLLMへのリクエストが2回行われているのがわかる。



ちなみにストリーミングの場合だとどうも最終レスポンスまでは記録されないっぽい。ストリーミングじゃない場合は入出力が両方記録される。

2. クエリ書き換え(プロンプト+LLM)と検索をチェーンさせる

2つのプロンプトを通してクエリを作成し検索を行うチェーン。

  1. 与えられたプロンプト変数に関する質問を生成する。
  2. 検索精度を上げるために、HyDEを使って検索用の仮説回答を生成する

まずインデックスを作成。

from pathlib import Path
import requests

wiki_titles = ["ドウデュース"]
for title in wiki_titles:
    response = requests.get(
        "https://ja.wikipedia.org/w/api.php",
        params={
            "action": "query",
            "format": "json",
            "titles": title,
            "prop": "extracts",
            # 'exintro': True,
            "explaintext": True,
        },
    ).json()
    page = next(iter(response["query"]["pages"].values()))
    wiki_text = page["extract"]

    data_path = Path("data")
    if not data_path.exists():
        Path.mkdir(data_path)

    with open(data_path / f"{title}.txt", "w") as fp:
        fp.write(wiki_text)
from llama_index import VectorStoreIndex, SimpleDirectoryReader, ServiceContext
from llama_index.text_splitter import SentenceSplitter
from llama_index.embeddings import OpenAIEmbedding
from llama_index.llms import OpenAI

llm = OpenAI(model="gpt-3.5-turbo", temperarture=0.3)
embed_model = OpenAIEmbedding()
node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=20)

service_context = ServiceContext.from_defaults(
  llm=llm,
  embed_model=embed_model,
  node_parser=node_parser,
)

documents = SimpleDirectoryReader("data").load_data()

index = VectorStoreIndex.from_documents(documents, service_context=service_context)

クエリパイプラインを作成。

from llama_index.query_pipeline.query import QueryPipeline
from llama_index.llms import OpenAI
from llama_index.prompts import PromptTemplate

prompt_str1 = "競走馬 ドウデュース について、次のトピックに関する簡潔な質問を作成してください: 「{topic}」"
prompt_tmpl1 = PromptTemplate(prompt_str1)

# use HyDE to hallucinate answer.
prompt_str2 = '''\
以下の質問に答える文章を書いてください。
できるだけ多くの重要な詳細を含めるようにしてください。


{query_str}


文章: """
'''

prompt_tmpl2 = PromptTemplate(prompt_str2)

llm = OpenAI(model="gpt-3.5-turbo")
retriever = index.as_retriever(similarity_top_k=5)

p = QueryPipeline(
    chain=[prompt_tmpl1, llm, prompt_tmpl2, llm, retriever], verbose=True
)

Since each prompt only takes in one input, note that the QueryPipeline will automatically chain LLM outputs into the prompt and then into the LLM.

とあるように、プロンプトの入力変数が1つだけの場合は、前のLLMの出力をそのままプロンプトに埋め込むらしい。

では実行。

nodes = p.run(topic="主な重賞勝ち鞍")
> Running module 124bf7de-83b8-43a7-8c05-d10e60330e6e with input: 
topic: 主な重賞勝ち鞍

> Running module c7de191c-3e23-4b96-b8b0-e228a222d381 with input: 
messages: 競走馬 ドウデュース について、次のトピックに関する簡潔な質問を作成してください: 「主な重賞勝ち鞍」

> Running module 404174ae-8d21-4816-94bb-af6372b831f3 with input: 
query_str: assistant: ドウデュースはどの重賞を勝ちましたか?

> Running module 5e928f17-b91e-4fe7-bc8c-3c59a719e6d8 with input: 
messages: 以下の質問に答える文章を書いてください。
できるだけ多くの重要な詳細を含めるようにしてください。


assistant: ドウデュースはどの重賞を勝ちましたか?


文章: """


> Running module a9e05c2d-99db-4a6d-a909-8a4d77dccff1 with input: 
input: assistant: ドウデュースは、2021年に行われた日本の競馬の重賞レースであるジャパンカップを勝ちました。ジャパンカップは、東京競馬場で行われる芝2400メートルのレースで、日本国内外からトップクラスの競走馬が集まる国際的なレースです。ドウデュースは、このレースで見事な走りを見せ、優勝しました。この勝利により、ドウデュースはその年の最も優れた競走馬として称えられ、競馬界での名声を高めました...

5

で、HyDEの仮説回答でインデックス検索して5つのノードが得られている。

len(nodes)
5

Arize Phoenixで確認してみる。




お、retrivalのところはちゃんとトレースが出るんだね、これは便利。

3. RAGのフルクエリパイプライン(クエリ書き換え、検索、リランキング、レスポンス合成)をチェーンさせる

RAGでよく使う処理を全部てんこ盛りで。今回のケースではchainは使えない、というのは、レスポンス合成のところは、検索結果と元のクエリの2つの入力を受け付ける必要があるため。

なるほど、さっき書いてあった、

Since each prompt only takes in one input, note that the QueryPipeline will automatically chain LLM outputs into the prompt and then into the LLM.

とか、

We simply define chain on initialization. This is a special case of a query pipeline where the components are purely sequential, and we automatically convert outputs into the right format for the next inputs.

というのはこういうことね。

その代わりに、add_modulesadd_linkを使って、DAG(ここでは、Data Augmented Generationではなくて、有向非巡回グラフ:Directed Acyclic Graph だと思う)を構築する。

CohereのRerankを使うので、パッケージインストールとAPIキーの設定を行っておく。

!pip install cohere
from google.colab import userdata
import os

os.environ["COHERE_API_KEY"] = userdata.get('COHERE_API_KEY')

あと、クエリパイプラインの可視化にpyvisを使うのでこれもパッケージインストールしておく。

!pip install pyvis

インデックスは引き続き同じものを使うので割愛。

クエリパイプラインで使う各モジュールを定義する。

from llama_index.prompts import PromptTemplate
from llama_index.llms import OpenAI
from llama_index import ServiceContext
from llama_index.postprocessor import CohereRerank
from llama_index.response_synthesizers import TreeSummarize

# モジュール定義

## プロンプトテンプレート
prompt_str = "競走馬 ドウデュース について、次のトピックに関する簡潔な質問を作成してください: 「{topic}」"
prompt_tmpl = PromptTemplate(prompt_str)

## LLM
llm = OpenAI(model="gpt-3.5-turbo")

## 検索
retriever = index.as_retriever(similarity_top_k=3)

## リランク
reranker = CohereRerank()

## 要約
summarizer = TreeSummarize(
    service_context=ServiceContext.from_defaults(llm=llm)
)

クエリパイプラインを定義、ここで各モジュールをクエリパイプラインに登録(add_module)する

from llama_index.query_pipeline.query import QueryPipeline

# クエリパイプラインの定義
p = QueryPipeline(verbose=True)
p.add_modules(
    {
        "llm": llm,
        "prompt_tmpl": prompt_tmpl,
        "retriever": retriever,
        "summarizer": summarizer,
        "reranker": reranker,
    }
)

次にadd_linkで各モジュール間のリンクを設定していく。接続元・接続先という形で流れを作るのだね。で、複数の出力/入力がある場合は、それぞれsource_keyまたはdest_keyを指定する必要がある。例えばrerankerとsummarizerは2つの入力(query_strnodes)が必要になるため、dest_keyで明示的に指定することになる。

p.add_link("prompt_tmpl", "llm")
p.add_link("llm", "retriever")
p.add_link("retriever", "reranker", dest_key="nodes")
p.add_link("llm", "reranker", dest_key="query_str")
p.add_link("reranker", "summarizer", dest_key="nodes")
p.add_link("llm", "summarizer", dest_key="query_str")

各モジュールが必要とする入出力のキーは以下で確認できる。

  • module.as_query_component().input_keys
  • module.as_query_component().output_keys

例えばこんな感じで。

for m in ["llm", "prompt_tmpl", "retriever", "summarizer", "reranker"]:
    print(f"##### {m} #####\n")
    obj = locals().get(m)
    print("INPUT:", obj.as_query_component().input_keys)
    print("OUTPUT:", obj.as_query_component().input_keys)
    print()
##### llm #####

INPUT: required_keys={'messages'} optional_keys=set()
OUTPUT: required_keys={'messages'} optional_keys=set()

##### prompt_tmpl #####

INPUT: required_keys={'topic'} optional_keys=set()
OUTPUT: required_keys={'topic'} optional_keys=set()

##### retriever #####

INPUT: required_keys={'input'} optional_keys=set()
OUTPUT: required_keys={'input'} optional_keys=set()

##### summarizer #####

INPUT: required_keys={'nodes', 'query_str'} optional_keys=set()
OUTPUT: required_keys={'nodes', 'query_str'} optional_keys=set()

##### reranker #####

INPUT: required_keys={'nodes'} optional_keys={'query_str'}
OUTPUT: required_keys={'nodes'} optional_keys={'query_str'}

わかりやすくpyvisで可視化する。

from pyvis.network import Network
from IPython.display import HTML

net = Network(notebook=True, cdn_resources="in_line", directed=True)
net.from_nx(p.dag)
net.save_graph("rag_dag.html")
HTML(filename="rag_dag.html")

流れがわかりやすい。

ではクエリ。

response = p.run(topic="主な重賞勝ち鞍")
> Running module prompt_tmpl with input: 
topic: 主な重賞勝ち鞍

> Running module llm with input: 
messages: 競走馬 ドウデュース について、次のトピックに関する簡潔な質問を作成してください: 「主な重賞勝ち鞍」

> Running module retriever with input: 
input: assistant: ドウデュースはどの重賞を勝ちましたか?

> Running module reranker with input: 
query_str: assistant: ドウデュースはどの重賞を勝ちましたか?
nodes: [NodeWithScore(node=TextNode(id_='eac6bdf4-b0db-4fc1-a2f7-bf3ac757147f', embedding=None, metadata={'file_path': 'data/ドウデュース.txt', 'file_name': 'ドウデュース.txt', 'file_type': 'text/plain', 'file_size': 95...

> Running module summarizer with input: 
query_str: assistant: ドウデュースはどの重賞を勝ちましたか?
nodes: [NodeWithScore(node=TextNode(id_='eac6bdf4-b0db-4fc1-a2f7-bf3ac757147f', embedding=None, metadata={'file_path': 'data/ドウデュース.txt', 'file_name': 'ドウデュース.txt', 'file_type': 'text/plain', 'file_size': 95...

生成された回答

print(str(response))
ドウデュースは日本ダービーを勝ちました。

Arize Phoenixでも。






非同期で処理させることもできる。

response = await p.arun(topic="主な重賞勝ち鞍")
print(str(response))

クエリの書き換えが不要でretrieverとsummarizerにそのまま入力クエリを渡す場合、InputComponentを使えば、入力クエリを複数のモジュールにリンクさせることができる。

from llama_index.response_synthesizers import TreeSummarize
from llama_index import ServiceContext
from llama_index.query_pipeline import InputComponent

retriever = index.as_retriever(similarity_top_k=5)
summarizer = TreeSummarize(
    service_context=ServiceContext.from_defaults(
        llm=OpenAI(model="gpt-3.5-turbo")
    )
)

p = QueryPipeline(verbose=True)
p.add_modules(
    {
        "input": InputComponent(),
        "retriever": retriever,
        "summarizer": summarizer,
    }
)
p.add_link("input", "retriever")
p.add_link("input", "summarizer", dest_key="query_str")
p.add_link("retriever", "summarizer", dest_key="nodes")

流れはこういう感じになる。

ではクエリ。

response = p.run(input="ドウデュースの主な重賞勝ち鞍を教えて。")
> Running module input with input: 
input: ドウデュースの主な重賞勝ち鞍を教えて。

> Running module retriever with input: 
input: ドウデュースの主な重賞勝ち鞍を教えて。

> Running module summarizer with input: 
query_str: ドウデュースの主な重賞勝ち鞍を教えて。
nodes: [NodeWithScore(node=TextNode(id_='eac6bdf4-b0db-4fc1-a2f7-bf3ac757147f', embedding=None, metadata={'file_path': 'data/ドウデュース.txt', 'file_name': 'ドウデュース.txt', 'file_type': 'text/plain', 'file_size': 95...
print(str(response))
ドウデュースの主な重賞勝ち鞍は、日本ダービー、有馬記念、京都記念です。





kun432kun432

4. カスタムクエリーコンポーネントのセットアップ

QueryComponentを使えば、クエリパイプラインのコンポーネントをカスタムで作成することができる。必要なのは、入出力のバリデーションと実行の関数+一部のヘルパー関数。

サンプルとして、上でやった映画名を生成するコンポーネントを作成してみる。

from llama_index.query_pipeline import (
    CustomQueryComponent,
    InputKeys,
    OutputKeys,
)
from llama_index.query_pipeline.query import QueryPipeline
from llama_index.prompts import PromptTemplate
from typing import Dict, Any
from llama_index.llms.llm import BaseLLM
from pydantic import Field


class RelatedMovieComponent(CustomQueryComponent):
    """関係する映画名を出力するコンポーネント"""

    llm: BaseLLM = Field(..., description="OpenAI LLM")

    def _validate_component_inputs(
        self, input: Dict[str, Any]
    ) -> Dict[str, Any]:
        """run_component実行時に入力をバリデーションする"""
        # 注記: ここはオプションだがバリデーションの例として実装
        return input

    @property
    def _input_keys(self) -> set:
        """入力キーの辞書"""
        # 注記: 必要な入力をリストアップ。オプション入力の場合は
        # `optional_input_keys_dict`をオーバーライドする必要がある。
        return {"movie"}

    @property
    def _output_keys(self) -> set:
        """出力キーの辞書"""
        return {"output"}

    def _run_component(self, **kwargs) -> Dict[str, Any]:
        """コンポーネントを実行する"""
        # 簡単のためQueryPipeline自体を使用する。
        prompt_str = "{movie_name} に関係する映画名を生成してください。"
        prompt_tmpl = PromptTemplate(prompt_str)
        p = QueryPipeline(chain=[prompt_tmpl, llm])
        return {"output": p.run(movie_name=kwargs["movie"])}

作成したカスタムコンポーネントを使ってクエリパイプラインを作ってみる。カスタムコンポーネントで映画名を生成して、それをプロンプト+LLMに流すようなチェーン。

from llama_index.llms import OpenAI
from llama_index.query_pipeline.query import QueryPipeline
from llama_index.prompts import PromptTemplate

llm = OpenAI(model="gpt-3.5-turbo")
component = RelatedMovieComponent(llm=llm)

prompt_str = """\
以下に文章があります:

{text}

これらにシェークスピアのコメントを追加して書き直してください。コメントは日本語で。
"""
prompt_tmpl = PromptTemplate(prompt_str)

p = QueryPipeline(chain=[component, prompt_tmpl, llm], verbose=True)

実行

output = p.run(movie="仮面ライダーW")
> Running module 37c59e50-218d-4fdb-a06b-74317b7aa58c with input: 
movie: 仮面ライダーW

> Running module e4f98fec-18b0-4b36-b774-35b3ec1a268d with input: 
text: assistant: 1. 仮面ライダーW: ダブルフォーエバー
2. 仮面ライダーW: エターナルサーガ
3. 仮面ライダーW: フォーゼ×スカル
4. 仮面ライダーW: ジョーカーの逆襲
5. 仮面ライダーW: サイクロンの復讐
6. 仮面ライダーW: ヒートアップ!サイクロンジョーカー
7. 仮面ライダーW: フォーゼとの共闘
8. 仮面ライダーW: サイクロンの秘密
9. 仮面ライダーW:...

> Running module dc9e8314-efc7-4db8-9f5d-f22800fef2ea with input: 
messages: 以下に文章があります:

assistant: 1. 仮面ライダーW: ダブルフォーエバー
2. 仮面ライダーW: エターナルサーガ
3. 仮面ライダーW: フォーゼ×スカル
4. 仮面ライダーW: ジョーカーの逆襲
5. 仮面ライダーW: サイクロンの復讐
6. 仮面ライダーW: ヒートアップ!サイクロンジョーカー
7. 仮面ライダーW: フォーゼとの共闘
8. 仮面ライダーW: サイクロンの秘...

print(str(output))
assistant: 1. 仮面ライダーW: ダブルフォーエバー - "愛と勇気が永遠に続くように。"
2. 仮面ライダーW: エターナルサーガ - "運命の糸が絡み合い、悲劇が幕を開ける。"
3. 仮面ライダーW: フォーゼ×スカル - "星々の輝きが骸と共に舞い踊る。"
4. 仮面ライダーW: ジョーカーの逆襲 - "闇の中に潜む笑いが、復讐の始まりを告げる。"
5. 仮面ライダーW: サイクロンの復讐 - "怒りの嵐が巻き起こり、復讐の刃が振り下ろされる。"
6. 仮面ライダーW: ヒートアップ!サイクロンジョーカー - "炎と風が融合し、新たな力が解き放たれる。"
7. 仮面ライダーW: フォーゼとの共闘 - "異なる運命が交差し、共に戦う絆が生まれる。"
8. 仮面ライダーW: サイクロンの秘密 - "風が運ぶ秘密が、闇に包まれた真実を明かす。"
9. 仮面ライダーW: ジョーカーの誕生 - "闇の中で生まれた笑いが、新たな英雄を生み出す。"
10. 仮面ライダーW: ダブルの絆 - "二つの心が交わり、絆が深まる。"
kun432kun432

長かった・・・まあ当然といえば当然か。

最後のQuery Pipelineは、LancChainでいうところのLCELに当たるのだと思う。書きっぷりはなんとなくLangGraphに近い感があるのだけども。

https://note.com/astropomeai/n/nfe2a969c45d0

LangGraphの主な用途は、LLMアプリケーションに「サイクル」を追加することです。ここで重要なのは、LangGraphはDAG(有向非巡回グラフ)フレームワークではないという点です。DAGを構築したい場合はLangChain Expression Languageを使用するべきです。

このスクラップは4ヶ月前にクローズされました