Spaces:
Paused
Paused
| """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 | |
| async def plan(self, task: str) -> list[str]: | |
| """Create a plan of steps to execute.""" | |
| 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 | |
| """ | |
| async def act(self, thought: dict[str, Any]) -> ToolCall: | |
| """Execute an action based on structured thought dict.""" | |
| async def observe(self, result: ToolResult) -> str: | |
| """Observe and summarize a tool result.""" | |
| 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 | |