mgbam commited on
Commit
34600da
·
verified ·
1 Parent(s): 056dfed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +55 -183
app.py CHANGED
@@ -1,116 +1,4 @@
1
- # rentbot/app.py
2
- import os
3
- import base64
4
- import json
5
- import asyncio
6
- import numpy as np
7
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect
8
- from dotenv import load_dotenv
9
-
10
- from audio_utils import ulaw_to_pcm16
11
- from stt_handler import transcribe_audio_chunk
12
- from llm_handler import get_llm_response
13
- from tts_handler import text_to_speech_stream
14
- from tool_handler import execute_tool_call
15
-
16
- # Load environment variables
17
- load_dotenv()
18
-
19
- app = FastAPI()
20
-
21
- # Configuration
22
- SILENCE_THRESHOLD_SECONDS = 0.7
23
- AUDIO_RATE = 8000 # Hz for Twilio media streams
24
- AUDIO_BUFFER_SIZE = int(SILENCE_THRESHOLD_SECONDS * AUDIO_RATE)
25
-
26
- # In-memory session storage (for demonstration)
27
- # In production, use Redis or another persistent store.
28
- sessions = {}
29
-
30
- @app.websocket("/rentbot")
31
- async def websocket_endpoint(ws: WebSocket):
32
- await ws.accept()
33
- stream_sid = None
34
- audio_buffer = np.array([], dtype=np.int16)
35
-
36
- try:
37
- # Initial greeting
38
- # We need a stream_sid to send audio, so we wait for the first 'start' message
39
- # A more robust solution might send a pre-recorded greeting or handle this flow differently.
40
-
41
- async for message in ws.iter_text():
42
- data = json.loads(message)
43
-
44
- if data['event'] == 'start':
45
- stream_sid = data['start']['streamSid']
46
- sessions[stream_sid] = {
47
- "messages": [{"role": "system", "content": os.getenv("SYSTEM_PROMPT")}],
48
- "processing_task": None
49
- }
50
- print(f"New stream started: {stream_sid}")
51
-
52
- # Send an initial greeting
53
- initial_greeting = "Hi! I'm RentBot, your leasing assistant. How can I help you today?"
54
- sessions[stream_sid]["messages"].append({"role": "assistant", "content": initial_greeting})
55
-
56
- async def send_initial_greeting():
57
- tts_iterator = text_to_speech_stream(iter([initial_greeting]))
58
- async for audio_chunk in tts_iterator:
59
- payload = base64.b64encode(audio_chunk).decode('utf-8')
60
- await ws.send_json({
61
- "event": "media",
62
- "streamSid": stream_sid,
63
- "media": {"payload": payload}
64
- })
65
- # Mark the end of the bot's speech
66
- await ws.send_json({"event": "mark", "streamSid": stream_sid, "mark": {"name": "bot_turn_end"}})
67
-
68
- asyncio.create_task(send_initial_greeting())
69
-
70
-
71
- elif data['event'] == 'media':
72
- if not stream_sid: continue
73
-
74
- # Decode the base64 µ-law audio and add to buffer
75
- chunk_ulaw = base64.b64decode(data['media']['payload'])
76
- chunk_pcm = ulaw_to_pcm16(chunk_ulaw)
77
- audio_buffer = np.append(audio_buffer, chunk_pcm)
78
-
79
- # If buffer is full (indicating continuous speech), process it
80
- if len(audio_buffer) >= AUDIO_BUFFER_SIZE:
81
- # If a task is already running, let it finish
82
- if sessions[stream_sid]["processing_task"] and not sessions[stream_sid]["processing_task"].done():
83
- continue # Skip starting a new task
84
-
85
- # Start processing in a background task
86
- task = asyncio.create_task(process_user_audio(ws, stream_sid, audio_buffer))
87
- sessions[stream_sid]["processing_task"] = task
88
- audio_buffer = np.array([], dtype=np.int16) # Reset buffer
89
-
90
- elif data['event'] == 'mark':
91
- # This indicates the user has likely paused. Let's process any remaining audio.
92
- if not stream_sid: continue
93
-
94
- if len(audio_buffer) > 1000: # Heuristic: process if there's meaningful audio left
95
- if not (sessions[stream_sid]["processing_task"] and not sessions[stream_sid]["processing_task"].done()):
96
- task = asyncio.create_task(process_user_audio(ws, stream_sid, audio_buffer))
97
- sessions[stream_sid]["processing_task"] = task
98
- audio_buffer = np.array([], dtype=np.int16) # Reset buffer
99
-
100
- elif data['event'] == 'stop':
101
- print(f"Stream stopped: {stream_sid}")
102
- break
103
-
104
- except WebSocketDisconnect:
105
- print(f"WebSocket disconnected for stream {stream_sid}")
106
- except Exception as e:
107
- print(f"An error occurred: {e}")
108
- finally:
109
- if stream_sid and stream_sid in sessions:
110
- if sessions[stream_sid]["processing_task"]:
111
- sessions[stream_sid]["processing_task"].cancel()
112
- del sessions[stream_sid]
113
- print(f"Session cleaned up for stream {stream_sid}")
114
 
