Spaces:
Paused
Paused
| # 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 ==================== | |
| 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() | |
| }) | |
| 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 | |
| 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("/tmp/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}") | |
| async def startup_event(): | |
| """Server ishga tushganda""" | |
| asyncio.create_task(periodic_cleanup()) | |
| logger.info("π Periodic cleanup task ishga tushdi") | |
| # ==================== CASE MANAGEMENT APIs ==================== | |
| 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") | |
| 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 | |
| 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 | |