forked from Rockachopa/Timmy-time-dashboard
383 lines
11 KiB
Python
383 lines
11 KiB
Python
"""TimAgent Interface — The substrate-agnostic agent contract.
|
|
|
|
This is the foundation for embodiment. Whether Timmy runs on:
|
|
- A server with Ollama (today)
|
|
- A Raspberry Pi with sensors
|
|
- A Boston Dynamics Spot robot
|
|
- A VR avatar
|
|
|
|
The interface remains constant. Implementation varies.
|
|
|
|
Architecture:
|
|
perceive() → reason → act()
|
|
↑ ↓
|
|
←←← remember() ←←←←←←┘
|
|
|
|
All methods return effects that can be logged, audited, and replayed.
|
|
"""
|
|
|
|
import uuid
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from enum import Enum, auto
|
|
from typing import Any, Optional
|
|
|
|
|
|
class PerceptionType(Enum):
|
|
"""Types of sensory input an agent can receive."""
|
|
|
|
TEXT = auto() # Natural language
|
|
IMAGE = auto() # Visual input
|
|
AUDIO = auto() # Sound/speech
|
|
SENSOR = auto() # Temperature, distance, etc.
|
|
MOTION = auto() # Accelerometer, gyroscope
|
|
NETWORK = auto() # API calls, messages
|
|
INTERNAL = auto() # Self-monitoring (battery, temp)
|
|
|
|
|
|
class ActionType(Enum):
|
|
"""Types of actions an agent can perform."""
|
|
|
|
TEXT = auto() # Generate text response
|
|
SPEAK = auto() # Text-to-speech
|
|
MOVE = auto() # Physical movement
|
|
GRIP = auto() # Manipulate objects
|
|
CALL = auto() # API/network call
|
|
EMIT = auto() # Signal/light/sound
|
|
SLEEP = auto() # Power management
|
|
|
|
|
|
class AgentCapability(Enum):
|
|
"""High-level capabilities a TimAgent may possess."""
|
|
|
|
REASONING = "reasoning"
|
|
CODING = "coding"
|
|
WRITING = "writing"
|
|
ANALYSIS = "analysis"
|
|
VISION = "vision"
|
|
SPEECH = "speech"
|
|
NAVIGATION = "navigation"
|
|
MANIPULATION = "manipulation"
|
|
LEARNING = "learning"
|
|
COMMUNICATION = "communication"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AgentIdentity:
|
|
"""Immutable identity for an agent instance.
|
|
|
|
This persists across sessions and substrates. If Timmy moves
|
|
from cloud to robot, the identity follows.
|
|
"""
|
|
|
|
id: str
|
|
name: str
|
|
version: str
|
|
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
|
|
@classmethod
|
|
def generate(cls, name: str, version: str = "1.0.0") -> "AgentIdentity":
|
|
"""Generate a new unique identity."""
|
|
return cls(
|
|
id=str(uuid.uuid4()),
|
|
name=name,
|
|
version=version,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Perception:
|
|
"""A sensory input to the agent.
|
|
|
|
Substrate-agnostic representation. A camera image and a
|
|
LiDAR point cloud are both Perception instances.
|
|
"""
|
|
|
|
type: PerceptionType
|
|
data: Any # Content depends on type
|
|
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
source: str = "unknown" # e.g., "camera_1", "microphone", "user_input"
|
|
metadata: dict = field(default_factory=dict)
|
|
|
|
@classmethod
|
|
def text(cls, content: str, source: str = "user") -> "Perception":
|
|
"""Factory for text perception."""
|
|
return cls(
|
|
type=PerceptionType.TEXT,
|
|
data=content,
|
|
source=source,
|
|
)
|
|
|
|
@classmethod
|
|
def sensor(cls, kind: str, value: float, unit: str = "") -> "Perception":
|
|
"""Factory for sensor readings."""
|
|
return cls(
|
|
type=PerceptionType.SENSOR,
|
|
data={"kind": kind, "value": value, "unit": unit},
|
|
source=f"sensor_{kind}",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Action:
|
|
"""An action the agent intends to perform.
|
|
|
|
Actions are effects — they describe what should happen,
|
|
not how. The substrate implements the "how."
|
|
"""
|
|
|
|
type: ActionType
|
|
payload: Any # Action-specific data
|
|
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
confidence: float = 1.0 # 0-1, agent's certainty
|
|
deadline: Optional[str] = None # When action must complete
|
|
|
|
@classmethod
|
|
def respond(cls, text: str, confidence: float = 1.0) -> "Action":
|
|
"""Factory for text response action."""
|
|
return cls(
|
|
type=ActionType.TEXT,
|
|
payload=text,
|
|
confidence=confidence,
|
|
)
|
|
|
|
@classmethod
|
|
def move(cls, vector: tuple[float, float, float], speed: float = 1.0) -> "Action":
|
|
"""Factory for movement action (x, y, z meters)."""
|
|
return cls(
|
|
type=ActionType.MOVE,
|
|
payload={"vector": vector, "speed": speed},
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Memory:
|
|
"""A stored experience or fact.
|
|
|
|
Memories are substrate-agnostic. A conversation history
|
|
and a video recording are both Memory instances.
|
|
"""
|
|
|
|
id: str
|
|
content: Any
|
|
created_at: str
|
|
access_count: int = 0
|
|
last_accessed: Optional[str] = None
|
|
importance: float = 0.5 # 0-1, for pruning decisions
|
|
tags: list[str] = field(default_factory=list)
|
|
|
|
def touch(self) -> None:
|
|
"""Mark memory as accessed."""
|
|
self.access_count += 1
|
|
self.last_accessed = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
@dataclass
|
|
class Communication:
|
|
"""A message to/from another agent or human."""
|
|
|
|
sender: str
|
|
recipient: str
|
|
content: Any
|
|
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
protocol: str = "direct" # e.g., "http", "websocket", "speech"
|
|
encrypted: bool = False
|
|
|
|
|
|
class TimAgent(ABC):
|
|
"""Abstract base class for all Timmy agent implementations.
|
|
|
|
This is the substrate-agnostic interface. Implementations:
|
|
- OllamaAgent: LLM-based reasoning (today)
|
|
- RobotAgent: Physical embodiment (future)
|
|
- SimulationAgent: Virtual environment (future)
|
|
|
|
Usage:
|
|
agent = OllamaAgent(identity) # Today's implementation
|
|
|
|
perception = Perception.text("Hello Timmy")
|
|
memory = agent.perceive(perception)
|
|
|
|
action = agent.reason("How should I respond?")
|
|
result = agent.act(action)
|
|
|
|
agent.remember(memory) # Store for future
|
|
"""
|
|
|
|
def __init__(self, identity: AgentIdentity) -> None:
|
|
self._identity = identity
|
|
self._capabilities: set[AgentCapability] = set()
|
|
self._state: dict[str, Any] = {}
|
|
|
|
@property
|
|
def identity(self) -> AgentIdentity:
|
|
"""Return this agent's immutable identity."""
|
|
return self._identity
|
|
|
|
@property
|
|
def capabilities(self) -> set[AgentCapability]:
|
|
"""Return set of supported capabilities."""
|
|
return self._capabilities.copy()
|
|
|
|
def has_capability(self, capability: AgentCapability) -> bool:
|
|
"""Check if agent supports a capability."""
|
|
return capability in self._capabilities
|
|
|
|
@abstractmethod
|
|
def perceive(self, perception: Perception) -> Memory:
|
|
"""Process sensory input and create a memory.
|
|
|
|
This is the entry point for all agent interaction.
|
|
A text message, camera frame, or temperature reading
|
|
all enter through perceive().
|
|
|
|
Args:
|
|
perception: Sensory input
|
|
|
|
Returns:
|
|
Memory: Stored representation of the perception
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def reason(self, query: str, context: list[Memory]) -> Action:
|
|
"""Reason about a situation and decide on action.
|
|
|
|
This is where "thinking" happens. The agent uses its
|
|
substrate-appropriate reasoning (LLM, neural net, rules)
|
|
to decide what to do.
|
|
|
|
Args:
|
|
query: What to reason about
|
|
context: Relevant memories for context
|
|
|
|
Returns:
|
|
Action: What the agent decides to do
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def act(self, action: Action) -> Any:
|
|
"""Execute an action in the substrate.
|
|
|
|
This is where the abstract action becomes concrete:
|
|
- TEXT → Generate LLM response
|
|
- MOVE → Send motor commands
|
|
- SPEAK → Call TTS engine
|
|
|
|
Args:
|
|
action: The action to execute
|
|
|
|
Returns:
|
|
Result of the action (substrate-specific)
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def remember(self, memory: Memory) -> None:
|
|
"""Store a memory for future retrieval.
|
|
|
|
The storage mechanism depends on substrate:
|
|
- Cloud: SQLite, vector DB
|
|
- Robot: Local flash storage
|
|
- Hybrid: Synced with conflict resolution
|
|
|
|
Args:
|
|
memory: Experience to store
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def recall(self, query: str, limit: int = 5) -> list[Memory]:
|
|
"""Retrieve relevant memories.
|
|
|
|
Args:
|
|
query: What to search for
|
|
limit: Maximum memories to return
|
|
|
|
Returns:
|
|
List of relevant memories, sorted by relevance
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def communicate(self, message: Communication) -> bool:
|
|
"""Send/receive communication with another agent.
|
|
|
|
Args:
|
|
message: Message to send
|
|
|
|
Returns:
|
|
True if communication succeeded
|
|
"""
|
|
pass
|
|
|
|
def get_state(self) -> dict[str, Any]:
|
|
"""Get current agent state for monitoring/debugging."""
|
|
return {
|
|
"identity": self._identity,
|
|
"capabilities": list(self._capabilities),
|
|
"state": self._state.copy(),
|
|
}
|
|
|
|
def shutdown(self) -> None:
|
|
"""Graceful shutdown. Persist state, close connections."""
|
|
# Override in subclass for cleanup
|
|
pass
|
|
|
|
|
|
class AgentEffect:
|
|
"""Log entry for agent actions — for audit and replay.
|
|
|
|
The complete history of an agent's life can be captured
|
|
as a sequence of AgentEffects. This enables:
|
|
- Debugging: What did the agent see and do?
|
|
- Audit: Why did it make that decision?
|
|
- Replay: Reconstruct agent state from log
|
|
- Training: Learn from agent experiences
|
|
"""
|
|
|
|
def __init__(self, log_path: Optional[str] = None) -> None:
|
|
self._effects: list[dict] = []
|
|
self._log_path = log_path
|
|
|
|
def log_perceive(self, perception: Perception, memory_id: str) -> None:
|
|
"""Log a perception event."""
|
|
self._effects.append(
|
|
{
|
|
"type": "perceive",
|
|
"perception_type": perception.type.name,
|
|
"source": perception.source,
|
|
"memory_id": memory_id,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
|
|
def log_reason(self, query: str, action_type: ActionType) -> None:
|
|
"""Log a reasoning event."""
|
|
self._effects.append(
|
|
{
|
|
"type": "reason",
|
|
"query": query,
|
|
"action_type": action_type.name,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
|
|
def log_act(self, action: Action, result: Any) -> None:
|
|
"""Log an action event."""
|
|
self._effects.append(
|
|
{
|
|
"type": "act",
|
|
"action_type": action.type.name,
|
|
"confidence": action.confidence,
|
|
"result_type": type(result).__name__,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
|
|
def export(self) -> list[dict]:
|
|
"""Export effect log for analysis."""
|
|
return self._effects.copy()
|