Files
gh-epieczko-betty/skills/agent.define/agent_define.py
2025-11-29 18:26:08 +08:00

420 lines
13 KiB
Python
Executable File
Raw 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.
#!/usr/bin/env python3
"""
agent_define.py Implementation of the agent.define Skill
Validates agent manifests (agent.yaml) and registers them in the Agent Registry.
"""
import os
import sys
import json
import yaml
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
from pydantic import ValidationError as PydanticValidationError
from betty.config import (
BASE_DIR,
REQUIRED_AGENT_FIELDS,
AGENTS_REGISTRY_FILE,
REGISTRY_FILE,
)
from betty.enums import AgentStatus, ReasoningMode
from betty.validation import (
validate_path,
validate_manifest_fields,
validate_agent_name,
validate_version,
validate_reasoning_mode,
validate_skills_exist
)
from betty.logging_utils import setup_logger
from betty.errors import AgentValidationError, AgentRegistryError, format_error_response
from betty.models import AgentManifest
from betty.file_utils import atomic_write_json
logger = setup_logger(__name__)
def build_response(ok: bool, path: str, errors: Optional[List[str]] = None, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Build standardized response dictionary.
Args:
ok: Whether operation succeeded
path: Path to agent manifest
errors: List of error messages
details: Additional details
Returns:
Response dictionary
"""
response: Dict[str, Any] = {
"ok": ok,
"status": "success" if ok else "failed",
"errors": errors or [],
"path": path,
}
if details is not None:
response["details"] = details
return response
def load_agent_manifest(path: str) -> Dict[str, Any]:
"""
Load and parse an agent manifest from YAML file.
Args:
path: Path to agent manifest file
Returns:
Parsed manifest dictionary
Raises:
AgentValidationError: If manifest cannot be loaded or parsed
"""
try:
with open(path) as f:
manifest = yaml.safe_load(f)
return manifest
except FileNotFoundError:
raise AgentValidationError(f"Manifest file not found: {path}")
except yaml.YAMLError as e:
raise AgentValidationError(f"Failed to parse YAML: {e}")
def load_skill_registry() -> Dict[str, Any]:
"""
Load skill registry for validation.
Returns:
Skill registry dictionary
Raises:
AgentValidationError: If registry cannot be loaded
"""
try:
with open(REGISTRY_FILE) as f:
return json.load(f)
except FileNotFoundError:
raise AgentValidationError(f"Skill registry not found: {REGISTRY_FILE}")
except json.JSONDecodeError as e:
raise AgentValidationError(f"Failed to parse skill registry: {e}")
def validate_agent_schema(manifest: Dict[str, Any]) -> List[str]:
"""
Validate agent manifest using Pydantic schema.
Args:
manifest: Agent manifest dictionary
Returns:
List of validation errors (empty if valid)
"""
errors: List[str] = []
try:
AgentManifest.model_validate(manifest)
logger.info("Pydantic schema validation passed for agent manifest")
except PydanticValidationError as exc:
logger.warning("Pydantic schema validation failed for agent manifest")
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"])
message = error["msg"]
error_type = error["type"]
errors.append(f"Schema validation error at '{field}': {message} (type: {error_type})")
return errors
def validate_manifest(path: str) -> Dict[str, Any]:
"""
Validate that an agent manifest meets all requirements.
Validation checks:
1. Required fields are present
2. Name format is valid
3. Version format is valid
4. Reasoning mode is valid
5. All referenced skills exist in skill registry
6. Capabilities list is non-empty
7. Skills list is non-empty
Args:
path: Path to agent manifest file
Returns:
Dictionary with validation results:
- valid: Boolean indicating if manifest is valid
- errors: List of validation errors (if any)
- manifest: The parsed manifest (if valid)
- path: Path to the manifest file
"""
validate_path(path, must_exist=True)
logger.info(f"Validating agent manifest: {path}")
errors = []
# Load manifest
try:
manifest = load_agent_manifest(path)
except AgentValidationError as e:
return {
"valid": False,
"errors": [str(e)],
"path": path
}
# Check required fields first so high-level issues are reported clearly
missing = validate_manifest_fields(manifest, REQUIRED_AGENT_FIELDS)
if missing:
missing_message = f"Missing required fields: {', '.join(missing)}"
errors.append(missing_message)
logger.warning(f"Missing required fields: {missing}")
# Validate with Pydantic schema while continuing custom validation
schema_errors = validate_agent_schema(manifest)
errors.extend(schema_errors)
name = manifest.get("name")
if name is not None:
try:
validate_agent_name(name)
except Exception as e:
errors.append(f"Invalid name: {str(e)}")
logger.warning(f"Invalid name: {e}")
version = manifest.get("version")
if version is not None:
try:
validate_version(version)
except Exception as e:
errors.append(f"Invalid version: {str(e)}")
logger.warning(f"Invalid version: {e}")
reasoning_mode = manifest.get("reasoning_mode")
if reasoning_mode is not None:
try:
validate_reasoning_mode(reasoning_mode)
except Exception as e:
errors.append(f"Invalid reasoning_mode: {str(e)}")
logger.warning(f"Invalid reasoning_mode: {e}")
elif "reasoning_mode" not in missing:
errors.append("reasoning_mode must be provided")
logger.warning("Reasoning mode missing")
# Validate capabilities is non-empty
capabilities = manifest.get("capabilities", [])
if not capabilities or len(capabilities) == 0:
errors.append("capabilities must contain at least one item")
logger.warning("Empty capabilities list")
# Validate skills_available is non-empty
skills_available = manifest.get("skills_available", [])
if not skills_available or len(skills_available) == 0:
errors.append("skills_available must contain at least one item")
logger.warning("Empty skills_available list")
# Validate all skills exist in skill registry
if skills_available:
try:
skill_registry = load_skill_registry()
missing_skills = validate_skills_exist(skills_available, skill_registry)
if missing_skills:
errors.append(f"Skills not found in registry: {', '.join(missing_skills)}")
logger.warning(f"Missing skills: {missing_skills}")
except AgentValidationError as e:
errors.append(f"Could not validate skills: {str(e)}")
logger.error(f"Skill validation error: {e}")
# Validate status if present
if "status" in manifest:
valid_statuses = [s.value for s in AgentStatus]
if manifest["status"] not in valid_statuses:
errors.append(f"Invalid status: '{manifest['status']}'. Must be one of: {', '.join(valid_statuses)}")
logger.warning(f"Invalid status: {manifest['status']}")
if errors:
logger.warning(f"Validation failed with {len(errors)} error(s)")
return {
"valid": False,
"errors": errors,
"path": path
}
logger.info("✅ Agent manifest validation passed")
return {
"valid": True,
"errors": [],
"path": path,
"manifest": manifest
}
def load_agent_registry() -> Dict[str, Any]:
"""
Load existing agent registry.
Returns:
Agent registry dictionary, or new empty registry if file doesn't exist
"""
if not os.path.exists(AGENTS_REGISTRY_FILE):
logger.info("Agent registry not found, creating new registry")
return {
"registry_version": "1.0.0",
"generated_at": datetime.now(timezone.utc).isoformat(),
"agents": []
}
try:
with open(AGENTS_REGISTRY_FILE) as f:
registry = json.load(f)
logger.info(f"Loaded agent registry with {len(registry.get('agents', []))} agent(s)")
return registry
except json.JSONDecodeError as e:
raise AgentRegistryError(f"Failed to parse agent registry: {e}")
def update_agent_registry(manifest: Dict[str, Any]) -> bool:
"""
Add or update agent in the agent registry.
Args:
manifest: Validated agent manifest
Returns:
True if registry was updated successfully
Raises:
AgentRegistryError: If registry update fails
"""
logger.info(f"Updating agent registry for: {manifest['name']}")
# Load existing registry
registry = load_agent_registry()
# Create registry entry
entry = {
"name": manifest["name"],
"version": manifest["version"],
"description": manifest["description"],
"reasoning_mode": manifest["reasoning_mode"],
"skills_available": manifest["skills_available"],
"capabilities": manifest.get("capabilities", []),
"status": manifest.get("status", "draft"),
"tags": manifest.get("tags", []),
"dependencies": manifest.get("dependencies", [])
}
# Check if agent already exists
agents = registry.get("agents", [])
existing_index = None
for i, agent in enumerate(agents):
if agent["name"] == manifest["name"]:
existing_index = i
break
if existing_index is not None:
# Update existing agent
agents[existing_index] = entry
logger.info(f"Updated existing agent: {manifest['name']}")
else:
# Add new agent
agents.append(entry)
logger.info(f"Added new agent: {manifest['name']}")
registry["agents"] = agents
registry["generated_at"] = datetime.now(timezone.utc).isoformat()
# Write registry back to disk atomically
try:
atomic_write_json(AGENTS_REGISTRY_FILE, registry)
logger.info(f"Agent registry updated successfully")
return True
except Exception as e:
raise AgentRegistryError(f"Failed to write agent registry: {e}")
def main():
"""Main CLI entry point."""
if len(sys.argv) < 2:
message = "Usage: agent_define.py <path_to_agent.yaml>"
response = build_response(
False,
path="",
errors=[message],
details={"error": {"error": "UsageError", "message": message, "details": {}}},
)
print(json.dumps(response, indent=2))
sys.exit(1)
path = sys.argv[1]
try:
# Validate manifest
validation = validate_manifest(path)
details = dict(validation)
if validation.get("valid"):
# Update registry
try:
registry_updated = update_agent_registry(validation["manifest"])
details["status"] = "registered"
details["registry_updated"] = registry_updated
except AgentRegistryError as e:
logger.error(f"Registry update failed: {e}")
details["status"] = "validated"
details["registry_updated"] = False
details["registry_error"] = str(e)
else:
# Check if there are schema validation errors
has_schema_errors = any("Schema validation error" in err for err in validation.get("errors", []))
if has_schema_errors:
details["error"] = {
"type": "SchemaError",
"error": "SchemaError",
"message": "Agent manifest schema validation failed",
"details": {"errors": validation.get("errors", [])}
}
# Build response
response = build_response(
bool(validation.get("valid")),
path=path,
errors=validation.get("errors", []),
details=details,
)
print(json.dumps(response, indent=2))
sys.exit(0 if response["ok"] else 1)
except AgentValidationError as e:
logger.error(str(e))
error_info = format_error_response(e)
response = build_response(
False,
path=path,
errors=[error_info.get("message", str(e))],
details={"error": error_info},
)
print(json.dumps(response, indent=2))
sys.exit(1)
except Exception as e:
logger.error(f"Unexpected error: {e}")
error_info = format_error_response(e, include_traceback=True)
response = build_response(
False,
path=path,
errors=[error_info.get("message", str(e))],
details={"error": error_info},
)
print(json.dumps(response, indent=2))
sys.exit(1)
if __name__ == "__main__":
main()