/ core / attention / voice_router.py
voice_router.py
  1  #!/usr/bin/env python3
  2  """
  3  Voice Command Router
  4  
  5  Parses natural language voice commands and routes to appropriate handlers.
  6  
  7  Designed for ambient computing - commands come from:
  8  - iPhone via Siri Shortcuts
  9  - Apple Watch dictation
 10  - AirPods "Hey Siri" relay
 11  - Meta Glasses relay
 12  
 13  Command patterns:
 14  - "What was I working on?" → context query
 15  - "Capture this: <content>" → store thought
 16  - "Run correlation" → trigger analysis
 17  - "Show me the aha moments" → query results
 18  - "That's important" → boost recent content
 19  - "Remind me about <topic>" → context search
 20  
 21  Usage:
 22      router = VoiceRouter()
 23      result = router.process("what was I working on today")
 24      # Returns: {action: 'context_query', response: '...', tts: '...'}
 25  """
 26  
 27  import re
 28  from dataclasses import dataclass, field
 29  from datetime import datetime, timedelta
 30  from typing import Dict, List, Optional, Any, Callable, Tuple
 31  from enum import Enum
 32  
 33  
 34  class CommandIntent(Enum):
 35      """Recognized command intents."""
 36      # Queries
 37      CONTEXT_QUERY = "context_query"  # "What was I working on?"
 38      STATUS_CHECK = "status_check"  # "How am I doing?"
 39      AHA_QUERY = "aha_query"  # "What insights today?"
 40      TOPIC_SEARCH = "topic_search"  # "Tell me about X"
 41  
 42      # Actions
 43      CAPTURE = "capture"  # "Capture this: ..."
 44      MARK_IMPORTANT = "mark_important"  # "That's important"
 45      MARK_IGNORE = "mark_ignore"  # "Scratch that"
 46      RUN_CORRELATION = "run_correlation"  # "Run correlation"
 47      RUN_SYNC = "run_sync"  # "Sync now"
 48  
 49      # Navigation
 50      SHOW_DASHBOARD = "show_dashboard"  # "Show me..."
 51      READ_BRIEF = "read_brief"  # "What's the brief?"
 52  
 53      # Meta
 54      HELP = "help"  # "What can you do?"
 55      UNKNOWN = "unknown"
 56  
 57  
 58  @dataclass
 59  class CommandResult:
 60      """Result of processing a voice command."""
 61      intent: CommandIntent
 62      confidence: float  # 0.0 to 1.0
 63  
 64      # What to do
 65      action: str  # API endpoint or handler name
 66      params: Dict[str, Any] = field(default_factory=dict)
 67  
 68      # Response
 69      response_text: str = ""  # Full response for display
 70      tts_text: str = ""  # Shortened version for speech
 71  
 72      # State
 73      success: bool = True
 74      error: Optional[str] = None
 75  
 76      def to_dict(self) -> Dict[str, Any]:
 77          return {
 78              'intent': self.intent.value,
 79              'confidence': self.confidence,
 80              'action': self.action,
 81              'params': self.params,
 82              'response_text': self.response_text,
 83              'tts_text': self.tts_text,
 84              'success': self.success,
 85              'error': self.error
 86          }
 87  
 88  
 89  class VoiceRouter:
 90      """
 91      Routes voice commands to appropriate handlers.
 92  
 93      Pattern matching is intentionally fuzzy to handle:
 94      - Transcription errors
 95      - Natural speech variations
 96      - Incomplete sentences
 97      """
 98  
 99      # Command patterns: (regex, intent, confidence_boost)
