Agents SDKを使った判例検索 (6)

Agents SDKを使った判例検索コード( database_query.py )解析の5回目。今回は「 本文取得ツール 」部分のコードについて、冒頭から1行ずつ、その働きを見ていく。

本文取得ツールの全体像

本文取得ツールは doc_id をキーにして、裁判例の本文(主文+理由)をDBから取り出す処理をしている。検索 → 候補特定 → 本文取得(ここ)という流れの中核部分になる。

class CaseGet(BaseModel):

本文取得ツールに渡す入力データの型定義。Pydantic の BaseModel を使い、「どんな引数を受け取るか」を厳密に定義している。これにより、送られてきたデータが正しい形式(例:IDがちゃんと入力されているか)をAIエージェントが自動でチェックできるようになる。

    doc_id: str = Field(..., description="cases.doc_id(完全一致)")
doc_id: str (名前と型)
  • doc_id:このフィールドの名前。データベース側でそれぞれの判例に割り振られている管理ID(例:”D12345″)を受け取る。
  • : str:このIDが文字列(テキスト)の形式であることを指定している。数字だけで構成されていても、プログラム上は文字として扱う。
= Field(...) (詳細設定)

Pydanticの機能を使って、このフィールドに特別な意味を持たせている。

  • (必須マーク):この「3点リーダー(Ellipsis)」は、Pydanticにおいて「この項目は絶対に省略できない(Required)」という意味を持つ。検索の時はキーワードがなくても動いたが、特定の判例を取得するのにIDがなければ、プログラムは何を返せばいいか分からないため、それを防ぐための制約。
  • description=”cases.doc_id(完全一致):AIへの指示書。
    • 「cases.doc_id」:AIに「データベースの cases という場所にある doc_id のことだよ」と教えている。
    • 「(完全一致)」:AIに「似ているもの」ではなく、「一文字一句違わず完全に同じID」を指定しなければならない、という厳格なルールを伝えている。
全体の流れにおける位置付け

前回の記事で解説した「検索(CaseSearch)」と、今回の「取得(CaseGet)」は、以下のように連携する。

  1. 検索: ユーザーが「損害賠償」で検索 → AIが CaseSearch を使って5件のリストを取得。
  2. 選択: ユーザーが「2番目の判例(ID: D9876)を詳しく見せて」と指示。
  3. 特定(今回のコード): AIは「ID: D9876」を、この CaseGet モデルの doc_id に「必須・完全一致」の条件でセットする。
  4. 取得: これを使って、データベースからその判例の全文データが引き出される。
    include_facts_and_reasons: bool = Field(True, description="理由部分も返すか")

特定の判例を取得する際に、「判決の理由を含む長文のテキストデータ」まで全部持ってくるかどうかを制御するオプションスイッチの定義です。必須だった doc_id とは異なり、こちらは、お好みで変更できる設定項目となっている。

include_facts_and_reasons: bool (名前と型)
  • include_facts_and_reasons:フィールド名。「事実と理由を含めるか」という分かりやすい名前になっている。
  • : bool:型はブーリアン型(真偽値)を指定。つまり、設定値は True(はい、含めます)か False(いいえ、含めません)のどちらかしか入らない。ON/OFFのスイッチのようなもの。
= Field(True, ...)

True (初期値):Pydanticの Field のデフォルトの引数として、「True(はい、含めます)」を入れている。

description="理由部分も返すか"

AIエージェントに対して、「このスイッチをONにすると、判決の『理由』と書かれた長い文章の部分も一緒に返ってくるよ」と教えている。

@function_tool
def get_case_text(q: CaseGet) -> str:

これまでに定義した「鍵(CaseGet)」を実際に使って、AIエージェントが判例の全文を読み取るための道具(ツール)を登録する宣言部分。

@function_tool (デコレータ)
  • 役割:この記号を関数の直前に置くだけで、OpenAI Agents SDKは「この関数はAIエージェントが自由に使える『道具箱』に入れておくんだな」と認識する。
  • 何が起きるのか:本来、AIはPythonコードをそのまま実行することはできない。しかし、このデコレータを付けることで、SDKが自動的に「この道具は doc_id という鍵が必要で、説明文はこうです」という説明書を生成し、AIに渡してくれる。
def get_case_text(q: CaseGet)
  • get_case_text:ツールの名前。AIはこの名前を見て、「判例のテキスト(本文)を取得したい時はこれを使おう」と判断する。
  • q: CaseGet:関数の引数。先ほど定義した「必須のID」や「全文を含めるかどうかのスイッチ」が入った CaseGet 型のデータが、AIからここに送り込まれてくる。
-> str
  • 戻り値の型:このツールが最終的に文字列(テキスト)を返すことを示している。具体的には、整形された判決の全文や、見つからなかった場合のエラーメッセージがAIに返される。
    """
    doc_id を指定して、裁判例本文を取得する。
    """

