OSS restructuring for open-core model: - Rename package from agentserver/ to xml_pipeline/ - Update all imports (44 Python files, 31 docs/configs) - Update pyproject.toml for OSS distribution (v0.3.0) - Move prompt_toolkit from core to optional [console] extra - Remove auth/server/lsp from core optional deps (-> Nextra) New console example in examples/console/: - Self-contained demo with handlers and config - Uses prompt_toolkit (optional, falls back to input()) - No password auth, no TUI, no LSP — just the basics - Shows how to use xml-pipeline as a library Import changes: - from agentserver.* -> from xml_pipeline.* - CLI entry points updated: xml_pipeline.cli:main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
243 lines
6.5 KiB
Python
243 lines
6.5 KiB
Python
"""
|
|
Agent configuration management.
|
|
|
|
Each agent has its own YAML config file in ~/.xml-pipeline/agents/
|
|
containing behavior settings (prompt, model, temperature, etc.)
|
|
|
|
The main organism.yaml defines wiring (listeners, peers, routing).
|
|
Agent YAML files define behavior.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field, asdict
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any, List
|
|
import yaml
|
|
|
|
|
|
CONFIG_DIR = Path.home() / ".xml-pipeline"
|
|
AGENTS_DIR = CONFIG_DIR / "agents"
|
|
|
|
|
|
@dataclass
|
|
class AgentConfig:
|
|
"""
|
|
Configuration for an individual agent.
|
|
|
|
Stored in ~/.xml-pipeline/agents/{name}.yaml
|
|
"""
|
|
name: str
|
|
|
|
# System prompt for the LLM
|
|
prompt: str = ""
|
|
|
|
# Model selection
|
|
model: str = "default" # "default", "claude-sonnet", "claude-opus", "gpt-4", etc.
|
|
|
|
# Generation parameters
|
|
temperature: float = 0.7
|
|
max_tokens: int = 4096
|
|
|
|
# Behavior flags
|
|
verbose: bool = False # Log detailed reasoning
|
|
confirm_actions: bool = False # Ask before tool calls
|
|
|
|
# Tool permissions (if empty, uses defaults from wiring)
|
|
allowed_tools: List[str] = field(default_factory=list)
|
|
blocked_tools: List[str] = field(default_factory=list)
|
|
|
|
# Custom metadata
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dict for YAML serialization."""
|
|
d = asdict(self)
|
|
# Remove name from dict (it's the filename)
|
|
del d["name"]
|
|
return d
|
|
|
|
@classmethod
|
|
def from_dict(cls, name: str, data: Dict[str, Any]) -> "AgentConfig":
|
|
"""Create from dict (loaded from YAML)."""
|
|
return cls(
|
|
name=name,
|
|
prompt=data.get("prompt", ""),
|
|
model=data.get("model", "default"),
|
|
temperature=data.get("temperature", 0.7),
|
|
max_tokens=data.get("max_tokens", 4096),
|
|
verbose=data.get("verbose", False),
|
|
confirm_actions=data.get("confirm_actions", False),
|
|
allowed_tools=data.get("allowed_tools", []),
|
|
blocked_tools=data.get("blocked_tools", []),
|
|
metadata=data.get("metadata", {}),
|
|
)
|
|
|
|
def to_yaml(self) -> str:
|
|
"""Serialize to YAML string."""
|
|
return yaml.dump(
|
|
self.to_dict(),
|
|
default_flow_style=False,
|
|
sort_keys=False,
|
|
allow_unicode=True,
|
|
)
|
|
|
|
@classmethod
|
|
def from_yaml(cls, name: str, yaml_str: str) -> "AgentConfig":
|
|
"""Parse from YAML string."""
|
|
data = yaml.safe_load(yaml_str) or {}
|
|
return cls.from_dict(name, data)
|
|
|
|
|
|
class AgentConfigStore:
|
|
"""
|
|
Manages agent configuration files.
|
|
|
|
Usage:
|
|
store = AgentConfigStore()
|
|
|
|
# Load or create config
|
|
config = store.get("greeter")
|
|
|
|
# Modify and save
|
|
config.prompt = "You are a friendly greeter."
|
|
store.save(config)
|
|
|
|
# Get path for editing
|
|
path = store.path_for("greeter")
|
|
"""
|
|
|
|
def __init__(self, agents_dir: Path = AGENTS_DIR):
|
|
self.agents_dir = agents_dir
|
|
self._ensure_dir()
|
|
|
|
def _ensure_dir(self) -> None:
|
|
"""Create agents directory if needed."""
|
|
self.agents_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def path_for(self, name: str) -> Path:
|
|
"""Get path to agent's config file."""
|
|
return self.agents_dir / f"{name}.yaml"
|
|
|
|
def exists(self, name: str) -> bool:
|
|
"""Check if agent config exists."""
|
|
return self.path_for(name).exists()
|
|
|
|
def get(self, name: str) -> AgentConfig:
|
|
"""
|
|
Load agent config, creating default if not exists.
|
|
"""
|
|
path = self.path_for(name)
|
|
|
|
if path.exists():
|
|
with open(path) as f:
|
|
data = yaml.safe_load(f) or {}
|
|
return AgentConfig.from_dict(name, data)
|
|
|
|
# Return default config (not saved yet)
|
|
return AgentConfig(name=name)
|
|
|
|
def save(self, config: AgentConfig) -> Path:
|
|
"""
|
|
Save agent config to file.
|
|
|
|
Returns path to saved file.
|
|
"""
|
|
path = self.path_for(config.name)
|
|
|
|
with open(path, "w") as f:
|
|
yaml.dump(
|
|
config.to_dict(),
|
|
f,
|
|
default_flow_style=False,
|
|
sort_keys=False,
|
|
allow_unicode=True,
|
|
)
|
|
|
|
return path
|
|
|
|
def save_yaml(self, name: str, yaml_content: str) -> Path:
|
|
"""
|
|
Save raw YAML content for an agent.
|
|
|
|
Used when saving from editor.
|
|
"""
|
|
path = self.path_for(name)
|
|
|
|
# Validate YAML before saving
|
|
yaml.safe_load(yaml_content) # Raises on invalid YAML
|
|
|
|
with open(path, "w") as f:
|
|
f.write(yaml_content)
|
|
|
|
return path
|
|
|
|
def load_yaml(self, name: str) -> str:
|
|
"""
|
|
Load raw YAML content for editing.
|
|
|
|
Returns default template if file doesn't exist.
|
|
"""
|
|
path = self.path_for(name)
|
|
|
|
if path.exists():
|
|
with open(path) as f:
|
|
return f.read()
|
|
|
|
# Return default template
|
|
return self._default_template(name)
|
|
|
|
def _default_template(self, name: str) -> str:
|
|
"""Generate default YAML template for new agent."""
|
|
return f"""# Agent configuration for: {name}
|
|
# This file defines behavior settings for the agent.
|
|
# The wiring (peers, routing) is in organism.yaml.
|
|
|
|
# System prompt - instructions for the LLM
|
|
prompt: |
|
|
You are {name}, an AI assistant.
|
|
|
|
Respond helpfully and concisely.
|
|
|
|
# Model selection: "default", "claude-sonnet", "claude-opus", "gpt-4", etc.
|
|
model: default
|
|
|
|
# Generation parameters
|
|
temperature: 0.7
|
|
max_tokens: 4096
|
|
|
|
# Behavior flags
|
|
verbose: false # Log detailed reasoning
|
|
confirm_actions: false # Ask before tool calls
|
|
|
|
# Tool permissions (empty = use defaults from wiring)
|
|
allowed_tools: []
|
|
blocked_tools: []
|
|
|
|
# Custom metadata (available to handler)
|
|
metadata: {{}}
|
|
"""
|
|
|
|
def list_agents(self) -> List[str]:
|
|
"""List all configured agents."""
|
|
return [
|
|
p.stem for p in self.agents_dir.glob("*.yaml")
|
|
]
|
|
|
|
def delete(self, name: str) -> bool:
|
|
"""Delete agent config file."""
|
|
path = self.path_for(name)
|
|
if path.exists():
|
|
path.unlink()
|
|
return True
|
|
return False
|
|
|
|
|
|
# Global instance
|
|
_store: Optional[AgentConfigStore] = None
|
|
|
|
|
|
def get_agent_config_store() -> AgentConfigStore:
|
|
"""Get the global agent config store."""
|
|
global _store
|
|
if _store is None:
|
|
_store = AgentConfigStore()
|
|
return _store
|