Files
evotraders/backend/agents/prompts/builder.py

300 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""PromptBuilder for constructing system prompts from workspace markdown files.
Based on CoPaw design - loads AGENTS.md, SOUL.md, PROFILE.md, etc. from
agent workspace directories with YAML frontmatter support.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
import yaml
logger = logging.getLogger(__name__)
DEFAULT_SYS_PROMPT = """You are a helpful trading analysis assistant."""
class PromptBuilder:
"""Builder for constructing system prompts from markdown files.
Loads markdown configuration files from agent workspace directories,
supporting YAML frontmatter for metadata extraction.
"""
DEFAULT_FILES = [
"AGENTS.md",
"SOUL.md",
"PROFILE.md",
"POLICY.md",
"MEMORY.md",
]
TITLE_MAP: Dict[str, str] = {
"AGENTS.md": "Agent Guide",
"SOUL.md": "Soul",
"PROFILE.md": "Profile",
"POLICY.md": "Policy",
"MEMORY.md": "Memory",
"BOOTSTRAP.md": "Bootstrap",
}
def __init__(
self,
workspace_dir: Path,
enabled_files: Optional[List[str]] = None,
):
"""Initialize prompt builder.
Args:
workspace_dir: Directory containing markdown configuration files
enabled_files: List of filenames to load (if None, uses defaults)
"""
self.workspace_dir = Path(workspace_dir)
self.enabled_files = enabled_files or self.DEFAULT_FILES.copy()
self._prompt_parts: List[str] = []
self._metadata: Dict[str, Any] = {}
self.loaded_count = 0
def _load_file(self, filename: str) -> tuple[str, Optional[Dict[str, Any]]]:
"""Load a single markdown file with YAML frontmatter support.
Args:
filename: Name of the file to load
Returns:
Tuple of (content, metadata dict or None)
"""
file_path = self.workspace_dir / filename
if not file_path.exists():
logger.debug("File %s not found in %s, skipping", filename, self.workspace_dir)
return "", None
try:
raw_content = file_path.read_text(encoding="utf-8").strip()
if not raw_content:
logger.debug("Skipped empty file: %s", filename)
return "", None
content, metadata = self._parse_frontmatter(raw_content)
if content:
self.loaded_count += 1
logger.debug("Loaded %s (metadata: %s)", filename, bool(metadata))
return content, metadata
except Exception as e:
logger.warning("Failed to read file %s: %s, skipping", filename, e)
return "", None
def _parse_frontmatter(self, raw_content: str) -> tuple[str, Optional[Dict[str, Any]]]:
"""Parse YAML frontmatter from markdown content.
Args:
raw_content: Raw file content
Returns:
Tuple of (content without frontmatter, metadata dict or None)
"""
if not raw_content.startswith("---"):
return raw_content, None
parts = raw_content.split("---", 2)
if len(parts) < 3:
return raw_content, None
frontmatter = parts[1].strip()
content = parts[2].strip()
try:
metadata = yaml.safe_load(frontmatter) or {}
if not isinstance(metadata, dict):
metadata = {}
return content, metadata
except yaml.YAMLError as e:
logger.warning("Failed to parse YAML frontmatter: %s", e)
return content, None
def _append_section(self, title: str, content: str) -> None:
"""Append a section to the prompt parts.
Args:
title: Section title
content: Section content
"""
content = content.strip()
if not content:
return
if self._prompt_parts:
self._prompt_parts.append("")
self._prompt_parts.append(f"## {title}")
self._prompt_parts.append("")
self._prompt_parts.append(content)
def build(self) -> str:
"""Build the system prompt from markdown files.
Returns:
Constructed system prompt string
"""
self._prompt_parts = []
self._metadata = {}
self.loaded_count = 0
for filename in self.enabled_files:
content, metadata = self._load_file(filename)
if metadata:
self._metadata[filename] = metadata
if content:
title = self.TITLE_MAP.get(filename, filename.replace(".md", ""))
self._append_section(title, content)
if not self._prompt_parts:
logger.warning("No content loaded from workspace: %s", self.workspace_dir)
return DEFAULT_SYS_PROMPT
final_prompt = "\n".join(self._prompt_parts)
logger.debug(
"System prompt built from %d file(s), total length: %d chars",
self.loaded_count,
len(final_prompt),
)
return final_prompt
def get_metadata(self) -> Dict[str, Any]:
"""Get metadata collected from YAML frontmatter.
Returns:
Dictionary mapping filenames to their metadata
"""
return self._metadata.copy()
def get_agent_identity(self) -> Optional[Dict[str, Any]]:
"""Extract agent identity from PROFILE.md metadata.
Returns:
Identity dict with name, role, etc. or None
"""
profile_meta = self._metadata.get("PROFILE.md", {})
if not profile_meta:
return None
return {
"name": profile_meta.get("name", "Unknown"),
"role": profile_meta.get("role", ""),
"expertise": profile_meta.get("expertise", []),
"style": profile_meta.get("style", ""),
}
def build_system_prompt_from_workspace(
workspace_dir: Path,
enabled_files: Optional[List[str]] = None,
agent_id: Optional[str] = None,
extra_context: Optional[str] = None,
) -> str:
"""Build system prompt from workspace markdown files.
This is the main entry point for building system prompts from
agent workspace directories.
Args:
workspace_dir: Directory containing markdown configuration files
enabled_files: List of filenames to load (if None, uses defaults)
agent_id: Agent identifier to include in system prompt
extra_context: Additional context to append to the prompt
Returns:
Constructed system prompt string
"""
builder = PromptBuilder(
workspace_dir=workspace_dir,
enabled_files=enabled_files,
)
prompt = builder.build()
# Add agent identity header if agent_id provided
if agent_id and agent_id != "default":
identity_header = (
f"# Agent Identity\n\n"
f"Your agent ID is `{agent_id}`. "
f"This is your unique identifier in the multi-agent system.\n\n"
)
prompt = identity_header + prompt
# Append extra context if provided
if extra_context:
prompt = prompt + "\n\n" + extra_context
return prompt
def build_bootstrap_guidance(language: str = "zh") -> str:
"""Build bootstrap guidance message for first-time setup.
Args:
language: Language code (zh/en)
Returns:
Formatted bootstrap guidance message
"""
if language == "zh":
return (
"# 引导模式\n"
"\n"
"工作目录中存在 `BOOTSTRAP.md` — 首次设置。\n"
"\n"
"1. 阅读 BOOTSTRAP.md友好地表示初次见面"
"引导用户完成设置。\n"
"2. 按照 BOOTSTRAP.md 的指示,"
"帮助用户定义你的身份和偏好。\n"
"3. 按指南创建/更新必要文件"
"PROFILE.md、MEMORY.md 等)。\n"
"4. 完成后删除 BOOTSTRAP.md。\n"
"\n"
"如果用户希望跳过,直接回答下面的问题即可。\n"
"\n"
"---\n"
"\n"
)
return (
"# BOOTSTRAP MODE\n"
"\n"
"`BOOTSTRAP.md` exists — first-time setup.\n"
"\n"
"1. Read BOOTSTRAP.md, greet the user, "
"and guide them through setup.\n"
"2. Follow BOOTSTRAP.md instructions "
"to define identity and preferences.\n"
"3. Create/update files "
"(PROFILE.md, MEMORY.md, etc.) as described.\n"
"4. Delete BOOTSTRAP.md when done.\n"
"\n"
"If the user wants to skip, answer their "
"question directly instead.\n"
"\n"
"---\n"
"\n"
)
__all__ = [
"PromptBuilder",
"build_system_prompt_from_workspace",
"build_bootstrap_guidance",
"DEFAULT_SYS_PROMPT",
]