vgtc-api / src /hermes /agents /base /agent.py
vora-sonnet's picture
Upload folder using huggingface_hub
0d3f7cc verified
Raw
History Blame Contribute Delete
6.64 kB
"""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