get_case_text 関数に関する AI への説明文(docstring)。この説明文を読んで、AIは「ユーザーから具体的なIDを指定して詳細を知りたいと言われたら、このツールを使えばいいんだな」と判断する。

    with _connect() as conn:
  • _connect():データベースファイル(例:cases.db)への「扉」を開く関数。
  • with … as conn (コンテキストマネージャ):Pythonの便利な構文で、この with ブロックの中にいる間だけ、データベースとの接続(conn)が有効になる。
  • 自動クローズ:最大のメリットは「処理が終わったら(あるいはエラーが起きても)、自動的に接続を閉じてくれる」点。「入る → 作業する → 出る(自動施錠)」という流れを徹底することで、AIエージェントが何度繰り返しツールを使っても、システムの安定した動作を確保できる。
        row = conn.execute(

SQL を実行して1行だけ取得する準備をしている。鍵(doc_id)を使って、データベースに対して「この判例の情報を1件だけ取り出して」と依頼を出す。前回の記事(検索編)では fetchall() を使ってたくさんの結果をごっそり取得したが、今回は特定の1件を仕留めるための、より精密な操作が行われる。

  • conn.execute(…):データベースの接続窓口(conn)を通じて、具体的なSQL命令を実行する。
  • row =:実行して得られた「1件分のデータ(行)」を変数 row に格納する。
        """
        SELECT doc_id, court, date, case_number, case_name,
               main_text, facts_and_reasons
        FROM cases
        WHERE doc_id = ?
        """,

データベースに対して「特定のID(doc_id)を持つ判例の、すべての情報を抜き出してください」と指示するSQL命令。SELECT の後に続く項目は、その判例について知りたい情報のリスト。検索ツールでは「重すぎる」という理由で省いていた本文データ(判決の理由など)を、ここではしっかりと取得している。

「= ?」が持つ重要な意味

前回の「検索編」で使った LIKE %?% と、今回の WHERE doc_id = ? には下表のような違いがある。

比較項目検索(Search)のSQL取得(Get)のSQL
記号LIKE (あいまい)= (完全一致)
対象キーワードが含まれるものすべてそのIDを持つ1件だけ
イメージ「不当利得」という言葉が含まれる本を全部探す。「請求番号:A-123」の本を1冊だけ持ってくる。

この「=」 を使うことで、データベースは他の余計なデータを見ることなく、指定されたIDのレコードだけを迅速かつピンポイントに特定できる。

            (q.doc_id,),

SQL文の中にある ?(プレースホルダ)に流し込むためのデータの入れ物を準備している部分。

  • タプル(Tuple)型:Pythonのデータベース操作(execute)では、渡すデータが1つだけでもリストやタプルといった「入れ物」の形で渡さなければならないという決まりがある。
  • 1項目だけのタプル:Pythonでは、単に (q.doc_id) と書くと、ただの「括弧に囲まれた文字列」として扱われてしまう。最後にカンマを付けて (q.doc_id,) と書くことで、初めてPythonは「これは要素が1つだけ入ったタプル(入れ物)だ」と認識してくれる。
        ).fetchone()

データベースへの問い合わせ結果から「最初に見つかった1件だけ」を取り出すための命令。これまでの search_cases(検索ツール)ではたくさんの候補を扱っていたが、今回の get_case_text では、特定のIDを狙い撃ちした究極の1件を受け取る。

データベースにSQLを送ると、裏側では、条件に合うデータのリスト(カーソル)が作られる。そこからデータを取り出す方法は主に2つある。

  • fetchall():見つかったものを全部、リストの形でもらう(検索用)。
  • fetchone():見つかったものの中から1つだけをもらう(詳細表示用)。

今回のケースでは、一意の doc_id を使って検索しているため、結果は「0件(見つからない)」か「1件(見つかった)」のどちらかしかありません。そのため、リスト形式で受け取る必要がない fetchone() を使うのが最も効率的でスマートな方法といえる。

fetchone() を実行すると、変数 row には以下のような形でデータが格納される。

  • 見つかった場合:(ID, 裁判所, 日付, …) といったデータのセット(タプル)が返る。
  • 見つからなかった場合:Noneが返る。
    if not row:

特定のIDで検索した結果、該当する判例がデータベースに存在しなかった場合を想定した処理の入り口。

        return f"doc_id={q.doc_id} は見つかりませんでした。"

特定のIDで判例を探した結果、データベースに存在しなかった場合に、AIエージェントに対して、明確な失敗理由を報告するための処理。単に「ありません」と答えるのではなく、探したIDをメッセージに含めることで、システムとしての親切さと正確さを保っている。

  • f”…” (f-string):文字列の中に変数の値を直接埋め込む手法。
  • doc_id={q.doc_id}:「どのIDを探して見つからなかったのか」を明記している。
    • 理由: AIエージェントが複数の判例を並行して調べている場合、単に「見つかりませんでした」とだけ返すと、AIが「えっ、どのIDのこと?」と混乱する可能性がある。そこで、IDを添えることで、AIは「ID: D12345は見つからなかったから、ユーザーには別の判例を案内しよう」と正しく判断できる。
  • return:このメッセージを関数の「答え」としてAIに返して、処理を終了する。プログラミングの内部処理であれば、FalseNone を返すだけでも十分。しかし、相手がAIエージェントの場合、以下のように文章で状況を伝えることが大きな意味を持つ。
  1. AIの理解を助ける:「見つかりませんでした」という自然言語を受け取ったAIは、それをそのままユーザーへの回答に組み込んだり、「入力したIDが間違っていませんか?」と聞き返したりといった、人間らしい柔軟な対応ができるようになる。
  2. ハルシネーションの防止:曖昧な返答(空のデータなど)を返すと、AIが気を利かせすぎて架空の判例を捏造してしまうリスクがある。明確に「ない」と言い切ることで、AIに「ないという事実」を正しく認識させる。