Help_Me_3 / app /api /routes.py
giyos1212's picture
Update app/api/routes.py
c8a87c8 verified
# 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("/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}")
@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