giyos1212 commited on
Commit
c8a87c8
·
verified ·
1 Parent(s): 516978e

Update app/api/routes.py

Browse files
Files changed (1) hide show
  1. app/api/routes.py +1125 -1125
app/api/routes.py CHANGED
@@ -1,1125 +1,1125 @@
1
- # app/api/routes.py - TO'LIQ YANGILANGAN (3 RISK TIZIMI)
2
- # QISM 1: Imports, Health Checks, va WebSocket Handler
3
-
4
- import os
5
- import uuid
6
- import json
7
- import asyncio
8
- import logging
9
- import time
10
- from typing import Optional, Dict, List
11
- from fastapi import (
12
- APIRouter,
13
- WebSocket,
14
- WebSocketDisconnect,
15
- HTTPException,
16
- UploadFile,
17
- File,
18
- BackgroundTasks,
19
- Query
20
- )
21
- from fastapi.responses import JSONResponse
22
- import shutil
23
-
24
- # Utils
25
- from app.utils.district_matcher import find_district_fuzzy, get_district_display_name, list_all_districts_text
26
- from app.utils.mahalla_matcher import find_mahalla_fuzzy, get_mahalla_display_name
27
- from app.utils.demo_gps import generate_random_tashkent_gps, get_gps_for_district, add_gps_noise, get_all_districts
28
-
29
- # Services
30
- from app.services.models import (
31
- transcribe_audio_from_bytes,
32
- transcribe_audio,
33
- get_gemini_response,
34
- synthesize_speech,
35
- check_model_status,
36
- detect_language
37
- )
38
- from app.services.geocoding import geocode_address, validate_location_in_tashkent, get_location_summary, extract_district_from_address
39
- from app.services.brigade_matcher import find_nearest_brigade, haversine_distance
40
- from app.services.location_validator import get_mahallas_by_district, format_mahallas_list, get_mahalla_coordinates
41
-
42
- # Core
43
- from app.core.database import db
44
- from app.core.config import GPS_VERIFICATION_MAX_DISTANCE_KM, USE_DEMO_GPS, GPS_NOISE_KM, MAX_UNCERTAINTY_ATTEMPTS
45
- from app.core.connections import active_connections
46
-
47
- # API
48
- from app.api.dispatcher_routes import notify_dispatchers
49
-
50
- # Schemas
51
- from app.models.schemas import (
52
- CaseResponse, CaseUpdate, MessageResponse,
53
- SuccessResponse, ErrorResponse,
54
- BrigadeLocation, PatientHistoryResponse,
55
- ClinicResponse, ClinicRecommendation
56
- )
57
-
58
-
59
- audio_buffers: Dict[str, list] = {}
60
-
61
-
62
- # Logging
63
- logging.basicConfig(level=logging.INFO)
64
- logger = logging.getLogger(__name__)
65
-
66
- router = APIRouter()
67
-
68
- # Global variables
69
- tasks = {}
70
- stats = {
71
- "total_messages": 0,
72
- "voice_messages": 0,
73
- "text_messages": 0,
74
- "active_connections": 0,
75
- "start_time": time.time()
76
- }
77
-
78
-
79
- # ==================== HEALTH & STATS ====================
80
-
81
- @router.get("/api/health")
82
- async def health_check():
83
- """Server va model holatini tekshirish"""
84
- model_status = check_model_status()
85
- uptime = time.time() - stats["start_time"]
86
-
87
- return JSONResponse({
88
- "status": "healthy",
89
- "uptime_seconds": int(uptime),
90
- "models": model_status,
91
- "stats": {
92
- **stats,
93
- "active_connections": len(active_connections)
94
- },
95
- "timestamp": time.time()
96
- })
97
-
98
-
99
- @router.get("/api/stats")
100
- async def get_stats():
101
- """Server statistikasi"""
102
- return JSONResponse({
103
- **stats,
104
- "active_connections": len(active_connections),
105
- "uptime_seconds": int(time.time() - stats["start_time"])
106
- })
107
-
108
-
109
- # app/api/routes.py - TUZATILGAN QISM (WebSocket Handler)
110
- # Faqat muammoli qismni tuzatamiz
111
- @router.websocket("/ws/chat")
112
- async def websocket_endpoint(websocket: WebSocket):
113
- """
114
- Bemor uchun WebSocket ulanish
115
-
116
- Frontend: /ws/chat ga ulanadi
117
- Backend: Session ID oladi, case yaratadi
118
- """
119
- await websocket.accept()
120
- active_connections.add(websocket)
121
-
122
- client_info = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
123
- logger.info(f"🔌 WebSocket ulanish o'rnatildi: {client_info}")
124
-
125
- case_id = None
126
-
127
- try:
128
- while True:
129
- # ========== XABAR QABUL QILISH ==========
130
- try:
131
- data = await websocket.receive()
132
- except RuntimeError as e:
133
- if "disconnect" in str(e).lower():
134
- logger.info(f"📴 WebSocket disconnect signal olindi: {client_info}")
135
- break
136
- raise
137
-
138
- # Disconnect message tekshirish
139
- if data.get("type") == "websocket.disconnect":
140
- logger.info(f"📴 WebSocket disconnect message: {client_info}")
141
- break
142
-
143
- # ========== TEXT MESSAGE (JSON) ==========
144
- if "text" in data:
145
- message_text = data["text"]
146
-
147
- # "__END__" string belgisi (audio oxiri)
148
- if message_text == "__END__":
149
- if not case_id:
150
- new_case = db.create_case(client_info)
151
- case_id = new_case['id']
152
- logger.info(f"✅ Yangi case yaratildi: {case_id}")
153
-
154
- if case_id not in audio_buffers or not audio_buffers[case_id]:
155
- logger.warning(f"⚠️ {case_id} uchun audio buffer bo'sh")
156
- continue
157
-
158
- logger.info(f"🎤 Audio oxiri belgisi (string) qabul qilindi")
159
-
160
- full_audio = b"".join(audio_buffers[case_id])
161
- audio_buffers[case_id] = []
162
-
163
- logger.info(f"📦 To'liq audio hajmi: {len(full_audio)} bytes")
164
-
165
- try:
166
- transcribed_text = transcribe_audio_from_bytes(full_audio)
167
- logger.info(f"✅ Transkripsiya: '{transcribed_text}'")
168
-
169
- if transcribed_text and len(transcribed_text.strip()) > 0:
170
- stats["voice_messages"] += 1
171
- db.create_message(case_id, "user", transcribed_text)
172
-
173
- await websocket.send_json({
174
- "type": "transcription_result",
175
- "text": transcribed_text
176
- })
177
-
178
- await process_text_input(websocket, case_id, transcribed_text, is_voice=True)
179
-
180
- except Exception as e:
181
- logger.error(f"❌ Transkripsiya xatoligi: {e}", exc_info=True)
182
- await websocket.send_json({
183
- "type": "error",
184
- "message": "Ovozni tanishda xatolik"
185
- })
186
-
187
- continue
188
-
189
- # ========== JSON MESSAGE ==========
190
- try:
191
- message = json.loads(message_text)
192
- message_type = message.get("type")
193
-
194
- # ========== TEXT INPUT ==========
195
- if message_type == "text_input":
196
- if not case_id:
197
- new_case = db.create_case(client_info)
198
- case_id = new_case['id']
199
- logger.info(f"✅ Yangi case yaratildi (text): {case_id}")
200
-
201
- text = message.get("text", "").strip()
202
-
203
- if text:
204
- db.create_message(case_id, "user", text)
205
- stats["text_messages"] += 1
206
-
207
- await process_text_input(websocket, case_id, text, is_voice=False)
208
-
209
- # ========== PATIENT NAME ==========
210
- elif message_type == "patient_name":
211
- if not case_id:
212
- logger.warning("⚠️ Case ID yo'q, ism qabul qilinmaydi")
213
- continue
214
-
215
- full_name = message.get("full_name", "").strip()
216
-
217
- if full_name:
218
- await process_name_input(websocket, case_id, full_name)
219
-
220
- # ========== GPS LOCATION ==========
221
- elif message_type == "gps_location":
222
- if not case_id:
223
- logger.warning("⚠️ Case ID yo'q, GPS qabul qilinmaydi")
224
- continue
225
-
226
- lat = message.get("latitude")
227
- lon = message.get("longitude")
228
-
229
- if lat and lon:
230
- await process_gps_and_brigade(websocket, case_id, lat, lon)
231
-
232
- except json.JSONDecodeError:
233
- logger.error(f"❌ JSON parse xatoligi: {message_text}")
234
-
235
- # ========== BINARY DATA (AUDIO CHUNKS) ==========
236
- elif "bytes" in data:
237
- if not case_id:
238
- new_case = db.create_case(client_info)
239
- case_id = new_case['id']
240
- logger.info(f"✅ Yangi case yaratildi (audio): {case_id}")
241
-
242
- audio_chunk = data["bytes"]
243
-
244
- if audio_chunk == b"__END__":
245
- logger.info("🎤 Audio oxiri belgisi (bytes) qabul qilindi")
246
- continue
247
-
248
- if case_id not in audio_buffers:
249
- audio_buffers[case_id] = []
250
-
251
- audio_buffers[case_id].append(audio_chunk)
252
- logger.debug(f"📝 Audio chunk qo'shildi ({len(audio_chunk)} bytes). Jami: {len(audio_buffers[case_id])} chunks")
253
-
254
- except WebSocketDisconnect:
255
- logger.info(f"📴 WebSocket disconnect exception: {client_info}")
256
-
257
- except Exception as e:
258
- logger.error(f"❌ WebSocket xatolik: {e}", exc_info=True)
259
-
260
- finally:
261
- # Cleanup (har qanday holatda ham ishga tushadi)
262
- active_connections.discard(websocket)
263
-
264
- if case_id and case_id in audio_buffers:
265
- del audio_buffers[case_id]
266
-
267
- logger.info(f"🧹 WebSocket cleanup tugadi: {client_info}")
268
-
269
-
270
- # ==================== MESSAGE HANDLERS ====================
271
-
272
- async def handle_voice_message(websocket: WebSocket, case_id: str, data: Dict):
273
- """
274
- Ovozli xabar qayta ishlash
275
-
276
- Flow:
277
- 1. Audio → Text (STT)
278
- 2. Text → AI tahlil (Gemini)
279
- 3. Risk darajasini aniqlash
280
- 4. Mos flow ni boshlash (qizil/sariq/yashil)
281
- """
282
- try:
283
- audio_data = data.get("audio")
284
- if not audio_data:
285
- await websocket.send_json({
286
- "type": "error",
287
- "message": "Audio ma'lumot topilmadi"
288
- })
289
- return
290
-
291
- # Audio bytes olish
292
- import base64
293
- audio_bytes = base64.b64decode(audio_data.split(',')[1] if ',' in audio_data else audio_data)
294
-
295
- logger.info(f"🎤 Ovoz yozuvi qabul qilindi: {len(audio_bytes)} bytes")
296
-
297
- # STT
298
- await websocket.send_json({
299
- "type": "status",
300
- "message": "Ovozingizni tinglab turaman..."
301
- })
302
-
303
- user_transcript = transcribe_audio_from_bytes(audio_bytes)
304
-
305
- if not user_transcript or len(user_transcript.strip()) < 3:
306
- await websocket.send_json({
307
- "type": "error",
308
- "message": "Ovozni tushunolmadim. Iltimos, qaytadan aytib bering."
309
- })
310
- return
311
-
312
- logger.info(f"📝 Transkripsiya: '{user_transcript}'")
313
-
314
- # Database ga saqlash
315
- db.create_message(case_id, "user", user_transcript)
316
- stats["voice_messages"] += 1
317
-
318
- # Text bilan davom etish
319
- await process_text_input(websocket, case_id, user_transcript, is_voice=True)
320
-
321
- except Exception as e:
322
- logger.error(f"❌ Ovozli xabar xatoligi: {e}", exc_info=True)
323
- await websocket.send_json({
324
- "type": "error",
325
- "message": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring."
326
- })
327
-
328
- async def handle_text_message(websocket: WebSocket, case_id: str, data: Dict):
329
- """Matnli xabar qayta ishlash"""
330
- try:
331
- text = data.get("text", "").strip()
332
-
333
- if not text or len(text) < 2:
334
- await websocket.send_json({
335
- "type": "error",
336
- "message": "Xabar bo'sh. Iltimos, biror narsa yozing."
337
- })
338
- return
339
-
340
- logger.info(f"💬 Matnli xabar: '{text}'")
341
-
342
- db.create_message(case_id, "user", text)
343
- stats["text_messages"] += 1
344
-
345
- await process_text_input(websocket, case_id, text, is_voice=False)
346
-
347
- except Exception as e:
348
- logger.error(f"❌ Matnli xabar xatoligi: {e}", exc_info=True)
349
- await websocket.send_json({
350
- "type": "error",
351
- "message": "Xatolik yuz berdi."
352
- })
353
-
354
-
355
- async def handle_gps_location(websocket: WebSocket, case_id: str, data: Dict):
356
- """GPS lokatsiya qayta ishlash"""
357
- try:
358
- lat = data.get("latitude")
359
- lon = data.get("longitude")
360
-
361
- if not lat or not lon:
362
- await websocket.send_json({
363
- "type": "error",
364
- "message": "GPS ma'lumot topilmadi"
365
- })
366
- return
367
-
368
- logger.info(f"📍 GPS qabul qilindi: ({lat}, {lon})")
369
-
370
- # GPS ni saqlash va brigada topish
371
- await process_gps_and_brigade(websocket, case_id, lat, lon)
372
-
373
- except Exception as e:
374
- logger.error(f"❌ GPS xatoligi: {e}", exc_info=True)
375
- await websocket.send_json({
376
- "type": "error",
377
- "message": "GPS xatolik"
378
- })
379
-
380
-
381
- # ==================== TEXT PROCESSING (ASOSIY MANTIQ) ====================
382
-
383
- async def process_text_input(websocket: WebSocket, case_id: str, prompt: str, is_voice: bool = False):
384
- """
385
- Matn kiritishni qayta ishlash - ASOSIY FLOW
386
-
387
- Args:
388
- websocket: WebSocket ulanish
389
- case_id: Case ID (string)
390
- prompt: Bemorning matni
391
- is_voice: Ovozli xabarmi? (True/False)
392
- """
393
- try:
394
- # Case ni olish
395
- current_case = db.get_case(case_id)
396
-
397
- if not current_case:
398
- logger.error(f"❌ Case topilmadi: {case_id}")
399
- await websocket.send_json({
400
- "type": "error",
401
- "message": "Sessiya xatoligi. Iltimos, sahifani yangilang."
402
- })
403
- return
404
-
405
- # ========== 1. ISM-FAMILIYA KUTILMOQDA? ==========
406
- if current_case.get('waiting_for_name_input'):
407
- await process_name_input(websocket, case_id, prompt)
408
- return
409
-
410
- # ========== 2. MANZIL ANIQLASHTIRILMOQDA? ==========
411
- if await handle_location_clarification(websocket, case_id, prompt, "voice" if is_voice else "text"):
412
- return
413
-
414
- # ========== 3. YANGI TAHLIL (GEMINI) ==========
415
- conversation_history = db.get_conversation_history(case_id)
416
- detected_lang = detect_language(prompt)
417
-
418
- logger.info(f"🧠 Gemini tahlil boshlandi...")
419
-
420
- full_prompt = f"{conversation_history}\nBemor: {prompt}"
421
- ai_analysis = get_gemini_response(full_prompt, stream=False)
422
-
423
- # JSON parse qilish
424
- if not ai_analysis or not isinstance(ai_analysis, dict):
425
- logger.error(f"❌ Gemini noto'g'ri javob: {ai_analysis}")
426
- await websocket.send_json({
427
- "type": "error",
428
- "message": "AI xatolik"
429
- })
430
- return
431
-
432
- risk_level = ai_analysis.get("risk_level", "yashil")
433
- response_text = ai_analysis.get("response_text", "Tushunmadim")
434
- language = ai_analysis.get("language", detected_lang)
435
-
436
- logger.info(f"📊 Risk darajasi: {risk_level.upper()}")
437
-
438
- # Database ga saqlash
439
- db.create_message(case_id, "ai", response_text)
440
- db.update_case(case_id, {
441
- "risk_level": risk_level,
442
- "language": language,
443
- "symptoms_text": ai_analysis.get("symptoms_extracted")
444
- })
445
-
446
- # ========== RISK DARAJASIGA QARAB HARAKAT ==========
447
-
448
- if risk_level == "qizil":
449
- await handle_qizil_flow(websocket, case_id, ai_analysis)
450
- elif risk_level == "sariq":
451
- await handle_sariq_flow(websocket, case_id, ai_analysis)
452
- elif risk_level == "yashil":
453
- await handle_yashil_flow(websocket, case_id, ai_analysis)
454
- else:
455
- logger.warning(f"⚠️ Noma'lum risk level: {risk_level}")
456
- await send_ai_response(websocket, case_id, response_text, language)
457
-
458
- except Exception as e:
459
- logger.error(f"❌ process_text_input xatoligi: {e}", exc_info=True)
460
- await websocket.send_json({
461
- "type": "error",
462
- "message": "Xatolik yuz berdi"
463
- })
464
-
465
-
466
- # ==================== HELPER FUNCTION ====================
467
-
468
- async def send_ai_response(websocket: WebSocket, case_id: str, text: str, language: str = "uzb"):
469
- """
470
- AI javobini frontendga yuborish (text + audio)
471
-
472
- TUZATILGAN: TTS output_path to'g'ri yaratiladi
473
-
474
- Args:
475
- websocket: WebSocket ulanish
476
- case_id: Case ID
477
- text: Javob matni
478
- language: Javob tili ("uzb" | "eng" | "rus")
479
- """
480
- try:
481
- # Database ga AI xabarini saqlash
482
- db.create_message(case_id, "ai", text)
483
-
484
- # 1. Text yuborish
485
- await websocket.send_json({
486
- "type": "ai_response",
487
- "text": text
488
- })
489
-
490
- # 2. TTS audio yaratish
491
- # ✅ TO'G'RI: output_path yaratish
492
- audio_filename = f"tts_{case_id}_{int(time.time())}.wav"
493
- audio_path = os.path.join("static/audio", audio_filename)
494
-
495
- logger.info(f"🎧 TTS uchun fayl yo'li: {audio_path}")
496
-
497
- # TTS chaqirish (to'g'ri parametrlar bilan)
498
- tts_success = synthesize_speech(text, audio_path, language)
499
-
500
- if tts_success and os.path.exists(audio_path):
501
- audio_url = f"/audio/{audio_filename}"
502
- await websocket.send_json({
503
- "type": "audio_response",
504
- "audio_url": audio_url
505
- })
506
- logger.info(f"📊 TTS audio yuborildi: {audio_url}")
507
- else:
508
- logger.warning("⚠️ TTS yaratilmadi, faqat text yuborildi")
509
-
510
- except Exception as e:
511
- logger.error(f"❌ send_ai_response xatoligi: {e}", exc_info=True)
512
-
513
- # app/api/routes.py - QISM 2
514
- # 3 TA ASOSIY FLOW: QIZIL, SARIQ, YASHIL
515
-
516
- # ==================== 🔴 QIZIL FLOW (EMERGENCY) ====================
517
-
518
- async def handle_qizil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
519
- """
520
- QIZIL (Emergency) - TEZ YORDAM BRIGADA
521
-
522
- Flow:
523
- 1. Manzil so'rash (tuman + mahalla)
524
- 2. Fuzzy matching orqali koordinata topish
525
- 3. Brigada topish va jo'natish
526
- 4. ISM-FAMILIYA so'rash (brigadadan KEYIN!)
527
- """
528
- try:
529
- logger.info(f"🔴 QIZIL HOLAT: Tez yordam jarayoni boshlandi")
530
-
531
- response_text = ai_analysis.get("response_text")
532
- language = ai_analysis.get("language", "uzb")
533
- address = ai_analysis.get("address_extracted")
534
- district = ai_analysis.get("district_extracted")
535
-
536
- # Case type ni belgilash
537
- db.update_case(case_id, {
538
- "type": "emergency",
539
- "risk_level": "qizil"
540
- })
541
-
542
- # 1. MANZIL SO'RASH
543
- if not address or not district:
544
- logger.info("📍 Manzil yo'q, so'ralmoqda...")
545
- await send_ai_response(websocket, case_id, response_text, language)
546
-
547
- # Flag qo'yish - keyingi xabarda manzil kutiladi
548
- db.update_case(case_id, {"waiting_for_address": True})
549
- return
550
-
551
- # 2. MANZILNI QAYTA ISHLASH
552
- logger.info(f"📍 Manzil aniqlandi: {address}")
553
-
554
- # Tuman fuzzy match
555
- district_match = find_district_fuzzy(district)
556
-
557
- if not district_match:
558
- logger.warning(f"⚠️ Tuman topilmadi: {district}")
559
- districts_list = get_all_districts()
560
-
561
- response = f"Tuman nomini aniq tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{districts_list}"
562
- await send_ai_response(websocket, case_id, response, language)
563
- return
564
-
565
- district_name = get_district_display_name(district_match)
566
- logger.info(f"✅ Tuman topildi: {district_name}")
567
-
568
- db.update_case(case_id, {
569
- "district": district_name,
570
- "selected_district": district_match
571
- })
572
-
573
- # 3. MAHALLA SO'RASH
574
- # Bu qism location_clarification da amalga oshiriladi
575
- # Hozircha flag qo'yamiz
576
- db.update_case(case_id, {
577
- "waiting_for_mahalla_input": True,
578
- "mahalla_retry_count": 0
579
- })
580
-
581
- response = f"Tushundim, {district_name}. Iltimos, mahallangizni ayting."
582
- await send_ai_response(websocket, case_id, response, language)
583
-
584
- # Dispetcherga bildirishnoma
585
- await notify_dispatchers({
586
- "type": "new_case",
587
- "case": db.get_case(case_id)
588
- })
589
-
590
- except Exception as e:
591
- logger.error(f"❌ handle_qizil_flow xatoligi: {e}", exc_info=True)
592
-
593
-
594
- async def process_gps_and_brigade(websocket: WebSocket, case_id: str, lat: float, lon: float):
595
- """
596
- GPS koordinatalariga qarab brigadani topish
597
-
598
- MUHIM: Brigadadan KEYIN ism-familiya so'raladi!
599
- """
600
- try:
601
- logger.info(f"📍 GPS koordinatalar: ({lat:.6f}, {lon:.6f})")
602
-
603
- # GPS validatsiya
604
- if not validate_location_in_tashkent(lat, lon):
605
- logger.warning("⚠️ GPS Toshkent chegarasidan tashqarida")
606
- await websocket.send_json({
607
- "type": "error",
608
- "message": "GPS manzil Toshkent chegarasidan tashqarida"
609
- })
610
- return
611
-
612
- # Case ga saqlash
613
- db.update_case(case_id, {
614
- "gps_lat": lat,
615
- "gps_lon": lon,
616
- "geocoded_lat": lat,
617
- "geocoded_lon": lon,
618
- "gps_verified": True
619
- })
620
-
621
- # Brigadani topish
622
- logger.info("🚑 Eng yaqin brigada qidirilmoqda...")
623
-
624
- nearest_brigade = find_nearest_brigade(lat, lon)
625
-
626
- if not nearest_brigade:
627
- logger.warning("⚠️ Brigada topilmadi")
628
- await websocket.send_json({
629
- "type": "error",
630
- "message": "Hozirda barcha brigadalar band"
631
- })
632
- return
633
-
634
- brigade_id = nearest_brigade['brigade_id']
635
- brigade_name = nearest_brigade['brigade_name']
636
- distance_km = nearest_brigade['distance_km']
637
-
638
- # Brigadani tayinlash
639
- db.update_case(case_id, {
640
- "assigned_brigade_id": brigade_id,
641
- "assigned_brigade_name": brigade_name,
642
- "distance_to_brigade_km": distance_km,
643
- "status": "brigada_junatildi"
644
- })
645
-
646
- logger.info(f"✅ Brigada tayinlandi: {brigade_name} ({distance_km:.2f} km)")
647
-
648
- # Bemorga xabar
649
- await websocket.send_json({
650
- "type": "brigade_assigned",
651
- "brigade": {
652
- "id": brigade_id,
653
- "name": brigade_name,
654
- "distance_km": distance_km,
655
- "estimated_time_min": int(distance_km * 3) # 3 min/km
656
- }
657
- })
658
-
659
- # ========== ENDI ISM-FAMILIYA SO'RASH ==========
660
- current_case = db.get_case(case_id)
661
- language = current_case.get("language", "uzb")
662
-
663
- if language == "eng":
664
- name_request = f"The ambulance is on its way, arriving in approximately {int(distance_km * 3)} minutes. Please tell me your full name."
665
- elif language == "rus":
666
- name_request = f"Скорая помощь в пути, прибудет примерно через {int(distance_km * 3)} минут. Пожалуйста, назовите ваше полное имя."
667
- else:
668
- name_request = f"Brigada yo'lda, taxminan {int(distance_km * 3)} daqiqada yetib keladi. Iltimos, to'liq ism-familiyangizni ayting."
669
-
670
- db.create_message(case_id, "ai", name_request)
671
- await send_ai_response(websocket, case_id, name_request, language)
672
-
673
- # Flag qo'yish
674
- db.update_case(case_id, {"waiting_for_name_input": True})
675
-
676
- # Dispetcherga yangilanish
677
- await notify_dispatchers({
678
- "type": "brigade_assigned",
679
- "case": db.get_case(case_id)
680
- })
681
-
682
- except Exception as e:
683
- logger.error(f"❌ process_gps_and_brigade xatoligi: {e}", exc_info=True)
684
-
685
-
686
- # ==================== 🟡 SARIQ FLOW (UNCERTAIN) ====================
687
-
688
- async def handle_sariq_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
689
- """
690
- SARIQ (Uncertain) - NOANIQ, OPERATOR KERAK
691
-
692
- Flow:
693
- 1. Aniqlashtiruvchi savol berish
694
- 2. Counter ni oshirish (max 3)
695
- 3. 3 marta tushunmasa → Operator
696
- """
697
- try:
698
- logger.info(f"🟡 SARIQ HOLAT: Noaniqlik")
699
-
700
- response_text = ai_analysis.get("response_text")
701
- language = ai_analysis.get("language", "uzb")
702
- uncertainty_reason = ai_analysis.get("uncertainty_reason")
703
- operator_needed = ai_analysis.get("operator_needed", False)
704
-
705
- current_case = db.get_case(case_id)
706
- current_attempts = current_case.get("uncertainty_attempts", 0)
707
-
708
- # Case type ni belgilash
709
- db.update_case(case_id, {
710
- "type": "uncertain",
711
- "risk_level": "sariq"
712
- })
713
-
714
- # Operator kerakmi?
715
- if operator_needed or current_attempts >= MAX_UNCERTAINTY_ATTEMPTS:
716
- logger.info(f"🎧 OPERATOR KERAK! (Attempts: {current_attempts})")
717
-
718
- db.update_case(case_id, {
719
- "operator_needed": True,
720
- "uncertainty_reason": uncertainty_reason or f"AI {current_attempts} marta tushunolmadi",
721
- "status": "operator_kutilmoqda",
722
- "uncertainty_attempts": current_attempts + 1
723
- })
724
-
725
- # Bemorga xabar
726
- if language == "eng":
727
- operator_msg = "I'm having trouble understanding you. Connecting you to an operator who can help..."
728
- elif language == "rus":
729
- operator_msg = "Мне сложно вас понять. Соединяю с оператором, который вам поможет..."
730
- else:
731
- operator_msg = "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi..."
732
-
733
- await send_ai_response(websocket, case_id, operator_msg, language)
734
-
735
- # Dispetcherga operator kerakligi haqida xabar
736
- await notify_dispatchers({
737
- "type": "operator_needed",
738
- "case": db.get_case(case_id)
739
- })
740
-
741
- return
742
-
743
- # Hali operator kerak emas, aniqlashtirish
744
- logger.info(f"❓ Aniqlashtirish (Attempt {current_attempts + 1}/{MAX_UNCERTAINTY_ATTEMPTS})")
745
-
746
- db.update_case(case_id, {
747
- "uncertainty_attempts": current_attempts + 1,
748
- "uncertainty_reason": uncertainty_reason
749
- })
750
-
751
- await send_ai_response(websocket, case_id, response_text, language)
752
-
753
- except Exception as e:
754
- logger.error(f"❌ handle_sariq_flow xatoligi: {e}", exc_info=True)
755
-
756
-
757
- # ==================== 🟢 YASHIL FLOW (CLINIC) ====================
758
-
759
- async def handle_yashil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
760
- """
761
- YASHIL (Non-urgent) - KLINIKA TAVSIYA
762
-
763
- Flow:
764
- 1. Bemorga xotirjamlik berish
765
- 2. Davlat yoki xususiy klinika taklif qilish
766
- 3. Bemor tanlasa, klinikalar ro'yxatini yuborish
767
- """
768
- try:
769
- logger.info(f"🟢 YASHIL HOLAT: Klinika tavsiyasi")
770
-
771
- response_text = ai_analysis.get("response_text")
772
- language = ai_analysis.get("language", "uzb")
773
- symptoms = ai_analysis.get("symptoms_extracted")
774
- preferred_clinic_type = ai_analysis.get("preferred_clinic_type", "both")
775
- recommended_specialty = ai_analysis.get("recommended_specialty", "Terapiya")
776
-
777
- # Case type ni belgilash
778
- db.update_case(case_id, {
779
- "type": "public_clinic", # Default, keyin o'zgarishi mumkin
780
- "risk_level": "yashil",
781
- "symptoms_text": symptoms
782
- })
783
-
784
- # 1. AI javobini yuborish (xotirjamlik + taklif)
785
- await send_ai_response(websocket, case_id, response_text, language)
786
-
787
- # 2. Klinikalarni qidirish
788
- logger.info(f"🏥 Klinikalar qidirilmoqda: {recommended_specialty}, type={preferred_clinic_type}")
789
-
790
- # Har ikki turdan ham topish
791
- if preferred_clinic_type == "both":
792
- davlat_clinics = db.recommend_clinics_by_symptoms(
793
- symptoms=symptoms,
794
- district=None,
795
- clinic_type="davlat"
796
- )
797
-
798
- xususiy_clinics = db.recommend_clinics_by_symptoms(
799
- symptoms=symptoms,
800
- district=None,
801
- clinic_type="xususiy"
802
- )
803
-
804
- # Formatlangan ro'yxat yaratish
805
- clinic_list_text = format_clinic_list(
806
- davlat_clinics.get('clinics', [])[:2], # Top 2 davlat
807
- xususiy_clinics.get('clinics', [])[:3], # Top 3 xususiy
808
- language
809
- )
810
-
811
- else:
812
- # Faqat bitta turni ko'rsatish
813
- recommendation = db.recommend_clinics_by_symptoms(
814
- symptoms=symptoms,
815
- district=None,
816
- clinic_type=preferred_clinic_type
817
- )
818
-
819
- clinic_list_text = format_clinic_list(
820
- recommendation.get('clinics', [])[:5] if preferred_clinic_type == "davlat" else [],
821
- recommendation.get('clinics', [])[:5] if preferred_clinic_type == "xususiy" else [],
822
- language
823
- )
824
-
825
- # 3. Klinikalar ro'yxatini yuborish
826
- await websocket.send_json({
827
- "type": "clinic_recommendation",
828
- "text": clinic_list_text
829
- })
830
-
831
- db.create_message(case_id, "ai", clinic_list_text)
832
-
833
- # Dispetcherga xabar
834
- await notify_dispatchers({
835
- "type": "clinic_case",
836
- "case": db.get_case(case_id)
837
- })
838
-
839
- logger.info(f"✅ Klinikalar ro'yxati yuborildi")
840
-
841
- except Exception as e:
842
- logger.error(f"❌ handle_yashil_flow xatoligi: {e}", exc_info=True)
843
-
844
-
845
- def format_clinic_list(davlat_clinics: List[Dict], xususiy_clinics: List[Dict], language: str = "uzb") -> str:
846
- """
847
- Klinikalar ro'yxatini formatlash
848
-
849
- Args:
850
- davlat_clinics: Davlat poliklinikalari
851
- xususiy_clinics: Xususiy klinikalar
852
- language: Til
853
-
854
- Returns:
855
- Formatlangan matn
856
- """
857
- result = []
858
-
859
- # Header
860
- if language == "eng":
861
- result.append("Here are my recommendations:\n")
862
- elif language == "rus":
863
- result.append("Вот мои рекомендации:\n")
864
- else:
865
- result.append("Mana sizga tavsiyalar:\n")
866
-
867
- # Davlat klinikalari
868
- if davlat_clinics:
869
- if language == "eng":
870
- result.append("\n🏥 PUBLIC CLINICS (Free):\n")
871
- elif language == "rus":
872
- result.append("\n🏥 ГОСУДАРСТВЕННЫЕ ПОЛИКЛИНИКИ (Бесплатно):\n")
873
- else:
874
- result.append("\n🏥 DAVLAT POLIKLINIKALARI (Bepul):\n")
875
-
876
- for idx, clinic in enumerate(davlat_clinics, 1):
877
- result.append(f"\n{idx}️⃣ {clinic['name']}")
878
- result.append(f" 📍 {clinic['address']}")
879
- result.append(f" 📞 {clinic['phone']}")
880
- result.append(f" ⏰ {clinic['working_hours']}")
881
- result.append(f" ⭐ {clinic['rating']}/5.0")
882
-
883
- # Xususiy klinikalar
884
- if xususiy_clinics:
885
- if language == "eng":
886
- result.append("\n\n🏥 PRIVATE CLINICS:\n")
887
- elif language == "rus":
888
- result.append("\n\n🏥 ЧАСТНЫЕ КЛИНИКИ:\n")
889
- else:
890
- result.append("\n\n🏥 XUSUSIY KLINIKALAR:\n")
891
-
892
- for idx, clinic in enumerate(xususiy_clinics, 1):
893
- result.append(f"\n{idx}️⃣ {clinic['name']}")
894
- result.append(f" 📍 {clinic['address']}")
895
- result.append(f" 📞 {clinic['phone']}")
896
- result.append(f" ⏰ {clinic['working_hours']}")
897
- result.append(f" 💰 {clinic['price_range']}")
898
- result.append(f" ⭐ {clinic['rating']}/5.0")
899
-
900
- return "\n".join(result)
901
-
902
-
903
- # ==================== HELPER FUNCTIONS ====================
904
-
905
- async def process_name_input(websocket: WebSocket, case_id: str, name_text: str):
906
- """
907
- Ism-familiyani qayta ishlash
908
-
909
- Bu funksiya brigadadan KEYIN chaqiriladi
910
- """
911
- try:
912
- logger.info(f"👤 Ism-familiya qabul qilindi: '{name_text}'")
913
-
914
- current_case = db.get_case(case_id)
915
- language = current_case.get("language", "uzb")
916
-
917
- # Ism-familiyani saqlash
918
- db.update_case(case_id, {
919
- "patient_full_name": name_text,
920
- "waiting_for_name_input": False
921
- })
922
-
923
- # Bemor tarixini tekshirish
924
- patient_history = db.get_patient_statistics(name_text)
925
-
926
- if patient_history and patient_history.get("total_cases", 0) > 0:
927
- previous_count = patient_history.get("total_cases")
928
- logger.info(f"📋 Bemor tarixi topildi: {previous_count} ta oldingi murojat")
929
-
930
- db.update_case(case_id, {
931
- "previous_cases_count": previous_count
932
- })
933
-
934
- # Tasdiq xabari
935
- if language == "eng":
936
- confirmation = f"Thank you, {name_text}. The ambulance will arrive shortly. Please stay calm."
937
- elif language == "rus":
938
- confirmation = f"Спасибо, {name_text}. Скорая помощь скоро прибудет. Пожалуйста, сохраняйте спокойствие."
939
- else:
940
- confirmation = f"Rahmat, {name_text}. Brigada tez orada yetib keladi. Iltimos, xotirjam bo'ling."
941
-
942
- await send_ai_response(websocket, case_id, confirmation, language)
943
-
944
- # Dispetcherga yangilanish
945
- await notify_dispatchers({
946
- "type": "name_received",
947
- "case": db.get_case(case_id)
948
- })
949
-
950
- except Exception as e:
951
- logger.error(f"❌ process_name_input xatoligi: {e}", exc_info=True)
952
-
953
-
954
- async def handle_location_clarification(websocket: WebSocket, case_id: str, user_input: str, input_type: str) -> bool:
955
- """
956
- Manzilni aniqlashtirish (mahalla)
957
-
958
- Returns:
959
- True - agar mahalla kutilgan bo'lsa va qayta ishlandi
960
- False - agar mahalla kutilmagan
961
- """
962
- try:
963
- current_case = db.get_case(case_id)
964
-
965
- if not current_case.get("waiting_for_mahalla_input"):
966
- return False
967
-
968
- logger.info(f"🏘️ Mahalla aniqlashtirilmoqda: '{user_input}'")
969
-
970
- district_id = current_case.get("selected_district")
971
- district_name = current_case.get("district")
972
- language = current_case.get("language", "uzb")
973
-
974
- if not district_id:
975
- logger.error("❌ District ID topilmadi")
976
- return False
977
-
978
- # Mahalla fuzzy match
979
- mahalla_match = find_mahalla_fuzzy(district_name, user_input, threshold=0.35)
980
-
981
- if mahalla_match:
982
- mahalla_full_name = get_mahalla_display_name(mahalla_match)
983
- logger.info(f"✅ Mahalla topildi: {mahalla_full_name}")
984
-
985
- # Mahalla koordinatalarini olish
986
- mahalla_coords = get_mahalla_coordinates(district_name, mahalla_match)
987
-
988
- if mahalla_coords:
989
- db.update_case(case_id, {
990
- "selected_mahalla": mahalla_full_name,
991
- "mahalla_lat": mahalla_coords['lat'],
992
- "mahalla_lon": mahalla_coords['lon'],
993
- "geocoded_lat": mahalla_coords['lat'],
994
- "geocoded_lon": mahalla_coords['lon'],
995
- "waiting_for_mahalla_input": False,
996
- "mahalla_retry_count": 0
997
- })
998
-
999
- # Brigadani topish
1000
- await process_gps_and_brigade(
1001
- websocket,
1002
- case_id,
1003
- mahalla_coords['lat'],
1004
- mahalla_coords['lon']
1005
- )
1006
-
1007
- return True
1008
-
1009
- # Mahalla topilmadi
1010
- retry_count = current_case.get("mahalla_retry_count", 0) + 1
1011
-
1012
- if retry_count >= 3:
1013
- # 3 marta topilmasa, faqat tuman bilan davom etamiz
1014
- logger.warning("⚠️ Mahalla 3 marta topilmadi, tuman markazidan foydalaniladi")
1015
-
1016
- district_gps = get_gps_for_district(district_id)
1017
-
1018
- if district_gps:
1019
- db.update_case(case_id, {
1020
- "geocoded_lat": district_gps['lat'],
1021
- "geocoded_lon": district_gps['lon'],
1022
- "waiting_for_mahalla_input": False,
1023
- "mahalla_retry_count": 0
1024
- })
1025
-
1026
- await process_gps_and_brigade(
1027
- websocket,
1028
- case_id,
1029
- district_gps['lat'],
1030
- district_gps['lon']
1031
- )
1032
-
1033
- return True
1034
-
1035
- # Qayta so'rash
1036
- db.update_case(case_id, {"mahalla_retry_count": retry_count})
1037
-
1038
- mahallas_list = format_mahallas_list(get_mahallas_by_district(district_name))
1039
-
1040
- response = f"Mahalla nomini tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{mahallas_list}"
1041
- await send_ai_response(websocket, case_id, response, language)
1042
-
1043
- return True
1044
-
1045
- except Exception as e:
1046
- logger.error(f"❌ handle_location_clarification xatoligi: {e}", exc_info=True)
1047
- return False
1048
-
1049
-
1050
- # ==================== PERIODIC CLEANUP ====================
1051
-
1052
- async def periodic_cleanup():
1053
- """Eski audio fayllarni tozalash (har 1 soatda)"""
1054
- while True:
1055
- try:
1056
- await asyncio.sleep(3600) # 1 soat
1057
- logger.info("🧹 Periodic cleanup boshlandi...")
1058
-
1059
- audio_dir = "static/audio"
1060
- if os.path.exists(audio_dir):
1061
- current_time = time.time()
1062
- for filename in os.listdir(audio_dir):
1063
- file_path = os.path.join(audio_dir, filename)
1064
- if os.path.isfile(file_path):
1065
- if current_time - os.path.getmtime(file_path) > 3600: # 1 soat
1066
- os.remove(file_path)
1067
- logger.info(f"🗑️ Eski fayl o'chirildi: {filename}")
1068
- except Exception as e:
1069
- logger.error(f"❌ Periodic cleanup xatoligi: {e}")
1070
-
1071
-
1072
- @router.on_event("startup")
1073
- async def startup_event():
1074
- """Server ishga tushganda"""
1075
- asyncio.create_task(periodic_cleanup())
1076
- logger.info("🚀 Periodic cleanup task ishga tushdi")
1077
-
1078
-
1079
- # ==================== CASE MANAGEMENT APIs ====================
1080
-
1081
- @router.get("/cases")
1082
- async def get_all_cases(status: Optional[str] = None):
1083
- """Barcha caselarni olish"""
1084
- try:
1085
- cases = db.get_all_cases(status=status)
1086
- return cases
1087
- except Exception as e:
1088
- logger.error(f"❌ Cases olishda xatolik: {e}")
1089
- raise HTTPException(status_code=500, detail="Server xatoligi")
1090
-
1091
-
1092
- @router.get("/cases/{case_id}")
1093
- async def get_case(case_id: str):
1094
- """Bitta case ma'lumotlarini olish"""
1095
- case = db.get_case(case_id)
1096
-
1097
- if not case:
1098
- raise HTTPException(status_code=404, detail="Case topilmadi")
1099
-
1100
- return case
1101
-
1102
-
1103
- @router.patch("/cases/{case_id}")
1104
- async def update_case(case_id: str, updates: CaseUpdate):
1105
- """Case ni yangilash"""
1106
- update_data = updates.dict(exclude_unset=True)
1107
-
1108
- success = db.update_case(case_id, update_data)
1109
-
1110
- if not success:
1111
- raise HTTPException(status_code=404, detail="Case topilmadi")
1112
-
1113
- updated_case = db.get_case(case_id)
1114
-
1115
- # Dispetcherlarga yangilanish
1116
- await notify_dispatchers({
1117
- "type": "case_updated",
1118
- "case": updated_case
1119
- })
1120
-
1121
- return updated_case
1122
-
1123
-
1124
-
1125
-
 
