Files
evotraders/backend/agents/prompt_loader.py
cillin 06a23c32a4 refactor: Fix code quality issues identified in analysis
1. Rename factory.py's EvoAgent data class to AgentConfig
   - Avoids naming conflict with base/evo_agent.py's EvoAgent

2. Export pipeline_runner functions in backend/core/__init__.py
   - Add create_agents, create_long_term_memory, stop_gateway

3. Consolidate PromptLoader to singleton pattern
   - Add get_prompt_loader() singleton function
   - Update all usages to use singleton

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 01:07:53 +08:00

154 lines
4.3 KiB
Python

# -*- coding: utf-8 -*-
"""
Prompt Loader - Unified management and loading of Agent Prompts
Supports Markdown and YAML formats
Uses simple string replacement, does not depend on Jinja2
"""
import re
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
# Singleton instance
_prompt_loader_instance: Optional["PromptLoader"] = None
def get_prompt_loader() -> "PromptLoader":
"""Get the singleton PromptLoader instance."""
global _prompt_loader_instance
if _prompt_loader_instance is None:
_prompt_loader_instance = PromptLoader()
return _prompt_loader_instance
class PromptLoader:
"""Unified Prompt loader"""
def __init__(self, prompts_dir: Optional[Path] = None):
"""
Initialize Prompt loader
Args:
prompts_dir: Prompts directory path,
defaults to prompts/ directory of current file
"""
if prompts_dir is None:
self.prompts_dir = Path(__file__).parent / "prompts"
else:
self.prompts_dir = Path(prompts_dir)
def load_prompt(
self,
agent_type: str,
prompt_name: str,
variables: Optional[Dict[str, Any]] = None,
) -> str:
"""
Load and render Prompt.
No caching — always reads fresh from disk (CoPaw-style).
"""
prompt_path = self.prompts_dir / agent_type / f"{prompt_name}.md"
if not prompt_path.exists():
raise FileNotFoundError(
f"Prompt file not found: {prompt_path}\n"
f"Please create the prompt file or check the path.",
)
with open(prompt_path, "r", encoding="utf-8") as f:
prompt_template = f.read()
# If variables provided, use simple string replacement
if variables:
rendered = self._render_template(prompt_template, variables)
else:
rendered = prompt_template
return rendered
def _render_template(
self,
template: str,
variables: Dict[str, Any],
) -> str:
"""
Render template using simple string replacement
Supports {{ variable }} syntax (compatible with previous Jinja2 format)
Args:
template: Template string
variables: Variable dictionary
Returns:
Rendered string
"""
rendered = template
# Replace {{ variable }} format
for key, value in variables.items():
# Support both {{ key }} and {{key}} formats
pattern1 = f"{{{{ {key} }}}}"
pattern2 = f"{{{{{key}}}}}"
rendered = rendered.replace(pattern1, str(value))
rendered = rendered.replace(pattern2, str(value))
return rendered
def _escape_json_braces(self, text: str) -> str:
"""
Escape braces in JSON code blocks, treating them as literals
Args:
text: Text to process
Returns:
Processed text
"""
def replace_code_block(match):
code_content = match.group(1)
# Escape all braces within code block
escaped = code_content.replace("{", "{{").replace("}", "}}")
return f"```json\n{escaped}\n```"
# Replace all braces in JSON code blocks
text = re.sub(
r"```json\n(.*?)\n```",
replace_code_block,
text,
flags=re.DOTALL,
)
return text
def load_yaml_config(
self,
agent_type: str,
config_name: str,
) -> Dict[str, Any]:
"""
Load YAML configuration file.
No caching — always reads fresh from disk (CoPaw-style).
"""
yaml_path = self.prompts_dir / agent_type / f"{config_name}.yaml"
if not yaml_path.exists():
raise FileNotFoundError(f"YAML config not found: {yaml_path}")
with open(yaml_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def clear_cache(self):
"""No-op — caching removed (CoPaw-style, always fresh reads)."""
pass
def reload_prompt(self, agent_type: str, prompt_name: str):
"""No-op — caching removed."""
pass
def reload_config(self, agent_type: str, config_name: str):
"""No-op — caching removed."""
pass