329 lines
12 KiB
Python
329 lines
12 KiB
Python
"""EvolutionAgent - Agent self-improvement and versioning system for VoIdea.
|
|
|
|
This agent:
|
|
- Analyzes agent performance
|
|
- Generates improvement suggestions
|
|
- Manages agent capabilities evolution
|
|
- Handles agent versioning (minor/major bumps)
|
|
- Coordinates learning
|
|
"""
|
|
|
|
import hashlib
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.agents.base import AgentResult, AgentStatus, AgentTrigger, BaseAgent
|
|
from app.core.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
ALL_AGENTS: list[dict[str, Any]] = [
|
|
{"id": "doc_agent", "capabilities": ["documentation", "docstrings", "runbook"]},
|
|
{"id": "backlog_agent", "capabilities": ["create", "list", "update", "delete", "suggest"]},
|
|
{"id": "spec_agent", "capabilities": ["versioning", "changelog", "project_json"]},
|
|
{"id": "audit_agent", "capabilities": ["style_check", "type_check", "docs_check"]},
|
|
{"id": "observer_agent", "capabilities": ["collect", "report", "analyze", "metrics"]},
|
|
{"id": "security_agent", "capabilities": ["vulnerability_scan", "dependency_check", "compliance"]},
|
|
{"id": "qa_tester_agent", "capabilities": ["functional_test", "smoke_test", "regression"]},
|
|
{"id": "fix_agent", "capabilities": ["bug_analysis", "patch_generation", "validation"]},
|
|
{"id": "ui_test_agent", "capabilities": ["screenshot_test", "layout_check", "accessibility"]},
|
|
{"id": "rollout_agent", "capabilities": ["gradual_deploy", "monitor", "rollback"]},
|
|
{"id": "evolution_agent", "capabilities": ["analyze", "evolve", "suggest", "status", "version_bump"]},
|
|
]
|
|
|
|
AGENT_IDS = [a["id"] for a in ALL_AGENTS]
|
|
|
|
|
|
class EvolutionAgent(BaseAgent):
|
|
"""Agent self-improvement and evolution system."""
|
|
|
|
name = "evolution_agent"
|
|
version = "1.0.0"
|
|
description = "Manages agent self-improvement and capability growth"
|
|
triggers = [
|
|
AgentTrigger.MANUAL,
|
|
AgentTrigger.CRON,
|
|
]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.project_root = Path(__file__).parent.parent.parent
|
|
|
|
async def run(self, context: dict[str, Any] | None = None) -> AgentResult:
|
|
"""Execute evolution task.
|
|
|
|
Context can contain:
|
|
- action: str (analyze, evolve, suggest, status, version_check, version_bump)
|
|
- agent_id: str (specific agent to analyze)
|
|
- version_type: str (minor, major) — for version_bump
|
|
- entries: list[str] — changelog entries for version_bump
|
|
- capabilities: list[str] — new capabilities for evolve
|
|
"""
|
|
await self.set_running("evolution")
|
|
|
|
try:
|
|
action = context.get("action", "status") if context else "status"
|
|
|
|
if action == "analyze":
|
|
result = await self._analyze_agents(context or {})
|
|
elif action == "evolve":
|
|
result = await self._evolve_agent(context or {})
|
|
elif action == "suggest":
|
|
result = await self._suggest_improvements(context or {})
|
|
elif action == "version_check":
|
|
result = await self._version_check(context or {})
|
|
elif action == "version_bump":
|
|
result = await self._version_bump(context or {})
|
|
else:
|
|
result = await self._get_status()
|
|
|
|
await self.set_idle()
|
|
return result
|
|
|
|
except Exception as e:
|
|
await self.set_error(str(e))
|
|
return AgentResult(
|
|
success=False,
|
|
message=f"EvolutionAgent failed: {str(e)}",
|
|
errors=[str(e)],
|
|
)
|
|
|
|
async def health_check(self) -> bool:
|
|
"""Check if EvolutionAgent is operational."""
|
|
return self.project_root.exists()
|
|
|
|
async def _get_status(self) -> AgentResult:
|
|
"""Get current evolution status with versions from changelogs."""
|
|
agents = []
|
|
for agent_info in ALL_AGENTS:
|
|
aid = agent_info["id"]
|
|
version = self._read_agent_version(aid)
|
|
agents.append({
|
|
"id": aid,
|
|
"version": version or "1.0.0",
|
|
"capabilities": agent_info["capabilities"],
|
|
"status": "stable",
|
|
})
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message="Evolution status retrieved",
|
|
data={
|
|
"total_agents": len(agents),
|
|
"agents": agents,
|
|
"last_evolution": datetime.now(timezone.utc).isoformat(),
|
|
},
|
|
)
|
|
|
|
def _read_agent_version(self, agent_id: str) -> str | None:
|
|
"""Read latest version from an agent's changelog file."""
|
|
changelog_path = Path("CHANGELOG") / "agents" / f"{agent_id}.md"
|
|
if not changelog_path.exists():
|
|
return None
|
|
content = changelog_path.read_text(encoding="utf-8")
|
|
matches = re.findall(r"##\s+(\d+\.\d+\.\d+)", content)
|
|
return matches[-1] if matches else None
|
|
|
|
async def _version_check(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Scan all agents and report their current versions."""
|
|
agent_id = context.get("agent_id")
|
|
agents_to_check = [agent_id] if agent_id else AGENT_IDS
|
|
|
|
versions = {}
|
|
for aid in agents_to_check:
|
|
versions[aid] = self._read_agent_version(aid) or "1.0.0"
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"Version check completed for {len(versions)} agents",
|
|
data={"versions": versions},
|
|
)
|
|
|
|
async def _version_bump(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Bump version of a specific agent (minor or major).
|
|
|
|
Context requires:
|
|
- agent_id: str
|
|
- version_type: str (minor or major)
|
|
- entries: list[str] — changelog entry lines
|
|
"""
|
|
agent_id = context.get("agent_id")
|
|
version_type = context.get("version_type", "minor")
|
|
entries = context.get("entries", [])
|
|
|
|
if not agent_id:
|
|
return AgentResult(
|
|
success=False,
|
|
message="agent_id required",
|
|
)
|
|
|
|
if version_type not in ("minor", "major"):
|
|
return AgentResult(
|
|
success=False,
|
|
message=f"Invalid version_type: {version_type}. Use minor or major.",
|
|
)
|
|
|
|
current_version = self._read_agent_version(agent_id) or "1.0.0"
|
|
major, minor, patch = map(int, current_version.split("."))
|
|
|
|
if version_type == "major":
|
|
major += 1
|
|
minor = 0
|
|
patch = 0
|
|
else:
|
|
minor += 1
|
|
patch = 0
|
|
|
|
new_version = f"{major}.{minor}.{patch}"
|
|
|
|
changelog_path = Path("CHANGELOG") / "agents" / f"{agent_id}.md"
|
|
changelog_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
entry_text = f"\n## {new_version} ({today})\n"
|
|
for line in entries:
|
|
entry_text += f"- {line}\n"
|
|
|
|
if changelog_path.exists():
|
|
content = changelog_path.read_text(encoding="utf-8")
|
|
changelog_path.write_text(content + entry_text, encoding="utf-8")
|
|
else:
|
|
from app.agents.base import BaseAgent
|
|
agent_src_path = Path("app") / "agents" / f"{agent_id}.py"
|
|
if agent_src_path.exists():
|
|
checksum = hashlib.sha256(agent_src_path.read_bytes()).hexdigest()
|
|
else:
|
|
checksum = self.compute_checksum()
|
|
changelog_path.write_text(
|
|
f"# {agent_id} Changelog\n<!-- checksum: {checksum} -->\n{entry_text}",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"{agent_id} bumped to {new_version}",
|
|
data={
|
|
"agent_id": agent_id,
|
|
"old_version": current_version,
|
|
"new_version": new_version,
|
|
"version_type": version_type,
|
|
"entries": entries,
|
|
},
|
|
)
|
|
|
|
async def _analyze_agents(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Analyze agent performance and suggest improvements."""
|
|
agent_id = context.get("agent_id")
|
|
|
|
analyses = {}
|
|
targets = [agent_id] if agent_id else AGENT_IDS
|
|
for aid in targets:
|
|
analyses[aid] = self._analyze_single_agent(aid)
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"Analyzed {len(analyses)} agents",
|
|
data={"analyses": analyses},
|
|
)
|
|
|
|
async def _evolve_agent(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Apply evolution to a specific agent with version bump."""
|
|
agent_id = context.get("agent_id")
|
|
new_capabilities = context.get("capabilities", [])
|
|
|
|
if not agent_id:
|
|
return AgentResult(
|
|
success=False,
|
|
message="agent_id required",
|
|
)
|
|
|
|
if new_capabilities:
|
|
entries = [f"Added: {cap}" for cap in new_capabilities]
|
|
bump_result = await self._version_bump({
|
|
"agent_id": agent_id,
|
|
"version_type": "minor",
|
|
"entries": entries,
|
|
})
|
|
new_version = bump_result.data.get("new_version", "unknown")
|
|
else:
|
|
new_version = self._read_agent_version(agent_id) or "1.0.0"
|
|
|
|
evolution_entry = {
|
|
"date": datetime.now(timezone.utc).isoformat(),
|
|
"agent_id": agent_id,
|
|
"new_capabilities": new_capabilities,
|
|
"new_version": new_version,
|
|
"trigger": context.get("trigger", "manual"),
|
|
}
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"Evolved {agent_id} to v{new_version}",
|
|
data={
|
|
"agent_id": agent_id,
|
|
"new_capabilities": new_capabilities,
|
|
"new_version": new_version,
|
|
"evolution": evolution_entry,
|
|
},
|
|
)
|
|
|
|
async def _suggest_improvements(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Suggest improvements for agents."""
|
|
suggestions = [
|
|
{
|
|
"agent_id": "doc_agent",
|
|
"suggestion": "Add auto-generation of API docs",
|
|
"priority": "medium",
|
|
"impact": "high",
|
|
},
|
|
{
|
|
"agent_id": "observer_agent",
|
|
"suggestion": "Add real-time dashboard updates",
|
|
"priority": "low",
|
|
"impact": "medium",
|
|
},
|
|
{
|
|
"agent_id": "audit_agent",
|
|
"suggestion": "Integrate with CI/CD",
|
|
"priority": "high",
|
|
"impact": "high",
|
|
},
|
|
]
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message="Generated improvement suggestions",
|
|
data={"suggestions": suggestions},
|
|
)
|
|
|
|
def _analyze_single_agent(self, agent_id: str) -> dict[str, Any]:
|
|
"""Analyze a single agent."""
|
|
version = self._read_agent_version(agent_id) or "1.0.0"
|
|
return {
|
|
"agent_id": agent_id,
|
|
"version": version,
|
|
"health": "good",
|
|
"performance": "optimal",
|
|
"capabilities": self._get_agent_capabilities(agent_id),
|
|
"suggestions": [],
|
|
"last_check": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
def _get_agent_capabilities(self, agent_id: str) -> list[str]:
|
|
"""Get capabilities for an agent."""
|
|
for agent_info in ALL_AGENTS:
|
|
if agent_info["id"] == agent_id:
|
|
return agent_info["capabilities"]
|
|
return []
|
|
|
|
async def get_metrics(self) -> dict[str, Any]:
|
|
"""Get EvolutionAgent metrics."""
|
|
return {
|
|
"agent_id": self.name,
|
|
"version": self.version,
|
|
"status": self.status.value,
|
|
"last_evolution": self.last_run.isoformat() if self.last_run else None,
|
|
"capabilities_tracked": len(ALL_AGENTS),
|
|
} |