115
  async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.ndarray):
116
  """The main logic loop: STT -> LLM -> (Tool/TTS)"""
@@ -125,45 +13,37 @@ async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.nda
125
  print(f"[{stream_sid}] User said: {user_text}")
126
  sessions[stream_sid]["messages"].append({"role": "user", "content": user_text})
127
 
128
- # 2. Text-to-Assistant Reply (LLM)
129
- llm_response_generator = get_llm_response(sessions[stream_sid]["messages"])
130
-
131
- # We need to accumulate the text to know if there's a tool call
132
- text_chunks_for_tts = []
133
- assistant_message = None
134
- tool_calls = None
135
-
136
- async def llm_logic_handler():
137
- nonlocal assistant_message, tool_calls
138
- # This async generator will yield text chunks and then return the final message and tool calls
139
- # This is a slightly advanced pattern to handle both streaming and final return values
140
- class ResponseHandler:
141
- def __init__(self, generator):
142
- self._generator = generator
143
- self.final_result = None
144
-
145
- async def __aiter__(self):
146
- return self
147
 
148
- async def __anext__(self):
149
- try:
150
- return await self._generator.__anext__()
151
- except StopAsyncIteration as e:
152
- self.final_result = e.value
153
- raise
154
 
155
- response_handler = ResponseHandler(llm_response_generator)
 
 
 
 
 
 
156
 
157
- async for chunk in response_handler:
158
- text_chunks_for_tts.append(chunk)
159
-
160
- assistant_message, tool_calls = response_handler.final_result
 
 
 
161
 
162
- # Run the LLM logic and TTS streaming concurrently
163
- llm_task = asyncio.create_task(llm_logic_handler())
164
- tts_task = asyncio.create_task(stream_and_send_audio(ws, stream_sid, (chunk for chunk in text_chunks_for_tts)))
165
 
166
- await asyncio.gather(llm_task, tts_task) # Wait for both to complete
 
 
 
 
167
 
168
  # Add the full assistant response to history
169
  if assistant_message and assistant_message.get("content"):
@@ -171,47 +51,39 @@ async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.nda
171
 
172
  # 3. Handle Tool Calls if any
173
  if tool_calls:
174
- # Add the assistant's request for a tool call to history
175
- sessions[stream_sid]["messages"].append(assistant_message)
176
-
177
- for tool_call in tool_calls:
 
 
 
178
  print(f"[{stream_sid}] Executing tool: {tool_call.function.name}")
179
  tool_result_message = execute_tool_call(tool_call)
180
  sessions[stream_sid]["messages"].append(tool_result_message)
181
 
182
  # 4. Get a final response from the LLM after executing the tool