100      PATTERNS: List[Tuple[str, CommandIntent, float]] = [
101          # Context queries
102          (r'what (was|were) (i|we) (working|doing|thinking)', CommandIntent.CONTEXT_QUERY, 0.9),
103          (r'remind me (what|about)', CommandIntent.CONTEXT_QUERY, 0.8),
104          (r'what.*(happened|going on)', CommandIntent.CONTEXT_QUERY, 0.7),
105          (r'catch me up', CommandIntent.CONTEXT_QUERY, 0.85),
106          (r'where (was|were|am) (i|we)', CommandIntent.CONTEXT_QUERY, 0.8),
107  
108          # Status
109          (r'(how|what).*(status|doing|going)', CommandIntent.STATUS_CHECK, 0.7),
110          (r'system status', CommandIntent.STATUS_CHECK, 0.95),
111          (r'health check', CommandIntent.STATUS_CHECK, 0.9),
112  
113          # Aha queries
114          (r'(aha|insight|breakthrough)', CommandIntent.AHA_QUERY, 0.85),
115          (r'what.*(learn|discover|realize)', CommandIntent.AHA_QUERY, 0.7),
116          (r'any.*(insight|important|significant)', CommandIntent.AHA_QUERY, 0.75),
117          (r'high resonance', CommandIntent.AHA_QUERY, 0.9),
118  
119          # Topic search
120          (r'(tell|remind) me about (.+)', CommandIntent.TOPIC_SEARCH, 0.85),
121          (r'what (do|did) (i|we) (know|say|think) about (.+)', CommandIntent.TOPIC_SEARCH, 0.8),
122          (r'search (for )?(.+)', CommandIntent.TOPIC_SEARCH, 0.7),
123          (r'find (.+)', CommandIntent.TOPIC_SEARCH, 0.65),
124  
125          # Capture
126          (r'capture (this|that)?:?\s*(.+)?', CommandIntent.CAPTURE, 0.9),
127          (r'(remember|note|save) (this|that)?:?\s*(.+)?', CommandIntent.CAPTURE, 0.85),
128          (r'(log|record) (this|that)?:?\s*(.+)?', CommandIntent.CAPTURE, 0.8),
129          (r"(here'?s|this is) (a )?(thought|idea|insight):?\s*(.+)?", CommandIntent.CAPTURE, 0.75),
130  
131          # Mark important
132          (r"(that'?s|this is|mark (as )?)(important|significant|key)", CommandIntent.MARK_IMPORTANT, 0.9),
133          (r'boost (that|this|it)', CommandIntent.MARK_IMPORTANT, 0.85),
134          (r'flag (that|this|it)', CommandIntent.MARK_IMPORTANT, 0.8),
135          (r'weight (that|this)', CommandIntent.MARK_IMPORTANT, 0.75),
136  
137          # Mark ignore
138          (r'(scratch|ignore|forget|discard|never ?mind) (that|this|it)?', CommandIntent.MARK_IGNORE, 0.9),
139          (r'(not|no) important', CommandIntent.MARK_IGNORE, 0.7),
140  
141          # Run operations
142          (r'run (the )?correlation', CommandIntent.RUN_CORRELATION, 0.95),
143          (r'correlate', CommandIntent.RUN_CORRELATION, 0.85),
144          (r'mine (the )?(data|transcripts|sessions)', CommandIntent.RUN_CORRELATION, 0.8),
145          (r'analyze', CommandIntent.RUN_CORRELATION, 0.7),
146  
147          (r'(sync|synchronize)( now)?', CommandIntent.RUN_SYNC, 0.9),
148          (r'(collect|gather)( events)?', CommandIntent.RUN_SYNC, 0.8),
149          (r'update( data)?', CommandIntent.RUN_SYNC, 0.65),
150  
151          # Show/display
152          (r'show (me )?(.+)', CommandIntent.SHOW_DASHBOARD, 0.75),
153          (r'display (.+)', CommandIntent.SHOW_DASHBOARD, 0.8),
154          (r'open (the )?dashboard', CommandIntent.SHOW_DASHBOARD, 0.9),
155  
156          # Brief
157          (r"(what'?s|give me) (the )?(brief|summary|overview)", CommandIntent.READ_BRIEF, 0.9),
158          (r'brief me', CommandIntent.READ_BRIEF, 0.95),
159          (r'(daily|morning|evening) (brief|summary)', CommandIntent.READ_BRIEF, 0.9),
160  
161          # Help
162          (r'(what can you|help|commands|how do i)', CommandIntent.HELP, 0.8),
163      ]
164  
165      def __init__(self):
166          # Compiled patterns
167          self._compiled_patterns = [
168              (re.compile(pattern, re.IGNORECASE), intent, conf)
169              for pattern, intent, conf in self.PATTERNS
170          ]
171  
172          # Recent context for "that" / "this" references
173          self._recent_content: List[str] = []
174          self._recent_timestamps: List[datetime] = []
175  
176      def process(self, command: str) -> CommandResult:
177          """
178          Process a voice command and return result.
179  
180          Args:
181              command: Raw voice command text
182  
183          Returns:
184              CommandResult with intent, action, and response
185          """
186          command = command.strip()
187          if not command:
188              return CommandResult(
189                  intent=CommandIntent.UNKNOWN,
190                  confidence=0.0,
191                  action='none',
192                  success=False,
193                  error='Empty command'
194              )
195  
196          # Find best matching intent
197          best_intent = CommandIntent.UNKNOWN
198          best_confidence = 0.0
199          best_match = None
200  
201          for pattern, intent, conf_boost in self._compiled_patterns:
202              match = pattern.search(command)
203              if match:
204                  # Calculate confidence based on match coverage
205                  coverage = len(match.group(0)) / len(command)
206                  confidence = min(1.0, coverage * conf_boost * 1.2)
207  
208                  if confidence > best_confidence:
209                      best_confidence = confidence
210                      best_intent = intent
211                      best_match = match
212  
213          # Route to handler
214          result = self._route_intent(best_intent, best_confidence, command, best_match)
215  
216          # Track for context
217          self._recent_content.append(command)
218          self._recent_timestamps.append(datetime.now())
219          self._prune_recent()
220  
221          return result
222  
223      def _route_intent(
224          self,
225          intent: CommandIntent,
226          confidence: float,
227          command: str,
228          match: Optional[re.Match]
229      ) -> CommandResult:
230          """Route to appropriate handler based on intent."""
231  
232          if intent == CommandIntent.CONTEXT_QUERY:
233              return self._handle_context_query(confidence, command)
234  
235          elif intent == CommandIntent.STATUS_CHECK:
236              return self._handle_status_check(confidence)
237  
238          elif intent == CommandIntent.AHA_QUERY:
239              return self._handle_aha_query(confidence)
240  
241          elif intent == CommandIntent.TOPIC_SEARCH:
242              topic = self._extract_topic(command, match)
243              return self._handle_topic_search(confidence, topic)
244  
245          elif intent == CommandIntent.CAPTURE:
246              content = self._extract_capture_content(command, match)
247              return self._handle_capture(confidence, content)
248  
249          elif intent == CommandIntent.MARK_IMPORTANT:
250              return self._handle_mark_important(confidence)
251  
252          elif intent == CommandIntent.MARK_IGNORE:
253              return self._handle_mark_ignore(confidence)
254  
255          elif intent == CommandIntent.RUN_CORRELATION:
256              return self._handle_correlation(confidence)
257  
258          elif intent == CommandIntent.RUN_SYNC:
259              return self._handle_sync(confidence)
260  
261          elif intent == CommandIntent.SHOW_DASHBOARD:
262              target = self._extract_show_target(command, match)
263              return self._handle_show(confidence, target)
264  
265          elif intent == CommandIntent.READ_BRIEF:
266              return self._handle_brief(confidence)
267  
268          elif intent == CommandIntent.HELP:
269              return self._handle_help(confidence)
270  
271          else:
272              return CommandResult(
273                  intent=CommandIntent.UNKNOWN,
274                  confidence=confidence,
275                  action='none',
276                  response_text=f"I didn't understand: {command}",
277                  tts_text="I didn't catch that. Try 'help' for commands.",
278                  success=False
279              )
280  
281      # ==================== Handlers ====================
282  
283      def _handle_context_query(self, confidence: float, command: str) -> CommandResult:
284          """Handle 'what was I working on' queries."""
285          # Parse time window from command
286          hours = 24  # default
287          if 'today' in command.lower():
288              hours = 12
289          elif 'yesterday' in command.lower():
290              hours = 48
291          elif 'week' in command.lower():
292              hours = 168
293          elif 'hour' in command.lower():
294              hours = 2
295  
296          return CommandResult(
297              intent=CommandIntent.CONTEXT_QUERY,
298              confidence=confidence,
299              action='/context/direct',
300              params={'input': command, 'hours': hours},
301              response_text=f"Querying context for last {hours} hours...",
302              tts_text="Let me check what you were working on."
303          )
304  
305      def _handle_status_check(self, confidence: float) -> CommandResult:
306          return CommandResult(
307              intent=CommandIntent.STATUS_CHECK,
308              confidence=confidence,
309              action='/mobile/status',
310              params={},
311              response_text="Fetching system status...",
312              tts_text="Checking status."
313          )
314  
315      def _handle_aha_query(self, confidence: float) -> CommandResult:
316          return CommandResult(
317              intent=CommandIntent.AHA_QUERY,
318              confidence=confidence,
319              action='/correlate/aha',
320              params={'limit': 10},
321              response_text="Fetching recent aha moments...",
322              tts_text="Looking for insights."
323          )
324  
325      def _handle_topic_search(self, confidence: float, topic: str) -> CommandResult:
326          if not topic:
327              return CommandResult(
328                  intent=CommandIntent.TOPIC_SEARCH,
329                  confidence=confidence * 0.5,
330                  action='none',
331                  response_text="What topic should I search for?",
332                  tts_text="What topic?",
333                  success=False
334              )
335  
336          return CommandResult(
337              intent=CommandIntent.TOPIC_SEARCH,
338              confidence=confidence,
339              action='/graph/search',
340              params={'q': topic, 'limit': 20},
341              response_text=f"Searching for '{topic}'...",
342              tts_text=f"Searching for {topic}."
343          )
344  
345      def _handle_capture(self, confidence: float, content: str) -> CommandResult:
346          if not content:
347              return CommandResult(
348                  intent=CommandIntent.CAPTURE,
349                  confidence=confidence * 0.5,
350                  action='none',
351                  response_text="What should I capture?",
352                  tts_text="What should I capture?",
353                  success=False
354              )
355  
356          return CommandResult(
357              intent=CommandIntent.CAPTURE,
358              confidence=confidence,
359              action='/mobile/capture',
360              params={'thought': content},
361              response_text=f"Capturing: {content[:100]}...",
362              tts_text="Captured."
363          )
364  
365      def _handle_mark_important(self, confidence: float) -> CommandResult:
366          # Get most recent content
367          recent = self._get_recent_reference()
368  
369          return CommandResult(
370              intent=CommandIntent.MARK_IMPORTANT,
371              confidence=confidence,
372              action='/mobile/capture',
373              params={
374                  'thought': f"[IMPORTANT] {recent}" if recent else "[IMPORTANT] Last 30 seconds",
375                  'tags': ['important', 'flagged']
376              },
377              response_text="Marked as important.",
378              tts_text="Marked."
379          )
380  
381      def _handle_mark_ignore(self, confidence: float) -> CommandResult:
382          return CommandResult(
383              intent=CommandIntent.MARK_IGNORE,
384              confidence=confidence,
385              action='internal',
386              params={'action': 'ignore_recent'},
387              response_text="Ignored.",
388              tts_text="Ignored."
389          )
390  
391      def _handle_correlation(self, confidence: float) -> CommandResult:
392          return CommandResult(
393              intent=CommandIntent.RUN_CORRELATION,
394              confidence=confidence,
395              action='/unified/report',
396              params={'hours': 24},
397              response_text="Running correlation analysis...",
398              tts_text="Running correlation."
399          )
400  
401      def _handle_sync(self, confidence: float) -> CommandResult:
402          return CommandResult(
403              intent=CommandIntent.RUN_SYNC,
404              confidence=confidence,
405              action='/pipeline/collect',
406              params={},
407              response_text="Syncing data sources...",
408              tts_text="Syncing."
409          )
410  
411      def _handle_show(self, confidence: float, target: str) -> CommandResult:
412          # Map targets to views
413          view_map = {
414              'dashboard': '/unified/report',
415              'aha': '/correlate/aha',
416              'graph': '/graph/status',
417              'atoms': '/graph/atoms',
418              'resonance': '/graph/resonance',
419              'gravity': '/graph/gravity-wells',
420              'timeline': '/unified/timeline',
421          }
422  
423          action = view_map.get(target.lower(), '/unified/report')
424  
425          return CommandResult(
426              intent=CommandIntent.SHOW_DASHBOARD,
427              confidence=confidence,
428              action=action,
429              params={'target': target},
430              response_text=f"Showing {target}...",
431              tts_text=f"Showing {target}."
432          )
433  
434      def _handle_brief(self, confidence: float) -> CommandResult:
435          return CommandResult(
436              intent=CommandIntent.READ_BRIEF,
437              confidence=confidence,
438              action='/mobile/brief',
439              params={},
440              response_text="Fetching your brief...",
441              tts_text="Here's your brief."
442          )
443  
444      def _handle_help(self, confidence: float) -> CommandResult:
445          help_text = """
446  Available commands:
447  - "What was I working on?" - Get context summary
448  - "Capture this: [thought]" - Save a thought
449  - "That's important" - Mark recent content high-weight
450  - "Run correlation" - Analyze all sources
451  - "Show me [aha/dashboard/graph]" - Display view
452  - "Brief me" - Get daily summary
453  - "Search for [topic]" - Find related content
454  - "Sync now" - Update data
455  """
456          return CommandResult(
457              intent=CommandIntent.HELP,
458              confidence=confidence,
459              action='none',
460              response_text=help_text,
461              tts_text="You can ask about your work, capture thoughts, run analysis, or get your brief."
462          )
463  
464      # ==================== Helpers ====================
465  
466      def _extract_topic(self, command: str, match: Optional[re.Match]) -> str:
467          """Extract search topic from command."""
468          if match and match.lastindex:
469              # Get last captured group (usually the topic)
470              return match.group(match.lastindex).strip()
471  
472          # Fallback: extract after keywords
473          for keyword in ['about', 'for', 'find']:
474              if keyword in command.lower():
475                  parts = command.lower().split(keyword)
476                  if len(parts) > 1:
477                      return parts[-1].strip()
478  
479          return ''
480  
481      def _extract_capture_content(self, command: str, match: Optional[re.Match]) -> str:
482          """Extract content to capture from command."""
483          if match and match.lastindex:
484              for i in range(match.lastindex, 0, -1):
485                  group = match.group(i)
486                  if group and group.strip() and group.lower() not in ['this', 'that', '']:
487                      return group.strip()
488  
489          # Fallback: extract after colon
490          if ':' in command:
491              return command.split(':', 1)[-1].strip()
492  
493          # Try after "capture this" etc
494          for trigger in ['capture this', 'capture that', 'remember this', 'note this']:
495              if trigger in command.lower():
496                  return command.lower().split(trigger)[-1].strip()
497  
498          return ''
499  
500      def _extract_show_target(self, command: str, match: Optional[re.Match]) -> str:
501          """Extract what to show from command."""
502          if match and match.lastindex:
503              return match.group(match.lastindex).strip()
504  
505          # Defaults
506          return 'dashboard'
507  
508      def _get_recent_reference(self) -> str:
509          """Get content that 'that' or 'this' might refer to."""
510          if self._recent_content:
511              return self._recent_content[-1]
512          return ''
513  
514      def _prune_recent(self, max_age_seconds: int = 300):
515          """Remove old entries from recent context."""
516          cutoff = datetime.now() - timedelta(seconds=max_age_seconds)
517  
518          while self._recent_timestamps and self._recent_timestamps[0] < cutoff:
519              self._recent_timestamps.pop(0)
520              if self._recent_content:
521                  self._recent_content.pop(0)
522  
523  
524  # Singleton
525  _voice_router: Optional[VoiceRouter] = None
526  
527  
528  def get_voice_router() -> VoiceRouter:
529      """Get or create voice router singleton."""
530      global _voice_router
531      if _voice_router is None:
532          _voice_router = VoiceRouter()
533      return _voice_router
534  
535  
536  def process_voice_command(command: str) -> Dict[str, Any]:
537      """Quick function to process a voice command."""
538      router = get_voice_router()
539      result = router.process(command)
540      return result.to_dict()
541  
542  
543  if __name__ == '__main__':
544      router = VoiceRouter()
545  
546      # Test commands
547      tests = [
548          "What was I working on today?",
549          "Capture this: The key insight is about attention systems",
550          "That's important",
551          "Run correlation",
552          "Show me the aha moments",
553          "Tell me about graph integration",
554          "Brief me",
555          "Scratch that",
556          "What can you do?",
557          "Sync now",
558      ]
559  
560      print("=== Voice Router Tests ===\n")
561      for cmd in tests:
562          result = router.process(cmd)
563          print(f"Command: {cmd}")
564          print(f"  Intent: {result.intent.value} ({result.confidence:.0%})")
565          print(f"  Action: {result.action}")
566          print(f"  TTS: {result.tts_text}")
567          print()