Add think service and supporting core modules
- 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 🦊
This commit is contained in:
278
services/think/reasoning/todo_manager.py
Normal file
278
services/think/reasoning/todo_manager.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user