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

160 lines
4.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class PathConfig:
project_root: Path
pdf_root: Path
database: Path
publish_database: Path
tmp_dir: Path
text_dir: Path
log_dir: Path
papers_org: Path
@dataclass(frozen=True)
class RemoteConfig:
host: str
database_dir: str
pdf_dir: str
@dataclass(frozen=True)
class AIConfig:
backend: str
claude_permission_mode: str
claude_allowed_tools: tuple[str, ...]
codex_model: str | None
codex_sandbox: str
codex_full_auto: bool
@property
def claude_allowed_tools_argument(self) -> str:
return ",".join(self.claude_allowed_tools)
@dataclass(frozen=True)
class ProcessingConfig:
scan_directories: tuple[str, ...]
ignore_directories: tuple[str, ...]
max_papers: int
initial_pages: int
fallback_pages: int
min_text_length: int
update_database: bool
validate_database: bool
export_org: bool
backup_database: bool
@dataclass(frozen=True)
class DomainConfig:
name: str
description: str
categories: tuple[str, ...]
default_category: str
summary_length_ja: str
summary_focus: tuple[str, ...]
relevance_criteria: tuple[str, ...]
priorities: tuple[str, ...] = ("A", "B", "C", "D")
def validate(self) -> None:
if not self.name.strip():
raise ValueError("domain.nameが空です")
if not self.description.strip():
raise ValueError("domain.descriptionが空です")
if not self.categories:
raise ValueError("domain.categoriesが空です")
if len(set(self.categories)) != len(self.categories):
raise ValueError("domain.categoriesに重複があります")
if self.default_category not in self.categories:
raise ValueError(
"default_categoryをcategoriesに含めてください"
)
if len(self.relevance_criteria) != 6:
raise ValueError(
"relevance_criteriaは0から5の6項目にしてください"
)
def categories_prompt(self) -> str:
return "\n".join(
f"- {category}" for category in self.categories
)
def summary_focus_prompt(self) -> str:
return "\n".join(
f"- {item}" for item in self.summary_focus
)
def relevance_prompt(self) -> str:
return "\n".join(
f"- {score}: {description}"
for score, description in enumerate(
self.relevance_criteria
)
)
@dataclass(frozen=True)
class Config:
path: PathConfig
remote: RemoteConfig
ai: AIConfig
processing: ProcessingConfig
domain: DomainConfig
@property
def scan_paths(self) -> tuple[Path, ...]:
return tuple(
self.path.pdf_root / directory
for directory in self.processing.scan_directories
)
def validate(self, *, require_remote: bool = False) -> None:
root = self.path.project_root.resolve()
if not root.is_dir():
raise NotADirectoryError(root)
if not self.path.pdf_root.is_dir():
raise NotADirectoryError(
f"PDF_ROOTがありません: {self.path.pdf_root}"
)
if not self.processing.scan_directories:
raise ValueError("scan_directoriesが空です")
for path in self.scan_paths:
if not path.is_dir():
raise NotADirectoryError(
f"探索ディレクトリがありません: {path}"
)
if self.processing.max_papers < 1:
raise ValueError("max_papersは1以上にしてください")
if self.processing.initial_pages < 1:
raise ValueError("initial_pagesは1以上にしてください")
if (
self.processing.fallback_pages
< self.processing.initial_pages
):
raise ValueError(
"fallback_pagesはinitial_pages以上にしてください"
)
if self.processing.min_text_length < 0:
raise ValueError(
"min_text_lengthは0以上にしてください"
)
if self.ai.backend.lower() not in {"claude", "codex"}:
raise ValueError(
"ai.backendはclaudeまたはcodexにしてください"
)
self.domain.validate()
if require_remote:
values = (
self.remote.host,
self.remote.database_dir,
self.remote.pdf_dir,
)
if any(not value.strip() for value in values):
raise ValueError("公開サーバ設定が不足しています")