1
+ # app/api/routes.py - TO'LIQ YANGILANGAN (3 RISK TIZIMI)
2
+ # QISM 1: Imports, Health Checks, va WebSocket Handler
3
+
4
+ import os
5
+ import uuid
6
+ import json
7
+ import asyncio
8
+ import logging
9
+ import time
10
+ from typing import Optional, Dict, List
11
+ from fastapi import (
12
+ APIRouter,
13
+ WebSocket,
14
+ WebSocketDisconnect,
15
+ HTTPException,
16
+ UploadFile,
17
+ File,
18
+ BackgroundTasks,
19
+ Query
20
+ )
21
+ from fastapi.responses import JSONResponse
22
+ import shutil
23
+
24
+ # Utils
25
+ from app.utils.district_matcher import find_district_fuzzy, get_district_display_name, list_all_districts_text
26
+ from app.utils.mahalla_matcher import find_mahalla_fuzzy, get_mahalla_display_name
27
+ from app.utils.demo_gps import generate_random_tashkent_gps, get_gps_for_district, add_gps_noise, get_all_districts
28
+
29
+ # Services
30
+ from app.services.models import (
31
+ transcribe_audio_from_bytes,
32
+ transcribe_audio,
33
+ get_gemini_response,
34
+ synthesize_speech,
35
+ check_model_status,
36
+ detect_language
37
+ )
38
+ from app.services.geocoding import geocode_address, validate_location_in_tashkent, get_location_summary, extract_district_from_address
39
+ from app.services.brigade_matcher import find_nearest_brigade, haversine_distance
40
+ from app.services.location_validator import get_mahallas_by_district, format_mahallas_list, get_mahalla_coordinates
41
+
42
+ # Core
43
+ from app.core.database import db
44
+ from app.core.config import GPS_VERIFICATION_MAX_DISTANCE_KM, USE_DEMO_GPS, GPS_NOISE_KM, MAX_UNCERTAINTY_ATTEMPTS
45
+ from app.core.connections import active_connections
46
+
47
+ # API
48
+ from app.api.dispatcher_routes import notify_dispatchers
49
+
50
+ # Schemas
51
+ from app.models.schemas import (
52
+ CaseResponse, CaseUpdate, MessageResponse,
53
+ SuccessResponse, ErrorResponse,
54
+ BrigadeLocation, PatientHistoryResponse,
55
+ ClinicResponse, ClinicRecommendation
56
+ )
57
+
58
+
59
+ audio_buffers: Dict[str, list] = {}
60
+
61
+
62
+ # Logging
63
+ logging.basicConfig(level=logging.INFO)
64
+ logger = logging.getLogger(__name__)
65
+
66
+ router = APIRouter()
67
+
68
+ # Global variables
69
+ tasks = {}
70
+ stats = {
71
+ "total_messages": 0,
72
+ "voice_messages": 0,
73
+ "text_messages": 0,
74
+ "active_connections": 0,
75
+ "start_time": time.time()
76
+ }
77
+
78
+
79
+ # ==================== HEALTH & STATS ====================
80
+
81
+ @router.get("/api/health")
82
+ async def health_check():
83
+ """Server va model holatini tekshirish"""
84
+ model_status = check_model_status()
85
+ uptime = time.time() - stats["start_time"]
86
+
87
+ return JSONResponse({
88
+ "status": "healthy",
89
+ "uptime_seconds": int(uptime),
90
+ "models": model_status,
91
+ "stats": {
92
+ **stats,
93
+ "active_connections": len(active_connections)
94
+ },
95
+ "timestamp": time.time()
96
+ })
97
+
98
+
99
+ @router.get("/api/stats")
100
+ async def get_stats():
101
+ """Server statistikasi"""
102
+ return JSONResponse({
103
+ **stats,
104
+ "active_connections": len(active_connections),
105
+ "uptime_seconds": int(time.time() - stats["start_time"])
106
+ })
107
+
108
+
109
+ # app/api/routes.py - TUZATILGAN QISM (WebSocket Handler)
110
+ # Faqat muammoli qismni tuzatamiz
111
+ @router.websocket("/ws/chat")
112
+ async def websocket_endpoint(websocket: WebSocket):
113
+ """
114
+ Bemor uchun WebSocket ulanish
115
+
116
+ Frontend: /ws/chat ga ulanadi
117
+ Backend: Session ID oladi, case yaratadi
118
+ """
119
+ await websocket.accept()
120
+ active_connections.add(websocket)
121
+
122
+ client_info = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
123
+ logger.info(f"🔌 WebSocket ulanish o'rnatildi: {client_info}")
124
+
125
+ case_id = None
126
+
127
+ try:
128
+ while True:
129
+ # ========== XABAR QABUL QILISH ==========
130
+ try:
131
+ data = await websocket.receive()
132
+ except RuntimeError as e:
133
+ if "disconnect" in str(e).lower():
134
+ logger.info(f"📴 WebSocket disconnect signal olindi: {client_info}")
135
+ break
136
+ raise
137
+
138
+ # Disconnect message tekshirish
139
+ if data.get("type") == "websocket.disconnect":
140
+ logger.info(f"📴 WebSocket disconnect message: {client_info}")
141
+ break
142
+
143
+ # ========== TEXT MESSAGE (JSON) ==========
144
+ if "text" in data:
145
+ message_text = data["text"]
146
+
147
+ # "__END__" string belgisi (audio oxiri)
148
+ if message_text == "__END__":
149
+ if not case_id:
150
+ new_case = db.create_case(client_info)
151
+ case_id = new_case['id']
152
+ logger.info(f"✅ Yangi case yaratildi: {case_id}")
153
+
154
+ if case_id not in audio_buffers or not audio_buffers[case_id]:
155
+ logger.warning(f"⚠️ {case_id} uchun audio buffer bo'sh")
156
+ continue
157
+
158
+ logger.info(f"🎤 Audio oxiri belgisi (string) qabul qilindi")
159
+
160
+ full_audio = b"".join(audio_buffers[case_id])
161
+ audio_buffers[case_id] = []
162
+
163
+ logger.info(f"📦 To'liq audio hajmi: {len(full_audio)} bytes")
164
+
165
+ try:
166
+ transcribed_text = transcribe_audio_from_bytes(full_audio)
167
+ logger.info(f"✅ Transkripsiya: '{transcribed_text}'")
168
+
169
+ if transcribed_text and len(transcribed_text.strip()) > 0:
170
+ stats["voice_messages"] += 1
171
+ db.create_message(case_id, "user", transcribed_text)
172
+
173
+ await websocket.send_json({
174
+ "type": "transcription_result",
175
+ "text": transcribed_text
176
+ })
177
+
178
+ await process_text_input(websocket, case_id, transcribed_text, is_voice=True)
179
+
180
+ except Exception as e:
181
+ logger.error(f"❌ Transkripsiya xatoligi: {e}", exc_info=True)
182
+ await websocket.send_json({
183
+ "type": "error",
184
+ "message": "Ovozni tanishda xatolik"
185
+ })
186
+
187
+ continue
188
+
189
+ # ========== JSON MESSAGE ==========
190
+ try:
191
+ message = json.loads(message_text)
192
+ message_type = message.get("type")
193
+
194
+ # ========== TEXT INPUT ==========
195
+ if message_type == "text_input":
196
+ if not case_id:
197
+ new_case = db.create_case(client_info)
198
+ case_id = new_case['id']
199
+ logger.info(f"✅ Yangi case yaratildi (text): {case_id}")
200
+
201
+ text = message.get("text", "").strip()
202
+
203
+ if text:
204
+ db.create_message(case_id, "user", text)
205
+ stats["text_messages"] += 1
206
+
207
+ await process_text_input(websocket, case_id, text, is_voice=False)
208
+
209
+ # ========== PATIENT NAME ==========
210
+ elif message_type == "patient_name":
211
+ if not case_id:
212
+ logger.warning("⚠️ Case ID yo'q, ism qabul qilinmaydi")
213
+ continue
214
+
215
+ full_name = message.get("full_name", "").strip()
216
+
217
+ if full_name:
218
+ await process_name_input(websocket, case_id, full_name)
219
+
220
+ # ========== GPS LOCATION ==========
221
+ elif message_type == "gps_location":
222
+ if not case_id:
223
+ logger.warning("⚠️ Case ID yo'q, GPS qabul qilinmaydi")
224
+ continue
225
+
226
+ lat = message.get("latitude")
227
+ lon = message.get("longitude")
228
+
229
+ if lat and lon:
230
+ await process_gps_and_brigade(websocket, case_id, lat, lon)
231
+
232
+ except json.JSONDecodeError:
233
+ logger.error(f"❌ JSON parse xatoligi: {message_text}")
234
+
235
+ # ========== BINARY DATA (AUDIO CHUNKS) ==========
236
+ elif "bytes" in data:
237
+ if not case_id:
238
+ new_case = db.create_case(client_info)
239
+ case_id = new_case['id']
240
+ logger.info(f"✅ Yangi case yaratildi (audio): {case_id}")
241
+
242
+ audio_chunk = data["bytes"]
243
+
244
+ if audio_chunk == b"__END__":
245
+ logger.info("🎤 Audio oxiri belgisi (bytes) qabul qilindi")
246
+ continue
247
+
248
+ if case_id not in audio_buffers:
249
+ audio_buffers[case_id] = []
250
+
251
+ audio_buffers[case_id].append(audio_chunk)
252
+ logger.debug(f"📝 Audio chunk qo'shildi ({len(audio_chunk)} bytes). Jami: {len(audio_buffers[case_id])} chunks")
253
+
254
+ except WebSocketDisconnect:
255
+ logger.info(f"📴 WebSocket disconnect exception: {client_info}")
256
+
257
+ except Exception as e:
258
+ logger.error(f"❌ WebSocket xatolik: {e}", exc_info=True)
259
+
260
+ finally:
261
+ # Cleanup (har qanday holatda ham ishga tushadi)
262
+ active_connections.discard(websocket)
263
+
264
+ if case_id and case_id in audio_buffers:
265
+ del audio_buffers[case_id]
266
+
267
+ logger.info(f"🧹 WebSocket cleanup tugadi: {client_info}")
268
+
269
+
270
+ # ==================== MESSAGE HANDLERS ====================
271
+
272
+ async def handle_voice_message(websocket: WebSocket, case_id: str, data: Dict):
273
+ """
274
+ Ovozli xabar qayta ishlash
275
+
276
+ Flow:
277
+ 1. Audio → Text (STT)
278
+ 2. Text → AI tahlil (Gemini)
279
+ 3. Risk darajasini aniqlash
280
+ 4. Mos flow ni boshlash (qizil/sariq/yashil)
281
+ """
282
+ try:
283
+ audio_data = data.get("audio")
284
+ if not audio_data:
285
+ await websocket.send_json({
286
+ "type": "error",
287
+ "message": "Audio ma'lumot topilmadi"
288
+ })
289
+ return
290
+
291
+ # Audio bytes olish
292
+ import base64
293
+ audio_bytes = base64.b64decode(audio_data.split(',')[1] if ',' in audio_data else audio_data)
294
+
295
+ logger.info(f"🎤 Ovoz yozuvi qabul qilindi: {len(audio_bytes)} bytes")
296
+
297
+ # STT
298
+ await websocket.send_json({
299
+ "type": "status",
300
+ "message": "Ovozingizni tinglab turaman..."
301
+ })
302
+
303
+ user_transcript = transcribe_audio_from_bytes(audio_bytes)
304
+
305
+ if not user_transcript or len(user_transcript.strip()) < 3:
306
+ await websocket.send_json({
307
+ "type": "error",
308
+ "message": "Ovozni tushunolmadim. Iltimos, qaytadan aytib bering."
309
+ })
310
+ return
311
+
312
+ logger.info(f"📝 Transkripsiya: '{user_transcript}'")
313
+
314
+ # Database ga saqlash
315
+ db.create_message(case_id, "user", user_transcript)
316
+ stats["voice_messages"] += 1
317
+
318
+ # Text bilan davom etish
319
+ await process_text_input(websocket, case_id, user_transcript, is_voice=True)
320
+
321
+ except Exception as e:
322
+ logger.error(f"❌ Ovozli xabar xatoligi: {e}", exc_info=True)
323
+ await websocket.send_json({
324
+ "type": "error",
325
+ "message": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring."
326
+ })
327
+
328
+ async def handle_text_message(websocket: WebSocket, case_id: str, data: Dict):
329
+ """Matnli xabar qayta ishlash"""
330
+ try:
331
+ text = data.get("text", "").strip()
332
+
333
+ if not text or len(text) < 2:
334
+ await websocket.send_json({
335
+ "type": "error",
336
+ "message": "Xabar bo'sh. Iltimos, biror narsa yozing."
337
+ })
338
+ return
339
+
340
+ logger.info(f"💬 Matnli xabar: '{text}'")
341
+
342
+ db.create_message(case_id, "user", text)
343
+ stats["text_messages"] += 1
344
+
345
+ await process_text_input(websocket, case_id, text, is_voice=False)
346
+
347
+ except Exception as e:
348
+ logger.error(f"❌ Matnli xabar xatoligi: {e}", exc_info=True)
349
+ await websocket.send_json({
350
+ "type": "error",
351
+ "message": "Xatolik yuz berdi."
352
+ })
353
+
354
+
355
+ async def handle_gps_location(websocket: WebSocket, case_id: str, data: Dict):
356
+ """GPS lokatsiya qayta ishlash"""
357
+ try:
358
+ lat = data.get("latitude")
359
+ lon = data.get("longitude")
360
+
361
+ if not lat or not lon:
362
+ await websocket.send_json({
363
+ "type": "error",
364
+ "message": "GPS ma'lumot topilmadi"
365
+ })
366
+ return
367
+
368
+ logger.info(f"📍 GPS qabul qilindi: ({lat}, {lon})")
369
+
370
+ # GPS ni saqlash va brigada topish
371
+ await process_gps_and_brigade(websocket, case_id, lat, lon)
372
+
373
+ except Exception as e:
374
+ logger.error(f"❌ GPS xatoligi: {e}", exc_info=True)
375
+ await websocket.send_json({
376
+ "type": "error",
377
+ "message": "GPS xatolik"
378
+ })
379
+
380
+
381
+ # ==================== TEXT PROCESSING (ASOSIY MANTIQ) ====================
382
+
383
+ async def process_text_input(websocket: WebSocket, case_id: str, prompt: str, is_voice: bool = False):
384
+ """
385
+ Matn kiritishni qayta ishlash - ASOSIY FLOW
386
+
387
+ Args:
388
+ websocket: WebSocket ulanish
389
+ case_id: Case ID (string)
390
+ prompt: Bemorning matni
391
+ is_voice: Ovozli xabarmi? (True/False)
392
+ """
393
+ try:
394
+ # Case ni olish
395
+ current_case = db.get_case(case_id)
396
+
397
+ if not current_case:
398
+ logger.error(f"❌ Case topilmadi: {case_id}")
399
+ await websocket.send_json({
400
+ "type": "error",
401
+ "message": "Sessiya xatoligi. Iltimos, sahifani yangilang."
402
+ })
403
+ return
404
+
405
+ # ========== 1. ISM-FAMILIYA KUTILMOQDA? ==========
406
+ if current_case.get('waiting_for_name_input'):
407
+ await process_name_input(websocket, case_id, prompt)
408
+ return
409
+
410
+ # ========== 2. MANZIL ANIQLASHTIRILMOQDA? ==========
411
+ if await handle_location_clarification(websocket, case_id, prompt, "voice" if is_voice else "text"):
412
+ return
413
+
414
+ # ========== 3. YANGI TAHLIL (GEMINI) ==========
415
+ conversation_history = db.get_conversation_history(case_id)
416
+ detected_lang = detect_language(prompt)
417
+
418
+ logger.info(f"🧠 Gemini tahlil boshlandi...")
419
+
420
+ full_prompt = f"{conversation_history}\nBemor: {prompt}"
421
+ ai_analysis = get_gemini_response(full_prompt, stream=False)
422
+
423
+ # JSON parse qilish
424
+ if not ai_analysis or not isinstance(ai_analysis, dict):
425
+ logger.error(f"❌ Gemini noto'g'ri javob: {ai_analysis}")
426
+ await websocket.send_json({
427
+ "type": "error",
428
+ "message": "AI xatolik"
429
+ })
430
+ return
431
+
432
+ risk_level = ai_analysis.get("risk_level", "yashil")
433
+ response_text = ai_analysis.get("response_text", "Tushunmadim")
434
+ language = ai_analysis.get("language", detected_lang)
435
+
436
+ logger.info(f"📊 Risk darajasi: {risk_level.upper()}")
437
+
438
+ # Database ga saqlash
439
+ db.create_message(case_id, "ai", response_text)
440
+ db.update_case(case_id, {
441
+ "risk_level": risk_level,
442
+ "language": language,
443
+ "symptoms_text": ai_analysis.get("symptoms_extracted")
444
+ })
445
+
446
+ # ========== RISK DARAJASIGA QARAB HARAKAT ==========
447
+
448
+ if risk_level == "qizil":
449
+ await handle_qizil_flow(websocket, case_id, ai_analysis)
450
+ elif risk_level == "sariq":
451
+ await handle_sariq_flow(websocket, case_id, ai_analysis)
452
+ elif risk_level == "yashil":
453
+ await handle_yashil_flow(websocket, case_id, ai_analysis)
454
+ else:
455
+ logger.warning(f"⚠️ Noma'lum risk level: {risk_level}")
456
+ await send_ai_response(websocket, case_id, response_text, language)
457
+
458
+ except Exception as e:
459
+ logger.error(f"❌ process_text_input xatoligi: {e}", exc_info=True)
460
+ await websocket.send_json({
461
+ "type": "error",
462
+ "message": "Xatolik yuz berdi"
463
+ })
464
+
465
+
466
+ # ==================== HELPER FUNCTION ====================
467
+
468
+ async def send_ai_response(websocket: WebSocket, case_id: str, text: str, language: str = "uzb"):
469
+ """
470
+ AI javobini frontendga yuborish (text + audio)
471
+
472
+ TUZATILGAN: TTS output_path to'g'ri yaratiladi
473
+
474
+ Args:
475
+ websocket: WebSocket ulanish
476
+ case_id: Case ID
477
+ text: Javob matni
478
+ language: Javob tili ("uzb" | "eng" | "rus")
479
+ """
480
+ try:
481
+ # Database ga AI xabarini saqlash
482
+ db.create_message(case_id, "ai", text)
483
+
484
+ # 1. Text yuborish
485
+ await websocket.send_json({
486
+ "type": "ai_response",
487
+ "text": text
488
+ })
489
+
490
+ # 2. TTS audio yaratish
491
+ # ✅ TO'G'RI: output_path yaratish
492
+ audio_filename = f"tts_{case_id}_{int(time.time())}.wav"
493
+ audio_path = os.path.join("/tmp/audio", audio_filename)
494
+
495
+ logger.info(f"🎧 TTS uchun fayl yo'li: {audio_path}")
496
+
497
+ # TTS chaqirish (to'g'ri parametrlar bilan)
498
+ tts_success = synthesize_speech(text, audio_path, language)
499
+
500
+ if tts_success and os.path.exists(audio_path):
501
+ audio_url = f"/audio/{audio_filename}"
502
+ await websocket.send_json({
503
+ "type": "audio_response",
504
+ "audio_url": audio_url
505
+ })
506
+ logger.info(f"📊 TTS audio yuborildi: {audio_url}")
507
+ else:
508
+ logger.warning("⚠️ TTS yaratilmadi, faqat text yuborildi")
509
+
510
+ except Exception as e:
511
+ logger.error(f"❌ send_ai_response xatoligi: {e}", exc_info=True)
512
+
513
+ # app/api/routes.py - QISM 2
514
+ # 3 TA ASOSIY FLOW: QIZIL, SARIQ, YASHIL
515
+
516
+ # ==================== 🔴 QIZIL FLOW (EMERGENCY) ====================
517
+
518
+ async def handle_qizil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
519
+ """
520
+ QIZIL (Emergency) - TEZ YORDAM BRIGADA
521
+
522
+ Flow:
523
+ 1. Manzil so'rash (tuman + mahalla)
524
+ 2. Fuzzy matching orqali koordinata topish
525
+ 3. Brigada topish va jo'natish
526
+ 4. ISM-FAMILIYA so'rash (brigadadan KEYIN!)
527
+ """
528
+ try:
529
+ logger.info(f"🔴 QIZIL HOLAT: Tez yordam jarayoni boshlandi")
530
+
531
+ response_text = ai_analysis.get("response_text")
532
+ language = ai_analysis.get("language", "uzb")
533
+ address = ai_analysis.get("address_extracted")
534
+ district = ai_analysis.get("district_extracted")
535
+
536
+ # Case type ni belgilash
537
+ db.update_case(case_id, {
538
+ "type": "emergency",
539
+ "risk_level": "qizil"
540
+ })
541
+
542
+ # 1. MANZIL SO'RASH
543
+ if not address or not district:
544
+ logger.info("📍 Manzil yo'q, so'ralmoqda...")
545
+ await send_ai_response(websocket, case_id, response_text, language)
546
+
547
+ # Flag qo'yish - keyingi xabarda manzil kutiladi
548
+ db.update_case(case_id, {"waiting_for_address": True})
549
+ return
550
+
551
+ # 2. MANZILNI QAYTA ISHLASH
552
+ logger.info(f"📍 Manzil aniqlandi: {address}")
553
+
554
+ # Tuman fuzzy match
555
+ district_match = find_district_fuzzy(district)
556
+
557
+ if not district_match:
558
+ logger.warning(f"⚠️ Tuman topilmadi: {district}")
559
+ districts_list = get_all_districts()
560
+
561
+ response = f"Tuman nomini aniq tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{districts_list}"
562
+ await send_ai_response(websocket, case_id, response, language)
563
+ return
564
+
565
+ district_name = get_district_display_name(district_match)
566
+ logger.info(f"✅ Tuman topildi: {district_name}")
567
+
568
+ db.update_case(case_id, {
569
+ "district": district_name,
570
+ "selected_district": district_match
571
+ })
572
+
573
+ # 3. MAHALLA SO'RASH
574
+ # Bu qism location_clarification da amalga oshiriladi
575
+ # Hozircha flag qo'yamiz
576
+ db.update_case(case_id, {
577
+ "waiting_for_mahalla_input": True,
578
+ "mahalla_retry_count": 0
579
+ })
580
+
581
+ response = f"Tushundim, {district_name}. Iltimos, mahallangizni ayting."
582
+ await send_ai_response(websocket, case_id, response, language)
583
+
584
+ # Dispetcherga bildirishnoma
585
+ await notify_dispatchers({
586
+ "type": "new_case",
587
+ "case": db.get_case(case_id)
588
+ })
589
+
590
+ except Exception as e:
591
+ logger.error(f"❌ handle_qizil_flow xatoligi: {e}", exc_info=True)
592
+
593
+
594
+ async def process_gps_and_brigade(websocket: WebSocket, case_id: str, lat: float, lon: float):
595
+ """
596
+ GPS koordinatalariga qarab brigadani topish
597
+
598
+ MUHIM: Brigadadan KEYIN ism-familiya so'raladi!
599
+ """
600
+ try:
601
+ logger.info(f"📍 GPS koordinatalar: ({lat:.6f}, {lon:.6f})")
602
+
603
+ # GPS validatsiya
604
+ if not validate_location_in_tashkent(lat, lon):
605
+ logger.warning("⚠️ GPS Toshkent chegarasidan tashqarida")
606
+ await websocket.send_json({
607
+ "type": "error",
608
+ "message": "GPS manzil Toshkent chegarasidan tashqarida"
609
+ })
610
+ return
611
+
612
+ # Case ga saqlash
613
+ db.update_case(case_id, {
614
+ "gps_lat": lat,
615
+ "gps_lon": lon,
616
+ "geocoded_lat": lat,
617
+ "geocoded_lon": lon,
618
+ "gps_verified": True
619
+ })
620
+
621
+ # Brigadani topish
622
+ logger.info("🚑 Eng yaqin brigada qidirilmoqda...")
623
+
624
+ nearest_brigade = find_nearest_brigade(lat, lon)
625
+
626
+ if not nearest_brigade:
627
+ logger.warning("⚠️ Brigada topilmadi")
628
+ await websocket.send_json({
629
+ "type": "error",
630
+ "message": "Hozirda barcha brigadalar band"
631
+ })
632
+ return
633
+
634
+ brigade_id = nearest_brigade['brigade_id']
635
+ brigade_name = nearest_brigade['brigade_name']
636
+ distance_km = nearest_brigade['distance_km']
637
+
638
+ # Brigadani tayinlash
639
+ db.update_case(case_id, {
640
+ "assigned_brigade_id": brigade_id,
641
+ "assigned_brigade_name": brigade_name,
642
+ "distance_to_brigade_km": distance_km,
643
+ "status": "brigada_junatildi"
644
+ })
645
+
646
+ logger.info(f"✅ Brigada tayinlandi: {brigade_name} ({distance_km:.2f} km)")
647
+
648
+ # Bemorga xabar
649
+ await websocket.send_json({
650
+ "type": "brigade_assigned",
651
+ "brigade": {
652
+ "id": brigade_id,
653
+ "name": brigade_name,
654
+ "distance_km": distance_km,
655
+ "estimated_time_min": int(distance_km * 3) # 3 min/km
656
+ }
657
+ })
658
+
659
+ # ========== ENDI ISM-FAMILIYA SO'RASH ==========
660
+ current_case = db.get_case(case_id)
661
+ language = current_case.get("language", "uzb")
662
+
663
+ if language == "eng":
664
+ name_request = f"The ambulance is on its way, arriving in approximately {int(distance_km * 3)} minutes. Please tell me your full name."
665
+ elif language == "rus":
666
+ name_request = f"Скорая помощь в пути, прибудет примерно через {int(distance_km * 3)} минут. Пожалуйста, назовите ваше полное имя."
667
+ else:
668
+ name_request = f"Brigada yo'lda, taxminan {int(distance_km * 3)} daqiqada yetib keladi. Iltimos, to'liq ism-familiyangizni ayting."
669
+
670
+ db.create_message(case_id, "ai", name_request)
671
+ await send_ai_response(websocket, case_id, name_request, language)
672
+
673
+ # Flag qo'yish
674
+ db.update_case(case_id, {"waiting_for_name_input": True})
675
+
676
+ # Dispetcherga yangilanish
677
+ await notify_dispatchers({
678
+ "type": "brigade_assigned",
679
+ "case": db.get_case(case_id)
680
+ })
681
+
682
+ except Exception as e:
683
+ logger.error(f"❌ process_gps_and_brigade xatoligi: {e}", exc_info=True)
684
+
685
+
686
+ # ==================== 🟡 SARIQ FLOW (UNCERTAIN) ====================
687
+
688
+ async def handle_sariq_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
689
+ """
690
+ SARIQ (Uncertain) - NOANIQ, OPERATOR KERAK
691
+
692
+ Flow:
693
+ 1. Aniqlashtiruvchi savol berish
694
+ 2. Counter ni oshirish (max 3)
695
+ 3. 3 marta tushunmasa → Operator
696
+ """
697
+ try:
698
+ logger.info(f"🟡 SARIQ HOLAT: Noaniqlik")
699
+
700
+ response_text = ai_analysis.get("response_text")
701
+ language = ai_analysis.get("language", "uzb")
702
+ uncertainty_reason = ai_analysis.get("uncertainty_reason")
703
+ operator_needed = ai_analysis.get("operator_needed", False)
704
+
705
+ current_case = db.get_case(case_id)
706
+ current_attempts = current_case.get("uncertainty_attempts", 0)
707
+
708
+ # Case type ni belgilash
709
+ db.update_case(case_id, {
710
+ "type": "uncertain",
711
+ "risk_level": "sariq"
712
+ })
713
+
714
+ # Operator kerakmi?
715
+ if operator_needed or current_attempts >= MAX_UNCERTAINTY_ATTEMPTS:
716
+ logger.info(f"🎧 OPERATOR KERAK! (Attempts: {current_attempts})")
717
+
718
+ db.update_case(case_id, {
719
+ "operator_needed": True,
720
+ "uncertainty_reason": uncertainty_reason or f"AI {current_attempts} marta tushunolmadi",
721
+ "status": "operator_kutilmoqda",
722
+ "uncertainty_attempts": current_attempts + 1
723
+ })
724
+
725
+ # Bemorga xabar
726
+ if language == "eng":
727
+ operator_msg = "I'm having trouble understanding you. Connecting you to an operator who can help..."
728
+ elif language == "rus":
729
+ operator_msg = "Мне сложно вас понять. Соединяю с оператором, который вам поможет..."
730
+ else:
731
+ operator_msg = "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi..."
732
+
733
+ await send_ai_response(websocket, case_id, operator_msg, language)
734
+
735
+ # Dispetcherga operator kerakligi haqida xabar
736
+ await notify_dispatchers({
737
+ "type": "operator_needed",
738
+ "case": db.get_case(case_id)
739
+ })
740
+
741
+ return
742
+
743
+ # Hali operator kerak emas, aniqlashtirish
744
+ logger.info(f"❓ Aniqlashtirish (Attempt {current_attempts + 1}/{MAX_UNCERTAINTY_ATTEMPTS})")
745
+
746
+ db.update_case(case_id, {
747
+ "uncertainty_attempts": current_attempts + 1,
748
+ "uncertainty_reason": uncertainty_reason
749
+ })
750
+
751
+ await send_ai_response(websocket, case_id, response_text, language)
752
+
753
+ except Exception as e:
754
+ logger.error(f"❌ handle_sariq_flow xatoligi: {e}", exc_info=True)
755
+
756
+
757
+ # ==================== 🟢 YASHIL FLOW (CLINIC) ====================
758
+
759
+ async def handle_yashil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
760
+ """
761
+ YASHIL (Non-urgent) - KLINIKA TAVSIYA
762
+
763
+ Flow:
764
+ 1. Bemorga xotirjamlik berish
765
+ 2. Davlat yoki xususiy klinika taklif qilish
766
+ 3. Bemor tanlasa, klinikalar ro'yxatini yuborish
767
+ """
768
+ try:
769
+ logger.info(f"🟢 YASHIL HOLAT: Klinika tavsiyasi")
770
+
771
+ response_text = ai_analysis.get("response_text")
772
+ language = ai_analysis.get("language", "uzb")
773
+ symptoms = ai_analysis.get("symptoms_extracted")
774
+ preferred_clinic_type = ai_analysis.get("preferred_clinic_type", "both")
775
+ recommended_specialty = ai_analysis.get("recommended_specialty", "Terapiya")
776
+
777
+ # Case type ni belgilash
778
+ db.update_case(case_id, {
779
+ "type": "public_clinic", # Default, keyin o'zgarishi mumkin
780
+ "risk_level": "yashil",
781
+ "symptoms_text": symptoms
782
+ })
783
+
784
+ # 1. AI javobini yuborish (xotirjamlik + taklif)
785
+ await send_ai_response(websocket, case_id, response_text, language)
786
+
787
+ # 2. Klinikalarni qidirish
788
+ logger.info(f"🏥 Klinikalar qidirilmoqda: {recommended_specialty}, type={preferred_clinic_type}")
789
+
790
+ # Har ikki turdan ham topish
791
+ if preferred_clinic_type == "both":
792
+ davlat_clinics = db.recommend_clinics_by_symptoms(
793
+ symptoms=symptoms,
794
+ district=None,
795
+ clinic_type="davlat"
796
+ )
797
+
798
+ xususiy_clinics = db.recommend_clinics_by_symptoms(
799
+ symptoms=symptoms,
800
+ district=None,
801
+ clinic_type="xususiy"
802
+ )
803
+
804
+ # Formatlangan ro'yxat yaratish
805
+ clinic_list_text = format_clinic_list(
806
+ davlat_clinics.get('clinics', [])[:2], # Top 2 davlat
807
+ xususiy_clinics.get('clinics', [])[:3], # Top 3 xususiy
808
+ language
809
+ )
810
+
811
+ else:
812
+ # Faqat bitta turni ko'rsatish
813
+ recommendation = db.recommend_clinics_by_symptoms(
814
+ symptoms=symptoms,
815
+ district=None,
816
+ clinic_type=preferred_clinic_type
817
+ )
818
+
819
+ clinic_list_text = format_clinic_list(
820
+ recommendation.get('clinics', [])[:5] if preferred_clinic_type == "davlat" else [],
821
+ recommendation.get('clinics', [])[:5] if preferred_clinic_type == "xususiy" else [],
822
+ language
823
+ )
824
+
825
+ # 3. Klinikalar ro'yxatini yuborish
826
+ await websocket.send_json({
827
+ "type": "clinic_recommendation",
828
+ "text": clinic_list_text
829
+ })
830
+
831
+ db.create_message(case_id, "ai", clinic_list_text)
832
+
833
+ # Dispetcherga xabar
834
+ await notify_dispatchers({
835
+ "type": "clinic_case",
836
+ "case": db.get_case(case_id)
837
+ })
838
+
839
+ logger.info(f"✅ Klinikalar ro'yxati yuborildi")
840
+
841
+ except Exception as e:
842
+ logger.error(f"❌ handle_yashil_flow xatoligi: {e}", exc_info=True)
843
+
844
+
845
+ def format_clinic_list(davlat_clinics: List[Dict], xususiy_clinics: List[Dict], language: str = "uzb") -> str:
846
+ """
847
+ Klinikalar ro'yxatini formatlash
848
+
849
+ Args:
850
+ davlat_clinics: Davlat poliklinikalari
851
+ xususiy_clinics: Xususiy klinikalar
852
+ language: Til
853
+
854
+ Returns:
855
+ Formatlangan matn
856
+ """
857
+ result = []
858
+
859
+ # Header
860
+ if language == "eng":
861
+ result.append("Here are my recommendations:\n")
862
+ elif language == "rus":
863
+ result.append("Вот мои рекомендации:\n")
864
+ else:
865
+ result.append("Mana sizga tavsiyalar:\n")
866
+
867
+ # Davlat klinikalari
868
+ if davlat_clinics:
869
+ if language == "eng":
870
+ result.append("\n🏥 PUBLIC CLINICS (Free):\n")
871
+ elif language == "rus":
872
+ result.append("\n🏥 ГОСУДАРСТВЕННЫЕ ПОЛИКЛИНИКИ (Бесплатно):\n")
873
+ else:
874
+ result.append("\n🏥 DAVLAT POLIKLINIKALARI (Bepul):\n")
875
+
876
+ for idx, clinic in enumerate(davlat_clinics, 1):
877
+ result.append(f"\n{idx}️⃣ {clinic['name']}")
878
+ result.append(f" 📍 {clinic['address']}")
879
+ result.append(f" 📞 {clinic['phone']}")
880
+ result.append(f" ⏰ {clinic['working_hours']}")
881
+ result.append(f" ⭐ {clinic['rating']}/5.0")
882
+
883
+ # Xususiy klinikalar
884
+ if xususiy_clinics:
885
+ if language == "eng":
886
+ result.append("\n\n🏥 PRIVATE CLINICS:\n")
887
+ elif language == "rus":
888
+ result.append("\n\n🏥 ЧАСТНЫЕ КЛИНИКИ:\n")
889
+ else:
890
+ result.append("\n\n🏥 XUSUSIY KLINIKALAR:\n")
891
+
892
+ for idx, clinic in enumerate(xususiy_clinics, 1):
893
+ result.append(f"\n{idx}️⃣ {clinic['name']}")
894
+ result.append(f" 📍 {clinic['address']}")
895
+ result.append(f" 📞 {clinic['phone']}")
896
+ result.append(f" ⏰ {clinic['working_hours']}")
897
+ result.append(f" 💰 {clinic['price_range']}")
898
+ result.append(f" ⭐ {clinic['rating']}/5.0")
899
+
900
+ return "\n".join(result)
901
+
902
+
903
+ # ==================== HELPER FUNCTIONS ====================
904
+
905
+ async def process_name_input(websocket: WebSocket, case_id: str, name_text: str):
906
+ """
907
+ Ism-familiyani qayta ishlash
908
+
909
+ Bu funksiya brigadadan KEYIN chaqiriladi
910
+ """
911
+ try:
912
+ logger.info(f"👤 Ism-familiya qabul qilindi: '{name_text}'")
913
+
914
+ current_case = db.get_case(case_id)
915
+ language = current_case.get("language", "uzb")
916
+
917
+ # Ism-familiyani saqlash
918
+ db.update_case(case_id, {
919
+ "patient_full_name": name_text,
920
+ "waiting_for_name_input": False
921
+ })
922
+
923
+ # Bemor tarixini tekshirish
924
+ patient_history = db.get_patient_statistics(name_text)
925
+
926
+ if patient_history and patient_history.get("total_cases", 0) > 0:
927
+ previous_count = patient_history.get("total_cases")
928
+ logger.info(f"📋 Bemor tarixi topildi: {previous_count} ta oldingi murojat")
929
+
930
+ db.update_case(case_id, {
931
+ "previous_cases_count": previous_count
932
+ })
933
+
934
+ # Tasdiq xabari
935
+ if language == "eng":
936
+ confirmation = f"Thank you, {name_text}. The ambulance will arrive shortly. Please stay calm."
937
+ elif language == "rus":
938
+ confirmation = f"Спасибо, {name_text}. Скорая помощь скоро прибудет. Пожалуйста, сохраняйте спокойствие."
939
+ else:
940
+ confirmation = f"Rahmat, {name_text}. Brigada tez orada yetib keladi. Iltimos, xotirjam bo'ling."
941
+
942
+ await send_ai_response(websocket, case_id, confirmation, language)
943
+
944
+ # Dispetcherga yangilanish
945
+ await notify_dispatchers({
946
+ "type": "name_received",
947
+ "case": db.get_case(case_id)
948
+ })
949
+
950
+ except Exception as e:
951
+ logger.error(f"❌ process_name_input xatoligi: {e}", exc_info=True)
952
+
953
+
954
+ async def handle_location_clarification(websocket: WebSocket, case_id: str, user_input: str, input_type: str) -> bool:
955
+ """
956
+ Manzilni aniqlashtirish (mahalla)
957
+
958
+ Returns:
959
+ True - agar mahalla kutilgan bo'lsa va qayta ishlandi
960
+ False - agar mahalla kutilmagan
961
+ """
962
+ try:
963
+ current_case = db.get_case(case_id)
964
+
965
+ if not current_case.get("waiting_for_mahalla_input"):
966
+ return False
967
+
968
+ logger.info(f"🏘️ Mahalla aniqlashtirilmoqda: '{user_input}'")
969
+
970
+ district_id = current_case.get("selected_district")
971
+ district_name = current_case.get("district")
972
+ language = current_case.get("language", "uzb")
973
+
974
+ if not district_id:
975
+ logger.error("❌ District ID topilmadi")
976
+ return False
977
+
978
+ # Mahalla fuzzy match
979
+ mahalla_match = find_mahalla_fuzzy(district_name, user_input, threshold=0.35)
980
+
981
+ if mahalla_match:
982
+ mahalla_full_name = get_mahalla_display_name(mahalla_match)
983
+ logger.info(f"✅ Mahalla topildi: {mahalla_full_name}")
984
+
985
+ # Mahalla koordinatalarini olish
986
+ mahalla_coords = get_mahalla_coordinates(district_name, mahalla_match)
987
+
988
+ if mahalla_coords:
989
+ db.update_case(case_id, {
990
+ "selected_mahalla": mahalla_full_name,
991
+ "mahalla_lat": mahalla_coords['lat'],
992
+ "mahalla_lon": mahalla_coords['lon'],
993
+ "geocoded_lat": mahalla_coords['lat'],
994
+ "geocoded_lon": mahalla_coords['lon'],
995
+ "waiting_for_mahalla_input": False,
996
+ "mahalla_retry_count": 0
997
+ })
998
+
999
+ # Brigadani topish
1000
+ await process_gps_and_brigade(
1001
+ websocket,
1002
+ case_id,
1003
+ mahalla_coords['lat'],
1004
+ mahalla_coords['lon']
1005
+ )
1006
+
1007
+ return True
1008
+
1009
+ # Mahalla topilmadi
1010
+ retry_count = current_case.get("mahalla_retry_count", 0) + 1
1011
+
1012
+ if retry_count >= 3:
1013
+ # 3 marta topilmasa, faqat tuman bilan davom etamiz
1014
+ logger.warning("⚠️ Mahalla 3 marta topilmadi, tuman markazidan foydalaniladi")
1015
+
1016
+ district_gps = get_gps_for_district(district_id)
1017
+
1018
+ if district_gps:
1019
+ db.update_case(case_id, {
1020
+ "geocoded_lat": district_gps['lat'],
1021
+ "geocoded_lon": district_gps['lon'],
1022
+ "waiting_for_mahalla_input": False,
1023
+ "mahalla_retry_count": 0
1024
+ })
1025
+
1026
+ await process_gps_and_brigade(
1027
+ websocket,
1028
+ case_id,
1029
+ district_gps['lat'],
1030
+ district_gps['lon']
1031
+ )
1032
+
1033
+ return True
1034
+
1035
+ # Qayta so'rash
1036
+ db.update_case(case_id, {"mahalla_retry_count": retry_count})
1037
+
1038
+ mahallas_list = format_mahallas_list(get_mahallas_by_district(district_name))
1039
+
1040
+ response = f"Mahalla nomini tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{mahallas_list}"
1041
+ await send_ai_response(websocket, case_id, response, language)
1042
+
1043
+ return True
1044
+
1045
+ except Exception as e:
1046
+ logger.error(f"❌ handle_location_clarification xatoligi: {e}", exc_info=True)
1047
+ return False
1048
+
1049
+
1050
+ # ==================== PERIODIC CLEANUP ====================
1051
+
1052
+ async def periodic_cleanup():
1053
+ """Eski audio fayllarni tozalash (har 1 soatda)"""
1054
+ while True:
1055
+ try:
1056
+ await asyncio.sleep(3600) # 1 soat
1057
+ logger.info("🧹 Periodic cleanup boshlandi...")
1058
+
1059
+ audio_dir = "static/audio"
1060
+ if os.path.exists(audio_dir):
1061
+ current_time = time.time()
1062
+ for filename in os.listdir(audio_dir):
1063
+ file_path = os.path.join(audio_dir, filename)
1064
+ if os.path.isfile(file_path):
1065
+ if current_time - os.path.getmtime(file_path) > 3600: # 1 soat
1066
+ os.remove(file_path)
1067
+ logger.info(f"🗑️ Eski fayl o'chirildi: {filename}")
1068
+ except Exception as e:
1069
+ logger.error(f"❌ Periodic cleanup xatoligi: {e}")
1070
+
1071
+
1072
+ @router.on_event("startup")
1073
+ async def startup_event():
1074
+ """Server ishga tushganda"""
1075
+ asyncio.create_task(periodic_cleanup())
1076
+ logger.info("🚀 Periodic cleanup task ishga tushdi")
1077
+
1078
+
1079
+ # ==================== CASE MANAGEMENT APIs ====================
1080
+
1081
+ @router.get("/cases")
1082
+ async def get_all_cases(status: Optional[str] = None):
1083
+ """Barcha caselarni olish"""
1084
+ try:
1085
+ cases = db.get_all_cases(status=status)
1086
+ return cases
1087
+ except Exception as e:
1088
+ logger.error(f"❌ Cases olishda xatolik: {e}")
1089
+ raise HTTPException(status_code=500, detail="Server xatoligi")
1090
+
1091
+
1092
+ @router.get("/cases/{case_id}")
1093
+ async def get_case(case_id: str):
1094
+ """Bitta case ma'lumotlarini olish"""
1095
+ case = db.get_case(case_id)
1096
+
1097
+ if not case:
1098
+ raise HTTPException(status_code=404, detail="Case topilmadi")
1099
+
1100
+ return case
1101
+
1102
+
1103
+ @router.patch("/cases/{case_id}")
1104
+ async def update_case(case_id: str, updates: CaseUpdate):
1105
+ """Case ni yangilash"""
1106
+ update_data = updates.dict(exclude_unset=True)
1107
+
1108
+ success = db.update_case(case_id, update_data)
1109
+
1110
+ if not success:
1111
+ raise HTTPException(status_code=404, detail="Case topilmadi")
1112
+
1113
+ updated_case = db.get_case(case_id)
1114
+
1115
+ # Dispetcherlarga yangilanish
1116
+ await notify_dispatchers({
1117
+ "type": "case_updated",
1118
+ "case": updated_case
1119
+ })
1120
+
1121
+ return updated_case
1122
+
1123
+
1124
+
1125
+