136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
"""
|
|
Agent Card — A2A-compliant agent discovery.
|
|
Part of #843: fix: implement A2A agent card for fleet discovery (#819)
|
|
|
|
Provides metadata about the agent's identity, capabilities, and installed skills
|
|
for discovery by other agents in the fleet.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from hermes_cli import __version__
|
|
from hermes_cli.config import load_config, get_hermes_home
|
|
from agent.skill_utils import (
|
|
iter_skill_index_files,
|
|
parse_frontmatter,
|
|
get_all_skills_dirs,
|
|
get_disabled_skill_names,
|
|
skill_matches_platform
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@dataclass
|
|
class AgentSkill:
|
|
id: str
|
|
name: str
|
|
description: str = ""
|
|
version: str = "1.0.0"
|
|
|
|
@dataclass
|
|
class AgentCapabilities:
|
|
streaming: bool = True
|
|
tools: bool = True
|
|
vision: bool = False
|
|
reasoning: bool = False
|
|
|
|
@dataclass
|
|
class AgentCard:
|
|
name: str
|
|
description: str
|
|
url: str
|
|
version: str = __version__
|
|
capabilities: AgentCapabilities = field(default_factory=AgentCapabilities)
|
|
skills: List[AgentSkill] = field(default_factory=list)
|
|
defaultInputModes: List[str] = field(default_factory=lambda: ["text/plain"])
|
|
defaultOutputModes: List[str] = field(default_factory=lambda: ["text/plain"])
|
|
|
|
def _load_skills() -> List[AgentSkill]:
|
|
"""Scan all enabled skills and return metadata."""
|
|
skills = []
|
|
disabled = get_disabled_skill_names()
|
|
|
|
for skills_dir in get_all_skills_dirs():
|
|
if not skills_dir.is_dir():
|
|
continue
|
|
for skill_file in iter_skill_index_files(skills_dir, "SKILL.md"):
|
|
try:
|
|
raw = skill_file.read_text(encoding="utf-8")
|
|
frontmatter, _ = parse_frontmatter(raw)
|
|
except Exception:
|
|
continue
|
|
|
|
skill_name = frontmatter.get("name") or skill_file.parent.name
|
|
if str(skill_name) in disabled:
|
|
continue
|
|
if not skill_matches_platform(frontmatter):
|
|
continue
|
|
|
|
skills.append(AgentSkill(
|
|
id=str(skill_name),
|
|
name=str(frontmatter.get("name", skill_name)),
|
|
description=str(frontmatter.get("description", "")),
|
|
version=str(frontmatter.get("version", "1.0.0"))
|
|
))
|
|
return skills
|
|
|
|
def build_agent_card() -> AgentCard:
|
|
"""Build the agent card from current configuration and environment."""
|
|
config = load_config()
|
|
|
|
# Identity
|
|
name = os.environ.get("HERMES_AGENT_NAME") or config.get("agent", {}).get("name") or "hermes"
|
|
description = os.environ.get("HERMES_AGENT_DESCRIPTION") or config.get("agent", {}).get("description") or "Sovereign AI agent"
|
|
|
|
# URL - try to determine from environment or config
|
|
port = os.environ.get("HERMES_WEB_PORT") or "9119"
|
|
host = os.environ.get("HERMES_WEB_HOST") or "localhost"
|
|
url = f"http://{host}:{port}"
|
|
|
|
# Capabilities
|
|
# In a real scenario, we'd check model metadata for vision/reasoning
|
|
capabilities = AgentCapabilities(
|
|
streaming=True,
|
|
tools=True,
|
|
vision=False, # Default to false unless we can confirm
|
|
reasoning=False
|
|
)
|
|
|
|
# Skills
|
|
skills = _load_skills()
|
|
|
|
return AgentCard(
|
|
name=name,
|
|
description=description,
|
|
url=url,
|
|
version=__version__,
|
|
capabilities=capabilities,
|
|
skills=skills
|
|
)
|
|
|
|
def get_agent_card_json() -> str:
|
|
"""Return the agent card as a JSON string."""
|
|
try:
|
|
card = build_agent_card()
|
|
return json.dumps(asdict(card), indent=2)
|
|
except Exception as e:
|
|
logger.error(f"Failed to build agent card: {e}")
|
|
# Minimal fallback card
|
|
fallback = {
|
|
"name": "hermes",
|
|
"description": "Sovereign AI agent (fallback)",
|
|
"version": __version__,
|
|
"error": str(e)
|
|
}
|
|
return json.dumps(fallback, indent=2)
|
|
|
|
def validate_agent_card(card_data: Dict[str, Any]) -> bool:
|
|
"""Check if the card data complies with the A2A schema."""
|
|
required = ["name", "description", "url", "version"]
|
|
return all(k in card_data for k in required)
|