183
- final_response_generator = get_llm_response(sessions[stream_sid]["messages"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- final_text_chunks = []
186
- async for chunk in final_response_generator:
187
- final_text_chunks.append(chunk)
188
 
189
- # Add final response to history
190
- final_assistant_message, _ = await final_response_generator
191
  if final_assistant_message:
192
- sessions[stream_sid]["messages"].append(final_assistant_message)
193
-
194
- # Stream the final response audio
195
- await stream_and_send_audio(ws, stream_sid, iter(final_text_chunks))
196
-
197
-
198
- async def stream_and_send_audio(ws: WebSocket, stream_sid: str, text_iterator):
199
- """Stream text to TTS and send the resulting audio back over the WebSocket."""
200
- async for audio_chunk in text_to_speech_stream(text_iterator):
201
- if audio_chunk:
202
- payload = base64.b64encode(audio_chunk).decode('utf-8')
203
- await ws.send_json({
204
- "event": "media",
205
- "streamSid": stream_sid,
206
- "media": {"payload": payload}
207
- })
208
-
209
- # Mark the end of the bot's turn to let Twilio know it can listen for the user again
210
- await ws.send_json({"event": "mark", "streamSid": stream_sid, "mark": {"name": "bot_turn_end"}})
211
- print(f"[{stream_sid}] Finished sending bot's audio turn.")
212
-
213
-
214
- if __name__ == "__main__":
215
- import uvicorn
216
- print("Starting RentBot server...")
217
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ # In rentbot/app.py, find the process_user_audio function and replace it with this:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.ndarray):
4
  """The main logic loop: STT -> LLM -> (Tool/TTS)"""
 
13
  print(f"[{stream_sid}] User said: {user_text}")
14
  sessions[stream_sid]["messages"].append({"role": "user", "content": user_text})
15
 
16
+ # This queue will hold text chunks from the LLM for the TTS to process.
17
+ tts_queue = asyncio.Queue()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ # This async function is the "handler" that get_llm_response will call.
20
+ async def llm_chunk_handler(chunk):
21
+ await tts_queue.put(chunk)
 
 
 
22
 
23
+ # This async generator reads from the queue for the TTS stream.
24
+ async def tts_text_iterator():
25
+ while True:
26
+ chunk = await tts_queue.get()
27
+ if chunk is None: # Use None as a sentinel to end the stream
28
+ break
29
+ yield chunk
30
 
31
+ # 2. Start LLM and TTS tasks concurrently
32
+ llm_task = asyncio.create_task(
33
+ get_llm_response(sessions[stream_sid]["messages"], llm_chunk_handler)
34
+ )
35
+ tts_task = asyncio.create_task(
36
+ stream_and_send_audio(ws, stream_sid, tts_text_iterator())
37
+ )
38
 
39
+ # Wait for the LLM to finish and get the final message and tool calls
40
+ assistant_message, tool_calls = await llm_task
 
41
 
42
+ # Now that the LLM is done, signal the TTS to finish by putting None on the queue
43
+ await tts_queue.put(None)
44
+
45
+ # Wait for the TTS to finish sending all audio
46
+ await tts_task
47
 
48
  # Add the full assistant response to history
49
  if assistant_message and assistant_message.get("content"):
 
51
 
52
  # 3. Handle Tool Calls if any
53
  if tool_calls:
54
+ for tool_call_data in tool_calls:
55
+ # Recreate the tool_call object structure for execute_tool_call
56
+ tool_call = type('ToolCall', (), {
57
+ 'id': tool_call_data['id'],
58
+ 'function': type('Function', (), tool_call_data['function'])
59
+ })
60
+
61
  print(f"[{stream_sid}] Executing tool: {tool_call.function.name}")
62
  tool_result_message = execute_tool_call(tool_call)
63
  sessions[stream_sid]["messages"].append(tool_result_message)
64
 
65
  # 4. Get a final response from the LLM after executing the tool
66
+ final_tts_queue = asyncio.Queue()
67
+ async def final_llm_chunk_handler(chunk):
68
+ await final_tts_queue.put(chunk)
69
+
70
+ async def final_tts_iterator():
71
+ while True:
72
+ chunk = await final_tts_queue.get()
73
+ if chunk is None:
74
+ break
75
+ yield chunk
76
+
77
+ final_llm_task = asyncio.create_task(
78
+ get_llm_response(sessions[stream_sid]["messages"], final_llm_chunk_handler)
79
+ )
80
+ final_tts_task = asyncio.create_task(
81
+ stream_and_send_audio(ws, stream_sid, final_tts_iterator())
82
+ )
83
 
84
+ final_assistant_message, _ = await final_llm_task
85
+ await final_tts_queue.put(None)
86
+ await final_tts_task
87
 
 
 
88
  if final_assistant_message:
89
+ sessions[stream_sid]["messages"].append(final_assistant_message)