# app/api/routes.py - TO'LIQ YANGILANGAN (3 RISK TIZIMI) # QISM 1: Imports, Health Checks, va WebSocket Handler import os import uuid import json import asyncio import logging import time from typing import Optional, Dict, List from fastapi import ( APIRouter, WebSocket, WebSocketDisconnect, HTTPException, UploadFile, File, BackgroundTasks, Query ) from fastapi.responses import JSONResponse import shutil # Utils from app.utils.district_matcher import find_district_fuzzy, get_district_display_name, list_all_districts_text from app.utils.mahalla_matcher import find_mahalla_fuzzy, get_mahalla_display_name from app.utils.demo_gps import generate_random_tashkent_gps, get_gps_for_district, add_gps_noise, get_all_districts # Services from app.services.models import ( transcribe_audio_from_bytes, transcribe_audio, get_gemini_response, synthesize_speech, check_model_status, detect_language ) from app.services.geocoding import geocode_address, validate_location_in_tashkent, get_location_summary, extract_district_from_address from app.services.brigade_matcher import find_nearest_brigade, haversine_distance from app.services.location_validator import get_mahallas_by_district, format_mahallas_list, get_mahalla_coordinates # Core from app.core.database import db from app.core.config import GPS_VERIFICATION_MAX_DISTANCE_KM, USE_DEMO_GPS, GPS_NOISE_KM, MAX_UNCERTAINTY_ATTEMPTS from app.core.connections import active_connections # API from app.api.dispatcher_routes import notify_dispatchers # Schemas from app.models.schemas import ( CaseResponse, CaseUpdate, MessageResponse, SuccessResponse, ErrorResponse, BrigadeLocation, PatientHistoryResponse, ClinicResponse, ClinicRecommendation ) audio_buffers: Dict[str, list] = {} # Logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) router = APIRouter() # Global variables tasks = {} stats = { "total_messages": 0, "voice_messages": 0, "text_messages": 0, "active_connections": 0, "start_time": time.time() } # ==================== HEALTH & STATS ==================== @router.get("/api/health") async def health_check(): """Server va model holatini tekshirish""" model_status = check_model_status() uptime = time.time() - stats["start_time"] return JSONResponse({ "status": "healthy", "uptime_seconds": int(uptime), "models": model_status, "stats": { **stats, "active_connections": len(active_connections) }, "timestamp": time.time() }) @router.get("/api/stats") async def get_stats(): """Server statistikasi""" return JSONResponse({ **stats, "active_connections": len(active_connections), "uptime_seconds": int(time.time() - stats["start_time"]) }) # app/api/routes.py - TUZATILGAN QISM (WebSocket Handler) # Faqat muammoli qismni tuzatamiz @router.websocket("/ws/chat") async def websocket_endpoint(websocket: WebSocket): """ Bemor uchun WebSocket ulanish Frontend: /ws/chat ga ulanadi Backend: Session ID oladi, case yaratadi """ await websocket.accept() active_connections.add(websocket) client_info = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" logger.info(f"🔌 WebSocket ulanish o'rnatildi: {client_info}") case_id = None try: while True: # ========== XABAR QABUL QILISH ========== try: data = await websocket.receive() except RuntimeError as e: if "disconnect" in str(e).lower(): logger.info(f"📴 WebSocket disconnect signal olindi: {client_info}") break raise # Disconnect message tekshirish if data.get("type") == "websocket.disconnect": logger.info(f"📴 WebSocket disconnect message: {client_info}") break # ========== TEXT MESSAGE (JSON) ========== if "text" in data: message_text = data["text"] # "__END__" string belgisi (audio oxiri) if message_text == "__END__": if not case_id: new_case = db.create_case(client_info) case_id = new_case['id'] logger.info(f"✅ Yangi case yaratildi: {case_id}") if case_id not in audio_buffers or not audio_buffers[case_id]: logger.warning(f"⚠️ {case_id} uchun audio buffer bo'sh") continue logger.info(f"🎤 Audio oxiri belgisi (string) qabul qilindi") full_audio = b"".join(audio_buffers[case_id]) audio_buffers[case_id] = [] logger.info(f"📦 To'liq audio hajmi: {len(full_audio)} bytes") try: transcribed_text = transcribe_audio_from_bytes(full_audio) logger.info(f"✅ Transkripsiya: '{transcribed_text}'") if transcribed_text and len(transcribed_text.strip()) > 0: stats["voice_messages"] += 1 db.create_message(case_id, "user", transcribed_text) await websocket.send_json({ "type": "transcription_result", "text": transcribed_text }) await process_text_input(websocket, case_id, transcribed_text, is_voice=True) except Exception as e: logger.error(f"❌ Transkripsiya xatoligi: {e}", exc_info=True) await websocket.send_json({ "type": "error", "message": "Ovozni tanishda xatolik" }) continue # ========== JSON MESSAGE ========== try: message = json.loads(message_text) message_type = message.get("type") # ========== TEXT INPUT ========== if message_type == "text_input": if not case_id: new_case = db.create_case(client_info) case_id = new_case['id'] logger.info(f"✅ Yangi case yaratildi (text): {case_id}") text = message.get("text", "").strip() if text: db.create_message(case_id, "user", text) stats["text_messages"] += 1 await process_text_input(websocket, case_id, text, is_voice=False) # ========== PATIENT NAME ========== elif message_type == "patient_name": if not case_id: logger.warning("⚠️ Case ID yo'q, ism qabul qilinmaydi") continue full_name = message.get("full_name", "").strip() if full_name: await process_name_input(websocket, case_id, full_name) # ========== GPS LOCATION ========== elif message_type == "gps_location": if not case_id: logger.warning("⚠️ Case ID yo'q, GPS qabul qilinmaydi") continue lat = message.get("latitude") lon = message.get("longitude") if lat and lon: await process_gps_and_brigade(websocket, case_id, lat, lon) except json.JSONDecodeError: logger.error(f"❌ JSON parse xatoligi: {message_text}") # ========== BINARY DATA (AUDIO CHUNKS) ========== elif "bytes" in data: if not case_id: new_case = db.create_case(client_info) case_id = new_case['id'] logger.info(f"✅ Yangi case yaratildi (audio): {case_id}") audio_chunk = data["bytes"] if audio_chunk == b"__END__": logger.info("🎤 Audio oxiri belgisi (bytes) qabul qilindi") continue if case_id not in audio_buffers: audio_buffers[case_id] = [] audio_buffers[case_id].append(audio_chunk) logger.debug(f"📝 Audio chunk qo'shildi ({len(audio_chunk)} bytes). Jami: {len(audio_buffers[case_id])} chunks") except WebSocketDisconnect: logger.info(f"📴 WebSocket disconnect exception: {client_info}") except Exception as e: logger.error(f"❌ WebSocket xatolik: {e}", exc_info=True) finally: # Cleanup (har qanday holatda ham ishga tushadi) active_connections.discard(websocket) if case_id and case_id in audio_buffers: del audio_buffers[case_id] logger.info(f"🧹 WebSocket cleanup tugadi: {client_info}") # ==================== MESSAGE HANDLERS ==================== async def handle_voice_message(websocket: WebSocket, case_id: str, data: Dict): """ Ovozli xabar qayta ishlash Flow: 1. Audio → Text (STT) 2. Text → AI tahlil (Gemini) 3. Risk darajasini aniqlash 4. Mos flow ni boshlash (qizil/sariq/yashil) """ try: audio_data = data.get("audio") if not audio_data: await websocket.send_json({ "type": "error", "message": "Audio ma'lumot topilmadi" }) return # Audio bytes olish import base64 audio_bytes = base64.b64decode(audio_data.split(',')[1] if ',' in audio_data else audio_data) logger.info(f"🎤 Ovoz yozuvi qabul qilindi: {len(audio_bytes)} bytes") # STT await websocket.send_json({ "type": "status", "message": "Ovozingizni tinglab turaman..." }) user_transcript = transcribe_audio_from_bytes(audio_bytes) if not user_transcript or len(user_transcript.strip()) < 3: await websocket.send_json({ "type": "error", "message": "Ovozni tushunolmadim. Iltimos, qaytadan aytib bering." }) return logger.info(f"📝 Transkripsiya: '{user_transcript}'") # Database ga saqlash db.create_message(case_id, "user", user_transcript) stats["voice_messages"] += 1 # Text bilan davom etish await process_text_input(websocket, case_id, user_transcript, is_voice=True) except Exception as e: logger.error(f"❌ Ovozli xabar xatoligi: {e}", exc_info=True) await websocket.send_json({ "type": "error", "message": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring." }) async def handle_text_message(websocket: WebSocket, case_id: str, data: Dict): """Matnli xabar qayta ishlash""" try: text = data.get("text", "").strip() if not text or len(text) < 2: await websocket.send_json({ "type": "error", "message": "Xabar bo'sh. Iltimos, biror narsa yozing." }) return logger.info(f"💬 Matnli xabar: '{text}'") db.create_message(case_id, "user", text) stats["text_messages"] += 1 await process_text_input(websocket, case_id, text, is_voice=False) except Exception as e: logger.error(f"❌ Matnli xabar xatoligi: {e}", exc_info=True) await websocket.send_json({ "type": "error", "message": "Xatolik yuz berdi." }) async def handle_gps_location(websocket: WebSocket, case_id: str, data: Dict): """GPS lokatsiya qayta ishlash""" try: lat = data.get("latitude") lon = data.get("longitude") if not lat or not lon: await websocket.send_json({ "type": "error", "message": "GPS ma'lumot topilmadi" }) return logger.info(f"📍 GPS qabul qilindi: ({lat}, {lon})") # GPS ni saqlash va brigada topish await process_gps_and_brigade(websocket, case_id, lat, lon) except Exception as e: logger.error(f"❌ GPS xatoligi: {e}", exc_info=True) await websocket.send_json({ "type": "error", "message": "GPS xatolik" }) # ==================== TEXT PROCESSING (ASOSIY MANTIQ) ==================== async def process_text_input(websocket: WebSocket, case_id: str, prompt: str, is_voice: bool = False): """ Matn kiritishni qayta ishlash - ASOSIY FLOW Args: websocket: WebSocket ulanish case_id: Case ID (string) prompt: Bemorning matni is_voice: Ovozli xabarmi? (True/False) """ try: # Case ni olish current_case = db.get_case(case_id) if not current_case: logger.error(f"❌ Case topilmadi: {case_id}") await websocket.send_json({ "type": "error", "message": "Sessiya xatoligi. Iltimos, sahifani yangilang." }) return # ========== 1. ISM-FAMILIYA KUTILMOQDA? ========== if current_case.get('waiting_for_name_input'): await process_name_input(websocket, case_id, prompt) return # ========== 2. MANZIL ANIQLASHTIRILMOQDA? ========== if await handle_location_clarification(websocket, case_id, prompt, "voice" if is_voice else "text"): return # ========== 3. YANGI TAHLIL (GEMINI) ========== conversation_history = db.get_conversation_history(case_id) detected_lang = detect_language(prompt) logger.info(f"🧠 Gemini tahlil boshlandi...") full_prompt = f"{conversation_history}\nBemor: {prompt}" ai_analysis = get_gemini_response(full_prompt, stream=False) # JSON parse qilish if not ai_analysis or not isinstance(ai_analysis, dict): logger.error(f"❌ Gemini noto'g'ri javob: {ai_analysis}") await websocket.send_json({ "type": "error", "message": "AI xatolik" }) return risk_level = ai_analysis.get("risk_level", "yashil") response_text = ai_analysis.get("response_text", "Tushunmadim") language = ai_analysis.get("language", detected_lang) logger.info(f"📊 Risk darajasi: {risk_level.upper()}") # Database ga saqlash db.create_message(case_id, "ai", response_text) db.update_case(case_id, { "risk_level": risk_level, "language": language, "symptoms_text": ai_analysis.get("symptoms_extracted") }) # ========== RISK DARAJASIGA QARAB HARAKAT ========== if risk_level == "qizil": await handle_qizil_flow(websocket, case_id, ai_analysis) elif risk_level == "sariq": await handle_sariq_flow(websocket, case_id, ai_analysis) elif risk_level == "yashil": await handle_yashil_flow(websocket, case_id, ai_analysis) else: logger.warning(f"⚠️ Noma'lum risk level: {risk_level}") await send_ai_response(websocket, case_id, response_text, language) except Exception as e: logger.error(f"❌ process_text_input xatoligi: {e}", exc_info=True) await websocket.send_json({ "type": "error", "message": "Xatolik yuz berdi" }) # ==================== HELPER FUNCTION ==================== async def send_ai_response(websocket: WebSocket, case_id: str, text: str, language: str = "uzb"): """ AI javobini frontendga yuborish (text + audio) TUZATILGAN: TTS output_path to'g'ri yaratiladi Args: websocket: WebSocket ulanish case_id: Case ID text: Javob matni language: Javob tili ("uzb" | "eng" | "rus") """ try: # Database ga AI xabarini saqlash db.create_message(case_id, "ai", text) # 1. Text yuborish await websocket.send_json({ "type": "ai_response", "text": text }) # 2. TTS audio yaratish # ✅ TO'G'RI: output_path yaratish audio_filename = f"tts_{case_id}_{int(time.time())}.wav" audio_path = os.path.join("static/audio", audio_filename) logger.info(f"🎧 TTS uchun fayl yo'li: {audio_path}") # TTS chaqirish (to'g'ri parametrlar bilan) tts_success = synthesize_speech(text, audio_path, language) if tts_success and os.path.exists(audio_path): audio_url = f"/audio/{audio_filename}" await websocket.send_json({ "type": "audio_response", "audio_url": audio_url }) logger.info(f"📊 TTS audio yuborildi: {audio_url}") else: logger.warning("⚠️ TTS yaratilmadi, faqat text yuborildi") except Exception as e: logger.error(f"❌ send_ai_response xatoligi: {e}", exc_info=True) # app/api/routes.py - QISM 2 # 3 TA ASOSIY FLOW: QIZIL, SARIQ, YASHIL # ==================== 🔴 QIZIL FLOW (EMERGENCY) ==================== async def handle_qizil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict): """ QIZIL (Emergency) - TEZ YORDAM BRIGADA Flow: 1. Manzil so'rash (tuman + mahalla) 2. Fuzzy matching orqali koordinata topish 3. Brigada topish va jo'natish 4. ISM-FAMILIYA so'rash (brigadadan KEYIN!) """ try: logger.info(f"🔴 QIZIL HOLAT: Tez yordam jarayoni boshlandi") response_text = ai_analysis.get("response_text") language = ai_analysis.get("language", "uzb") address = ai_analysis.get("address_extracted") district = ai_analysis.get("district_extracted") # Case type ni belgilash db.update_case(case_id, { "type": "emergency", "risk_level": "qizil" }) # 1. MANZIL SO'RASH if not address or not district: logger.info("📍 Manzil yo'q, so'ralmoqda...") await send_ai_response(websocket, case_id, response_text, language) # Flag qo'yish - keyingi xabarda manzil kutiladi db.update_case(case_id, {"waiting_for_address": True}) return # 2. MANZILNI QAYTA ISHLASH logger.info(f"📍 Manzil aniqlandi: {address}") # Tuman fuzzy match district_match = find_district_fuzzy(district) if not district_match: logger.warning(f"⚠️ Tuman topilmadi: {district}") districts_list = get_all_districts() response = f"Tuman nomini aniq tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{districts_list}" await send_ai_response(websocket, case_id, response, language) return district_name = get_district_display_name(district_match) logger.info(f"✅ Tuman topildi: {district_name}") db.update_case(case_id, { "district": district_name, "selected_district": district_match }) # 3. MAHALLA SO'RASH # Bu qism location_clarification da amalga oshiriladi # Hozircha flag qo'yamiz db.update_case(case_id, { "waiting_for_mahalla_input": True, "mahalla_retry_count": 0 }) response = f"Tushundim, {district_name}. Iltimos, mahallangizni ayting." await send_ai_response(websocket, case_id, response, language) # Dispetcherga bildirishnoma await notify_dispatchers({ "type": "new_case", "case": db.get_case(case_id) }) except Exception as e: logger.error(f"❌ handle_qizil_flow xatoligi: {e}", exc_info=True) async def process_gps_and_brigade(websocket: WebSocket, case_id: str, lat: float, lon: float): """ GPS koordinatalariga qarab brigadani topish MUHIM: Brigadadan KEYIN ism-familiya so'raladi! """ try: logger.info(f"📍 GPS koordinatalar: ({lat:.6f}, {lon:.6f})") # GPS validatsiya if not validate_location_in_tashkent(lat, lon): logger.warning("⚠️ GPS Toshkent chegarasidan tashqarida") await websocket.send_json({ "type": "error", "message": "GPS manzil Toshkent chegarasidan tashqarida" }) return # Case ga saqlash db.update_case(case_id, { "gps_lat": lat, "gps_lon": lon, "geocoded_lat": lat, "geocoded_lon": lon, "gps_verified": True }) # Brigadani topish logger.info("🚑 Eng yaqin brigada qidirilmoqda...") nearest_brigade = find_nearest_brigade(lat, lon) if not nearest_brigade: logger.warning("⚠️ Brigada topilmadi") await websocket.send_json({ "type": "error", "message": "Hozirda barcha brigadalar band" }) return brigade_id = nearest_brigade['brigade_id'] brigade_name = nearest_brigade['brigade_name'] distance_km = nearest_brigade['distance_km'] # Brigadani tayinlash db.update_case(case_id, { "assigned_brigade_id": brigade_id, "assigned_brigade_name": brigade_name, "distance_to_brigade_km": distance_km, "status": "brigada_junatildi" }) logger.info(f"✅ Brigada tayinlandi: {brigade_name} ({distance_km:.2f} km)") # Bemorga xabar await websocket.send_json({ "type": "brigade_assigned", "brigade": { "id": brigade_id, "name": brigade_name, "distance_km": distance_km, "estimated_time_min": int(distance_km * 3) # 3 min/km } }) # ========== ENDI ISM-FAMILIYA SO'RASH ========== current_case = db.get_case(case_id) language = current_case.get("language", "uzb") if language == "eng": name_request = f"The ambulance is on its way, arriving in approximately {int(distance_km * 3)} minutes. Please tell me your full name." elif language == "rus": name_request = f"Скорая помощь в пути, прибудет примерно через {int(distance_km * 3)} минут. Пожалуйста, назовите ваше полное имя." else: name_request = f"Brigada yo'lda, taxminan {int(distance_km * 3)} daqiqada yetib keladi. Iltimos, to'liq ism-familiyangizni ayting." db.create_message(case_id, "ai", name_request) await send_ai_response(websocket, case_id, name_request, language) # Flag qo'yish db.update_case(case_id, {"waiting_for_name_input": True}) # Dispetcherga yangilanish await notify_dispatchers({ "type": "brigade_assigned", "case": db.get_case(case_id) }) except Exception as e: logger.error(f"❌ process_gps_and_brigade xatoligi: {e}", exc_info=True) # ==================== 🟡 SARIQ FLOW (UNCERTAIN) ==================== async def handle_sariq_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict): """ SARIQ (Uncertain) - NOANIQ, OPERATOR KERAK Flow: 1. Aniqlashtiruvchi savol berish 2. Counter ni oshirish (max 3) 3. 3 marta tushunmasa → Operator """ try: logger.info(f"🟡 SARIQ HOLAT: Noaniqlik") response_text = ai_analysis.get("response_text") language = ai_analysis.get("language", "uzb") uncertainty_reason = ai_analysis.get("uncertainty_reason") operator_needed = ai_analysis.get("operator_needed", False) current_case = db.get_case(case_id) current_attempts = current_case.get("uncertainty_attempts", 0) # Case type ni belgilash db.update_case(case_id, { "type": "uncertain", "risk_level": "sariq" }) # Operator kerakmi? if operator_needed or current_attempts >= MAX_UNCERTAINTY_ATTEMPTS: logger.info(f"🎧 OPERATOR KERAK! (Attempts: {current_attempts})") db.update_case(case_id, { "operator_needed": True, "uncertainty_reason": uncertainty_reason or f"AI {current_attempts} marta tushunolmadi", "status": "operator_kutilmoqda", "uncertainty_attempts": current_attempts + 1 }) # Bemorga xabar if language == "eng": operator_msg = "I'm having trouble understanding you. Connecting you to an operator who can help..." elif language == "rus": operator_msg = "Мне сложно вас понять. Соединяю с оператором, который вам поможет..." else: operator_msg = "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi..." await send_ai_response(websocket, case_id, operator_msg, language) # Dispetcherga operator kerakligi haqida xabar await notify_dispatchers({ "type": "operator_needed", "case": db.get_case(case_id) }) return # Hali operator kerak emas, aniqlashtirish logger.info(f"❓ Aniqlashtirish (Attempt {current_attempts + 1}/{MAX_UNCERTAINTY_ATTEMPTS})") db.update_case(case_id, { "uncertainty_attempts": current_attempts + 1, "uncertainty_reason": uncertainty_reason }) await send_ai_response(websocket, case_id, response_text, language) except Exception as e: logger.error(f"❌ handle_sariq_flow xatoligi: {e}", exc_info=True) # ==================== 🟢 YASHIL FLOW (CLINIC) ==================== async def handle_yashil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict): """ YASHIL (Non-urgent) - KLINIKA TAVSIYA Flow: 1. Bemorga xotirjamlik berish 2. Davlat yoki xususiy klinika taklif qilish 3. Bemor tanlasa, klinikalar ro'yxatini yuborish """ try: logger.info(f"🟢 YASHIL HOLAT: Klinika tavsiyasi") response_text = ai_analysis.get("response_text") language = ai_analysis.get("language", "uzb") symptoms = ai_analysis.get("symptoms_extracted") preferred_clinic_type = ai_analysis.get("preferred_clinic_type", "both") recommended_specialty = ai_analysis.get("recommended_specialty", "Terapiya") # Case type ni belgilash db.update_case(case_id, { "type": "public_clinic", # Default, keyin o'zgarishi mumkin "risk_level": "yashil", "symptoms_text": symptoms }) # 1. AI javobini yuborish (xotirjamlik + taklif) await send_ai_response(websocket, case_id, response_text, language) # 2. Klinikalarni qidirish logger.info(f"🏥 Klinikalar qidirilmoqda: {recommended_specialty}, type={preferred_clinic_type}") # Har ikki turdan ham topish if preferred_clinic_type == "both": davlat_clinics = db.recommend_clinics_by_symptoms( symptoms=symptoms, district=None, clinic_type="davlat" ) xususiy_clinics = db.recommend_clinics_by_symptoms( symptoms=symptoms, district=None, clinic_type="xususiy" ) # Formatlangan ro'yxat yaratish clinic_list_text = format_clinic_list( davlat_clinics.get('clinics', [])[:2], # Top 2 davlat xususiy_clinics.get('clinics', [])[:3], # Top 3 xususiy language ) else: # Faqat bitta turni ko'rsatish recommendation = db.recommend_clinics_by_symptoms( symptoms=symptoms, district=None, clinic_type=preferred_clinic_type ) clinic_list_text = format_clinic_list( recommendation.get('clinics', [])[:5] if preferred_clinic_type == "davlat" else [], recommendation.get('clinics', [])[:5] if preferred_clinic_type == "xususiy" else [], language ) # 3. Klinikalar ro'yxatini yuborish await websocket.send_json({ "type": "clinic_recommendation", "text": clinic_list_text }) db.create_message(case_id, "ai", clinic_list_text) # Dispetcherga xabar await notify_dispatchers({ "type": "clinic_case", "case": db.get_case(case_id) }) logger.info(f"✅ Klinikalar ro'yxati yuborildi") except Exception as e: logger.error(f"❌ handle_yashil_flow xatoligi: {e}", exc_info=True) def format_clinic_list(davlat_clinics: List[Dict], xususiy_clinics: List[Dict], language: str = "uzb") -> str: """ Klinikalar ro'yxatini formatlash Args: davlat_clinics: Davlat poliklinikalari xususiy_clinics: Xususiy klinikalar language: Til Returns: Formatlangan matn """ result = [] # Header if language == "eng": result.append("Here are my recommendations:\n") elif language == "rus": result.append("Вот мои рекомендации:\n") else: result.append("Mana sizga tavsiyalar:\n") # Davlat klinikalari if davlat_clinics: if language == "eng": result.append("\n🏥 PUBLIC CLINICS (Free):\n") elif language == "rus": result.append("\n🏥 ГОСУДАРСТВЕННЫЕ ПОЛИКЛИНИКИ (Бесплатно):\n") else: result.append("\n🏥 DAVLAT POLIKLINIKALARI (Bepul):\n") for idx, clinic in enumerate(davlat_clinics, 1): result.append(f"\n{idx}️⃣ {clinic['name']}") result.append(f" 📍 {clinic['address']}") result.append(f" 📞 {clinic['phone']}") result.append(f" ⏰ {clinic['working_hours']}") result.append(f" ⭐ {clinic['rating']}/5.0") # Xususiy klinikalar if xususiy_clinics: if language == "eng": result.append("\n\n🏥 PRIVATE CLINICS:\n") elif language == "rus": result.append("\n\n🏥 ЧАСТНЫЕ КЛИНИКИ:\n") else: result.append("\n\n🏥 XUSUSIY KLINIKALAR:\n") for idx, clinic in enumerate(xususiy_clinics, 1): result.append(f"\n{idx}️⃣ {clinic['name']}") result.append(f" 📍 {clinic['address']}") result.append(f" 📞 {clinic['phone']}") result.append(f" ⏰ {clinic['working_hours']}") result.append(f" 💰 {clinic['price_range']}") result.append(f" ⭐ {clinic['rating']}/5.0") return "\n".join(result) # ==================== HELPER FUNCTIONS ==================== async def process_name_input(websocket: WebSocket, case_id: str, name_text: str): """ Ism-familiyani qayta ishlash Bu funksiya brigadadan KEYIN chaqiriladi """ try: logger.info(f"👤 Ism-familiya qabul qilindi: '{name_text}'") current_case = db.get_case(case_id) language = current_case.get("language", "uzb") # Ism-familiyani saqlash db.update_case(case_id, { "patient_full_name": name_text, "waiting_for_name_input": False }) # Bemor tarixini tekshirish patient_history = db.get_patient_statistics(name_text) if patient_history and patient_history.get("total_cases", 0) > 0: previous_count = patient_history.get("total_cases") logger.info(f"📋 Bemor tarixi topildi: {previous_count} ta oldingi murojat") db.update_case(case_id, { "previous_cases_count": previous_count }) # Tasdiq xabari if language == "eng": confirmation = f"Thank you, {name_text}. The ambulance will arrive shortly. Please stay calm." elif language == "rus": confirmation = f"Спасибо, {name_text}. Скорая помощь скоро прибудет. Пожалуйста, сохраняйте спокойствие." else: confirmation = f"Rahmat, {name_text}. Brigada tez orada yetib keladi. Iltimos, xotirjam bo'ling." await send_ai_response(websocket, case_id, confirmation, language) # Dispetcherga yangilanish await notify_dispatchers({ "type": "name_received", "case": db.get_case(case_id) }) except Exception as e: logger.error(f"❌ process_name_input xatoligi: {e}", exc_info=True) async def handle_location_clarification(websocket: WebSocket, case_id: str, user_input: str, input_type: str) -> bool: """ Manzilni aniqlashtirish (mahalla) Returns: True - agar mahalla kutilgan bo'lsa va qayta ishlandi False - agar mahalla kutilmagan """ try: current_case = db.get_case(case_id) if not current_case.get("waiting_for_mahalla_input"): return False logger.info(f"🏘️ Mahalla aniqlashtirilmoqda: '{user_input}'") district_id = current_case.get("selected_district") district_name = current_case.get("district") language = current_case.get("language", "uzb") if not district_id: logger.error("❌ District ID topilmadi") return False # Mahalla fuzzy match mahalla_match = find_mahalla_fuzzy(district_name, user_input, threshold=0.35) if mahalla_match: mahalla_full_name = get_mahalla_display_name(mahalla_match) logger.info(f"✅ Mahalla topildi: {mahalla_full_name}") # Mahalla koordinatalarini olish mahalla_coords = get_mahalla_coordinates(district_name, mahalla_match) if mahalla_coords: db.update_case(case_id, { "selected_mahalla": mahalla_full_name, "mahalla_lat": mahalla_coords['lat'], "mahalla_lon": mahalla_coords['lon'], "geocoded_lat": mahalla_coords['lat'], "geocoded_lon": mahalla_coords['lon'], "waiting_for_mahalla_input": False, "mahalla_retry_count": 0 }) # Brigadani topish await process_gps_and_brigade( websocket, case_id, mahalla_coords['lat'], mahalla_coords['lon'] ) return True # Mahalla topilmadi retry_count = current_case.get("mahalla_retry_count", 0) + 1 if retry_count >= 3: # 3 marta topilmasa, faqat tuman bilan davom etamiz logger.warning("⚠️ Mahalla 3 marta topilmadi, tuman markazidan foydalaniladi") district_gps = get_gps_for_district(district_id) if district_gps: db.update_case(case_id, { "geocoded_lat": district_gps['lat'], "geocoded_lon": district_gps['lon'], "waiting_for_mahalla_input": False, "mahalla_retry_count": 0 }) await process_gps_and_brigade( websocket, case_id, district_gps['lat'], district_gps['lon'] ) return True # Qayta so'rash db.update_case(case_id, {"mahalla_retry_count": retry_count}) mahallas_list = format_mahallas_list(get_mahallas_by_district(district_name)) response = f"Mahalla nomini tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{mahallas_list}" await send_ai_response(websocket, case_id, response, language) return True except Exception as e: logger.error(f"❌ handle_location_clarification xatoligi: {e}", exc_info=True) return False # ==================== PERIODIC CLEANUP ==================== async def periodic_cleanup(): """Eski audio fayllarni tozalash (har 1 soatda)""" while True: try: await asyncio.sleep(3600) # 1 soat logger.info("🧹 Periodic cleanup boshlandi...") audio_dir = "static/audio" if os.path.exists(audio_dir): current_time = time.time() for filename in os.listdir(audio_dir): file_path = os.path.join(audio_dir, filename) if os.path.isfile(file_path): if current_time - os.path.getmtime(file_path) > 3600: # 1 soat os.remove(file_path) logger.info(f"🗑️ Eski fayl o'chirildi: {filename}") except Exception as e: logger.error(f"❌ Periodic cleanup xatoligi: {e}") @router.on_event("startup") async def startup_event(): """Server ishga tushganda""" asyncio.create_task(periodic_cleanup()) logger.info("🚀 Periodic cleanup task ishga tushdi") # ==================== CASE MANAGEMENT APIs ==================== @router.get("/cases") async def get_all_cases(status: Optional[str] = None): """Barcha caselarni olish""" try: cases = db.get_all_cases(status=status) return cases except Exception as e: logger.error(f"❌ Cases olishda xatolik: {e}") raise HTTPException(status_code=500, detail="Server xatoligi") @router.get("/cases/{case_id}") async def get_case(case_id: str): """Bitta case ma'lumotlarini olish""" case = db.get_case(case_id) if not case: raise HTTPException(status_code=404, detail="Case topilmadi") return case @router.patch("/cases/{case_id}") async def update_case(case_id: str, updates: CaseUpdate): """Case ni yangilash""" update_data = updates.dict(exclude_unset=True) success = db.update_case(case_id, update_data) if not success: raise HTTPException(status_code=404, detail="Case topilmadi") updated_case = db.get_case(case_id) # Dispetcherlarga yangilanish await notify_dispatchers({ "type": "case_updated", "case": updated_case }) return updated_case