Files
Timmy-time-dashboard/src/timmy/agents/base.py
Kimi Agent 061c8f6628
Some checks failed
Tests / lint (pull_request) Successful in 15s
Tests / test (pull_request) Failing after 24s
fix: brevity tuning — plain text prompts, markdown=False, front-loaded brevity
Closes #71: Timmy was responding with elaborate markdown formatting
(tables, headers, emoji, bullet lists) for simple questions.

Root causes fixed:
1. Agno Agent markdown=True flag explicitly told the model to format
   responses as markdown. Set to False in both agent.py and agents/base.py.
2. SYSTEM_PROMPT_FULL used ## and ### markdown headers, bold (**), and
   numbered lists — teaching by example that markdown is expected.
   Rewritten to plain text with labeled sections.
3. Brevity instructions were buried at the bottom of the full prompt.
   Moved to immediately after the opening line as 'VOICE AND BREVITY'
   with explicit override priority.
4. Orchestrator prompt in agents.yaml was silent on response style.
   Added 'Voice: brief, plain, direct' with concrete examples.

The full prompt is now 41 lines shorter (124 → 83). The prompt itself
practices the brevity it preaches.

SOUL.md alignment:
- 'Brevity is a kindness' — now front-loaded in both base and agent prompt
- 'I do not fill silence with noise' — explicit in both tiers
- 'I speak plainly. I prefer short sentences.' — structural enforcement

4 new tests guard against regression:
- test_full_prompt_brevity_first: brevity section before tools/memory
- test_full_prompt_no_markdown_headers: no ## or ### in prompt text
- test_full_prompt_plain_text_brevity: 'plain text' instruction present
- test_lite_prompt_brevity: lite tier also instructs brevity
2026-03-14 17:15:56 -04:00

191 lines
5.7 KiB
Python

"""Base agent class and configurable SubAgent.
BaseAgent provides:
- MCP tool registry access
- Event bus integration
- Memory integration
- Structured logging
SubAgent is the single seed class for ALL agents. Differentiation
comes entirely from config (agents.yaml), not from Python subclasses.
"""
import logging
from abc import ABC, abstractmethod
from typing import Any
from agno.agent import Agent
from agno.models.ollama import Ollama
from config import settings
from infrastructure.events.bus import Event, EventBus
try:
from mcp.registry import tool_registry
except ImportError:
tool_registry = None
logger = logging.getLogger(__name__)
class BaseAgent(ABC):
"""Base class for all agents."""
def __init__(
self,
agent_id: str,
name: str,
role: str,
system_prompt: str,
tools: list[str] | None = None,
model: str | None = None,
max_history: int = 10,
) -> None:
self.agent_id = agent_id
self.name = name
self.role = role
self.tools = tools or []
self.model = model or settings.ollama_model
self.max_history = max_history
# Create Agno agent
self.system_prompt = system_prompt
self.agent = self._create_agent(system_prompt)
# Event bus for communication
self.event_bus: EventBus | None = None
logger.info(
"%s agent initialized (id: %s, model: %s)",
name,
agent_id,
self.model,
)
def _create_agent(self, system_prompt: str) -> Agent:
"""Create the underlying Agno agent with per-agent model."""
# Get tools from registry
tool_instances = []
if tool_registry is not None:
for tool_name in self.tools:
handler = tool_registry.get_handler(tool_name)
if handler:
tool_instances.append(handler)
return Agent(
name=self.name,
model=Ollama(id=self.model, host=settings.ollama_url, timeout=300),
description=system_prompt,
tools=tool_instances if tool_instances else None,
add_history_to_context=True,
num_history_runs=self.max_history,
markdown=False,
telemetry=settings.telemetry_enabled,
)
def connect_event_bus(self, bus: EventBus) -> None:
"""Connect to the event bus for inter-agent communication."""
self.event_bus = bus
# Subscribe to relevant events
bus.subscribe(f"agent.{self.agent_id}.*")(self._handle_direct_message)
bus.subscribe("agent.task.assigned")(self._handle_task_assignment)
async def _handle_direct_message(self, event: Event) -> None:
"""Handle direct messages to this agent."""
logger.debug("%s received message: %s", self.name, event.type)
async def _handle_task_assignment(self, event: Event) -> None:
"""Handle task assignment events."""
assigned_agent = event.data.get("agent_id")
if assigned_agent == self.agent_id:
task_id = event.data.get("task_id")
description = event.data.get("description", "")
logger.info("%s assigned task %s: %s", self.name, task_id, description[:50])
# Execute the task
await self.execute_task(task_id, description, event.data)
@abstractmethod
async def execute_task(self, task_id: str, description: str, context: dict) -> Any:
"""Execute a task assigned to this agent.
Must be implemented by subclasses.
"""
pass
async def run(self, message: str) -> str:
"""Run the agent with a message.
Returns:
Agent response
"""
result = self.agent.run(message, stream=False)
response = result.content if hasattr(result, "content") else str(result)
# Emit completion event
if self.event_bus:
await self.event_bus.publish(
Event(
type=f"agent.{self.agent_id}.response",
source=self.agent_id,
data={"input": message, "output": response},
)
)
return response
def get_capabilities(self) -> list[str]:
"""Get list of capabilities this agent provides."""
return self.tools
def get_status(self) -> dict:
"""Get current agent status."""
return {
"agent_id": self.agent_id,
"name": self.name,
"role": self.role,
"model": self.model,
"status": "ready",
"tools": self.tools,
}
class SubAgent(BaseAgent):
"""Concrete agent — the single seed class for all agents.
Every agent in the system is an instance of SubAgent, differentiated
only by the config values passed in from agents.yaml. No subclassing
needed — add new agents by editing YAML, not Python.
"""
def __init__(
self,
agent_id: str,
name: str,
role: str,
system_prompt: str,
tools: list[str] | None = None,
model: str | None = None,
max_history: int = 10,
) -> None:
super().__init__(
agent_id=agent_id,
name=name,
role=role,
system_prompt=system_prompt,
tools=tools,
model=model,
max_history=max_history,
)
async def execute_task(self, task_id: str, description: str, context: dict) -> Any:
"""Execute a task by running the agent with the description."""
result = await self.run(description)
return {
"task_id": task_id,
"agent": self.agent_id,
"result": result,
"status": "completed",
}