novel-doomsday-resurgence/skills/fanfic-writer/scripts/v2/prompt_registry.py
唐天洛 cb9b16e5a8 初始提交:番茄小说创作工作区
包含:
- 核心配置文件(AGENTS.md, SOUL.md, USER.md等)
- 记忆系统(memory/文件夹)
- 技能库(skills/文件夹)
- 小说内容(novel/文件夹)
- .gitignore配置
2026-03-30 15:46:26 +08:00

380 lines
13 KiB
Python

"""
Fanfic Writer v2.0 - Prompt Registry
Manages prompt templates with versioning and audit trail
"""
import json
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
from dataclasses import dataclass
from .atomic_io import atomic_write_json
from .utils import get_timestamp_iso
@dataclass
class PromptTemplate:
"""A registered prompt template"""
name: str
path: Path
version: str
description: str
source: str # 'v1', 'v2_addons', or 'builtin'
def to_dict(self) -> Dict[str, Any]:
return {
'name': self.name,
'path': str(self.path),
'version': self.version,
'description': self.description,
'source': self.source
}
class PromptRegistry:
"""
Manages prompt template registry
Ensures v1.0 prompts are used for core functions (chapter_outline, chapter_draft)
as required by design spec.
"""
# Required templates with their expected sources
REQUIRED_TEMPLATES = {
'chapter_outline': {'source': 'v1', 'required': True},
'chapter_draft': {'source': 'v1', 'required': True},
'critic_editor': {'source': 'v2_addons', 'required': True},
'critic_logic': {'source': 'v2_addons', 'required': True},
'critic_continuity': {'source': 'v2_addons', 'required': True},
'backpatch_plan': {'source': 'v2_addons', 'required': True},
'sanitizer': {'source': 'v2_addons', 'required': True},
'main_outline': {'source': 'v1', 'required': True},
'chapter_plan': {'source': 'v1', 'required': True},
'world_building': {'source': 'v1', 'required': True},
'style_guide': {'source': 'v1', 'required': False},
'qc_evaluate': {'source': 'v2_addons', 'required': True}
}
def __init__(self, run_dir: Path, skill_dir: Path):
"""
Initialize PromptRegistry
Args:
run_dir: Current run directory
skill_dir: Skill root directory (for accessing prompts/)
"""
self.run_dir = Path(run_dir)
self.skill_dir = Path(skill_dir)
self.registry_path = self.run_dir / "4-state" / "prompt_registry.json"
self.prompts_dir = self.skill_dir / "prompts"
self._registry: Optional[Dict[str, Any]] = None
self._templates: Dict[str, PromptTemplate] = {}
def initialize(self, run_id: str, skill_version: str = "2.0.0") -> bool:
"""
Initialize registry for a new run
Returns:
True on success
Raises:
RuntimeError: If required v1 templates are missing (blocking error)
"""
# Discover available templates
templates = self._discover_templates()
# Validate required templates
missing_required = []
for name, spec in self.REQUIRED_TEMPLATES.items():
if spec['required'] and name not in templates:
missing_required.append(name)
if missing_required:
raise RuntimeError(
f"Missing required prompt templates: {missing_required}. "
f"Auto mode requires v1.0 templates for chapter_outline and chapter_draft."
)
# Verify audit chain capability (logs/prompts/ must be writable)
audit_dir = self.run_dir / "logs" / "prompts"
try:
audit_dir.mkdir(parents=True, exist_ok=True)
# Test write access
test_file = audit_dir / ".write_test"
test_file.write_text("test")
test_file.unlink()
except Exception as e:
raise RuntimeError(
f"CRITICAL: Cannot create prompt audit directory: {audit_dir}. "
f"Audit chain is mandatory per design spec. Error: {e}"
)
# Build registry
self._registry = {
'run_id': run_id,
'skill_version': skill_version,
'initialized_at': get_timestamp_iso(),
'templates': {name: tmpl.to_dict() for name, tmpl in templates.items()},
'audit_chain': {
'enabled': True,
'directory': str(audit_dir),
'mandatory': True # Design spec: missing audit = fatal
}
}
self._templates = templates
# Save registry
return atomic_write_json(self.registry_path, self._registry)
def load(self) -> Dict[str, Any]:
"""Load existing registry"""
if self._registry is not None:
return self._registry
if not self.registry_path.exists():
raise FileNotFoundError(f"Prompt registry not found: {self.registry_path}")
with open(self.registry_path, 'r', encoding='utf-8') as f:
self._registry = json.load(f)
# Rebuild template objects
self._templates = {}
for name, data in self._registry.get('templates', {}).items():
self._templates[name] = PromptTemplate(
name=data['name'],
path=Path(data['path']),
version=data['version'],
description=data['description'],
source=data['source']
)
return self._registry
def _discover_templates(self) -> Dict[str, PromptTemplate]:
"""Discover all available prompt templates"""
templates = {}
# Check v1 prompts
v1_dir = self.prompts_dir / "v1"
if v1_dir.exists():
for md_file in v1_dir.glob("*.md"):
name = md_file.stem
templates[name] = PromptTemplate(
name=name,
path=md_file,
version="1.0.0",
description=f"v1.0 template: {name}",
source="v1"
)
# Check v2 addons
v2_dir = self.prompts_dir / "v2_addons"
if v2_dir.exists():
for md_file in v2_dir.glob("*.md"):
name = md_file.stem
# v2 overrides v1 if same name
templates[name] = PromptTemplate(
name=name,
path=md_file,
version="2.0.0",
description=f"v2.0 addon: {name}",
source="v2_addons"
)
return templates
def get_template(self, name: str) -> Optional[PromptTemplate]:
"""Get a template by name"""
self.load()
return self._templates.get(name)
def get_template_content(self, name: str) -> Optional[str]:
"""Get the actual content of a template"""
template = self.get_template(name)
if not template:
return None
if not template.path.exists():
return None
with open(template.path, 'r', encoding='utf-8') as f:
return f.read()
def validate_for_auto_mode(self) -> Tuple[bool, List[str]]:
"""
Validate that registry is ready for auto mode
Returns:
(is_valid, list_of_errors)
"""
errors = []
try:
self.load()
except FileNotFoundError as e:
return False, [str(e)]
# Check v1 templates for auto mode
for name, spec in self.REQUIRED_TEMPLATES.items():
if spec['source'] == 'v1' and spec['required']:
template = self._templates.get(name)
if not template:
errors.append(f"Missing required v1 template: {name}")
elif template.source != 'v1':
errors.append(
f"Template {name} must be from v1 source for auto mode, "
f"got: {template.source}"
)
elif not template.path.exists():
errors.append(f"Template file missing: {template.path}")
return len(errors) == 0, errors
def list_templates(self) -> List[PromptTemplate]:
"""List all registered templates"""
self.load()
return list(self._templates.values())
def get_template_path(self, name: str) -> Optional[Path]:
"""Get the file path for a template"""
template = self.get_template(name)
return template.path if template else None
# ============================================================================
# Template Validation Helpers
# ============================================================================
def validate_template_content(content: str, template_type: str) -> Tuple[bool, List[str]]:
"""
Validate that a template contains required placeholders
Args:
content: Template content
template_type: Type of template (chapter_outline, chapter_draft, etc.)
Returns:
(is_valid, list_of_warnings)
"""
warnings = []
# Common placeholders all templates should have
common_placeholders = ['{', '}'] # Basic check for placeholder syntax
# Type-specific required placeholders
type_placeholders = {
'chapter_outline': [
'previous_chapter_content',
'chapter_summary',
'chapter_title',
'target_words'
],
'chapter_draft': [
'previous_chapter_content',
'detailed_outline',
'chapter_title',
'segment_summary',
'segment_words'
]
}
# Check for placeholder syntax
if '{' not in content or '}' not in content:
warnings.append("Template may be missing placeholder syntax {}")
# Check type-specific placeholders
required = type_placeholders.get(template_type, [])
for placeholder in required:
if placeholder not in content:
warnings.append(f"Template may be missing required placeholder: {placeholder}")
return len(warnings) == 0, warnings
# ============================================================================
# Default Template Generation (for initial setup)
# ============================================================================
def generate_default_templates(skill_dir: Path) -> bool:
"""
Generate default prompt templates if they don't exist
This is called during skill installation to ensure prompts/ directory
is populated with default templates.
"""
prompts_dir = Path(skill_dir) / "prompts"
prompts_dir.mkdir(parents=True, exist_ok=True)
v1_dir = prompts_dir / "v1"
v1_dir.mkdir(parents=True, exist_ok=True)
v2_dir = prompts_dir / "v2_addons"
v2_dir.mkdir(parents=True, exist_ok=True)
# Note: Actual template content should be written separately
# This function just ensures directory structure
return True
# ============================================================================
# Module Test
# ============================================================================
if __name__ == "__main__":
import tempfile
print("=== Prompt Registry Test ===\n")
with tempfile.TemporaryDirectory() as tmpdir:
# Create mock skill structure
skill_dir = Path(tmpdir) / "skill"
prompts_v1 = skill_dir / "prompts" / "v1"
prompts_v1.mkdir(parents=True)
# Create test templates
(prompts_v1 / "chapter_outline.md").write_text("# Chapter Outline\n{chapter_title}")
(prompts_v1 / "chapter_draft.md").write_text("# Chapter Draft\n{previous_chapter_content}")
prompts_v2 = skill_dir / "prompts" / "v2_addons"
prompts_v2.mkdir(parents=True)
(prompts_v2 / "critic_editor.md").write_text("# Critic Editor\nAnalyze:")
# Create run directory
run_dir = Path(tmpdir) / "run"
state_dir = run_dir / "4-state"
state_dir.mkdir(parents=True)
# Test registry initialization
registry = PromptRegistry(run_dir, skill_dir)
try:
registry.initialize("20260215_224500_TEST", "2.0.0")
print("[Test] Registry initialized: PASS")
except RuntimeError as e:
print(f"[Test] Registry initialized: FAIL - {e}")
# Test load
data = registry.load()
print(f"[Test] Registry loaded: {len(data['templates'])} templates")
# Test get template
tmpl = registry.get_template("chapter_outline")
print(f"[Test] Get template: {'PASS' if tmpl else 'FAIL'}")
if tmpl:
print(f" Source: {tmpl.source}")
# Test get content
content = registry.get_template_content("chapter_outline")
print(f"[Test] Get content: {'PASS' if content else 'FAIL'}")
# Test validation
is_valid, errors = registry.validate_for_auto_mode()
print(f"[Test] Auto mode validation: {'PASS' if is_valid else 'FAIL'}")
if errors:
for err in errors:
print(f" Error: {err}")
print("\n=== All tests completed ===")