"""Base agent abstraction for all agents in the Hermes platform.""" from __future__ import annotations import json import logging from abc import ABC, abstractmethod from datetime import UTC, datetime from typing import Any from hermes.agents.base.tool_executor import ToolExecutor from hermes.config.settings import get_settings from hermes.core.llm import LLMProvider, get_llm_provider from hermes.core.types import ( AgentState, AgentStrategy, Message, MessageRole, TaskStatus, ToolCall, ToolResult, ) logger = logging.getLogger(__name__) class BaseAgent(ABC): """Abstract base agent implementing core agent loop.""" def __init__( self, agent_type: str, strategy: AgentStrategy = AgentStrategy.REACT, tools: list[str] | None = None, llm_provider: LLMProvider | None = None, ) -> None: self.agent_type = agent_type self.strategy = strategy self.tool_executor = ToolExecutor(tools or []) self.state = AgentState(agent_type=agent_type, strategy=strategy) self.settings = get_settings() self._running = False self._llm_provider = llm_provider async def _call_llm( self, messages: list[dict[str, str]], temperature: float = 0.1, max_tokens: int = 4096 ) -> str: """Call LLM with messages. Falls back to mock if no provider configured.""" try: provider = self._llm_provider if provider is None: provider = get_llm_provider() return await provider.chat(messages=messages, temperature=temperature, max_tokens=max_tokens) except Exception as e: logger.warning(f"LLM call failed, using fallback: {e}") if messages: return f"[LLM unavailable - fallback response to: {messages[-1].get('content', '')[:80]}...]" return "[LLM unavailable]" def _parse_json_response(self, text: str) -> dict[str, Any] | None: """Attempt to parse JSON from LLM response, handling markdown code blocks.""" text = text.strip() if text.startswith("```json"): text = text[7:] elif text.startswith("```"): text = text[3:] if text.endswith("```"): text = text[:-3] text = text.strip() try: return json.loads(text) except json.JSONDecodeError: start = text.find("{") end = text.rfind("}") if start != -1 and end != -1 and end > start: try: return json.loads(text[start:end + 1]) except json.JSONDecodeError: pass return None @abstractmethod async def plan(self, task: str) -> list[str]: """Create a plan of steps to execute.""" @abstractmethod async def think(self, task: str, observations: list[str]) -> dict[str, Any]: """Reason about the current state and decide next action. Returns a dict with keys: reasoning, tool, arguments, done """ @abstractmethod async def act(self, thought: dict[str, Any]) -> ToolCall: """Execute an action based on structured thought dict.""" @abstractmethod async def observe(self, result: ToolResult) -> str: """Observe and summarize a tool result.""" @abstractmethod async def synthesize(self, task: str) -> str: """Synthesize final answer from all observations.""" async def execute(self, task: str) -> str: """Execute the agent loop.""" self.state.status = TaskStatus.RUNNING self.state.updated_at = datetime.now(UTC) self._running = True try: self.add_message(MessageRole.USER, task) steps = await self.plan(task) logger.info(f"Agent {self.agent_type} created plan with {len(steps)} steps") observations: list[str] = [] max_iterations = 10 for i, step in enumerate(steps): if not self._running: break if i >= max_iterations: logger.warning(f"Agent {self.agent_type} reached max iterations") break logger.info(f"Agent {self.agent_type} executing step {i + 1}/{len(steps)}") thought = await self.think(step, observations) self.state.current_thought = thought.get("reasoning", str(thought)) self.add_message(MessageRole.ASSISTANT, json.dumps(thought)) if thought.get("done"): logger.info(f"Agent {self.agent_type} decided task is done") break tool_call = await self.act(thought) self.state.tool_calls.append(tool_call) result = await self.tool_executor.execute(tool_call) self.state.tool_results.append(result) observation = await self.observe(result) observations.append(observation) self.state.observations.append(observation) final_answer = await self.synthesize(task) self.add_message(MessageRole.ASSISTANT, final_answer) self.state.status = TaskStatus.COMPLETED self.state.updated_at = datetime.now(UTC) return final_answer except Exception as e: self.state.status = TaskStatus.FAILED self.state.updated_at = datetime.now(UTC) logger.error(f"Agent {self.agent_type} failed: {e}") raise def add_message(self, role: MessageRole, content: str) -> Message: """Add a message to the conversation.""" message = Message(role=role, content=content) self.state.messages.append(message) return message def get_messages(self) -> list[dict[str, str]]: """Get messages in LLM format.""" return [{"role": msg.role.value, "content": msg.content} for msg in self.state.messages] def stop(self) -> None: """Stop the agent.""" self._running = False def get_state(self) -> AgentState: """Get current agent state.""" return self.state.model_copy() def reset(self) -> None: """Reset agent state.""" agent_type = self.agent_type strategy = self.strategy self.state = AgentState(agent_type=agent_type, strategy=strategy) self._running = False