Initial commit
This commit is contained in:
419
skills/agent.define/agent_define.py
Executable file
419
skills/agent.define/agent_define.py
Executable file
@@ -0,0 +1,419 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user