"""Base classes and utilities for VoIdea agents.""" import hashlib import inspect import re from abc import ABC, abstractmethod from datetime import datetime, timezone from enum import Enum from pathlib import Path from typing import Any, Generic, TypeVar from uuid import UUID, uuid4 from pydantic import BaseModel, ConfigDict, Field from app.core.base import CoreModel T = TypeVar("T") class AgentStatus(str, Enum): """Agent status enumeration.""" IDLE = "idle" RUNNING = "running" ERROR = "error" OFFLINE = "offline" class AgentTrigger(str, Enum): """Agent trigger types.""" MANUAL = "manual" PRE_COMMIT = "pre_commit" PUSH = "push" TAG_CREATION = "tag_creation" CRON = "cron" API = "api" EVENT = "event" class AgentResult(CoreModel): """Result of agent execution.""" success: bool message: str = "" data: dict[str, Any] = Field(default_factory=dict) errors: list[str] = Field(default_factory=list) duration_ms: int = 0 timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class AgentMetrics(CoreModel): """Agent performance metrics.""" agent_id: str version: str = "1.0.0" requests_total: int = 0 requests_success: int = 0 requests_failed: int = 0 average_duration_ms: float = 0.0 last_run: datetime | None = None class BaseAgent(ABC): """Abstract base class for all agents. All agents must inherit from this class and implement required methods. """ name: str = "" version: str = "1.0.0" description: str = "" triggers: list[AgentTrigger] = [AgentTrigger.MANUAL] changelog_dir: Path = Path("CHANGELOG") / "agents" def __init__(self): self._status = AgentStatus.IDLE self._last_run: datetime | None = None self._current_task: str | None = None self._changelog_path = self.changelog_dir / f"{self.name}.md" @property def status(self) -> AgentStatus: """Get current agent status.""" return self._status @property def last_run(self) -> datetime | None: """Get last run timestamp.""" return self._last_run @abstractmethod async def run(self, context: dict[str, Any] | None = None) -> AgentResult: """Execute agent task. Args: context: Optional context data for the agent Returns: AgentResult with execution outcome """ pass @abstractmethod async def health_check(self) -> bool: """Check if agent is healthy and operational. Returns: True if agent can execute tasks """ pass async def get_status(self) -> AgentStatus: """Get current agent status. Returns: Current status from status property """ return self.status async def get_metrics(self) -> AgentMetrics: """Get agent performance metrics. Returns: AgentMetrics instance """ return AgentMetrics( agent_id=self.name, version=self.version, last_run=self.last_run, ) async def set_running(self, task: str) -> None: """Set agent to running state.""" self._status = AgentStatus.RUNNING self._current_task = task self._last_run = datetime.now(timezone.utc) async def set_idle(self) -> None: """Set agent to idle state.""" self._status = AgentStatus.IDLE self._current_task = None async def set_error(self, error: str) -> None: """Set agent to error state.""" self._status = AgentStatus.ERROR self._current_task = None async def set_offline(self) -> None: """Set agent to offline state.""" self._status = AgentStatus.OFFLINE def compute_checksum(self) -> str: """Compute SHA256 checksum of this agent's source file. Returns: Hex digest of the file content. """ file_path = inspect.getfile(self.__class__) content = Path(file_path).read_bytes() return hashlib.sha256(content).hexdigest() def _read_changelog_checksum(self) -> str | None: """Read stored checksum from changelog file. Returns: Stored checksum or None if file doesn't exist. """ if not self._changelog_path.exists(): return None content = self._changelog_path.read_text(encoding="utf-8") match = re.search(r"", content) return match.group(1) if match else None def bump_version(self, version_type: str = "patch") -> str: """Bump agent version (major.minor.patch). Args: version_type: "major", "minor", or "patch" Returns: New version string. """ major, minor, patch = map(int, self.version.split(".")) if version_type == "major": major += 1 minor = 0 patch = 0 elif version_type == "minor": minor += 1 patch = 0 else: patch += 1 self.version = f"{major}.{minor}.{patch}" return self.version def _write_changelog_entry(self, version: str, entries: list[str]) -> None: """Write a changelog entry for this agent. Args: version: New version string. entries: List of change descriptions. """ self.changelog_dir.mkdir(parents=True, exist_ok=True) today = datetime.now(timezone.utc).strftime("%Y-%m-%d") checksum = self.compute_checksum() header = f"# {self.name} Changelog\n\n" entry = f"\n## {version} ({today})\n" for line in entries: entry += f"- {line}\n" if self._changelog_path.exists(): old = self._changelog_path.read_text(encoding="utf-8") new = header + entry + old.split("\n", 2)[-1] if "\n" in old else old self._changelog_path.write_text(new, encoding="utf-8") else: self._changelog_path.write_text(header + entry, encoding="utf-8") async def _check_version(self, changelog_entries: list[str] | None = None) -> str | None: """Check if agent changed and bump version if needed. Should be called after run(). Compares current file checksum with stored checksum in changelog. On mismatch bumps patch and writes changelog entry. Args: changelog_entries: Optional list of change descriptions. If None, auto-generated from git diff summary. Returns: New version string if bumped, None if unchanged. """ current_checksum = self.compute_checksum() stored_checksum = self._read_changelog_checksum() if current_checksum == stored_checksum: return None old_version = self.version new_version = self.bump_version("patch") entries = changelog_entries or ["Auto-detected code changes"] self._write_changelog_entry(new_version, entries) return new_version def __repr__(self) -> str: return f"<{self.__class__.__name__}(name={self.name}, status={self.status.value})>" class AgentResponse(CoreModel): """Standardized agent response.""" agent: str status: str message: str data: dict[str, Any] = Field(default_factory=dict) timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))