Agents SDKを使った判例検索コード( database_query.py )解析の4回目。引き続き「 検索ツール 」部分のコードについて1行ずつその働きを見ていく。
if q.court:
「q」(search_cases関数に渡す引数)に値が入っているか(検索条件として裁判所名が指定されているか)をチェックしている。入っていれば if 文中の処理を行う。
◼︎q の中身は、たとえば以下のようになる。
q = CaseSearch(
keywords=”発信者情報開示 プロバイダ責任制限法”,
court=”東京地裁”,
limit=5
)
where_parts.append("court LIKE ?")
if 文中の処理。検索条件として裁判所名が指定されていれば、検索条件を格納しているリスト(where_parts)の末尾に「”court LIKE ?”」という要素を追加(append)する。「court」はデータベース(cases.db)内にある裁判所名が格納された列の名前。「LIKE」は完全一致ではなく、「〜を含む」というあいまい検索を行うための命令。「?」はプレースホルダ(穴埋め用の場所)で、実際の裁判所名(例:%東京地裁%)が安全な方法でここに流し込まれる。
params.append(f"%{q.court}%")
前行の「?」(プレースホルダ)に検索文字列(裁判所名)を追加する処理。「 f”%{q.court}%” 」により、ユーザー(またはAI)が指定した裁判所名を、SQLのあいまい検索用の形式に変換している。たとえば、q.court が "東京地裁" なら、"%東京地裁%" という文字列に変換されて プレースホルダのリスト(params)末尾に格納される。
sql = f"""
SELECT doc_id, court, date, case_number, case_name
FROM cases
WHERE {" AND ".join(where_parts)}
ORDER BY date DESC
LIMIT ?
"""
これまでバラバラに準備してきた検索条件の部品を組み合わせて、データベースに送る最終的な命令書(SQL文)を完成させるための処理をしている。
各要素の意味
◼︎SELECT … FROM cases:データベースの「cases」という表から、画面に表示したい項目(ID、裁判所名、判決日、事件番号、事件名)だけをピックアップして持ってくるという宣言。
◼︎WHERE {” AND “.join(where_parts)} :このプログラムの心臓部。これまでリスト(where_parts)に溜めてきた条件(例:キーワード、裁判所名)を、"AND" でつなげて1つのWHERE句にしている。
直前のコードで、where_parts には、次のような文字列のリストが入っている。
where_parts = [
"(doc_id LIKE ? OR court LIKE ? OR ... OR facts_and_reasons LIKE ?)",
"(doc_id LIKE ? OR court LIKE ? OR ... OR facts_and_reasons LIKE ?)",
"court LIKE ?"
]
” AND “.join(where_parts) は、このリストの要素を" AND " を間に挟んで1つの文字列に連結するという処理を行なっている。この処理により以下のような1つの長いSQL文ができあがる。
(doc_id LIKE ? OR court LIKE ? OR ...)
AND
(doc_id LIKE ? OR court LIKE ? OR ...)
AND
court LIKE ?
これにより、すべての条件に一致する(AND検索)ものだけを絞り込むことができる。
◼︎ORDER BY date DESC:検索結果を判決日の新しい順(降順)に並び替える。最新の判例からチェックしたいというユーザーのニーズに応えるための設定。
◼︎LIMIT ?:最大件数(例:5件)で結果をカットするための処理。上限を設けることでサーバーが重くなるのを防いでいる。
params.append(int(q.limit))
検索結果の件数を制限するための最後の仕上げ。SQL文の最後にある LIMIT ? というプレースホルダに、具体的な数値を流し込む処理を行っている。
◼︎q.limit:前回のステップで作成した CaseSearch モデルの limit フィールドを取得している(デフォルトは5件、最大20件という制約をしている部分)。
◼︎int(…): 数値を確実に整数型として扱うための処理。Pydanticモデルですでに整数としてバリデーションされているが、データベースの種類によっては型に非常に厳格なものがあるため、念のため明示的に変換して安全性を高めている。
◼︎params.append(…):これまでキーワードや裁判所名を追加してきた params リストの一番最後に、q.limit で指定された件数を追加している。
3. 全体の完成図(paramsの中身)
ユーザーが「損害賠償」「東京地裁」で検索し、上限「5件」を指定した場合、最終的な params は以下のような1つの長いリストになる。
["%損害賠償%", ..., "%損害賠償%", "%東京地裁%", 5](キーワード分7個) + (裁判所分1個) + (件数分1個)
その意味するところは、次のとおりである。
casesという表から、5つの項目を選んで持ってきて。ただし、キーワードに「損害賠償」が含まれていて、かつ(AND)、裁判所名に「東京地裁」が含まれているものに限定して。さらに、日付が新しい順に並べて、最初の5件だけを見せて。
with _connect() as conn:
この with は、Pythonにおいてリソースの管理を自動化するための構文。データベース操作において、最も避けるべき失敗は接続(コネクション)を開きっぱなしにして、サーバーに負荷をかけ続けてしまうこと。with を使うことで、処理が終わった後(あるいは途中でエラーが起きた時でも)、自動的にデータベースとの接続を閉じてくれるようになる。
- _connect():データベースへ接続するための関数。実行すると、データベースへの専用の通り道を確保して戻してくれる。
- as conn:確保された通り道(接続オブジェクト)に conn という名前を付けて、その後の処理で使えるようにしている。
rows = conn.execute(sql, params).fetchall()
これまで準備してきたSQL文とデータ(params)をデータベースに渡し、検索を実行して結果を受け取る処理をしている。
if not rows:
return "該当なし。"
データベースから取得した結果(rows)が空(false)なら「該当なし」を戻り値として処理を終了する。
lines = []
表示用の文字列をためるリスト(lines)を作成している。
for i, r in enumerate(rows, 1):
検索結果(rows)を1件ずつ1から始まる番号付きで取得している。「i」には振られた番号(1, 2, 3…)が、「r」にはデータベースから取り出した1件分の判例データ(IDや裁判所名などのセット)が、それぞれ格納されている。
lines.append(
f"{i}. doc_id={r['doc_id']} / {r['court']} / {r['date']} / "
f"{r['case_number']} / {r['case_name']}"
)
データベースから取り出した生のデータを、AIや人間が一見して理解できる1行の要約レポートに書き換える処理を行なっている。これにより、見やすく整理されたデータが lines リストに格納される。
- {i}.:前回のステップで振った番号(1, 2, 3…)を冒頭に置く。
- doc_id={r[‘doc_id’]}:管理用のIDを明記する。AIが後で「このIDの詳しい内容を教えて」と判断しやすくするため。
- {r[‘court’]} / {r[‘date’]} / …:「裁判所名」「判決日」「事件番号」「事件名」という、判例を特定するために欠かせない5つの重要項目を、スラッシュ(/)で区切って並べている。
return "\n".join(lines)
整理したデータ(lines)を1つの大きな検索結果レポートとしてAIに返している。
- “\n”:改行を意味する特殊記号。
- .join(lines):lines リストに保管されている各判例の1行要約の間に改行を挟み込みながら、すべての要素を1つの長い文字列に合体させる。
- return:完成した最終的な検索結果を、関数の外にいるAIエージェントへと返している。
最終的に返される検索結果のイメージは以下のようになる。
1. doc_id=AAA / 東京地裁 / 令和7年3月7日 / 令和6年(ワ)70278号 / 発信者情報開示請求事件
2. doc_id=BBB / 大阪地裁 / 令和6年12月1日 / 令和6年(ワ)12345号 / 損害賠償請求事件
3. doc_id=CCC / 東京高裁 / 令和5年5月20日 / 令和5年(ネ)54321号 / 請求異議控訴事件
これで「検索ツール」部分の解析は終わり。次回から「本文取得ツール」部分の解析に着手する。