forked from ShaerWare/AI_Secretary_System
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathphone_service.py
More file actions
217 lines (168 loc) · 7.87 KB
/
phone_service.py
File metadata and controls
217 lines (168 loc) · 7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#!/usr/bin/env python3
"""
Сервис телефонной интеграции через Twilio
Принимает входящие звонки и обрабатывает их через оркестратор
"""
import logging
import os
import requests
from dotenv import load_dotenv
from fastapi import FastAPI, Request, Response
from twilio.rest import Client
from twilio.twiml.voice_response import Gather, VoiceResponse
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Phone Service")
# Twilio настройки
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
TWILIO_PHONE_NUMBER = os.getenv("TWILIO_PHONE_NUMBER")
# URL оркестратора
ORCHESTRATOR_URL = os.getenv("ORCHESTRATOR_URL", "http://localhost:8000")
# Инициализация Twilio клиента
if TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN:
twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
logger.info("✅ Twilio клиент инициализирован")
else:
twilio_client = None
logger.warning("⚠️ Twilio credentials не найдены. Только локальное тестирование.")
@app.get("/")
async def root():
return {
"status": "ok",
"service": "Phone Service (Twilio Integration)",
"endpoints": {
"incoming_call": "/incoming_call (POST)",
"handle_speech": "/handle_speech (POST)",
"status": "/status (GET)",
},
}
@app.get("/status")
async def status():
"""Статус телефонного сервиса"""
return {
"twilio_configured": twilio_client is not None,
"orchestrator_url": ORCHESTRATOR_URL,
"phone_number": TWILIO_PHONE_NUMBER if TWILIO_PHONE_NUMBER else "not_configured",
}
@app.post("/incoming_call")
async def incoming_call(request: Request):
"""
Обработка входящего звонка от Twilio
Twilio отправляет POST запрос когда поступает звонок
"""
form_data = await request.form()
caller = form_data.get("From", "Unknown")
call_sid = form_data.get("CallSid", "Unknown")
logger.info(f"📞 Входящий звонок от {caller}, CallSid: {call_sid}")
# Создаем TwiML ответ
response = VoiceResponse()
# Приветствие
response.say(
"Здравствуйте! Это виртуальный секретарь. Пожалуйста, говорите после сигнала.",
language="ru-RU",
voice="alice", # Голос Яндекса для русского языка
)
# Записываем речь абонента
response.record(
action="/handle_speech",
method="POST",
max_length=30, # Максимум 30 секунд
play_beep=True,
transcribe=False, # Мы сами распознаем через Whisper
recording_status_callback="/recording_status",
)
return Response(content=str(response), media_type="application/xml")
@app.post("/handle_speech")
async def handle_speech(request: Request):
"""
Обработка записанной речи
Twilio передает URL записи
"""
form_data = await request.form()
recording_url = form_data.get("RecordingUrl")
call_sid = form_data.get("CallSid", "Unknown")
logger.info(f"🎙️ Обработка записи для звонка {call_sid}")
logger.info(f"📎 Recording URL: {recording_url}")
try:
# Скачиваем аудио запись от Twilio
audio_response = requests.get(
recording_url + ".wav", auth=(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
)
if audio_response.status_code != 200:
logger.error(f"❌ Не удалось скачать запись: {audio_response.status_code}")
return _error_response()
# Отправляем на обработку в оркестратор
files = {"audio": ("recording.wav", audio_response.content, "audio/wav")}
orchestrator_response = requests.post(
f"{ORCHESTRATOR_URL}/process_call",
files=files,
timeout=60, # Даем 60 секунд на обработку
)
if orchestrator_response.status_code != 200:
logger.error(f"❌ Ошибка от оркестратора: {orchestrator_response.status_code}")
return _error_response()
# Получаем аудио ответ
_response_audio = orchestrator_response.content
response_text = orchestrator_response.headers.get("X-Response-Text", "")
logger.info(f"✅ Ответ от секретаря: {response_text}")
# Сохраняем аудио ответ и возвращаем URL для Twilio
# В продакшене нужно загрузить в облако (S3, etc)
# Для упрощения возвращаем текстовый ответ через say()
twiml_response = VoiceResponse()
twiml_response.say(response_text, language="ru-RU", voice="alice")
# Спрашиваем, нужно ли что-то еще
twiml_response.say(
"Могу ли я еще чем-то помочь? Нажмите 1 чтобы продолжить или повесьте трубку.",
language="ru-RU",
voice="alice",
)
gather = Gather(num_digits=1, action="/continue_or_end", method="POST", timeout=5)
twiml_response.append(gather)
# Если не нажали - завершаем
twiml_response.say("Спасибо за звонок. До свидания!", language="ru-RU", voice="alice")
twiml_response.hangup()
return Response(content=str(twiml_response), media_type="application/xml")
except Exception as e:
logger.error(f"❌ Ошибка обработки речи: {e}")
return _error_response()
@app.post("/continue_or_end")
async def continue_or_end(request: Request):
"""Продолжить диалог или завершить"""
form_data = await request.form()
digits = form_data.get("Digits", "")
response = VoiceResponse()
if digits == "1":
# Продолжаем диалог
response.say("Пожалуйста, говорите после сигнала.", language="ru-RU", voice="alice")
response.record(
action="/handle_speech", method="POST", max_length=30, play_beep=True, transcribe=False
)
else:
# Завершаем
response.say("Спасибо за звонок. До свидания!", language="ru-RU", voice="alice")
response.hangup()
return Response(content=str(response), media_type="application/xml")
@app.post("/recording_status")
async def recording_status(request: Request):
"""Callback для статуса записи"""
form_data = await request.form()
recording_status = form_data.get("RecordingStatus", "unknown")
call_sid = form_data.get("CallSid", "Unknown")
logger.info(f"📊 Статус записи для {call_sid}: {recording_status}")
return {"status": "ok"}
def _error_response() -> Response:
"""Ответ при ошибке"""
response = VoiceResponse()
response.say(
"Извините, произошла техническая ошибка. Пожалуйста, перезвоните позже.",
language="ru-RU",
voice="alice",
)
response.hangup()
return Response(content=str(response), media_type="application/xml")
if __name__ == "__main__":
import uvicorn
logger.info("📞 Запуск Phone Service на порту 8001")
uvicorn.run("phone_service:app", host="0.0.0.0", port=8001, reload=False, log_level="info")