207 lines
No EOL
8.8 KiB
Python
Executable file
207 lines
No EOL
8.8 KiB
Python
Executable file
"""
|
|
Prompt Loading Utility for Synthetic Society
|
|
This utility provides centralized prompt loading from markdown files
|
|
with template variable substitution support.
|
|
"""
|
|
|
|
import os
|
|
from typing import Dict, Any, Optional
|
|
from pathlib import Path
|
|
|
|
class PromptLoaderError(Exception):
|
|
"""Exception raised for errors in prompt loading."""
|
|
pass
|
|
|
|
class PromptLoader:
|
|
"""Centralized prompt loading service."""
|
|
|
|
def __init__(self, prompts_dir: Optional[str] = None):
|
|
"""
|
|
Initialize the prompt loader.
|
|
|
|
Args:
|
|
prompts_dir: Optional directory path for prompts. Defaults to
|
|
backend/prompts relative to this file's location.
|
|
"""
|
|
if prompts_dir:
|
|
self.prompts_dir = Path(prompts_dir)
|
|
else:
|
|
# Default to prompts directory in backend folder
|
|
current_file = Path(__file__)
|
|
backend_dir = current_file.parent.parent.parent # Go up from app/utils to backend
|
|
self.prompts_dir = backend_dir / "prompts"
|
|
|
|
if not self.prompts_dir.exists():
|
|
raise PromptLoaderError(f"Prompts directory not found: {self.prompts_dir}")
|
|
|
|
def load_prompt(self, prompt_name: str, variables: Optional[Dict[str, Any]] = None) -> str:
|
|
"""
|
|
Load a prompt from a markdown file and substitute template variables.
|
|
|
|
Args:
|
|
prompt_name: Name of the prompt file (without .md extension)
|
|
variables: Dictionary of variables to substitute in the template
|
|
|
|
Returns:
|
|
The prompt text with variables substituted
|
|
|
|
Raises:
|
|
PromptLoaderError: If the prompt file is not found or cannot be processed
|
|
"""
|
|
try:
|
|
prompt_file = self.prompts_dir / f"{prompt_name}.md"
|
|
|
|
if not prompt_file.exists():
|
|
raise PromptLoaderError(f"Prompt file not found: {prompt_file}")
|
|
|
|
# Read the prompt content
|
|
with open(prompt_file, 'r', encoding='utf-8') as f:
|
|
prompt_content = f.read().strip()
|
|
|
|
# Substitute variables if provided
|
|
if variables:
|
|
try:
|
|
# Handle JSON examples by protecting them from template substitution
|
|
# Look for JSON example blocks and temporarily replace them
|
|
json_blocks = []
|
|
import re
|
|
|
|
# Find JSON example blocks (between EXAMPLE_JSON_START and EXAMPLE_JSON_END)
|
|
json_pattern = r'EXAMPLE_JSON_START\s*\n(.*?)\nEXAMPLE_JSON_END'
|
|
matches = list(re.finditer(json_pattern, prompt_content, re.DOTALL))
|
|
|
|
# Replace in reverse order to avoid index shifting issues
|
|
for i, match in enumerate(reversed(matches)):
|
|
json_blocks.insert(0, match.group(1))
|
|
prompt_content = prompt_content[:match.start()] + f"__JSON_PLACEHOLDER_{len(matches)-1-i}__" + prompt_content[match.end():]
|
|
|
|
# Also protect any remaining JSON-like patterns that might have been missed
|
|
# This is a fallback for any {key: value} patterns
|
|
json_fallback_pattern = r'\{[^{}]*"[^"]*"[^{}]*\}'
|
|
fallback_matches = list(re.finditer(json_fallback_pattern, prompt_content))
|
|
|
|
for i, match in enumerate(reversed(fallback_matches)):
|
|
json_blocks.insert(0, match.group(0))
|
|
prompt_content = prompt_content[:match.start()] + f"__JSON_FALLBACK_{len(fallback_matches)-1-i}__" + prompt_content[match.end():]
|
|
|
|
# Now do the template substitution
|
|
prompt_content = prompt_content.format(**variables)
|
|
|
|
# Restore the JSON blocks
|
|
for i, json_block in enumerate(json_blocks[:len(matches)]):
|
|
prompt_content = prompt_content.replace(f"__JSON_PLACEHOLDER_{i}__", json_block)
|
|
|
|
# Restore fallback blocks
|
|
for i, json_block in enumerate(json_blocks[len(matches):]):
|
|
fallback_index = len(json_blocks[len(matches):]) - 1 - i
|
|
prompt_content = prompt_content.replace(f"__JSON_FALLBACK_{fallback_index}__", json_block)
|
|
|
|
except KeyError as e:
|
|
raise PromptLoaderError(f"Missing template variable in prompt '{prompt_name}': {e}")
|
|
except ValueError as e:
|
|
raise PromptLoaderError(f"Template formatting error in prompt '{prompt_name}': {e}")
|
|
|
|
return prompt_content
|
|
|
|
except Exception as e:
|
|
if isinstance(e, PromptLoaderError):
|
|
raise
|
|
raise PromptLoaderError(f"Error loading prompt '{prompt_name}': {str(e)}")
|
|
|
|
def list_available_prompts(self) -> list:
|
|
"""
|
|
List all available prompt files.
|
|
|
|
Returns:
|
|
List of prompt names (without .md extension)
|
|
"""
|
|
try:
|
|
prompt_files = []
|
|
for file_path in self.prompts_dir.glob("*.md"):
|
|
prompt_files.append(file_path.stem)
|
|
return sorted(prompt_files)
|
|
except Exception as e:
|
|
raise PromptLoaderError(f"Error listing prompts: {str(e)}")
|
|
|
|
def validate_prompt_variables(self, prompt_name: str, variables: Dict[str, Any]) -> bool:
|
|
"""
|
|
Validate that all required variables are provided for a prompt.
|
|
|
|
Args:
|
|
prompt_name: Name of the prompt file
|
|
variables: Dictionary of variables to validate
|
|
|
|
Returns:
|
|
True if all required variables are present
|
|
|
|
Raises:
|
|
PromptLoaderError: If validation fails
|
|
"""
|
|
try:
|
|
# Load the raw prompt to check for template variables
|
|
prompt_file = self.prompts_dir / f"{prompt_name}.md"
|
|
|
|
if not prompt_file.exists():
|
|
raise PromptLoaderError(f"Prompt file not found: {prompt_file}")
|
|
|
|
with open(prompt_file, 'r', encoding='utf-8') as f:
|
|
prompt_content = f.read()
|
|
|
|
# Try to format with provided variables to see if any are missing
|
|
try:
|
|
# Handle JSON examples by protecting them from template substitution
|
|
import re
|
|
json_blocks = []
|
|
json_pattern = r'EXAMPLE_JSON_START\s*\n(.*?)\nEXAMPLE_JSON_END'
|
|
matches = list(re.finditer(json_pattern, prompt_content, re.DOTALL))
|
|
|
|
# Replace in reverse order to avoid index shifting issues
|
|
for i, match in enumerate(reversed(matches)):
|
|
json_blocks.insert(0, match.group(1))
|
|
prompt_content = prompt_content[:match.start()] + f"__JSON_PLACEHOLDER_{len(matches)-1-i}__" + prompt_content[match.end():]
|
|
|
|
# Also protect any remaining JSON-like patterns
|
|
json_fallback_pattern = r'\{[^{}]*"[^"]*"[^{}]*\}'
|
|
fallback_matches = list(re.finditer(json_fallback_pattern, prompt_content))
|
|
|
|
for i, match in enumerate(reversed(fallback_matches)):
|
|
json_blocks.insert(0, match.group(0))
|
|
prompt_content = prompt_content[:match.start()] + f"__JSON_FALLBACK_{len(fallback_matches)-1-i}__" + prompt_content[match.end():]
|
|
|
|
prompt_content.format(**variables)
|
|
return True
|
|
except KeyError as e:
|
|
raise PromptLoaderError(f"Missing required variable for prompt '{prompt_name}': {e}")
|
|
|
|
except Exception as e:
|
|
if isinstance(e, PromptLoaderError):
|
|
raise
|
|
raise PromptLoaderError(f"Error validating prompt variables: {str(e)}")
|
|
|
|
# Global prompt loader instance
|
|
_prompt_loader = None
|
|
|
|
def get_prompt_loader() -> PromptLoader:
|
|
"""
|
|
Get the global prompt loader instance.
|
|
|
|
Returns:
|
|
The global PromptLoader instance
|
|
"""
|
|
global _prompt_loader
|
|
if _prompt_loader is None:
|
|
_prompt_loader = PromptLoader()
|
|
return _prompt_loader
|
|
|
|
def load_prompt(prompt_name: str, variables: Optional[Dict[str, Any]] = None) -> str:
|
|
"""
|
|
Convenience function to load a prompt using the global loader.
|
|
|
|
Args:
|
|
prompt_name: Name of the prompt file (without .md extension)
|
|
variables: Dictionary of variables to substitute in the template
|
|
|
|
Returns:
|
|
The prompt text with variables substituted
|
|
"""
|
|
return get_prompt_loader().load_prompt(prompt_name, variables) |