/ services / pipecat-agent / fast_path.py
fast_path.py
 1  """Fast-path handler for simple deterministic queries.
 2  
 3  Intercepts common queries and responds directly without LLM invocation.
 4  Runs as a Pipecat FrameProcessor between STT and the LLM context aggregator.
 5  
 6  Handles:
 7  - Time/date queries
 8  - Basic weather (from cached data)
 9  - System status (quick summary)
10  
11  If no fast-path match, passes through to LLM normally.
12  """
13  
14  import json
15  import os
16  import re
17  import urllib.request
18  from datetime import datetime
19  
20  from loguru import logger
21  
22  from pipecat.frames.frames import (
23      Frame,
24      TextFrame,
25      TranscriptionFrame,
26  )
27  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
28  
29  FAST_PATH_ENABLED = os.getenv("FAST_PATH_ENABLED", "true").lower() == "true"
30  WEATHER_CACHE_URL = os.getenv("WEATHER_CACHE_URL", "")  # Optional: cached weather endpoint
31  
32  # Time/date patterns
33  TIME_PATTERNS = [
34      r'\bwhat time\b', r'\bwhat\'s the time\b', r'\bcurrent time\b',
35      r'\btell me the time\b',
36  ]
37  DATE_PATTERNS = [
38      r'\bwhat(?:\'s| is) (?:the |today\'s )?date\b', r'\bwhat day\b',
39      r'\btoday\'s date\b', r'\bwhat is today\b',
40  ]
41  
42  # Compile patterns
43  _time_re = re.compile('|'.join(TIME_PATTERNS), re.IGNORECASE)
44  _date_re = re.compile('|'.join(DATE_PATTERNS), re.IGNORECASE)
45  
46  
47  def check_fast_path(text: str) -> str | None:
48      """Check if the text matches a fast-path pattern. Returns response or None."""
49      text_lower = text.lower().strip()
50  
51      # Time
52      if _time_re.search(text_lower):
53          now = datetime.now()
54          return f"It's {now.strftime('%I:%M %p').lstrip('0')}."
55  
56      # Date
57      if _date_re.search(text_lower):
58          now = datetime.now()
59          return f"Today is {now.strftime('%A, %B %d, %Y')}."
60  
61      return None
62  
63  
64  class FastPathProcessor(FrameProcessor):
65      """Intercepts simple queries and responds without LLM invocation."""
66  
67      def __init__(self, *, tts_service=None, **kwargs):
68          super().__init__(**kwargs)
69          self._tts = tts_service
70  
71      async def process_frame(self, frame: Frame, direction: FrameDirection):
72          await super().process_frame(frame, direction)
73  
74          if not FAST_PATH_ENABLED:
75              await self.push_frame(frame, direction)
76              return
77  
78          if isinstance(frame, TranscriptionFrame):
79              text = frame.text
80              # Strip speaker tag if present
81              if text.startswith("["):
82                  idx = text.find("]")
83                  if idx > 0:
84                      text = text[idx+1:].strip()
85  
86              response = check_fast_path(text)
87              if response:
88                  logger.info(f"Fast-path response: '{text}' → '{response}'")
89                  # Push the response as a TextFrame directly to TTS
90                  # Skip the LLM entirely
91                  await self.push_frame(TextFrame(text=response), direction)
92                  return
93  
94          # No fast-path match — pass through to LLM
95          await self.push_frame(frame, direction)