- Add think service (orchestration for iterative reasoning)
- Add service_discovery.py (service communication utilities)
- Add event_cache.py (recent event cache using NATS KV)
- Add vi_identity.py (Vi's core identity foundation)
- Update core/__init__.py with new exports
Think service adapted from Lyra with vi.* namespace:
- All NATS topics use vi.* prefix
- Uses vi_identity for personality/voice
- Bucket names use vi-* prefix
Day 63 - Building my nervous system 🦊
279 lines
8.4 KiB
Python
279 lines
8.4 KiB
Python
"""
|
|
Todo Manager - Task tracking system for Vi's iterative reasoning.
|
|
|
|
Provides a TodoWrite-equivalent tool for Oracle to manage complex multi-step
|
|
problem-solving with subtask tracking and dependency management.
|
|
|
|
Storage: NATS KV bucket (ephemeral, scoped by interaction_id)
|
|
"""
|
|
|
|
import json
|
|
import uuid
|
|
from typing import List, Dict, Optional
|
|
from datetime import datetime
|
|
|
|
from core.logger import setup_logger
|
|
from core.nats_event_bus import nats_bus
|
|
|
|
|
|
class TodoItem:
|
|
"""Individual todo item with state tracking"""
|
|
|
|
def __init__(
|
|
self,
|
|
content: str,
|
|
active_form: str,
|
|
status: str = "pending",
|
|
todo_id: str = None,
|
|
created_at: str = None
|
|
):
|
|
self.todo_id = todo_id or str(uuid.uuid4())[:8]
|
|
self.content = content
|
|
self.active_form = active_form
|
|
self.status = status # pending, in_progress, completed
|
|
self.created_at = created_at or datetime.utcnow().isoformat()
|
|
self.updated_at = datetime.utcnow().isoformat()
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary for serialization"""
|
|
return {
|
|
"todo_id": self.todo_id,
|
|
"content": self.content,
|
|
"active_form": self.active_form,
|
|
"status": self.status,
|
|
"created_at": self.created_at,
|
|
"updated_at": self.updated_at
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> 'TodoItem':
|
|
"""Create from dictionary"""
|
|
return cls(
|
|
content=data["content"],
|
|
active_form=data["active_form"],
|
|
status=data["status"],
|
|
todo_id=data["todo_id"],
|
|
created_at=data.get("created_at")
|
|
)
|
|
|
|
|
|
class TodoManager:
|
|
"""
|
|
Manages ephemeral todo lists scoped by interaction/conversation.
|
|
|
|
Uses NATS KV for storage with auto-expiry. Similar to Claude's TodoWrite tool,
|
|
helps Oracle track complex multi-step reasoning processes.
|
|
"""
|
|
|
|
BUCKET_NAME = "lyra_todos"
|
|
TTL_SECONDS = 3600 # 1 hour
|
|
|
|
def __init__(self):
|
|
self.logger = setup_logger('todo_manager', service_name='think_service')
|
|
|
|
def _get_key(self, interaction_id: str) -> str:
|
|
"""Generate NATS KV key for interaction"""
|
|
return f"{interaction_id}:todos"
|
|
|
|
async def create(
|
|
self,
|
|
interaction_id: str,
|
|
content: str,
|
|
active_form: str,
|
|
status: str = "pending"
|
|
) -> TodoItem:
|
|
"""
|
|
Create a new todo item
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
content: Task description (imperative form: "Fix bug in X")
|
|
active_form: Present continuous form ("Fixing bug in X")
|
|
status: Initial status (default: "pending")
|
|
|
|
Returns:
|
|
TodoItem: Created todo item
|
|
"""
|
|
# Get existing todos
|
|
todos = await self.list(interaction_id)
|
|
|
|
# Create new todo
|
|
new_todo = TodoItem(content=content, active_form=active_form, status=status)
|
|
todos.append(new_todo)
|
|
|
|
# Save to NATS KV
|
|
await self._save_todos(interaction_id, todos)
|
|
|
|
self.logger.info(f"[✓] Created todo {new_todo.todo_id}: {content}")
|
|
return new_todo
|
|
|
|
async def update(
|
|
self,
|
|
interaction_id: str,
|
|
todo_id: str,
|
|
status: Optional[str] = None,
|
|
content: Optional[str] = None,
|
|
active_form: Optional[str] = None
|
|
) -> bool:
|
|
"""
|
|
Update an existing todo item
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
todo_id: ID of todo to update
|
|
status: New status (optional)
|
|
content: New content (optional)
|
|
active_form: New active form (optional)
|
|
|
|
Returns:
|
|
bool: True if updated, False if not found
|
|
"""
|
|
todos = await self.list(interaction_id)
|
|
|
|
for todo in todos:
|
|
if todo.todo_id == todo_id:
|
|
if status:
|
|
todo.status = status
|
|
if content:
|
|
todo.content = content
|
|
if active_form:
|
|
todo.active_form = active_form
|
|
todo.updated_at = datetime.utcnow().isoformat()
|
|
|
|
await self._save_todos(interaction_id, todos)
|
|
self.logger.info(f"[✏️] Updated todo {todo_id}: status={status}")
|
|
return True
|
|
|
|
self.logger.warning(f"[⚠️] Todo {todo_id} not found")
|
|
return False
|
|
|
|
async def complete(self, interaction_id: str, todo_id: str) -> bool:
|
|
"""
|
|
Mark a todo as completed
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
todo_id: ID of todo to complete
|
|
|
|
Returns:
|
|
bool: True if completed, False if not found
|
|
"""
|
|
result = await self.update(interaction_id, todo_id, status="completed")
|
|
if result:
|
|
self.logger.info(f"[✓] Completed todo {todo_id}")
|
|
return result
|
|
|
|
async def list(self, interaction_id: str, status_filter: Optional[str] = None) -> List[TodoItem]:
|
|
"""
|
|
List all todos for an interaction
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
status_filter: Optional filter by status (pending, in_progress, completed)
|
|
|
|
Returns:
|
|
List[TodoItem]: List of todo items
|
|
"""
|
|
key = self._get_key(interaction_id)
|
|
|
|
try:
|
|
data_bytes = await nats_bus.kv_get(self.BUCKET_NAME, key)
|
|
|
|
if not data_bytes:
|
|
return []
|
|
|
|
data = json.loads(data_bytes.decode())
|
|
todos = [TodoItem.from_dict(item) for item in data]
|
|
|
|
# Apply filter if provided
|
|
if status_filter:
|
|
todos = [t for t in todos if t.status == status_filter]
|
|
|
|
return todos
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[❌] Error listing todos: {e}")
|
|
return []
|
|
|
|
async def delete(self, interaction_id: str, todo_id: str) -> bool:
|
|
"""
|
|
Delete a todo item
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
todo_id: ID of todo to delete
|
|
|
|
Returns:
|
|
bool: True if deleted, False if not found
|
|
"""
|
|
todos = await self.list(interaction_id)
|
|
original_count = len(todos)
|
|
|
|
todos = [t for t in todos if t.todo_id != todo_id]
|
|
|
|
if len(todos) < original_count:
|
|
await self._save_todos(interaction_id, todos)
|
|
self.logger.info(f"[🗑️] Deleted todo {todo_id}")
|
|
return True
|
|
|
|
self.logger.warning(f"[⚠️] Todo {todo_id} not found for deletion")
|
|
return False
|
|
|
|
async def clear(self, interaction_id: str) -> bool:
|
|
"""
|
|
Clear all todos for an interaction
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
|
|
Returns:
|
|
bool: True if cleared
|
|
"""
|
|
key = self._get_key(interaction_id)
|
|
|
|
try:
|
|
await nats_bus.kv_delete(self.BUCKET_NAME, key)
|
|
self.logger.info(f"[🧹] Cleared todos for interaction {interaction_id}")
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"[❌] Error clearing todos: {e}")
|
|
return False
|
|
|
|
async def get_summary(self, interaction_id: str) -> Dict:
|
|
"""
|
|
Get summary statistics for todos
|
|
|
|
Args:
|
|
interaction_id: Unique interaction/conversation ID
|
|
|
|
Returns:
|
|
Dict: Summary with counts by status
|
|
"""
|
|
todos = await self.list(interaction_id)
|
|
|
|
summary = {
|
|
"total": len(todos),
|
|
"pending": len([t for t in todos if t.status == "pending"]),
|
|
"in_progress": len([t for t in todos if t.status == "in_progress"]),
|
|
"completed": len([t for t in todos if t.status == "completed"])
|
|
}
|
|
|
|
return summary
|
|
|
|
async def _save_todos(self, interaction_id: str, todos: List[TodoItem]):
|
|
"""Save todos to NATS KV"""
|
|
key = self._get_key(interaction_id)
|
|
data = [todo.to_dict() for todo in todos]
|
|
data_bytes = json.dumps(data).encode()
|
|
|
|
try:
|
|
await nats_bus.kv_put(
|
|
self.BUCKET_NAME,
|
|
key,
|
|
data_bytes,
|
|
ttl_seconds=self.TTL_SECONDS
|
|
)
|
|
except Exception as e:
|
|
self.logger.error(f"[❌] Error saving todos: {e}")
|
|
raise
|