Initial commit
This commit is contained in:
Executable
+220
@@ -0,0 +1,220 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user