ilia bdbf09a9ac feat: Implement voice I/O services (TICKET-006, TICKET-010, TICKET-014)
 TICKET-006: Wake-word Detection Service
- Implemented wake-word detection using openWakeWord
- HTTP/WebSocket server on port 8002
- Real-time detection with configurable threshold
- Event emission for ASR integration
- Location: home-voice-agent/wake-word/

 TICKET-010: ASR Service
- Implemented ASR using faster-whisper
- HTTP endpoint for file transcription
- WebSocket endpoint for streaming transcription
- Support for multiple audio formats
- Auto language detection
- GPU acceleration support
- Location: home-voice-agent/asr/

 TICKET-014: TTS Service
- Implemented TTS using Piper
- HTTP endpoint for text-to-speech synthesis
- Low-latency processing (< 500ms)
- Multiple voice support
- WAV audio output
- Location: home-voice-agent/tts/

 TICKET-047: Updated Hardware Purchases
- Marked Pi5 kit, SSD, microphone, and speakers as purchased
- Updated progress log with purchase status

📚 Documentation:
- Added VOICE_SERVICES_README.md with complete testing guide
- Each service includes README.md with usage instructions
- All services ready for Pi5 deployment

🧪 Testing:
- Created test files for each service
- All imports validated
- FastAPI apps created successfully
- Code passes syntax validation

🚀 Ready for:
- Pi5 deployment
- End-to-end voice flow testing
- Integration with MCP server

Files Added:
- wake-word/detector.py
- wake-word/server.py
- wake-word/requirements.txt
- wake-word/README.md
- wake-word/test_detector.py
- asr/service.py
- asr/server.py
- asr/requirements.txt
- asr/README.md
- asr/test_service.py
- tts/service.py
- tts/server.py
- tts/requirements.txt
- tts/README.md
- tts/test_service.py
- VOICE_SERVICES_README.md

Files Modified:
- tickets/done/TICKET-047_hardware-purchases.md

Files Moved:
- tickets/backlog/TICKET-006_prototype-wake-word-node.md → tickets/done/
- tickets/backlog/TICKET-010_streaming-asr-service.md → tickets/done/
- tickets/backlog/TICKET-014_tts-service.md → tickets/done/
2026-01-12 22:22:38 -05:00

371 lines
12 KiB
Python

