Files
paper_db/paper_db_common/scripts/run_ai.py
T
2026-07-30 10:51:43 +09:00

221 lines
6.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""設定に応じてClaude CodeまたはCodex CLIを実行する。"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from common import CONFIG
COMMON_ROOT = Path(__file__).resolve().parents[1]
INSTRUCTIONS_FILE = COMMON_ROOT / "CLAUDE.md"
AI_PROMPT = """
以下の共通指示に従い、TASK=process_new_papersを
追加確認なしで最後まで実行してください。
--- 共通指示開始 ---
{common_instructions}
--- 共通指示終了 ---
tmp/text/には、未登録PDFから抽出したテキストがあります。
対象分野:
{domain_name}
対象分野の説明:
{domain_description}
categoryは次のいずれかを使用してください。
{categories}
日本語要約は{summary_length_ja}を目安とし、
次の観点を優先してください。
{summary_focus}
relevanceは次の基準で判定してください。
{relevance_criteria}
Priorityは次の対応にしてください。
- A: relevance=5
- B: relevance=4
- C: relevance=3
- D: relevance=0〜2
行う処理は次だけです。
- tmp/text/にあるテキストファイルを解析する
- 各テキスト先頭のPDF_PATHをJSONのpdf_pathに使う
- DOI、書誌情報、要約、キーワード、分類を抽出する
- DOIがある場合はlanding_urlを生成する
- 必要最小限のWeb検索で公開PDFを確認する
- 各論文についてtmp/paper-*.jsonを作成する
- 作成件数と結果を日本語で報告する
原則としてPDFは直接読まないでください。
次の場合のみPDF_PATHに記載された元PDFを参照できます。
- PDF_TEXT_EMPTYがyes
- TEXT_INSUFFICIENTがyes
- TEXT_LENGTHが{min_text_length}未満
- テキストが文字化けしている
- タイトル、著者、発行年、要約を確認できない
元PDFを参照する場合も、PDF_PATHに記載された1ファイルだけを読み、
他のPDFやディレクトリを探索しないでください。
確認できない内容、DOI、pdf_urlは推測しないでください。
papers.dbの直接更新、DB検証、Org出力は実行しないでください。
最大{max_papers}件だけ処理してください。
""".strip()
@dataclass(frozen=True)
class RuntimeOptions:
backend: str
max_papers: int
def positive_int(value: str) -> int:
number = int(value)
if number < 1:
raise argparse.ArgumentTypeError(
"1以上の整数を指定してください"
)
return number
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Claude CodeまたはCodex CLIを実行する"
)
parser.add_argument(
"--backend",
choices=("claude", "codex"),
default=None,
)
parser.add_argument(
"--max-papers",
type=positive_int,
default=None,
metavar="N",
)
return parser.parse_args()
def resolve_options(args: argparse.Namespace) -> RuntimeOptions:
return RuntimeOptions(
backend=args.backend or CONFIG.ai.backend,
max_papers=(
args.max_papers
if args.max_papers is not None
else CONFIG.processing.max_papers
),
)
def load_common_instructions() -> str:
if not INSTRUCTIONS_FILE.is_file():
raise FileNotFoundError(
f"共通CLAUDE.mdがありません: {INSTRUCTIONS_FILE}"
)
return INSTRUCTIONS_FILE.read_text(encoding="utf-8").strip()
def build_prompt(max_papers: int) -> str:
domain = CONFIG.domain
return AI_PROMPT.format(
common_instructions=load_common_instructions(),
domain_name=domain.name,
domain_description=domain.description,
categories=domain.categories_prompt(),
summary_length_ja=domain.summary_length_ja,
summary_focus=domain.summary_focus_prompt(),
relevance_criteria=domain.relevance_prompt(),
min_text_length=CONFIG.processing.min_text_length,
max_papers=max_papers,
)
def build_claude_command() -> list[str]:
command = [
"claude",
"--print",
"--add-dir",
str(CONFIG.path.text_dir),
"--permission-mode",
CONFIG.ai.claude_permission_mode,
"--allowedTools",
CONFIG.ai.claude_allowed_tools_argument,
]
for path in CONFIG.scan_paths:
command.extend(["--add-dir", str(path)])
return command
def build_codex_command(max_papers: int) -> list[str]:
command = [
"codex",
"exec",
"--cd",
str(CONFIG.path.project_root),
"--sandbox",
CONFIG.ai.codex_sandbox,
]
if CONFIG.ai.codex_model:
command.extend(["--model", CONFIG.ai.codex_model])
if CONFIG.ai.codex_full_auto:
command.append("--full-auto")
command.append(build_prompt(max_papers))
return command
def build_command(options: RuntimeOptions) -> list[str]:
if options.backend == "claude":
return build_claude_command()
if options.backend == "codex":
return build_codex_command(options.max_papers)
raise ValueError(f"未対応のAIバックエンドです: {options.backend}")
def require_backend_command(backend: str) -> None:
executable = {"claude": "claude", "codex": "codex"}.get(backend)
if executable is None:
raise ValueError(f"未対応のAIバックエンドです: {backend}")
if shutil.which(executable) is None:
raise RuntimeError(
f"必要なコマンドが見つかりません: {executable}"
)
def main() -> int:
args = parse_args()
options = resolve_options(args)
try:
CONFIG.validate()
require_backend_command(options.backend)
command = build_command(options)
prompt = build_prompt(options.max_papers)
completed = subprocess.run(
command,
cwd=CONFIG.path.project_root,
input=(prompt if options.backend == "claude" else None),
text=True,
check=False,
)
return completed.returncode
except Exception as exc:
print(f"run_ai.py: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())