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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -36
app.py CHANGED
@@ -1,5 +1,104 @@
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)"""
5
  print(f"[{stream_sid}] Processing audio chunk of size {len(audio_chunk)}...")
@@ -13,50 +112,38 @@ async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.nda
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"):
50
  sessions[stream_sid]["messages"].append(assistant_message)
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)
@@ -64,26 +151,40 @@ async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.nda
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rentbot/app.py
2
 
3
+ import os
4
+ import base64
5
+ import json
6
+ import asyncio
7
+ import numpy as np
8
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
9
+ from dotenv import load_dotenv
10
+
11
+ from audio_utils import ulaw_to_pcm16
12
+ from stt_handler import transcribe_audio_chunk
13
+ from llm_handler import get_llm_response
14
+ from tts_handler import text_to_speech_stream
15
+ from tool_handler import execute_tool_call
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ app = FastAPI()
21
+
22
+ # Configuration
23
+ SILENCE_THRESHOLD_SECONDS = 0.7
24
+ AUDIO_RATE = 8000 # Hz for Twilio media streams
25
+ AUDIO_BUFFER_SIZE = int(SILENCE_THRESHOLD_SECONDS * AUDIO_RATE)
26
+
27
+ # In-memory session storage (for demonstration)
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
+ async for message in ws.iter_text():
38
+ data = json.loads(message)
39
+
40
+ if data['event'] == 'start':
41
+ stream_sid = data['start']['streamSid']
42
+ sessions[stream_sid] = {
43
+ "messages": [{"role": "system", "content": os.getenv("SYSTEM_PROMPT")}],
44
+ "processing_task": None
45
+ }
46
+ print(f"New stream started: {stream_sid}")
47
+
48
+ # Send an initial greeting
49
+ initial_greeting = "Hi! I'm RentBot, your leasing assistant. How can I help you today?"
50
+ sessions[stream_sid]["messages"].append({"role": "assistant", "content": initial_greeting})
51
+
52
+ async def send_initial_greeting():
53
+ tts_iterator = text_to_speech_stream(iter([initial_greeting]))
54
+ async for audio_chunk in tts_iterator:
55
+ payload = base64.b64encode(audio_chunk).decode('utf-8')
56
+ await ws.send_json({
57
+ "event": "media",
58
+ "streamSid": stream_sid,
59
+ "media": {"payload": payload}
60
+ })
61
+ await ws.send_json({"event": "mark", "streamSid": stream_sid, "mark": {"name": "bot_turn_end"}})
62
+
63
+ asyncio.create_task(send_initial_greeting())
64
+
65
+ elif data['event'] == 'media':
66
+ if not stream_sid: continue
67
+ chunk_ulaw = base64.b64decode(data['media']['payload'])
68
+ chunk_pcm = ulaw_to_pcm16(chunk_ulaw)
69
+ audio_buffer = np.append(audio_buffer, chunk_pcm)
70
+
71
+ if len(audio_buffer) >= AUDIO_BUFFER_SIZE:
72
+ if sessions[stream_sid]["processing_task"] and not sessions[stream_sid]["processing_task"].done():
73
+ continue
74
+ task = asyncio.create_task(process_user_audio(ws, stream_sid, audio_buffer))
75
+ sessions[stream_sid]["processing_task"] = task
76
+ audio_buffer = np.array([], dtype=np.int16)
77
+
78
+ elif data['event'] == 'mark':
79
+ if not stream_sid: continue
80
+ if len(audio_buffer) > 1000:
81
+ if not (sessions[stream_sid]["processing_task"] and not sessions[stream_sid]["processing_task"].done()):
82
+ task = asyncio.create_task(process_user_audio(ws, stream_sid, audio_buffer))
83
+ sessions[stream_sid]["processing_task"] = task
84
+ audio_buffer = np.array([], dtype=np.int16)
85
+
86
+ elif data['event'] == 'stop':
87
+ print(f"Stream stopped: {stream_sid}")
88
+ break
89
+
90
+ except WebSocketDisconnect:
91
+ print(f"WebSocket disconnected for stream {stream_sid}")
92
+ except Exception as e:
93
+ print(f"An error occurred: {e}")
94
+ finally:
95
+ if stream_sid and stream_sid in sessions:
96
+ if sessions[stream_sid]["processing_task"]:
97
+ sessions[stream_sid]["processing_task"].cancel()
98
+ del sessions[stream_sid]
99
+ print(f"Session cleaned up for stream {stream_sid}")
100
+
101
+ # THIS IS THE CORRECTED FUNCTION FROM THE PREVIOUS STEP, NOW IN THE FULL FILE
102
  async def process_user_audio(ws: WebSocket, stream_sid: str, audio_chunk: np.ndarray):
103
  """The main logic loop: STT -> LLM -> (Tool/TTS)"""
104
  print(f"[{stream_sid}] Processing audio chunk of size {len(audio_chunk)}...")
 
112
  print(f"[{stream_sid}] User said: {user_text}")
113
  sessions[stream_sid]["messages"].append({"role": "user", "content": user_text})
114
 
 
115
  tts_queue = asyncio.Queue()
 
 
116
  async def llm_chunk_handler(chunk):
117
  await tts_queue.put(chunk)
118
 
 
119
  async def tts_text_iterator():
120
  while True:
121
  chunk = await tts_queue.get()
122
+ if chunk is None: break
 
123
  yield chunk
124
 
125
  # 2. Start LLM and TTS tasks concurrently
126
+ llm_task = asyncio.create_task(get_llm_response(sessions[stream_sid]["messages"], llm_chunk_handler))
127
+ tts_task = asyncio.create_task(stream_and_send_audio(ws, stream_sid, tts_text_iterator()))
 
 
 
 
 
 
 
128
 
129
+ assistant_message, tool_calls = await llm_task
130
  await tts_queue.put(None)
 
 
131
  await tts_task
132
 
 
133
  if assistant_message and assistant_message.get("content"):
134
  sessions[stream_sid]["messages"].append(assistant_message)
135
 
136
  # 3. Handle Tool Calls if any
137
  if tool_calls:
138
+ # Before executing, add the assistant's tool request to history
139
+ sessions[stream_sid]["messages"].append(assistant_message)
140
+
141
  for tool_call_data in tool_calls:
142
+ # Recreate a simple object that mimics the structure tool_handler expects
143
  tool_call = type('ToolCall', (), {
144
+ 'id': tool_call_data.get('id'),
145
+ 'function': type('Function', (), tool_call_data.get('function'))
146
+ })()
147
 
148
  print(f"[{stream_sid}] Executing tool: {tool_call.function.name}")
149
  tool_result_message = execute_tool_call(tool_call)
 
151
 
152
  # 4. Get a final response from the LLM after executing the tool
153
  final_tts_queue = asyncio.Queue()
154
+ async def final_llm_chunk_handler(chunk): await final_tts_queue.put(chunk)
 
 
155
  async def final_tts_iterator():
156
  while True:
157
  chunk = await final_tts_queue.get()
158
+ if chunk is None: break
 
159
  yield chunk
160
 
161
+ final_llm_task = asyncio.create_task(get_llm_response(sessions[stream_sid]["messages"], final_llm_chunk_handler))
162
+ final_tts_task = asyncio.create_task(stream_and_send_audio(ws, stream_sid, final_tts_iterator()))
 
 
 
 
163
 
164
  final_assistant_message, _ = await final_llm_task
165
  await final_tts_queue.put(None)
166
  await final_tts_task
167
 
168
  if final_assistant_message:
169
+ sessions[stream_sid]["messages"].append(final_assistant_message)
170
+
171
+
172
+ async def stream_and_send_audio(ws: WebSocket, stream_sid: str, text_iterator):
173
+ """Stream text to TTS and send the resulting audio back over the WebSocket."""
174
+ async for audio_chunk in text_to_speech_stream(text_iterator):
175
+ if audio_chunk:
176
+ payload = base64.b64encode(audio_chunk).decode('utf-8')
177
+ await ws.send_json({
178
+ "event": "media",
179
+ "streamSid": stream_sid,
180
+ "media": {"payload": payload}
181
+ })
182
+
183
+ await ws.send_json({"event": "mark", "streamSid": stream_sid, "mark": {"name": "bot_turn_end"}})
184
+ print(f"[{stream_sid}] Finished sending bot's audio turn.")
185
+
186
+
187
+ if __name__ == "__main__":
188
+ import uvicorn
189
+ print("Starting RentBot server...")
190
+ uvicorn.run(app, host="0.0.0.0", port=8000)