"""
MCP tools for memory management.
Allows LLM to read and write to long-term memory.
"""
import sys
from pathlib import Path
# Add parent directories to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from typing import Dict, Any
from tools.base import BaseTool
from memory.manager import get_memory_manager
from memory.schema import MemoryCategory, MemorySource
logger = None
try:
import logging
logger = logging.getLogger(__name__)
except:
pass
class StoreMemoryTool(BaseTool):
"""Store a fact in long-term memory."""
def __init__(self):
self._name = "store_memory"
self._description = "Store a fact in long-term memory. Use this when the user explicitly states a fact about themselves, their preferences, or routines."
self._parameters = {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["personal", "family", "preferences", "routines", "facts"],
"description": "Category of the memory"
},
"key": {
"type": "string",
"description": "Key for the memory (e.g., 'favorite_color', 'morning_routine')"
},
"value": {
"type": "string",
"description": "Value of the memory (e.g., 'blue', 'coffee at 7am')"
},
"confidence": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"default": 1.0,
"description": "Confidence level (1.0 for explicit, 0.7-0.9 for inferred)"
},
"context": {
"type": "string",
"description": "Additional context about the memory"
}
},
"required": ["category", "key", "value"]
}
self.memory_manager = get_memory_manager()
@property
def name(self) -> str:
return self._name
@property
def description(self) -> str:
return self._description
def get_schema(self):
return {
"name": self._name,
"description": self._description,
"inputSchema": {
"type": "object",
"properties": self._parameters["properties"],
"required": self._parameters.get("required", [])
}
}
def execute(self, arguments: Dict[str, Any]):
"""Store a memory entry."""
category_str = arguments.get("category")
key = arguments.get("key")
value = arguments.get("value")
confidence = arguments.get("confidence", 1.0)
context = arguments.get("context")
# Map string to enum
category_map = {
"personal": MemoryCategory.PERSONAL,
"family": MemoryCategory.FAMILY,
"preferences": MemoryCategory.PREFERENCES,
"routines": MemoryCategory.ROUTINES,
"facts": MemoryCategory.FACTS
}
category = category_map.get(category_str)
if not category:
raise ValueError(f"Invalid category: {category_str}")
# Determine source based on confidence
if confidence >= 0.95:
source = MemorySource.EXPLICIT
elif confidence >= 0.7:
source = MemorySource.INFERRED
else:
source = MemorySource.CONFIRMED # User confirmed inferred fact
# Store memory
entry = self.memory_manager.store_fact(
category=category,
key=key,
value=value,
confidence=confidence,
source=source,
context=context
)
return {
"success": True,
"message": f"Stored memory: {category_str}/{key} = {value}",
"entry_id": entry.id,
"confidence": entry.confidence
}
class GetMemoryTool(BaseTool):
"""Get a fact from long-term memory."""
def __init__(self):
self._name = "get_memory"
self._description = "Get a fact from long-term memory by category and key."
self._parameters = {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["personal", "family", "preferences", "routines", "facts"],
"description": "Category of the memory"
},
"key": {
"type": "string",
"description": "Key for the memory"
}
},
"required": ["category", "key"]
}
self.memory_manager = get_memory_manager()
@property
def name(self) -> str:
return self._name
@property
def description(self) -> str:
return self._description
def get_schema(self):
return {
"name": self._name,
"description": self._description,
"inputSchema": {
"type": "object",
"properties": self._parameters["properties"],
"required": self._parameters.get("required", [])
}
}
def execute(self, arguments: Dict[str, Any]):
"""Get a memory entry."""
category_str = arguments.get("category")
key = arguments.get("key")
# Map string to enum
category_map = {
"personal": MemoryCategory.PERSONAL,
"family": MemoryCategory.FAMILY,
"preferences": MemoryCategory.PREFERENCES,
"routines": MemoryCategory.ROUTINES,
"facts": MemoryCategory.FACTS
}
category = category_map.get(category_str)
if not category:
raise ValueError(f"Invalid category: {category_str}")
# Get memory
entry = self.memory_manager.get_fact(category, key)
if entry:
return {
"found": True,
"category": category_str,
"key": entry.key,
"value": entry.value,
"confidence": entry.confidence,
"source": entry.source.value,
"context": entry.context
}
else:
return {
"found": False,
"message": f"No memory found for {category_str}/{key}"
}
class SearchMemoryTool(BaseTool):
"""Search memory entries by query."""
def __init__(self):
self._name = "search_memory"
self._description = "Search memory entries by query string. Useful for finding related facts."
self._parameters = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"category": {
"type": "string",
"enum": ["personal", "family", "preferences", "routines", "facts"],
"description": "Optional category filter"
},
"limit": {
"type": "integer",
"default": 10,
"description": "Maximum number of results"
}
},
"required": ["query"]
}
self.memory_manager = get_memory_manager()
@property
def name(self) -> str:
return self._name
@property
def description(self) -> str:
return self._description
def get_schema(self):
return {
"name": self._name,
"description": self._description,
"inputSchema": {
"type": "object",
"properties": self._parameters["properties"],
"required": self._parameters.get("required", [])
}
}
def execute(self, arguments: Dict[str, Any]):
"""Search memory entries."""
query = arguments.get("query")
category_str = arguments.get("category")
limit = arguments.get("limit", 10)
category = None
if category_str:
category_map = {
"personal": MemoryCategory.PERSONAL,
"family": MemoryCategory.FAMILY,
"preferences": MemoryCategory.PREFERENCES,
"routines": MemoryCategory.ROUTINES,
"facts": MemoryCategory.FACTS
}
category = category_map.get(category_str)
if not category:
raise ValueError(f"Invalid category: {category_str}")
# Search memory
results = self.memory_manager.search_facts(query, category, limit)
return {
"query": query,
"count": len(results),
"results": [
{
"category": entry.category.value,
"key": entry.key,
"value": entry.value,
"confidence": entry.confidence,
"source": entry.source.value
}
for entry in results
]
}
class ListMemoryTool(BaseTool):
"""List all memory entries in a category."""
def __init__(self):
self._name = "list_memory"
self._description = "List all memory entries in a category."
self._parameters = {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["personal", "family", "preferences", "routines", "facts"],
"description": "Category to list"
},
"limit": {
"type": "integer",
"default": 20,
"description": "Maximum number of entries"
}
},
"required": ["category"]
}
self.memory_manager = get_memory_manager()
@property
def name(self) -> str:
return self._name
@property
def description(self) -> str:
return self._description
def get_schema(self):
return {
"name": self._name,
"description": self._description,
"inputSchema": {
"type": "object",
"properties": self._parameters["properties"],
"required": self._parameters.get("required", [])
}
}
def execute(self, arguments: Dict[str, Any]):
"""List memory entries in category."""
category_str = arguments.get("category")
limit = arguments.get("limit", 20)
category_map = {
"personal": MemoryCategory.PERSONAL,
"family": MemoryCategory.FAMILY,
"preferences": MemoryCategory.PREFERENCES,
"routines": MemoryCategory.ROUTINES,
"facts": MemoryCategory.FACTS
}
category = category_map.get(category_str)
if not category:
raise ValueError(f"Invalid category: {category_str}")
# Get category facts
entries = self.memory_manager.get_category_facts(category, limit)
return {
"category": category_str,
"count": len(entries),
"entries": [
{
"key": entry.key,
"value": entry.value,
"confidence": entry.confidence,
"source": entry.source.value
}
for entry in entries
]
}