160 lines
4.7 KiB
Python
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("公開サーバ設定が不足しています")
|