feat: Add agent workspace system and runtime management
- Add agent core modules (agent_core, factory, registry, skill_loader) - Add runtime system for agent execution management - Add REST API for agents, workspaces, and runtime control - Add process supervisor for agent lifecycle management - Add workspace template system with agent profiles - Add frontend RuntimeView and runtime API integration - Add per-agent skill workspaces for smoke_fullstack run - Refactor skill system with active/installed separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
19
backend/agents/prompts/__init__.py
Normal file
19
backend/agents/prompts/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Prompt building utilities for EvoAgent.
|
||||
|
||||
This module provides prompt construction from workspace markdown files
|
||||
with YAML frontmatter support.
|
||||
"""
|
||||
from .builder import (
|
||||
PromptBuilder,
|
||||
build_system_prompt_from_workspace,
|
||||
build_bootstrap_guidance,
|
||||
DEFAULT_SYS_PROMPT,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PromptBuilder",
|
||||
"build_system_prompt_from_workspace",
|
||||
"build_bootstrap_guidance",
|
||||
"DEFAULT_SYS_PROMPT",
|
||||
]
|
||||
305
backend/agents/prompts/builder.py
Normal file
305
backend/agents/prompts/builder.py
Normal file
@@ -0,0 +1,305 @@
|
||||
# -*- 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",
|
||||
"ROLE.md",
|
||||
"POLICY.md",
|
||||
"MEMORY.md",
|
||||
"HEARTBEAT.md",
|
||||
"STYLE.md",
|
||||
]
|
||||
|
||||
TITLE_MAP: Dict[str, str] = {
|
||||
"AGENTS.md": "Agent Guide",
|
||||
"SOUL.md": "Soul",
|
||||
"PROFILE.md": "Profile",
|
||||
"ROLE.md": "Role",
|
||||
"POLICY.md": "Policy",
|
||||
"MEMORY.md": "Memory",
|
||||
"HEARTBEAT.md": "Heartbeat",
|
||||
"STYLE.md": "Style",
|
||||
"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",
|
||||
]
|
||||
Reference in New Issue
Block a user