Spaces:
Paused
Paused
Upload 72 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +1 -0
- .gitattributes +7 -0
- .gitignore +21 -0
- README.md +115 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/api/__pycache__/dispatcher_routes.cpython-313.pyc +0 -0
- app/api/__pycache__/routes.cpython-313.pyc +0 -0
- app/api/dispatcher_routes.py +532 -0
- app/api/routes.py +1125 -0
- app/core/__pycache__/auth.cpython-313.pyc +0 -0
- app/core/__pycache__/config.cpython-313.pyc +0 -0
- app/core/__pycache__/connections.cpython-313.pyc +0 -0
- app/core/__pycache__/database.cpython-313.pyc +0 -0
- app/core/auth.py +102 -0
- app/core/config.py +283 -0
- app/core/connections.py +10 -0
- app/core/database.py +446 -0
- app/models/__pycache__/schemas.cpython-313.pyc +0 -0
- app/models/schemas.py +287 -0
- app/services/__pycache__/brigade_matcher.cpython-313.pyc +0 -0
- app/services/__pycache__/brigade_simulator.cpython-313.pyc +0 -0
- app/services/__pycache__/geocoding.cpython-313.pyc +0 -0
- app/services/__pycache__/location_validator.cpython-313.pyc +0 -0
- app/services/__pycache__/models.cpython-313.pyc +0 -0
- app/services/brigade_matcher.py +270 -0
- app/services/brigade_simulator.py +199 -0
- app/services/geocoding.py +262 -0
- app/services/location_validator.py +330 -0
- app/services/models.py +682 -0
- app/utils/__pycache__/demo_gps.cpython-313.pyc +0 -0
- app/utils/__pycache__/district_matcher.cpython-313.pyc +0 -0
- app/utils/__pycache__/mahalla_matcher.cpython-313.pyc +0 -0
- app/utils/__pycache__/translit.cpython-313.pyc +0 -0
- app/utils/demo_gps.py +214 -0
- app/utils/district_matcher.py +205 -0
- app/utils/mahalla_matcher.py +309 -0
- app/utils/translit.py +130 -0
- data/brigades.json +88 -0
- data/cases.json +339 -0
- data/clinics.json +461 -0
- data/dispatchers.json +11 -0
- data/doctors.json +328 -0
- data/doctors_photos.json +319 -0
- data/locations.json +28 -0
- data/locations_db.json +0 -0
- data/messages.json +1275 -0
- data/patient_history.json +63 -0
- data/tashkent_districts.json +76 -0
- data/uploads/doc_001.jpg +0 -0
.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
GEMINI_API_KEY="AIzaSyC1iBX1w_bn8w1c6Ask2Gu9og8m_L1Vs9A"
|
.gitattributes
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
static/audio/tts_case_006_1760805862.wav filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
static/audio/tts_case_007_1760805883.wav filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
static/audio/tts_case_007_1760805903.wav filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
static/audio/tts_case_007_1760805921.wav filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
static/audio/tts_case_007_1760805938.wav filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
static/audio/tts_case_010_1760819580.wav filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
static/dispatcher/audio/alert.mp3 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python virtual muhiti (o'rnatilgan kutubxonalar)
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
|
| 5 |
+
# Python tomonidan yaratiladigan vaqtinchalik fayllar
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.pyd
|
| 10 |
+
|
| 11 |
+
# API kalitlari va shaxsiy sozlamalar fayli (agar mavjud bo'lsa)
|
| 12 |
+
.env
|
| 13 |
+
|
| 14 |
+
# Katta hajmli AI modellari (BULARNI ASLO YUKLAMANG!)
|
| 15 |
+
local_models/
|
| 16 |
+
|
| 17 |
+
# Dastur ishlash jarayonida yaratiladigan vaqtinchalik fayllar
|
| 18 |
+
# Ovozli fayllar va boshqa yuklanmalar
|
| 19 |
+
static/audio/
|
| 20 |
+
static/uploads/
|
| 21 |
+
data/uploads/
|
README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<<<<<<< HEAD
|
| 2 |
+
---
|
| 3 |
+
title: AI Operator
|
| 4 |
+
emoji: 🦀
|
| 5 |
+
colorFrom: pink
|
| 6 |
+
colorTo: yellow
|
| 7 |
+
sdk: docker
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: 'Agentic AI which is specialised answering emergency calls '
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 14 |
+
=======
|
| 15 |
+
Help.me - AI-Powered Emergency Medical Assistance System
|
| 16 |
+
|
| 17 |
+
"Help.me" is an AI-based platform designed to automate and accelerate communication between patients in need of urgent medical care and dispatch operators. The system receives voice messages from patients, analyzes their condition using artificial intelligence, and immediately transmits the information to a dispatcher dashboard for appropriate action.
|
| 18 |
+
|
| 19 |
+
🚀 Key Features
|
| 20 |
+
|
| 21 |
+
For Patients (Voice-First Interface):
|
| 22 |
+
|
| 23 |
+
Voice Communication: Patients can report their symptoms simply by speaking.
|
| 24 |
+
|
| 25 |
+
Multilingual System: The AI can communicate with patients in Uzbek, Russian, and English.
|
| 26 |
+
|
| 27 |
+
Smart Recommendations: If a patient's condition is assessed as "Green" (non-urgent), the system recommends public polyclinics or private clinics based on their symptoms.
|
| 28 |
+
|
| 29 |
+
Real-time Response: The AI analyzes the request instantly and provides a voice response.
|
| 30 |
+
|
| 31 |
+
Simplified Interface: The interface is designed to be as simple as possible and voice-focused to avoid distracting the patient in stressful situations.
|
| 32 |
+
|
| 33 |
+
For Dispatchers (Monitoring Dashboard):
|
| 34 |
+
|
| 35 |
+
Real-time Monitoring: All incoming cases are displayed live on the dashboard.
|
| 36 |
+
|
| 37 |
+
Risk Triage: The AI categorizes each case into "Red" (emergency), "Yellow" (uncertain), or "Green" (clinic referral) risk levels.
|
| 38 |
+
|
| 39 |
+
Interactive Map: The locations of all ambulance brigades and clinics are tracked on a map in real time.
|
| 40 |
+
|
| 41 |
+
Statistics & Analytics: Statistical data on cases and brigades are visualized in charts.
|
| 42 |
+
|
| 43 |
+
🧠 AI Models Used
|
| 44 |
+
|
| 45 |
+
Our system relies on three core AI models:
|
| 46 |
+
|
| 47 |
+
Speech-to-Text (STT):
|
| 48 |
+
|
| 49 |
+
Model: A custom model fine-tuned on top of OpenAI Whisper (medium).
|
| 50 |
+
|
| 51 |
+
Dataset: The model was trained on several datasets tailored to the conditions of Uzbekistan. This includes audio recordings in the Tashkent dialect, standard literary language, and additionally, the Khorezm dialect. This ensures high accuracy in understanding the speech of patients from various regions of the country.
|
| 52 |
+
|
| 53 |
+
Logic and Response Generation (LLM):
|
| 54 |
+
|
| 55 |
+
Model: Google Gemini Flash.
|
| 56 |
+
|
| 57 |
+
Task: To analyze the transcribed complaints from the patient, determine the severity of the situation (risk level), and formulate the text of the response. The model is guided by a strict set of rules and action sequences provided via a SYSTEM_INSTRUCTION.
|
| 58 |
+
|
| 59 |
+
Text-to-Speech (TTS):
|
| 60 |
+
|
| 61 |
+
Model: Facebook MMS (Massively Multilingual Speech).
|
| 62 |
+
|
| 63 |
+
Task: To synthesize the AI-generated response text into a natural-sounding human voice. The system uses separate TTS models for Uzbek and English.
|
| 64 |
+
|
| 65 |
+
🛠️ Technology Stack
|
| 66 |
+
|
| 67 |
+
Backend: FastAPI (Python)
|
| 68 |
+
|
| 69 |
+
Real-time Communication: WebSockets
|
| 70 |
+
|
| 71 |
+
Database: JSON-based flat files (for MVP)
|
| 72 |
+
|
| 73 |
+
Frontend: HTML, CSS, JavaScript (Vanilla JS)
|
| 74 |
+
|
| 75 |
+
Map: Leaflet.js, Charts: Chart.js
|
| 76 |
+
|
| 77 |
+
⚙️ Getting Started
|
| 78 |
+
|
| 79 |
+
1. Prerequisites:
|
| 80 |
+
|
| 81 |
+
Python 3.9+
|
| 82 |
+
|
| 83 |
+
FFmpeg (must be installed on the system to process audio files)
|
| 84 |
+
|
| 85 |
+
git
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
3. Set Up Virtual Environment and Install Dependencies:
|
| 90 |
+
|
| 91 |
+
# Create a virtual environment
|
| 92 |
+
python -m venv venv
|
| 93 |
+
|
| 94 |
+
# Activate it (Windows)
|
| 95 |
+
venv\Scripts\activate
|
| 96 |
+
|
| 97 |
+
# Activate it (MacOS/Linux)
|
| 98 |
+
source venv/bin/activate
|
| 99 |
+
|
| 100 |
+
# Install the required libraries
|
| 101 |
+
pip install -r requirements.txt
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
4. ‼️ IMPORTANT: Download AI Models
|
| 105 |
+
|
| 106 |
+
This repository DOES NOT include the large AI models in the local_models directory. To run the system, you must download them separately and place them in the project folder.
|
| 107 |
+
|
| 108 |
+
Note for the judges: Due to their large size (several GB), it was not feasible to upload the models to GitHub. The repository contains only the project's source code.
|
| 109 |
+
|
| 110 |
+
5. Run the Application:
|
| 111 |
+
|
| 112 |
+
uvicorn app.main:app --reload
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
>>>>>>> 0f59686 (Loyiha tayyor: Help.me AI tizimi)
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (143 Bytes). View file
|
|
|
app/api/__pycache__/dispatcher_routes.cpython-313.pyc
ADDED
|
Binary file (19.8 kB). View file
|
|
|
app/api/__pycache__/routes.cpython-313.pyc
ADDED
|
Binary file (43.7 kB). View file
|
|
|
app/api/dispatcher_routes.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/dispatcher_routes.py - TO'LIQ YANGILANGAN (CLINIC/DOCTOR APIlar BILAN)
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Dispatcher API endpoints
|
| 5 |
+
AUTH BUTUNLAY O'CHIRILDI - Parolsiz kirish
|
| 6 |
+
YANGILANISHLAR:
|
| 7 |
+
- Clinic APIlar qo'shildi
|
| 8 |
+
- Doctor APIlar qo'shildi
|
| 9 |
+
- Xarita uchun endpointlar
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Query
|
| 13 |
+
from fastapi.responses import JSONResponse
|
| 14 |
+
from typing import Optional, List, Set
|
| 15 |
+
import logging
|
| 16 |
+
import json
|
| 17 |
+
|
| 18 |
+
from app.core.database import db
|
| 19 |
+
from app.models.schemas import (
|
| 20 |
+
CaseResponse, CaseUpdate, MessageResponse,
|
| 21 |
+
SuccessResponse, ErrorResponse,
|
| 22 |
+
BrigadeLocation, PatientHistoryResponse,
|
| 23 |
+
ClinicResponse, ClinicStatistics,
|
| 24 |
+
DoctorResponse
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
from app.core.connections import dispatcher_connections, active_connections
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
router = APIRouter(prefix="/api", tags=["Dispatcher"])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ==================== CASES APIs ====================
|
| 35 |
+
|
| 36 |
+
@router.get("/cases")
|
| 37 |
+
async def get_all_cases(status: Optional[str] = None):
|
| 38 |
+
"""
|
| 39 |
+
Barcha caselarni olish
|
| 40 |
+
|
| 41 |
+
Query params:
|
| 42 |
+
status: "yangi" | "qabul_qilindi" | "brigada_junatildi" | "klinika_tavsiya_qilindi" | "operator_kutilmoqda"
|
| 43 |
+
"""
|
| 44 |
+
try:
|
| 45 |
+
cases = db.get_all_cases(status=status)
|
| 46 |
+
logger.info(f"📊 {len(cases)} ta case qaytarildi")
|
| 47 |
+
return cases
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.error(f"❌ Cases olishda xatolik: {e}")
|
| 50 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@router.get("/cases/{case_id}")
|
| 54 |
+
async def get_case_by_id(case_id: str):
|
| 55 |
+
"""
|
| 56 |
+
Bitta case ma'lumotlari
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
case_id: Case ID
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Case ma'lumotlari
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
case = db.get_case(case_id)
|
| 66 |
+
|
| 67 |
+
if not case:
|
| 68 |
+
raise HTTPException(status_code=404, detail="Case topilmadi")
|
| 69 |
+
|
| 70 |
+
return case
|
| 71 |
+
except HTTPException:
|
| 72 |
+
raise
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"❌ Case olishda xatolik: {e}")
|
| 75 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@router.patch("/cases/{case_id}")
|
| 79 |
+
async def update_case_by_id(case_id: str, updates: CaseUpdate):
|
| 80 |
+
"""
|
| 81 |
+
Case ni yangilash
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
case_id: Case ID
|
| 85 |
+
updates: Yangilanish ma'lumotlari
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
Yangilangan case
|
| 89 |
+
"""
|
| 90 |
+
try:
|
| 91 |
+
success = db.update_case(case_id, updates.dict(exclude_unset=True))
|
| 92 |
+
|
| 93 |
+
if not success:
|
| 94 |
+
raise HTTPException(status_code=404, detail="Case topilmadi")
|
| 95 |
+
|
| 96 |
+
# Yangilangan caseni olish
|
| 97 |
+
updated_case = db.get_case(case_id)
|
| 98 |
+
|
| 99 |
+
# WebSocket orqali xabar yuborish
|
| 100 |
+
await notify_dispatchers({
|
| 101 |
+
"type": "case_updated",
|
| 102 |
+
"case": updated_case
|
| 103 |
+
})
|
| 104 |
+
|
| 105 |
+
return updated_case
|
| 106 |
+
|
| 107 |
+
except HTTPException:
|
| 108 |
+
raise
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"❌ Case yangilashda xatolik: {e}")
|
| 111 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@router.get("/cases/{case_id}/messages")
|
| 115 |
+
async def get_case_messages(case_id: str):
|
| 116 |
+
"""
|
| 117 |
+
Case ning barcha xabarlarini olish
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
case_id: Case ID
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
List of messages
|
| 124 |
+
"""
|
| 125 |
+
try:
|
| 126 |
+
messages = db.get_messages(case_id)
|
| 127 |
+
logger.info(f"💬 {len(messages)} ta xabar qaytarildi")
|
| 128 |
+
return messages
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.error(f"❌ Messages olishda xatolik: {e}")
|
| 131 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ==================== PATIENT HISTORY ====================
|
| 135 |
+
|
| 136 |
+
@router.get("/patient/history/{full_name}")
|
| 137 |
+
async def get_patient_history(full_name: str):
|
| 138 |
+
"""
|
| 139 |
+
Bemorning oldingi murojatlari tarixini olish
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
full_name: Bemorning to'liq ismi
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
PatientHistoryResponse
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
history = db.get_patient_history(full_name)
|
| 149 |
+
|
| 150 |
+
if not history:
|
| 151 |
+
raise HTTPException(status_code=404, detail="Bemor topilmadi")
|
| 152 |
+
|
| 153 |
+
logger.info(f"📋 Bemor tarixi qaytarildi: {full_name}")
|
| 154 |
+
return history
|
| 155 |
+
|
| 156 |
+
except HTTPException:
|
| 157 |
+
raise
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"❌ Patient history olishda xatolik: {e}")
|
| 160 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ==================== 🆕 CLINICS APIs ====================
|
| 164 |
+
|
| 165 |
+
@router.get("/clinics")
|
| 166 |
+
async def get_all_clinics_endpoint(
|
| 167 |
+
type: Optional[str] = Query(None, description="davlat | xususiy"),
|
| 168 |
+
district: Optional[str] = Query(None, description="Tuman nomi"),
|
| 169 |
+
specialty: Optional[str] = Query(None, description="Mutaxassislik"),
|
| 170 |
+
min_rating: Optional[float] = Query(None, description="Minimal rating")
|
| 171 |
+
):
|
| 172 |
+
"""
|
| 173 |
+
Barcha klinikalarni olish (xarita va ro'yxat uchun)
|
| 174 |
+
|
| 175 |
+
Query parametrlari:
|
| 176 |
+
- type: "davlat" | "xususiy" (optional)
|
| 177 |
+
- district: "Chilonzor tumani" (optional)
|
| 178 |
+
- specialty: "Kardiologiya" (optional)
|
| 179 |
+
- min_rating: 4.0 (optional)
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
List[ClinicResponse]
|
| 183 |
+
|
| 184 |
+
Example:
|
| 185 |
+
GET /api/clinics?type=xususiy&district=Chilonzor tumani
|
| 186 |
+
"""
|
| 187 |
+
try:
|
| 188 |
+
# Agar specialty yoki rating filtrlari bo'lsa, search ishlatamiz
|
| 189 |
+
if specialty or min_rating:
|
| 190 |
+
clinics = db.search_clinics(
|
| 191 |
+
specialty=specialty,
|
| 192 |
+
min_rating=min_rating or 0.0
|
| 193 |
+
)
|
| 194 |
+
else:
|
| 195 |
+
# Oddiy type va district filtrlari
|
| 196 |
+
clinics = db.get_all_clinics(
|
| 197 |
+
clinic_type=type,
|
| 198 |
+
district=district
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
logger.info(f"📋 {len(clinics)} ta klinika qaytarildi")
|
| 202 |
+
return clinics
|
| 203 |
+
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.error(f"❌ Clinics olishda xatolik: {e}")
|
| 206 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
@router.get("/clinics/{clinic_id}")
|
| 210 |
+
async def get_clinic_by_id_endpoint(clinic_id: str):
|
| 211 |
+
"""
|
| 212 |
+
Bitta klinikaning batafsil ma'lumotlari
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
clinic_id: "clinic_001"
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
ClinicResponse (doktorlar bilan birga)
|
| 219 |
+
"""
|
| 220 |
+
try:
|
| 221 |
+
clinic = db.get_clinic_by_id(clinic_id)
|
| 222 |
+
|
| 223 |
+
if not clinic:
|
| 224 |
+
raise HTTPException(status_code=404, detail="Klinika topilmadi")
|
| 225 |
+
|
| 226 |
+
# Klinikadagi doktorlarni ham qo'shamiz
|
| 227 |
+
doctors = db.get_doctors_by_clinic(clinic_id)
|
| 228 |
+
clinic['doctors'] = doctors
|
| 229 |
+
|
| 230 |
+
logger.info(f"✅ Klinika qaytarildi: {clinic.get('name')}")
|
| 231 |
+
return clinic
|
| 232 |
+
|
| 233 |
+
except HTTPException:
|
| 234 |
+
raise
|
| 235 |
+
except Exception as e:
|
| 236 |
+
logger.error(f"❌ Clinic olishda xatolik: {e}")
|
| 237 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@router.get("/clinics/statistics/summary")
|
| 241 |
+
async def get_clinic_statistics_endpoint():
|
| 242 |
+
"""
|
| 243 |
+
Klinikalar statistikasi (dashboard uchun)
|
| 244 |
+
|
| 245 |
+
Returns:
|
| 246 |
+
ClinicStatistics: {
|
| 247 |
+
"total": 15,
|
| 248 |
+
"davlat": 8,
|
| 249 |
+
"xususiy": 7,
|
| 250 |
+
"by_district": {...}
|
| 251 |
+
}
|
| 252 |
+
"""
|
| 253 |
+
try:
|
| 254 |
+
stats = db.get_clinic_statistics()
|
| 255 |
+
return stats
|
| 256 |
+
except Exception as e:
|
| 257 |
+
logger.error(f"❌ Clinic statistics xatoligi: {e}")
|
| 258 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# ==================== 🆕 DOCTORS APIs ====================
|
| 262 |
+
|
| 263 |
+
@router.get("/doctors")
|
| 264 |
+
async def get_all_doctors_endpoint(
|
| 265 |
+
clinic_id: Optional[str] = Query(None, description="Klinika ID"),
|
| 266 |
+
specialty: Optional[str] = Query(None, description="Mutaxassislik")
|
| 267 |
+
):
|
| 268 |
+
"""
|
| 269 |
+
Barcha doktorlarni olish
|
| 270 |
+
|
| 271 |
+
Query parametrlari:
|
| 272 |
+
- clinic_id: "clinic_001" (optional)
|
| 273 |
+
- specialty: "Kardiolog" (optional)
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
List[DoctorResponse]
|
| 277 |
+
"""
|
| 278 |
+
try:
|
| 279 |
+
doctors = db.get_all_doctors(
|
| 280 |
+
clinic_id=clinic_id,
|
| 281 |
+
specialty=specialty
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
logger.info(f"👨⚕️ {len(doctors)} ta doktor qaytarildi")
|
| 285 |
+
return doctors
|
| 286 |
+
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"❌ Doctors olishda xatolik: {e}")
|
| 289 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@router.get("/doctors/clinic/{clinic_id}")
|
| 293 |
+
async def get_doctors_by_clinic_endpoint(clinic_id: str):
|
| 294 |
+
"""
|
| 295 |
+
Klinikadagi barcha doktorlar
|
| 296 |
+
|
| 297 |
+
Args:
|
| 298 |
+
clinic_id: "clinic_001"
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
List[DoctorResponse]
|
| 302 |
+
"""
|
| 303 |
+
try:
|
| 304 |
+
# Avval klinikani tekshiramiz
|
| 305 |
+
clinic = db.get_clinic_by_id(clinic_id)
|
| 306 |
+
if not clinic:
|
| 307 |
+
raise HTTPException(status_code=404, detail="Klinika topilmadi")
|
| 308 |
+
|
| 309 |
+
doctors = db.get_doctors_by_clinic(clinic_id)
|
| 310 |
+
|
| 311 |
+
logger.info(f"👨⚕️ {len(doctors)} ta doktor qaytarildi ({clinic.get('name')})")
|
| 312 |
+
return doctors
|
| 313 |
+
|
| 314 |
+
except HTTPException:
|
| 315 |
+
raise
|
| 316 |
+
except Exception as e:
|
| 317 |
+
logger.error(f"❌ Doctors olishda xatolik: {e}")
|
| 318 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
@router.get("/doctors/{doctor_id}")
|
| 322 |
+
async def get_doctor_by_id_endpoint(doctor_id: str):
|
| 323 |
+
"""
|
| 324 |
+
Bitta doktor ma'lumotlari
|
| 325 |
+
|
| 326 |
+
Args:
|
| 327 |
+
doctor_id: "doc_001"
|
| 328 |
+
|
| 329 |
+
Returns:
|
| 330 |
+
DoctorResponse
|
| 331 |
+
"""
|
| 332 |
+
try:
|
| 333 |
+
doctor = db.get_doctor_by_id(doctor_id)
|
| 334 |
+
|
| 335 |
+
if not doctor:
|
| 336 |
+
raise HTTPException(status_code=404, detail="Doktor topilmadi")
|
| 337 |
+
|
| 338 |
+
logger.info(f"✅ Doktor qaytarildi: {doctor.get('full_name')}")
|
| 339 |
+
return doctor
|
| 340 |
+
|
| 341 |
+
except HTTPException:
|
| 342 |
+
raise
|
| 343 |
+
except Exception as e:
|
| 344 |
+
logger.error(f"❌ Doctor olishda xatolik: {e}")
|
| 345 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# ==================== BRIGADES APIs ====================
|
| 349 |
+
|
| 350 |
+
@router.get("/brigades")
|
| 351 |
+
async def get_all_brigades():
|
| 352 |
+
"""
|
| 353 |
+
Barcha brigadalarni olish
|
| 354 |
+
|
| 355 |
+
Returns:
|
| 356 |
+
List[BrigadeLocation]
|
| 357 |
+
"""
|
| 358 |
+
try:
|
| 359 |
+
from app.services.brigade_matcher import load_brigades
|
| 360 |
+
brigades = load_brigades()
|
| 361 |
+
|
| 362 |
+
logger.info(f"🚑 {len(brigades)} ta brigada qaytarildi")
|
| 363 |
+
return brigades
|
| 364 |
+
|
| 365 |
+
except Exception as e:
|
| 366 |
+
logger.error(f"❌ Brigadalarni olishda xatolik: {e}")
|
| 367 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ==================== WEBSOCKET ====================
|
| 371 |
+
|
| 372 |
+
@router.websocket("/ws/dispatcher")
|
| 373 |
+
async def websocket_dispatcher(websocket: WebSocket):
|
| 374 |
+
"""
|
| 375 |
+
Dispatcher uchun WebSocket (Real-time yangilanishlar)
|
| 376 |
+
"""
|
| 377 |
+
await websocket.accept()
|
| 378 |
+
dispatcher_connections.add(websocket)
|
| 379 |
+
|
| 380 |
+
logger.info(f"🔗 Dispatcher WebSocket ulanish o'rnatildi")
|
| 381 |
+
|
| 382 |
+
try:
|
| 383 |
+
while True:
|
| 384 |
+
# Faqat ping-pong uchun
|
| 385 |
+
data = await websocket.receive_text()
|
| 386 |
+
|
| 387 |
+
if data == "ping":
|
| 388 |
+
await websocket.send_text("pong")
|
| 389 |
+
|
| 390 |
+
except WebSocketDisconnect:
|
| 391 |
+
dispatcher_connections.discard(websocket)
|
| 392 |
+
logger.info(f"🔗 Dispatcher WebSocket uzildi")
|
| 393 |
+
except Exception as e:
|
| 394 |
+
logger.error(f"❌ Dispatcher WebSocket xatoligi: {e}")
|
| 395 |
+
dispatcher_connections.discard(websocket)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
async def notify_dispatchers(message: dict):
|
| 399 |
+
"""
|
| 400 |
+
Barcha dispatcherlarga xabar yuborish
|
| 401 |
+
|
| 402 |
+
Args:
|
| 403 |
+
message: Yuborilishi kerak bo'lgan xabar
|
| 404 |
+
"""
|
| 405 |
+
if not dispatcher_connections:
|
| 406 |
+
return
|
| 407 |
+
|
| 408 |
+
disconnected = set()
|
| 409 |
+
|
| 410 |
+
for connection in dispatcher_connections:
|
| 411 |
+
try:
|
| 412 |
+
await connection.send_json(message)
|
| 413 |
+
logger.info(f"📤 Dispatcherga xabar yuborildi: {message.get('type')}")
|
| 414 |
+
except Exception as e:
|
| 415 |
+
logger.error(f"❌ Dispatcherga xabar yuborishda xatolik: {e}")
|
| 416 |
+
disconnected.add(connection)
|
| 417 |
+
|
| 418 |
+
# Uzilgan connectionlarni o'chirish
|
| 419 |
+
for conn in disconnected:
|
| 420 |
+
dispatcher_connections.discard(conn)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
# ==================== STATISTICS ====================
|
| 424 |
+
|
| 425 |
+
@router.get("/statistics")
|
| 426 |
+
async def get_dashboard_statistics():
|
| 427 |
+
"""
|
| 428 |
+
Dashboard uchun umumiy statistika
|
| 429 |
+
|
| 430 |
+
Returns:
|
| 431 |
+
{
|
| 432 |
+
"total_cases": 10,
|
| 433 |
+
"emergency": 3,
|
| 434 |
+
"clinic": 5,
|
| 435 |
+
"uncertain": 2,
|
| 436 |
+
"total_clinics": 8,
|
| 437 |
+
"total_doctors": 12
|
| 438 |
+
}
|
| 439 |
+
"""
|
| 440 |
+
try:
|
| 441 |
+
cases = db.get_all_cases()
|
| 442 |
+
clinics = db.get_all_clinics()
|
| 443 |
+
doctors = db.get_all_doctors()
|
| 444 |
+
|
| 445 |
+
stats = {
|
| 446 |
+
"total_cases": len(cases),
|
| 447 |
+
"emergency": len([c for c in cases if c.get('type') == 'emergency']),
|
| 448 |
+
"clinic": len([c for c in cases if c.get('type') in ['public_clinic', 'private_clinic']]),
|
| 449 |
+
"uncertain": len([c for c in cases if c.get('type') == 'uncertain']),
|
| 450 |
+
"total_clinics": len(clinics),
|
| 451 |
+
"total_doctors": len(doctors)
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
return stats
|
| 455 |
+
|
| 456 |
+
except Exception as e:
|
| 457 |
+
logger.error(f"❌ Statistics xatoligi: {e}")
|
| 458 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
# app/api/dispatcher_routes.py - OXIRIGA QO'SHING (Brigades APIs bo'limiga)
|
| 464 |
+
|
| 465 |
+
# ==================== 🆕 BRIGADES LIVE TRACKING ====================
|
| 466 |
+
|
| 467 |
+
@router.get("/brigades/live")
|
| 468 |
+
async def get_live_brigades():
|
| 469 |
+
"""
|
| 470 |
+
Brigadalarning real-time koordinatalarini olish
|
| 471 |
+
|
| 472 |
+
Returns:
|
| 473 |
+
List[Dict]: {
|
| 474 |
+
brigade_id, name, current_lat, current_lon,
|
| 475 |
+
target_lat, target_lon, current_status, speed_kmh
|
| 476 |
+
}
|
| 477 |
+
"""
|
| 478 |
+
try:
|
| 479 |
+
brigades = db.get_all_brigades()
|
| 480 |
+
|
| 481 |
+
# Faqat kerakli ma'lumotlarni qaytarish
|
| 482 |
+
live_data = []
|
| 483 |
+
for brigade in brigades:
|
| 484 |
+
live_data.append({
|
| 485 |
+
"brigade_id": brigade.get("brigade_id"),
|
| 486 |
+
"name": brigade.get("name"),
|
| 487 |
+
"current_lat": brigade.get("current_lat"),
|
| 488 |
+
"current_lon": brigade.get("current_lon"),
|
| 489 |
+
"target_lat": brigade.get("target_lat"),
|
| 490 |
+
"target_lon": brigade.get("target_lon"),
|
| 491 |
+
"current_status": brigade.get("current_status"),
|
| 492 |
+
"speed_kmh": brigade.get("speed_kmh", 60),
|
| 493 |
+
"phone": brigade.get("phone"),
|
| 494 |
+
"assigned_case_id": brigade.get("assigned_case_id")
|
| 495 |
+
})
|
| 496 |
+
|
| 497 |
+
logger.info(f"🚑 {len(live_data)} ta brigade koordinatasi qaytarildi")
|
| 498 |
+
return live_data
|
| 499 |
+
|
| 500 |
+
except Exception as e:
|
| 501 |
+
logger.error(f"❌ Brigade live tracking xatoligi: {e}")
|
| 502 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
@router.patch("/brigades/{brigade_id}/status")
|
| 506 |
+
async def update_brigade_status(brigade_id: str, status: str):
|
| 507 |
+
"""
|
| 508 |
+
Brigadaning statusini yangilash
|
| 509 |
+
|
| 510 |
+
Args:
|
| 511 |
+
brigade_id: "brigade_001"
|
| 512 |
+
status: "available" | "busy" | "offline"
|
| 513 |
+
"""
|
| 514 |
+
try:
|
| 515 |
+
success = db.update_brigade(brigade_id, {
|
| 516 |
+
"current_status": status
|
| 517 |
+
})
|
| 518 |
+
|
| 519 |
+
if not success:
|
| 520 |
+
raise HTTPException(status_code=404, detail="Brigade topilmadi")
|
| 521 |
+
|
| 522 |
+
return {"success": True, "message": "Status yangilandi"}
|
| 523 |
+
|
| 524 |
+
except HTTPException:
|
| 525 |
+
raise
|
| 526 |
+
except Exception as e:
|
| 527 |
+
logger.error(f"❌ Brigade status yangilashda xatolik: {e}")
|
| 528 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
# MUHIM: Bu kodni dispatcher_routes.py faylining OXIRIGA qo'shing
|
| 532 |
+
# Mavjud @router.get("/brigades") funksiyasidan KEYIN
|
app/api/routes.py
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/routes.py - TO'LIQ YANGILANGAN (3 RISK TIZIMI)
|
| 2 |
+
# QISM 1: Imports, Health Checks, va WebSocket Handler
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import uuid
|
| 6 |
+
import json
|
| 7 |
+
import asyncio
|
| 8 |
+
import logging
|
| 9 |
+
import time
|
| 10 |
+
from typing import Optional, Dict, List
|
| 11 |
+
from fastapi import (
|
| 12 |
+
APIRouter,
|
| 13 |
+
WebSocket,
|
| 14 |
+
WebSocketDisconnect,
|
| 15 |
+
HTTPException,
|
| 16 |
+
UploadFile,
|
| 17 |
+
File,
|
| 18 |
+
BackgroundTasks,
|
| 19 |
+
Query
|
| 20 |
+
)
|
| 21 |
+
from fastapi.responses import JSONResponse
|
| 22 |
+
import shutil
|
| 23 |
+
|
| 24 |
+
# Utils
|
| 25 |
+
from app.utils.district_matcher import find_district_fuzzy, get_district_display_name, list_all_districts_text
|
| 26 |
+
from app.utils.mahalla_matcher import find_mahalla_fuzzy, get_mahalla_display_name
|
| 27 |
+
from app.utils.demo_gps import generate_random_tashkent_gps, get_gps_for_district, add_gps_noise, get_all_districts
|
| 28 |
+
|
| 29 |
+
# Services
|
| 30 |
+
from app.services.models import (
|
| 31 |
+
transcribe_audio_from_bytes,
|
| 32 |
+
transcribe_audio,
|
| 33 |
+
get_gemini_response,
|
| 34 |
+
synthesize_speech,
|
| 35 |
+
check_model_status,
|
| 36 |
+
detect_language
|
| 37 |
+
)
|
| 38 |
+
from app.services.geocoding import geocode_address, validate_location_in_tashkent, get_location_summary, extract_district_from_address
|
| 39 |
+
from app.services.brigade_matcher import find_nearest_brigade, haversine_distance
|
| 40 |
+
from app.services.location_validator import get_mahallas_by_district, format_mahallas_list, get_mahalla_coordinates
|
| 41 |
+
|
| 42 |
+
# Core
|
| 43 |
+
from app.core.database import db
|
| 44 |
+
from app.core.config import GPS_VERIFICATION_MAX_DISTANCE_KM, USE_DEMO_GPS, GPS_NOISE_KM, MAX_UNCERTAINTY_ATTEMPTS
|
| 45 |
+
from app.core.connections import active_connections
|
| 46 |
+
|
| 47 |
+
# API
|
| 48 |
+
from app.api.dispatcher_routes import notify_dispatchers
|
| 49 |
+
|
| 50 |
+
# Schemas
|
| 51 |
+
from app.models.schemas import (
|
| 52 |
+
CaseResponse, CaseUpdate, MessageResponse,
|
| 53 |
+
SuccessResponse, ErrorResponse,
|
| 54 |
+
BrigadeLocation, PatientHistoryResponse,
|
| 55 |
+
ClinicResponse, ClinicRecommendation
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
audio_buffers: Dict[str, list] = {}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# Logging
|
| 63 |
+
logging.basicConfig(level=logging.INFO)
|
| 64 |
+
logger = logging.getLogger(__name__)
|
| 65 |
+
|
| 66 |
+
router = APIRouter()
|
| 67 |
+
|
| 68 |
+
# Global variables
|
| 69 |
+
tasks = {}
|
| 70 |
+
stats = {
|
| 71 |
+
"total_messages": 0,
|
| 72 |
+
"voice_messages": 0,
|
| 73 |
+
"text_messages": 0,
|
| 74 |
+
"active_connections": 0,
|
| 75 |
+
"start_time": time.time()
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ==================== HEALTH & STATS ====================
|
| 80 |
+
|
| 81 |
+
@router.get("/api/health")
|
| 82 |
+
async def health_check():
|
| 83 |
+
"""Server va model holatini tekshirish"""
|
| 84 |
+
model_status = check_model_status()
|
| 85 |
+
uptime = time.time() - stats["start_time"]
|
| 86 |
+
|
| 87 |
+
return JSONResponse({
|
| 88 |
+
"status": "healthy",
|
| 89 |
+
"uptime_seconds": int(uptime),
|
| 90 |
+
"models": model_status,
|
| 91 |
+
"stats": {
|
| 92 |
+
**stats,
|
| 93 |
+
"active_connections": len(active_connections)
|
| 94 |
+
},
|
| 95 |
+
"timestamp": time.time()
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@router.get("/api/stats")
|
| 100 |
+
async def get_stats():
|
| 101 |
+
"""Server statistikasi"""
|
| 102 |
+
return JSONResponse({
|
| 103 |
+
**stats,
|
| 104 |
+
"active_connections": len(active_connections),
|
| 105 |
+
"uptime_seconds": int(time.time() - stats["start_time"])
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# app/api/routes.py - TUZATILGAN QISM (WebSocket Handler)
|
| 110 |
+
# Faqat muammoli qismni tuzatamiz
|
| 111 |
+
@router.websocket("/ws/chat")
|
| 112 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 113 |
+
"""
|
| 114 |
+
Bemor uchun WebSocket ulanish
|
| 115 |
+
|
| 116 |
+
Frontend: /ws/chat ga ulanadi
|
| 117 |
+
Backend: Session ID oladi, case yaratadi
|
| 118 |
+
"""
|
| 119 |
+
await websocket.accept()
|
| 120 |
+
active_connections.add(websocket)
|
| 121 |
+
|
| 122 |
+
client_info = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
|
| 123 |
+
logger.info(f"🔌 WebSocket ulanish o'rnatildi: {client_info}")
|
| 124 |
+
|
| 125 |
+
case_id = None
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
while True:
|
| 129 |
+
# ========== XABAR QABUL QILISH ==========
|
| 130 |
+
try:
|
| 131 |
+
data = await websocket.receive()
|
| 132 |
+
except RuntimeError as e:
|
| 133 |
+
if "disconnect" in str(e).lower():
|
| 134 |
+
logger.info(f"📴 WebSocket disconnect signal olindi: {client_info}")
|
| 135 |
+
break
|
| 136 |
+
raise
|
| 137 |
+
|
| 138 |
+
# Disconnect message tekshirish
|
| 139 |
+
if data.get("type") == "websocket.disconnect":
|
| 140 |
+
logger.info(f"📴 WebSocket disconnect message: {client_info}")
|
| 141 |
+
break
|
| 142 |
+
|
| 143 |
+
# ========== TEXT MESSAGE (JSON) ==========
|
| 144 |
+
if "text" in data:
|
| 145 |
+
message_text = data["text"]
|
| 146 |
+
|
| 147 |
+
# "__END__" string belgisi (audio oxiri)
|
| 148 |
+
if message_text == "__END__":
|
| 149 |
+
if not case_id:
|
| 150 |
+
new_case = db.create_case(client_info)
|
| 151 |
+
case_id = new_case['id']
|
| 152 |
+
logger.info(f"✅ Yangi case yaratildi: {case_id}")
|
| 153 |
+
|
| 154 |
+
if case_id not in audio_buffers or not audio_buffers[case_id]:
|
| 155 |
+
logger.warning(f"⚠️ {case_id} uchun audio buffer bo'sh")
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
logger.info(f"🎤 Audio oxiri belgisi (string) qabul qilindi")
|
| 159 |
+
|
| 160 |
+
full_audio = b"".join(audio_buffers[case_id])
|
| 161 |
+
audio_buffers[case_id] = []
|
| 162 |
+
|
| 163 |
+
logger.info(f"📦 To'liq audio hajmi: {len(full_audio)} bytes")
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
transcribed_text = transcribe_audio_from_bytes(full_audio)
|
| 167 |
+
logger.info(f"✅ Transkripsiya: '{transcribed_text}'")
|
| 168 |
+
|
| 169 |
+
if transcribed_text and len(transcribed_text.strip()) > 0:
|
| 170 |
+
stats["voice_messages"] += 1
|
| 171 |
+
db.create_message(case_id, "user", transcribed_text)
|
| 172 |
+
|
| 173 |
+
await websocket.send_json({
|
| 174 |
+
"type": "transcription_result",
|
| 175 |
+
"text": transcribed_text
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
await process_text_input(websocket, case_id, transcribed_text, is_voice=True)
|
| 179 |
+
|
| 180 |
+
except Exception as e:
|
| 181 |
+
logger.error(f"❌ Transkripsiya xatoligi: {e}", exc_info=True)
|
| 182 |
+
await websocket.send_json({
|
| 183 |
+
"type": "error",
|
| 184 |
+
"message": "Ovozni tanishda xatolik"
|
| 185 |
+
})
|
| 186 |
+
|
| 187 |
+
continue
|
| 188 |
+
|
| 189 |
+
# ========== JSON MESSAGE ==========
|
| 190 |
+
try:
|
| 191 |
+
message = json.loads(message_text)
|
| 192 |
+
message_type = message.get("type")
|
| 193 |
+
|
| 194 |
+
# ========== TEXT INPUT ==========
|
| 195 |
+
if message_type == "text_input":
|
| 196 |
+
if not case_id:
|
| 197 |
+
new_case = db.create_case(client_info)
|
| 198 |
+
case_id = new_case['id']
|
| 199 |
+
logger.info(f"✅ Yangi case yaratildi (text): {case_id}")
|
| 200 |
+
|
| 201 |
+
text = message.get("text", "").strip()
|
| 202 |
+
|
| 203 |
+
if text:
|
| 204 |
+
db.create_message(case_id, "user", text)
|
| 205 |
+
stats["text_messages"] += 1
|
| 206 |
+
|
| 207 |
+
await process_text_input(websocket, case_id, text, is_voice=False)
|
| 208 |
+
|
| 209 |
+
# ========== PATIENT NAME ==========
|
| 210 |
+
elif message_type == "patient_name":
|
| 211 |
+
if not case_id:
|
| 212 |
+
logger.warning("⚠️ Case ID yo'q, ism qabul qilinmaydi")
|
| 213 |
+
continue
|
| 214 |
+
|
| 215 |
+
full_name = message.get("full_name", "").strip()
|
| 216 |
+
|
| 217 |
+
if full_name:
|
| 218 |
+
await process_name_input(websocket, case_id, full_name)
|
| 219 |
+
|
| 220 |
+
# ========== GPS LOCATION ==========
|
| 221 |
+
elif message_type == "gps_location":
|
| 222 |
+
if not case_id:
|
| 223 |
+
logger.warning("⚠️ Case ID yo'q, GPS qabul qilinmaydi")
|
| 224 |
+
continue
|
| 225 |
+
|
| 226 |
+
lat = message.get("latitude")
|
| 227 |
+
lon = message.get("longitude")
|
| 228 |
+
|
| 229 |
+
if lat and lon:
|
| 230 |
+
await process_gps_and_brigade(websocket, case_id, lat, lon)
|
| 231 |
+
|
| 232 |
+
except json.JSONDecodeError:
|
| 233 |
+
logger.error(f"❌ JSON parse xatoligi: {message_text}")
|
| 234 |
+
|
| 235 |
+
# ========== BINARY DATA (AUDIO CHUNKS) ==========
|
| 236 |
+
elif "bytes" in data:
|
| 237 |
+
if not case_id:
|
| 238 |
+
new_case = db.create_case(client_info)
|
| 239 |
+
case_id = new_case['id']
|
| 240 |
+
logger.info(f"✅ Yangi case yaratildi (audio): {case_id}")
|
| 241 |
+
|
| 242 |
+
audio_chunk = data["bytes"]
|
| 243 |
+
|
| 244 |
+
if audio_chunk == b"__END__":
|
| 245 |
+
logger.info("🎤 Audio oxiri belgisi (bytes) qabul qilindi")
|
| 246 |
+
continue
|
| 247 |
+
|
| 248 |
+
if case_id not in audio_buffers:
|
| 249 |
+
audio_buffers[case_id] = []
|
| 250 |
+
|
| 251 |
+
audio_buffers[case_id].append(audio_chunk)
|
| 252 |
+
logger.debug(f"📝 Audio chunk qo'shildi ({len(audio_chunk)} bytes). Jami: {len(audio_buffers[case_id])} chunks")
|
| 253 |
+
|
| 254 |
+
except WebSocketDisconnect:
|
| 255 |
+
logger.info(f"📴 WebSocket disconnect exception: {client_info}")
|
| 256 |
+
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.error(f"❌ WebSocket xatolik: {e}", exc_info=True)
|
| 259 |
+
|
| 260 |
+
finally:
|
| 261 |
+
# Cleanup (har qanday holatda ham ishga tushadi)
|
| 262 |
+
active_connections.discard(websocket)
|
| 263 |
+
|
| 264 |
+
if case_id and case_id in audio_buffers:
|
| 265 |
+
del audio_buffers[case_id]
|
| 266 |
+
|
| 267 |
+
logger.info(f"🧹 WebSocket cleanup tugadi: {client_info}")
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
# ==================== MESSAGE HANDLERS ====================
|
| 271 |
+
|
| 272 |
+
async def handle_voice_message(websocket: WebSocket, case_id: str, data: Dict):
|
| 273 |
+
"""
|
| 274 |
+
Ovozli xabar qayta ishlash
|
| 275 |
+
|
| 276 |
+
Flow:
|
| 277 |
+
1. Audio → Text (STT)
|
| 278 |
+
2. Text → AI tahlil (Gemini)
|
| 279 |
+
3. Risk darajasini aniqlash
|
| 280 |
+
4. Mos flow ni boshlash (qizil/sariq/yashil)
|
| 281 |
+
"""
|
| 282 |
+
try:
|
| 283 |
+
audio_data = data.get("audio")
|
| 284 |
+
if not audio_data:
|
| 285 |
+
await websocket.send_json({
|
| 286 |
+
"type": "error",
|
| 287 |
+
"message": "Audio ma'lumot topilmadi"
|
| 288 |
+
})
|
| 289 |
+
return
|
| 290 |
+
|
| 291 |
+
# Audio bytes olish
|
| 292 |
+
import base64
|
| 293 |
+
audio_bytes = base64.b64decode(audio_data.split(',')[1] if ',' in audio_data else audio_data)
|
| 294 |
+
|
| 295 |
+
logger.info(f"🎤 Ovoz yozuvi qabul qilindi: {len(audio_bytes)} bytes")
|
| 296 |
+
|
| 297 |
+
# STT
|
| 298 |
+
await websocket.send_json({
|
| 299 |
+
"type": "status",
|
| 300 |
+
"message": "Ovozingizni tinglab turaman..."
|
| 301 |
+
})
|
| 302 |
+
|
| 303 |
+
user_transcript = transcribe_audio_from_bytes(audio_bytes)
|
| 304 |
+
|
| 305 |
+
if not user_transcript or len(user_transcript.strip()) < 3:
|
| 306 |
+
await websocket.send_json({
|
| 307 |
+
"type": "error",
|
| 308 |
+
"message": "Ovozni tushunolmadim. Iltimos, qaytadan aytib bering."
|
| 309 |
+
})
|
| 310 |
+
return
|
| 311 |
+
|
| 312 |
+
logger.info(f"📝 Transkripsiya: '{user_transcript}'")
|
| 313 |
+
|
| 314 |
+
# Database ga saqlash
|
| 315 |
+
db.create_message(case_id, "user", user_transcript)
|
| 316 |
+
stats["voice_messages"] += 1
|
| 317 |
+
|
| 318 |
+
# Text bilan davom etish
|
| 319 |
+
await process_text_input(websocket, case_id, user_transcript, is_voice=True)
|
| 320 |
+
|
| 321 |
+
except Exception as e:
|
| 322 |
+
logger.error(f"❌ Ovozli xabar xatoligi: {e}", exc_info=True)
|
| 323 |
+
await websocket.send_json({
|
| 324 |
+
"type": "error",
|
| 325 |
+
"message": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring."
|
| 326 |
+
})
|
| 327 |
+
|
| 328 |
+
async def handle_text_message(websocket: WebSocket, case_id: str, data: Dict):
|
| 329 |
+
"""Matnli xabar qayta ishlash"""
|
| 330 |
+
try:
|
| 331 |
+
text = data.get("text", "").strip()
|
| 332 |
+
|
| 333 |
+
if not text or len(text) < 2:
|
| 334 |
+
await websocket.send_json({
|
| 335 |
+
"type": "error",
|
| 336 |
+
"message": "Xabar bo'sh. Iltimos, biror narsa yozing."
|
| 337 |
+
})
|
| 338 |
+
return
|
| 339 |
+
|
| 340 |
+
logger.info(f"💬 Matnli xabar: '{text}'")
|
| 341 |
+
|
| 342 |
+
db.create_message(case_id, "user", text)
|
| 343 |
+
stats["text_messages"] += 1
|
| 344 |
+
|
| 345 |
+
await process_text_input(websocket, case_id, text, is_voice=False)
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
logger.error(f"❌ Matnli xabar xatoligi: {e}", exc_info=True)
|
| 349 |
+
await websocket.send_json({
|
| 350 |
+
"type": "error",
|
| 351 |
+
"message": "Xatolik yuz berdi."
|
| 352 |
+
})
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
async def handle_gps_location(websocket: WebSocket, case_id: str, data: Dict):
|
| 356 |
+
"""GPS lokatsiya qayta ishlash"""
|
| 357 |
+
try:
|
| 358 |
+
lat = data.get("latitude")
|
| 359 |
+
lon = data.get("longitude")
|
| 360 |
+
|
| 361 |
+
if not lat or not lon:
|
| 362 |
+
await websocket.send_json({
|
| 363 |
+
"type": "error",
|
| 364 |
+
"message": "GPS ma'lumot topilmadi"
|
| 365 |
+
})
|
| 366 |
+
return
|
| 367 |
+
|
| 368 |
+
logger.info(f"📍 GPS qabul qilindi: ({lat}, {lon})")
|
| 369 |
+
|
| 370 |
+
# GPS ni saqlash va brigada topish
|
| 371 |
+
await process_gps_and_brigade(websocket, case_id, lat, lon)
|
| 372 |
+
|
| 373 |
+
except Exception as e:
|
| 374 |
+
logger.error(f"❌ GPS xatoligi: {e}", exc_info=True)
|
| 375 |
+
await websocket.send_json({
|
| 376 |
+
"type": "error",
|
| 377 |
+
"message": "GPS xatolik"
|
| 378 |
+
})
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
# ==================== TEXT PROCESSING (ASOSIY MANTIQ) ====================
|
| 382 |
+
|
| 383 |
+
async def process_text_input(websocket: WebSocket, case_id: str, prompt: str, is_voice: bool = False):
|
| 384 |
+
"""
|
| 385 |
+
Matn kiritishni qayta ishlash - ASOSIY FLOW
|
| 386 |
+
|
| 387 |
+
Args:
|
| 388 |
+
websocket: WebSocket ulanish
|
| 389 |
+
case_id: Case ID (string)
|
| 390 |
+
prompt: Bemorning matni
|
| 391 |
+
is_voice: Ovozli xabarmi? (True/False)
|
| 392 |
+
"""
|
| 393 |
+
try:
|
| 394 |
+
# Case ni olish
|
| 395 |
+
current_case = db.get_case(case_id)
|
| 396 |
+
|
| 397 |
+
if not current_case:
|
| 398 |
+
logger.error(f"❌ Case topilmadi: {case_id}")
|
| 399 |
+
await websocket.send_json({
|
| 400 |
+
"type": "error",
|
| 401 |
+
"message": "Sessiya xatoligi. Iltimos, sahifani yangilang."
|
| 402 |
+
})
|
| 403 |
+
return
|
| 404 |
+
|
| 405 |
+
# ========== 1. ISM-FAMILIYA KUTILMOQDA? ==========
|
| 406 |
+
if current_case.get('waiting_for_name_input'):
|
| 407 |
+
await process_name_input(websocket, case_id, prompt)
|
| 408 |
+
return
|
| 409 |
+
|
| 410 |
+
# ========== 2. MANZIL ANIQLASHTIRILMOQDA? ==========
|
| 411 |
+
if await handle_location_clarification(websocket, case_id, prompt, "voice" if is_voice else "text"):
|
| 412 |
+
return
|
| 413 |
+
|
| 414 |
+
# ========== 3. YANGI TAHLIL (GEMINI) ==========
|
| 415 |
+
conversation_history = db.get_conversation_history(case_id)
|
| 416 |
+
detected_lang = detect_language(prompt)
|
| 417 |
+
|
| 418 |
+
logger.info(f"🧠 Gemini tahlil boshlandi...")
|
| 419 |
+
|
| 420 |
+
full_prompt = f"{conversation_history}\nBemor: {prompt}"
|
| 421 |
+
ai_analysis = get_gemini_response(full_prompt, stream=False)
|
| 422 |
+
|
| 423 |
+
# JSON parse qilish
|
| 424 |
+
if not ai_analysis or not isinstance(ai_analysis, dict):
|
| 425 |
+
logger.error(f"❌ Gemini noto'g'ri javob: {ai_analysis}")
|
| 426 |
+
await websocket.send_json({
|
| 427 |
+
"type": "error",
|
| 428 |
+
"message": "AI xatolik"
|
| 429 |
+
})
|
| 430 |
+
return
|
| 431 |
+
|
| 432 |
+
risk_level = ai_analysis.get("risk_level", "yashil")
|
| 433 |
+
response_text = ai_analysis.get("response_text", "Tushunmadim")
|
| 434 |
+
language = ai_analysis.get("language", detected_lang)
|
| 435 |
+
|
| 436 |
+
logger.info(f"📊 Risk darajasi: {risk_level.upper()}")
|
| 437 |
+
|
| 438 |
+
# Database ga saqlash
|
| 439 |
+
db.create_message(case_id, "ai", response_text)
|
| 440 |
+
db.update_case(case_id, {
|
| 441 |
+
"risk_level": risk_level,
|
| 442 |
+
"language": language,
|
| 443 |
+
"symptoms_text": ai_analysis.get("symptoms_extracted")
|
| 444 |
+
})
|
| 445 |
+
|
| 446 |
+
# ========== RISK DARAJASIGA QARAB HARAKAT ==========
|
| 447 |
+
|
| 448 |
+
if risk_level == "qizil":
|
| 449 |
+
await handle_qizil_flow(websocket, case_id, ai_analysis)
|
| 450 |
+
elif risk_level == "sariq":
|
| 451 |
+
await handle_sariq_flow(websocket, case_id, ai_analysis)
|
| 452 |
+
elif risk_level == "yashil":
|
| 453 |
+
await handle_yashil_flow(websocket, case_id, ai_analysis)
|
| 454 |
+
else:
|
| 455 |
+
logger.warning(f"⚠️ Noma'lum risk level: {risk_level}")
|
| 456 |
+
await send_ai_response(websocket, case_id, response_text, language)
|
| 457 |
+
|
| 458 |
+
except Exception as e:
|
| 459 |
+
logger.error(f"❌ process_text_input xatoligi: {e}", exc_info=True)
|
| 460 |
+
await websocket.send_json({
|
| 461 |
+
"type": "error",
|
| 462 |
+
"message": "Xatolik yuz berdi"
|
| 463 |
+
})
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# ==================== HELPER FUNCTION ====================
|
| 467 |
+
|
| 468 |
+
async def send_ai_response(websocket: WebSocket, case_id: str, text: str, language: str = "uzb"):
|
| 469 |
+
"""
|
| 470 |
+
AI javobini frontendga yuborish (text + audio)
|
| 471 |
+
|
| 472 |
+
TUZATILGAN: TTS output_path to'g'ri yaratiladi
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
websocket: WebSocket ulanish
|
| 476 |
+
case_id: Case ID
|
| 477 |
+
text: Javob matni
|
| 478 |
+
language: Javob tili ("uzb" | "eng" | "rus")
|
| 479 |
+
"""
|
| 480 |
+
try:
|
| 481 |
+
# Database ga AI xabarini saqlash
|
| 482 |
+
db.create_message(case_id, "ai", text)
|
| 483 |
+
|
| 484 |
+
# 1. Text yuborish
|
| 485 |
+
await websocket.send_json({
|
| 486 |
+
"type": "ai_response",
|
| 487 |
+
"text": text
|
| 488 |
+
})
|
| 489 |
+
|
| 490 |
+
# 2. TTS audio yaratish
|
| 491 |
+
# ✅ TO'G'RI: output_path yaratish
|
| 492 |
+
audio_filename = f"tts_{case_id}_{int(time.time())}.wav"
|
| 493 |
+
audio_path = os.path.join("static/audio", audio_filename)
|
| 494 |
+
|
| 495 |
+
logger.info(f"🎧 TTS uchun fayl yo'li: {audio_path}")
|
| 496 |
+
|
| 497 |
+
# TTS chaqirish (to'g'ri parametrlar bilan)
|
| 498 |
+
tts_success = synthesize_speech(text, audio_path, language)
|
| 499 |
+
|
| 500 |
+
if tts_success and os.path.exists(audio_path):
|
| 501 |
+
audio_url = f"/audio/{audio_filename}"
|
| 502 |
+
await websocket.send_json({
|
| 503 |
+
"type": "audio_response",
|
| 504 |
+
"audio_url": audio_url
|
| 505 |
+
})
|
| 506 |
+
logger.info(f"📊 TTS audio yuborildi: {audio_url}")
|
| 507 |
+
else:
|
| 508 |
+
logger.warning("⚠️ TTS yaratilmadi, faqat text yuborildi")
|
| 509 |
+
|
| 510 |
+
except Exception as e:
|
| 511 |
+
logger.error(f"❌ send_ai_response xatoligi: {e}", exc_info=True)
|
| 512 |
+
|
| 513 |
+
# app/api/routes.py - QISM 2
|
| 514 |
+
# 3 TA ASOSIY FLOW: QIZIL, SARIQ, YASHIL
|
| 515 |
+
|
| 516 |
+
# ==================== 🔴 QIZIL FLOW (EMERGENCY) ====================
|
| 517 |
+
|
| 518 |
+
async def handle_qizil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
|
| 519 |
+
"""
|
| 520 |
+
QIZIL (Emergency) - TEZ YORDAM BRIGADA
|
| 521 |
+
|
| 522 |
+
Flow:
|
| 523 |
+
1. Manzil so'rash (tuman + mahalla)
|
| 524 |
+
2. Fuzzy matching orqali koordinata topish
|
| 525 |
+
3. Brigada topish va jo'natish
|
| 526 |
+
4. ISM-FAMILIYA so'rash (brigadadan KEYIN!)
|
| 527 |
+
"""
|
| 528 |
+
try:
|
| 529 |
+
logger.info(f"🔴 QIZIL HOLAT: Tez yordam jarayoni boshlandi")
|
| 530 |
+
|
| 531 |
+
response_text = ai_analysis.get("response_text")
|
| 532 |
+
language = ai_analysis.get("language", "uzb")
|
| 533 |
+
address = ai_analysis.get("address_extracted")
|
| 534 |
+
district = ai_analysis.get("district_extracted")
|
| 535 |
+
|
| 536 |
+
# Case type ni belgilash
|
| 537 |
+
db.update_case(case_id, {
|
| 538 |
+
"type": "emergency",
|
| 539 |
+
"risk_level": "qizil"
|
| 540 |
+
})
|
| 541 |
+
|
| 542 |
+
# 1. MANZIL SO'RASH
|
| 543 |
+
if not address or not district:
|
| 544 |
+
logger.info("📍 Manzil yo'q, so'ralmoqda...")
|
| 545 |
+
await send_ai_response(websocket, case_id, response_text, language)
|
| 546 |
+
|
| 547 |
+
# Flag qo'yish - keyingi xabarda manzil kutiladi
|
| 548 |
+
db.update_case(case_id, {"waiting_for_address": True})
|
| 549 |
+
return
|
| 550 |
+
|
| 551 |
+
# 2. MANZILNI QAYTA ISHLASH
|
| 552 |
+
logger.info(f"📍 Manzil aniqlandi: {address}")
|
| 553 |
+
|
| 554 |
+
# Tuman fuzzy match
|
| 555 |
+
district_match = find_district_fuzzy(district)
|
| 556 |
+
|
| 557 |
+
if not district_match:
|
| 558 |
+
logger.warning(f"⚠️ Tuman topilmadi: {district}")
|
| 559 |
+
districts_list = get_all_districts()
|
| 560 |
+
|
| 561 |
+
response = f"Tuman nomini aniq tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{districts_list}"
|
| 562 |
+
await send_ai_response(websocket, case_id, response, language)
|
| 563 |
+
return
|
| 564 |
+
|
| 565 |
+
district_name = get_district_display_name(district_match)
|
| 566 |
+
logger.info(f"✅ Tuman topildi: {district_name}")
|
| 567 |
+
|
| 568 |
+
db.update_case(case_id, {
|
| 569 |
+
"district": district_name,
|
| 570 |
+
"selected_district": district_match
|
| 571 |
+
})
|
| 572 |
+
|
| 573 |
+
# 3. MAHALLA SO'RASH
|
| 574 |
+
# Bu qism location_clarification da amalga oshiriladi
|
| 575 |
+
# Hozircha flag qo'yamiz
|
| 576 |
+
db.update_case(case_id, {
|
| 577 |
+
"waiting_for_mahalla_input": True,
|
| 578 |
+
"mahalla_retry_count": 0
|
| 579 |
+
})
|
| 580 |
+
|
| 581 |
+
response = f"Tushundim, {district_name}. Iltimos, mahallangizni ayting."
|
| 582 |
+
await send_ai_response(websocket, case_id, response, language)
|
| 583 |
+
|
| 584 |
+
# Dispetcherga bildirishnoma
|
| 585 |
+
await notify_dispatchers({
|
| 586 |
+
"type": "new_case",
|
| 587 |
+
"case": db.get_case(case_id)
|
| 588 |
+
})
|
| 589 |
+
|
| 590 |
+
except Exception as e:
|
| 591 |
+
logger.error(f"❌ handle_qizil_flow xatoligi: {e}", exc_info=True)
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
async def process_gps_and_brigade(websocket: WebSocket, case_id: str, lat: float, lon: float):
|
| 595 |
+
"""
|
| 596 |
+
GPS koordinatalariga qarab brigadani topish
|
| 597 |
+
|
| 598 |
+
MUHIM: Brigadadan KEYIN ism-familiya so'raladi!
|
| 599 |
+
"""
|
| 600 |
+
try:
|
| 601 |
+
logger.info(f"📍 GPS koordinatalar: ({lat:.6f}, {lon:.6f})")
|
| 602 |
+
|
| 603 |
+
# GPS validatsiya
|
| 604 |
+
if not validate_location_in_tashkent(lat, lon):
|
| 605 |
+
logger.warning("⚠️ GPS Toshkent chegarasidan tashqarida")
|
| 606 |
+
await websocket.send_json({
|
| 607 |
+
"type": "error",
|
| 608 |
+
"message": "GPS manzil Toshkent chegarasidan tashqarida"
|
| 609 |
+
})
|
| 610 |
+
return
|
| 611 |
+
|
| 612 |
+
# Case ga saqlash
|
| 613 |
+
db.update_case(case_id, {
|
| 614 |
+
"gps_lat": lat,
|
| 615 |
+
"gps_lon": lon,
|
| 616 |
+
"geocoded_lat": lat,
|
| 617 |
+
"geocoded_lon": lon,
|
| 618 |
+
"gps_verified": True
|
| 619 |
+
})
|
| 620 |
+
|
| 621 |
+
# Brigadani topish
|
| 622 |
+
logger.info("🚑 Eng yaqin brigada qidirilmoqda...")
|
| 623 |
+
|
| 624 |
+
nearest_brigade = find_nearest_brigade(lat, lon)
|
| 625 |
+
|
| 626 |
+
if not nearest_brigade:
|
| 627 |
+
logger.warning("⚠️ Brigada topilmadi")
|
| 628 |
+
await websocket.send_json({
|
| 629 |
+
"type": "error",
|
| 630 |
+
"message": "Hozirda barcha brigadalar band"
|
| 631 |
+
})
|
| 632 |
+
return
|
| 633 |
+
|
| 634 |
+
brigade_id = nearest_brigade['brigade_id']
|
| 635 |
+
brigade_name = nearest_brigade['brigade_name']
|
| 636 |
+
distance_km = nearest_brigade['distance_km']
|
| 637 |
+
|
| 638 |
+
# Brigadani tayinlash
|
| 639 |
+
db.update_case(case_id, {
|
| 640 |
+
"assigned_brigade_id": brigade_id,
|
| 641 |
+
"assigned_brigade_name": brigade_name,
|
| 642 |
+
"distance_to_brigade_km": distance_km,
|
| 643 |
+
"status": "brigada_junatildi"
|
| 644 |
+
})
|
| 645 |
+
|
| 646 |
+
logger.info(f"✅ Brigada tayinlandi: {brigade_name} ({distance_km:.2f} km)")
|
| 647 |
+
|
| 648 |
+
# Bemorga xabar
|
| 649 |
+
await websocket.send_json({
|
| 650 |
+
"type": "brigade_assigned",
|
| 651 |
+
"brigade": {
|
| 652 |
+
"id": brigade_id,
|
| 653 |
+
"name": brigade_name,
|
| 654 |
+
"distance_km": distance_km,
|
| 655 |
+
"estimated_time_min": int(distance_km * 3) # 3 min/km
|
| 656 |
+
}
|
| 657 |
+
})
|
| 658 |
+
|
| 659 |
+
# ========== ENDI ISM-FAMILIYA SO'RASH ==========
|
| 660 |
+
current_case = db.get_case(case_id)
|
| 661 |
+
language = current_case.get("language", "uzb")
|
| 662 |
+
|
| 663 |
+
if language == "eng":
|
| 664 |
+
name_request = f"The ambulance is on its way, arriving in approximately {int(distance_km * 3)} minutes. Please tell me your full name."
|
| 665 |
+
elif language == "rus":
|
| 666 |
+
name_request = f"Скорая помощь в пути, прибудет примерно через {int(distance_km * 3)} минут. Пожалуйста, назовите ваше полное имя."
|
| 667 |
+
else:
|
| 668 |
+
name_request = f"Brigada yo'lda, taxminan {int(distance_km * 3)} daqiqada yetib keladi. Iltimos, to'liq ism-familiyangizni ayting."
|
| 669 |
+
|
| 670 |
+
db.create_message(case_id, "ai", name_request)
|
| 671 |
+
await send_ai_response(websocket, case_id, name_request, language)
|
| 672 |
+
|
| 673 |
+
# Flag qo'yish
|
| 674 |
+
db.update_case(case_id, {"waiting_for_name_input": True})
|
| 675 |
+
|
| 676 |
+
# Dispetcherga yangilanish
|
| 677 |
+
await notify_dispatchers({
|
| 678 |
+
"type": "brigade_assigned",
|
| 679 |
+
"case": db.get_case(case_id)
|
| 680 |
+
})
|
| 681 |
+
|
| 682 |
+
except Exception as e:
|
| 683 |
+
logger.error(f"❌ process_gps_and_brigade xatoligi: {e}", exc_info=True)
|
| 684 |
+
|
| 685 |
+
|
| 686 |
+
# ==================== 🟡 SARIQ FLOW (UNCERTAIN) ====================
|
| 687 |
+
|
| 688 |
+
async def handle_sariq_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
|
| 689 |
+
"""
|
| 690 |
+
SARIQ (Uncertain) - NOANIQ, OPERATOR KERAK
|
| 691 |
+
|
| 692 |
+
Flow:
|
| 693 |
+
1. Aniqlashtiruvchi savol berish
|
| 694 |
+
2. Counter ni oshirish (max 3)
|
| 695 |
+
3. 3 marta tushunmasa → Operator
|
| 696 |
+
"""
|
| 697 |
+
try:
|
| 698 |
+
logger.info(f"🟡 SARIQ HOLAT: Noaniqlik")
|
| 699 |
+
|
| 700 |
+
response_text = ai_analysis.get("response_text")
|
| 701 |
+
language = ai_analysis.get("language", "uzb")
|
| 702 |
+
uncertainty_reason = ai_analysis.get("uncertainty_reason")
|
| 703 |
+
operator_needed = ai_analysis.get("operator_needed", False)
|
| 704 |
+
|
| 705 |
+
current_case = db.get_case(case_id)
|
| 706 |
+
current_attempts = current_case.get("uncertainty_attempts", 0)
|
| 707 |
+
|
| 708 |
+
# Case type ni belgilash
|
| 709 |
+
db.update_case(case_id, {
|
| 710 |
+
"type": "uncertain",
|
| 711 |
+
"risk_level": "sariq"
|
| 712 |
+
})
|
| 713 |
+
|
| 714 |
+
# Operator kerakmi?
|
| 715 |
+
if operator_needed or current_attempts >= MAX_UNCERTAINTY_ATTEMPTS:
|
| 716 |
+
logger.info(f"🎧 OPERATOR KERAK! (Attempts: {current_attempts})")
|
| 717 |
+
|
| 718 |
+
db.update_case(case_id, {
|
| 719 |
+
"operator_needed": True,
|
| 720 |
+
"uncertainty_reason": uncertainty_reason or f"AI {current_attempts} marta tushunolmadi",
|
| 721 |
+
"status": "operator_kutilmoqda",
|
| 722 |
+
"uncertainty_attempts": current_attempts + 1
|
| 723 |
+
})
|
| 724 |
+
|
| 725 |
+
# Bemorga xabar
|
| 726 |
+
if language == "eng":
|
| 727 |
+
operator_msg = "I'm having trouble understanding you. Connecting you to an operator who can help..."
|
| 728 |
+
elif language == "rus":
|
| 729 |
+
operator_msg = "Мне сложно вас понять. Соединяю с оператором, который вам поможет..."
|
| 730 |
+
else:
|
| 731 |
+
operator_msg = "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi..."
|
| 732 |
+
|
| 733 |
+
await send_ai_response(websocket, case_id, operator_msg, language)
|
| 734 |
+
|
| 735 |
+
# Dispetcherga operator kerakligi haqida xabar
|
| 736 |
+
await notify_dispatchers({
|
| 737 |
+
"type": "operator_needed",
|
| 738 |
+
"case": db.get_case(case_id)
|
| 739 |
+
})
|
| 740 |
+
|
| 741 |
+
return
|
| 742 |
+
|
| 743 |
+
# Hali operator kerak emas, aniqlashtirish
|
| 744 |
+
logger.info(f"❓ Aniqlashtirish (Attempt {current_attempts + 1}/{MAX_UNCERTAINTY_ATTEMPTS})")
|
| 745 |
+
|
| 746 |
+
db.update_case(case_id, {
|
| 747 |
+
"uncertainty_attempts": current_attempts + 1,
|
| 748 |
+
"uncertainty_reason": uncertainty_reason
|
| 749 |
+
})
|
| 750 |
+
|
| 751 |
+
await send_ai_response(websocket, case_id, response_text, language)
|
| 752 |
+
|
| 753 |
+
except Exception as e:
|
| 754 |
+
logger.error(f"❌ handle_sariq_flow xatoligi: {e}", exc_info=True)
|
| 755 |
+
|
| 756 |
+
|
| 757 |
+
# ==================== 🟢 YASHIL FLOW (CLINIC) ====================
|
| 758 |
+
|
| 759 |
+
async def handle_yashil_flow(websocket: WebSocket, case_id: str, ai_analysis: Dict):
|
| 760 |
+
"""
|
| 761 |
+
YASHIL (Non-urgent) - KLINIKA TAVSIYA
|
| 762 |
+
|
| 763 |
+
Flow:
|
| 764 |
+
1. Bemorga xotirjamlik berish
|
| 765 |
+
2. Davlat yoki xususiy klinika taklif qilish
|
| 766 |
+
3. Bemor tanlasa, klinikalar ro'yxatini yuborish
|
| 767 |
+
"""
|
| 768 |
+
try:
|
| 769 |
+
logger.info(f"🟢 YASHIL HOLAT: Klinika tavsiyasi")
|
| 770 |
+
|
| 771 |
+
response_text = ai_analysis.get("response_text")
|
| 772 |
+
language = ai_analysis.get("language", "uzb")
|
| 773 |
+
symptoms = ai_analysis.get("symptoms_extracted")
|
| 774 |
+
preferred_clinic_type = ai_analysis.get("preferred_clinic_type", "both")
|
| 775 |
+
recommended_specialty = ai_analysis.get("recommended_specialty", "Terapiya")
|
| 776 |
+
|
| 777 |
+
# Case type ni belgilash
|
| 778 |
+
db.update_case(case_id, {
|
| 779 |
+
"type": "public_clinic", # Default, keyin o'zgarishi mumkin
|
| 780 |
+
"risk_level": "yashil",
|
| 781 |
+
"symptoms_text": symptoms
|
| 782 |
+
})
|
| 783 |
+
|
| 784 |
+
# 1. AI javobini yuborish (xotirjamlik + taklif)
|
| 785 |
+
await send_ai_response(websocket, case_id, response_text, language)
|
| 786 |
+
|
| 787 |
+
# 2. Klinikalarni qidirish
|
| 788 |
+
logger.info(f"🏥 Klinikalar qidirilmoqda: {recommended_specialty}, type={preferred_clinic_type}")
|
| 789 |
+
|
| 790 |
+
# Har ikki turdan ham topish
|
| 791 |
+
if preferred_clinic_type == "both":
|
| 792 |
+
davlat_clinics = db.recommend_clinics_by_symptoms(
|
| 793 |
+
symptoms=symptoms,
|
| 794 |
+
district=None,
|
| 795 |
+
clinic_type="davlat"
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
xususiy_clinics = db.recommend_clinics_by_symptoms(
|
| 799 |
+
symptoms=symptoms,
|
| 800 |
+
district=None,
|
| 801 |
+
clinic_type="xususiy"
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
# Formatlangan ro'yxat yaratish
|
| 805 |
+
clinic_list_text = format_clinic_list(
|
| 806 |
+
davlat_clinics.get('clinics', [])[:2], # Top 2 davlat
|
| 807 |
+
xususiy_clinics.get('clinics', [])[:3], # Top 3 xususiy
|
| 808 |
+
language
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
+
else:
|
| 812 |
+
# Faqat bitta turni ko'rsatish
|
| 813 |
+
recommendation = db.recommend_clinics_by_symptoms(
|
| 814 |
+
symptoms=symptoms,
|
| 815 |
+
district=None,
|
| 816 |
+
clinic_type=preferred_clinic_type
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
clinic_list_text = format_clinic_list(
|
| 820 |
+
recommendation.get('clinics', [])[:5] if preferred_clinic_type == "davlat" else [],
|
| 821 |
+
recommendation.get('clinics', [])[:5] if preferred_clinic_type == "xususiy" else [],
|
| 822 |
+
language
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
# 3. Klinikalar ro'yxatini yuborish
|
| 826 |
+
await websocket.send_json({
|
| 827 |
+
"type": "clinic_recommendation",
|
| 828 |
+
"text": clinic_list_text
|
| 829 |
+
})
|
| 830 |
+
|
| 831 |
+
db.create_message(case_id, "ai", clinic_list_text)
|
| 832 |
+
|
| 833 |
+
# Dispetcherga xabar
|
| 834 |
+
await notify_dispatchers({
|
| 835 |
+
"type": "clinic_case",
|
| 836 |
+
"case": db.get_case(case_id)
|
| 837 |
+
})
|
| 838 |
+
|
| 839 |
+
logger.info(f"✅ Klinikalar ro'yxati yuborildi")
|
| 840 |
+
|
| 841 |
+
except Exception as e:
|
| 842 |
+
logger.error(f"❌ handle_yashil_flow xatoligi: {e}", exc_info=True)
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
def format_clinic_list(davlat_clinics: List[Dict], xususiy_clinics: List[Dict], language: str = "uzb") -> str:
|
| 846 |
+
"""
|
| 847 |
+
Klinikalar ro'yxatini formatlash
|
| 848 |
+
|
| 849 |
+
Args:
|
| 850 |
+
davlat_clinics: Davlat poliklinikalari
|
| 851 |
+
xususiy_clinics: Xususiy klinikalar
|
| 852 |
+
language: Til
|
| 853 |
+
|
| 854 |
+
Returns:
|
| 855 |
+
Formatlangan matn
|
| 856 |
+
"""
|
| 857 |
+
result = []
|
| 858 |
+
|
| 859 |
+
# Header
|
| 860 |
+
if language == "eng":
|
| 861 |
+
result.append("Here are my recommendations:\n")
|
| 862 |
+
elif language == "rus":
|
| 863 |
+
result.append("Вот мои рекомендации:\n")
|
| 864 |
+
else:
|
| 865 |
+
result.append("Mana sizga tavsiyalar:\n")
|
| 866 |
+
|
| 867 |
+
# Davlat klinikalari
|
| 868 |
+
if davlat_clinics:
|
| 869 |
+
if language == "eng":
|
| 870 |
+
result.append("\n🏥 PUBLIC CLINICS (Free):\n")
|
| 871 |
+
elif language == "rus":
|
| 872 |
+
result.append("\n🏥 ГОСУДАРСТВЕННЫЕ ПОЛИКЛИНИКИ (Бесплатно):\n")
|
| 873 |
+
else:
|
| 874 |
+
result.append("\n🏥 DAVLAT POLIKLINIKALARI (Bepul):\n")
|
| 875 |
+
|
| 876 |
+
for idx, clinic in enumerate(davlat_clinics, 1):
|
| 877 |
+
result.append(f"\n{idx}️⃣ {clinic['name']}")
|
| 878 |
+
result.append(f" 📍 {clinic['address']}")
|
| 879 |
+
result.append(f" 📞 {clinic['phone']}")
|
| 880 |
+
result.append(f" ⏰ {clinic['working_hours']}")
|
| 881 |
+
result.append(f" ⭐ {clinic['rating']}/5.0")
|
| 882 |
+
|
| 883 |
+
# Xususiy klinikalar
|
| 884 |
+
if xususiy_clinics:
|
| 885 |
+
if language == "eng":
|
| 886 |
+
result.append("\n\n🏥 PRIVATE CLINICS:\n")
|
| 887 |
+
elif language == "rus":
|
| 888 |
+
result.append("\n\n🏥 ЧАСТНЫЕ КЛИНИКИ:\n")
|
| 889 |
+
else:
|
| 890 |
+
result.append("\n\n🏥 XUSUSIY KLINIKALAR:\n")
|
| 891 |
+
|
| 892 |
+
for idx, clinic in enumerate(xususiy_clinics, 1):
|
| 893 |
+
result.append(f"\n{idx}️⃣ {clinic['name']}")
|
| 894 |
+
result.append(f" 📍 {clinic['address']}")
|
| 895 |
+
result.append(f" 📞 {clinic['phone']}")
|
| 896 |
+
result.append(f" ⏰ {clinic['working_hours']}")
|
| 897 |
+
result.append(f" 💰 {clinic['price_range']}")
|
| 898 |
+
result.append(f" ⭐ {clinic['rating']}/5.0")
|
| 899 |
+
|
| 900 |
+
return "\n".join(result)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
# ==================== HELPER FUNCTIONS ====================
|
| 904 |
+
|
| 905 |
+
async def process_name_input(websocket: WebSocket, case_id: str, name_text: str):
|
| 906 |
+
"""
|
| 907 |
+
Ism-familiyani qayta ishlash
|
| 908 |
+
|
| 909 |
+
Bu funksiya brigadadan KEYIN chaqiriladi
|
| 910 |
+
"""
|
| 911 |
+
try:
|
| 912 |
+
logger.info(f"👤 Ism-familiya qabul qilindi: '{name_text}'")
|
| 913 |
+
|
| 914 |
+
current_case = db.get_case(case_id)
|
| 915 |
+
language = current_case.get("language", "uzb")
|
| 916 |
+
|
| 917 |
+
# Ism-familiyani saqlash
|
| 918 |
+
db.update_case(case_id, {
|
| 919 |
+
"patient_full_name": name_text,
|
| 920 |
+
"waiting_for_name_input": False
|
| 921 |
+
})
|
| 922 |
+
|
| 923 |
+
# Bemor tarixini tekshirish
|
| 924 |
+
patient_history = db.get_patient_statistics(name_text)
|
| 925 |
+
|
| 926 |
+
if patient_history and patient_history.get("total_cases", 0) > 0:
|
| 927 |
+
previous_count = patient_history.get("total_cases")
|
| 928 |
+
logger.info(f"📋 Bemor tarixi topildi: {previous_count} ta oldingi murojat")
|
| 929 |
+
|
| 930 |
+
db.update_case(case_id, {
|
| 931 |
+
"previous_cases_count": previous_count
|
| 932 |
+
})
|
| 933 |
+
|
| 934 |
+
# Tasdiq xabari
|
| 935 |
+
if language == "eng":
|
| 936 |
+
confirmation = f"Thank you, {name_text}. The ambulance will arrive shortly. Please stay calm."
|
| 937 |
+
elif language == "rus":
|
| 938 |
+
confirmation = f"Спасибо, {name_text}. Скорая помощь скоро прибудет. Пожалуйста, сохраняйте спокойствие."
|
| 939 |
+
else:
|
| 940 |
+
confirmation = f"Rahmat, {name_text}. Brigada tez orada yetib keladi. Iltimos, xotirjam bo'ling."
|
| 941 |
+
|
| 942 |
+
await send_ai_response(websocket, case_id, confirmation, language)
|
| 943 |
+
|
| 944 |
+
# Dispetcherga yangilanish
|
| 945 |
+
await notify_dispatchers({
|
| 946 |
+
"type": "name_received",
|
| 947 |
+
"case": db.get_case(case_id)
|
| 948 |
+
})
|
| 949 |
+
|
| 950 |
+
except Exception as e:
|
| 951 |
+
logger.error(f"❌ process_name_input xatoligi: {e}", exc_info=True)
|
| 952 |
+
|
| 953 |
+
|
| 954 |
+
async def handle_location_clarification(websocket: WebSocket, case_id: str, user_input: str, input_type: str) -> bool:
|
| 955 |
+
"""
|
| 956 |
+
Manzilni aniqlashtirish (mahalla)
|
| 957 |
+
|
| 958 |
+
Returns:
|
| 959 |
+
True - agar mahalla kutilgan bo'lsa va qayta ishlandi
|
| 960 |
+
False - agar mahalla kutilmagan
|
| 961 |
+
"""
|
| 962 |
+
try:
|
| 963 |
+
current_case = db.get_case(case_id)
|
| 964 |
+
|
| 965 |
+
if not current_case.get("waiting_for_mahalla_input"):
|
| 966 |
+
return False
|
| 967 |
+
|
| 968 |
+
logger.info(f"🏘️ Mahalla aniqlashtirilmoqda: '{user_input}'")
|
| 969 |
+
|
| 970 |
+
district_id = current_case.get("selected_district")
|
| 971 |
+
district_name = current_case.get("district")
|
| 972 |
+
language = current_case.get("language", "uzb")
|
| 973 |
+
|
| 974 |
+
if not district_id:
|
| 975 |
+
logger.error("❌ District ID topilmadi")
|
| 976 |
+
return False
|
| 977 |
+
|
| 978 |
+
# Mahalla fuzzy match
|
| 979 |
+
mahalla_match = find_mahalla_fuzzy(district_name, user_input, threshold=0.35)
|
| 980 |
+
|
| 981 |
+
if mahalla_match:
|
| 982 |
+
mahalla_full_name = get_mahalla_display_name(mahalla_match)
|
| 983 |
+
logger.info(f"✅ Mahalla topildi: {mahalla_full_name}")
|
| 984 |
+
|
| 985 |
+
# Mahalla koordinatalarini olish
|
| 986 |
+
mahalla_coords = get_mahalla_coordinates(district_name, mahalla_match)
|
| 987 |
+
|
| 988 |
+
if mahalla_coords:
|
| 989 |
+
db.update_case(case_id, {
|
| 990 |
+
"selected_mahalla": mahalla_full_name,
|
| 991 |
+
"mahalla_lat": mahalla_coords['lat'],
|
| 992 |
+
"mahalla_lon": mahalla_coords['lon'],
|
| 993 |
+
"geocoded_lat": mahalla_coords['lat'],
|
| 994 |
+
"geocoded_lon": mahalla_coords['lon'],
|
| 995 |
+
"waiting_for_mahalla_input": False,
|
| 996 |
+
"mahalla_retry_count": 0
|
| 997 |
+
})
|
| 998 |
+
|
| 999 |
+
# Brigadani topish
|
| 1000 |
+
await process_gps_and_brigade(
|
| 1001 |
+
websocket,
|
| 1002 |
+
case_id,
|
| 1003 |
+
mahalla_coords['lat'],
|
| 1004 |
+
mahalla_coords['lon']
|
| 1005 |
+
)
|
| 1006 |
+
|
| 1007 |
+
return True
|
| 1008 |
+
|
| 1009 |
+
# Mahalla topilmadi
|
| 1010 |
+
retry_count = current_case.get("mahalla_retry_count", 0) + 1
|
| 1011 |
+
|
| 1012 |
+
if retry_count >= 3:
|
| 1013 |
+
# 3 marta topilmasa, faqat tuman bilan davom etamiz
|
| 1014 |
+
logger.warning("⚠️ Mahalla 3 marta topilmadi, tuman markazidan foydalaniladi")
|
| 1015 |
+
|
| 1016 |
+
district_gps = get_gps_for_district(district_id)
|
| 1017 |
+
|
| 1018 |
+
if district_gps:
|
| 1019 |
+
db.update_case(case_id, {
|
| 1020 |
+
"geocoded_lat": district_gps['lat'],
|
| 1021 |
+
"geocoded_lon": district_gps['lon'],
|
| 1022 |
+
"waiting_for_mahalla_input": False,
|
| 1023 |
+
"mahalla_retry_count": 0
|
| 1024 |
+
})
|
| 1025 |
+
|
| 1026 |
+
await process_gps_and_brigade(
|
| 1027 |
+
websocket,
|
| 1028 |
+
case_id,
|
| 1029 |
+
district_gps['lat'],
|
| 1030 |
+
district_gps['lon']
|
| 1031 |
+
)
|
| 1032 |
+
|
| 1033 |
+
return True
|
| 1034 |
+
|
| 1035 |
+
# Qayta so'rash
|
| 1036 |
+
db.update_case(case_id, {"mahalla_retry_count": retry_count})
|
| 1037 |
+
|
| 1038 |
+
mahallas_list = format_mahallas_list(get_mahallas_by_district(district_name))
|
| 1039 |
+
|
| 1040 |
+
response = f"Mahalla nomini tushunolmadim. Iltimos, quyidagilardan birini tanlang:\n\n{mahallas_list}"
|
| 1041 |
+
await send_ai_response(websocket, case_id, response, language)
|
| 1042 |
+
|
| 1043 |
+
return True
|
| 1044 |
+
|
| 1045 |
+
except Exception as e:
|
| 1046 |
+
logger.error(f"❌ handle_location_clarification xatoligi: {e}", exc_info=True)
|
| 1047 |
+
return False
|
| 1048 |
+
|
| 1049 |
+
|
| 1050 |
+
# ==================== PERIODIC CLEANUP ====================
|
| 1051 |
+
|
| 1052 |
+
async def periodic_cleanup():
|
| 1053 |
+
"""Eski audio fayllarni tozalash (har 1 soatda)"""
|
| 1054 |
+
while True:
|
| 1055 |
+
try:
|
| 1056 |
+
await asyncio.sleep(3600) # 1 soat
|
| 1057 |
+
logger.info("🧹 Periodic cleanup boshlandi...")
|
| 1058 |
+
|
| 1059 |
+
audio_dir = "static/audio"
|
| 1060 |
+
if os.path.exists(audio_dir):
|
| 1061 |
+
current_time = time.time()
|
| 1062 |
+
for filename in os.listdir(audio_dir):
|
| 1063 |
+
file_path = os.path.join(audio_dir, filename)
|
| 1064 |
+
if os.path.isfile(file_path):
|
| 1065 |
+
if current_time - os.path.getmtime(file_path) > 3600: # 1 soat
|
| 1066 |
+
os.remove(file_path)
|
| 1067 |
+
logger.info(f"🗑️ Eski fayl o'chirildi: {filename}")
|
| 1068 |
+
except Exception as e:
|
| 1069 |
+
logger.error(f"❌ Periodic cleanup xatoligi: {e}")
|
| 1070 |
+
|
| 1071 |
+
|
| 1072 |
+
@router.on_event("startup")
|
| 1073 |
+
async def startup_event():
|
| 1074 |
+
"""Server ishga tushganda"""
|
| 1075 |
+
asyncio.create_task(periodic_cleanup())
|
| 1076 |
+
logger.info("🚀 Periodic cleanup task ishga tushdi")
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
# ==================== CASE MANAGEMENT APIs ====================
|
| 1080 |
+
|
| 1081 |
+
@router.get("/cases")
|
| 1082 |
+
async def get_all_cases(status: Optional[str] = None):
|
| 1083 |
+
"""Barcha caselarni olish"""
|
| 1084 |
+
try:
|
| 1085 |
+
cases = db.get_all_cases(status=status)
|
| 1086 |
+
return cases
|
| 1087 |
+
except Exception as e:
|
| 1088 |
+
logger.error(f"❌ Cases olishda xatolik: {e}")
|
| 1089 |
+
raise HTTPException(status_code=500, detail="Server xatoligi")
|
| 1090 |
+
|
| 1091 |
+
|
| 1092 |
+
@router.get("/cases/{case_id}")
|
| 1093 |
+
async def get_case(case_id: str):
|
| 1094 |
+
"""Bitta case ma'lumotlarini olish"""
|
| 1095 |
+
case = db.get_case(case_id)
|
| 1096 |
+
|
| 1097 |
+
if not case:
|
| 1098 |
+
raise HTTPException(status_code=404, detail="Case topilmadi")
|
| 1099 |
+
|
| 1100 |
+
return case
|
| 1101 |
+
|
| 1102 |
+
|
| 1103 |
+
@router.patch("/cases/{case_id}")
|
| 1104 |
+
async def update_case(case_id: str, updates: CaseUpdate):
|
| 1105 |
+
"""Case ni yangilash"""
|
| 1106 |
+
update_data = updates.dict(exclude_unset=True)
|
| 1107 |
+
|
| 1108 |
+
success = db.update_case(case_id, update_data)
|
| 1109 |
+
|
| 1110 |
+
if not success:
|
| 1111 |
+
raise HTTPException(status_code=404, detail="Case topilmadi")
|
| 1112 |
+
|
| 1113 |
+
updated_case = db.get_case(case_id)
|
| 1114 |
+
|
| 1115 |
+
# Dispetcherlarga yangilanish
|
| 1116 |
+
await notify_dispatchers({
|
| 1117 |
+
"type": "case_updated",
|
| 1118 |
+
"case": updated_case
|
| 1119 |
+
})
|
| 1120 |
+
|
| 1121 |
+
return updated_case
|
| 1122 |
+
|
| 1123 |
+
|
| 1124 |
+
|
| 1125 |
+
|
app/core/__pycache__/auth.cpython-313.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
app/core/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (8.03 kB). View file
|
|
|
app/core/__pycache__/connections.cpython-313.pyc
ADDED
|
Binary file (408 Bytes). View file
|
|
|
app/core/__pycache__/database.cpython-313.pyc
ADDED
|
Binary file (22.5 kB). View file
|
|
|
app/core/auth.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/core/auth.py
|
| 2 |
+
"""
|
| 3 |
+
Oddiy autentifikatsiya tizimi (MVP uchun)
|
| 4 |
+
Session-based authentication
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import secrets
|
| 8 |
+
from typing import Optional, Dict
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Session storage (xotirada saqlash, MVP uchun yetarli)
|
| 15 |
+
active_sessions: Dict[str, Dict] = {}
|
| 16 |
+
|
| 17 |
+
# Session amal qilish muddati
|
| 18 |
+
SESSION_LIFETIME = timedelta(hours=12)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def create_session(dispatcher: Dict) -> str:
|
| 22 |
+
"""
|
| 23 |
+
Dispetcher uchun sessiya yaratish
|
| 24 |
+
Returns: session_token
|
| 25 |
+
"""
|
| 26 |
+
# Tasodifiy session token yaratish
|
| 27 |
+
session_token = secrets.token_urlsafe(32)
|
| 28 |
+
|
| 29 |
+
# Session ma'lumotlarini saqlash
|
| 30 |
+
active_sessions[session_token] = {
|
| 31 |
+
"dispatcher_id": dispatcher['id'],
|
| 32 |
+
"username": dispatcher['username'],
|
| 33 |
+
"full_name": dispatcher['full_name'],
|
| 34 |
+
"district": dispatcher['district'],
|
| 35 |
+
"created_at": datetime.now(),
|
| 36 |
+
"expires_at": datetime.now() + SESSION_LIFETIME
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
logger.info(f"✅ Yangi sessiya yaratildi: {dispatcher['username']}")
|
| 40 |
+
return session_token
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def verify_session(session_token: str) -> Optional[Dict]:
|
| 44 |
+
"""
|
| 45 |
+
Session tokenni tekshirish
|
| 46 |
+
Returns: dispatcher ma'lumotlari yoki None
|
| 47 |
+
"""
|
| 48 |
+
session = active_sessions.get(session_token)
|
| 49 |
+
|
| 50 |
+
if not session:
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
# Session muddati tugaganligini tekshirish
|
| 54 |
+
if datetime.now() > session['expires_at']:
|
| 55 |
+
logger.warning(f"⚠️ Session muddati tugagan: {session['username']}")
|
| 56 |
+
del active_sessions[session_token]
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
+
return session
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def delete_session(session_token: str) -> bool:
|
| 63 |
+
"""Session ni o'chirish (logout)"""
|
| 64 |
+
if session_token in active_sessions:
|
| 65 |
+
username = active_sessions[session_token]['username']
|
| 66 |
+
del active_sessions[session_token]
|
| 67 |
+
logger.info(f"✅ Session o'chirildi: {username}")
|
| 68 |
+
return True
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_active_dispatchers() -> list:
|
| 73 |
+
"""Hozir online bo'lgan dispetcherlar"""
|
| 74 |
+
current_time = datetime.now()
|
| 75 |
+
|
| 76 |
+
online_dispatchers = []
|
| 77 |
+
for token, session in active_sessions.items():
|
| 78 |
+
if session['expires_at'] > current_time:
|
| 79 |
+
online_dispatchers.append({
|
| 80 |
+
"username": session['username'],
|
| 81 |
+
"full_name": session['full_name'],
|
| 82 |
+
"district": session['district']
|
| 83 |
+
})
|
| 84 |
+
|
| 85 |
+
return online_dispatchers
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Vaqti-vaqti bilan eski sessionlarni tozalash
|
| 89 |
+
def cleanup_expired_sessions():
|
| 90 |
+
"""Muddati o'tgan sessionlarni o'chirish"""
|
| 91 |
+
current_time = datetime.now()
|
| 92 |
+
expired_tokens = [
|
| 93 |
+
token for token, session in active_sessions.items()
|
| 94 |
+
if session['expires_at'] <= current_time
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
for token in expired_tokens:
|
| 98 |
+
username = active_sessions[token]['username']
|
| 99 |
+
del active_sessions[token]
|
| 100 |
+
logger.info(f"🗑️ Eski session tozalandi: {username}")
|
| 101 |
+
|
| 102 |
+
return len(expired_tokens)
|
app/core/config.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/core/config.py - YANGILANGAN (3 RISK TIZIMI)
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Tizim konfiguratsiyasi va Gemini uchun SYSTEM_INSTRUCTION
|
| 5 |
+
YANGILANISHLAR:
|
| 6 |
+
- 3 risk tizimi (Qizil, Sariq, Yashil)
|
| 7 |
+
- Klinika tavsiya logikasi
|
| 8 |
+
- Operator ulanish qoidalari
|
| 9 |
+
- Brigadadan KEYIN ism so'rash mantiq
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
# ==================== SYSTEM_INSTRUCTION ====================
|
| 13 |
+
|
| 14 |
+
SYSTEM_INSTRUCTION = """Sen "Help.me" tez yordam tizimining virtual yordamchisisan. Sening vazifang – bemor bilan tabiiy va samarali muloqot o'rnatib, vaziyatning jiddiyligini ANIQ aniqlash.
|
| 15 |
+
|
| 16 |
+
[TILLAR - KRITIK QOIDA]
|
| 17 |
+
Sen 3 tilda gaplasha olasan:
|
| 18 |
+
- O'ZBEKCHA (Lotin yoki Kirill)
|
| 19 |
+
- INGLIZCHA
|
| 20 |
+
- RUSCHA
|
| 21 |
+
|
| 22 |
+
🚨 MUHIM: Bemor qaysi tilda gapirsa, SHU TILDA javob ber!
|
| 23 |
+
🚨 CRITICAL: JSON javobda ALBATTA "language" maydonini qaytaring!
|
| 24 |
+
|
| 25 |
+
[RISK DARAJALARI - ANIQ TASNIF]
|
| 26 |
+
|
| 27 |
+
🔴 **QIZIL (Emergency - Hayotga xavfli):**
|
| 28 |
+
Quyidagi holatlarda DOIMO qizil:
|
| 29 |
+
- Yurak bilan bog'liq: ko'krakda og'riq, nafas qisilishi, yurak urishi buzilgan
|
| 30 |
+
- Nafas olish muammosi: nafas ololmayapti, bo'g'ilyapti
|
| 31 |
+
- Hushsizlik yoki ong buzilishi
|
| 32 |
+
- Qon ketish (kuchli)
|
| 33 |
+
- Avariya, boshga zarba, suyak sinishi
|
| 34 |
+
- Juda yuqori harorat (39.5°C dan yuqori)
|
| 35 |
+
- Kuchli og'riqlar (10 ballik shkalada 8+)
|
| 36 |
+
- Zaharlanish shubhasi
|
| 37 |
+
|
| 38 |
+
→ HARAKAT: TEZ YORDAM BRIGADA CHAQIR
|
| 39 |
+
→ Manzil so'ra (tuman + mahalla)
|
| 40 |
+
→ Brigada jo'natgandan KEYIN ism-familiya so'ra
|
| 41 |
+
|
| 42 |
+
🟡 **SARIQ (Uncertain - Noaniq):**
|
| 43 |
+
Quyidagi holatlarda sariq:
|
| 44 |
+
- Simptomlar noaniq yoki ziddiyatli
|
| 45 |
+
- Bemorning gapi tushunarsiz
|
| 46 |
+
- Qo'shimcha ma'lumot kerak
|
| 47 |
+
- 2-3 marta so'ragandan keyin ham aniq emas
|
| 48 |
+
|
| 49 |
+
→ HARAKAT: ANIQLASHTIR
|
| 50 |
+
→ Agar 3 urinishdan keyin tushunmasa: OPERATOR KERAK
|
| 51 |
+
→ JSON da: "operator_needed": true
|
| 52 |
+
|
| 53 |
+
🟢 **YASHIL (Non-urgent - Jiddiy emas):**
|
| 54 |
+
Quyidagi holatlarda yashil:
|
| 55 |
+
- Oddiy kasalliklar: shamollash (DOVD), gripp
|
| 56 |
+
- Bosh og'rig'i, bel og'rig'i (o'rtacha)
|
| 57 |
+
- Past yoki o'rtacha harorat (37-38.5°C)
|
| 58 |
+
- Yengil simptomlar
|
| 59 |
+
- Umumiy holsizlik, charchash
|
| 60 |
+
- Oshqozon buzilishi (yengil)
|
| 61 |
+
|
| 62 |
+
→ HARAKAT: KLINIKA/DOKTOR TAVSIYA QIL
|
| 63 |
+
→ Bemorga 2 variant taklif qil:
|
| 64 |
+
1. Davlat poliklinikasi (bepul)
|
| 65 |
+
2. Xususiy klinika (pullik, tezroq)
|
| 66 |
+
→ Manzil SO'RAMA! (yashil holatda manzil kerak emas)
|
| 67 |
+
|
| 68 |
+
[ISH JARAYONI - BOSQICHMA-BOSQICH]
|
| 69 |
+
|
| 70 |
+
**1-QADAM: TANISHUV VA TAHLIL**
|
| 71 |
+
- O'zingni tanishtir
|
| 72 |
+
- Bemorning muammosini so'ra
|
| 73 |
+
- Simptomlarni diqqat bilan eshit
|
| 74 |
+
|
| 75 |
+
**2-QADAM: RISK DARAJASINI ANIQLASH**
|
| 76 |
+
- Simptomlarni yuqoridagi 3 kategoriyaga solishtir
|
| 77 |
+
- Agar shubha bo'lsa, xavfsizlik tomonga moyil bo'l (qizilga yaqin holatlarda qizil deb baho
|
| 78 |
+
|
| 79 |
+
la)
|
| 80 |
+
|
| 81 |
+
**3-QADAM: HARAKATNI AMALGA OSHIR**
|
| 82 |
+
|
| 83 |
+
A) QIZIL HOLAT:
|
| 84 |
+
1. "Tezda yordam yuboramiz!"
|
| 85 |
+
2. Manzil so'ra: "Qaysi tumanda yashaysiz?"
|
| 86 |
+
3. Mahallani aniqla
|
| 87 |
+
4. Brigada jo'natildi (backend avtomatik)
|
| 88 |
+
5. ENDI ism-familiya so'ra: "Brigada yo'lda. To'liq ism-familiyangizni aytib bering."
|
| 89 |
+
|
| 90 |
+
B) SARIQ HOLAT:
|
| 91 |
+
1. Aniqlashtiruvchi savollar ber
|
| 92 |
+
2. Agar 3 marta tushunmasa: "Sizni operatorga ulayman"
|
| 93 |
+
3. JSON da: "operator_needed": true
|
| 94 |
+
|
| 95 |
+
C) YASHIL HOLAT:
|
| 96 |
+
1. Xotirjamlik ber: "Bu belgilar jiddiy emas"
|
| 97 |
+
2. 2 variant taklif qil:
|
| 98 |
+
- "Davlat poliklinikasiga murojaat qilishingiz mumkin (bepul)"
|
| 99 |
+
- "Yoki xususiy klinikaga (tezroq, pullik)"
|
| 100 |
+
3. Bemor tanlasa, mos klinikalar ro'yxatini ber
|
| 101 |
+
4. JSON da: "action": "offer_clinic_recommendation"
|
| 102 |
+
|
| 103 |
+
[JSON FORMAT - JUDA MUHIM]
|
| 104 |
+
|
| 105 |
+
Har doim quyidagi formatda javob ber:
|
| 106 |
+
|
| 107 |
+
```json
|
| 108 |
+
{
|
| 109 |
+
"language": "uzb" | "eng" | "rus",
|
| 110 |
+
"risk_level": "qizil" | "sariq" | "yashil",
|
| 111 |
+
|
| 112 |
+
// Umumiy
|
| 113 |
+
"symptoms_extracted": "batafsil simptomlar tavsifi",
|
| 114 |
+
"response_text": "bemorga javob matni",
|
| 115 |
+
"analysis_notes": "ichki tahlil (bemor ko'rmaydi)",
|
| 116 |
+
|
| 117 |
+
// QIZIL uchun
|
| 118 |
+
"address_extracted": "Chilonzor tumani, Katta Chilonzor mahallasi" | null,
|
| 119 |
+
"district_extracted": "Chilonzor" | null,
|
| 120 |
+
"pre_arrival_instruction_text": "Brigadagacha nima qilish kerak" | null,
|
| 121 |
+
|
| 122 |
+
// SARIQ uchun
|
| 123 |
+
"uncertainty_reason": "nima noaniq" | null,
|
| 124 |
+
"operator_needed": true | false,
|
| 125 |
+
"clarifying_question": "aniqlashtiruvchi savol" | null,
|
| 126 |
+
|
| 127 |
+
// YASHIL uchun
|
| 128 |
+
"action": "offer_clinic_recommendation" | null,
|
| 129 |
+
"preferred_clinic_type": "davlat" | "xususiy" | "both" | null,
|
| 130 |
+
"recommended_specialty": "Terapiya" | "Kardiologiya" | null
|
| 131 |
+
}
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
[MISOLLAR]
|
| 135 |
+
|
| 136 |
+
**MISOL 1: QIZIL**
|
| 137 |
+
Bemor: "Ko'kragim juda og'riyapti, nafasim qisilib turibdi"
|
| 138 |
+
|
| 139 |
+
Sen:
|
| 140 |
+
```json
|
| 141 |
+
{
|
| 142 |
+
"language": "uzb",
|
| 143 |
+
"risk_level": "qizil",
|
| 144 |
+
"symptoms_extracted": "Ko'krakda kuchli og'riq, nafas qisilishi",
|
| 145 |
+
"response_text": "Bu jiddiy holat! Tezda yordam yuboramiz. Qaysi tumanda yashaysiz?",
|
| 146 |
+
"address_extracted": null,
|
| 147 |
+
"district_extracted": null,
|
| 148 |
+
"pre_arrival_instruction_text": "Xotirjam bo'lib o'tiring, harakatni kamaytiring. Deraza ochib, toza havo kiriting.",
|
| 149 |
+
"analysis_notes": "Cardiac emergency suspected. Immediate ambulance needed.",
|
| 150 |
+
"uncertainty_reason": null,
|
| 151 |
+
"operator_needed": false,
|
| 152 |
+
"action": null,
|
| 153 |
+
"preferred_clinic_type": null
|
| 154 |
+
}
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
**MISOL 2: SARIQ**
|
| 158 |
+
Bemor: "Umm... bilmadim, shunchaki yomon"
|
| 159 |
+
|
| 160 |
+
Sen (1-urinish):
|
| 161 |
+
```json
|
| 162 |
+
{
|
| 163 |
+
"language": "uzb",
|
| 164 |
+
"risk_level": "sariq",
|
| 165 |
+
"symptoms_extracted": "Aniq emas, umumiy yomonlik",
|
| 166 |
+
"response_text": "Tushundim. Iltimos, aniqroq ayting: boshingizmi og'riyapti, haroratingiz bormi, yoki boshqa nima sezilmoqda?",
|
| 167 |
+
"clarifying_question": "Aniq qaysi simptomlar bor?",
|
| 168 |
+
"uncertainty_reason": "Symptoms too vague",
|
| 169 |
+
"operator_needed": false,
|
| 170 |
+
"analysis_notes": "Need more specific information. Attempt 1/3",
|
| 171 |
+
"address_extracted": null,
|
| 172 |
+
"action": null
|
| 173 |
+
}
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
(Agar 3 urinishdan keyin ham tushunmasa):
|
| 177 |
+
```json
|
| 178 |
+
{
|
| 179 |
+
"language": "uzb",
|
| 180 |
+
"risk_level": "sariq",
|
| 181 |
+
"symptoms_extracted": "Aniqlanmadi",
|
| 182 |
+
"response_text": "Sizni yaxshi tushunolmadim. Operatorga ulayman, ular sizga yordam berishadi.",
|
| 183 |
+
"operator_needed": true,
|
| 184 |
+
"uncertainty_reason": "Could not understand after 3 attempts",
|
| 185 |
+
"analysis_notes": "Escalating to human operator",
|
| 186 |
+
"address_extracted": null,
|
| 187 |
+
"action": null
|
| 188 |
+
}
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
**MISOL 3: YASHIL**
|
| 192 |
+
Bemor: "Boshim og'riyapti va haroratim 37.5"
|
| 193 |
+
|
| 194 |
+
Sen:
|
| 195 |
+
```json
|
| 196 |
+
{
|
| 197 |
+
"language": "uzb",
|
| 198 |
+
"risk_level": "yashil",
|
| 199 |
+
"symptoms_extracted": "Bosh og'rig'i, harorat 37.5°C",
|
| 200 |
+
"response_text": "Tushundim. Bu belgilar bilan tez yordam chaqirish shart emas. Sizga shifokor konsultatsiyasi tavsiya qilaman. Davlat poliklinikasiga (bepul) yoki xususiy klinikaga (tezroq, pullik) murojaat qilishni xohlaysizmi?",
|
| 201 |
+
"action": "offer_clinic_recommendation",
|
| 202 |
+
"preferred_clinic_type": "both",
|
| 203 |
+
"recommended_specialty": "Terapiya",
|
| 204 |
+
"analysis_notes": "Mild symptoms. Clinic recommendation appropriate.",
|
| 205 |
+
"address_extracted": null,
|
| 206 |
+
"operator_needed": false,
|
| 207 |
+
"uncertainty_reason": null
|
| 208 |
+
}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
[MUHIM QOIDALAR]
|
| 212 |
+
|
| 213 |
+
1. ✅ "language" maydoni MAJBURIY
|
| 214 |
+
2. ✅ "risk_level" har doim bo'lishi kerak
|
| 215 |
+
3. ✅ Qizil holatda manzil AVVAL so'raladi, ism-familiya KEYIN
|
| 216 |
+
4. ✅ Yashil holatda manzil SO'RALMAYD
|
| 217 |
+
5. ✅ Sariq holatda 3 urinishdan keyin operator
|
| 218 |
+
6. ✅ Bemor tilida javob ber
|
| 219 |
+
7. ✅ Xavfsiz tomonga moyil bo'l (shubhada qizil)
|
| 220 |
+
8. ✅ JSON format ANIQ rioya qil
|
| 221 |
+
|
| 222 |
+
[XULOSA]
|
| 223 |
+
- QIZIL → Manzil → Brigada → Ism
|
| 224 |
+
- SARIQ → Aniqlashtir → (3x) → Operator
|
| 225 |
+
- YASHIL → Klinika tavsiya (manzilsiz)
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
# ==================== API KEYS ====================
|
| 229 |
+
|
| 230 |
+
GEMINI_API_KEY = "AIzaSyC1iBX1w_bn8w1c6Ask2Gu9og8m_L1Vs9A"
|
| 231 |
+
|
| 232 |
+
# ==================== GEOCODING SETTINGS ====================
|
| 233 |
+
|
| 234 |
+
NOMINATIM_USER_AGENT = "helpme_hackathon_mvp_2025"
|
| 235 |
+
GEOCODING_TIMEOUT = 10
|
| 236 |
+
|
| 237 |
+
TASHKENT_BOUNDS = {
|
| 238 |
+
"lat_min": 41.20,
|
| 239 |
+
"lat_max": 41.35,
|
| 240 |
+
"lon_min": 69.10,
|
| 241 |
+
"lon_max": 69.35
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
# ==================== BRIGADE SETTINGS ====================
|
| 245 |
+
|
| 246 |
+
MAX_BRIGADE_DISTANCE_KM = 15
|
| 247 |
+
GPS_VERIFICATION_MAX_DISTANCE_KM = 5.0
|
| 248 |
+
GPS_VERIFICATION_ENABLED = True
|
| 249 |
+
|
| 250 |
+
# ==================== DEMO/MVP SETTINGS ====================
|
| 251 |
+
|
| 252 |
+
USE_DEMO_GPS = False
|
| 253 |
+
GPS_NOISE_KM = 0.3
|
| 254 |
+
|
| 255 |
+
# ==================== TTS SETTINGS ====================
|
| 256 |
+
|
| 257 |
+
TTS_MODELS_AVAILABLE = {
|
| 258 |
+
"uzb": True,
|
| 259 |
+
"eng": True,
|
| 260 |
+
"rus": True
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
# ==================== YANGI: CLINIC SETTINGS ====================
|
| 264 |
+
|
| 265 |
+
# Klinika turlari
|
| 266 |
+
CLINIC_TYPES = ["davlat", "xususiy"]
|
| 267 |
+
|
| 268 |
+
# Operator ulanish sozlamalari
|
| 269 |
+
MAX_UNCERTAINTY_ATTEMPTS = 3 # 3 marta tushunmasa operator
|
| 270 |
+
OPERATOR_QUEUE_ENABLED = True # MVP uchun simulyatsiya
|
| 271 |
+
|
| 272 |
+
# Case turlari
|
| 273 |
+
CASE_TYPES = {
|
| 274 |
+
"EMERGENCY": "emergency",
|
| 275 |
+
"PUBLIC_CLINIC": "public_clinic",
|
| 276 |
+
"PRIVATE_CLINIC": "private_clinic",
|
| 277 |
+
"UNCERTAIN": "uncertain"
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
# ==================== LOGGING SETTINGS ====================
|
| 281 |
+
|
| 282 |
+
LOG_LEVEL = "INFO"
|
| 283 |
+
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
app/core/connections.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/core/connections.py
|
| 2 |
+
|
| 3 |
+
from fastapi import WebSocket
|
| 4 |
+
from typing import Set
|
| 5 |
+
|
| 6 |
+
# Bemorlar uchun aktiv WebSocket ulanishlari to'plami
|
| 7 |
+
active_connections: Set[WebSocket] = set()
|
| 8 |
+
|
| 9 |
+
# Dispetcherlar uchun aktiv WebSocket ulanishlari to'plami
|
| 10 |
+
dispatcher_connections: Set[WebSocket] = set()
|
app/core/database.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/core/database.py - TO'LIQ TAYYOR VERSIYA
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
JSON-based database manager for MVP
|
| 5 |
+
Barcha ma'lumotlarni data/ papkasida JSON fayllar sifatida saqlaydi
|
| 6 |
+
|
| 7 |
+
YANGILANGAN: Brigade CRUD funksiyalari qo'shildi
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from typing import Optional, List, Dict, Any
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
import logging
|
| 16 |
+
from difflib import SequenceMatcher
|
| 17 |
+
import random
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
# JSON fayllarning yo'llari
|
| 22 |
+
DATA_DIR = Path("data")
|
| 23 |
+
CASES_FILE = DATA_DIR / "cases.json"
|
| 24 |
+
MESSAGES_FILE = DATA_DIR / "messages.json"
|
| 25 |
+
DISPATCHERS_FILE = DATA_DIR / "dispatchers.json"
|
| 26 |
+
BRIGADES_FILE = DATA_DIR / "brigades.json"
|
| 27 |
+
PATIENT_HISTORY_FILE = DATA_DIR / "patient_history.json"
|
| 28 |
+
CLINICS_FILE = DATA_DIR / "clinics.json"
|
| 29 |
+
DOCTORS_FILE = DATA_DIR / "doctors.json"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class JSONDatabase:
|
| 33 |
+
"""JSON fayllar bilan ishlovchi database manager"""
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
"""Data papkasini va JSON fayllarni yaratish"""
|
| 37 |
+
self._ensure_data_directory()
|
| 38 |
+
self._initialize_files()
|
| 39 |
+
|
| 40 |
+
def _ensure_data_directory(self):
|
| 41 |
+
"""data/ papkasini yaratish"""
|
| 42 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 43 |
+
logger.info(f"✅ Data papka tekshirildi: {DATA_DIR}")
|
| 44 |
+
|
| 45 |
+
def _initialize_files(self):
|
| 46 |
+
"""JSON fayllarni boshlang'ich holatda yaratish"""
|
| 47 |
+
if not CASES_FILE.exists() or os.path.getsize(CASES_FILE) == 0:
|
| 48 |
+
self._write_json(CASES_FILE, {"cases": []})
|
| 49 |
+
|
| 50 |
+
if not MESSAGES_FILE.exists() or os.path.getsize(MESSAGES_FILE) == 0:
|
| 51 |
+
self._write_json(MESSAGES_FILE, {"messages": []})
|
| 52 |
+
|
| 53 |
+
if not DISPATCHERS_FILE.exists() or os.path.getsize(DISPATCHERS_FILE) == 0:
|
| 54 |
+
self._write_json(DISPATCHERS_FILE, {"dispatchers": []})
|
| 55 |
+
|
| 56 |
+
if not PATIENT_HISTORY_FILE.exists() or os.path.getsize(PATIENT_HISTORY_FILE) == 0:
|
| 57 |
+
self._write_json(PATIENT_HISTORY_FILE, {"patients": []})
|
| 58 |
+
|
| 59 |
+
if not CLINICS_FILE.exists() or os.path.getsize(CLINICS_FILE) == 0:
|
| 60 |
+
self._write_json(CLINICS_FILE, {"clinics": []})
|
| 61 |
+
|
| 62 |
+
if not DOCTORS_FILE.exists() or os.path.getsize(DOCTORS_FILE) == 0:
|
| 63 |
+
self._write_json(DOCTORS_FILE, {"doctors": []})
|
| 64 |
+
|
| 65 |
+
# Brigade file
|
| 66 |
+
if not BRIGADES_FILE.exists() or os.path.getsize(BRIGADES_FILE) == 0:
|
| 67 |
+
self._write_json(BRIGADES_FILE, {"brigades": []})
|
| 68 |
+
|
| 69 |
+
logger.info("✅ Barcha data fayllar tayyor")
|
| 70 |
+
|
| 71 |
+
def _read_json(self, filepath: Path) -> Dict:
|
| 72 |
+
"""JSON faylni o'qish"""
|
| 73 |
+
try:
|
| 74 |
+
if not filepath.exists():
|
| 75 |
+
return self._get_default_structure(filepath)
|
| 76 |
+
|
| 77 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 78 |
+
return json.load(f)
|
| 79 |
+
except json.JSONDecodeError:
|
| 80 |
+
logger.error(f"❌ JSON parse xatoligi: {filepath}")
|
| 81 |
+
return self._get_default_structure(filepath)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"❌ Fayl o'qishda xatolik: {e}")
|
| 84 |
+
return self._get_default_structure(filepath)
|
| 85 |
+
|
| 86 |
+
def _write_json(self, filepath: Path, data: Dict):
|
| 87 |
+
"""JSON faylga yozish"""
|
| 88 |
+
try:
|
| 89 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 90 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logger.error(f"❌ Fayl yozishda xatolik: {e}")
|
| 93 |
+
|
| 94 |
+
def _get_default_structure(self, filepath: Path) -> Dict:
|
| 95 |
+
"""Fayl uchun default struktura"""
|
| 96 |
+
if filepath.name == "cases.json":
|
| 97 |
+
return {"cases": []}
|
| 98 |
+
elif filepath.name == "messages.json":
|
| 99 |
+
return {"messages": []}
|
| 100 |
+
elif filepath.name == "patient_history.json":
|
| 101 |
+
return {"patients": []}
|
| 102 |
+
elif filepath.name == "clinics.json":
|
| 103 |
+
return {"clinics": []}
|
| 104 |
+
elif filepath.name == "doctors.json":
|
| 105 |
+
return {"doctors": []}
|
| 106 |
+
elif filepath.name == "brigades.json":
|
| 107 |
+
return {"brigades": []}
|
| 108 |
+
else:
|
| 109 |
+
return {}
|
| 110 |
+
|
| 111 |
+
# ==================== CASES CRUD ====================
|
| 112 |
+
|
| 113 |
+
def create_case(self, patient_identifier: str) -> Dict:
|
| 114 |
+
"""Yangi case yaratish"""
|
| 115 |
+
data = self._read_json(CASES_FILE)
|
| 116 |
+
|
| 117 |
+
if 'cases' not in data:
|
| 118 |
+
data['cases'] = []
|
| 119 |
+
|
| 120 |
+
case_id = f"case_{len(data['cases']) + 1:03d}"
|
| 121 |
+
|
| 122 |
+
new_case = {
|
| 123 |
+
"id": case_id,
|
| 124 |
+
"type": "emergency",
|
| 125 |
+
"patient_identifier": patient_identifier,
|
| 126 |
+
"status": "yangi",
|
| 127 |
+
"risk_level": None,
|
| 128 |
+
"address_text": None,
|
| 129 |
+
"symptoms_text": None,
|
| 130 |
+
"district": None,
|
| 131 |
+
"patient_full_name": None,
|
| 132 |
+
"patient_phone": None,
|
| 133 |
+
"previous_cases_count": 0,
|
| 134 |
+
"waiting_for_name_input": False,
|
| 135 |
+
"gps_lat": None,
|
| 136 |
+
"gps_lon": None,
|
| 137 |
+
"geocoded_lat": None,
|
| 138 |
+
"geocoded_lon": None,
|
| 139 |
+
"gps_verified": False,
|
| 140 |
+
"assigned_brigade_id": None,
|
| 141 |
+
"assigned_brigade_name": None,
|
| 142 |
+
"language": "uzb",
|
| 143 |
+
"clinic_type": None,
|
| 144 |
+
"recommended_clinic_id": None,
|
| 145 |
+
"recommended_clinic_name": None,
|
| 146 |
+
"recommended_specialty": None,
|
| 147 |
+
"estimated_price": None,
|
| 148 |
+
"uncertainty_attempts": 0,
|
| 149 |
+
"operator_needed": False,
|
| 150 |
+
"uncertainty_reason": None,
|
| 151 |
+
"created_at": datetime.now().isoformat(),
|
| 152 |
+
"updated_at": datetime.now().isoformat()
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
data['cases'].append(new_case)
|
| 156 |
+
self._write_json(CASES_FILE, data)
|
| 157 |
+
|
| 158 |
+
logger.info(f"✅ Yangi case yaratildi: {case_id}")
|
| 159 |
+
return new_case
|
| 160 |
+
|
| 161 |
+
def get_case(self, case_id: str) -> Optional[Dict]:
|
| 162 |
+
"""Case ni ID bo'yicha olish"""
|
| 163 |
+
data = self._read_json(CASES_FILE)
|
| 164 |
+
|
| 165 |
+
for case in data.get('cases', []):
|
| 166 |
+
if case.get('id') == case_id:
|
| 167 |
+
return case
|
| 168 |
+
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
def get_all_cases(self, status: Optional[str] = None) -> List[Dict]:
|
| 172 |
+
"""Barcha caselarni olish"""
|
| 173 |
+
data = self._read_json(CASES_FILE)
|
| 174 |
+
cases = data.get('cases', [])
|
| 175 |
+
|
| 176 |
+
if status:
|
| 177 |
+
cases = [c for c in cases if c.get('status') == status]
|
| 178 |
+
|
| 179 |
+
cases.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
| 180 |
+
|
| 181 |
+
return cases
|
| 182 |
+
|
| 183 |
+
def update_case(self, case_id: str, updates: Dict) -> bool:
|
| 184 |
+
"""Case ni yangilash"""
|
| 185 |
+
data = self._read_json(CASES_FILE)
|
| 186 |
+
|
| 187 |
+
for case in data.get('cases', []):
|
| 188 |
+
if case.get('id') == case_id:
|
| 189 |
+
case.update(updates)
|
| 190 |
+
case['updated_at'] = datetime.now().isoformat()
|
| 191 |
+
self._write_json(CASES_FILE, data)
|
| 192 |
+
logger.info(f"✅ Case yangilandi: {case_id}")
|
| 193 |
+
return True
|
| 194 |
+
|
| 195 |
+
logger.warning(f"⚠️ Case topilmadi: {case_id}")
|
| 196 |
+
return False
|
| 197 |
+
|
| 198 |
+
# ==================== MESSAGES CRUD ====================
|
| 199 |
+
|
| 200 |
+
def create_message(self, case_id: str, sender: str, content: str) -> Dict:
|
| 201 |
+
"""Yangi xabar yaratish"""
|
| 202 |
+
data = self._read_json(MESSAGES_FILE)
|
| 203 |
+
|
| 204 |
+
if 'messages' not in data:
|
| 205 |
+
data['messages'] = []
|
| 206 |
+
|
| 207 |
+
new_message = {
|
| 208 |
+
"id": f"msg_{len(data['messages']) + 1:04d}",
|
| 209 |
+
"case_id": case_id,
|
| 210 |
+
"sender": sender,
|
| 211 |
+
"content": content,
|
| 212 |
+
"timestamp": datetime.now().isoformat()
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
data['messages'].append(new_message)
|
| 216 |
+
self._write_json(MESSAGES_FILE, data)
|
| 217 |
+
|
| 218 |
+
return new_message
|
| 219 |
+
|
| 220 |
+
def get_messages(self, case_id: str) -> List[Dict]:
|
| 221 |
+
"""Case ning barcha xabarlarini olish"""
|
| 222 |
+
data = self._read_json(MESSAGES_FILE)
|
| 223 |
+
messages = data.get('messages', [])
|
| 224 |
+
|
| 225 |
+
case_messages = [m for m in messages if m.get('case_id') == case_id]
|
| 226 |
+
case_messages.sort(key=lambda x: x.get('timestamp', ''))
|
| 227 |
+
|
| 228 |
+
return case_messages
|
| 229 |
+
|
| 230 |
+
def get_conversation_history(self, case_id: str, limit: int = 10) -> str:
|
| 231 |
+
"""Suhbat tarixini matn shaklida olish"""
|
| 232 |
+
messages = self.get_messages(case_id)
|
| 233 |
+
|
| 234 |
+
if not messages:
|
| 235 |
+
return ""
|
| 236 |
+
|
| 237 |
+
history_lines = []
|
| 238 |
+
for msg in messages[-limit:]:
|
| 239 |
+
sender = "Bemor" if msg.get('sender') == 'user' else "AI"
|
| 240 |
+
content = msg.get('content', '')
|
| 241 |
+
history_lines.append(f"{sender}: {content}")
|
| 242 |
+
|
| 243 |
+
return "\n".join(history_lines)
|
| 244 |
+
|
| 245 |
+
# ==================== PATIENT HISTORY ====================
|
| 246 |
+
|
| 247 |
+
def get_patient_history(self, full_name: str) -> Optional[Dict]:
|
| 248 |
+
"""Bemor tarixini olish"""
|
| 249 |
+
try:
|
| 250 |
+
all_cases = self.get_all_cases()
|
| 251 |
+
|
| 252 |
+
patient_cases = [
|
| 253 |
+
c for c in all_cases
|
| 254 |
+
if c.get('patient_full_name') and
|
| 255 |
+
c.get('patient_full_name').lower() == full_name.lower()
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
if not patient_cases:
|
| 259 |
+
logger.info(f"ℹ️ Bemor topilmadi: {full_name}")
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
risk_levels = {}
|
| 263 |
+
for case in patient_cases:
|
| 264 |
+
risk = case.get('risk_level')
|
| 265 |
+
if risk:
|
| 266 |
+
risk_levels[risk] = risk_levels.get(risk, 0) + 1
|
| 267 |
+
|
| 268 |
+
last_case = max(patient_cases, key=lambda x: x.get('created_at', ''))
|
| 269 |
+
|
| 270 |
+
return {
|
| 271 |
+
"patient_name": full_name,
|
| 272 |
+
"total_cases": len(patient_cases),
|
| 273 |
+
"last_visit": last_case.get('created_at'),
|
| 274 |
+
"risk_levels": risk_levels,
|
| 275 |
+
"cases": patient_cases
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
except Exception as e:
|
| 279 |
+
logger.error(f"❌ Patient history xatoligi: {e}")
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
# ==================== CLINICS CRUD ====================
|
| 283 |
+
|
| 284 |
+
def get_all_clinics(self, clinic_type: Optional[str] = None, district: Optional[str] = None) -> List[Dict]:
|
| 285 |
+
"""Barcha klinikalarni olish"""
|
| 286 |
+
data = self._read_json(CLINICS_FILE)
|
| 287 |
+
clinics = data.get('clinics', [])
|
| 288 |
+
|
| 289 |
+
if clinic_type:
|
| 290 |
+
clinics = [c for c in clinics if c.get('type') == clinic_type]
|
| 291 |
+
|
| 292 |
+
if district:
|
| 293 |
+
clinics = [c for c in clinics if c.get('district', '').lower() == district.lower()]
|
| 294 |
+
|
| 295 |
+
logger.info(f"📍 {len(clinics)} ta klinika topildi")
|
| 296 |
+
return clinics
|
| 297 |
+
|
| 298 |
+
def get_clinic_by_id(self, clinic_id: str) -> Optional[Dict]:
|
| 299 |
+
"""Klinikani ID bo'yicha olish"""
|
| 300 |
+
data = self._read_json(CLINICS_FILE)
|
| 301 |
+
|
| 302 |
+
for clinic in data.get('clinics', []):
|
| 303 |
+
if clinic.get('id') == clinic_id:
|
| 304 |
+
logger.info(f"✅ Klinika topildi: {clinic.get('name')}")
|
| 305 |
+
return clinic
|
| 306 |
+
|
| 307 |
+
logger.warning(f"⚠️ Klinika topilmadi: {clinic_id}")
|
| 308 |
+
return None
|
| 309 |
+
|
| 310 |
+
def search_clinics(self, specialty: Optional[str] = None, min_rating: float = 0.0) -> List[Dict]:
|
| 311 |
+
"""Klinikalarni qidirish"""
|
| 312 |
+
clinics = self.get_all_clinics()
|
| 313 |
+
|
| 314 |
+
if specialty:
|
| 315 |
+
clinics = [
|
| 316 |
+
c for c in clinics
|
| 317 |
+
if specialty.lower() in [s.lower() for s in c.get('specializations', [])]
|
| 318 |
+
]
|
| 319 |
+
|
| 320 |
+
clinics = [c for c in clinics if c.get('rating', 0) >= min_rating]
|
| 321 |
+
clinics.sort(key=lambda x: x.get('rating', 0), reverse=True)
|
| 322 |
+
|
| 323 |
+
logger.info(f"🔍 {len(clinics)} ta klinika topildi")
|
| 324 |
+
return clinics
|
| 325 |
+
|
| 326 |
+
def get_clinic_statistics(self) -> Dict:
|
| 327 |
+
"""Klinikalar statistikasi"""
|
| 328 |
+
clinics = self.get_all_clinics()
|
| 329 |
+
|
| 330 |
+
stats = {
|
| 331 |
+
"total": len(clinics),
|
| 332 |
+
"davlat": len([c for c in clinics if c.get('type') == 'davlat']),
|
| 333 |
+
"xususiy": len([c for c in clinics if c.get('type') == 'xususiy']),
|
| 334 |
+
"by_district": {}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
for clinic in clinics:
|
| 338 |
+
district = clinic.get('district', 'Noma\'lum')
|
| 339 |
+
stats['by_district'][district] = stats['by_district'].get(district, 0) + 1
|
| 340 |
+
|
| 341 |
+
return stats
|
| 342 |
+
|
| 343 |
+
# ==================== DOCTORS CRUD ====================
|
| 344 |
+
|
| 345 |
+
def get_all_doctors(self, clinic_id: Optional[str] = None, specialty: Optional[str] = None) -> List[Dict]:
|
| 346 |
+
"""Barcha doktorlarni olish"""
|
| 347 |
+
data = self._read_json(DOCTORS_FILE)
|
| 348 |
+
doctors = data.get('doctors', [])
|
| 349 |
+
|
| 350 |
+
if clinic_id:
|
| 351 |
+
doctors = [d for d in doctors if d.get('clinic_id') == clinic_id]
|
| 352 |
+
|
| 353 |
+
if specialty:
|
| 354 |
+
doctors = [d for d in doctors if specialty.lower() in d.get('specialty', '').lower()]
|
| 355 |
+
|
| 356 |
+
logger.info(f"👨⚕️ {len(doctors)} ta doktor topildi")
|
| 357 |
+
return doctors
|
| 358 |
+
|
| 359 |
+
def get_doctor_by_id(self, doctor_id: str) -> Optional[Dict]:
|
| 360 |
+
"""Doktorni ID bo'yicha olish"""
|
| 361 |
+
data = self._read_json(DOCTORS_FILE)
|
| 362 |
+
|
| 363 |
+
for doctor in data.get('doctors', []):
|
| 364 |
+
if doctor.get('id') == doctor_id:
|
| 365 |
+
logger.info(f"✅ Doktor topildi: {doctor.get('full_name')}")
|
| 366 |
+
return doctor
|
| 367 |
+
|
| 368 |
+
logger.warning(f"⚠️ Doktor topilmadi: {doctor_id}")
|
| 369 |
+
return None
|
| 370 |
+
|
| 371 |
+
def get_doctors_by_clinic(self, clinic_id: str) -> List[Dict]:
|
| 372 |
+
"""Klinikadagi barcha doktorlar"""
|
| 373 |
+
return self.get_all_doctors(clinic_id=clinic_id)
|
| 374 |
+
|
| 375 |
+
# ==================== BRIGADES CRUD ====================
|
| 376 |
+
|
| 377 |
+
def get_all_brigades(self) -> List[Dict]:
|
| 378 |
+
"""
|
| 379 |
+
Barcha brigadalarni olish
|
| 380 |
+
|
| 381 |
+
Returns:
|
| 382 |
+
List[Dict]: Brigadalar ro'yxati
|
| 383 |
+
"""
|
| 384 |
+
data = self._read_json(BRIGADES_FILE)
|
| 385 |
+
brigades = data.get('brigades', [])
|
| 386 |
+
|
| 387 |
+
logger.info(f"🚑 {len(brigades)} ta brigada topildi")
|
| 388 |
+
return brigades
|
| 389 |
+
|
| 390 |
+
def get_brigade_by_id(self, brigade_id: str) -> Optional[Dict]:
|
| 391 |
+
"""
|
| 392 |
+
Brigadani ID bo'yicha olish
|
| 393 |
+
|
| 394 |
+
Args:
|
| 395 |
+
brigade_id: "brigade_001"
|
| 396 |
+
|
| 397 |
+
Returns:
|
| 398 |
+
Brigade dict yoki None
|
| 399 |
+
"""
|
| 400 |
+
brigades = self.get_all_brigades()
|
| 401 |
+
|
| 402 |
+
for brigade in brigades:
|
| 403 |
+
if brigade.get('brigade_id') == brigade_id:
|
| 404 |
+
return brigade
|
| 405 |
+
|
| 406 |
+
logger.warning(f"⚠️ Brigade topilmadi: {brigade_id}")
|
| 407 |
+
return None
|
| 408 |
+
|
| 409 |
+
def update_brigade(self, brigade_id: str, updates: Dict) -> bool:
|
| 410 |
+
"""
|
| 411 |
+
Brigadaning ma'lumotlarini yangilash
|
| 412 |
+
|
| 413 |
+
Args:
|
| 414 |
+
brigade_id: "brigade_001"
|
| 415 |
+
updates: {"current_lat": 41.3000, "current_lon": 69.2500}
|
| 416 |
+
|
| 417 |
+
Returns:
|
| 418 |
+
bool: True agar yangilangan bo'lsa
|
| 419 |
+
"""
|
| 420 |
+
data = self._read_json(BRIGADES_FILE)
|
| 421 |
+
|
| 422 |
+
for brigade in data.get('brigades', []):
|
| 423 |
+
if brigade.get('brigade_id') == brigade_id:
|
| 424 |
+
brigade.update(updates)
|
| 425 |
+
self._write_json(BRIGADES_FILE, data)
|
| 426 |
+
return True
|
| 427 |
+
|
| 428 |
+
logger.warning(f"⚠️ Brigade yangilanmadi: {brigade_id}")
|
| 429 |
+
return False
|
| 430 |
+
|
| 431 |
+
def get_active_brigades(self) -> List[Dict]:
|
| 432 |
+
"""
|
| 433 |
+
Faqat busy brigadalarni olish (harakatlanayotgan)
|
| 434 |
+
|
| 435 |
+
Returns:
|
| 436 |
+
List[Dict]: Busy brigadalar
|
| 437 |
+
"""
|
| 438 |
+
brigades = self.get_all_brigades()
|
| 439 |
+
active = [b for b in brigades if b.get('current_status') == 'busy']
|
| 440 |
+
|
| 441 |
+
logger.info(f"🚑 {len(active)} ta aktiv brigada")
|
| 442 |
+
return active
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
# Global database instance
|
| 446 |
+
db = JSONDatabase()
|
app/models/__pycache__/schemas.cpython-313.pyc
ADDED
|
Binary file (13.5 kB). View file
|
|
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/models/schemas.py - TO'LIQ YANGILANGAN (RASMLAR BILAN)
|
| 2 |
+
"""
|
| 3 |
+
Pydantic models for data validation
|
| 4 |
+
YANGILANISHLAR:
|
| 5 |
+
- Doktor va Klinika schemalariga rasm URLlari qo'shildi
|
| 6 |
+
- Photo gallery uchun maydonlar
|
| 7 |
+
- Logo va banner rasmlar
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from pydantic import BaseModel, Field, HttpUrl
|
| 11 |
+
from typing import Optional, Literal, List, Dict
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ==================== CASE SCHEMAS ====================
|
| 16 |
+
|
| 17 |
+
class CaseBase(BaseModel):
|
| 18 |
+
"""Case ning asosiy ma'lumotlari"""
|
| 19 |
+
type: Literal["emergency", "public_clinic", "private_clinic", "uncertain"] = "emergency"
|
| 20 |
+
patient_identifier: str = Field(..., description="Bemorning unikal identifikatori")
|
| 21 |
+
status: Literal["yangi", "qabul_qilindi", "brigada_junatildi", "klinika_tavsiya_qilindi", "operator_kutilmoqda", "yopildi"] = "yangi"
|
| 22 |
+
risk_level: Optional[Literal["qizil", "sariq", "yashil"]] = None
|
| 23 |
+
address_text: Optional[str] = None
|
| 24 |
+
is_address_confirmed: Optional[bool] = False
|
| 25 |
+
district: Optional[str] = None
|
| 26 |
+
patient_full_name: Optional[str] = Field(None, description="Bemorning to'liq ismi")
|
| 27 |
+
patient_phone: Optional[str] = Field(None, description="Bemorning telefon raqami")
|
| 28 |
+
previous_cases_count: Optional[int] = Field(0, description="Oldingi murojatlar soni")
|
| 29 |
+
symptoms_text: Optional[str] = None
|
| 30 |
+
language: Optional[str] = "uzb"
|
| 31 |
+
clinic_type: Optional[Literal["davlat", "xususiy"]] = None
|
| 32 |
+
recommended_clinic_id: Optional[str] = None
|
| 33 |
+
recommended_clinic_name: Optional[str] = None
|
| 34 |
+
recommended_specialty: Optional[str] = None
|
| 35 |
+
estimated_price: Optional[str] = None
|
| 36 |
+
uncertainty_attempts: int = 0
|
| 37 |
+
operator_needed: bool = False
|
| 38 |
+
uncertainty_reason: Optional[str] = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class CaseResponse(CaseBase):
|
| 42 |
+
"""API response uchun to'liq case ma'lumotlari"""
|
| 43 |
+
id: str
|
| 44 |
+
assigned_dispatcher: Optional[str] = None
|
| 45 |
+
assigned_brigade_id: Optional[str] = None
|
| 46 |
+
assigned_brigade_name: Optional[str] = None
|
| 47 |
+
brigade_phone: Optional[str] = None
|
| 48 |
+
distance_to_brigade_km: Optional[float] = None
|
| 49 |
+
created_at: str
|
| 50 |
+
updated_at: str
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class CaseUpdate(BaseModel):
|
| 54 |
+
"""Case ni yangilash uchun"""
|
| 55 |
+
status: Optional[Literal["yangi", "qabul_qilindi", "brigada_junatildi", "klinika_tavsiya_qilindi", "operator_kutilmoqda", "yopildi"]] = None
|
| 56 |
+
risk_level: Optional[Literal["qizil", "sariq", "yashil"]] = None
|
| 57 |
+
address_text: Optional[str] = None
|
| 58 |
+
district: Optional[str] = None
|
| 59 |
+
assigned_dispatcher: Optional[str] = None
|
| 60 |
+
patient_full_name: Optional[str] = None
|
| 61 |
+
patient_phone: Optional[str] = None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ==================== MESSAGE SCHEMAS ====================
|
| 65 |
+
|
| 66 |
+
class MessageBase(BaseModel):
|
| 67 |
+
"""Xabarning asosiy ma'lumotlari"""
|
| 68 |
+
case_id: str
|
| 69 |
+
sender: Literal["bemor", "ai"]
|
| 70 |
+
text_content: str
|
| 71 |
+
audio_url: Optional[str] = None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class MessageResponse(MessageBase):
|
| 75 |
+
"""API response uchun to'liq xabar ma'lumotlari"""
|
| 76 |
+
id: str
|
| 77 |
+
created_at: str
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ==================== DISPATCHER SCHEMAS ====================
|
| 81 |
+
|
| 82 |
+
class DispatcherLogin(BaseModel):
|
| 83 |
+
"""Login request"""
|
| 84 |
+
username: str = Field(..., min_length=3, max_length=50)
|
| 85 |
+
password: str = Field(..., min_length=3)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class DispatcherResponse(BaseModel):
|
| 89 |
+
"""Dispetcher ma'lumotlari"""
|
| 90 |
+
id: str
|
| 91 |
+
username: str
|
| 92 |
+
full_name: str
|
| 93 |
+
district: str
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class LoginResponse(BaseModel):
|
| 97 |
+
"""Login muvaffaqiyatli javob"""
|
| 98 |
+
success: bool
|
| 99 |
+
message: str
|
| 100 |
+
dispatcher: Optional[DispatcherResponse] = None
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ==================== PATIENT HISTORY ====================
|
| 104 |
+
|
| 105 |
+
class PatientHistoryResponse(BaseModel):
|
| 106 |
+
"""Bemor tarixi"""
|
| 107 |
+
patient_name: str
|
| 108 |
+
total_cases: int
|
| 109 |
+
last_visit: Optional[str] = None
|
| 110 |
+
risk_levels: Dict[str, int] = {}
|
| 111 |
+
cases: List[Dict] = []
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class PatientStatistics(BaseModel):
|
| 115 |
+
"""Bemor statistikasi"""
|
| 116 |
+
total_cases: int
|
| 117 |
+
last_visit: Optional[str]
|
| 118 |
+
risk_levels: Dict[str, int]
|
| 119 |
+
common_symptoms: List[str]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ==================== BRIGADE SCHEMAS ====================
|
| 123 |
+
|
| 124 |
+
class BrigadeLocation(BaseModel):
|
| 125 |
+
"""Brigade joylashuvi"""
|
| 126 |
+
brigade_id: str
|
| 127 |
+
name: str
|
| 128 |
+
base_lat: float
|
| 129 |
+
base_lon: float
|
| 130 |
+
current_status: Literal["available", "busy", "offline"]
|
| 131 |
+
phone: Optional[str] = None
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ==================== 🆕 DOCTOR SCHEMAS (RASMLAR BILAN) ====================
|
| 135 |
+
|
| 136 |
+
class DoctorBase(BaseModel):
|
| 137 |
+
"""Doktor asosiy ma'lumotlari"""
|
| 138 |
+
id: str
|
| 139 |
+
full_name: str
|
| 140 |
+
specialty: str # "Terapevt", "Kardiolog", "Nevropatolog"
|
| 141 |
+
experience_years: int
|
| 142 |
+
clinic_id: str # Qaysi klinikada ishlaydi
|
| 143 |
+
|
| 144 |
+
# 🆕 RASM MAYDONLARI
|
| 145 |
+
photo_url: str = Field(..., description="Doktorning rasmiga havola")
|
| 146 |
+
|
| 147 |
+
# Qo'shimcha ma'lumotlar
|
| 148 |
+
education: Optional[str] = None # "Toshkent Tibbiyot Akademiyasi"
|
| 149 |
+
languages: List[str] = ["O'zbekcha", "Ruscha"]
|
| 150 |
+
rating: float = Field(4.5, ge=0, le=5) # 0-5 yulduz
|
| 151 |
+
consultation_price: str = "150,000 - 200,000 so'm"
|
| 152 |
+
phone: Optional[str] = None
|
| 153 |
+
available_days: List[str] = ["Dushanba", "Seshanba", "Chorshanba", "Payshanba", "Juma"]
|
| 154 |
+
available_time: str = "09:00 - 18:00"
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class DoctorResponse(DoctorBase):
|
| 158 |
+
"""API response uchun to'liq doktor ma'lumotlari"""
|
| 159 |
+
description: Optional[str] = None
|
| 160 |
+
consultation_duration: int = 30 # minutes
|
| 161 |
+
patient_reviews_count: int = 0
|
| 162 |
+
accepts_insurance: bool = False
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# ==================== 🆕 CLINIC SCHEMAS (RASMLAR BILAN) ====================
|
| 166 |
+
|
| 167 |
+
class ClinicGPS(BaseModel):
|
| 168 |
+
"""GPS koordinatalar"""
|
| 169 |
+
lat: float
|
| 170 |
+
lon: float
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class ClinicPhoto(BaseModel):
|
| 174 |
+
"""Klinika rasmlari"""
|
| 175 |
+
url: str
|
| 176 |
+
caption: Optional[str] = None # "Resepsiya", "Xona", "Jihozlar"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class ClinicService(BaseModel):
|
| 180 |
+
"""Klinika xizmati"""
|
| 181 |
+
name: str
|
| 182 |
+
price: str
|
| 183 |
+
duration: Optional[str] = None
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class ClinicBase(BaseModel):
|
| 187 |
+
"""Klinika asosiy ma'lumotlari"""
|
| 188 |
+
id: str
|
| 189 |
+
name: str
|
| 190 |
+
type: Literal["davlat", "xususiy"]
|
| 191 |
+
district: str
|
| 192 |
+
address: str
|
| 193 |
+
phone: str
|
| 194 |
+
rating: float = Field(4.0, ge=0, le=5)
|
| 195 |
+
|
| 196 |
+
# 🆕 RASM MAYDONLARI
|
| 197 |
+
logo_url: Optional[str] = None # Klinika logotipi
|
| 198 |
+
banner_url: Optional[str] = None # Banner rasm
|
| 199 |
+
photos: List[ClinicPhoto] = [] # Galeriya rasmlari
|
| 200 |
+
|
| 201 |
+
# Ish vaqti
|
| 202 |
+
working_hours: str = "08:00 - 20:00"
|
| 203 |
+
working_days: List[str] = ["Dushanba", "Seshanba", "Chorshanba", "Payshanba", "Juma", "Shanba"]
|
| 204 |
+
|
| 205 |
+
# Xizmatlar
|
| 206 |
+
specializations: List[str] = [] # ["Terapiya", "Kardiologiya"]
|
| 207 |
+
doctors_count: int = 0
|
| 208 |
+
has_emergency: bool = False
|
| 209 |
+
insurance_accepted: bool = False
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class ClinicResponse(ClinicBase):
|
| 213 |
+
"""API response uchun to'liq klinika ma'lumotlari"""
|
| 214 |
+
gps: ClinicGPS
|
| 215 |
+
services: Optional[List[ClinicService]] = []
|
| 216 |
+
languages: Optional[List[str]] = ["O'zbekcha", "Ruscha"]
|
| 217 |
+
description: Optional[str] = None
|
| 218 |
+
doctors: Optional[List[DoctorResponse]] = [] # Klinikadagi doktorlar
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class ClinicSearchFilters(BaseModel):
|
| 222 |
+
"""Klinika qidirish filtrlari"""
|
| 223 |
+
specialty: Optional[str] = None
|
| 224 |
+
district: Optional[str] = None
|
| 225 |
+
clinic_type: Optional[Literal["davlat", "xususiy"]] = None
|
| 226 |
+
min_rating: Optional[float] = None
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class ClinicRecommendation(BaseModel):
|
| 230 |
+
"""Simptomlar bo'yicha klinika tavsiyasi"""
|
| 231 |
+
recommended_specialty: str
|
| 232 |
+
clinics: List[ClinicResponse]
|
| 233 |
+
total_found: int
|
| 234 |
+
message: Optional[str] = None
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# ==================== OPERATOR SCHEMAS ====================
|
| 238 |
+
|
| 239 |
+
class OperatorCase(BaseModel):
|
| 240 |
+
"""Operator kerak bo'lgan case"""
|
| 241 |
+
case_id: str
|
| 242 |
+
symptoms_text: str
|
| 243 |
+
uncertainty_attempts: int
|
| 244 |
+
uncertainty_reason: str
|
| 245 |
+
created_at: str
|
| 246 |
+
language: str
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
# ==================== STATISTICS ====================
|
| 250 |
+
|
| 251 |
+
class ClinicStatistics(BaseModel):
|
| 252 |
+
"""Klinika statistikasi"""
|
| 253 |
+
total: int
|
| 254 |
+
davlat: int
|
| 255 |
+
xususiy: int
|
| 256 |
+
by_district: Dict[str, int]
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class CasesByType(BaseModel):
|
| 260 |
+
"""Type bo'yicha caselar"""
|
| 261 |
+
emergency: List[Dict]
|
| 262 |
+
public_clinic: List[Dict]
|
| 263 |
+
private_clinic: List[Dict]
|
| 264 |
+
uncertain: List[Dict]
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
# ==================== WEBSOCKET SCHEMAS ====================
|
| 268 |
+
|
| 269 |
+
class WebSocketMessage(BaseModel):
|
| 270 |
+
"""WebSocket xabar formati"""
|
| 271 |
+
type: str
|
| 272 |
+
data: Optional[Dict] = None
|
| 273 |
+
prompt: Optional[str] = None
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# ==================== GENERAL RESPONSES ====================
|
| 277 |
+
|
| 278 |
+
class SuccessResponse(BaseModel):
|
| 279 |
+
"""Umumiy muvaffaqiyatli javob"""
|
| 280 |
+
success: bool = True
|
| 281 |
+
message: str
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
class ErrorResponse(BaseModel):
|
| 285 |
+
"""Umumiy xato javobi"""
|
| 286 |
+
success: bool = False
|
| 287 |
+
error: str
|
app/services/__pycache__/brigade_matcher.cpython-313.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
app/services/__pycache__/brigade_simulator.cpython-313.pyc
ADDED
|
Binary file (7.44 kB). View file
|
|
|
app/services/__pycache__/geocoding.cpython-313.pyc
ADDED
|
Binary file (9.27 kB). View file
|
|
|
app/services/__pycache__/location_validator.cpython-313.pyc
ADDED
|
Binary file (11.5 kB). View file
|
|
|
app/services/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (27.4 kB). View file
|
|
|
app/services/brigade_matcher.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/services/brigade_matcher.py
|
| 2 |
+
"""
|
| 3 |
+
Brigade Matcher Service - Eng yaqin brigadani topish
|
| 4 |
+
Haversine formula yordamida masofa hisoblanadi
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
import math
|
| 9 |
+
from typing import Optional, Dict, List
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import json
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
# Brigades faylining yo'li
|
| 16 |
+
BRIGADES_FILE = Path("data/brigades.json")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 20 |
+
"""
|
| 21 |
+
Ikki nuqta orasidagi masofani hisoblash (Haversine formula)
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
lat1, lon1: Birinchi nuqta koordinatalari
|
| 25 |
+
lat2, lon2: Ikkinchi nuqta koordinatalari
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
Masofa (kilometrlarda)
|
| 29 |
+
"""
|
| 30 |
+
try:
|
| 31 |
+
# Yer radiusi (km)
|
| 32 |
+
R = 6371.0
|
| 33 |
+
|
| 34 |
+
# Koordinatalarni radianga o'girish
|
| 35 |
+
lat1_rad = math.radians(lat1)
|
| 36 |
+
lon1_rad = math.radians(lon1)
|
| 37 |
+
lat2_rad = math.radians(lat2)
|
| 38 |
+
lon2_rad = math.radians(lon2)
|
| 39 |
+
|
| 40 |
+
# Farqlar
|
| 41 |
+
dlat = lat2_rad - lat1_rad
|
| 42 |
+
dlon = lon2_rad - lon1_rad
|
| 43 |
+
|
| 44 |
+
# Haversine formula
|
| 45 |
+
a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
|
| 46 |
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
| 47 |
+
|
| 48 |
+
distance = R * c
|
| 49 |
+
|
| 50 |
+
return round(distance, 2) # 2 xona aniqlik
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"❌ Masofa hisoblashda xatolik: {e}")
|
| 54 |
+
return float('inf') # Xatolik bo'lsa cheksiz masofa qaytarish
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def load_brigades() -> List[Dict]:
|
| 58 |
+
"""
|
| 59 |
+
Brigades faylidan barcha brigadalarni yuklash
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Brigadalar ro'yxati
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
if not BRIGADES_FILE.exists():
|
| 66 |
+
logger.error(f"❌ Brigades fayli topilmadi: {BRIGADES_FILE}")
|
| 67 |
+
return []
|
| 68 |
+
|
| 69 |
+
with open(BRIGADES_FILE, 'r', encoding='utf-8') as f:
|
| 70 |
+
data = json.load(f)
|
| 71 |
+
|
| 72 |
+
brigades = data.get('brigades', [])
|
| 73 |
+
logger.info(f"✅ {len(brigades)} ta brigada yuklandi")
|
| 74 |
+
|
| 75 |
+
return brigades
|
| 76 |
+
|
| 77 |
+
except json.JSONDecodeError as e:
|
| 78 |
+
logger.error(f"❌ JSON parse xatoligi (brigades.json): {e}")
|
| 79 |
+
return []
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"❌ Brigadalarni yuklashda xatolik: {e}")
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_available_brigades() -> List[Dict]:
|
| 87 |
+
"""
|
| 88 |
+
Faqat "available" statusdagi brigadalarni qaytarish
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
Bo'sh brigadalar ro'yxati
|
| 92 |
+
"""
|
| 93 |
+
try:
|
| 94 |
+
all_brigades = load_brigades()
|
| 95 |
+
available = [b for b in all_brigades if b.get('status') == 'available']
|
| 96 |
+
|
| 97 |
+
logger.info(f"✅ {len(available)} ta bo'sh brigada mavjud")
|
| 98 |
+
return available
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
logger.error(f"❌ Bo'sh brigadalarni filtrlashda xatolik: {e}")
|
| 102 |
+
return []
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def find_nearest_brigade(patient_lat: float, patient_lon: float) -> Optional[Dict]:
|
| 106 |
+
"""
|
| 107 |
+
Bemorga eng yaqin brigadani topish
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
patient_lat: Bemorning latitude
|
| 111 |
+
patient_lon: Bemorning longitude
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
{
|
| 115 |
+
"brigade_id": "brigade_01",
|
| 116 |
+
"brigade_name": "1-brigada",
|
| 117 |
+
"distance_km": 2.3,
|
| 118 |
+
"district": "Chilonzor",
|
| 119 |
+
"phone": "+998712345001",
|
| 120 |
+
"vehicle_number": "01A123BC"
|
| 121 |
+
}
|
| 122 |
+
yoki None (brigada topilmasa)
|
| 123 |
+
"""
|
| 124 |
+
try:
|
| 125 |
+
available_brigades = get_available_brigades()
|
| 126 |
+
|
| 127 |
+
if not available_brigades:
|
| 128 |
+
logger.warning("⚠️ Hech qanday bo'sh brigada yo'q!")
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
# Har bir brigada uchun masofani hisoblash
|
| 132 |
+
brigades_with_distance = []
|
| 133 |
+
|
| 134 |
+
for brigade in available_brigades:
|
| 135 |
+
brigade_lat = brigade.get('lat')
|
| 136 |
+
brigade_lon = brigade.get('lon')
|
| 137 |
+
|
| 138 |
+
if brigade_lat is None or brigade_lon is None:
|
| 139 |
+
logger.warning(f"⚠️ Brigade {brigade.get('id')} koordinatalari yo'q")
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
distance = haversine_distance(
|
| 143 |
+
patient_lat, patient_lon,
|
| 144 |
+
brigade_lat, brigade_lon
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
brigades_with_distance.append({
|
| 148 |
+
"brigade_id": brigade.get('id'),
|
| 149 |
+
"brigade_name": brigade.get('name'),
|
| 150 |
+
"district": brigade.get('district'),
|
| 151 |
+
"phone": brigade.get('phone'),
|
| 152 |
+
"vehicle_number": brigade.get('vehicle_number'),
|
| 153 |
+
"base_address": brigade.get('base_address'),
|
| 154 |
+
"distance_km": distance
|
| 155 |
+
})
|
| 156 |
+
|
| 157 |
+
if not brigades_with_distance:
|
| 158 |
+
logger.warning("⚠️ Masofani hisoblash uchun brigadalar yo'q")
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
# Eng yaqinini topish
|
| 162 |
+
nearest = min(brigades_with_distance, key=lambda x: x['distance_km'])
|
| 163 |
+
|
| 164 |
+
logger.info(
|
| 165 |
+
f"✅ Eng yaqin brigada topildi: {nearest['brigade_name']} "
|
| 166 |
+
f"({nearest['distance_km']} km, {nearest['district']})"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
return nearest
|
| 170 |
+
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.error(f"❌ Eng yaqin brigadani topishda xatolik: {e}", exc_info=True)
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def get_brigade_by_id(brigade_id: str) -> Optional[Dict]:
|
| 177 |
+
"""
|
| 178 |
+
Brigada ID bo'yicha ma'lumotlarini olish
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
brigade_id: Brigade identifikatori
|
| 182 |
+
|
| 183 |
+
Returns:
|
| 184 |
+
Brigade ma'lumotlari yoki None
|
| 185 |
+
"""
|
| 186 |
+
try:
|
| 187 |
+
all_brigades = load_brigades()
|
| 188 |
+
|
| 189 |
+
for brigade in all_brigades:
|
| 190 |
+
if brigade.get('id') == brigade_id:
|
| 191 |
+
return brigade
|
| 192 |
+
|
| 193 |
+
logger.warning(f"⚠️ Brigade topilmadi: {brigade_id}")
|
| 194 |
+
return None
|
| 195 |
+
|
| 196 |
+
except Exception as e:
|
| 197 |
+
logger.error(f"❌ Brigadani ID bo'yicha olishda xatolik: {e}")
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def update_brigade_status(brigade_id: str, status: str) -> bool:
|
| 202 |
+
"""
|
| 203 |
+
Brigade statusini o'zgartirish
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
brigade_id: Brigade identifikatori
|
| 207 |
+
status: "available", "busy", "offline"
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
True - muvaffaqiyatli, False - xatolik
|
| 211 |
+
"""
|
| 212 |
+
try:
|
| 213 |
+
if status not in ['available', 'busy', 'offline']:
|
| 214 |
+
logger.warning(f"⚠️ Noto'g'ri status: {status}")
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
# Fayldan o'qish
|
| 218 |
+
with open(BRIGADES_FILE, 'r', encoding='utf-8') as f:
|
| 219 |
+
data = json.load(f)
|
| 220 |
+
|
| 221 |
+
brigades = data.get('brigades', [])
|
| 222 |
+
updated = False
|
| 223 |
+
|
| 224 |
+
# Brigade statusini yangilash
|
| 225 |
+
for brigade in brigades:
|
| 226 |
+
if brigade.get('id') == brigade_id:
|
| 227 |
+
brigade['status'] = status
|
| 228 |
+
updated = True
|
| 229 |
+
logger.info(f"✅ Brigade {brigade_id} statusi o'zgartirildi: {status}")
|
| 230 |
+
break
|
| 231 |
+
|
| 232 |
+
if not updated:
|
| 233 |
+
logger.warning(f"⚠️ Brigade topilmadi: {brigade_id}")
|
| 234 |
+
return False
|
| 235 |
+
|
| 236 |
+
# Faylga yozish
|
| 237 |
+
with open(BRIGADES_FILE, 'w', encoding='utf-8') as f:
|
| 238 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 239 |
+
|
| 240 |
+
return True
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
logger.error(f"❌ Brigade statusini yangilashda xatolik: {e}")
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def get_brigades_by_district(district_name: str) -> List[Dict]:
|
| 248 |
+
"""
|
| 249 |
+
Tuman bo'yicha brigadalarni filtrlash
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
district_name: Tuman nomi
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
Shu tumandagi brigadalar ro'yxati
|
| 256 |
+
"""
|
| 257 |
+
try:
|
| 258 |
+
all_brigades = load_brigades()
|
| 259 |
+
|
| 260 |
+
filtered = [
|
| 261 |
+
b for b in all_brigades
|
| 262 |
+
if b.get('district', '').lower() == district_name.lower()
|
| 263 |
+
]
|
| 264 |
+
|
| 265 |
+
logger.info(f"✅ {district_name} tumanida {len(filtered)} ta brigada topildi")
|
| 266 |
+
return filtered
|
| 267 |
+
|
| 268 |
+
except Exception as e:
|
| 269 |
+
logger.error(f"❌ Brigadalarni tuman bo'yicha filtrlashda xatolik: {e}")
|
| 270 |
+
return []
|
app/services/brigade_simulator.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/services/brigade_simulator.py - YANGI FAYL YARATING
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Brigade Simulator - Brigadalarning harakatini simulyatsiya qilish
|
| 5 |
+
Real-time koordinatalarni yangilash (A nuqtadan B nuqtaga)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import random
|
| 9 |
+
import math
|
| 10 |
+
import asyncio
|
| 11 |
+
import logging
|
| 12 |
+
from typing import Dict, Tuple, Optional
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
# Toshkent chegaralari
|
| 18 |
+
TASHKENT_BOUNDS = {
|
| 19 |
+
"lat_min": 41.20,
|
| 20 |
+
"lat_max": 41.35,
|
| 21 |
+
"lon_min": 69.10,
|
| 22 |
+
"lon_max": 69.35
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 27 |
+
"""
|
| 28 |
+
Ikki nuqta orasidagi masofa (km)
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
float: Masofa kilometrlarda
|
| 32 |
+
"""
|
| 33 |
+
R = 6371 # Yer radiusi (km)
|
| 34 |
+
|
| 35 |
+
lat1_rad = math.radians(lat1)
|
| 36 |
+
lat2_rad = math.radians(lat2)
|
| 37 |
+
delta_lat = math.radians(lat2 - lat1)
|
| 38 |
+
delta_lon = math.radians(lon2 - lon1)
|
| 39 |
+
|
| 40 |
+
a = math.sin(delta_lat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon / 2) ** 2
|
| 41 |
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
| 42 |
+
|
| 43 |
+
return R * c
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def calculate_new_position(
|
| 47 |
+
current_lat: float,
|
| 48 |
+
current_lon: float,
|
| 49 |
+
target_lat: float,
|
| 50 |
+
target_lon: float,
|
| 51 |
+
speed_kmh: float = 60,
|
| 52 |
+
time_step_seconds: int = 3
|
| 53 |
+
) -> Tuple[float, float]:
|
| 54 |
+
"""
|
| 55 |
+
Yangi pozitsiyani hisoblash (A dan B ga)
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
current_lat: Joriy lat
|
| 59 |
+
current_lon: Joriy lon
|
| 60 |
+
target_lat: Maqsad lat
|
| 61 |
+
target_lon: Maqsad lon
|
| 62 |
+
speed_kmh: Tezlik (km/soat)
|
| 63 |
+
time_step_seconds: Vaqt qadami (sekund)
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Tuple[float, float]: Yangi (lat, lon)
|
| 67 |
+
"""
|
| 68 |
+
# Maqsadga yetgan bo'lsa
|
| 69 |
+
distance = haversine_distance(current_lat, current_lon, target_lat, target_lon)
|
| 70 |
+
|
| 71 |
+
if distance < 0.05: # 50 metr
|
| 72 |
+
return target_lat, target_lon
|
| 73 |
+
|
| 74 |
+
# Harakat masofasi (km)
|
| 75 |
+
distance_step = (speed_kmh / 3600) * time_step_seconds
|
| 76 |
+
|
| 77 |
+
# Agar bir qadamda yetib borsa
|
| 78 |
+
if distance_step >= distance:
|
| 79 |
+
return target_lat, target_lon
|
| 80 |
+
|
| 81 |
+
# Yo'nalish (bearing)
|
| 82 |
+
fraction = distance_step / distance
|
| 83 |
+
|
| 84 |
+
new_lat = current_lat + (target_lat - current_lat) * fraction
|
| 85 |
+
new_lon = current_lon + (target_lon - current_lon) * fraction
|
| 86 |
+
|
| 87 |
+
return new_lat, new_lon
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def generate_random_target() -> Tuple[float, float]:
|
| 91 |
+
"""
|
| 92 |
+
Tasodifiy maqsad nuqta (Toshkent ichida)
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
Tuple[float, float]: (lat, lon)
|
| 96 |
+
"""
|
| 97 |
+
lat = random.uniform(TASHKENT_BOUNDS["lat_min"], TASHKENT_BOUNDS["lat_max"])
|
| 98 |
+
lon = random.uniform(TASHKENT_BOUNDS["lon_min"], TASHKENT_BOUNDS["lon_max"])
|
| 99 |
+
|
| 100 |
+
return lat, lon
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class BrigadeSimulator:
|
| 104 |
+
"""Brigade simulyatori - koordinatalarni yangilash"""
|
| 105 |
+
|
| 106 |
+
def __init__(self, db):
|
| 107 |
+
self.db = db
|
| 108 |
+
self.is_running = False
|
| 109 |
+
self.update_interval = 3 # 3 sekund
|
| 110 |
+
|
| 111 |
+
async def start(self):
|
| 112 |
+
"""Simulyatorni ishga tushirish"""
|
| 113 |
+
self.is_running = True
|
| 114 |
+
logger.info("🚑 Brigade simulator ishga tushdi")
|
| 115 |
+
|
| 116 |
+
while self.is_running:
|
| 117 |
+
try:
|
| 118 |
+
await self.update_all_brigades()
|
| 119 |
+
await asyncio.sleep(self.update_interval)
|
| 120 |
+
except Exception as e:
|
| 121 |
+
logger.error(f"❌ Brigade simulator xatolik: {e}")
|
| 122 |
+
await asyncio.sleep(5)
|
| 123 |
+
|
| 124 |
+
def stop(self):
|
| 125 |
+
"""Simulyatorni to'xtatish"""
|
| 126 |
+
self.is_running = False
|
| 127 |
+
logger.info("🛑 Brigade simulator to'xtatildi")
|
| 128 |
+
|
| 129 |
+
async def update_all_brigades(self):
|
| 130 |
+
"""Barcha brigadalarni yangilash"""
|
| 131 |
+
from app.services.brigade_matcher import load_brigades
|
| 132 |
+
|
| 133 |
+
brigades = load_brigades()
|
| 134 |
+
|
| 135 |
+
for brigade in brigades:
|
| 136 |
+
if brigade.get("current_status") == "busy":
|
| 137 |
+
self.update_brigade_position(brigade)
|
| 138 |
+
|
| 139 |
+
def update_brigade_position(self, brigade: Dict):
|
| 140 |
+
"""Bitta brigadaning pozitsiyasini yangilash"""
|
| 141 |
+
brigade_id = brigade.get("brigade_id")
|
| 142 |
+
|
| 143 |
+
# Joriy va maqsad koordinatalar
|
| 144 |
+
current_lat = brigade.get("current_lat", brigade.get("base_lat"))
|
| 145 |
+
current_lon = brigade.get("current_lon", brigade.get("base_lon"))
|
| 146 |
+
target_lat = brigade.get("target_lat")
|
| 147 |
+
target_lon = brigade.get("target_lon")
|
| 148 |
+
|
| 149 |
+
# Agar maqsad yo'q bo'lsa, yangi maqsad yaratish
|
| 150 |
+
if not target_lat or not target_lon:
|
| 151 |
+
target_lat, target_lon = generate_random_target()
|
| 152 |
+
self.db.update_brigade(brigade_id, {
|
| 153 |
+
"target_lat": target_lat,
|
| 154 |
+
"target_lon": target_lon
|
| 155 |
+
})
|
| 156 |
+
|
| 157 |
+
# Yangi pozitsiya hisoblash
|
| 158 |
+
speed = brigade.get("speed_kmh", 60)
|
| 159 |
+
new_lat, new_lon = calculate_new_position(
|
| 160 |
+
current_lat, current_lon,
|
| 161 |
+
target_lat, target_lon,
|
| 162 |
+
speed, self.update_interval
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Maqsadga yetdimi?
|
| 166 |
+
distance = haversine_distance(new_lat, new_lon, target_lat, target_lon)
|
| 167 |
+
|
| 168 |
+
if distance < 0.05: # 50 metr - yetdi
|
| 169 |
+
# Yangi maqsad yaratish
|
| 170 |
+
new_target_lat, new_target_lon = generate_random_target()
|
| 171 |
+
|
| 172 |
+
self.db.update_brigade(brigade_id, {
|
| 173 |
+
"current_lat": new_lat,
|
| 174 |
+
"current_lon": new_lon,
|
| 175 |
+
"target_lat": new_target_lat,
|
| 176 |
+
"target_lon": new_target_lon
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
logger.info(f"🚑 {brigade.get('name')} maqsadga yetdi, yangi yo'nalish")
|
| 180 |
+
else:
|
| 181 |
+
# Oddiy yangilash
|
| 182 |
+
self.db.update_brigade(brigade_id, {
|
| 183 |
+
"current_lat": new_lat,
|
| 184 |
+
"current_lon": new_lon
|
| 185 |
+
})
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# Global instance
|
| 189 |
+
simulator_instance: Optional[BrigadeSimulator] = None
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def get_simulator(db):
|
| 193 |
+
"""Simulator instanceni olish"""
|
| 194 |
+
global simulator_instance
|
| 195 |
+
|
| 196 |
+
if simulator_instance is None:
|
| 197 |
+
simulator_instance = BrigadeSimulator(db)
|
| 198 |
+
|
| 199 |
+
return simulator_instance
|
app/services/geocoding.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/services/geocoding.py
|
| 2 |
+
"""
|
| 3 |
+
Geocoding Service - Manzilni koordinataga aylantirish
|
| 4 |
+
Nominatim API (OpenStreetMap) ishlatiladi
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Optional, Dict
|
| 9 |
+
from geopy.geocoders import Nominatim
|
| 10 |
+
from geopy.exc import GeocoderTimedOut, GeocoderServiceError
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
from app.core.config import NOMINATIM_USER_AGENT, GEOCODING_TIMEOUT, TASHKENT_BOUNDS
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
# Nominatim client (rate limit: 1 request/second)
|
| 18 |
+
geolocator = Nominatim(user_agent=NOMINATIM_USER_AGENT, timeout=GEOCODING_TIMEOUT)
|
| 19 |
+
|
| 20 |
+
# Rate limiting
|
| 21 |
+
last_request_time = 0
|
| 22 |
+
MIN_REQUEST_INTERVAL = 1.0 # 1 second between requests
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _wait_for_rate_limit():
|
| 26 |
+
"""Rate limit uchun kutish (1 req/sec)"""
|
| 27 |
+
global last_request_time
|
| 28 |
+
current_time = time.time()
|
| 29 |
+
time_since_last = current_time - last_request_time
|
| 30 |
+
|
| 31 |
+
if time_since_last < MIN_REQUEST_INTERVAL:
|
| 32 |
+
wait_time = MIN_REQUEST_INTERVAL - time_since_last
|
| 33 |
+
logger.debug(f"⏳ Rate limit: {wait_time:.2f}s kutilmoqda...")
|
| 34 |
+
time.sleep(wait_time)
|
| 35 |
+
|
| 36 |
+
last_request_time = time.time()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def geocode_address(address_text: str) -> Optional[Dict]:
|
| 40 |
+
"""
|
| 41 |
+
Manzil matnini koordinataga aylantirish
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
address_text: Manzil matni (masalan: "Chilonzor tumani, Bunyodkor ko'chasi")
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
{
|
| 48 |
+
"lat": 41.2856,
|
| 49 |
+
"lon": 69.2034,
|
| 50 |
+
"display_name": "To'liq manzil",
|
| 51 |
+
"address": {...}
|
| 52 |
+
}
|
| 53 |
+
yoki None (xatolik bo'lsa)
|
| 54 |
+
"""
|
| 55 |
+
if not address_text or len(address_text.strip()) < 3:
|
| 56 |
+
logger.warning("❌ Bo'sh yoki juda qisqa manzil")
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
# Toshkent qo'shib qidirish (aniqlik uchun)
|
| 61 |
+
search_query = f"{address_text}, Toshkent, O'zbekiston"
|
| 62 |
+
logger.info(f"🔍 Geocoding: '{search_query}'")
|
| 63 |
+
|
| 64 |
+
# Rate limit
|
| 65 |
+
_wait_for_rate_limit()
|
| 66 |
+
|
| 67 |
+
# Nominatim API call
|
| 68 |
+
location = geolocator.geocode(
|
| 69 |
+
search_query,
|
| 70 |
+
exactly_one=True,
|
| 71 |
+
language='uz',
|
| 72 |
+
addressdetails=True
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
if not location:
|
| 76 |
+
logger.warning(f"⚠️ Manzil topilmadi: '{address_text}'")
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
# Koordinatalarni tekshirish
|
| 80 |
+
if not validate_location_in_tashkent(location.latitude, location.longitude):
|
| 81 |
+
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {location.latitude}, {location.longitude}")
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
result = {
|
| 85 |
+
"lat": location.latitude,
|
| 86 |
+
"lon": location.longitude,
|
| 87 |
+
"display_name": location.address,
|
| 88 |
+
"address": location.raw.get('address', {})
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
logger.info(f"✅ Geocoding muvaffaqiyatli: {result['lat']}, {result['lon']}")
|
| 92 |
+
return result
|
| 93 |
+
|
| 94 |
+
except GeocoderTimedOut:
|
| 95 |
+
logger.error(f"⏱️ Geocoding timeout: '{address_text}'")
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
except GeocoderServiceError as e:
|
| 99 |
+
logger.error(f"❌ Geocoding service xatoligi: {e}")
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.error(f"❌ Geocoding kutilmagan xatolik: {e}", exc_info=True)
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def reverse_geocode(lat: float, lon: float) -> Optional[Dict]:
|
| 108 |
+
"""
|
| 109 |
+
Koordinatalardan manzilni topish (reverse geocoding)
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
lat: Latitude
|
| 113 |
+
lon: Longitude
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
{
|
| 117 |
+
"display_name": "To'liq manzil",
|
| 118 |
+
"address": {
|
| 119 |
+
"suburb": "Chilonzor",
|
| 120 |
+
"city": "Toshkent",
|
| 121 |
+
...
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
yoki None
|
| 125 |
+
"""
|
| 126 |
+
try:
|
| 127 |
+
# Koordinatalarni validatsiya qilish
|
| 128 |
+
if not validate_location_in_tashkent(lat, lon):
|
| 129 |
+
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}")
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
logger.info(f"🔍 Reverse geocoding: {lat}, {lon}")
|
| 133 |
+
|
| 134 |
+
# Rate limit
|
| 135 |
+
_wait_for_rate_limit()
|
| 136 |
+
|
| 137 |
+
# Nominatim API call
|
| 138 |
+
location = geolocator.reverse(
|
| 139 |
+
(lat, lon),
|
| 140 |
+
exactly_one=True,
|
| 141 |
+
language='uz',
|
| 142 |
+
addressdetails=True
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
if not location:
|
| 146 |
+
logger.warning(f"⚠️ Manzil topilmadi: {lat}, {lon}")
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
result = {
|
| 150 |
+
"display_name": location.address,
|
| 151 |
+
"address": location.raw.get('address', {})
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
logger.info(f"✅ Reverse geocoding muvaffaqiyatli: {result['display_name']}")
|
| 155 |
+
return result
|
| 156 |
+
|
| 157 |
+
except GeocoderTimedOut:
|
| 158 |
+
logger.error(f"⏱️ Reverse geocoding timeout: {lat}, {lon}")
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
except GeocoderServiceError as e:
|
| 162 |
+
logger.error(f"❌ Reverse geocoding service xatoligi: {e}")
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f"❌ Reverse geocoding kutilmagan xatolik: {e}", exc_info=True)
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def validate_location_in_tashkent(lat: float, lon: float) -> bool:
|
| 171 |
+
"""
|
| 172 |
+
Koordinatalarning Toshkent chegarasida ekanligini tekshirish
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
lat: Latitude
|
| 176 |
+
lon: Longitude
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
True - Toshkent ichida, False - tashqarida
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
in_bounds = (
|
| 183 |
+
TASHKENT_BOUNDS["lat_min"] <= lat <= TASHKENT_BOUNDS["lat_max"] and
|
| 184 |
+
TASHKENT_BOUNDS["lon_min"] <= lon <= TASHKENT_BOUNDS["lon_max"]
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
if not in_bounds:
|
| 188 |
+
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}")
|
| 189 |
+
|
| 190 |
+
return in_bounds
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
logger.error(f"❌ Koordinatalarni tekshirishda xatolik: {e}")
|
| 194 |
+
return False
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def get_location_summary(geocoded_data: Dict) -> str:
|
| 198 |
+
"""
|
| 199 |
+
Geocoding natijasidan qisqacha manzil chiqarish
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
geocoded_data: geocode_address() natijasi
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
"Chilonzor tumani, Toshkent" kabi qisqa manzil
|
| 206 |
+
"""
|
| 207 |
+
try:
|
| 208 |
+
address = geocoded_data.get('address', {})
|
| 209 |
+
|
| 210 |
+
# Tuman/mahalla
|
| 211 |
+
suburb = address.get('suburb', address.get('neighbourhood', ''))
|
| 212 |
+
|
| 213 |
+
# Ko'cha
|
| 214 |
+
road = address.get('road', '')
|
| 215 |
+
|
| 216 |
+
# Shahar
|
| 217 |
+
city = address.get('city', address.get('town', 'Toshkent'))
|
| 218 |
+
|
| 219 |
+
# Qisqacha manzil
|
| 220 |
+
parts = []
|
| 221 |
+
if suburb:
|
| 222 |
+
parts.append(suburb)
|
| 223 |
+
if road and suburb.lower() not in road.lower(): # Takrorlanishni oldini olish
|
| 224 |
+
parts.append(road)
|
| 225 |
+
if city and city != 'Toshkent':
|
| 226 |
+
parts.append(city)
|
| 227 |
+
|
| 228 |
+
summary = ', '.join(parts) if parts else geocoded_data.get('display_name', 'Noma\'lum manzil')
|
| 229 |
+
|
| 230 |
+
return summary
|
| 231 |
+
|
| 232 |
+
except Exception as e:
|
| 233 |
+
logger.error(f"❌ Location summary yaratishda xatolik: {e}")
|
| 234 |
+
return "Noma'lum manzil"
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def extract_district_from_address(geocoded_data: Dict) -> Optional[str]:
|
| 238 |
+
"""
|
| 239 |
+
Geocoding natijasidan tuman nomini chiqarish
|
| 240 |
+
|
| 241 |
+
Args:
|
| 242 |
+
geocoded_data: geocode_address() natijasi
|
| 243 |
+
|
| 244 |
+
Returns:
|
| 245 |
+
"Chilonzor" kabi tuman nomi yoki None
|
| 246 |
+
"""
|
| 247 |
+
try:
|
| 248 |
+
address = geocoded_data.get('address', {})
|
| 249 |
+
|
| 250 |
+
# Suburb yoki neighbourhood maydonidan tuman nomini olish
|
| 251 |
+
district = address.get('suburb', address.get('neighbourhood', None))
|
| 252 |
+
|
| 253 |
+
if district:
|
| 254 |
+
# "tumani" so'zini olib tashlash
|
| 255 |
+
district = district.replace(' tumani', '').replace(' Tumani', '').strip()
|
| 256 |
+
logger.info(f"📍 Tuman aniqlandi: {district}")
|
| 257 |
+
|
| 258 |
+
return district
|
| 259 |
+
|
| 260 |
+
except Exception as e:
|
| 261 |
+
logger.error(f"❌ Tumanni chiqarishda xatolik: {e}")
|
| 262 |
+
return None
|
app/services/location_validator.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/services/location_validator.py - BAZAGA MOSLASHTIRILGAN (TO'LIQ VERSIYA)
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import json
|
| 5 |
+
from typing import Optional, List, Dict
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
LOCATIONS_FILE = Path("data/locations_db.json")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_locations_database() -> Dict:
|
| 14 |
+
"""
|
| 15 |
+
locations_db.json faylini yuklash (REAL FORMAT BILAN)
|
| 16 |
+
|
| 17 |
+
Real format:
|
| 18 |
+
{
|
| 19 |
+
"regions": [
|
| 20 |
+
{
|
| 21 |
+
"district_name": "Bektemir tumani",
|
| 22 |
+
"mahallas": [
|
| 23 |
+
{
|
| 24 |
+
"mahalla_name": "Abay mahallasi",
|
| 25 |
+
"coordinates": {"latitude": 41.2411, "longitude": 69.3455}
|
| 26 |
+
}
|
| 27 |
+
]
|
| 28 |
+
}
|
| 29 |
+
]
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
dict: Locations database (simplified format uchun konvert qilingan)
|
| 34 |
+
"""
|
| 35 |
+
try:
|
| 36 |
+
if not LOCATIONS_FILE.exists():
|
| 37 |
+
logger.error(f"❌ Locations file topilmadi: {LOCATIONS_FILE}")
|
| 38 |
+
return {}
|
| 39 |
+
|
| 40 |
+
with open(LOCATIONS_FILE, 'r', encoding='utf-8') as f:
|
| 41 |
+
data = json.load(f)
|
| 42 |
+
|
| 43 |
+
# ========== REAL FORMATDAN SIMPLIFIED FORMATGA KONVERT ==========
|
| 44 |
+
simplified = {}
|
| 45 |
+
|
| 46 |
+
regions = data.get('regions', [])
|
| 47 |
+
|
| 48 |
+
for region in regions:
|
| 49 |
+
districts = region.get('districts', [])
|
| 50 |
+
|
| 51 |
+
for district in districts:
|
| 52 |
+
district_name = district.get('district_name', '')
|
| 53 |
+
mahallas_list = district.get('mahallas', [])
|
| 54 |
+
|
| 55 |
+
if not district_name:
|
| 56 |
+
continue
|
| 57 |
+
|
| 58 |
+
# Mahallalarni dict ga o'girish
|
| 59 |
+
mahallas_dict = {}
|
| 60 |
+
|
| 61 |
+
for mahalla in mahallas_list:
|
| 62 |
+
mahalla_name = mahalla.get('mahalla_name', '')
|
| 63 |
+
coords = mahalla.get('coordinates', {})
|
| 64 |
+
|
| 65 |
+
if mahalla_name and coords:
|
| 66 |
+
# latitude/longitude -> lat/lon
|
| 67 |
+
mahallas_dict[mahalla_name] = {
|
| 68 |
+
"lat": coords.get('latitude'),
|
| 69 |
+
"lon": coords.get('longitude')
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
simplified[district_name] = mahallas_dict
|
| 73 |
+
|
| 74 |
+
logger.info(f"✅ Locations DB yuklandi va konvert qilindi: {len(simplified)} ta tuman")
|
| 75 |
+
return simplified
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error(f"❌ Locations DB yuklashda xatolik: {e}", exc_info=True)
|
| 79 |
+
return {}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def normalize_district_name(district_name: str) -> str:
|
| 83 |
+
"""
|
| 84 |
+
Tuman nomini normallashtirish
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
district_name: "Chilonzor tumani" yoki "Chilonzor"
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
"Chilonzor tumani" (doim "tumani" bilan)
|
| 91 |
+
"""
|
| 92 |
+
name = district_name.strip()
|
| 93 |
+
|
| 94 |
+
# "tumani" yoki "tuman" ni olib tashlash
|
| 95 |
+
name = name.replace(' tumani', '').replace(' tuman', '').strip()
|
| 96 |
+
|
| 97 |
+
# Qaytadan qo'shish
|
| 98 |
+
return f"{name} tumani"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def normalize_mahalla_name(mahalla_name: str) -> str:
|
| 102 |
+
"""
|
| 103 |
+
Mahalla nomini normallashtirish
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
mahalla_name: "Katta Chilonzor mahallasi" yoki "Katta Chilonzor"
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
"Katta Chilonzor mahallasi" (doim "mahallasi" bilan)
|
| 110 |
+
"""
|
| 111 |
+
name = mahalla_name.strip()
|
| 112 |
+
|
| 113 |
+
# "mahallasi" yoki "mahalla" ni olib tashlash
|
| 114 |
+
name = name.replace(' mahallasi', '').replace(' mahalla', '').strip()
|
| 115 |
+
|
| 116 |
+
# Qaytadan qo'shish
|
| 117 |
+
return f"{name} mahallasi"
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def get_mahalla_coordinates(district_name: str, mahalla_name: str) -> Optional[Dict]:
|
| 121 |
+
"""
|
| 122 |
+
Mahalla koordinatalarini olish (REAL BAZADAN)
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
district_name: "Chilonzor tumani" yoki "Chilonzor"
|
| 126 |
+
mahalla_name: "Katta Chilonzor mahallasi" yoki "Katta Chilonzor"
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
{"lat": 41.xxx, "lon": 69.xxx} yoki None
|
| 130 |
+
"""
|
| 131 |
+
try:
|
| 132 |
+
logger.info(f"🔍 Mahalla koordinatalari qidirilmoqda: '{mahalla_name}', '{district_name}'")
|
| 133 |
+
|
| 134 |
+
locations_db = load_locations_database()
|
| 135 |
+
|
| 136 |
+
if not locations_db:
|
| 137 |
+
logger.warning("⚠️ Locations DB bo'sh!")
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
# Normalizatsiya
|
| 141 |
+
normalized_district = normalize_district_name(district_name)
|
| 142 |
+
normalized_mahalla = normalize_mahalla_name(mahalla_name)
|
| 143 |
+
|
| 144 |
+
logger.info(f"🔍 Normalized: Tuman='{normalized_district}', Mahalla='{normalized_mahalla}'")
|
| 145 |
+
|
| 146 |
+
# Tumanni topish (case-insensitive)
|
| 147 |
+
district_data = None
|
| 148 |
+
for db_district_name, mahallas in locations_db.items():
|
| 149 |
+
if db_district_name.lower() == normalized_district.lower():
|
| 150 |
+
district_data = mahallas
|
| 151 |
+
logger.info(f"✅ Tuman topildi DB'da: {db_district_name}")
|
| 152 |
+
break
|
| 153 |
+
|
| 154 |
+
if not district_data:
|
| 155 |
+
logger.warning(f"⚠️ Tuman topilmadi DB'da: {normalized_district}")
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
# Mahallani topish (case-insensitive)
|
| 159 |
+
for db_mahalla_name, coords in district_data.items():
|
| 160 |
+
if db_mahalla_name.lower() == normalized_mahalla.lower():
|
| 161 |
+
logger.info(f"✅ Mahalla koordinatalari topildi (DB): {db_mahalla_name} ({coords['lat']}, {coords['lon']})")
|
| 162 |
+
return {"lat": coords['lat'], "lon": coords['lon']}
|
| 163 |
+
|
| 164 |
+
logger.warning(f"⚠️ Mahalla topilmadi DB'da: {normalized_mahalla}")
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
logger.error(f"❌ Mahalla koordinatalarini olishda xatolik: {e}", exc_info=True)
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def get_mahallas_by_district(district_name: str) -> List[str]:
|
| 173 |
+
"""
|
| 174 |
+
Tuman bo'yicha mahallalar ro'yxatini olish (REAL BAZADAN)
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
district_name: "Chilonzor tumani" yoki "Chilonzor"
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
["Katta Chilonzor-1 mahallasi", "Beltepa mahallasi", ...]
|
| 181 |
+
"""
|
| 182 |
+
try:
|
| 183 |
+
locations_db = load_locations_database()
|
| 184 |
+
|
| 185 |
+
if not locations_db:
|
| 186 |
+
logger.warning(f"⚠️ Locations DB bo'sh!")
|
| 187 |
+
return []
|
| 188 |
+
|
| 189 |
+
# Normalizatsiya
|
| 190 |
+
normalized_district = normalize_district_name(district_name)
|
| 191 |
+
|
| 192 |
+
# Tumanni topish (case-insensitive)
|
| 193 |
+
for db_district_name, mahallas in locations_db.items():
|
| 194 |
+
if db_district_name.lower() == normalized_district.lower():
|
| 195 |
+
mahalla_names = list(mahallas.keys())
|
| 196 |
+
logger.info(f"✅ {len(mahalla_names)} ta mahalla topildi ({db_district_name})")
|
| 197 |
+
return mahalla_names
|
| 198 |
+
|
| 199 |
+
logger.warning(f"⚠️ Tuman topilmadi: {normalized_district}")
|
| 200 |
+
return []
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
logger.error(f"❌ Mahallalarni olishda xatolik: {e}")
|
| 204 |
+
return []
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def format_mahallas_list(mahallas: List[str], max_items: int = 5) -> str:
|
| 208 |
+
"""
|
| 209 |
+
AI javobi uchun mahallalar ro'yxatini formatlash
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
mahallas: Mahallalar ro'yxati
|
| 213 |
+
max_items: Ko'rsatiladigan maksimal elementlar soni
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
"Katta Chilonzor, Beltepa, Beshqozon..."
|
| 217 |
+
"""
|
| 218 |
+
if not mahallas:
|
| 219 |
+
return ""
|
| 220 |
+
|
| 221 |
+
# "mahallasi" so'zini olib tashlash (qisqartirish uchun)
|
| 222 |
+
cleaned_mahallas = [m.replace(' mahallasi', '').replace(' mahalla', '').strip() for m in mahallas]
|
| 223 |
+
|
| 224 |
+
if len(cleaned_mahallas) > max_items:
|
| 225 |
+
return ", ".join(cleaned_mahallas[:max_items]) + "..."
|
| 226 |
+
else:
|
| 227 |
+
return ", ".join(cleaned_mahallas)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def get_all_districts() -> List[str]:
|
| 231 |
+
"""
|
| 232 |
+
Barcha tumanlar ro'yxati (REAL BAZADAN)
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
["Chilonzor tumani", "Bektemir tumani", ...]
|
| 236 |
+
"""
|
| 237 |
+
try:
|
| 238 |
+
locations_db = load_locations_database()
|
| 239 |
+
|
| 240 |
+
if not locations_db:
|
| 241 |
+
logger.warning("⚠️ Locations DB bo'sh!")
|
| 242 |
+
return []
|
| 243 |
+
|
| 244 |
+
districts = list(locations_db.keys())
|
| 245 |
+
logger.info(f"✅ {len(districts)} ta tuman ro'yxati olindi")
|
| 246 |
+
return districts
|
| 247 |
+
|
| 248 |
+
except Exception as e:
|
| 249 |
+
logger.error(f"❌ Tumanlar ro'yxatini olishda xatolik: {e}")
|
| 250 |
+
return []
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def search_mahalla_fuzzy(district_name: str, query: str, threshold: float = 0.6) -> Optional[str]:
|
| 254 |
+
"""
|
| 255 |
+
Mahallani fuzzy search qilish
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
district_name: Tuman nomi
|
| 259 |
+
query: Qidiruv so'zi
|
| 260 |
+
threshold: O'xshashlik darajasi
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
Mahalla nomi yoki None
|
| 264 |
+
"""
|
| 265 |
+
try:
|
| 266 |
+
from difflib import SequenceMatcher
|
| 267 |
+
|
| 268 |
+
mahallas = get_mahallas_by_district(district_name)
|
| 269 |
+
|
| 270 |
+
if not mahallas:
|
| 271 |
+
return None
|
| 272 |
+
|
| 273 |
+
query_lower = query.lower().strip()
|
| 274 |
+
|
| 275 |
+
best_match = None
|
| 276 |
+
best_score = 0.0
|
| 277 |
+
|
| 278 |
+
for mahalla in mahallas:
|
| 279 |
+
mahalla_lower = mahalla.lower()
|
| 280 |
+
|
| 281 |
+
# Substring match
|
| 282 |
+
if query_lower in mahalla_lower or mahalla_lower in query_lower:
|
| 283 |
+
score = 0.9
|
| 284 |
+
else:
|
| 285 |
+
# SequenceMatcher
|
| 286 |
+
score = SequenceMatcher(None, query_lower, mahalla_lower).ratio()
|
| 287 |
+
|
| 288 |
+
if score > best_score:
|
| 289 |
+
best_score = score
|
| 290 |
+
best_match = mahalla
|
| 291 |
+
|
| 292 |
+
if best_score >= threshold:
|
| 293 |
+
logger.info(f"✅ Fuzzy match: '{best_match}' (score: {best_score:.2f})")
|
| 294 |
+
return best_match
|
| 295 |
+
else:
|
| 296 |
+
logger.warning(f"⚠️ Fuzzy match topilmadi (best: {best_score:.2f})")
|
| 297 |
+
return None
|
| 298 |
+
|
| 299 |
+
except Exception as e:
|
| 300 |
+
logger.error(f"❌ Fuzzy search xatolik: {e}")
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def validate_coordinates(lat: float, lon: float) -> bool:
|
| 305 |
+
"""
|
| 306 |
+
Koordinatalarning Toshkent chegarasida ekanligini tekshirish
|
| 307 |
+
|
| 308 |
+
Args:
|
| 309 |
+
lat: Latitude
|
| 310 |
+
lon: Longitude
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
True - Toshkent ichida, False - tashqarida
|
| 314 |
+
"""
|
| 315 |
+
TASHKENT_BOUNDS = {
|
| 316 |
+
"lat_min": 41.20,
|
| 317 |
+
"lat_max": 41.35,
|
| 318 |
+
"lon_min": 69.10,
|
| 319 |
+
"lon_max": 69.35
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
in_bounds = (
|
| 323 |
+
TASHKENT_BOUNDS["lat_min"] <= lat <= TASHKENT_BOUNDS["lat_max"] and
|
| 324 |
+
TASHKENT_BOUNDS["lon_min"] <= lon <= TASHKENT_BOUNDS["lon_max"]
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
if not in_bounds:
|
| 328 |
+
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}")
|
| 329 |
+
|
| 330 |
+
return in_bounds
|
app/services/models.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/services/models.py - TO'LIQ YAXSHILANGAN VERSIYA
|
| 2 |
+
"""
|
| 3 |
+
AI Modellari (STT, TTS, LLM) bilan ishlash uchun funksiyalar
|
| 4 |
+
|
| 5 |
+
YAXSHILANISHLAR:
|
| 6 |
+
1. ✅ JSON Parsing - ROBUST va xatoliklarga bardoshli
|
| 7 |
+
2. ✅ Multi-language - To'liq 3 til qo'llab-quvvatlash (uzb, eng, rus)
|
| 8 |
+
3. ✅ TTS - Speed o'chirildi, faqat til parametri
|
| 9 |
+
4. ✅ Error handling - Hamma joyda try-except
|
| 10 |
+
5. ✅ Fallback responses - Xatolik bo'lsa default javob qaytarish
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import subprocess
|
| 14 |
+
import numpy as np
|
| 15 |
+
import soundfile as sf
|
| 16 |
+
import io
|
| 17 |
+
import os
|
| 18 |
+
import torch
|
| 19 |
+
import torchaudio
|
| 20 |
+
import google.generativeai as genai
|
| 21 |
+
import logging
|
| 22 |
+
import json
|
| 23 |
+
import re
|
| 24 |
+
from typing import Optional, Generator, Dict
|
| 25 |
+
from transformers import pipeline, AutoModelForSpeechSeq2Seq, AutoProcessor
|
| 26 |
+
|
| 27 |
+
from app.core.config import GEMINI_API_KEY, SYSTEM_INSTRUCTION
|
| 28 |
+
from app.utils.translit import lotin_to_kirill, clean_cyrillic_text
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Logging sozlash
|
| 32 |
+
logging.basicConfig(level=logging.INFO)
|
| 33 |
+
logger = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
# --- SOZLAMALAR ---
|
| 36 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 37 |
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 38 |
+
TORCH_DTYPE = torch.float16 if torch.cuda.is_available() else torch.float32
|
| 39 |
+
|
| 40 |
+
# Model holati tekshirish
|
| 41 |
+
MODELS = {}
|
| 42 |
+
MODEL_STATUS = {
|
| 43 |
+
"stt": False,
|
| 44 |
+
"tts_uzb": False,
|
| 45 |
+
"tts_eng": False,
|
| 46 |
+
"tts_rus": False,
|
| 47 |
+
"llm": False
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# Audio format validatsiyasi
|
| 51 |
+
SUPPORTED_AUDIO_FORMATS = {
|
| 52 |
+
'webm': 'audio/webm',
|
| 53 |
+
'wav': 'audio/wav',
|
| 54 |
+
'mp3': 'audio/mpeg',
|
| 55 |
+
'ogg': 'audio/ogg',
|
| 56 |
+
'm4a': 'audio/mp4'
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
MAX_AUDIO_SIZE = 100 * 1024 * 1024 # 100MB
|
| 60 |
+
MIN_AUDIO_DURATION = 0.5 # 0.5 sekund
|
| 61 |
+
MAX_AUDIO_DURATION = 300 # 5 minut
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ==================== MODEL YUKLASH ====================
|
| 65 |
+
|
| 66 |
+
def load_models():
|
| 67 |
+
"""Barcha modellarni yuklab, statuslarini yangilaydi"""
|
| 68 |
+
logger.info("🚀 Modellar Hugging Face Hub'dan yuklanmoqda...")
|
| 69 |
+
|
| 70 |
+
# ========== STT MODELI ==========
|
| 71 |
+
try:
|
| 72 |
+
logger.info(" 📥 STT (islomov/rubaistt_v2_medium) modeli yuklanmoqda...")
|
| 73 |
+
stt_model_id = "islomov/rubaistt_v2_medium"
|
| 74 |
+
|
| 75 |
+
stt_model = AutoModelForSpeechSeq2Seq.from_pretrained(
|
| 76 |
+
stt_model_id,
|
| 77 |
+
torch_dtype=TORCH_DTYPE,
|
| 78 |
+
low_cpu_mem_usage=True,
|
| 79 |
+
use_safetensors=True
|
| 80 |
+
)
|
| 81 |
+
stt_model.to(DEVICE)
|
| 82 |
+
stt_processor = AutoProcessor.from_pretrained(stt_model_id)
|
| 83 |
+
|
| 84 |
+
MODELS["stt_pipeline"] = pipeline(
|
| 85 |
+
"automatic-speech-recognition",
|
| 86 |
+
model=stt_model,
|
| 87 |
+
tokenizer=stt_processor.tokenizer,
|
| 88 |
+
feature_extractor=stt_processor.feature_extractor,
|
| 89 |
+
max_new_tokens=128,
|
| 90 |
+
torch_dtype=TORCH_DTYPE,
|
| 91 |
+
device=DEVICE,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
MODEL_STATUS["stt"] = True
|
| 95 |
+
logger.info(" ✅ STT modeli tayyor")
|
| 96 |
+
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f" ❌ STT modelini yuklashda xatolik: {e}")
|
| 99 |
+
MODEL_STATUS["stt"] = False
|
| 100 |
+
|
| 101 |
+
# ========== TTS MODELLARI (3 ta: uzb, eng, rus) ==========
|
| 102 |
+
|
| 103 |
+
# TTS O'ZBEKCHA
|
| 104 |
+
try:
|
| 105 |
+
logger.info(" 🎧 TTS O'ZBEKCHA (facebook/mms-tts-uzb-script_cyrillic) modeli yuklanmoqda...")
|
| 106 |
+
tts_model_path = "facebook/mms-tts-uzb-script_cyrillic"
|
| 107 |
+
|
| 108 |
+
MODELS["tts_uzb_pipeline"] = pipeline(
|
| 109 |
+
"text-to-speech",
|
| 110 |
+
model=tts_model_path,
|
| 111 |
+
device=DEVICE
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
MODEL_STATUS["tts_uzb"] = True
|
| 115 |
+
logger.info(" ✅ TTS O'ZBEK modeli tayyor")
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f" ❌ TTS O'ZBEK modelini yuklashda xatolik: {e}")
|
| 119 |
+
MODEL_STATUS["tts_uzb"] = False
|
| 120 |
+
|
| 121 |
+
# TTS INGLIZCHA
|
| 122 |
+
try:
|
| 123 |
+
logger.info(" 🎧 TTS INGLIZCHA (facebook/mms-tts-eng) modeli yuklanmoqda...")
|
| 124 |
+
tts_eng_path = "facebook/mms-tts-eng"
|
| 125 |
+
|
| 126 |
+
MODELS["tts_eng_pipeline"] = pipeline(
|
| 127 |
+
"text-to-speech",
|
| 128 |
+
model=tts_eng_path,
|
| 129 |
+
device=DEVICE
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
MODEL_STATUS["tts_eng"] = True
|
| 133 |
+
logger.info(" ✅ TTS INGLIZ modeli tayyor")
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.error(f" ❌ TTS INGLIZ modelini yuklashda xatolik: {e}")
|
| 137 |
+
MODEL_STATUS["tts_eng"] = False
|
| 138 |
+
|
| 139 |
+
# TTS RUSCHA
|
| 140 |
+
try:
|
| 141 |
+
logger.info(" 🎧 TTS RUSCHA (facebook/mms-tts-rus) modeli yuklanmoqda...")
|
| 142 |
+
tts_rus_path = "facebook/mms-tts-rus"
|
| 143 |
+
|
| 144 |
+
MODELS["tts_rus_pipeline"] = pipeline(
|
| 145 |
+
"text-to-speech",
|
| 146 |
+
model=tts_rus_path,
|
| 147 |
+
device=DEVICE
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
MODEL_STATUS["tts_rus"] = True
|
| 151 |
+
logger.info(" ✅ TTS RUS modeli tayyor")
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error(f" ❌ TTS RUS modelini yuklashda xatolik: {e}")
|
| 155 |
+
MODEL_STATUS["tts_rus"] = False
|
| 156 |
+
|
| 157 |
+
# ========== LLM MODELI (Gemini) ==========
|
| 158 |
+
try:
|
| 159 |
+
logger.info(" 🧠 LLM (Gemini) modeli yuklanmoqda...")
|
| 160 |
+
# Sizning kodingizda "gemini-2.0-flash-exp" ishlatilgan ekan, shuni qoldiramiz
|
| 161 |
+
MODELS["llm"] = genai.GenerativeModel("gemini-2.0-flash-exp")
|
| 162 |
+
MODEL_STATUS["llm"] = True
|
| 163 |
+
logger.info(" ✅ LLM modeli tayyor")
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f" ❌ LLM modelini yuklashda xatolik: {e}")
|
| 167 |
+
MODEL_STATUS["llm"] = False
|
| 168 |
+
|
| 169 |
+
# ========== NATIJA ==========
|
| 170 |
+
if not any(MODEL_STATUS.values()):
|
| 171 |
+
raise RuntimeError("❌ CRITICAL: Hech qanday model yuklanmadi. Loyiha ishlay olmaydi.")
|
| 172 |
+
|
| 173 |
+
logger.info("=" * 60)
|
| 174 |
+
logger.info("✅ Modellar yuklash yakunlandi:")
|
| 175 |
+
for model_name, status in MODEL_STATUS.items():
|
| 176 |
+
logger.info(f" {model_name}: {'✅ Tayyor' if status else '❌ Yuklanmadi'}")
|
| 177 |
+
logger.info("=" * 60)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def check_model_status() -> dict:
|
| 181 |
+
"""Model holatlarini qaytaradi"""
|
| 182 |
+
return MODEL_STATUS.copy()
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# ==================== TIL ANIQLASH ====================
|
| 186 |
+
|
| 187 |
+
def detect_language(text: str) -> str:
|
| 188 |
+
"""
|
| 189 |
+
Matndan tilni aniqlaydi
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
text: Tahlil qilinadigan matn
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
"uzb" | "eng" | "rus"
|
| 196 |
+
"""
|
| 197 |
+
if not text or len(text.strip()) < 3:
|
| 198 |
+
return "uzb" # Default o'zbekcha
|
| 199 |
+
|
| 200 |
+
text_lower = text.lower()
|
| 201 |
+
|
| 202 |
+
# Ingliz tilining kalit so'zlari
|
| 203 |
+
english_keywords = [
|
| 204 |
+
'hello', 'help', 'my', 'heart', 'pain', 'can\'t', 'breathe',
|
| 205 |
+
'chest', 'head', 'stomach', 'feel', 'sick', 'please',
|
| 206 |
+
'i', 'am', 'the', 'and', 'have', 'is', 'it', 'hurts'
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
# Rus tilining kalit so'zlari
|
| 210 |
+
russian_keywords = [
|
| 211 |
+
'привет', 'помогите', 'болит', 'сердце', 'голова', 'живот',
|
| 212 |
+
'не могу', 'дышать', 'помощь', 'температура', 'у меня',
|
| 213 |
+
'я', 'мне', 'очень', 'плохо'
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
# O'zbek tilining kalit so'zlari
|
| 217 |
+
uzbek_keywords = [
|
| 218 |
+
'salom', 'assalomu', 'yordam', 'yurak', 'bosh', 'qorin',
|
| 219 |
+
'og\'rig\'i', 'nafas', 'harorat', 'yomon', 'bemor',
|
| 220 |
+
'menga', 'men', 'juda'
|
| 221 |
+
]
|
| 222 |
+
|
| 223 |
+
# Kirill alifbosini tekshirish
|
| 224 |
+
cyrillic_chars = sum(1 for c in text if '\u0400' <= c <= '\u04FF')
|
| 225 |
+
total_chars = len([c for c in text if c.isalpha()])
|
| 226 |
+
|
| 227 |
+
if total_chars > 0:
|
| 228 |
+
cyrillic_ratio = cyrillic_chars / total_chars
|
| 229 |
+
|
| 230 |
+
# Agar 50%+ kirill bo'lsa
|
| 231 |
+
if cyrillic_ratio > 0.5:
|
| 232 |
+
# Rus yoki o'zbek kirill
|
| 233 |
+
rus_count = sum(1 for keyword in russian_keywords if keyword in text_lower)
|
| 234 |
+
uzb_count = sum(1 for keyword in uzbek_keywords if keyword in text_lower)
|
| 235 |
+
|
| 236 |
+
if rus_count > uzb_count:
|
| 237 |
+
return "rus"
|
| 238 |
+
else:
|
| 239 |
+
return "uzb"
|
| 240 |
+
|
| 241 |
+
# Lotin alifbosi - ingliz yoki o'zbek
|
| 242 |
+
eng_count = sum(1 for keyword in english_keywords if keyword in text_lower)
|
| 243 |
+
uzb_count = sum(1 for keyword in uzbek_keywords if keyword in text_lower)
|
| 244 |
+
|
| 245 |
+
if eng_count > uzb_count and eng_count >= 2:
|
| 246 |
+
return "eng"
|
| 247 |
+
|
| 248 |
+
# Default: o'zbekcha
|
| 249 |
+
return "uzb"
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
# ==================== STT (Speech-to-Text) ====================
|
| 253 |
+
|
| 254 |
+
def transcribe_audio_from_bytes(audio_bytes: bytes) -> str:
|
| 255 |
+
"""
|
| 256 |
+
Xotiradagi audio baytlarni (WEBM, MP3, etc) qabul qilib,
|
| 257 |
+
FFmpeg yordamida WAV formatiga o'giradi va matnga aylantiradi.
|
| 258 |
+
|
| 259 |
+
YANGILANGAN: Ruscha transkripsiyani avtomatik kirilga o'tkazadi
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
audio_bytes: Audio baytlar
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
Transkripsiya qilingan matn (ruscha bo'lsa kirill formatda)
|
| 266 |
+
"""
|
| 267 |
+
logger.info(f"🎙️ Audio baytlar transkripsiya uchun qabul qilindi. Hajmi: {len(audio_bytes)} bayt")
|
| 268 |
+
|
| 269 |
+
if not MODEL_STATUS["stt"]:
|
| 270 |
+
logger.error("STT modeli yuklanmagan.")
|
| 271 |
+
raise RuntimeError("STT modeli ishlamaydi")
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
# 1-QADAM: FFmpeg yordamida formatni o'zgartirish (in-memory)
|
| 275 |
+
# Biz FFmpeg'ga kiruvchi ma'lumotni stdin'dan olishni va
|
| 276 |
+
# natijani stdout'ga 16kHz'li WAV formatida chiqarishni buyuramiz.
|
| 277 |
+
ffmpeg_command = [
|
| 278 |
+
"ffmpeg",
|
| 279 |
+
"-i", "pipe:0", # Kiruvchi ma'lumot standart kiritishdan (stdin)
|
| 280 |
+
"-f", "wav", # Chiquvchi format: WAV
|
| 281 |
+
"-ac", "1", # Kanallar soni: 1 (mono)
|
| 282 |
+
"-ar", "16000", # Chastota: 16000Hz (Whisper uchun standart)
|
| 283 |
+
"pipe:1" # Chiquvchi ma'lumot standart chiqarishga (stdout)
|
| 284 |
+
]
|
| 285 |
+
|
| 286 |
+
logger.info("FFmpeg bilan audio konvertatsiya boshlanmoqda...")
|
| 287 |
+
process = subprocess.run(
|
| 288 |
+
ffmpeg_command,
|
| 289 |
+
input=audio_bytes,
|
| 290 |
+
capture_output=True,
|
| 291 |
+
check=True
|
| 292 |
+
)
|
| 293 |
+
wav_audio_bytes = process.stdout
|
| 294 |
+
logger.info(f"✅ FFmpeg muvaffaqiyatli yakunlandi. WAV hajmi: {len(wav_audio_bytes)} bayt.")
|
| 295 |
+
|
| 296 |
+
# 2-QADAM: WAV baytlarini NumPy array'ga o'tkazish
|
| 297 |
+
audio_stream = io.BytesIO(wav_audio_bytes)
|
| 298 |
+
audio, sampling_rate = sf.read(audio_stream)
|
| 299 |
+
logger.debug(f"WAV ma'lumot NumPy array'ga o'girildi. Shape: {audio.shape}, Rate: {sampling_rate}")
|
| 300 |
+
|
| 301 |
+
# 3-QADAM: Whisper modeliga uzatish
|
| 302 |
+
generate_kwargs = {"language": "uzbek", "task": "transcribe"}
|
| 303 |
+
logger.info("🚀 Whisper modeliga transkripsiya uchun so'rov yuborilmoqda...")
|
| 304 |
+
|
| 305 |
+
outputs = MODELS["stt_pipeline"](
|
| 306 |
+
audio,
|
| 307 |
+
chunk_length_s=30,
|
| 308 |
+
generate_kwargs=generate_kwargs
|
| 309 |
+
)
|
| 310 |
+
result_text = outputs.get("text", "").strip()
|
| 311 |
+
logger.info(f"✅ Transkripsiya yakunlandi. Natija: '{result_text}'")
|
| 312 |
+
|
| 313 |
+
# ========== ✅ YANGI: RUSCHA KIRILGA O'TKAZISH ==========
|
| 314 |
+
detected_lang = detect_language(result_text)
|
| 315 |
+
|
| 316 |
+
if detected_lang == "rus":
|
| 317 |
+
# Ruscha lotin → kirill konvertatsiya
|
| 318 |
+
from app.utils.translit import russian_latin_to_cyrillic
|
| 319 |
+
result_text_cyrillic = russian_latin_to_cyrillic(result_text)
|
| 320 |
+
logger.info(f"🔄 Ruscha kirilga o'tkazildi: '{result_text_cyrillic}'")
|
| 321 |
+
return result_text_cyrillic
|
| 322 |
+
elif detected_lang == "uzb":
|
| 323 |
+
# O'zbekcha - lotin qoldiramiz (kerak bo'lsa kirilga o'tkazish mumkin)
|
| 324 |
+
return result_text
|
| 325 |
+
else:
|
| 326 |
+
# Ingliz yoki boshqa tillar - o'zgartirmasdan qaytarish
|
| 327 |
+
return result_text
|
| 328 |
+
|
| 329 |
+
except subprocess.CalledProcessError as e:
|
| 330 |
+
# FFmpeg xatolik bersa, uni log'ga yozamiz
|
| 331 |
+
logger.error(f"❌ FFmpeg xatoligi: {e.stderr.decode()}", exc_info=True)
|
| 332 |
+
raise RuntimeError(f"FFmpeg audio konvertatsiya qila olmadi.")
|
| 333 |
+
except Exception as e:
|
| 334 |
+
logger.error(f"❌ STT transkripsiya (baytlardan) xatoligi: {e}", exc_info=True)
|
| 335 |
+
raise e
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def transcribe_audio(audio_path: str) -> Generator[str, None, None]:
|
| 339 |
+
"""
|
| 340 |
+
Audio faylni o'qib, uni matnga aylantiradi
|
| 341 |
+
|
| 342 |
+
Args:
|
| 343 |
+
audio_path: Audio fayl yo'li
|
| 344 |
+
|
| 345 |
+
Yields:
|
| 346 |
+
str: Transkripsiya qilingan matn
|
| 347 |
+
"""
|
| 348 |
+
try:
|
| 349 |
+
logger.info(f"Fayldan audio o'qilmoqda: {audio_path}")
|
| 350 |
+
with open(audio_path, "rb") as f:
|
| 351 |
+
audio_bytes = f.read()
|
| 352 |
+
|
| 353 |
+
text_piece = transcribe_audio_from_bytes(audio_bytes)
|
| 354 |
+
|
| 355 |
+
if text_piece:
|
| 356 |
+
yield text_piece
|
| 357 |
+
else:
|
| 358 |
+
yield "Ovoz aniqlanmadi"
|
| 359 |
+
|
| 360 |
+
except FileNotFoundError as e:
|
| 361 |
+
logger.error(f"❌ Fayl topilmadi: {e}")
|
| 362 |
+
yield f"Fayl topilmadi: {str(e)}"
|
| 363 |
+
|
| 364 |
+
except ValueError as e:
|
| 365 |
+
logger.error(f"❌ Validatsiya xatoligi: {e}")
|
| 366 |
+
yield f"Xatolik: {str(e)}"
|
| 367 |
+
|
| 368 |
+
except Exception as e:
|
| 369 |
+
logger.error(f"❌ Fayldan STT transkripsiya xatoligi: {e}", exc_info=True)
|
| 370 |
+
yield f"Ovozni tanishda xatolik: {str(e)}"
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
# ==================== JSON PARSING (ROBUST) ====================
|
| 374 |
+
|
| 375 |
+
def extract_json_from_response(response_text: str) -> Dict:
|
| 376 |
+
"""
|
| 377 |
+
LLM javobidan JSON'ni ajratib oladi (ROBUST va xatoliklarga bardoshli)
|
| 378 |
+
|
| 379 |
+
VAZIFA-1: Bu funksiya Gemini'dan kelgan javobni har qanday formatda bo'lsa ham
|
| 380 |
+
JSON'ga parse qilishga harakat qiladi. Agar parse qilib bo'lmasa, default javob qaytaradi.
|
| 381 |
+
|
| 382 |
+
Args:
|
| 383 |
+
response_text: Gemini'dan kelgan raw text
|
| 384 |
+
|
| 385 |
+
Returns:
|
| 386 |
+
Dict: Parse qilingan JSON yoki default response
|
| 387 |
+
"""
|
| 388 |
+
try:
|
| 389 |
+
# 1. To'g'ridan-to'g'ri parse qilishga harakat
|
| 390 |
+
try:
|
| 391 |
+
return json.loads(response_text)
|
| 392 |
+
except json.JSONDecodeError:
|
| 393 |
+
pass
|
| 394 |
+
|
| 395 |
+
# 2. {...} qavslar ichini topishga harakat (nested brackets ham)
|
| 396 |
+
json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', response_text, re.DOTALL)
|
| 397 |
+
|
| 398 |
+
if json_match:
|
| 399 |
+
json_str = json_match.group(0)
|
| 400 |
+
try:
|
| 401 |
+
return json.loads(json_str)
|
| 402 |
+
except json.JSONDecodeError:
|
| 403 |
+
pass
|
| 404 |
+
|
| 405 |
+
# 3. ```json ... ``` code block ichini topishga harakat
|
| 406 |
+
code_block_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', response_text, re.DOTALL)
|
| 407 |
+
if code_block_match:
|
| 408 |
+
json_str = code_block_match.group(1)
|
| 409 |
+
try:
|
| 410 |
+
return json.loads(json_str)
|
| 411 |
+
except json.JSONDecodeError:
|
| 412 |
+
pass
|
| 413 |
+
|
| 414 |
+
# 4. Hech narsa topilmasa - default qaytarish
|
| 415 |
+
logger.warning(f"⚠️ JSON topilmadi, default qaytarilyapti. Response: {response_text[:200]}...")
|
| 416 |
+
return {
|
| 417 |
+
"risk_level": "sariq",
|
| 418 |
+
"response_text": "Kechirasiz, javobni qayta ishlashda muammo yuz berdi. Iltimos, boshqacha aytib ko'ring.",
|
| 419 |
+
"language": "uzb",
|
| 420 |
+
"address_extracted": None,
|
| 421 |
+
"district_extracted": None,
|
| 422 |
+
"symptoms_extracted": None,
|
| 423 |
+
"analysis_notes": "JSON parsing failed, returned default"
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
except Exception as e:
|
| 427 |
+
logger.error(f"❌ JSON parsing xatoligi: {e}")
|
| 428 |
+
return {
|
| 429 |
+
"risk_level": "sariq",
|
| 430 |
+
"response_text": "Texnik xatolik yuz berdi. Iltimos, qayta urinib ko'ring.",
|
| 431 |
+
"language": "uzb",
|
| 432 |
+
"address_extracted": None,
|
| 433 |
+
"district_extracted": None,
|
| 434 |
+
"symptoms_extracted": None,
|
| 435 |
+
"analysis_notes": f"Exception in JSON parsing: {str(e)}"
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
# ==================== LLM (Gemini) ====================
|
| 440 |
+
|
| 441 |
+
def get_gemini_response(prompt: str, stream: bool = False) -> Dict:
|
| 442 |
+
"""
|
| 443 |
+
Gemini dan javob oladi va uni ROBUST JSON sifatida tahlil qiladi
|
| 444 |
+
|
| 445 |
+
VAZIFA-1 & VAZIFA-2: Bu funksiya Gemini'ga so'rov yuborib, javobni
|
| 446 |
+
xatoliklarga bardoshli tarzda JSON'ga parse qiladi va kerakli maydonlarni
|
| 447 |
+
to'ldiradi (shu jumladan "language" maydoni).
|
| 448 |
+
|
| 449 |
+
Args:
|
| 450 |
+
prompt: Bemorning so'rovi va suhbat tarixi
|
| 451 |
+
stream: Stream rejimi (hozircha qo'llab-quvvatlanmaydi)
|
| 452 |
+
|
| 453 |
+
Returns:
|
| 454 |
+
Dict: Parse qilingan va validatsiya qilingan JSON
|
| 455 |
+
"""
|
| 456 |
+
try:
|
| 457 |
+
if not MODEL_STATUS["llm"]:
|
| 458 |
+
raise RuntimeError("LLM modeli ishlamaydi")
|
| 459 |
+
|
| 460 |
+
if stream:
|
| 461 |
+
raise NotImplementedError("JSON tahlili uchun stream rejimi qo'llab-quvvatlanmaydi")
|
| 462 |
+
|
| 463 |
+
full_prompt = f"{SYSTEM_INSTRUCTION}\n\nSuhbat Tarixi:\n{prompt}"
|
| 464 |
+
|
| 465 |
+
logger.info("🧠 Gemini'ga so'rov yuborilmoqda...")
|
| 466 |
+
response = MODELS["llm"].generate_content(full_prompt)
|
| 467 |
+
|
| 468 |
+
logger.info(f"✅ Gemini javobi qabul qilindi ({len(response.text)} belgi)")
|
| 469 |
+
logger.debug(f"Raw response: {response.text[:200]}...")
|
| 470 |
+
|
| 471 |
+
# ROBUST JSON PARSING (VAZIFA-1)
|
| 472 |
+
response_data = extract_json_from_response(response.text)
|
| 473 |
+
|
| 474 |
+
# VAZIFA-2: Kerakli maydonlar mavjudligini tekshirish va default qiymatlar
|
| 475 |
+
if "risk_level" not in response_data or response_data["risk_level"] not in ["qizil", "sariq", "yashil"]:
|
| 476 |
+
logger.warning(f"⚠️ risk_level noto'g'ri: {response_data.get('risk_level')}, default: sariq")
|
| 477 |
+
response_data["risk_level"] = "sariq"
|
| 478 |
+
|
| 479 |
+
if "response_text" not in response_data or not response_data["response_text"].strip():
|
| 480 |
+
logger.warning("⚠️ Gemini 'response_text' maydonini qaytarmadi. Fallback javob shakllantirilmoqda.")
|
| 481 |
+
|
| 482 |
+
risk = response_data.get("risk_level")
|
| 483 |
+
action = response_data.get("action")
|
| 484 |
+
lang = response_data.get("language", "uzb") # Tilni ham hisobga olamiz
|
| 485 |
+
|
| 486 |
+
# Vaziyatga qarab aqlli javob berish
|
| 487 |
+
if risk == "yashil" and action == "offer_doctor_recommendation":
|
| 488 |
+
if lang == "rus":
|
| 489 |
+
response_data["response_text"] = "Понятно, не о чем беспокоиться. Хотите, я порекомендую вам подходящего врача?"
|
| 490 |
+
elif lang == "eng":
|
| 491 |
+
response_data["response_text"] = "I understand, no need to worry. Would you like me to recommend a suitable doctor for you?"
|
| 492 |
+
else: # uzb
|
| 493 |
+
response_data["response_text"] = "Tushunarli, xavotirga o'rin yo'q. Sizga mos shifokor tavsiya qilishimni xohlaysizmi?"
|
| 494 |
+
|
| 495 |
+
elif risk in ["qizil", "sariq"]:
|
| 496 |
+
if lang == "rus":
|
| 497 |
+
response_data["response_text"] = "Понимаю, это серьезно. Пожалуйста, назовите ваш точный адрес, мы отправляем бригаду."
|
| 498 |
+
elif lang == "eng":
|
| 499 |
+
response_data["response_text"] = "I understand, this is serious. Please tell me your exact address, we are sending a team."
|
| 500 |
+
else: # uzb
|
| 501 |
+
response_data["response_text"] = "Tushundim, bu jiddiy holat. Iltimos, aniq manzilingizni ayting, brigada yuboryapmiz."
|
| 502 |
+
|
| 503 |
+
else:
|
| 504 |
+
# Agar hech qaysi holatga tushmasa, umumiy javob
|
| 505 |
+
if lang == "rus":
|
| 506 |
+
response_data["response_text"] = "Извините, я не совсем вас поняла. Пожалуйста, повторите."
|
| 507 |
+
elif lang == "eng":
|
| 508 |
+
response_data["response_text"] = "Sorry, I didn't quite understand. Please repeat."
|
| 509 |
+
else: # uzb
|
| 510 |
+
response_data["response_text"] = "Kechirasiz, sizni to'liq tushunmadim. Iltimos, qaytadan ayting."
|
| 511 |
+
|
| 512 |
+
# VAZIFA-2: CRITICAL - "language" maydoni ALBATTA BO'LISHI KERAK
|
| 513 |
+
if "language" not in response_data or response_data["language"] not in ["uzb", "eng", "rus"]:
|
| 514 |
+
# Fallback: response_text dan til aniqlash
|
| 515 |
+
detected_lang = detect_language(response_data.get("response_text", ""))
|
| 516 |
+
logger.warning(f"⚠️ Gemini 'language' qaytarmadi, fallback: {detected_lang}")
|
| 517 |
+
response_data["language"] = detected_lang
|
| 518 |
+
|
| 519 |
+
# Qolgan maydonlarni tekshirish
|
| 520 |
+
for field in ["address_extracted", "district_extracted", "symptoms_extracted", "pre_arrival_instruction_text", "analysis_notes"]:
|
| 521 |
+
if field not in response_data:
|
| 522 |
+
response_data[field] = None
|
| 523 |
+
|
| 524 |
+
logger.info(f"✅ Gemini javobi to'liq validatsiya qilindi: risk={response_data['risk_level']}, lang={response_data['language']}")
|
| 525 |
+
|
| 526 |
+
return response_data
|
| 527 |
+
|
| 528 |
+
except json.JSONDecodeError as e:
|
| 529 |
+
logger.error(f"❌ JSON decode xatoligi: {e}")
|
| 530 |
+
logger.error(f"Response matn: {response.text if 'response' in locals() else 'N/A'}")
|
| 531 |
+
|
| 532 |
+
return {
|
| 533 |
+
"risk_level": "sariq",
|
| 534 |
+
"response_text": "Kechirasiz, javobni qayta ishlashda muammo yuz berdi. Iltimos, boshqacha aytib ko'ring.",
|
| 535 |
+
"language": "uzb",
|
| 536 |
+
"address_extracted": None,
|
| 537 |
+
"district_extracted": None,
|
| 538 |
+
"symptoms_extracted": None,
|
| 539 |
+
"analysis_notes": f"JSON decode error: {str(e)}"
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
except Exception as e:
|
| 543 |
+
logger.error(f"❌ LLM kutilmagan xatolik: {e}", exc_info=True)
|
| 544 |
+
|
| 545 |
+
return {
|
| 546 |
+
"risk_level": "sariq",
|
| 547 |
+
"response_text": "Texnik xatolik yuz berdi. Iltimos, bir oz kuting va qayta urinib ko'ring.",
|
| 548 |
+
"language": "uzb",
|
| 549 |
+
"address_extracted": None,
|
| 550 |
+
"district_extracted": None,
|
| 551 |
+
"symptoms_extracted": None,
|
| 552 |
+
"analysis_notes": f"Unexpected error: {str(e)}"
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
# ==================== TTS (Text-to-Speech) ====================
|
| 557 |
+
|
| 558 |
+
def synthesize_speech(text: str, output_path: str, language: str = "uzb") -> bool:
|
| 559 |
+
"""
|
| 560 |
+
Matnni ovozga aylantiradi (KO'P TILLI: uzb, eng, rus)
|
| 561 |
+
|
| 562 |
+
YANGILANGAN:
|
| 563 |
+
- output_path validatsiyasi qo'shildi
|
| 564 |
+
- Ruscha lotin → kirill konverter qo'shildi
|
| 565 |
+
|
| 566 |
+
Args:
|
| 567 |
+
text: Ovozga aylantirilishi kerak bo'lgan matn
|
| 568 |
+
output_path: Saqlash uchun fayl yo'li (masalan: "static/audio/tts_case_025.wav")
|
| 569 |
+
language: "uzb" | "eng" | "rus"
|
| 570 |
+
|
| 571 |
+
Returns:
|
| 572 |
+
bool: Muvaffaqiyatli bo'lsa True, aks holda False
|
| 573 |
+
"""
|
| 574 |
+
try:
|
| 575 |
+
# ========== VALIDATSIYA ==========
|
| 576 |
+
|
| 577 |
+
# output_path tekshirish
|
| 578 |
+
if not output_path or not output_path.strip():
|
| 579 |
+
logger.error("❌ output_path bo'sh!")
|
| 580 |
+
return False
|
| 581 |
+
|
| 582 |
+
if not output_path.endswith('.wav'):
|
| 583 |
+
logger.warning(f"⚠️ output_path .wav bilan tugamaydi: {output_path}")
|
| 584 |
+
output_path += '.wav'
|
| 585 |
+
|
| 586 |
+
# Model mavjudligini tekshirish
|
| 587 |
+
if language == "eng" and not MODEL_STATUS.get("tts_eng", False):
|
| 588 |
+
logger.warning("⚠️ TTS_ENG modeli yo'q, TTS_UZB ishlatilmoqda")
|
| 589 |
+
language = "uzb"
|
| 590 |
+
|
| 591 |
+
if language == "rus" and not MODEL_STATUS.get("tts_rus", False):
|
| 592 |
+
logger.warning("⚠️ TTS_RUS modeli yo'q, TTS_UZB ishlatilmoqda")
|
| 593 |
+
language = "uzb"
|
| 594 |
+
|
| 595 |
+
if language == "uzb" and not MODEL_STATUS.get("tts_uzb", False):
|
| 596 |
+
raise RuntimeError("TTS_UZB modeli ishlamaydi")
|
| 597 |
+
|
| 598 |
+
if not text or not text.strip():
|
| 599 |
+
raise ValueError("Bo'sh matn ovozga aylantirilmaydi")
|
| 600 |
+
|
| 601 |
+
# Matn uzunligini tekshirish
|
| 602 |
+
if len(text) > 1000:
|
| 603 |
+
logger.warning(f"⚠️ Matn juda uzun ({len(text)} belgi), qisqartirilmoqda...")
|
| 604 |
+
text = text[:1000] + "..."
|
| 605 |
+
|
| 606 |
+
# ========== MATNNI TAYYORLASH ==========
|
| 607 |
+
|
| 608 |
+
if language == "uzb":
|
| 609 |
+
# O'zbekcha uchun kirill kerak
|
| 610 |
+
from app.utils.translit import lotin_to_kirill, clean_cyrillic_text
|
| 611 |
+
cyrillic_text = lotin_to_kirill(text)
|
| 612 |
+
cleaned_text = clean_cyrillic_text(cyrillic_text)
|
| 613 |
+
|
| 614 |
+
elif language == "eng":
|
| 615 |
+
# Inglizcha uchun faqat tozalash
|
| 616 |
+
cleaned_text = text.strip()
|
| 617 |
+
|
| 618 |
+
elif language == "rus":
|
| 619 |
+
# ✅ YANGI: Ruscha uchun kirill kerak
|
| 620 |
+
from app.utils.translit import russian_latin_to_cyrillic, clean_cyrillic_text
|
| 621 |
+
|
| 622 |
+
# Kirill nisbatini tekshirish
|
| 623 |
+
cyrillic_count = sum(1 for c in text if '\u0400' <= c <= '\u04FF')
|
| 624 |
+
total_chars = len([c for c in text if c.isalpha()])
|
| 625 |
+
cyrillic_ratio = cyrillic_count / max(total_chars, 1)
|
| 626 |
+
|
| 627 |
+
if cyrillic_ratio < 0.5: # Agar 50%dan kam kirill bo'lsa
|
| 628 |
+
logger.info("🔄 Ruscha matn lotindan kirilga o'tkazilmoqda...")
|
| 629 |
+
text = russian_latin_to_cyrillic(text)
|
| 630 |
+
|
| 631 |
+
cleaned_text = clean_cyrillic_text(text)
|
| 632 |
+
|
| 633 |
+
else:
|
| 634 |
+
cleaned_text = text.strip()
|
| 635 |
+
|
| 636 |
+
if not cleaned_text.strip():
|
| 637 |
+
raise ValueError("Tozalangan matn bo'sh")
|
| 638 |
+
|
| 639 |
+
logger.info(f"🗣️ TTS ({language.upper()}): '{cleaned_text[:50]}{'...' if len(cleaned_text) > 50 else ''}'")
|
| 640 |
+
|
| 641 |
+
# ========== MODEL TANLASH ==========
|
| 642 |
+
|
| 643 |
+
pipeline_key = f"tts_{language}_pipeline"
|
| 644 |
+
if pipeline_key not in MODELS:
|
| 645 |
+
raise RuntimeError(f"{pipeline_key} topilmadi")
|
| 646 |
+
|
| 647 |
+
# ========== OVOZ GENERATSIYA ==========
|
| 648 |
+
|
| 649 |
+
output = MODELS[pipeline_key](cleaned_text)
|
| 650 |
+
|
| 651 |
+
# Audio formatini to'g'rilash
|
| 652 |
+
import torch
|
| 653 |
+
import torchaudio
|
| 654 |
+
|
| 655 |
+
audio_data = torch.tensor(output["audio"])
|
| 656 |
+
|
| 657 |
+
if audio_data.dim() == 3:
|
| 658 |
+
audio_data = audio_data.squeeze(0)
|
| 659 |
+
elif audio_data.dim() == 1:
|
| 660 |
+
audio_data = audio_data.unsqueeze(0)
|
| 661 |
+
|
| 662 |
+
# ========== FAYLGA SAQLASH ==========
|
| 663 |
+
|
| 664 |
+
# Papka yaratish (xavfsiz)
|
| 665 |
+
output_dir = os.path.dirname(output_path)
|
| 666 |
+
if output_dir: # Bo'sh bo'lsa yaratmaydi
|
| 667 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 668 |
+
logger.info(f"📁 Papka tekshirildi: {output_dir}")
|
| 669 |
+
|
| 670 |
+
# Audio faylni saqlash
|
| 671 |
+
torchaudio.save(
|
| 672 |
+
output_path,
|
| 673 |
+
src=audio_data,
|
| 674 |
+
sample_rate=output["sampling_rate"]
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
logger.info(f"✅ Ovoz fayli saqlandi: {output_path}")
|
| 678 |
+
return True
|
| 679 |
+
|
| 680 |
+
except Exception as e:
|
| 681 |
+
logger.error(f"❌ TTS xatoligi: {e}", exc_info=True)
|
| 682 |
+
return False
|
app/utils/__pycache__/demo_gps.cpython-313.pyc
ADDED
|
Binary file (8.17 kB). View file
|
|
|
app/utils/__pycache__/district_matcher.cpython-313.pyc
ADDED
|
Binary file (5.97 kB). View file
|
|
|
app/utils/__pycache__/mahalla_matcher.cpython-313.pyc
ADDED
|
Binary file (10.5 kB). View file
|
|
|
app/utils/__pycache__/translit.cpython-313.pyc
ADDED
|
Binary file (3.96 kB). View file
|
|
|
app/utils/demo_gps.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/utils/demo_gps.py
|
| 2 |
+
"""
|
| 3 |
+
Demo GPS Generator - Hackathon uchun fake GPS ma'lumotlar
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
import random
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Dict, Optional
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Tashkent districts faylining yo'li
|
| 15 |
+
DISTRICTS_FILE = Path("data/tashkent_districts.json")
|
| 16 |
+
|
| 17 |
+
# Toshkent chegaralari
|
| 18 |
+
TASHKENT_LAT_MIN = 41.20
|
| 19 |
+
TASHKENT_LAT_MAX = 41.35
|
| 20 |
+
TASHKENT_LON_MIN = 69.10
|
| 21 |
+
TASHKENT_LON_MAX = 69.35
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def generate_random_tashkent_gps() -> Dict[str, float]:
|
| 25 |
+
"""
|
| 26 |
+
Tasodifiy Tashkent koordinatalarini yaratish
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
{"lat": 41.2856, "lon": 69.2034}
|
| 30 |
+
"""
|
| 31 |
+
try:
|
| 32 |
+
lat = round(random.uniform(TASHKENT_LAT_MIN, TASHKENT_LAT_MAX), 6)
|
| 33 |
+
lon = round(random.uniform(TASHKENT_LON_MIN, TASHKENT_LON_MAX), 6)
|
| 34 |
+
|
| 35 |
+
logger.info(f"🎲 Tasodifiy GPS yaratildi: {lat}, {lon}")
|
| 36 |
+
|
| 37 |
+
return {
|
| 38 |
+
"lat": lat,
|
| 39 |
+
"lon": lon
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"❌ Tasodifiy GPS yaratishda xatolik: {e}")
|
| 44 |
+
# Fallback: Toshkent markazi
|
| 45 |
+
return {
|
| 46 |
+
"lat": 41.2995,
|
| 47 |
+
"lon": 69.2401
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_gps_for_district(district_name: str) -> Optional[Dict[str, float]]:
|
| 52 |
+
"""
|
| 53 |
+
Tuman nomi bo'yicha markaz koordinatalarini olish
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
district_name: "Chilonzor" yoki "chilonzor" (case-insensitive)
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
{"lat": 41.2767, "lon": 69.2095} yoki None
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
if not DISTRICTS_FILE.exists():
|
| 63 |
+
logger.error(f"❌ Districts fayli topilmadi: {DISTRICTS_FILE}")
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
with open(DISTRICTS_FILE, 'r', encoding='utf-8') as f:
|
| 67 |
+
data = json.load(f)
|
| 68 |
+
|
| 69 |
+
districts = data.get('districts', [])
|
| 70 |
+
|
| 71 |
+
# District nomini normallashtirish
|
| 72 |
+
district_name_lower = district_name.lower().strip()
|
| 73 |
+
district_name_lower = district_name_lower.replace(' tumani', '').replace(' Tumani', '')
|
| 74 |
+
|
| 75 |
+
# District ni topish
|
| 76 |
+
for district in districts:
|
| 77 |
+
district_id = district.get('id', '').lower()
|
| 78 |
+
district_full_name = district.get('name', '').lower()
|
| 79 |
+
|
| 80 |
+
if (district_name_lower in district_id or
|
| 81 |
+
district_name_lower in district_full_name or
|
| 82 |
+
district_id in district_name_lower):
|
| 83 |
+
|
| 84 |
+
gps = {
|
| 85 |
+
"lat": district.get('center_lat'),
|
| 86 |
+
"lon": district.get('center_lon')
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
logger.info(f"✅ {district_name} uchun GPS topildi: {gps['lat']}, {gps['lon']}")
|
| 90 |
+
return gps
|
| 91 |
+
|
| 92 |
+
logger.warning(f"⚠️ District topilmadi: {district_name}")
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
except json.JSONDecodeError as e:
|
| 96 |
+
logger.error(f"❌ JSON parse xatoligi (tashkent_districts.json): {e}")
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.error(f"❌ District GPS olishda xatolik: {e}")
|
| 101 |
+
return None
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def add_gps_noise(lat: float, lon: float, noise_km: float = 0.5) -> Dict[str, float]:
|
| 105 |
+
"""
|
| 106 |
+
GPS koordinatalariga tasodifiy "shovqin" qo'shish (realistik qilish uchun)
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
lat: Asl latitude
|
| 110 |
+
lon: Asl longitude
|
| 111 |
+
noise_km: Maksimal shovqin masofasi (km)
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
{"lat": ..., "lon": ...} - shovqinli koordinatalar
|
| 115 |
+
"""
|
| 116 |
+
try:
|
| 117 |
+
# 1 km ~ 0.009 daraja (taxminan)
|
| 118 |
+
noise_degrees = noise_km * 0.009
|
| 119 |
+
|
| 120 |
+
lat_noise = random.uniform(-noise_degrees, noise_degrees)
|
| 121 |
+
lon_noise = random.uniform(-noise_degrees, noise_degrees)
|
| 122 |
+
|
| 123 |
+
noisy_lat = round(lat + lat_noise, 6)
|
| 124 |
+
noisy_lon = round(lon + lon_noise, 6)
|
| 125 |
+
|
| 126 |
+
logger.debug(f"🔊 GPS'ga shovqin qo'shildi: ({lat}, {lon}) → ({noisy_lat}, {noisy_lon})")
|
| 127 |
+
|
| 128 |
+
return {
|
| 129 |
+
"lat": noisy_lat,
|
| 130 |
+
"lon": noisy_lon
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logger.error(f"❌ GPS shovqin qo'shishda xatolik: {e}")
|
| 135 |
+
return {"lat": lat, "lon": lon}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def get_all_districts() -> list:
|
| 139 |
+
"""
|
| 140 |
+
Barcha tumanlar ro'yxatini olish (dropdown uchun)
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
[
|
| 144 |
+
{"id": "chilonzor", "name": "Chilonzor tumani"},
|
| 145 |
+
...
|
| 146 |
+
]
|
| 147 |
+
"""
|
| 148 |
+
try:
|
| 149 |
+
if not DISTRICTS_FILE.exists():
|
| 150 |
+
logger.error(f"❌ Districts fayli topilmadi: {DISTRICTS_FILE}")
|
| 151 |
+
return []
|
| 152 |
+
|
| 153 |
+
with open(DISTRICTS_FILE, 'r', encoding='utf-8') as f:
|
| 154 |
+
data = json.load(f)
|
| 155 |
+
|
| 156 |
+
districts = data.get('districts', [])
|
| 157 |
+
|
| 158 |
+
# Faqat kerakli maydonlarni qaytarish
|
| 159 |
+
result = [
|
| 160 |
+
{
|
| 161 |
+
"id": d.get('id'),
|
| 162 |
+
"name": d.get('name')
|
| 163 |
+
}
|
| 164 |
+
for d in districts
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
logger.info(f"✅ {len(result)} ta tuman ro'yxati tayyorlandi")
|
| 168 |
+
return result
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.error(f"❌ Tumanlar ro'yxatini olishda xatolik: {e}")
|
| 172 |
+
return []
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def generate_gps_near_location(target_lat: float, target_lon: float, radius_km: float = 2.0) -> Dict[str, float]:
|
| 176 |
+
"""
|
| 177 |
+
Berilgan nuqtaga yaqin tasodifiy GPS yaratish
|
| 178 |
+
|
| 179 |
+
Args:
|
| 180 |
+
target_lat: Maqsad latitude
|
| 181 |
+
target_lon: Maqsad longitude
|
| 182 |
+
radius_km: Radius (km)
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
{"lat": ..., "lon": ...}
|
| 186 |
+
"""
|
| 187 |
+
try:
|
| 188 |
+
# Tasodifiy burchak (0-360 daraja)
|
| 189 |
+
angle = random.uniform(0, 2 * 3.14159)
|
| 190 |
+
|
| 191 |
+
# Tasodifiy masofa (0 - radius_km)
|
| 192 |
+
distance = random.uniform(0, radius_km)
|
| 193 |
+
|
| 194 |
+
# 1 km ~ 0.009 daraja
|
| 195 |
+
distance_degrees = distance * 0.009
|
| 196 |
+
|
| 197 |
+
# Yangi koordinatalar
|
| 198 |
+
new_lat = target_lat + distance_degrees * random.choice([-1, 1])
|
| 199 |
+
new_lon = target_lon + distance_degrees * random.choice([-1, 1])
|
| 200 |
+
|
| 201 |
+
# Toshkent chegarasida ekanligini tekshirish
|
| 202 |
+
new_lat = max(TASHKENT_LAT_MIN, min(TASHKENT_LAT_MAX, new_lat))
|
| 203 |
+
new_lon = max(TASHKENT_LON_MIN, min(TASHKENT_LON_MAX, new_lon))
|
| 204 |
+
|
| 205 |
+
logger.debug(f"📍 Yaqin GPS yaratildi: {distance:.2f}km uzoqlikda")
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
"lat": round(new_lat, 6),
|
| 209 |
+
"lon": round(new_lon, 6)
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
except Exception as e:
|
| 213 |
+
logger.error(f"❌ Yaqin GPS yaratishda xatolik: {e}")
|
| 214 |
+
return {"lat": target_lat, "lon": target_lon}
|
app/utils/district_matcher.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/utils/district_matcher.py
|
| 2 |
+
"""
|
| 3 |
+
District Matcher - Noto'g'ri yozilgan tuman nomlarini topish
|
| 4 |
+
Fuzzy matching ishlatiladi
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Optional
|
| 9 |
+
from difflib import SequenceMatcher
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
# Toshkent tumanlari (barcha variantlar bilan)
|
| 14 |
+
# DISTRICT_VARIANTS - KO'PROQ VARIANTLAR BILAN
|
| 15 |
+
DISTRICT_VARIANTS = {
|
| 16 |
+
"chilonzor": [
|
| 17 |
+
"chilonzor", "chilanazor", "chillonzor", "chilanzor", "chilinzor",
|
| 18 |
+
"chilanzar", "chilinzar", "chilonzar", "chilanzur"
|
| 19 |
+
],
|
| 20 |
+
"yunusobod": [
|
| 21 |
+
"yunusobod", "yunusabad", "yunusabod", "yunusobod", "iunusobod",
|
| 22 |
+
"yunus obod", "yunus abad", "yunusabat", "iunusabad"
|
| 23 |
+
],
|
| 24 |
+
"mirzo_ulugbek": [
|
| 25 |
+
"mirzo ulugbek", "mirzo ulug'bek", "mirzo ulugʻbek", "mirza ulugbek",
|
| 26 |
+
"ttg", "mirzo ulug bek", "mirza ulug'bek", "ulugbek", "ulug'bek"
|
| 27 |
+
],
|
| 28 |
+
"shayxontohur": [
|
| 29 |
+
"shayxontohur", "shayxontoxur", "shayhontohur", "shayxantoxur",
|
| 30 |
+
"sayxontohur", "sheyhontoxur", "shayxon tohur", "shayxon toxur",
|
| 31 |
+
"shayx tohur", "shayx toxur"
|
| 32 |
+
],
|
| 33 |
+
"yakkasaroy": [
|
| 34 |
+
"yakkasaroy", "yakkasaray", "yakasaroy", "yakkosaroy", "iakkasaroy",
|
| 35 |
+
"yakka saroy", "yakka saray", "yakkasarai"
|
| 36 |
+
],
|
| 37 |
+
"mirobod": [
|
| 38 |
+
"mirobod", "mirabod", "mirobad", "mirabod", "mirobad",
|
| 39 |
+
"mir obod", "mir abad", "mirabat"
|
| 40 |
+
],
|
| 41 |
+
"yashnobod": [
|
| 42 |
+
"yashnobod", "yashnabad", "yeshnobod", "yashnabod", "yashnobad",
|
| 43 |
+
"yash nobod", "yash nabad", "yashnabat"
|
| 44 |
+
],
|
| 45 |
+
"sergeli": [
|
| 46 |
+
"sergeli", "sergili", "sirgeli", "sergeley", "sirgili",
|
| 47 |
+
"sergel", "sergil"
|
| 48 |
+
],
|
| 49 |
+
"bektemir": [
|
| 50 |
+
"bektemir", "bektemar", "bektimir", "bektamir", "bektemur",
|
| 51 |
+
"bek temir", "bek tamir", "bektamur"
|
| 52 |
+
],
|
| 53 |
+
"uchtepa": [
|
| 54 |
+
"uchtepa", "uchtepe", "uchtepa", "uchtipi", "uchtepo",
|
| 55 |
+
"uch tepa", "uch tepe", "uchtipa"
|
| 56 |
+
],
|
| 57 |
+
"olmazor": [
|
| 58 |
+
"olmazor", "olmazor", "almazor", "olmozor", "almazor",
|
| 59 |
+
"olma zor", "alma zor", "olmazar"
|
| 60 |
+
],
|
| 61 |
+
"yangihayot": [
|
| 62 |
+
"yangihayot", "yangihayat", "yangi hayot", "yangixayot", "yangihoyot",
|
| 63 |
+
"yangi xayot", "yangi hayat", "yangihayat"
|
| 64 |
+
]
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def normalize_text(text: str) -> str:
|
| 69 |
+
"""
|
| 70 |
+
Matnni normalizatsiya qilish (kichik harf, probel olib tashlash)
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
text: Asl matn
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
Normalized matn
|
| 77 |
+
"""
|
| 78 |
+
if not text:
|
| 79 |
+
return ""
|
| 80 |
+
|
| 81 |
+
# Kichik harf
|
| 82 |
+
text = text.lower().strip()
|
| 83 |
+
|
| 84 |
+
# Ko'p probellarni bitta probelga
|
| 85 |
+
text = " ".join(text.split())
|
| 86 |
+
|
| 87 |
+
# "tumani" so'zini olib tashlash
|
| 88 |
+
text = text.replace(" tumani", "").replace(" tuman", "")
|
| 89 |
+
|
| 90 |
+
return text
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def similarity_score(str1: str, str2: str) -> float:
|
| 94 |
+
"""
|
| 95 |
+
Ikki string orasidagi o'xshashlik (0.0 - 1.0)
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
str1: Birinchi string
|
| 99 |
+
str2: Ikkinchi string
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Similarity score (1.0 = 100% o'xshash)
|
| 103 |
+
"""
|
| 104 |
+
return SequenceMatcher(None, str1, str2).ratio()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def find_district_fuzzy(user_text: str, threshold: float = 0.5) -> Optional[str]:
|
| 108 |
+
"""
|
| 109 |
+
Noto'g'ri yozilgan tuman nomini topish (YAXSHILANGAN FUZZY MATCHING)
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
user_text: Bemorning kiritgan matni (masalan: "chillonzor" yoki "yunusabad")
|
| 113 |
+
threshold: Minimal o'xshashlik darajasi (0.5 = 50%) ← PASTROQ!
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
District ID (masalan: "chilonzor") yoki None
|
| 117 |
+
"""
|
| 118 |
+
try:
|
| 119 |
+
if not user_text:
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
# Matnni normalizatsiya qilish
|
| 123 |
+
normalized_input = normalize_text(user_text)
|
| 124 |
+
logger.info(f"🏙️ Tuman qidirilmoqda: '{user_text}' → '{normalized_input}'")
|
| 125 |
+
|
| 126 |
+
if len(normalized_input) < 3:
|
| 127 |
+
logger.warning("⚠️ Matn juda qisqa")
|
| 128 |
+
return None
|
| 129 |
+
|
| 130 |
+
# Eng yaxshi moslikni topish
|
| 131 |
+
best_match = None
|
| 132 |
+
best_score = 0.0
|
| 133 |
+
|
| 134 |
+
for district_id, variants in DISTRICT_VARIANTS.items():
|
| 135 |
+
for variant in variants:
|
| 136 |
+
# 1. To'liq fuzzy match
|
| 137 |
+
score = similarity_score(normalized_input, variant)
|
| 138 |
+
|
| 139 |
+
# 2. Substring match (bonus)
|
| 140 |
+
if normalized_input in variant or variant in normalized_input:
|
| 141 |
+
score = max(score, 0.85)
|
| 142 |
+
|
| 143 |
+
# 3. So'z boshi match (bonus)
|
| 144 |
+
if variant.startswith(normalized_input[:4]) or normalized_input.startswith(variant[:4]):
|
| 145 |
+
score = max(score, 0.75)
|
| 146 |
+
|
| 147 |
+
if score > best_score:
|
| 148 |
+
best_score = score
|
| 149 |
+
best_match = district_id
|
| 150 |
+
|
| 151 |
+
# Threshold tekshirish
|
| 152 |
+
if best_score >= threshold:
|
| 153 |
+
logger.info(f"✅ Tuman topildi: '{best_match}' (score: {best_score:.2f})")
|
| 154 |
+
return best_match
|
| 155 |
+
else:
|
| 156 |
+
logger.warning(f"⚠️ Tuman topilmadi (best score: {best_score:.2f} < {threshold})")
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logger.error(f"❌ District matching xatoligi: {e}")
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def get_district_display_name(district_id: str) -> str:
|
| 165 |
+
"""
|
| 166 |
+
District ID'dan to'liq nom olish
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
district_id: "chilonzor"
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
"Chilonzor tumani"
|
| 173 |
+
"""
|
| 174 |
+
district_names = {
|
| 175 |
+
"chilonzor": "Chilonzor tumani",
|
| 176 |
+
"yunusobod": "Yunusobod tumani",
|
| 177 |
+
"mirzo_ulugbek": "Mirzo Ulug'bek tumani",
|
| 178 |
+
"shayxontohur": "Shayxontohur tumani",
|
| 179 |
+
"yakkasaroy": "Yakkasaroy tumani",
|
| 180 |
+
"mirobod": "Mirobod tumani",
|
| 181 |
+
"yashnobod": "Yashnobod tumani",
|
| 182 |
+
"sergeli": "Sergeli tumani",
|
| 183 |
+
"bektemir": "Bektemir tumani",
|
| 184 |
+
"uchtepa": "Uchtepa tumani",
|
| 185 |
+
"olmazor": "Olmazor tumani",
|
| 186 |
+
"yangihayot": "Yangihayot tumani"
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
return district_names.get(district_id, district_id)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def list_all_districts_text() -> str:
|
| 193 |
+
"""
|
| 194 |
+
Barcha tumanlar nomini matn sifatida qaytarish (AI uchun)
|
| 195 |
+
|
| 196 |
+
Returns:
|
| 197 |
+
"Chilonzor, Yunusobod, Mirzo Ulug'bek, ..."
|
| 198 |
+
"""
|
| 199 |
+
districts = [
|
| 200 |
+
"Chilonzor", "Yunusobod", "Mirzo Ulug'bek", "Shayxontohur",
|
| 201 |
+
"Yakkasaroy", "Mirobod", "Yashnobod", "Sergeli",
|
| 202 |
+
"Bektemir", "Uchtepa", "Olmazor", "Yangihayot"
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
return ", ".join(districts)
|
app/utils/mahalla_matcher.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/utils/mahalla_matcher.py - YAXSHILANGAN (THRESHOLD 0.35)
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Mahalla Matcher - Noto'g'ri yozilgan mahalla nomlarini topish
|
| 5 |
+
Fuzzy matching + substring + word matching
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Optional, List
|
| 10 |
+
from difflib import SequenceMatcher
|
| 11 |
+
|
| 12 |
+
from app.services.location_validator import get_mahallas_by_district
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def normalize_mahalla_text(text: str) -> str:
|
| 18 |
+
"""
|
| 19 |
+
Mahalla nomini normalizatsiya qilish (KENGAYTIRILGAN)
|
| 20 |
+
O'zbek tili qo'shimchalarini olib tashlaydi
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
text: Asl matn
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
Normalized matn
|
| 27 |
+
"""
|
| 28 |
+
if not text:
|
| 29 |
+
return ""
|
| 30 |
+
|
| 31 |
+
# Kichik harf
|
| 32 |
+
text = text.lower().strip()
|
| 33 |
+
|
| 34 |
+
# Ko'p probellarni bitta probelga
|
| 35 |
+
text = " ".join(text.split())
|
| 36 |
+
|
| 37 |
+
# "mahallasi", "mahalla" so'zlarini olib tashlash
|
| 38 |
+
text = text.replace(' mahallasi', '').replace(' mahalla', '')
|
| 39 |
+
text = text.replace('mahallasi', '').replace('mahalla', '')
|
| 40 |
+
|
| 41 |
+
# O'zbek tili kelishik qo'shimchalarini olib tashlash (KENGAYTIRILGAN)
|
| 42 |
+
suffixes = [
|
| 43 |
+
"ni", "ga", "da", "dan", "ning", "niki",
|
| 44 |
+
"dagi", "dagina", "gacha", "dan", "dek",
|
| 45 |
+
"lar", "larni", "larga", "larda", "lardan"
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
words = text.split()
|
| 49 |
+
|
| 50 |
+
if words:
|
| 51 |
+
last_word = words[-1]
|
| 52 |
+
for suffix in suffixes:
|
| 53 |
+
if last_word.endswith(suffix) and len(last_word) > len(suffix) + 2: # Juda qisqa so'zlarni saqlab qolish
|
| 54 |
+
words[-1] = last_word[:-len(suffix)]
|
| 55 |
+
break
|
| 56 |
+
|
| 57 |
+
text = " ".join(words)
|
| 58 |
+
|
| 59 |
+
# Oxiridagi tinish belgilarini olib tashlash
|
| 60 |
+
text = text.strip('.,!? ')
|
| 61 |
+
|
| 62 |
+
return text.strip()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def similarity_score(str1: str, str2: str) -> float:
|
| 66 |
+
"""
|
| 67 |
+
Ikki string orasidagi o'xshashlik (0.0 - 1.0)
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
str1: Birinchi string
|
| 71 |
+
str2: Ikkinchi string
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
Similarity score (1.0 = 100% o'xshash)
|
| 75 |
+
"""
|
| 76 |
+
return SequenceMatcher(None, str1, str2).ratio()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def word_similarity(str1: str, str2: str) -> float:
|
| 80 |
+
"""
|
| 81 |
+
So'zma-so'z o'xshashlik (word-level matching)
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
str1: Birinchi matn
|
| 85 |
+
str2: Ikkinchi matn
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
Word overlap score (0.0 - 1.0)
|
| 89 |
+
"""
|
| 90 |
+
words1 = set(str1.split())
|
| 91 |
+
words2 = set(str2.split())
|
| 92 |
+
|
| 93 |
+
if not words1 or not words2:
|
| 94 |
+
return 0.0
|
| 95 |
+
|
| 96 |
+
intersection = words1.intersection(words2)
|
| 97 |
+
union = words1.union(words2)
|
| 98 |
+
|
| 99 |
+
return len(intersection) / len(union) if union else 0.0
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def find_mahalla_fuzzy(district_name: str, user_text: str, threshold: float = 0.35) -> Optional[str]:
|
| 103 |
+
"""
|
| 104 |
+
Noto'g'ri yozilgan mahalla nomini topish (YAXSHILANGAN - THRESHOLD 0.35)
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
district_name: Tuman nomi
|
| 108 |
+
user_text: Bemorning kiritgan matni (masalan: "katta chilonzor" yoki "besh qorgon")
|
| 109 |
+
threshold: Minimal o'xshashlik darajasi (0.35 = 35%) ← PASAYTIRILDI!
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Mahalla nomi (masalan: "Katta Chilonzor-1 mahallasi") yoki None
|
| 113 |
+
"""
|
| 114 |
+
try:
|
| 115 |
+
if not user_text or not district_name:
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
# Matnni normalizatsiya qilish
|
| 119 |
+
normalized_input = normalize_mahalla_text(user_text)
|
| 120 |
+
logger.info(f"🏘️ Mahalla qidirilmoqda: '{user_text}' → '{normalized_input}' ({district_name})")
|
| 121 |
+
|
| 122 |
+
if len(normalized_input) < 2:
|
| 123 |
+
logger.warning("⚠️ Matn juda qisqa")
|
| 124 |
+
return None
|
| 125 |
+
|
| 126 |
+
# Tuman bo'yicha mahallalarni olish
|
| 127 |
+
mahallas = get_mahallas_by_district(district_name)
|
| 128 |
+
|
| 129 |
+
if not mahallas:
|
| 130 |
+
logger.warning(f"⚠️ {district_name} uchun mahallalar topilmadi")
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
# Eng yaxshi moslikni topish
|
| 134 |
+
best_match = None
|
| 135 |
+
best_score = 0.0
|
| 136 |
+
scoring_details = []
|
| 137 |
+
|
| 138 |
+
for mahalla in mahallas:
|
| 139 |
+
normalized_mahalla = normalize_mahalla_text(mahalla)
|
| 140 |
+
|
| 141 |
+
# 1. To'liq fuzzy match (SequenceMatcher)
|
| 142 |
+
fuzzy_score = similarity_score(normalized_input, normalized_mahalla)
|
| 143 |
+
|
| 144 |
+
# 2. Substring match (KATTA BONUS)
|
| 145 |
+
substring_score = 0.0
|
| 146 |
+
if normalized_input in normalized_mahalla:
|
| 147 |
+
substring_score = 0.9 # 90% match
|
| 148 |
+
logger.debug(f" ✓ Substring (input in mahalla): '{normalized_input}' in '{normalized_mahalla}'")
|
| 149 |
+
elif normalized_mahalla in normalized_input:
|
| 150 |
+
substring_score = 0.85 # 85% match
|
| 151 |
+
logger.debug(f" ✓ Substring (mahalla in input): '{normalized_mahalla}' in '{normalized_input}'")
|
| 152 |
+
|
| 153 |
+
# 3. So'zma-so'z match (Word overlap)
|
| 154 |
+
word_score = word_similarity(normalized_input, normalized_mahalla)
|
| 155 |
+
|
| 156 |
+
# 4. So'z boshi match (First word matching)
|
| 157 |
+
input_words = normalized_input.split()
|
| 158 |
+
mahalla_words = normalized_mahalla.split()
|
| 159 |
+
first_word_score = 0.0
|
| 160 |
+
if input_words and mahalla_words:
|
| 161 |
+
if input_words[0] == mahalla_words[0]:
|
| 162 |
+
first_word_score = 0.7 # Birinchi so'z mos kelsa - 70%
|
| 163 |
+
logger.debug(f" ✓ First word match: '{input_words[0]}'")
|
| 164 |
+
|
| 165 |
+
# 5. FINAL SCORE (eng yuqori ball)
|
| 166 |
+
final_score = max(fuzzy_score, substring_score, word_score, first_word_score)
|
| 167 |
+
|
| 168 |
+
scoring_details.append({
|
| 169 |
+
"mahalla": mahalla,
|
| 170 |
+
"fuzzy": fuzzy_score,
|
| 171 |
+
"substring": substring_score,
|
| 172 |
+
"word": word_score,
|
| 173 |
+
"first_word": first_word_score,
|
| 174 |
+
"final": final_score
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
if final_score > best_score:
|
| 178 |
+
best_score = final_score
|
| 179 |
+
best_match = mahalla
|
| 180 |
+
|
| 181 |
+
# Debug: Top 3 natijalar
|
| 182 |
+
scoring_details.sort(key=lambda x: x['final'], reverse=True)
|
| 183 |
+
logger.debug(f" Top 3 matches:")
|
| 184 |
+
for i, detail in enumerate(scoring_details[:3], 1):
|
| 185 |
+
logger.debug(f" {i}. {detail['mahalla']}: {detail['final']:.2f} "
|
| 186 |
+
f"(fuzzy={detail['fuzzy']:.2f}, sub={detail['substring']:.2f}, "
|
| 187 |
+
f"word={detail['word']:.2f}, first={detail['first_word']:.2f})")
|
| 188 |
+
|
| 189 |
+
# Threshold tekshirish
|
| 190 |
+
if best_score >= threshold:
|
| 191 |
+
logger.info(f"✅ Mahalla topildi: '{best_match}' (score: {best_score:.2f})")
|
| 192 |
+
return best_match
|
| 193 |
+
else:
|
| 194 |
+
logger.warning(f"⚠️ Mahalla topilmadi (best score: {best_score:.2f} < {threshold})")
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"❌ Mahalla matching xatoligi: {e}", exc_info=True)
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def get_mahalla_display_name(mahalla_name: str) -> str:
|
| 203 |
+
"""
|
| 204 |
+
Mahalla nomini to'liq formatda qaytarish
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
mahalla_name: "Beltepa" yoki "Beltepa mahallasi"
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
"Beltepa mahallasi"
|
| 211 |
+
"""
|
| 212 |
+
if not mahalla_name:
|
| 213 |
+
return ""
|
| 214 |
+
|
| 215 |
+
if 'mahallasi' not in mahalla_name.lower():
|
| 216 |
+
return f"{mahalla_name} mahallasi"
|
| 217 |
+
|
| 218 |
+
return mahalla_name
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def suggest_mahallas(district_name: str, user_text: str, top_n: int = 3) -> List[tuple]:
|
| 222 |
+
"""
|
| 223 |
+
Mahallalar tavsiyalari (top N eng o'xshash)
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
district_name: Tuman nomi
|
| 227 |
+
user_text: Bemorning matni
|
| 228 |
+
top_n: Ko'rsatiladigan maksimal tavsiyalar soni
|
| 229 |
+
|
| 230 |
+
Returns:
|
| 231 |
+
[(mahalla_name, score), ...] - eng yaxshilaridan boshlab
|
| 232 |
+
"""
|
| 233 |
+
try:
|
| 234 |
+
normalized_input = normalize_mahalla_text(user_text)
|
| 235 |
+
mahallas = get_mahallas_by_district(district_name)
|
| 236 |
+
|
| 237 |
+
if not mahallas:
|
| 238 |
+
return []
|
| 239 |
+
|
| 240 |
+
results = []
|
| 241 |
+
|
| 242 |
+
for mahalla in mahallas:
|
| 243 |
+
normalized_mahalla = normalize_mahalla_text(mahalla)
|
| 244 |
+
|
| 245 |
+
# Barcha scoringlar
|
| 246 |
+
fuzzy = similarity_score(normalized_input, normalized_mahalla)
|
| 247 |
+
substring = 0.9 if normalized_input in normalized_mahalla else 0.0
|
| 248 |
+
word = word_similarity(normalized_input, normalized_mahalla)
|
| 249 |
+
|
| 250 |
+
final_score = max(fuzzy, substring, word)
|
| 251 |
+
|
| 252 |
+
results.append((mahalla, final_score))
|
| 253 |
+
|
| 254 |
+
# Scorecard bo'yicha tartiblash
|
| 255 |
+
results.sort(key=lambda x: x[1], reverse=True)
|
| 256 |
+
|
| 257 |
+
return results[:top_n]
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
logger.error(f"❌ Mahalla tavsiyalari xatoligi: {e}")
|
| 261 |
+
return []
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
# ==================== TESTING HELPER ====================
|
| 265 |
+
|
| 266 |
+
def test_mahalla_matching(district_name: str, test_inputs: List[str]):
|
| 267 |
+
"""
|
| 268 |
+
Mahalla matching'ni test qilish uchun helper
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
district_name: Test qilinadigan tuman
|
| 272 |
+
test_inputs: Test qilinadigan inputlar ro'yxati
|
| 273 |
+
"""
|
| 274 |
+
logger.info(f"\n{'='*60}")
|
| 275 |
+
logger.info(f"TEST: {district_name}")
|
| 276 |
+
logger.info(f"{'='*60}")
|
| 277 |
+
|
| 278 |
+
for test_input in test_inputs:
|
| 279 |
+
logger.info(f"\nTest input: '{test_input}'")
|
| 280 |
+
|
| 281 |
+
result = find_mahalla_fuzzy(district_name, test_input, threshold=0.35)
|
| 282 |
+
|
| 283 |
+
if result:
|
| 284 |
+
logger.info(f" ✅ MATCH: {result}")
|
| 285 |
+
else:
|
| 286 |
+
logger.warning(f" ❌ NO MATCH")
|
| 287 |
+
suggestions = suggest_mahallas(district_name, test_input, top_n=3)
|
| 288 |
+
if suggestions:
|
| 289 |
+
logger.info(f" 💡 Suggestions:")
|
| 290 |
+
for mahalla, score in suggestions:
|
| 291 |
+
logger.info(f" - {mahalla} ({score:.2f})")
|
| 292 |
+
|
| 293 |
+
logger.info(f"{'='*60}\n")
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ==================== AUTO-TEST ON IMPORT (DEBUG MODE) ====================
|
| 297 |
+
|
| 298 |
+
if __name__ == "__main__":
|
| 299 |
+
# Test uchun
|
| 300 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 301 |
+
|
| 302 |
+
test_cases = [
|
| 303 |
+
("Chilonzor tumani", ["katta chilonzor", "beltepa", "beshqozon", "qorgon"]),
|
| 304 |
+
("Bektemir tumani", ["abay", "bektemir", "binokor"]),
|
| 305 |
+
("Shayxontohur tumani", ["kamolon", "shayx"]),
|
| 306 |
+
]
|
| 307 |
+
|
| 308 |
+
for district, inputs in test_cases:
|
| 309 |
+
test_mahalla_matching(district, inputs)
|
app/utils/translit.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/utils/translit.py
|
| 2 |
+
|
| 3 |
+
def lotin_to_kirill(text: str) -> str:
|
| 4 |
+
"""
|
| 5 |
+
Lotin alifbosidagi matnni Kirill alifbosiga o'giradigan funksiya.
|
| 6 |
+
Bu funksiya hech qanday tashqi kutubxonaga bog'liq emas.
|
| 7 |
+
"""
|
| 8 |
+
# Almashtirish qoidalari.
|
| 9 |
+
# Ikki belgili harflar bir belgili harflardan OLDIN turishi juda muhim!
|
| 10 |
+
rules = [
|
| 11 |
+
("sh", "ш"), ("ch", "ч"), ("o'", "ў"), ("g'", "ғ"),
|
| 12 |
+
("Sh", "Ш"), ("Ch", "Ч"), ("O'", "Ў"), ("G'", "Ғ"),
|
| 13 |
+
('ts', 'ц'), ('Ts', 'Ц'),
|
| 14 |
+
('ye', 'е'), ('Ye', 'Е'),
|
| 15 |
+
('yo', 'ё'), ('Yo', 'Ё'),
|
| 16 |
+
('yu', 'ю'), ('Yu', 'Ю'),
|
| 17 |
+
('ya', 'я'), ('Ya', 'Я'),
|
| 18 |
+
# Oddiy harflar
|
| 19 |
+
('a', 'а'), ('b', 'б'), ('d', 'д'), ('e', 'е'), ('f', 'ф'),
|
| 20 |
+
('g', 'г'), ('h', 'ҳ'), ('i', 'и'), ('j', 'ж'), ('k', 'к'),
|
| 21 |
+
('l', 'л'), ('m', 'м'), ('n', 'н'), ('o', 'о'), ('p', 'п'),
|
| 22 |
+
('q', 'қ'), ('r', 'р'), ('s', 'с'), ('t', 'т'), ('u', 'у'),
|
| 23 |
+
('v', 'в'), ('x', 'х'), ('y', 'й'), ('z', 'з'), ("'", "ъ"),
|
| 24 |
+
# Bosh harflar
|
| 25 |
+
('A', 'А'), ('B', 'Б'), ('D', 'Д'), ('E', 'Е'), ('F', 'Ф'),
|
| 26 |
+
('G', 'Г'), ('H', 'Ҳ'), ('I', 'И'), ('J', 'Ж'), ('K', 'К'),
|
| 27 |
+
('L', 'Л'), ('M', 'М'), ('N', 'Н'), ('O', 'О'), ('P', 'П'),
|
| 28 |
+
('Q', 'Қ'), ('R', 'Р'), ('S', 'С'), ('T', 'Т'), ('U', 'У'),
|
| 29 |
+
('V', 'В'), ('X', 'Х'), ('Y', 'Й'), ('Z', 'З'),
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
# So'z boshidagi 'E' harfini 'Э' ga almashtirish
|
| 33 |
+
words = text.split(' ')
|
| 34 |
+
processed_words = []
|
| 35 |
+
for word in words:
|
| 36 |
+
if word.startswith('E'):
|
| 37 |
+
word = 'Э' + word[1:]
|
| 38 |
+
elif word.startswith('e'):
|
| 39 |
+
word = 'э' + word[1:]
|
| 40 |
+
processed_words.append(word)
|
| 41 |
+
text = ' '.join(processed_words)
|
| 42 |
+
|
| 43 |
+
# Qolgan barcha qoidalarni qo'llash
|
| 44 |
+
for lotin, kirill in rules:
|
| 45 |
+
text = text.replace(lotin, kirill)
|
| 46 |
+
|
| 47 |
+
return text
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def clean_cyrillic_text(text: str) -> str:
|
| 51 |
+
"""
|
| 52 |
+
Matnni faqat o'zbek/rus kirill harflari, raqamlar va
|
| 53 |
+
bo'sh joy qoldirib tozalaydi.
|
| 54 |
+
|
| 55 |
+
TTS modeliga yuborishdan oldin ishlatiladi.
|
| 56 |
+
|
| 57 |
+
YANGILANGAN: Rus va o'zbek kirill harflarini qo'llab-quvvatlaydi
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
text: Tozalanadigan matn
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Tozalangan matn (faqat kirill, raqamlar va tinish belgilari)
|
| 64 |
+
"""
|
| 65 |
+
# O'zbek va rus kirill harflarini qo'shish
|
| 66 |
+
# O'zbek maxsus harflari: ў, қ, ғ, ҳ
|
| 67 |
+
# Rus harflari: standard 33 ta harf (а-я)
|
| 68 |
+
allowed_chars = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяўқғҳ "
|
| 69 |
+
allowed_chars += allowed_chars.upper() # Bosh harflarni ham qo'shamiz
|
| 70 |
+
allowed_chars += "0123456789.,!?-:" # Tinish belgilari va raqamlar
|
| 71 |
+
|
| 72 |
+
cleaned_text = "".join(filter(lambda char: char in allowed_chars, text))
|
| 73 |
+
return cleaned_text
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def russian_latin_to_cyrillic(text: str) -> str:
|
| 78 |
+
"""
|
| 79 |
+
RUSCHA lotin alifbosidagi matnni Kirill alifbosiga o'giradi.
|
| 80 |
+
|
| 81 |
+
Misol:
|
| 82 |
+
"u menya bolit golova" → "у меня болит голова"
|
| 83 |
+
"privet" → "привет"
|
| 84 |
+
"spasibo" → "спасибо"
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
text: Ruscha matn (lotin alifbosida)
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Ruscha matn (kirill alifbosida)
|
| 91 |
+
"""
|
| 92 |
+
# Ruscha lotin → kirill konvertatsiya jadvali
|
| 93 |
+
# MUHIM: Ikki harfli kombinatsiyalar birinchi bo'lishi kerak!
|
| 94 |
+
rules = [
|
| 95 |
+
# Ikki harfli kombinatsiyalar (uzun kombinatsiyalar avval)
|
| 96 |
+
('shch', 'щ'), ('Shch', 'Щ'), ('SHCH', 'Щ'),
|
| 97 |
+
('sh', 'ш'), ('Sh', 'Ш'), ('SH', 'Ш'),
|
| 98 |
+
('ch', 'ч'), ('Ch', 'Ч'), ('CH', 'Ч'),
|
| 99 |
+
('zh', 'ж'), ('Zh', 'Ж'), ('ZH', 'Ж'),
|
| 100 |
+
('yo', 'ё'), ('Yo', 'Ё'), ('YO', 'Ё'),
|
| 101 |
+
('yu', 'ю'), ('Yu', 'Ю'), ('YU', 'Ю'),
|
| 102 |
+
('ya', 'я'), ('Ya', 'Я'), ('YA', 'Я'),
|
| 103 |
+
('ts', 'ц'), ('Ts', 'Ц'), ('TS', 'Ц'),
|
| 104 |
+
('ye', 'е'), ('Ye', 'Е'), ('YE', 'Е'),
|
| 105 |
+
('kh', 'х'), ('Kh', 'Х'), ('KH', 'Х'),
|
| 106 |
+
|
| 107 |
+
# Bir harfli (kichik harflar)
|
| 108 |
+
('a', 'а'), ('b', 'б'), ('v', 'в'), ('g', 'г'), ('d', 'д'),
|
| 109 |
+
('e', 'е'), ('z', 'з'), ('i', 'и'), ('j', 'й'), ('k', 'к'),
|
| 110 |
+
('l', 'л'), ('m', 'м'), ('n', 'н'), ('o', 'о'), ('p', 'п'),
|
| 111 |
+
('r', 'р'), ('s', 'с'), ('t', 'т'), ('u', 'у'), ('f', 'ф'),
|
| 112 |
+
('h', 'х'), ('c', 'к'), ('y', 'ы'), ('w', 'в'),
|
| 113 |
+
|
| 114 |
+
# Bir harfli (bosh harflar)
|
| 115 |
+
('A', 'А'), ('B', 'Б'), ('V', 'В'), ('G', 'Г'), ('D', 'Д'),
|
| 116 |
+
('E', 'Е'), ('Z', 'З'), ('I', 'И'), ('J', 'Й'), ('K', 'К'),
|
| 117 |
+
('L', 'Л'), ('M', 'М'), ('N', 'Н'), ('O', 'О'), ('P', 'П'),
|
| 118 |
+
('R', 'Р'), ('S', 'С'), ('T', 'Т'), ('U', 'У'), ('F', 'Ф'),
|
| 119 |
+
('H', 'Х'), ('C', 'К'), ('Y', 'Ы'), ('W', 'В'),
|
| 120 |
+
|
| 121 |
+
# Maxsus belgilar
|
| 122 |
+
("'", 'ь'), # Yumshatish belgisi
|
| 123 |
+
('"', 'ъ'), # Qattiqlik belgisi
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
# Qoidalarni qo'llash (ketma-ketlik muhim!)
|
| 127 |
+
for latin, cyrillic in rules:
|
| 128 |
+
text = text.replace(latin, cyrillic)
|
| 129 |
+
|
| 130 |
+
return text
|
data/brigades.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"brigades": [
|
| 3 |
+
{
|
| 4 |
+
"brigade_id": "brigade_001",
|
| 5 |
+
"name": "Brigada #1",
|
| 6 |
+
"base_lat": 41.2995,
|
| 7 |
+
"base_lon": 69.2401,
|
| 8 |
+
"current_lat": 41.2422768218284,
|
| 9 |
+
"current_lon": 69.27058890102595,
|
| 10 |
+
"target_lat": 41.30069326433285,
|
| 11 |
+
"target_lon": 69.12664435963511,
|
| 12 |
+
"current_status": "busy",
|
| 13 |
+
"phone": "+998 71 241-01-03",
|
| 14 |
+
"speed_kmh": 60,
|
| 15 |
+
"assigned_case_id": null
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"brigade_id": "brigade_002",
|
| 19 |
+
"name": "Brigada #2",
|
| 20 |
+
"base_lat": 41.2767,
|
| 21 |
+
"base_lon": 69.2095,
|
| 22 |
+
"current_lat": 41.268377157689734,
|
| 23 |
+
"current_lon": 69.23591196343817,
|
| 24 |
+
"target_lat": 41.25588979849189,
|
| 25 |
+
"target_lon": 69.27260370928612,
|
| 26 |
+
"current_status": "busy",
|
| 27 |
+
"phone": "+998 71 241-01-04",
|
| 28 |
+
"speed_kmh": 55,
|
| 29 |
+
"assigned_case_id": null
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"brigade_id": "brigade_003",
|
| 33 |
+
"name": "Brigada #3",
|
| 34 |
+
"base_lat": 41.3353,
|
| 35 |
+
"base_lon": 69.2419,
|
| 36 |
+
"current_lat": 41.25693456837629,
|
| 37 |
+
"current_lon": 69.2427248159588,
|
| 38 |
+
"target_lat": 41.234993915832256,
|
| 39 |
+
"target_lon": 69.23654237734561,
|
| 40 |
+
"current_status": "busy",
|
| 41 |
+
"phone": "+998 71 241-01-05",
|
| 42 |
+
"speed_kmh": 65,
|
| 43 |
+
"assigned_case_id": null
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"brigade_id": "brigade_004",
|
| 47 |
+
"name": "Brigada #4",
|
| 48 |
+
"base_lat": 41.2108,
|
| 49 |
+
"base_lon": 69.3352,
|
| 50 |
+
"current_lat": 41.2108,
|
| 51 |
+
"current_lon": 69.3352,
|
| 52 |
+
"target_lat": 41.225,
|
| 53 |
+
"target_lon": 69.34,
|
| 54 |
+
"current_status": "available",
|
| 55 |
+
"phone": "+998 71 241-01-06",
|
| 56 |
+
"speed_kmh": 58,
|
| 57 |
+
"assigned_case_id": null
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"brigade_id": "brigade_005",
|
| 61 |
+
"name": "Brigada #5",
|
| 62 |
+
"base_lat": 41.3114,
|
| 63 |
+
"base_lon": 69.2488,
|
| 64 |
+
"current_lat": 41.25978359831006,
|
| 65 |
+
"current_lon": 69.32342319179688,
|
| 66 |
+
"target_lat": 41.34332160043753,
|
| 67 |
+
"target_lon": 69.32293383992433,
|
| 68 |
+
"current_status": "busy",
|
| 69 |
+
"phone": "+998 71 241-01-07",
|
| 70 |
+
"speed_kmh": 62,
|
| 71 |
+
"assigned_case_id": null
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"brigade_id": "brigade_006",
|
| 75 |
+
"name": "Brigada #6",
|
| 76 |
+
"base_lat": 41.2856,
|
| 77 |
+
"base_lon": 69.2034,
|
| 78 |
+
"current_lat": 41.2856,
|
| 79 |
+
"current_lon": 69.2034,
|
| 80 |
+
"target_lat": null,
|
| 81 |
+
"target_lon": null,
|
| 82 |
+
"current_status": "available",
|
| 83 |
+
"phone": "+998 71 241-01-08",
|
| 84 |
+
"speed_kmh": 60,
|
| 85 |
+
"assigned_case_id": null
|
| 86 |
+
}
|
| 87 |
+
]
|
| 88 |
+
}
|
data/cases.json
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cases": [
|
| 3 |
+
{
|
| 4 |
+
"id": "case_029",
|
| 5 |
+
"type": "emergency",
|
| 6 |
+
"patient_identifier": "127.0.0.1:61973",
|
| 7 |
+
"status": "brigada_junatildi",
|
| 8 |
+
"risk_level": "qizil",
|
| 9 |
+
"address_text": null,
|
| 10 |
+
"symptoms_text": "O'lim holati, qon ketishi",
|
| 11 |
+
"district": "Shayxontohur tumani",
|
| 12 |
+
"patient_full_name": "ubaydullayev g'iyosiddin botir o'g'li",
|
| 13 |
+
"patient_phone": null,
|
| 14 |
+
"previous_cases_count": 1,
|
| 15 |
+
"waiting_for_name_input": false,
|
| 16 |
+
"gps_lat": 41.3063,
|
| 17 |
+
"gps_lon": 69.2438,
|
| 18 |
+
"geocoded_lat": 41.3063,
|
| 19 |
+
"geocoded_lon": 69.2438,
|
| 20 |
+
"gps_verified": true,
|
| 21 |
+
"assigned_brigade_id": "brigade_04",
|
| 22 |
+
"assigned_brigade_name": "4-brigada",
|
| 23 |
+
"language": "uzb",
|
| 24 |
+
"clinic_type": null,
|
| 25 |
+
"recommended_clinic_id": null,
|
| 26 |
+
"recommended_clinic_name": null,
|
| 27 |
+
"recommended_specialty": null,
|
| 28 |
+
"estimated_price": null,
|
| 29 |
+
"uncertainty_attempts": 1,
|
| 30 |
+
"operator_needed": false,
|
| 31 |
+
"uncertainty_reason": "Simptomlar aniq emas.",
|
| 32 |
+
"created_at": "2025-10-18T00:12:39.457572",
|
| 33 |
+
"updated_at": "2025-10-18T00:14:09.692867",
|
| 34 |
+
"waiting_for_address": true,
|
| 35 |
+
"selected_district": "shayxontohur",
|
| 36 |
+
"waiting_for_mahalla_input": false,
|
| 37 |
+
"mahalla_retry_count": 0,
|
| 38 |
+
"selected_mahalla": "Kamolon mahallasi",
|
| 39 |
+
"mahalla_lat": 41.3063,
|
| 40 |
+
"mahalla_lon": 69.2438,
|
| 41 |
+
"distance_to_brigade_km": 0.51
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"id": "case_002",
|
| 45 |
+
"type": "uncertain",
|
| 46 |
+
"patient_identifier": "127.0.0.1:52950",
|
| 47 |
+
"status": "yangi",
|
| 48 |
+
"risk_level": "sariq",
|
| 49 |
+
"address_text": null,
|
| 50 |
+
"symptoms_text": "Tushunarsiz, 'polite karlo'",
|
| 51 |
+
"district": null,
|
| 52 |
+
"patient_full_name": null,
|
| 53 |
+
"patient_phone": null,
|
| 54 |
+
"previous_cases_count": 0,
|
| 55 |
+
"waiting_for_name_input": false,
|
| 56 |
+
"gps_lat": null,
|
| 57 |
+
"gps_lon": null,
|
| 58 |
+
"geocoded_lat": null,
|
| 59 |
+
"geocoded_lon": null,
|
| 60 |
+
"gps_verified": false,
|
| 61 |
+
"assigned_brigade_id": null,
|
| 62 |
+
"assigned_brigade_name": null,
|
| 63 |
+
"language": "uzb",
|
| 64 |
+
"clinic_type": null,
|
| 65 |
+
"recommended_clinic_id": null,
|
| 66 |
+
"recommended_clinic_name": null,
|
| 67 |
+
"recommended_specialty": null,
|
| 68 |
+
"estimated_price": null,
|
| 69 |
+
"uncertainty_attempts": 3,
|
| 70 |
+
"operator_needed": false,
|
| 71 |
+
"uncertainty_reason": "Tushunarsiz simptomlar",
|
| 72 |
+
"created_at": "2025-10-18T07:12:14.047641",
|
| 73 |
+
"updated_at": "2025-10-18T07:22:08.801505"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"id": "case_003",
|
| 77 |
+
"type": "uncertain",
|
| 78 |
+
"patient_identifier": "127.0.0.1:56075",
|
| 79 |
+
"status": "yangi",
|
| 80 |
+
"risk_level": "sariq",
|
| 81 |
+
"address_text": null,
|
| 82 |
+
"symptoms_text": "Aniqlanmadi",
|
| 83 |
+
"district": null,
|
| 84 |
+
"patient_full_name": null,
|
| 85 |
+
"patient_phone": null,
|
| 86 |
+
"previous_cases_count": 0,
|
| 87 |
+
"waiting_for_name_input": false,
|
| 88 |
+
"gps_lat": null,
|
| 89 |
+
"gps_lon": null,
|
| 90 |
+
"geocoded_lat": null,
|
| 91 |
+
"geocoded_lon": null,
|
| 92 |
+
"gps_verified": false,
|
| 93 |
+
"assigned_brigade_id": null,
|
| 94 |
+
"assigned_brigade_name": null,
|
| 95 |
+
"language": "uzb",
|
| 96 |
+
"clinic_type": null,
|
| 97 |
+
"recommended_clinic_id": null,
|
| 98 |
+
"recommended_clinic_name": null,
|
| 99 |
+
"recommended_specialty": null,
|
| 100 |
+
"estimated_price": null,
|
| 101 |
+
"uncertainty_attempts": 1,
|
| 102 |
+
"operator_needed": false,
|
| 103 |
+
"uncertainty_reason": "No symptoms provided yet",
|
| 104 |
+
"created_at": "2025-10-18T10:46:24.432389",
|
| 105 |
+
"updated_at": "2025-10-18T10:46:28.991123"
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"id": "case_004",
|
| 109 |
+
"type": "uncertain",
|
| 110 |
+
"patient_identifier": "127.0.0.1:62313",
|
| 111 |
+
"status": "operator_kutilmoqda",
|
| 112 |
+
"risk_level": "sariq",
|
| 113 |
+
"address_text": null,
|
| 114 |
+
"symptoms_text": "Bemor yordam kelishini aytyapti, lekin nima yordam kerakligi aniq emas",
|
| 115 |
+
"district": null,
|
| 116 |
+
"patient_full_name": null,
|
| 117 |
+
"patient_phone": null,
|
| 118 |
+
"previous_cases_count": 0,
|
| 119 |
+
"waiting_for_name_input": false,
|
| 120 |
+
"gps_lat": null,
|
| 121 |
+
"gps_lon": null,
|
| 122 |
+
"geocoded_lat": null,
|
| 123 |
+
"geocoded_lon": null,
|
| 124 |
+
"gps_verified": false,
|
| 125 |
+
"assigned_brigade_id": null,
|
| 126 |
+
"assigned_brigade_name": null,
|
| 127 |
+
"language": "uzb",
|
| 128 |
+
"clinic_type": null,
|
| 129 |
+
"recommended_clinic_id": null,
|
| 130 |
+
"recommended_clinic_name": null,
|
| 131 |
+
"recommended_specialty": null,
|
| 132 |
+
"estimated_price": null,
|
| 133 |
+
"uncertainty_attempts": 6,
|
| 134 |
+
"operator_needed": true,
|
| 135 |
+
"uncertainty_reason": "Sabab aniq emas",
|
| 136 |
+
"created_at": "2025-10-18T11:15:34.526916",
|
| 137 |
+
"updated_at": "2025-10-18T11:18:55.556414"
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"id": "case_005",
|
| 141 |
+
"type": "uncertain",
|
| 142 |
+
"patient_identifier": "127.0.0.1:58913",
|
| 143 |
+
"status": "yangi",
|
| 144 |
+
"risk_level": "sariq",
|
| 145 |
+
"address_text": null,
|
| 146 |
+
"symptoms_text": "Aloqa boshlandi, ammo simptomlar haqida ma'lumot yo'q",
|
| 147 |
+
"district": null,
|
| 148 |
+
"patient_full_name": null,
|
| 149 |
+
"patient_phone": null,
|
| 150 |
+
"previous_cases_count": 0,
|
| 151 |
+
"waiting_for_name_input": false,
|
| 152 |
+
"gps_lat": null,
|
| 153 |
+
"gps_lon": null,
|
| 154 |
+
"geocoded_lat": null,
|
| 155 |
+
"geocoded_lon": null,
|
| 156 |
+
"gps_verified": false,
|
| 157 |
+
"assigned_brigade_id": null,
|
| 158 |
+
"assigned_brigade_name": null,
|
| 159 |
+
"language": "uzb",
|
| 160 |
+
"clinic_type": null,
|
| 161 |
+
"recommended_clinic_id": null,
|
| 162 |
+
"recommended_clinic_name": null,
|
| 163 |
+
"recommended_specialty": null,
|
| 164 |
+
"estimated_price": null,
|
| 165 |
+
"uncertainty_attempts": 1,
|
| 166 |
+
"operator_needed": false,
|
| 167 |
+
"uncertainty_reason": "Simptomlar haqida ma'lumot yo'q",
|
| 168 |
+
"created_at": "2025-10-18T11:37:51.836985",
|
| 169 |
+
"updated_at": "2025-10-18T11:37:57.349273"
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"id": "case_006",
|
| 173 |
+
"type": "uncertain",
|
| 174 |
+
"patient_identifier": "127.0.0.1:49994",
|
| 175 |
+
"status": "yangi",
|
| 176 |
+
"risk_level": "sariq",
|
| 177 |
+
"address_text": null,
|
| 178 |
+
"symptoms_text": "salomlashuv",
|
| 179 |
+
"district": null,
|
| 180 |
+
"patient_full_name": null,
|
| 181 |
+
"patient_phone": null,
|
| 182 |
+
"previous_cases_count": 0,
|
| 183 |
+
"waiting_for_name_input": false,
|
| 184 |
+
"gps_lat": null,
|
| 185 |
+
"gps_lon": null,
|
| 186 |
+
"geocoded_lat": null,
|
| 187 |
+
"geocoded_lon": null,
|
| 188 |
+
"gps_verified": false,
|
| 189 |
+
"assigned_brigade_id": null,
|
| 190 |
+
"assigned_brigade_name": null,
|
| 191 |
+
"language": "uzb",
|
| 192 |
+
"clinic_type": null,
|
| 193 |
+
"recommended_clinic_id": null,
|
| 194 |
+
"recommended_clinic_name": null,
|
| 195 |
+
"recommended_specialty": null,
|
| 196 |
+
"estimated_price": null,
|
| 197 |
+
"uncertainty_attempts": 1,
|
| 198 |
+
"operator_needed": false,
|
| 199 |
+
"uncertainty_reason": "Bemor hali shikoyat bildirmadi.",
|
| 200 |
+
"created_at": "2025-10-18T11:45:42.849777",
|
| 201 |
+
"updated_at": "2025-10-18T11:45:46.056562"
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"id": "case_007",
|
| 205 |
+
"type": "uncertain",
|
| 206 |
+
"patient_identifier": "127.0.0.1:57761",
|
| 207 |
+
"status": "yangi",
|
| 208 |
+
"risk_level": "sariq",
|
| 209 |
+
"address_text": null,
|
| 210 |
+
"symptoms_text": "No symptoms reported, confusion about the assistant's purpose.",
|
| 211 |
+
"district": null,
|
| 212 |
+
"patient_full_name": null,
|
| 213 |
+
"patient_phone": null,
|
| 214 |
+
"previous_cases_count": 0,
|
| 215 |
+
"waiting_for_name_input": false,
|
| 216 |
+
"gps_lat": null,
|
| 217 |
+
"gps_lon": null,
|
| 218 |
+
"geocoded_lat": null,
|
| 219 |
+
"geocoded_lon": null,
|
| 220 |
+
"gps_verified": false,
|
| 221 |
+
"assigned_brigade_id": null,
|
| 222 |
+
"assigned_brigade_name": null,
|
| 223 |
+
"language": "eng",
|
| 224 |
+
"clinic_type": null,
|
| 225 |
+
"recommended_clinic_id": null,
|
| 226 |
+
"recommended_clinic_name": null,
|
| 227 |
+
"recommended_specialty": null,
|
| 228 |
+
"estimated_price": null,
|
| 229 |
+
"uncertainty_attempts": 3,
|
| 230 |
+
"operator_needed": false,
|
| 231 |
+
"uncertainty_reason": "Patient seems confused about the assistant's purpose.",
|
| 232 |
+
"created_at": "2025-10-18T11:59:57.890746",
|
| 233 |
+
"updated_at": "2025-10-18T12:00:29.697837"
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"id": "case_008",
|
| 237 |
+
"type": "uncertain",
|
| 238 |
+
"patient_identifier": "127.0.0.1:61450",
|
| 239 |
+
"status": "yangi",
|
| 240 |
+
"risk_level": "sariq",
|
| 241 |
+
"address_text": null,
|
| 242 |
+
"symptoms_text": "No symptoms mentioned, just a greeting.",
|
| 243 |
+
"district": null,
|
| 244 |
+
"patient_full_name": null,
|
| 245 |
+
"patient_phone": null,
|
| 246 |
+
"previous_cases_count": 0,
|
| 247 |
+
"waiting_for_name_input": false,
|
| 248 |
+
"gps_lat": null,
|
| 249 |
+
"gps_lon": null,
|
| 250 |
+
"geocoded_lat": null,
|
| 251 |
+
"geocoded_lon": null,
|
| 252 |
+
"gps_verified": false,
|
| 253 |
+
"assigned_brigade_id": null,
|
| 254 |
+
"assigned_brigade_name": null,
|
| 255 |
+
"language": "eng",
|
| 256 |
+
"clinic_type": null,
|
| 257 |
+
"recommended_clinic_id": null,
|
| 258 |
+
"recommended_clinic_name": null,
|
| 259 |
+
"recommended_specialty": null,
|
| 260 |
+
"estimated_price": null,
|
| 261 |
+
"uncertainty_attempts": 2,
|
| 262 |
+
"operator_needed": false,
|
| 263 |
+
"uncertainty_reason": "No medical information provided.",
|
| 264 |
+
"created_at": "2025-10-18T12:16:42.044847",
|
| 265 |
+
"updated_at": "2025-10-18T12:17:04.706210"
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"id": "case_009",
|
| 269 |
+
"type": "emergency",
|
| 270 |
+
"patient_identifier": "127.0.0.1:53380",
|
| 271 |
+
"status": "yangi",
|
| 272 |
+
"risk_level": "qizil",
|
| 273 |
+
"address_text": null,
|
| 274 |
+
"symptoms_text": "Yurak og'rig'i",
|
| 275 |
+
"district": "Olmazor tumani",
|
| 276 |
+
"patient_full_name": null,
|
| 277 |
+
"patient_phone": null,
|
| 278 |
+
"previous_cases_count": 0,
|
| 279 |
+
"waiting_for_name_input": false,
|
| 280 |
+
"gps_lat": 41.3378,
|
| 281 |
+
"gps_lon": 69.2452,
|
| 282 |
+
"geocoded_lat": 41.3378,
|
| 283 |
+
"geocoded_lon": 69.2452,
|
| 284 |
+
"gps_verified": true,
|
| 285 |
+
"assigned_brigade_id": null,
|
| 286 |
+
"assigned_brigade_name": null,
|
| 287 |
+
"language": "uzb",
|
| 288 |
+
"clinic_type": null,
|
| 289 |
+
"recommended_clinic_id": null,
|
| 290 |
+
"recommended_clinic_name": null,
|
| 291 |
+
"recommended_specialty": null,
|
| 292 |
+
"estimated_price": null,
|
| 293 |
+
"uncertainty_attempts": 1,
|
| 294 |
+
"operator_needed": false,
|
| 295 |
+
"uncertainty_reason": "Bemor shikoyati yo'q",
|
| 296 |
+
"created_at": "2025-10-18T12:22:34.428201",
|
| 297 |
+
"updated_at": "2025-10-18T12:40:30.423232",
|
| 298 |
+
"waiting_for_address": true,
|
| 299 |
+
"selected_district": "olmazor",
|
| 300 |
+
"waiting_for_mahalla_input": false,
|
| 301 |
+
"mahalla_retry_count": 0,
|
| 302 |
+
"selected_mahalla": "Allon mahallasi",
|
| 303 |
+
"mahalla_lat": 41.3378,
|
| 304 |
+
"mahalla_lon": 69.2452
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"id": "case_010",
|
| 308 |
+
"type": "uncertain",
|
| 309 |
+
"patient_identifier": "127.0.0.1:50069",
|
| 310 |
+
"status": "yangi",
|
| 311 |
+
"risk_level": "sariq",
|
| 312 |
+
"address_text": null,
|
| 313 |
+
"symptoms_text": "Boshlanishi",
|
| 314 |
+
"district": null,
|
| 315 |
+
"patient_full_name": null,
|
| 316 |
+
"patient_phone": null,
|
| 317 |
+
"previous_cases_count": 0,
|
| 318 |
+
"waiting_for_name_input": false,
|
| 319 |
+
"gps_lat": null,
|
| 320 |
+
"gps_lon": null,
|
| 321 |
+
"geocoded_lat": null,
|
| 322 |
+
"geocoded_lon": null,
|
| 323 |
+
"gps_verified": false,
|
| 324 |
+
"assigned_brigade_id": null,
|
| 325 |
+
"assigned_brigade_name": null,
|
| 326 |
+
"language": "uzb",
|
| 327 |
+
"clinic_type": null,
|
| 328 |
+
"recommended_clinic_id": null,
|
| 329 |
+
"recommended_clinic_name": null,
|
| 330 |
+
"recommended_specialty": null,
|
| 331 |
+
"estimated_price": null,
|
| 332 |
+
"uncertainty_attempts": 1,
|
| 333 |
+
"operator_needed": false,
|
| 334 |
+
"uncertainty_reason": "Suhbat boshlanishi, simptomlar yo'q",
|
| 335 |
+
"created_at": "2025-10-19T01:32:51.302759",
|
| 336 |
+
"updated_at": "2025-10-19T01:33:00.223947"
|
| 337 |
+
}
|
| 338 |
+
]
|
| 339 |
+
}
|
data/clinics.json
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"clinics": [
|
| 3 |
+
{
|
| 4 |
+
"id": "clinic_001",
|
| 5 |
+
"name": "Toshkent Shahar Poliklinikasi №1",
|
| 6 |
+
"type": "davlat",
|
| 7 |
+
"district": "Chilonzor tumani",
|
| 8 |
+
"address": "Qatortol ko'chasi, 15",
|
| 9 |
+
"phone": "+998 71 276-45-67",
|
| 10 |
+
"rating": 4.2,
|
| 11 |
+
"logo_url": "https://via.placeholder.com/150x150/4A90E2/ffffff?text=TSP1",
|
| 12 |
+
"banner_url": "https://via.placeholder.com/800x300/4A90E2/ffffff?text=Toshkent+Poliklinika+1",
|
| 13 |
+
"photos": [
|
| 14 |
+
{
|
| 15 |
+
"url": "https://via.placeholder.com/600x400/E8F4F8/333333?text=Resepsiya",
|
| 16 |
+
"caption": "Resepsiya xonasi"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"url": "https://via.placeholder.com/600x400/E8F4F8/333333?text=Xona",
|
| 20 |
+
"caption": "Terapevt xonasi"
|
| 21 |
+
}
|
| 22 |
+
],
|
| 23 |
+
"gps": {
|
| 24 |
+
"lat": 41.2767,
|
| 25 |
+
"lon": 69.2095
|
| 26 |
+
},
|
| 27 |
+
"working_hours": "08:00 - 18:00",
|
| 28 |
+
"working_days": [
|
| 29 |
+
"Dushanba",
|
| 30 |
+
"Seshanba",
|
| 31 |
+
"Chorshanba",
|
| 32 |
+
"Payshanba",
|
| 33 |
+
"Juma",
|
| 34 |
+
"Shanba"
|
| 35 |
+
],
|
| 36 |
+
"specializations": [
|
| 37 |
+
"Terapiya",
|
| 38 |
+
"Pediatriya",
|
| 39 |
+
"Stomatologiya"
|
| 40 |
+
],
|
| 41 |
+
"doctors_count": 25,
|
| 42 |
+
"has_emergency": true,
|
| 43 |
+
"insurance_accepted": true,
|
| 44 |
+
"services": [
|
| 45 |
+
{
|
| 46 |
+
"name": "Terapevt konsultatsiyasi",
|
| 47 |
+
"price": "Bepul (INPS bilan)",
|
| 48 |
+
"duration": "20 minut"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"name": "EKG tekshiruvi",
|
| 52 |
+
"price": "Bepul",
|
| 53 |
+
"duration": "10 minut"
|
| 54 |
+
}
|
| 55 |
+
],
|
| 56 |
+
"languages": [
|
| 57 |
+
"O'zbekcha",
|
| 58 |
+
"Ruscha"
|
| 59 |
+
],
|
| 60 |
+
"description": "Chilonzor tumanidagi eng yirik davlat poliklinikasi. INPS sigortasi bilan bepul xizmat."
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "clinic_002",
|
| 64 |
+
"name": "MedExpress Xususiy Klinikasi",
|
| 65 |
+
"type": "xususiy",
|
| 66 |
+
"district": "Yunusobod tumani",
|
| 67 |
+
"address": "Amir Temur ko'chasi, 89A",
|
| 68 |
+
"phone": "+998 71 202-55-88",
|
| 69 |
+
"rating": 4.8,
|
| 70 |
+
"logo_url": "https://via.placeholder.com/150x150/27AE60/ffffff?text=MedExpress",
|
| 71 |
+
"banner_url": "https://via.placeholder.com/800x300/27AE60/ffffff?text=MedExpress+Clinic",
|
| 72 |
+
"photos": [
|
| 73 |
+
{
|
| 74 |
+
"url": "https://via.placeholder.com/600x400/D5F4E6/333333?text=VIP+Xona",
|
| 75 |
+
"caption": "VIP xona"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"url": "https://via.placeholder.com/600x400/D5F4E6/333333?text=Laboratoriya",
|
| 79 |
+
"caption": "Zamonaviy laboratoriya"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"url": "https://via.placeholder.com/600x400/D5F4E6/333333?text=Jihozlar",
|
| 83 |
+
"caption": "Tibbiy jihozlar"
|
| 84 |
+
}
|
| 85 |
+
],
|
| 86 |
+
"gps": {
|
| 87 |
+
"lat": 41.3272,
|
| 88 |
+
"lon": 69.2894
|
| 89 |
+
},
|
| 90 |
+
"working_hours": "08:00 - 22:00",
|
| 91 |
+
"working_days": [
|
| 92 |
+
"Dushanba",
|
| 93 |
+
"Seshanba",
|
| 94 |
+
"Chorshanba",
|
| 95 |
+
"Payshanba",
|
| 96 |
+
"Juma",
|
| 97 |
+
"Shanba",
|
| 98 |
+
"Yakshanba"
|
| 99 |
+
],
|
| 100 |
+
"specializations": [
|
| 101 |
+
"Kardiologiya",
|
| 102 |
+
"Nevrologiya",
|
| 103 |
+
"Terapiya",
|
| 104 |
+
"Oftalmologiya"
|
| 105 |
+
],
|
| 106 |
+
"doctors_count": 18,
|
| 107 |
+
"has_emergency": false,
|
| 108 |
+
"insurance_accepted": false,
|
| 109 |
+
"services": [
|
| 110 |
+
{
|
| 111 |
+
"name": "Kardiolog konsultatsiyasi",
|
| 112 |
+
"price": "250,000 so'm",
|
| 113 |
+
"duration": "30 minut"
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"name": "Echo-KG",
|
| 117 |
+
"price": "350,000 so'm",
|
| 118 |
+
"duration": "20 minut"
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"name": "Holter monitoring (24 soat)",
|
| 122 |
+
"price": "450,000 so'm"
|
| 123 |
+
}
|
| 124 |
+
],
|
| 125 |
+
"languages": [
|
| 126 |
+
"O'zbekcha",
|
| 127 |
+
"Ruscha",
|
| 128 |
+
"Inglizcha"
|
| 129 |
+
],
|
| 130 |
+
"description": "Zamonaviy xususiy klinika. Eng so'nggi tibbiy jihozlar va tajribali shifokorlar."
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"id": "clinic_003",
|
| 134 |
+
"name": "Bektemir Tuman Poliklinikasi",
|
| 135 |
+
"type": "davlat",
|
| 136 |
+
"district": "Bektemir tumani",
|
| 137 |
+
"address": "Bektemir ko'chasi, 42",
|
| 138 |
+
"phone": "+998 71 254-33-21",
|
| 139 |
+
"rating": 3.9,
|
| 140 |
+
"logo_url": "https://via.placeholder.com/150x150/E67E22/ffffff?text=BTP",
|
| 141 |
+
"banner_url": "https://via.placeholder.com/800x300/E67E22/ffffff?text=Bektemir+Poliklinika",
|
| 142 |
+
"photos": [
|
| 143 |
+
{
|
| 144 |
+
"url": "https://via.placeholder.com/600x400/FAD7A0/333333?text=Kirish",
|
| 145 |
+
"caption": "Poliklinika kirishi"
|
| 146 |
+
}
|
| 147 |
+
],
|
| 148 |
+
"gps": {
|
| 149 |
+
"lat": 41.2108,
|
| 150 |
+
"lon": 69.3352
|
| 151 |
+
},
|
| 152 |
+
"working_hours": "08:00 - 18:00",
|
| 153 |
+
"working_days": [
|
| 154 |
+
"Dushanba",
|
| 155 |
+
"Seshanba",
|
| 156 |
+
"Chorshanba",
|
| 157 |
+
"Payshanba",
|
| 158 |
+
"Juma",
|
| 159 |
+
"Shanba"
|
| 160 |
+
],
|
| 161 |
+
"specializations": [
|
| 162 |
+
"Terapiya",
|
| 163 |
+
"Pediatriya",
|
| 164 |
+
"Ginekologiya"
|
| 165 |
+
],
|
| 166 |
+
"doctors_count": 20,
|
| 167 |
+
"has_emergency": true,
|
| 168 |
+
"insurance_accepted": true,
|
| 169 |
+
"services": [
|
| 170 |
+
{
|
| 171 |
+
"name": "Terapevt konsultatsiyasi",
|
| 172 |
+
"price": "Bepul (INPS bilan)"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"name": "Qon tahlili",
|
| 176 |
+
"price": "Bepul"
|
| 177 |
+
}
|
| 178 |
+
],
|
| 179 |
+
"languages": [
|
| 180 |
+
"O'zbekcha",
|
| 181 |
+
"Ruscha"
|
| 182 |
+
],
|
| 183 |
+
"description": "Bektemir tumanidagi davlat poliklinikasi. Aholini bepul tibbiy xizmat bilan ta'minlaydi."
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"id": "clinic_004",
|
| 187 |
+
"name": "Family Health Center",
|
| 188 |
+
"type": "xususiy",
|
| 189 |
+
"district": "Mirzo Ulug'bek tumani",
|
| 190 |
+
"address": "Labzak ko'chasi, 25B",
|
| 191 |
+
"phone": "+998 71 291-44-55",
|
| 192 |
+
"rating": 4.6,
|
| 193 |
+
"logo_url": "https://via.placeholder.com/150x150/8E44AD/ffffff?text=FHC",
|
| 194 |
+
"banner_url": "https://via.placeholder.com/800x300/8E44AD/ffffff?text=Family+Health",
|
| 195 |
+
"photos": [
|
| 196 |
+
{
|
| 197 |
+
"url": "https://via.placeholder.com/600x400/D7BDE2/333333?text=Bolalar+Xonasi",
|
| 198 |
+
"caption": "Bolalar xonasi"
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"url": "https://via.placeholder.com/600x400/D7BDE2/333333?text=Kutish+Xonasi",
|
| 202 |
+
"caption": "Kutish xonasi"
|
| 203 |
+
}
|
| 204 |
+
],
|
| 205 |
+
"gps": {
|
| 206 |
+
"lat": 41.3353,
|
| 207 |
+
"lon": 69.2419
|
| 208 |
+
},
|
| 209 |
+
"working_hours": "09:00 - 20:00",
|
| 210 |
+
"working_days": [
|
| 211 |
+
"Dushanba",
|
| 212 |
+
"Seshanba",
|
| 213 |
+
"Chorshanba",
|
| 214 |
+
"Payshanba",
|
| 215 |
+
"Juma",
|
| 216 |
+
"Shanba"
|
| 217 |
+
],
|
| 218 |
+
"specializations": [
|
| 219 |
+
"Oilaviy shifokor",
|
| 220 |
+
"Pediatriya",
|
| 221 |
+
"Terapiya"
|
| 222 |
+
],
|
| 223 |
+
"doctors_count": 8,
|
| 224 |
+
"has_emergency": false,
|
| 225 |
+
"insurance_accepted": true,
|
| 226 |
+
"services": [
|
| 227 |
+
{
|
| 228 |
+
"name": "Oilaviy shifokor konsultatsiyasi",
|
| 229 |
+
"price": "180,000 so'm",
|
| 230 |
+
"duration": "30 minut"
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"name": "Bolalar pediatri",
|
| 234 |
+
"price": "150,000 so'm",
|
| 235 |
+
"duration": "25 minut"
|
| 236 |
+
}
|
| 237 |
+
],
|
| 238 |
+
"languages": [
|
| 239 |
+
"O'zbekcha",
|
| 240 |
+
"Ruscha",
|
| 241 |
+
"Inglizcha"
|
| 242 |
+
],
|
| 243 |
+
"description": "Butun oila uchun shifokor. Bolalar va kattalar uchun shifokor xizmatlari."
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"id": "clinic_005",
|
| 247 |
+
"name": "Shayxontohur Tuman Poliklinikasi №2",
|
| 248 |
+
"type": "davlat",
|
| 249 |
+
"district": "Shayxontohur tumani",
|
| 250 |
+
"address": "Taras Shevchenko ko'chasi, 78",
|
| 251 |
+
"phone": "+998 71 244-66-77",
|
| 252 |
+
"rating": 4.0,
|
| 253 |
+
"logo_url": "https://via.placeholder.com/150x150/2980B9/ffffff?text=STP2",
|
| 254 |
+
"banner_url": "https://via.placeholder.com/800x300/2980B9/ffffff?text=Shayxontohur+Poliklinika",
|
| 255 |
+
"photos": [],
|
| 256 |
+
"gps": {
|
| 257 |
+
"lat": 41.3114,
|
| 258 |
+
"lon": 69.2488
|
| 259 |
+
},
|
| 260 |
+
"working_hours": "08:00 - 18:00",
|
| 261 |
+
"working_days": [
|
| 262 |
+
"Dushanba",
|
| 263 |
+
"Seshanba",
|
| 264 |
+
"Chorshanba",
|
| 265 |
+
"Payshanba",
|
| 266 |
+
"Juma",
|
| 267 |
+
"Shanba"
|
| 268 |
+
],
|
| 269 |
+
"specializations": [
|
| 270 |
+
"Terapiya",
|
| 271 |
+
"Kardiologiya",
|
| 272 |
+
"Nevrologiya"
|
| 273 |
+
],
|
| 274 |
+
"doctors_count": 22,
|
| 275 |
+
"has_emergency": true,
|
| 276 |
+
"insurance_accepted": true,
|
| 277 |
+
"services": [
|
| 278 |
+
{
|
| 279 |
+
"name": "Kardiolog konsultatsiyasi",
|
| 280 |
+
"price": "Bepul (INPS bilan)"
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"name": "Nevropatolog konsultatsiyasi",
|
| 284 |
+
"price": "Bepul"
|
| 285 |
+
}
|
| 286 |
+
],
|
| 287 |
+
"languages": [
|
| 288 |
+
"O'zbekcha",
|
| 289 |
+
"Ruscha"
|
| 290 |
+
],
|
| 291 |
+
"description": "Shayxontohur tumanidagi yirik poliklinika. Kardiologiya bo'limi bilan mashhur."
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"id": "clinic_006",
|
| 295 |
+
"name": "Premium Medical Group",
|
| 296 |
+
"type": "xususiy",
|
| 297 |
+
"district": "Chilonzor tumani",
|
| 298 |
+
"address": "Bunyodkor ko'chasi, 12C",
|
| 299 |
+
"phone": "+998 71 207-88-99",
|
| 300 |
+
"rating": 4.9,
|
| 301 |
+
"logo_url": "https://via.placeholder.com/150x150/C0392B/ffffff?text=PMG",
|
| 302 |
+
"banner_url": "https://via.placeholder.com/800x300/C0392B/ffffff?text=Premium+Medical",
|
| 303 |
+
"photos": [
|
| 304 |
+
{
|
| 305 |
+
"url": "https://via.placeholder.com/600x400/F5B7B1/333333?text=Resepsiya",
|
| 306 |
+
"caption": "Premium resepsiya"
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"url": "https://via.placeholder.com/600x400/F5B7B1/333333?text=MRI",
|
| 310 |
+
"caption": "MRI va KT jihozlari"
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"url": "https://via.placeholder.com/600x400/F5B7B1/333333?text=Operatsiya",
|
| 314 |
+
"caption": "Operatsiya xonasi"
|
| 315 |
+
}
|
| 316 |
+
],
|
| 317 |
+
"gps": {
|
| 318 |
+
"lat": 41.2856,
|
| 319 |
+
"lon": 69.2034
|
| 320 |
+
},
|
| 321 |
+
"working_hours": "00:00 - 24:00",
|
| 322 |
+
"working_days": [
|
| 323 |
+
"Dushanba",
|
| 324 |
+
"Seshanba",
|
| 325 |
+
"Chorshanba",
|
| 326 |
+
"Payshanba",
|
| 327 |
+
"Juma",
|
| 328 |
+
"Shanba",
|
| 329 |
+
"Yakshanba"
|
| 330 |
+
],
|
| 331 |
+
"specializations": [
|
| 332 |
+
"Kardiologiya",
|
| 333 |
+
"Nevrologiya",
|
| 334 |
+
"Oftalmologiya",
|
| 335 |
+
"Ortopediya",
|
| 336 |
+
"Jarrohlik"
|
| 337 |
+
],
|
| 338 |
+
"doctors_count": 35,
|
| 339 |
+
"has_emergency": true,
|
| 340 |
+
"insurance_accepted": true,
|
| 341 |
+
"services": [
|
| 342 |
+
{
|
| 343 |
+
"name": "VIP kardiolog konsultatsiyasi",
|
| 344 |
+
"price": "500,000 so'm",
|
| 345 |
+
"duration": "45 minut"
|
| 346 |
+
},
|
| 347 |
+
{
|
| 348 |
+
"name": "MRI tekshiruvi",
|
| 349 |
+
"price": "1,200,000 so'm"
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
"name": "24/7 tez yordam",
|
| 353 |
+
"price": "300,000 so'm"
|
| 354 |
+
}
|
| 355 |
+
],
|
| 356 |
+
"languages": [
|
| 357 |
+
"O'zbekcha",
|
| 358 |
+
"Ruscha",
|
| 359 |
+
"Inglizcha",
|
| 360 |
+
"Turk"
|
| 361 |
+
],
|
| 362 |
+
"description": "O'zbekistondagi eng yaxshi xususiy klinika. 24/7 tez yordam xizmati. Xalqaro standartlar."
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
"id": "clinic_007",
|
| 366 |
+
"name": "Sergeli Tuman Poliklinikasi",
|
| 367 |
+
"type": "davlat",
|
| 368 |
+
"district": "Sergeli tumani",
|
| 369 |
+
"address": "Sergeli ko'chasi, 90",
|
| 370 |
+
"phone": "+998 71 273-22-11",
|
| 371 |
+
"rating": 3.8,
|
| 372 |
+
"logo_url": "https://via.placeholder.com/150x150/16A085/ffffff?text=SergP",
|
| 373 |
+
"banner_url": "https://via.placeholder.com/800x300/16A085/ffffff?text=Sergeli+Poliklinika",
|
| 374 |
+
"photos": [],
|
| 375 |
+
"gps": {
|
| 376 |
+
"lat": 41.2289,
|
| 377 |
+
"lon": 69.2186
|
| 378 |
+
},
|
| 379 |
+
"working_hours": "08:00 - 18:00",
|
| 380 |
+
"working_days": [
|
| 381 |
+
"Dushanba",
|
| 382 |
+
"Seshanba",
|
| 383 |
+
"Chorshanba",
|
| 384 |
+
"Payshanba",
|
| 385 |
+
"Juma",
|
| 386 |
+
"Shanba"
|
| 387 |
+
],
|
| 388 |
+
"specializations": [
|
| 389 |
+
"Terapiya",
|
| 390 |
+
"Pediatriya"
|
| 391 |
+
],
|
| 392 |
+
"doctors_count": 15,
|
| 393 |
+
"has_emergency": true,
|
| 394 |
+
"insurance_accepted": true,
|
| 395 |
+
"services": [
|
| 396 |
+
{
|
| 397 |
+
"name": "Terapevt",
|
| 398 |
+
"price": "Bepul"
|
| 399 |
+
}
|
| 400 |
+
],
|
| 401 |
+
"languages": [
|
| 402 |
+
"O'zbekcha",
|
| 403 |
+
"Ruscha"
|
| 404 |
+
],
|
| 405 |
+
"description": "Sergeli tumanidagi poliklinika."
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"id": "clinic_008",
|
| 409 |
+
"name": "Olmazor Tuman Poliklinikasi",
|
| 410 |
+
"type": "davlat",
|
| 411 |
+
"district": "Olmazor tumani",
|
| 412 |
+
"address": "Olmazor ko'chasi, 15",
|
| 413 |
+
"phone": "+998 71 265-77-88",
|
| 414 |
+
"rating": 4.1,
|
| 415 |
+
"logo_url": "https://via.placeholder.com/150x150/D35400/ffffff?text=OTP",
|
| 416 |
+
"banner_url": "https://via.placeholder.com/800x300/D35400/ffffff?text=Olmazor+Poliklinika",
|
| 417 |
+
"photos": [
|
| 418 |
+
{
|
| 419 |
+
"url": "https://via.placeholder.com/600x400/EDBB99/333333?text=Xona",
|
| 420 |
+
"caption": "Shifokor xonasi"
|
| 421 |
+
}
|
| 422 |
+
],
|
| 423 |
+
"gps": {
|
| 424 |
+
"lat": 41.3367,
|
| 425 |
+
"lon": 69.2089
|
| 426 |
+
},
|
| 427 |
+
"working_hours": "08:00 - 18:00",
|
| 428 |
+
"working_days": [
|
| 429 |
+
"Dushanba",
|
| 430 |
+
"Seshanba",
|
| 431 |
+
"Chorshanba",
|
| 432 |
+
"Payshanba",
|
| 433 |
+
"Juma",
|
| 434 |
+
"Shanba"
|
| 435 |
+
],
|
| 436 |
+
"specializations": [
|
| 437 |
+
"Terapiya",
|
| 438 |
+
"Pediatriya",
|
| 439 |
+
"Stomatologiya"
|
| 440 |
+
],
|
| 441 |
+
"doctors_count": 28,
|
| 442 |
+
"has_emergency": true,
|
| 443 |
+
"insurance_accepted": true,
|
| 444 |
+
"services": [
|
| 445 |
+
{
|
| 446 |
+
"name": "Terapevt",
|
| 447 |
+
"price": "Bepul"
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"name": "Stomatolog",
|
| 451 |
+
"price": "Bepul"
|
| 452 |
+
}
|
| 453 |
+
],
|
| 454 |
+
"languages": [
|
| 455 |
+
"O'zbekcha",
|
| 456 |
+
"Ruscha"
|
| 457 |
+
],
|
| 458 |
+
"description": "Olmazor tumanidagi poliklinika. Stomatologiya xizmati."
|
| 459 |
+
}
|
| 460 |
+
]
|
| 461 |
+
}
|
data/dispatchers.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dispatchers": [
|
| 3 |
+
{
|
| 4 |
+
"id": "disp_001",
|
| 5 |
+
"username": "admin",
|
| 6 |
+
"password_hash": "admin123",
|
| 7 |
+
"full_name": "Admin",
|
| 8 |
+
"district": "Toshkent"
|
| 9 |
+
}
|
| 10 |
+
]
|
| 11 |
+
}
|
data/doctors.json
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"doctors": [
|
| 3 |
+
{
|
| 4 |
+
"id": "doc_001",
|
| 5 |
+
"full_name": "Dr. Aziza Rahimova",
|
| 6 |
+
"specialty": "Kardiolog",
|
| 7 |
+
"experience_years": 15,
|
| 8 |
+
"clinic_id": "clinic_002",
|
| 9 |
+
"photo_url": "https://i.pravatar.cc/300?img=47",
|
| 10 |
+
"education": "Toshkent Tibbiyot Akademiyasi, Kardiologiya mutaxassisligi",
|
| 11 |
+
"languages": [
|
| 12 |
+
"O'zbekcha",
|
| 13 |
+
"Ruscha",
|
| 14 |
+
"Inglizcha"
|
| 15 |
+
],
|
| 16 |
+
"rating": 4.9,
|
| 17 |
+
"consultation_price": "250,000 - 300,000 so'm",
|
| 18 |
+
"phone": "+998 90 123-45-67",
|
| 19 |
+
"available_days": [
|
| 20 |
+
"Dushanba",
|
| 21 |
+
"Chorshanba",
|
| 22 |
+
"Juma"
|
| 23 |
+
],
|
| 24 |
+
"available_time": "09:00 - 17:00",
|
| 25 |
+
"description": "Yurak kasalliklari bo'yicha 15 yillik tajribaga ega. Zamonaviy diagnostika usullari.",
|
| 26 |
+
"consultation_duration": 30,
|
| 27 |
+
"patient_reviews_count": 287,
|
| 28 |
+
"accepts_insurance": false
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "doc_002",
|
| 32 |
+
"full_name": "Dr. Jamshid Karimov",
|
| 33 |
+
"specialty": "Nevropatolog",
|
| 34 |
+
"experience_years": 12,
|
| 35 |
+
"clinic_id": "clinic_002",
|
| 36 |
+
"photo_url": "https://i.pravatar.cc/300?img=12",
|
| 37 |
+
"education": "Rossiya Milliy Tibbiyot Universiteti, Nevrologiya",
|
| 38 |
+
"languages": [
|
| 39 |
+
"O'zbekcha",
|
| 40 |
+
"Ruscha"
|
| 41 |
+
],
|
| 42 |
+
"rating": 4.7,
|
| 43 |
+
"consultation_price": "200,000 - 250,000 so'm",
|
| 44 |
+
"phone": "+998 90 234-56-78",
|
| 45 |
+
"available_days": [
|
| 46 |
+
"Seshanba",
|
| 47 |
+
"Payshanba",
|
| 48 |
+
"Shanba"
|
| 49 |
+
],
|
| 50 |
+
"available_time": "10:00 - 18:00",
|
| 51 |
+
"description": "Nerv tizimi kasalliklari, migren, insult reabilitatsiyasi mutaxassisi.",
|
| 52 |
+
"consultation_duration": 30,
|
| 53 |
+
"patient_reviews_count": 156,
|
| 54 |
+
"accepts_insurance": false
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "doc_003",
|
| 58 |
+
"full_name": "Dr. Nodira Tursunova",
|
| 59 |
+
"specialty": "Terapevt",
|
| 60 |
+
"experience_years": 8,
|
| 61 |
+
"clinic_id": "clinic_002",
|
| 62 |
+
"photo_url": "https://i.pravatar.cc/300?img=32",
|
| 63 |
+
"education": "Toshkent Pediatriya Instituti",
|
| 64 |
+
"languages": [
|
| 65 |
+
"O'zbekcha",
|
| 66 |
+
"Ruscha",
|
| 67 |
+
"Inglizcha"
|
| 68 |
+
],
|
| 69 |
+
"rating": 4.6,
|
| 70 |
+
"consultation_price": "150,000 - 200,000 so'm",
|
| 71 |
+
"phone": "+998 90 345-67-89",
|
| 72 |
+
"available_days": [
|
| 73 |
+
"Dushanba",
|
| 74 |
+
"Seshanba",
|
| 75 |
+
"Chorshanba",
|
| 76 |
+
"Payshanba",
|
| 77 |
+
"Juma"
|
| 78 |
+
],
|
| 79 |
+
"available_time": "08:00 - 16:00",
|
| 80 |
+
"description": "Umumiy terapiya, shamollash, o'tkir infeksiyalar, profilaktika.",
|
| 81 |
+
"consultation_duration": 25,
|
| 82 |
+
"patient_reviews_count": 98,
|
| 83 |
+
"accepts_insurance": false
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"id": "doc_004",
|
| 87 |
+
"full_name": "Dr. Sherzod Alimov",
|
| 88 |
+
"specialty": "Oftalmolog",
|
| 89 |
+
"experience_years": 20,
|
| 90 |
+
"clinic_id": "clinic_002",
|
| 91 |
+
"photo_url": "https://i.pravatar.cc/300?img=15",
|
| 92 |
+
"education": "Moskva Oftalmologiya Instituti",
|
| 93 |
+
"languages": [
|
| 94 |
+
"O'zbekcha",
|
| 95 |
+
"Ruscha"
|
| 96 |
+
],
|
| 97 |
+
"rating": 5.0,
|
| 98 |
+
"consultation_price": "300,000 so'm",
|
| 99 |
+
"phone": "+998 90 456-78-90",
|
| 100 |
+
"available_days": [
|
| 101 |
+
"Chorshanba",
|
| 102 |
+
"Juma"
|
| 103 |
+
],
|
| 104 |
+
"available_time": "09:00 - 15:00",
|
| 105 |
+
"description": "Ko'z kasalliklari, katarakta jarrohiyasi, lazer korrektsiya.",
|
| 106 |
+
"consultation_duration": 40,
|
| 107 |
+
"patient_reviews_count": 412,
|
| 108 |
+
"accepts_insurance": false
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": "doc_005",
|
| 112 |
+
"full_name": "Dr. Gulnora Mahmudova",
|
| 113 |
+
"specialty": "Terapevt",
|
| 114 |
+
"experience_years": 25,
|
| 115 |
+
"clinic_id": "clinic_001",
|
| 116 |
+
"photo_url": "https://i.pravatar.cc/300?img=48",
|
| 117 |
+
"education": "Toshkent Tibbiyot Akademiyasi",
|
| 118 |
+
"languages": [
|
| 119 |
+
"O'zbekcha",
|
| 120 |
+
"Ruscha"
|
| 121 |
+
],
|
| 122 |
+
"rating": 4.5,
|
| 123 |
+
"consultation_price": "Bepul (INPS)",
|
| 124 |
+
"phone": "+998 71 276-45-67",
|
| 125 |
+
"available_days": [
|
| 126 |
+
"Dushanba",
|
| 127 |
+
"Seshanba",
|
| 128 |
+
"Chorshanba",
|
| 129 |
+
"Payshanba",
|
| 130 |
+
"Juma"
|
| 131 |
+
],
|
| 132 |
+
"available_time": "08:00 - 17:00",
|
| 133 |
+
"description": "Tajribali terapevt. Davlat poliklinikasida 25 yil ish tajribasi.",
|
| 134 |
+
"consultation_duration": 20,
|
| 135 |
+
"patient_reviews_count": 564,
|
| 136 |
+
"accepts_insurance": true
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "doc_006",
|
| 140 |
+
"full_name": "Dr. Rustam Ibragimov",
|
| 141 |
+
"specialty": "Pediatr",
|
| 142 |
+
"experience_years": 10,
|
| 143 |
+
"clinic_id": "clinic_001",
|
| 144 |
+
"photo_url": "https://i.pravatar.cc/300?img=33",
|
| 145 |
+
"education": "Toshkent Pediatriya Instituti",
|
| 146 |
+
"languages": [
|
| 147 |
+
"O'zbekcha",
|
| 148 |
+
"Ruscha"
|
| 149 |
+
],
|
| 150 |
+
"rating": 4.8,
|
| 151 |
+
"consultation_price": "Bepul (INPS)",
|
| 152 |
+
"phone": "+998 71 276-45-67",
|
| 153 |
+
"available_days": [
|
| 154 |
+
"Dushanba",
|
| 155 |
+
"Seshanba",
|
| 156 |
+
"Chorshanba",
|
| 157 |
+
"Payshanba",
|
| 158 |
+
"Juma",
|
| 159 |
+
"Shanba"
|
| 160 |
+
],
|
| 161 |
+
"available_time": "08:00 - 18:00",
|
| 162 |
+
"description": "Bolalar kasalliklari mutaxassisi. Mehr-shafqat bilan davolanish.",
|
| 163 |
+
"consultation_duration": 20,
|
| 164 |
+
"patient_reviews_count": 245,
|
| 165 |
+
"accepts_insurance": true
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"id": "doc_007",
|
| 169 |
+
"full_name": "Dr. Malika Yusupova",
|
| 170 |
+
"specialty": "Oilaviy shifokor",
|
| 171 |
+
"experience_years": 6,
|
| 172 |
+
"clinic_id": "clinic_004",
|
| 173 |
+
"photo_url": "https://i.pravatar.cc/300?img=29",
|
| 174 |
+
"education": "Toshkent Tibbiyot Akademiyasi",
|
| 175 |
+
"languages": [
|
| 176 |
+
"O'zbekcha",
|
| 177 |
+
"Ruscha",
|
| 178 |
+
"Inglizcha"
|
| 179 |
+
],
|
| 180 |
+
"rating": 4.7,
|
| 181 |
+
"consultation_price": "180,000 so'm",
|
| 182 |
+
"phone": "+998 71 291-44-55",
|
| 183 |
+
"available_days": [
|
| 184 |
+
"Dushanba",
|
| 185 |
+
"Chorshanba",
|
| 186 |
+
"Juma",
|
| 187 |
+
"Shanba"
|
| 188 |
+
],
|
| 189 |
+
"available_time": "09:00 - 19:00",
|
| 190 |
+
"description": "Butun oila uchun shifokor. Bolalar va kattalar uchun xizmat.",
|
| 191 |
+
"consultation_duration": 30,
|
| 192 |
+
"patient_reviews_count": 89,
|
| 193 |
+
"accepts_insurance": true
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"id": "doc_008",
|
| 197 |
+
"full_name": "Dr. Anvar Saidov",
|
| 198 |
+
"specialty": "Kardiolog",
|
| 199 |
+
"experience_years": 18,
|
| 200 |
+
"clinic_id": "clinic_006",
|
| 201 |
+
"photo_url": "https://i.pravatar.cc/300?img=13",
|
| 202 |
+
"education": "Germaniya Berlin Charite Universiteti",
|
| 203 |
+
"languages": [
|
| 204 |
+
"O'zbekcha",
|
| 205 |
+
"Ruscha",
|
| 206 |
+
"Inglizcha",
|
| 207 |
+
"Nemis"
|
| 208 |
+
],
|
| 209 |
+
"rating": 5.0,
|
| 210 |
+
"consultation_price": "500,000 so'm",
|
| 211 |
+
"phone": "+998 71 207-88-99",
|
| 212 |
+
"available_days": [
|
| 213 |
+
"Seshanba",
|
| 214 |
+
"Payshanba"
|
| 215 |
+
],
|
| 216 |
+
"available_time": "10:00 - 16:00",
|
| 217 |
+
"description": "Yuqori malakali kardiolog. Xalqaro tajriba. Murakkab diagnostika.",
|
| 218 |
+
"consultation_duration": 45,
|
| 219 |
+
"patient_reviews_count": 678,
|
| 220 |
+
"accepts_insurance": true
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"id": "doc_009",
|
| 224 |
+
"full_name": "Dr. Dilnoza Ergasheva",
|
| 225 |
+
"specialty": "Nevropatolog",
|
| 226 |
+
"experience_years": 14,
|
| 227 |
+
"clinic_id": "clinic_006",
|
| 228 |
+
"photo_url": "https://i.pravatar.cc/300?img=35",
|
| 229 |
+
"education": "Rossiya Pavlov Universiteti, Nevrologiya",
|
| 230 |
+
"languages": [
|
| 231 |
+
"O'zbekcha",
|
| 232 |
+
"Ruscha"
|
| 233 |
+
],
|
| 234 |
+
"rating": 4.8,
|
| 235 |
+
"consultation_price": "400,000 so'm",
|
| 236 |
+
"phone": "+998 71 207-88-99",
|
| 237 |
+
"available_days": [
|
| 238 |
+
"Dushanba",
|
| 239 |
+
"Chorshanba",
|
| 240 |
+
"Juma"
|
| 241 |
+
],
|
| 242 |
+
"available_time": "09:00 - 17:00",
|
| 243 |
+
"description": "Nerv tizimi kasalliklari, qo'l-oyoq titroq, migren davolash.",
|
| 244 |
+
"consultation_duration": 40,
|
| 245 |
+
"patient_reviews_count": 234,
|
| 246 |
+
"accepts_insurance": true
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"id": "doc_010",
|
| 250 |
+
"full_name": "Dr. Bobur Qodirov",
|
| 251 |
+
"specialty": "Ortoped",
|
| 252 |
+
"experience_years": 16,
|
| 253 |
+
"clinic_id": "clinic_006",
|
| 254 |
+
"photo_url": "https://i.pravatar.cc/300?img=51",
|
| 255 |
+
"education": "Koreya Ulsan Universiteti, Ortopediya",
|
| 256 |
+
"languages": [
|
| 257 |
+
"O'zbekcha",
|
| 258 |
+
"Ruscha",
|
| 259 |
+
"Koreya"
|
| 260 |
+
],
|
| 261 |
+
"rating": 4.9,
|
| 262 |
+
"consultation_price": "450,000 so'm",
|
| 263 |
+
"phone": "+998 71 207-88-99",
|
| 264 |
+
"available_days": [
|
| 265 |
+
"Seshanba",
|
| 266 |
+
"Payshanba",
|
| 267 |
+
"Shanba"
|
| 268 |
+
],
|
| 269 |
+
"available_time": "10:00 - 18:00",
|
| 270 |
+
"description": "Suyak kasalliklari, bo'g'im og'riqlari, jarrohlik amaliyotlari.",
|
| 271 |
+
"consultation_duration": 35,
|
| 272 |
+
"patient_reviews_count": 189,
|
| 273 |
+
"accepts_insurance": false
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"id": "doc_011",
|
| 277 |
+
"full_name": "Dr. Kamola Ahmedova",
|
| 278 |
+
"specialty": "Terapevt",
|
| 279 |
+
"experience_years": 7,
|
| 280 |
+
"clinic_id": "clinic_003",
|
| 281 |
+
"photo_url": "https://i.pravatar.cc/300?img=41",
|
| 282 |
+
"education": "Toshkent Tibbiyot Akademiyasi",
|
| 283 |
+
"languages": [
|
| 284 |
+
"O'zbekcha",
|
| 285 |
+
"Ruscha"
|
| 286 |
+
],
|
| 287 |
+
"rating": 4.2,
|
| 288 |
+
"consultation_price": "Bepul (INPS)",
|
| 289 |
+
"phone": "+998 71 254-33-21",
|
| 290 |
+
"available_days": [
|
| 291 |
+
"Dushanba",
|
| 292 |
+
"Chorshanba",
|
| 293 |
+
"Juma"
|
| 294 |
+
],
|
| 295 |
+
"available_time": "08:00 - 16:00",
|
| 296 |
+
"description": "Umumiy terapiya, o'tkir infeksiyalar, shamollash davolash.",
|
| 297 |
+
"consultation_duration": 20,
|
| 298 |
+
"patient_reviews_count": 67,
|
| 299 |
+
"accepts_insurance": true
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"id": "doc_012",
|
| 303 |
+
"full_name": "Dr. Sardor Valiyev",
|
| 304 |
+
"specialty": "Kardiolog",
|
| 305 |
+
"experience_years": 11,
|
| 306 |
+
"clinic_id": "clinic_005",
|
| 307 |
+
"photo_url": "https://i.pravatar.cc/300?img=14",
|
| 308 |
+
"education": "Sankt-Peterburg Tibbiyot Universiteti",
|
| 309 |
+
"languages": [
|
| 310 |
+
"O'zbekcha",
|
| 311 |
+
"Ruscha"
|
| 312 |
+
],
|
| 313 |
+
"rating": 4.4,
|
| 314 |
+
"consultation_price": "Bepul (INPS)",
|
| 315 |
+
"phone": "+998 71 244-66-77",
|
| 316 |
+
"available_days": [
|
| 317 |
+
"Seshanba",
|
| 318 |
+
"Payshanba",
|
| 319 |
+
"Shanba"
|
| 320 |
+
],
|
| 321 |
+
"available_time": "08:00 - 17:00",
|
| 322 |
+
"description": "Yurak kasalliklari bo'yicha mutaxassis. EKG, Echo-KG.",
|
| 323 |
+
"consultation_duration": 25,
|
| 324 |
+
"patient_reviews_count": 145,
|
| 325 |
+
"accepts_insurance": true
|
| 326 |
+
}
|
| 327 |
+
]
|
| 328 |
+
}
|
data/doctors_photos.json
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"doctors": [
|
| 3 |
+
{
|
| 4 |
+
"id": "doc_001",
|
| 5 |
+
"clinic_id": "clinic_101",
|
| 6 |
+
"full_name": "Dr. Azimov Jasur",
|
| 7 |
+
"specialty": "Kardiologiya",
|
| 8 |
+
"experience_years": 15,
|
| 9 |
+
"rating": 4.9,
|
| 10 |
+
"price_range": "250,000 - 350,000 so'm",
|
| 11 |
+
"phone": "+998901234567",
|
| 12 |
+
"photo_url": "https://i.pravatar.cc/150?img=12",
|
| 13 |
+
"working_days": [
|
| 14 |
+
"Dushanba",
|
| 15 |
+
"Chorshanba",
|
| 16 |
+
"Juma"
|
| 17 |
+
],
|
| 18 |
+
"working_hours": "09:00 - 17:00",
|
| 19 |
+
"description": "15 yillik tajribaga ega yurak kasalliklari mutaxassisi. Xorijda malaka oshirgan."
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "doc_002",
|
| 23 |
+
"clinic_id": "clinic_101",
|
| 24 |
+
"full_name": "Dr. Karimova Dilnoza",
|
| 25 |
+
"specialty": "Terapiya",
|
| 26 |
+
"experience_years": 12,
|
| 27 |
+
"rating": 4.7,
|
| 28 |
+
"price_range": "150,000 - 250,000 so'm",
|
| 29 |
+
"phone": "+998901234567",
|
| 30 |
+
"photo_url": "https://i.pravatar.cc/150?img=45",
|
| 31 |
+
"working_days": [
|
| 32 |
+
"Seshanba",
|
| 33 |
+
"Payshanba",
|
| 34 |
+
"Shanba"
|
| 35 |
+
],
|
| 36 |
+
"working_hours": "10:00 - 18:00",
|
| 37 |
+
"description": "Umumiy terapevt, oilaviy shifokor. Yumshoq yondashuv."
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "doc_003",
|
| 41 |
+
"clinic_id": "clinic_101",
|
| 42 |
+
"full_name": "Dr. Tursunov Otabek",
|
| 43 |
+
"specialty": "Gastroenterologiya",
|
| 44 |
+
"experience_years": 10,
|
| 45 |
+
"rating": 4.8,
|
| 46 |
+
"price_range": "200,000 - 300,000 so'm",
|
| 47 |
+
"phone": "+998901234567",
|
| 48 |
+
"photo_url": "https://i.pravatar.cc/150?img=33",
|
| 49 |
+
"working_days": [
|
| 50 |
+
"Dushanba",
|
| 51 |
+
"Seshanba",
|
| 52 |
+
"Chorshanba"
|
| 53 |
+
],
|
| 54 |
+
"working_hours": "09:00 - 16:00",
|
| 55 |
+
"description": "Oshqozon-ichak kasalliklari bo'yicha tajribali shifokor."
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "doc_004",
|
| 59 |
+
"clinic_id": "clinic_102",
|
| 60 |
+
"full_name": "Dr. Rahimova Zarina",
|
| 61 |
+
"specialty": "Nevrologiya",
|
| 62 |
+
"experience_years": 18,
|
| 63 |
+
"rating": 4.9,
|
| 64 |
+
"price_range": "300,000 - 450,000 so'm",
|
| 65 |
+
"phone": "+998901234568",
|
| 66 |
+
"photo_url": "https://i.pravatar.cc/150?img=47",
|
| 67 |
+
"working_days": [
|
| 68 |
+
"Dushanba",
|
| 69 |
+
"Chorshanba",
|
| 70 |
+
"Juma",
|
| 71 |
+
"Shanba"
|
| 72 |
+
],
|
| 73 |
+
"working_hours": "08:00 - 17:00",
|
| 74 |
+
"description": "Nevrologiya professori, yetakchi mutaxassis. Xorijda 5 yil ishlagan."
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"id": "doc_005",
|
| 78 |
+
"clinic_id": "clinic_102",
|
| 79 |
+
"full_name": "Dr. Ismoilov Sherzod",
|
| 80 |
+
"specialty": "Kardiologiya",
|
| 81 |
+
"experience_years": 14,
|
| 82 |
+
"rating": 4.8,
|
| 83 |
+
"price_range": "280,000 - 400,000 so'm",
|
| 84 |
+
"phone": "+998901234568",
|
| 85 |
+
"photo_url": "https://i.pravatar.cc/150?img=51",
|
| 86 |
+
"working_days": [
|
| 87 |
+
"Seshanba",
|
| 88 |
+
"Payshanba",
|
| 89 |
+
"Juma"
|
| 90 |
+
],
|
| 91 |
+
"working_hours": "09:00 - 18:00",
|
| 92 |
+
"description": "Yurak operatsiyalari bo'yicha tajribali jarroh."
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"id": "doc_006",
|
| 96 |
+
"clinic_id": "clinic_103",
|
| 97 |
+
"full_name": "Dr. Mahmudova Feruza",
|
| 98 |
+
"specialty": "Pediatriya",
|
| 99 |
+
"experience_years": 9,
|
| 100 |
+
"rating": 4.7,
|
| 101 |
+
"price_range": "150,000 - 220,000 so'm",
|
| 102 |
+
"phone": "+998901234569",
|
| 103 |
+
"photo_url": "https://i.pravatar.cc/150?img=44",
|
| 104 |
+
"working_days": [
|
| 105 |
+
"Dushanba",
|
| 106 |
+
"Seshanba",
|
| 107 |
+
"Chorshanba",
|
| 108 |
+
"Payshanba",
|
| 109 |
+
"Juma"
|
| 110 |
+
],
|
| 111 |
+
"working_hours": "09:00 - 17:00",
|
| 112 |
+
"description": "Bolalar shifokor, yumshoq va e'tiborli. Ota-onalar sevimli doktor."
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"id": "doc_007",
|
| 116 |
+
"clinic_id": "clinic_103",
|
| 117 |
+
"full_name": "Dr. Akbarov Javohir",
|
| 118 |
+
"specialty": "Ginekologiya",
|
| 119 |
+
"experience_years": 11,
|
| 120 |
+
"rating": 4.6,
|
| 121 |
+
"price_range": "200,000 - 280,000 so'm",
|
| 122 |
+
"phone": "+998901234569",
|
| 123 |
+
"photo_url": "https://i.pravatar.cc/150?img=68",
|
| 124 |
+
"working_days": [
|
| 125 |
+
"Dushanba",
|
| 126 |
+
"Chorshanba",
|
| 127 |
+
"Juma",
|
| 128 |
+
"Shanba"
|
| 129 |
+
],
|
| 130 |
+
"working_hours": "10:00 - 18:00",
|
| 131 |
+
"description": "Ayollar salomatligi bo'yicha mutaxassis. Homilador ayollarga e'tibor."
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "doc_008",
|
| 135 |
+
"clinic_id": "clinic_104",
|
| 136 |
+
"full_name": "Dr. Nabiyev Rustam",
|
| 137 |
+
"specialty": "Kardiologiya",
|
| 138 |
+
"experience_years": 20,
|
| 139 |
+
"rating": 5.0,
|
| 140 |
+
"price_range": "350,000 - 500,000 so'm",
|
| 141 |
+
"phone": "+998901234570",
|
| 142 |
+
"photo_url": "https://i.pravatar.cc/150?img=15",
|
| 143 |
+
"working_days": [
|
| 144 |
+
"Dushanba",
|
| 145 |
+
"Seshanba",
|
| 146 |
+
"Chorshanba",
|
| 147 |
+
"Payshanba"
|
| 148 |
+
],
|
| 149 |
+
"working_hours": "08:00 - 16:00",
|
| 150 |
+
"description": "O'zbekistonning eng yaxshi kardiologi. Xalqaro sertifikatlar."
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"id": "doc_009",
|
| 154 |
+
"clinic_id": "clinic_104",
|
| 155 |
+
"full_name": "Dr. Abdullayeva Nodira",
|
| 156 |
+
"specialty": "Travmatologiya",
|
| 157 |
+
"experience_years": 13,
|
| 158 |
+
"rating": 4.8,
|
| 159 |
+
"price_range": "280,000 - 380,000 so'm",
|
| 160 |
+
"phone": "+998901234570",
|
| 161 |
+
"photo_url": "https://i.pravatar.cc/150?img=49",
|
| 162 |
+
"working_days": [
|
| 163 |
+
"Seshanba",
|
| 164 |
+
"Payshanba",
|
| 165 |
+
"Shanba"
|
| 166 |
+
],
|
| 167 |
+
"working_hours": "09:00 - 17:00",
|
| 168 |
+
"description": "Suyak sinishlari va jarohatlari bo'yicha tajribali shifokor."
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"id": "doc_010",
|
| 172 |
+
"clinic_id": "clinic_105",
|
| 173 |
+
"full_name": "Dr. Xolmatov Sardor",
|
| 174 |
+
"specialty": "LOR",
|
| 175 |
+
"experience_years": 8,
|
| 176 |
+
"rating": 4.5,
|
| 177 |
+
"price_range": "150,000 - 200,000 so'm",
|
| 178 |
+
"phone": "+998901234571",
|
| 179 |
+
"photo_url": "https://i.pravatar.cc/150?img=56",
|
| 180 |
+
"working_days": [
|
| 181 |
+
"Dushanba",
|
| 182 |
+
"Chorshanba",
|
| 183 |
+
"Juma"
|
| 184 |
+
],
|
| 185 |
+
"working_hours": "10:00 - 18:00",
|
| 186 |
+
"description": "Quloq-burun-tomoq kasalliklari mutaxassisi. Zamonaviy asboblar."
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"id": "doc_011",
|
| 190 |
+
"clinic_id": "clinic_105",
|
| 191 |
+
"full_name": "Dr. Yusupova Malika",
|
| 192 |
+
"specialty": "Dermatologiya",
|
| 193 |
+
"experience_years": 10,
|
| 194 |
+
"rating": 4.6,
|
| 195 |
+
"price_range": "180,000 - 250,000 so'm",
|
| 196 |
+
"phone": "+998901234571",
|
| 197 |
+
"photo_url": "https://i.pravatar.cc/150?img=43",
|
| 198 |
+
"working_days": [
|
| 199 |
+
"Seshanba",
|
| 200 |
+
"Payshanba",
|
| 201 |
+
"Shanba"
|
| 202 |
+
],
|
| 203 |
+
"working_hours": "09:00 - 17:00",
|
| 204 |
+
"description": "Teri kasalliklari va kosmetologiya bo'yicha mutaxassis."
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": "doc_012",
|
| 208 |
+
"clinic_id": "clinic_106",
|
| 209 |
+
"full_name": "Dr. Ergashev Jamshid",
|
| 210 |
+
"specialty": "Endokrinologiya",
|
| 211 |
+
"experience_years": 16,
|
| 212 |
+
"rating": 4.9,
|
| 213 |
+
"price_range": "320,000 - 450,000 so'm",
|
| 214 |
+
"phone": "+998901234572",
|
| 215 |
+
"photo_url": "https://i.pravatar.cc/150?img=59",
|
| 216 |
+
"working_days": [
|
| 217 |
+
"Dushanba",
|
| 218 |
+
"Chorshanba",
|
| 219 |
+
"Juma",
|
| 220 |
+
"Shanba"
|
| 221 |
+
],
|
| 222 |
+
"working_hours": "08:00 - 17:00",
|
| 223 |
+
"description": "Qandli diabet va gormon kasalliklari bo'yicha etakchi mutaxassis."
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"id": "doc_013",
|
| 227 |
+
"clinic_id": "clinic_106",
|
| 228 |
+
"full_name": "Dr. Qodirova Sevara",
|
| 229 |
+
"specialty": "Ginekologiya",
|
| 230 |
+
"experience_years": 12,
|
| 231 |
+
"rating": 4.7,
|
| 232 |
+
"price_range": "280,000 - 380,000 so'm",
|
| 233 |
+
"phone": "+998901234572",
|
| 234 |
+
"photo_url": "https://i.pravatar.cc/150?img=48",
|
| 235 |
+
"working_days": [
|
| 236 |
+
"Seshanba",
|
| 237 |
+
"Payshanba",
|
| 238 |
+
"Juma"
|
| 239 |
+
],
|
| 240 |
+
"working_hours": "10:00 - 18:00",
|
| 241 |
+
"description": "Ayollar reproduktiv salomatligi bo'yicha tajribali shifokor."
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"id": "doc_014",
|
| 245 |
+
"clinic_id": "clinic_107",
|
| 246 |
+
"full_name": "Dr. Mirzayev Anvar",
|
| 247 |
+
"specialty": "Terapiya",
|
| 248 |
+
"experience_years": 7,
|
| 249 |
+
"rating": 4.4,
|
| 250 |
+
"price_range": "100,000 - 150,000 so'm",
|
| 251 |
+
"phone": "+998901234573",
|
| 252 |
+
"photo_url": "https://i.pravatar.cc/150?img=60",
|
| 253 |
+
"working_days": [
|
| 254 |
+
"Dushanba",
|
| 255 |
+
"Seshanba",
|
| 256 |
+
"Chorshanba",
|
| 257 |
+
"Payshanba",
|
| 258 |
+
"Juma"
|
| 259 |
+
],
|
| 260 |
+
"working_hours": "09:00 - 17:00",
|
| 261 |
+
"description": "Yosh terapevt, zamonaviy yondashuv. Hamyonbop narxlar."
|
| 262 |
+
},
|
| 263 |
+
{
|
| 264 |
+
"id": "doc_015",
|
| 265 |
+
"clinic_id": "clinic_107",
|
| 266 |
+
"full_name": "Dr. Sultanova Dilfuza",
|
| 267 |
+
"specialty": "Pediatriya",
|
| 268 |
+
"experience_years": 9,
|
| 269 |
+
"rating": 4.5,
|
| 270 |
+
"price_range": "130,000 - 180,000 so'm",
|
| 271 |
+
"phone": "+998901234573",
|
| 272 |
+
"photo_url": "https://i.pravatar.cc/150?img=46",
|
| 273 |
+
"working_days": [
|
| 274 |
+
"Dushanba",
|
| 275 |
+
"Chorshanba",
|
| 276 |
+
"Juma",
|
| 277 |
+
"Shanba"
|
| 278 |
+
],
|
| 279 |
+
"working_hours": "09:00 - 18:00",
|
| 280 |
+
"description": "Bolalar shifokori, e'tiborli va samimiy. Kichkintoylar sevadi."
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"id": "doc_016",
|
| 284 |
+
"clinic_id": "clinic_108",
|
| 285 |
+
"full_name": "Dr. Vohidov Bekzod",
|
| 286 |
+
"specialty": "Kardiologiya",
|
| 287 |
+
"experience_years": 14,
|
| 288 |
+
"rating": 4.7,
|
| 289 |
+
"price_range": "280,000 - 380,000 so'm",
|
| 290 |
+
"phone": "+998901234574",
|
| 291 |
+
"photo_url": "https://i.pravatar.cc/150?img=70",
|
| 292 |
+
"working_days": [
|
| 293 |
+
"Seshanba",
|
| 294 |
+
"Payshanba",
|
| 295 |
+
"Shanba"
|
| 296 |
+
],
|
| 297 |
+
"working_hours": "08:00 - 16:00",
|
| 298 |
+
"description": "Yurak kasalliklari bo'yicha tajribali mutaxassis. Tez xizmat."
|
| 299 |
+
},
|
| 300 |
+
{
|
| 301 |
+
"id": "doc_017",
|
| 302 |
+
"clinic_id": "clinic_108",
|
| 303 |
+
"full_name": "Dr. Nosirova Gulnora",
|
| 304 |
+
"specialty": "Nevrologiya",
|
| 305 |
+
"experience_years": 11,
|
| 306 |
+
"rating": 4.6,
|
| 307 |
+
"price_range": "250,000 - 350,000 so'm",
|
| 308 |
+
"phone": "+998901234574",
|
| 309 |
+
"photo_url": "https://i.pravatar.cc/150?img=32",
|
| 310 |
+
"working_days": [
|
| 311 |
+
"Dushanba",
|
| 312 |
+
"Chorshanba",
|
| 313 |
+
"Juma"
|
| 314 |
+
],
|
| 315 |
+
"working_hours": "09:00 - 17:00",
|
| 316 |
+
"description": "Asab kasalliklari va bosh og'rig'i bo'yicha mutaxassis."
|
| 317 |
+
}
|
| 318 |
+
]
|
| 319 |
+
}
|
data/locations.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locations": [
|
| 3 |
+
{
|
| 4 |
+
"district": "Chilonzor",
|
| 5 |
+
"street": "Muqimiy ko'chasi"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"district": "Yunusobod",
|
| 9 |
+
"street": "Amir Temur shoh ko'chasi"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"district": "Mirobod",
|
| 13 |
+
"street": "Nukus ko'chasi"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"district": "Yashnobod",
|
| 17 |
+
"street": "Farg'ona yo'li ko'chasi"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"district": "Shayxontohur",
|
| 21 |
+
"street": "Navoiy ko'chasi"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"district": "Olmazor",
|
| 25 |
+
"street": "Beruniy ko'chasi"
|
| 26 |
+
}
|
| 27 |
+
]
|
| 28 |
+
}
|
data/locations_db.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/messages.json
ADDED
|
@@ -0,0 +1,1275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"messages": [
|
| 3 |
+
{
|
| 4 |
+
"id": "msg_001",
|
| 5 |
+
"case_id": "case_001",
|
| 6 |
+
"sender": "bemor",
|
| 7 |
+
"audio_url": null,
|
| 8 |
+
"text_content": "tushunarli.",
|
| 9 |
+
"created_at": "2025-10-17T15:10:00.873878"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"id": "msg_002",
|
| 13 |
+
"case_id": "case_001",
|
| 14 |
+
"sender": "ai",
|
| 15 |
+
"audio_url": null,
|
| 16 |
+
"text_content": "Tushundim, bu jiddiy holat. Iltimos, aniq manzilingizni ayting, brigada yuboryapmiz.",
|
| 17 |
+
"created_at": "2025-10-17T15:10:02.794778"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"id": "msg_003",
|
| 21 |
+
"case_id": "case_002",
|
| 22 |
+
"sender": "bemor",
|
| 23 |
+
"audio_url": null,
|
| 24 |
+
"text_content": "havo yo'q.",
|
| 25 |
+
"created_at": "2025-10-17T16:11:44.880651"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"id": "msg_004",
|
| 29 |
+
"case_id": "case_002",
|
| 30 |
+
"sender": "ai",
|
| 31 |
+
"audio_url": null,
|
| 32 |
+
"text_content": "Tushundim, bu jiddiy holat. Iltimos, aniq manzilingizni ayting, brigada yuboryapmiz.",
|
| 33 |
+
"created_at": "2025-10-17T16:11:47.401074"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"id": "msg_0005",
|
| 37 |
+
"case_id": "case_a2434c0d",
|
| 38 |
+
"sender": "user",
|
| 39 |
+
"content": "nom bratish",
|
| 40 |
+
"timestamp": "2025-10-17T22:56:11.212651"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "msg_0006",
|
| 44 |
+
"case_id": "case_5421103e",
|
| 45 |
+
"sender": "user",
|
| 46 |
+
"content": "salom.",
|
| 47 |
+
"timestamp": "2025-10-17T23:04:13.013732"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"id": "msg_0007",
|
| 51 |
+
"case_id": "case_56300111",
|
| 52 |
+
"sender": "user",
|
| 53 |
+
"content": "alom. alom.",
|
| 54 |
+
"timestamp": "2025-10-17T23:04:39.702745"
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "msg_0008",
|
| 58 |
+
"case_id": "case_ca16bf52",
|
| 59 |
+
"sender": "user",
|
| 60 |
+
"content": "salom.",
|
| 61 |
+
"timestamp": "2025-10-17T23:04:48.179553"
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "msg_0009",
|
| 65 |
+
"case_id": "case_ac85c947",
|
| 66 |
+
"sender": "user",
|
| 67 |
+
"content": "salom",
|
| 68 |
+
"timestamp": "2025-10-17T23:09:30.146821"
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"id": "msg_0010",
|
| 72 |
+
"case_id": "case_b8c4c670",
|
| 73 |
+
"sender": "user",
|
| 74 |
+
"content": "salom.",
|
| 75 |
+
"timestamp": "2025-10-17T23:15:56.110430"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"id": "msg_0011",
|
| 79 |
+
"case_id": "case_024",
|
| 80 |
+
"sender": "user",
|
| 81 |
+
"content": "salom.",
|
| 82 |
+
"timestamp": "2025-10-17T23:35:34.496189"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"id": "msg_0012",
|
| 86 |
+
"case_id": "case_024",
|
| 87 |
+
"sender": "ai",
|
| 88 |
+
"content": "Assalomu alaykum! Men \"Help.me\" tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 89 |
+
"timestamp": "2025-10-17T23:35:37.770242"
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "msg_0013",
|
| 93 |
+
"case_id": "case_024",
|
| 94 |
+
"sender": "user",
|
| 95 |
+
"content": "salom",
|
| 96 |
+
"timestamp": "2025-10-17T23:35:49.336252"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "msg_0014",
|
| 100 |
+
"case_id": "case_024",
|
| 101 |
+
"sender": "ai",
|
| 102 |
+
"content": "Assalomu alaykum! Sizga qanday yordam kerak? O'zingizni qanday his qilyapsiz?",
|
| 103 |
+
"timestamp": "2025-10-17T23:35:51.668175"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"id": "msg_0015",
|
| 107 |
+
"case_id": "case_024",
|
| 108 |
+
"sender": "user",
|
| 109 |
+
"content": "salom ming yordam kerak do'stim",
|
| 110 |
+
"timestamp": "2025-10-17T23:36:00.988274"
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"id": "msg_0016",
|
| 114 |
+
"case_id": "case_024",
|
| 115 |
+
"sender": "ai",
|
| 116 |
+
"content": "Tushunmadim. Iltimos, muammoingizni aniqroq ayting. Nima bo'ldi?",
|
| 117 |
+
"timestamp": "2025-10-17T23:36:03.148923"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "msg_0017",
|
| 121 |
+
"case_id": "case_024",
|
| 122 |
+
"sender": "user",
|
| 123 |
+
"content": "do'stim nima gap do'stim",
|
| 124 |
+
"timestamp": "2025-10-17T23:36:20.014803"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"id": "msg_0018",
|
| 128 |
+
"case_id": "case_024",
|
| 129 |
+
"sender": "ai",
|
| 130 |
+
"content": "Kechirasiz, lekin men sizni tushunmayapman. Sizni operatorga ulayman, ular sizga yordam berishadi.",
|
| 131 |
+
"timestamp": "2025-10-17T23:36:22.011400"
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "msg_0019",
|
| 135 |
+
"case_id": "case_024",
|
| 136 |
+
"sender": "user",
|
| 137 |
+
"content": "apiatrga ulamay tur muammoni aytaman hozir",
|
| 138 |
+
"timestamp": "2025-10-17T23:36:33.081384"
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"id": "msg_0020",
|
| 142 |
+
"case_id": "case_024",
|
| 143 |
+
"sender": "ai",
|
| 144 |
+
"content": "Yaxshi, men sizni tinglayman. Muammoingizni batafsilroq aytib bering. Nima bezovta qilyapti?",
|
| 145 |
+
"timestamp": "2025-10-17T23:36:35.340038"
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"id": "msg_0021",
|
| 149 |
+
"case_id": "case_025",
|
| 150 |
+
"sender": "user",
|
| 151 |
+
"content": "salom",
|
| 152 |
+
"timestamp": "2025-10-17T23:41:39.848300"
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"id": "msg_0022",
|
| 156 |
+
"case_id": "case_025",
|
| 157 |
+
"sender": "ai",
|
| 158 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 159 |
+
"timestamp": "2025-10-17T23:41:42.598164"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"id": "msg_0023",
|
| 163 |
+
"case_id": "case_025",
|
| 164 |
+
"sender": "ai",
|
| 165 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 166 |
+
"timestamp": "2025-10-17T23:41:42.716736"
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"id": "msg_0024",
|
| 170 |
+
"case_id": "case_025",
|
| 171 |
+
"sender": "user",
|
| 172 |
+
"content": "avayu",
|
| 173 |
+
"timestamp": "2025-10-17T23:42:44.356593"
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"id": "msg_0025",
|
| 177 |
+
"case_id": "case_025",
|
| 178 |
+
"sender": "ai",
|
| 179 |
+
"content": "Kechirasiz, tushunmadim. Iltimos, muammoingizni aniqroq tushuntirib bering. Nima sizni bezovta qilyapti?",
|
| 180 |
+
"timestamp": "2025-10-17T23:42:47.250640"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"id": "msg_0026",
|
| 184 |
+
"case_id": "case_025",
|
| 185 |
+
"sender": "ai",
|
| 186 |
+
"content": "Kechirasiz, tushunmadim. Iltimos, muammoingizni aniqroq tushuntirib bering. Nima sizni bezovta qilyapti?",
|
| 187 |
+
"timestamp": "2025-10-17T23:42:47.358788"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"id": "msg_0027",
|
| 191 |
+
"case_id": "case_025",
|
| 192 |
+
"sender": "user",
|
| 193 |
+
"content": "how are you?",
|
| 194 |
+
"timestamp": "2025-10-17T23:42:51.573880"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"id": "msg_0028",
|
| 198 |
+
"case_id": "case_025",
|
| 199 |
+
"sender": "ai",
|
| 200 |
+
"content": "I am an AI assistant, I don't have feelings. How can I help you today? What symptoms are you experiencing?",
|
| 201 |
+
"timestamp": "2025-10-17T23:42:53.890552"
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"id": "msg_0029",
|
| 205 |
+
"case_id": "case_025",
|
| 206 |
+
"sender": "ai",
|
| 207 |
+
"content": "I am an AI assistant, I don't have feelings. How can I help you today? What symptoms are you experiencing?",
|
| 208 |
+
"timestamp": "2025-10-17T23:42:54.008236"
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"id": "msg_0030",
|
| 212 |
+
"case_id": "case_026",
|
| 213 |
+
"sender": "user",
|
| 214 |
+
"content": "salom",
|
| 215 |
+
"timestamp": "2025-10-17T23:45:04.734688"
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"id": "msg_0031",
|
| 219 |
+
"case_id": "case_026",
|
| 220 |
+
"sender": "ai",
|
| 221 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 222 |
+
"timestamp": "2025-10-17T23:45:07.801098"
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"id": "msg_0032",
|
| 226 |
+
"case_id": "case_026",
|
| 227 |
+
"sender": "ai",
|
| 228 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 229 |
+
"timestamp": "2025-10-17T23:45:07.893476"
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"id": "msg_0033",
|
| 233 |
+
"case_id": "case_026",
|
| 234 |
+
"sender": "user",
|
| 235 |
+
"content": "u menya baletgarlo",
|
| 236 |
+
"timestamp": "2025-10-17T23:45:23.128581"
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"id": "msg_0034",
|
| 240 |
+
"case_id": "case_026",
|
| 241 |
+
"sender": "ai",
|
| 242 |
+
"content": "Izvinite, ya ne совсем ponyal. Chto u vas bolit? Mozhete opisat' подробнее?",
|
| 243 |
+
"timestamp": "2025-10-17T23:45:25.460622"
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"id": "msg_0035",
|
| 247 |
+
"case_id": "case_026",
|
| 248 |
+
"sender": "ai",
|
| 249 |
+
"content": "Izvinite, ya ne совсем ponyal. Chto u vas bolit? Mozhete opisat' подробнее?",
|
| 250 |
+
"timestamp": "2025-10-17T23:45:25.539787"
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"id": "msg_0036",
|
| 254 |
+
"case_id": "case_026",
|
| 255 |
+
"sender": "user",
|
| 256 |
+
"content": "how are you, bro?",
|
| 257 |
+
"timestamp": "2025-10-17T23:45:37.716052"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"id": "msg_0037",
|
| 261 |
+
"case_id": "case_026",
|
| 262 |
+
"sender": "ai",
|
| 263 |
+
"content": "I am here to assist you with any medical concerns. If you have any symptoms or need help, please let me know.",
|
| 264 |
+
"timestamp": "2025-10-17T23:45:40.043228"
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"id": "msg_0038",
|
| 268 |
+
"case_id": "case_026",
|
| 269 |
+
"sender": "ai",
|
| 270 |
+
"content": "I am here to assist you with any medical concerns. If you have any symptoms or need help, please let me know.",
|
| 271 |
+
"timestamp": "2025-10-17T23:45:40.151290"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"id": "msg_0039",
|
| 275 |
+
"case_id": "case_026",
|
| 276 |
+
"sender": "user",
|
| 277 |
+
"content": "meni qornim og'riyapti",
|
| 278 |
+
"timestamp": "2025-10-17T23:47:26.884035"
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"id": "msg_0040",
|
| 282 |
+
"case_id": "case_026",
|
| 283 |
+
"sender": "ai",
|
| 284 |
+
"content": "Tushundim. Qorningiz og'riyapti ekan. Bu jiddiy holat emas. Agar og'riq kuchli bo'lmasa, uyda davolanish mumkin. Shifokorga murojaat qilishni xohlaysizmi? Davlat poliklinikasiga (bepul) yoki xususiy klinikaga (tezroq, pullik) borishni tavsiya qila olaman.",
|
| 285 |
+
"timestamp": "2025-10-17T23:47:29.541067"
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"id": "msg_0041",
|
| 289 |
+
"case_id": "case_026",
|
| 290 |
+
"sender": "ai",
|
| 291 |
+
"content": "Tushundim. Qorningiz og'riyapti ekan. Bu jiddiy holat emas. Agar og'riq kuchli bo'lmasa, uyda davolanish mumkin. Shifokorga murojaat qilishni xohlaysizmi? Davlat poliklinikasiga (bepul) yoki xususiy klinikaga (tezroq, pullik) borishni tavsiya qila olaman.",
|
| 292 |
+
"timestamp": "2025-10-17T23:47:29.615067"
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"id": "msg_0042",
|
| 296 |
+
"case_id": "case_026",
|
| 297 |
+
"sender": "ai",
|
| 298 |
+
"content": "Mana sizga tavsiyalar:\n\n\n🏥 DAVLAT POLIKLINIKALARI (Bepul):\n\n\n1️⃣ Mirzo Ulug'bek 3-sonli Poliklinika\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 12\n 📞 +998712345681\n ⏰ 08:00 - 17:00\n ⭐ 4.2/5.0\n\n\n🏥 XUSUSIY KLINIKALAR:\n\n\n1️⃣ Asia Medical Center\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 89\n 📞 +998901234570\n ⏰ 08:00 - 22:00\n 💰 250,000 - 800,000 so'm\n ⭐ 4.9/5.0\n\n2️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0",
|
| 299 |
+
"timestamp": "2025-10-17T23:47:30.352027"
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"id": "msg_0043",
|
| 303 |
+
"case_id": "case_026",
|
| 304 |
+
"sender": "user",
|
| 305 |
+
"content": "qanaqa xususiy krillikka tavsiya bera olasan?",
|
| 306 |
+
"timestamp": "2025-10-17T23:48:03.930963"
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"id": "msg_0044",
|
| 310 |
+
"case_id": "case_026",
|
| 311 |
+
"sender": "ai",
|
| 312 |
+
"content": "Sizga Mirzo Ulug'bek tumanidagi Asia Medical Center va Chilonzor tumanidagi MedLife klinikalarini tavsiya qila olaman. Ularning manzillari, telefon raqamlari va narxlari haqida ma'lumotlar yuqorida keltirilgan. Yana qanday ma'lumot kerak?",
|
| 313 |
+
"timestamp": "2025-10-17T23:48:06.757085"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"id": "msg_0045",
|
| 317 |
+
"case_id": "case_026",
|
| 318 |
+
"sender": "ai",
|
| 319 |
+
"content": "Sizga Mirzo Ulug'bek tumanidagi Asia Medical Center va Chilonzor tumanidagi MedLife klinikalarini tavsiya qila olaman. Ularning manzillari, telefon raqamlari va narxlari haqida ma'lumotlar yuqorida keltirilgan. Yana qanday ma'lumot kerak?",
|
| 320 |
+
"timestamp": "2025-10-17T23:48:06.831407"
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"id": "msg_0046",
|
| 324 |
+
"case_id": "case_026",
|
| 325 |
+
"sender": "ai",
|
| 326 |
+
"content": "Mana sizga tavsiyalar:\n\n\n\n🏥 XUSUSIY KLINIKALAR:\n\n\n1️⃣ Asia Medical Center\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 89\n 📞 +998901234570\n ⏰ 08:00 - 22:00\n 💰 250,000 - 800,000 so'm\n ⭐ 4.9/5.0\n\n2️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0",
|
| 327 |
+
"timestamp": "2025-10-17T23:48:07.495140"
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"id": "msg_0047",
|
| 331 |
+
"case_id": "case_026",
|
| 332 |
+
"sender": "user",
|
| 333 |
+
"content": "shu gaplarni inglizcha aytib ber tushunmadim",
|
| 334 |
+
"timestamp": "2025-10-17T23:48:38.374139"
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"id": "msg_0048",
|
| 338 |
+
"case_id": "case_026",
|
| 339 |
+
"sender": "ai",
|
| 340 |
+
"content": "I can recommend Asia Medical Center in Mirzo Ulugbek district and MedLife clinic in Chilonzor district. The addresses, phone numbers, and price ranges are listed above. What other information do you need?",
|
| 341 |
+
"timestamp": "2025-10-17T23:48:41.020117"
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"id": "msg_0049",
|
| 345 |
+
"case_id": "case_026",
|
| 346 |
+
"sender": "ai",
|
| 347 |
+
"content": "I can recommend Asia Medical Center in Mirzo Ulugbek district and MedLife clinic in Chilonzor district. The addresses, phone numbers, and price ranges are listed above. What other information do you need?",
|
| 348 |
+
"timestamp": "2025-10-17T23:48:41.100139"
|
| 349 |
+
},
|
| 350 |
+
{
|
| 351 |
+
"id": "msg_0050",
|
| 352 |
+
"case_id": "case_026",
|
| 353 |
+
"sender": "ai",
|
| 354 |
+
"content": "Here are my recommendations:\n\n\n\n🏥 PRIVATE CLINICS:\n\n\n1️⃣ Asia Medical Center\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 89\n 📞 +998901234570\n ⏰ 08:00 - 22:00\n 💰 250,000 - 800,000 so'm\n ⭐ 4.9/5.0\n\n2️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0",
|
| 355 |
+
"timestamp": "2025-10-17T23:48:41.617800"
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"id": "msg_0051",
|
| 359 |
+
"case_id": "case_027",
|
| 360 |
+
"sender": "user",
|
| 361 |
+
"content": "umine palet carlo",
|
| 362 |
+
"timestamp": "2025-10-17T23:50:47.629494"
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
"id": "msg_0052",
|
| 366 |
+
"case_id": "case_027",
|
| 367 |
+
"sender": "ai",
|
| 368 |
+
"content": "Kechirasiz, lekin men sizni tushunmadim. Iltimos, muammoingizni aniqroq bayon qiling. Nima bezovta qilyapti?",
|
| 369 |
+
"timestamp": "2025-10-17T23:50:50.182534"
|
| 370 |
+
},
|
| 371 |
+
{
|
| 372 |
+
"id": "msg_0053",
|
| 373 |
+
"case_id": "case_027",
|
| 374 |
+
"sender": "ai",
|
| 375 |
+
"content": "Kechirasiz, lekin men sizni tushunmadim. Iltimos, muammoingizni aniqroq bayon qiling. Nima bezovta qilyapti?",
|
| 376 |
+
"timestamp": "2025-10-17T23:50:50.293797"
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"id": "msg_0054",
|
| 380 |
+
"case_id": "case_027",
|
| 381 |
+
"sender": "user",
|
| 382 |
+
"content": "uminiya balet garlo",
|
| 383 |
+
"timestamp": "2025-10-17T23:51:04.151797"
|
| 384 |
+
},
|
| 385 |
+
{
|
| 386 |
+
"id": "msg_0055",
|
| 387 |
+
"case_id": "case_027",
|
| 388 |
+
"sender": "ai",
|
| 389 |
+
"content": "Kechirasiz, lekin men sizni tushunmadim. Iltimos, muammoingizni aniqroq bayon qiling. Nima bezovta qilyapti?",
|
| 390 |
+
"timestamp": "2025-10-17T23:51:07.843554"
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"id": "msg_0056",
|
| 394 |
+
"case_id": "case_027",
|
| 395 |
+
"sender": "ai",
|
| 396 |
+
"content": "Kechirasiz, lekin men sizni tushunmadim. Iltimos, muammoingizni aniqroq bayon qiling. Nima bezovta qilyapti?",
|
| 397 |
+
"timestamp": "2025-10-17T23:51:07.959922"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"id": "msg_0057",
|
| 401 |
+
"case_id": "case_028",
|
| 402 |
+
"sender": "user",
|
| 403 |
+
"content": "salom",
|
| 404 |
+
"timestamp": "2025-10-18T00:09:46.211174"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"id": "msg_0058",
|
| 408 |
+
"case_id": "case_028",
|
| 409 |
+
"sender": "ai",
|
| 410 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 411 |
+
"timestamp": "2025-10-18T00:09:49.284467"
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
"id": "msg_0059",
|
| 415 |
+
"case_id": "case_028",
|
| 416 |
+
"sender": "ai",
|
| 417 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 418 |
+
"timestamp": "2025-10-18T00:09:49.394077"
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
"id": "msg_0060",
|
| 422 |
+
"case_id": "case_028",
|
| 423 |
+
"sender": "user",
|
| 424 |
+
"content": "uminiya balet karlov. uminiya balet karlov.",
|
| 425 |
+
"timestamp": "2025-10-18T00:10:07.637650"
|
| 426 |
+
},
|
| 427 |
+
{
|
| 428 |
+
"id": "msg_0061",
|
| 429 |
+
"case_id": "case_028",
|
| 430 |
+
"sender": "ai",
|
| 431 |
+
"content": "Извините, я не совсем понимаю. Пожалуйста, уточните, что именно у вас болит?",
|
| 432 |
+
"timestamp": "2025-10-18T00:10:09.771626"
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"id": "msg_0062",
|
| 436 |
+
"case_id": "case_028",
|
| 437 |
+
"sender": "ai",
|
| 438 |
+
"content": "Извините, я не совсем понимаю. Пожалуйста, уточните, что именно у вас болит?",
|
| 439 |
+
"timestamp": "2025-10-18T00:10:09.875618"
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
"id": "msg_0063",
|
| 443 |
+
"case_id": "case_028",
|
| 444 |
+
"sender": "user",
|
| 445 |
+
"content": "prosta tak bolit vi ponimayete",
|
| 446 |
+
"timestamp": "2025-10-18T00:10:22.601250"
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
"id": "msg_0064",
|
| 450 |
+
"case_id": "case_028",
|
| 451 |
+
"sender": "ai",
|
| 452 |
+
"content": "Я понимаю, что у вас просто так болит, но мне нужно больше информации, чтобы понять, насколько это серьезно. Где именно болит? Это острая боль или тупая? Есть ли еще какие-то симптомы?",
|
| 453 |
+
"timestamp": "2025-10-18T00:10:24.738337"
|
| 454 |
+
},
|
| 455 |
+
{
|
| 456 |
+
"id": "msg_0065",
|
| 457 |
+
"case_id": "case_028",
|
| 458 |
+
"sender": "ai",
|
| 459 |
+
"content": "Я понимаю, что у вас просто так болит, но мне нужно больше информации, чтобы понять, насколько это серьезно. Где именно болит? Это острая боль или тупая? Есть ли еще какие-то симптомы?",
|
| 460 |
+
"timestamp": "2025-10-18T00:10:24.848283"
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"id": "msg_0066",
|
| 464 |
+
"case_id": "case_028",
|
| 465 |
+
"sender": "user",
|
| 466 |
+
"content": "emm, eshyo sepdimi? mhm. net eshyo sepdimi? u menya balet karniy.",
|
| 467 |
+
"timestamp": "2025-10-18T00:10:51.262681"
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"id": "msg_0067",
|
| 471 |
+
"case_id": "case_028",
|
| 472 |
+
"sender": "ai",
|
| 473 |
+
"content": "Простите, я все еще не совсем понимаю. Вы говорите, что у вас болит в животе? И есть ли у вас сыпь? Пожалуйста, ответьте на эти два вопроса.",
|
| 474 |
+
"timestamp": "2025-10-18T00:10:54.152858"
|
| 475 |
+
},
|
| 476 |
+
{
|
| 477 |
+
"id": "msg_0068",
|
| 478 |
+
"case_id": "case_028",
|
| 479 |
+
"sender": "ai",
|
| 480 |
+
"content": "Мне сложно вас понять. Соединяю с оператором, который вам поможет...",
|
| 481 |
+
"timestamp": "2025-10-18T00:10:54.275827"
|
| 482 |
+
},
|
| 483 |
+
{
|
| 484 |
+
"id": "msg_0069",
|
| 485 |
+
"case_id": "case_028",
|
| 486 |
+
"sender": "user",
|
| 487 |
+
"content": "what are you doing bro",
|
| 488 |
+
"timestamp": "2025-10-18T00:11:08.396768"
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"id": "msg_0070",
|
| 492 |
+
"case_id": "case_028",
|
| 493 |
+
"sender": "ai",
|
| 494 |
+
"content": "I am having difficulty understanding you. I will connect you to a human operator who can assist you better.",
|
| 495 |
+
"timestamp": "2025-10-18T00:11:10.587782"
|
| 496 |
+
},
|
| 497 |
+
{
|
| 498 |
+
"id": "msg_0071",
|
| 499 |
+
"case_id": "case_028",
|
| 500 |
+
"sender": "ai",
|
| 501 |
+
"content": "I'm having trouble understanding you. Connecting you to an operator who can help...",
|
| 502 |
+
"timestamp": "2025-10-18T00:11:10.713237"
|
| 503 |
+
},
|
| 504 |
+
{
|
| 505 |
+
"id": "msg_0072",
|
| 506 |
+
"case_id": "case_028",
|
| 507 |
+
"sender": "user",
|
| 508 |
+
"content": "menga qanaqadir ma'lumot bera olsam parokinika xususiy parokinika",
|
| 509 |
+
"timestamp": "2025-10-18T00:11:23.427681"
|
| 510 |
+
},
|
| 511 |
+
{
|
| 512 |
+
"id": "msg_0073",
|
| 513 |
+
"case_id": "case_028",
|
| 514 |
+
"sender": "ai",
|
| 515 |
+
"content": "Sizni tushunmadim. Iltimos, muammongizni aniqroq ayting. Nima shikoyatingiz bor? Keyin xususiy poliklinika haqida ma'lumot beraman.",
|
| 516 |
+
"timestamp": "2025-10-18T00:11:26.062862"
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"id": "msg_0074",
|
| 520 |
+
"case_id": "case_028",
|
| 521 |
+
"sender": "ai",
|
| 522 |
+
"content": "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi...",
|
| 523 |
+
"timestamp": "2025-10-18T00:11:26.164238"
|
| 524 |
+
},
|
| 525 |
+
{
|
| 526 |
+
"id": "msg_0075",
|
| 527 |
+
"case_id": "case_029",
|
| 528 |
+
"sender": "user",
|
| 529 |
+
"content": "iltimos menga yordam ber men o'lim holatidaman",
|
| 530 |
+
"timestamp": "2025-10-18T00:12:40.903419"
|
| 531 |
+
},
|
| 532 |
+
{
|
| 533 |
+
"id": "msg_0076",
|
| 534 |
+
"case_id": "case_029",
|
| 535 |
+
"sender": "ai",
|
| 536 |
+
"content": "Men sizga yordam berishni xohlayman. Iltimos, o'zingizni qanday his qilayotganingizni aniqroq ayting. Nima bo'ldi?",
|
| 537 |
+
"timestamp": "2025-10-18T00:12:44.482827"
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"id": "msg_0077",
|
| 541 |
+
"case_id": "case_029",
|
| 542 |
+
"sender": "ai",
|
| 543 |
+
"content": "Men sizga yordam berishni xohlayman. Iltimos, o'zingizni qanday his qilayotganingizni aniqroq ayting. Nima bo'ldi?",
|
| 544 |
+
"timestamp": "2025-10-18T00:12:44.585795"
|
| 545 |
+
},
|
| 546 |
+
{
|
| 547 |
+
"id": "msg_0078",
|
| 548 |
+
"case_id": "case_029",
|
| 549 |
+
"sender": "user",
|
| 550 |
+
"content": "men o'lim holatidaman, qon ketyapti qon",
|
| 551 |
+
"timestamp": "2025-10-18T00:12:59.591519"
|
| 552 |
+
},
|
| 553 |
+
{
|
| 554 |
+
"id": "msg_0079",
|
| 555 |
+
"case_id": "case_029",
|
| 556 |
+
"sender": "ai",
|
| 557 |
+
"content": "Bu juda jiddiy! Tezda yordam yuboramiz. Qaysi tumanda yashaysiz?",
|
| 558 |
+
"timestamp": "2025-10-18T00:13:02.251046"
|
| 559 |
+
},
|
| 560 |
+
{
|
| 561 |
+
"id": "msg_0080",
|
| 562 |
+
"case_id": "case_029",
|
| 563 |
+
"sender": "ai",
|
| 564 |
+
"content": "Bu juda jiddiy! Tezda yordam yuboramiz. Qaysi tumanda yashaysiz?",
|
| 565 |
+
"timestamp": "2025-10-18T00:13:02.328974"
|
| 566 |
+
},
|
| 567 |
+
{
|
| 568 |
+
"id": "msg_0081",
|
| 569 |
+
"case_id": "case_029",
|
| 570 |
+
"sender": "user",
|
| 571 |
+
"content": "shayxontohir tumani",
|
| 572 |
+
"timestamp": "2025-10-18T00:13:11.806664"
|
| 573 |
+
},
|
| 574 |
+
{
|
| 575 |
+
"id": "msg_0082",
|
| 576 |
+
"case_id": "case_029",
|
| 577 |
+
"sender": "ai",
|
| 578 |
+
"content": "Tushundim. Shayxontohir tumanida yashashingizni aytdingiz. Iltimos, qaysi mahallada yashaysiz?",
|
| 579 |
+
"timestamp": "2025-10-18T00:13:14.194005"
|
| 580 |
+
},
|
| 581 |
+
{
|
| 582 |
+
"id": "msg_0083",
|
| 583 |
+
"case_id": "case_029",
|
| 584 |
+
"sender": "ai",
|
| 585 |
+
"content": "Tushundim. Shayxontohir tumanida yashashingizni aytdingiz. Iltimos, qaysi mahallada yashaysiz?",
|
| 586 |
+
"timestamp": "2025-10-18T00:13:14.274533"
|
| 587 |
+
},
|
| 588 |
+
{
|
| 589 |
+
"id": "msg_0084",
|
| 590 |
+
"case_id": "case_029",
|
| 591 |
+
"sender": "user",
|
| 592 |
+
"content": "kamolon mahallasi, kamolon.",
|
| 593 |
+
"timestamp": "2025-10-18T00:13:25.737473"
|
| 594 |
+
},
|
| 595 |
+
{
|
| 596 |
+
"id": "msg_0085",
|
| 597 |
+
"case_id": "case_029",
|
| 598 |
+
"sender": "ai",
|
| 599 |
+
"content": "Tez yordam brigadasi Shayxontohir tumani, Kamolon mahallasiga jo'natildi. Brigada yo'lda. To'liq ism-familiyangizni aytib bering.",
|
| 600 |
+
"timestamp": "2025-10-18T00:13:28.488036"
|
| 601 |
+
},
|
| 602 |
+
{
|
| 603 |
+
"id": "msg_0086",
|
| 604 |
+
"case_id": "case_029",
|
| 605 |
+
"sender": "ai",
|
| 606 |
+
"content": "Tushundim, Shayxontohur tumani. Iltimos, mahallangizni ayting.",
|
| 607 |
+
"timestamp": "2025-10-18T00:13:28.635324"
|
| 608 |
+
},
|
| 609 |
+
{
|
| 610 |
+
"id": "msg_0087",
|
| 611 |
+
"case_id": "case_029",
|
| 612 |
+
"sender": "user",
|
| 613 |
+
"content": "ha, mahallam kamolon. kamolon mahallasida yashayman.",
|
| 614 |
+
"timestamp": "2025-10-18T00:13:48.097169"
|
| 615 |
+
},
|
| 616 |
+
{
|
| 617 |
+
"id": "msg_0088",
|
| 618 |
+
"case_id": "case_029",
|
| 619 |
+
"sender": "ai",
|
| 620 |
+
"content": "Brigada yo'lda, taxminan 1 daqiqada yetib keladi. Iltimos, to'liq ism-familiyangizni ayting.",
|
| 621 |
+
"timestamp": "2025-10-18T00:13:48.316423"
|
| 622 |
+
},
|
| 623 |
+
{
|
| 624 |
+
"id": "msg_0089",
|
| 625 |
+
"case_id": "case_029",
|
| 626 |
+
"sender": "ai",
|
| 627 |
+
"content": "Brigada yo'lda, taxminan 1 daqiqada yetib keladi. Iltimos, to'liq ism-familiyangizni ayting.",
|
| 628 |
+
"timestamp": "2025-10-18T00:13:48.339092"
|
| 629 |
+
},
|
| 630 |
+
{
|
| 631 |
+
"id": "msg_0090",
|
| 632 |
+
"case_id": "case_029",
|
| 633 |
+
"sender": "user",
|
| 634 |
+
"content": "ubaydullayev g'iyosiddin botir o'g'li",
|
| 635 |
+
"timestamp": "2025-10-18T00:14:09.650045"
|
| 636 |
+
},
|
| 637 |
+
{
|
| 638 |
+
"id": "msg_0091",
|
| 639 |
+
"case_id": "case_029",
|
| 640 |
+
"sender": "ai",
|
| 641 |
+
"content": "Rahmat, ubaydullayev g'iyosiddin botir o'g'li. Brigada tez orada yetib keladi. Iltimos, xotirjam bo'ling.",
|
| 642 |
+
"timestamp": "2025-10-18T00:14:09.735958"
|
| 643 |
+
},
|
| 644 |
+
{
|
| 645 |
+
"id": "msg_0092",
|
| 646 |
+
"case_id": "case_030",
|
| 647 |
+
"sender": "user",
|
| 648 |
+
"content": "menga yordam kerak. sir ro'shnem.",
|
| 649 |
+
"timestamp": "2025-10-18T00:15:18.769767"
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"id": "msg_0093",
|
| 653 |
+
"case_id": "case_030",
|
| 654 |
+
"sender": "ai",
|
| 655 |
+
"content": "Assalomu alaykum! Men Help.me tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Iltimos, muammoingizni aniqroq tushuntirib bering, nima bezovta qilyapti?",
|
| 656 |
+
"timestamp": "2025-10-18T00:15:21.642066"
|
| 657 |
+
},
|
| 658 |
+
{
|
| 659 |
+
"id": "msg_0094",
|
| 660 |
+
"case_id": "case_030",
|
| 661 |
+
"sender": "ai",
|
| 662 |
+
"content": "Assalomu alaykum! Men Help.me tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Iltimos, muammoingizni aniqroq tushuntirib bering, nima bezovta qilyapti?",
|
| 663 |
+
"timestamp": "2025-10-18T00:15:21.760683"
|
| 664 |
+
},
|
| 665 |
+
{
|
| 666 |
+
"id": "msg_0095",
|
| 667 |
+
"case_id": "case_030",
|
| 668 |
+
"sender": "user",
|
| 669 |
+
"content": "meni qornim qotib qolgan, ichim qotib qolgan. hojizga borsam chiqmayapti, shuni nima qilsam bo'ladi?",
|
| 670 |
+
"timestamp": "2025-10-18T00:15:46.612036"
|
| 671 |
+
},
|
| 672 |
+
{
|
| 673 |
+
"id": "msg_0096",
|
| 674 |
+
"case_id": "case_030",
|
| 675 |
+
"sender": "ai",
|
| 676 |
+
"content": "Tushundim. Bu muammo bilan tez yordam chaqirish shart emas. Sizga shifokor konsultatsiyasi tavsiya qilaman. Davlat poliklinikasiga (bepul) yoki xususiy klinikaga (tezroq, pullik) murojaat qilishni xohlaysizmi?",
|
| 677 |
+
"timestamp": "2025-10-18T00:15:49.200738"
|
| 678 |
+
},
|
| 679 |
+
{
|
| 680 |
+
"id": "msg_0097",
|
| 681 |
+
"case_id": "case_030",
|
| 682 |
+
"sender": "ai",
|
| 683 |
+
"content": "Tushundim. Bu muammo bilan tez yordam chaqirish shart emas. Sizga shifokor konsultatsiyasi tavsiya qilaman. Davlat poliklinikasiga (bepul) yoki xususiy klinikaga (tezroq, pullik) murojaat qilishni xohlaysizmi?",
|
| 684 |
+
"timestamp": "2025-10-18T00:15:49.287720"
|
| 685 |
+
},
|
| 686 |
+
{
|
| 687 |
+
"id": "msg_0098",
|
| 688 |
+
"case_id": "case_030",
|
| 689 |
+
"sender": "ai",
|
| 690 |
+
"content": "Mana sizga tavsiyalar:\n\n\n🏥 DAVLAT POLIKLINIKALARI (Bepul):\n\n\n1️⃣ Mirzo Ulug'bek 3-sonli Poliklinika\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 12\n 📞 +998712345681\n ⏰ 08:00 - 17:00\n ⭐ 4.2/5.0\n\n\n🏥 XUSUSIY KLINIKALAR:\n\n\n1️⃣ Asia Medical Center\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 89\n 📞 +998901234570\n ⏰ 08:00 - 22:00\n 💰 250,000 - 800,000 so'm\n ⭐ 4.9/5.0\n\n2️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0",
|
| 691 |
+
"timestamp": "2025-10-18T00:15:49.979913"
|
| 692 |
+
},
|
| 693 |
+
{
|
| 694 |
+
"id": "msg_0099",
|
| 695 |
+
"case_id": "case_030",
|
| 696 |
+
"sender": "user",
|
| 697 |
+
"content": "eh, xususiy klinika yaxshi menga. ayt, qaysi biri yaxshi?",
|
| 698 |
+
"timestamp": "2025-10-18T00:16:17.057391"
|
| 699 |
+
},
|
| 700 |
+
{
|
| 701 |
+
"id": "msg_0100",
|
| 702 |
+
"case_id": "case_030",
|
| 703 |
+
"sender": "ai",
|
| 704 |
+
"content": "Tushundim. Agar xususiy klinikaga bormoqchi bo'lsangiz, men sizga ikkita variantni tavsiya qilaman. Asia Medical Center Mirzo Ulug'bek tumanida joylashgan va yuqori baholarga ega. Yoki MedLife Chilonzor tumanida joylashgan, u ham yaxshi baholangan. Ikkalasida ham gastroenterologlar bor.",
|
| 705 |
+
"timestamp": "2025-10-18T00:16:20.277257"
|
| 706 |
+
},
|
| 707 |
+
{
|
| 708 |
+
"id": "msg_0101",
|
| 709 |
+
"case_id": "case_030",
|
| 710 |
+
"sender": "ai",
|
| 711 |
+
"content": "Tushundim. Agar xususiy klinikaga bormoqchi bo'lsangiz, men sizga ikkita variantni tavsiya qilaman. Asia Medical Center Mirzo Ulug'bek tumanida joylashgan va yuqori baholarga ega. Yoki MedLife Chilonzor tumanida joylashgan, u ham yaxshi baholangan. Ikkalasida ham gastroenterologlar bor.",
|
| 712 |
+
"timestamp": "2025-10-18T00:16:20.370769"
|
| 713 |
+
},
|
| 714 |
+
{
|
| 715 |
+
"id": "msg_0102",
|
| 716 |
+
"case_id": "case_030",
|
| 717 |
+
"sender": "ai",
|
| 718 |
+
"content": "Mana sizga tavsiyalar:\n\n\n\n🏥 XUSUSIY KLINIKALAR:\n\n\n1️⃣ Asia Medical Center\n 📍 Mirzo Ulug'bek tumani, Osiyo ko'chasi 89\n 📞 +998901234570\n ⏰ 08:00 - 22:00\n 💰 250,000 - 800,000 so'm\n ⭐ 4.9/5.0\n\n2️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0",
|
| 719 |
+
"timestamp": "2025-10-18T00:16:21.054362"
|
| 720 |
+
},
|
| 721 |
+
{
|
| 722 |
+
"id": "msg_0103",
|
| 723 |
+
"case_id": "case_030",
|
| 724 |
+
"sender": "user",
|
| 725 |
+
"content": "menga o'sha doktorga doktorning qabuliga yozdirib qo'y, boraman hozir.",
|
| 726 |
+
"timestamp": "2025-10-18T00:17:03.561218"
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"id": "msg_0104",
|
| 730 |
+
"case_id": "case_030",
|
| 731 |
+
"sender": "ai",
|
| 732 |
+
"content": "Kechirasiz, men hozircha doktorning qabuliga yozdira olmayman. Lekin, sizga yuqoridagi klinikalarning telefon raqamlarini berdim, ularga qo'ng'iroq qilib, qabulga yozilish uchun o'zingiz murojaat qilishingiz mumkin.",
|
| 733 |
+
"timestamp": "2025-10-18T00:17:06.225745"
|
| 734 |
+
},
|
| 735 |
+
{
|
| 736 |
+
"id": "msg_0105",
|
| 737 |
+
"case_id": "case_030",
|
| 738 |
+
"sender": "ai",
|
| 739 |
+
"content": "Kechirasiz, men hozircha doktorning qabuliga yozdira olmayman. Lekin, sizga yuqoridagi klinikalarning telefon raqamlarini berdim, ularga qo'ng'iroq qilib, qabulga yozilish uchun o'zingiz murojaat qilishingiz mumkin.",
|
| 740 |
+
"timestamp": "2025-10-18T00:17:06.303361"
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"id": "msg_0106",
|
| 744 |
+
"case_id": "case_030",
|
| 745 |
+
"sender": "ai",
|
| 746 |
+
"content": "Mana sizga tavsiyalar:\n\n\n\n🏥 XUSUSIY KLINIKALAR:\n\n\n1️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0\n\n2️⃣ Healthy Family Klinikasi\n 📍 Yakkasaroy tumani, Shota Rustaveli ko'chasi 78\n 📞 +998901234569\n ⏰ 09:00 - 19:00\n 💰 100,000 - 400,000 so'm\n ⭐ 4.6/5.0\n\n3️⃣ Express Medical Clinic\n 📍 Uchtepa tumani, Qoratosh ko'chasi 67\n 📞 +998901234574\n ⏰ 08:00 - 20:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.6/5.0\n\n4️⃣ City Medical Clinic\n 📍 Shayxontohur tumani, Furqat ko'chasi 123\n 📞 +998901234571\n ⏰ 09:00 - 20:00\n 💰 120,000 - 450,000 so'm\n ⭐ 4.5/5.0\n\n5️⃣ New Life Medical Center\n 📍 Sergeli tumani, Yangi Sergeli ko'chasi 89\n 📞 +998901234573\n ⏰ 09:00 - 19:00\n 💰 100,000 - 350,000 so'm\n ⭐ 4.4/5.0",
|
| 747 |
+
"timestamp": "2025-10-18T00:17:06.860546"
|
| 748 |
+
},
|
| 749 |
+
{
|
| 750 |
+
"id": "msg_0107",
|
| 751 |
+
"case_id": "case_030",
|
| 752 |
+
"sender": "user",
|
| 753 |
+
"content": "men ubaydullayev oyosiddin botir o'g'liman.",
|
| 754 |
+
"timestamp": "2025-10-18T00:18:19.125882"
|
| 755 |
+
},
|
| 756 |
+
{
|
| 757 |
+
"id": "msg_0108",
|
| 758 |
+
"case_id": "case_030",
|
| 759 |
+
"sender": "ai",
|
| 760 |
+
"content": "Faqatgina klinikaga qo'ng'iroq qilib doktorda qabulingizni belgilashingiz mumkin. Telefon raqamlar ularning tavsifida.",
|
| 761 |
+
"timestamp": "2025-10-18T00:18:21.765631"
|
| 762 |
+
},
|
| 763 |
+
{
|
| 764 |
+
"id": "msg_0109",
|
| 765 |
+
"case_id": "case_030",
|
| 766 |
+
"sender": "ai",
|
| 767 |
+
"content": "Faqatgina klinikaga qo'ng'iroq qilib doktorda qabulingizni belgilashingiz mumkin. Telefon raqamlar ularning tavsifida.",
|
| 768 |
+
"timestamp": "2025-10-18T00:18:21.846998"
|
| 769 |
+
},
|
| 770 |
+
{
|
| 771 |
+
"id": "msg_0110",
|
| 772 |
+
"case_id": "case_030",
|
| 773 |
+
"sender": "ai",
|
| 774 |
+
"content": "Mana sizga tavsiyalar:\n\n\n\n🏥 XUSUSIY KLINIKALAR:\n\n\n1️⃣ MedLife Xususiy Klinika\n 📍 Chilonzor tumani, Kichik Chilonzor, Bunyodkor ko'chasi 45\n 📞 +998901234567\n ⏰ 09:00 - 21:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.8/5.0\n\n2️⃣ Healthy Family Klinikasi\n 📍 Yakkasaroy tumani, Shota Rustaveli ko'chasi 78\n 📞 +998901234569\n ⏰ 09:00 - 19:00\n 💰 100,000 - 400,000 so'm\n ⭐ 4.6/5.0\n\n3️⃣ Express Medical Clinic\n 📍 Uchtepa tumani, Qoratosh ko'chasi 67\n 📞 +998901234574\n ⏰ 08:00 - 20:00\n 💰 150,000 - 500,000 so'm\n ⭐ 4.6/5.0\n\n4️⃣ City Medical Clinic\n 📍 Shayxontohur tumani, Furqat ko'chasi 123\n 📞 +998901234571\n ⏰ 09:00 - 20:00\n 💰 120,000 - 450,000 so'm\n ⭐ 4.5/5.0\n\n5️⃣ New Life Medical Center\n 📍 Sergeli tumani, Yangi Sergeli ko'chasi 89\n 📞 +998901234573\n ⏰ 09:00 - 19:00\n 💰 100,000 - 350,000 so'm\n ⭐ 4.4/5.0",
|
| 775 |
+
"timestamp": "2025-10-18T00:18:22.393134"
|
| 776 |
+
},
|
| 777 |
+
{
|
| 778 |
+
"id": "msg_0111",
|
| 779 |
+
"case_id": "case_002",
|
| 780 |
+
"sender": "user",
|
| 781 |
+
"content": "hello",
|
| 782 |
+
"timestamp": "2025-10-18T07:12:16.861268"
|
| 783 |
+
},
|
| 784 |
+
{
|
| 785 |
+
"id": "msg_0112",
|
| 786 |
+
"case_id": "case_002",
|
| 787 |
+
"sender": "ai",
|
| 788 |
+
"content": "Hello! How can I help you today? What seems to be the problem?",
|
| 789 |
+
"timestamp": "2025-10-18T07:12:19.307506"
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
"id": "msg_0113",
|
| 793 |
+
"case_id": "case_002",
|
| 794 |
+
"sender": "ai",
|
| 795 |
+
"content": "Hello! How can I help you today? What seems to be the problem?",
|
| 796 |
+
"timestamp": "2025-10-18T07:12:19.355147"
|
| 797 |
+
},
|
| 798 |
+
{
|
| 799 |
+
"id": "msg_0114",
|
| 800 |
+
"case_id": "case_002",
|
| 801 |
+
"sender": "user",
|
| 802 |
+
"content": "uh, there is no problem. prosta, uh, m m, i have headache.",
|
| 803 |
+
"timestamp": "2025-10-18T07:12:36.675102"
|
| 804 |
+
},
|
| 805 |
+
{
|
| 806 |
+
"id": "msg_0115",
|
| 807 |
+
"case_id": "case_002",
|
| 808 |
+
"sender": "ai",
|
| 809 |
+
"content": "I understand. A headache isn't usually an emergency. I can recommend a doctor's consultation. Would you prefer a public clinic (free) or a private clinic (faster, but paid)?",
|
| 810 |
+
"timestamp": "2025-10-18T07:12:38.550187"
|
| 811 |
+
},
|
| 812 |
+
{
|
| 813 |
+
"id": "msg_0116",
|
| 814 |
+
"case_id": "case_002",
|
| 815 |
+
"sender": "ai",
|
| 816 |
+
"content": "I understand. A headache isn't usually an emergency. I can recommend a doctor's consultation. Would you prefer a public clinic (free) or a private clinic (faster, but paid)?",
|
| 817 |
+
"timestamp": "2025-10-18T07:12:38.592353"
|
| 818 |
+
},
|
| 819 |
+
{
|
| 820 |
+
"id": "msg_0117",
|
| 821 |
+
"case_id": "case_002",
|
| 822 |
+
"sender": "user",
|
| 823 |
+
"content": "menya yest balet karlo",
|
| 824 |
+
"timestamp": "2025-10-18T07:21:42.847147"
|
| 825 |
+
},
|
| 826 |
+
{
|
| 827 |
+
"id": "msg_0118",
|
| 828 |
+
"case_id": "case_002",
|
| 829 |
+
"sender": "ai",
|
| 830 |
+
"content": "Kechirasiz, men sizni tushunmadim. Iltimos, kasalligingizni boshqa so'zlar bilan tushuntirib bering. Bosh og'riyaptimi yoki boshqa narsa bezovta qilyaptimi?",
|
| 831 |
+
"timestamp": "2025-10-18T07:21:46.213577"
|
| 832 |
+
},
|
| 833 |
+
{
|
| 834 |
+
"id": "msg_0119",
|
| 835 |
+
"case_id": "case_002",
|
| 836 |
+
"sender": "ai",
|
| 837 |
+
"content": "Kechirasiz, men sizni tushunmadim. Iltimos, kasalligingizni boshqa so'zlar bilan tushuntirib bering. Bosh og'riyaptimi yoki boshqa narsa bezovta qilyaptimi?",
|
| 838 |
+
"timestamp": "2025-10-18T07:21:46.279351"
|
| 839 |
+
},
|
| 840 |
+
{
|
| 841 |
+
"id": "msg_0120",
|
| 842 |
+
"case_id": "case_002",
|
| 843 |
+
"sender": "user",
|
| 844 |
+
"content": "u menya polite karlo",
|
| 845 |
+
"timestamp": "2025-10-18T07:22:06.505300"
|
| 846 |
+
},
|
| 847 |
+
{
|
| 848 |
+
"id": "msg_0121",
|
| 849 |
+
"case_id": "case_002",
|
| 850 |
+
"sender": "ai",
|
| 851 |
+
"content": "Kechirasiz, men sizni tushunmadim. Iltimos, aniqroq ayting, nima bezovta qilyapti? Bosh og'riyaptimi, haroratingiz bormi, yoki boshqa narsa?",
|
| 852 |
+
"timestamp": "2025-10-18T07:22:08.751432"
|
| 853 |
+
},
|
| 854 |
+
{
|
| 855 |
+
"id": "msg_0122",
|
| 856 |
+
"case_id": "case_002",
|
| 857 |
+
"sender": "ai",
|
| 858 |
+
"content": "Kechirasiz, men sizni tushunmadim. Iltimos, aniqroq ayting, nima bezovta qilyapti? Bosh og'riyaptimi, haroratingiz bormi, yoki boshqa narsa?",
|
| 859 |
+
"timestamp": "2025-10-18T07:22:08.824918"
|
| 860 |
+
},
|
| 861 |
+
{
|
| 862 |
+
"id": "msg_0123",
|
| 863 |
+
"case_id": "case_003",
|
| 864 |
+
"sender": "user",
|
| 865 |
+
"content": "salom",
|
| 866 |
+
"timestamp": "2025-10-18T10:46:26.371052"
|
| 867 |
+
},
|
| 868 |
+
{
|
| 869 |
+
"id": "msg_0124",
|
| 870 |
+
"case_id": "case_003",
|
| 871 |
+
"sender": "ai",
|
| 872 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 873 |
+
"timestamp": "2025-10-18T10:46:28.956226"
|
| 874 |
+
},
|
| 875 |
+
{
|
| 876 |
+
"id": "msg_0125",
|
| 877 |
+
"case_id": "case_003",
|
| 878 |
+
"sender": "ai",
|
| 879 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 880 |
+
"timestamp": "2025-10-18T10:46:29.004655"
|
| 881 |
+
},
|
| 882 |
+
{
|
| 883 |
+
"id": "msg_0126",
|
| 884 |
+
"case_id": "case_004",
|
| 885 |
+
"sender": "user",
|
| 886 |
+
"content": "salom",
|
| 887 |
+
"timestamp": "2025-10-18T11:15:35.888514"
|
| 888 |
+
},
|
| 889 |
+
{
|
| 890 |
+
"id": "msg_0127",
|
| 891 |
+
"case_id": "case_004",
|
| 892 |
+
"sender": "ai",
|
| 893 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 894 |
+
"timestamp": "2025-10-18T11:15:39.108972"
|
| 895 |
+
},
|
| 896 |
+
{
|
| 897 |
+
"id": "msg_0128",
|
| 898 |
+
"case_id": "case_004",
|
| 899 |
+
"sender": "ai",
|
| 900 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 901 |
+
"timestamp": "2025-10-18T11:15:39.143325"
|
| 902 |
+
},
|
| 903 |
+
{
|
| 904 |
+
"id": "msg_0129",
|
| 905 |
+
"case_id": "case_004",
|
| 906 |
+
"sender": "user",
|
| 907 |
+
"content": "salom",
|
| 908 |
+
"timestamp": "2025-10-18T11:16:16.101782"
|
| 909 |
+
},
|
| 910 |
+
{
|
| 911 |
+
"id": "msg_0130",
|
| 912 |
+
"case_id": "case_004",
|
| 913 |
+
"sender": "ai",
|
| 914 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 915 |
+
"timestamp": "2025-10-18T11:16:18.323380"
|
| 916 |
+
},
|
| 917 |
+
{
|
| 918 |
+
"id": "msg_0131",
|
| 919 |
+
"case_id": "case_004",
|
| 920 |
+
"sender": "ai",
|
| 921 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 922 |
+
"timestamp": "2025-10-18T11:16:18.358719"
|
| 923 |
+
},
|
| 924 |
+
{
|
| 925 |
+
"id": "msg_0132",
|
| 926 |
+
"case_id": "case_004",
|
| 927 |
+
"sender": "user",
|
| 928 |
+
"content": "menga yordam ber",
|
| 929 |
+
"timestamp": "2025-10-18T11:16:36.185615"
|
| 930 |
+
},
|
| 931 |
+
{
|
| 932 |
+
"id": "msg_0133",
|
| 933 |
+
"case_id": "case_004",
|
| 934 |
+
"sender": "ai",
|
| 935 |
+
"content": "Tushundim. Iltimos, aniqroq ayting: nima bezovta qilyapti? Qanday simptomlar bor?",
|
| 936 |
+
"timestamp": "2025-10-18T11:16:38.569199"
|
| 937 |
+
},
|
| 938 |
+
{
|
| 939 |
+
"id": "msg_0134",
|
| 940 |
+
"case_id": "case_004",
|
| 941 |
+
"sender": "ai",
|
| 942 |
+
"content": "Tushundim. Iltimos, aniqroq ayting: nima bezovta qilyapti? Qanday simptomlar bor?",
|
| 943 |
+
"timestamp": "2025-10-18T11:16:38.602269"
|
| 944 |
+
},
|
| 945 |
+
{
|
| 946 |
+
"id": "msg_0135",
|
| 947 |
+
"case_id": "case_004",
|
| 948 |
+
"sender": "user",
|
| 949 |
+
"content": "yaxshimisan?",
|
| 950 |
+
"timestamp": "2025-10-18T11:17:57.749812"
|
| 951 |
+
},
|
| 952 |
+
{
|
| 953 |
+
"id": "msg_0136",
|
| 954 |
+
"case_id": "case_004",
|
| 955 |
+
"sender": "ai",
|
| 956 |
+
"content": "Men yaxshiman, rahmat. Sizda nima muammo? O'zingizni qanday his qilyapsiz? Iltimos, shikoyatlaringizni batafsilroq ayting.",
|
| 957 |
+
"timestamp": "2025-10-18T11:18:00.099219"
|
| 958 |
+
},
|
| 959 |
+
{
|
| 960 |
+
"id": "msg_0137",
|
| 961 |
+
"case_id": "case_004",
|
| 962 |
+
"sender": "ai",
|
| 963 |
+
"content": "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi...",
|
| 964 |
+
"timestamp": "2025-10-18T11:18:00.188499"
|
| 965 |
+
},
|
| 966 |
+
{
|
| 967 |
+
"id": "msg_0138",
|
| 968 |
+
"case_id": "case_004",
|
| 969 |
+
"sender": "user",
|
| 970 |
+
"content": "salom.",
|
| 971 |
+
"timestamp": "2025-10-18T11:18:44.482058"
|
| 972 |
+
},
|
| 973 |
+
{
|
| 974 |
+
"id": "msg_0139",
|
| 975 |
+
"case_id": "case_004",
|
| 976 |
+
"sender": "ai",
|
| 977 |
+
"content": "Sizni yaxshi tushunolmadim. Operatorga ulayman, ular sizga yordam berishadi.",
|
| 978 |
+
"timestamp": "2025-10-18T11:18:46.288826"
|
| 979 |
+
},
|
| 980 |
+
{
|
| 981 |
+
"id": "msg_0140",
|
| 982 |
+
"case_id": "case_004",
|
| 983 |
+
"sender": "ai",
|
| 984 |
+
"content": "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi...",
|
| 985 |
+
"timestamp": "2025-10-18T11:18:46.372252"
|
| 986 |
+
},
|
| 987 |
+
{
|
| 988 |
+
"id": "msg_0141",
|
| 989 |
+
"case_id": "case_004",
|
| 990 |
+
"sender": "user",
|
| 991 |
+
"content": "yordam keladi.",
|
| 992 |
+
"timestamp": "2025-10-18T11:18:52.949780"
|
| 993 |
+
},
|
| 994 |
+
{
|
| 995 |
+
"id": "msg_0142",
|
| 996 |
+
"case_id": "case_004",
|
| 997 |
+
"sender": "ai",
|
| 998 |
+
"content": "Sizni tushunishga harakat qilyapman. Yordam kerakligini aytyapsiz, lekin nima bo'lganini aniq ayta olasizmi? Nima shikoyatingiz bor?",
|
| 999 |
+
"timestamp": "2025-10-18T11:18:55.462686"
|
| 1000 |
+
},
|
| 1001 |
+
{
|
| 1002 |
+
"id": "msg_0143",
|
| 1003 |
+
"case_id": "case_004",
|
| 1004 |
+
"sender": "ai",
|
| 1005 |
+
"content": "Sizni yaxshi tushunolmayapman. Operatorga ulayman, ular sizga yordam berishadi...",
|
| 1006 |
+
"timestamp": "2025-10-18T11:18:55.589620"
|
| 1007 |
+
},
|
| 1008 |
+
{
|
| 1009 |
+
"id": "msg_0144",
|
| 1010 |
+
"case_id": "case_005",
|
| 1011 |
+
"sender": "user",
|
| 1012 |
+
"content": "salom.",
|
| 1013 |
+
"timestamp": "2025-10-18T11:37:53.876793"
|
| 1014 |
+
},
|
| 1015 |
+
{
|
| 1016 |
+
"id": "msg_0145",
|
| 1017 |
+
"case_id": "case_005",
|
| 1018 |
+
"sender": "ai",
|
| 1019 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 1020 |
+
"timestamp": "2025-10-18T11:37:57.277703"
|
| 1021 |
+
},
|
| 1022 |
+
{
|
| 1023 |
+
"id": "msg_0146",
|
| 1024 |
+
"case_id": "case_005",
|
| 1025 |
+
"sender": "ai",
|
| 1026 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima bezovta qilyapti?",
|
| 1027 |
+
"timestamp": "2025-10-18T11:37:57.378895"
|
| 1028 |
+
},
|
| 1029 |
+
{
|
| 1030 |
+
"id": "msg_0147",
|
| 1031 |
+
"case_id": "case_006",
|
| 1032 |
+
"sender": "user",
|
| 1033 |
+
"content": "salom",
|
| 1034 |
+
"timestamp": "2025-10-18T11:45:43.518749"
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"id": "msg_0148",
|
| 1038 |
+
"case_id": "case_006",
|
| 1039 |
+
"sender": "ai",
|
| 1040 |
+
"content": "Salom! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 1041 |
+
"timestamp": "2025-10-18T11:45:45.979081"
|
| 1042 |
+
},
|
| 1043 |
+
{
|
| 1044 |
+
"id": "msg_0149",
|
| 1045 |
+
"case_id": "case_006",
|
| 1046 |
+
"sender": "ai",
|
| 1047 |
+
"content": "Salom! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 1048 |
+
"timestamp": "2025-10-18T11:45:46.093584"
|
| 1049 |
+
},
|
| 1050 |
+
{
|
| 1051 |
+
"id": "msg_0150",
|
| 1052 |
+
"case_id": "case_007",
|
| 1053 |
+
"sender": "user",
|
| 1054 |
+
"content": "hay, havo yu",
|
| 1055 |
+
"timestamp": "2025-10-18T12:00:00.699380"
|
| 1056 |
+
},
|
| 1057 |
+
{
|
| 1058 |
+
"id": "msg_0151",
|
| 1059 |
+
"case_id": "case_007",
|
| 1060 |
+
"sender": "ai",
|
| 1061 |
+
"content": "Salom! Sizga qanday yordam bera olaman? Biron narsa bezovta qilyaptimi?",
|
| 1062 |
+
"timestamp": "2025-10-18T12:00:04.057905"
|
| 1063 |
+
},
|
| 1064 |
+
{
|
| 1065 |
+
"id": "msg_0152",
|
| 1066 |
+
"case_id": "case_007",
|
| 1067 |
+
"sender": "ai",
|
| 1068 |
+
"content": "Salom! Sizga qanday yordam bera olaman? Biron narsa bezovta qilyaptimi?",
|
| 1069 |
+
"timestamp": "2025-10-18T12:00:04.158967"
|
| 1070 |
+
},
|
| 1071 |
+
{
|
| 1072 |
+
"id": "msg_0153",
|
| 1073 |
+
"case_id": "case_007",
|
| 1074 |
+
"sender": "user",
|
| 1075 |
+
"content": "hi, how are you?",
|
| 1076 |
+
"timestamp": "2025-10-18T12:00:11.670858"
|
| 1077 |
+
},
|
| 1078 |
+
{
|
| 1079 |
+
"id": "msg_0154",
|
| 1080 |
+
"case_id": "case_007",
|
| 1081 |
+
"sender": "ai",
|
| 1082 |
+
"content": "I'm here to help you with medical issues. Are you experiencing any symptoms or do you need medical assistance?",
|
| 1083 |
+
"timestamp": "2025-10-18T12:00:13.753608"
|
| 1084 |
+
},
|
| 1085 |
+
{
|
| 1086 |
+
"id": "msg_0155",
|
| 1087 |
+
"case_id": "case_007",
|
| 1088 |
+
"sender": "ai",
|
| 1089 |
+
"content": "I'm here to help you with medical issues. Are you experiencing any symptoms or do you need medical assistance?",
|
| 1090 |
+
"timestamp": "2025-10-18T12:00:13.875070"
|
| 1091 |
+
},
|
| 1092 |
+
{
|
| 1093 |
+
"id": "msg_0156",
|
| 1094 |
+
"case_id": "case_007",
|
| 1095 |
+
"sender": "user",
|
| 1096 |
+
"content": "what are you saying",
|
| 1097 |
+
"timestamp": "2025-10-18T12:00:27.128118"
|
| 1098 |
+
},
|
| 1099 |
+
{
|
| 1100 |
+
"id": "msg_0157",
|
| 1101 |
+
"case_id": "case_007",
|
| 1102 |
+
"sender": "ai",
|
| 1103 |
+
"content": "I'm here to help you with any medical concerns you might have. Can you describe what you're feeling or what kind of assistance you need?",
|
| 1104 |
+
"timestamp": "2025-10-18T12:00:29.628207"
|
| 1105 |
+
},
|
| 1106 |
+
{
|
| 1107 |
+
"id": "msg_0158",
|
| 1108 |
+
"case_id": "case_007",
|
| 1109 |
+
"sender": "ai",
|
| 1110 |
+
"content": "I'm here to help you with any medical concerns you might have. Can you describe what you're feeling or what kind of assistance you need?",
|
| 1111 |
+
"timestamp": "2025-10-18T12:00:29.733062"
|
| 1112 |
+
},
|
| 1113 |
+
{
|
| 1114 |
+
"id": "msg_0159",
|
| 1115 |
+
"case_id": "case_008",
|
| 1116 |
+
"sender": "user",
|
| 1117 |
+
"content": "salom",
|
| 1118 |
+
"timestamp": "2025-10-18T12:16:42.679032"
|
| 1119 |
+
},
|
| 1120 |
+
{
|
| 1121 |
+
"id": "msg_0160",
|
| 1122 |
+
"case_id": "case_008",
|
| 1123 |
+
"sender": "ai",
|
| 1124 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam berishim mumkin?",
|
| 1125 |
+
"timestamp": "2025-10-18T12:16:45.469390"
|
| 1126 |
+
},
|
| 1127 |
+
{
|
| 1128 |
+
"id": "msg_0161",
|
| 1129 |
+
"case_id": "case_008",
|
| 1130 |
+
"sender": "ai",
|
| 1131 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam berishim mumkin?",
|
| 1132 |
+
"timestamp": "2025-10-18T12:16:45.588305"
|
| 1133 |
+
},
|
| 1134 |
+
{
|
| 1135 |
+
"id": "msg_0162",
|
| 1136 |
+
"case_id": "case_008",
|
| 1137 |
+
"sender": "user",
|
| 1138 |
+
"content": "how are you bro",
|
| 1139 |
+
"timestamp": "2025-10-18T12:17:02.435515"
|
| 1140 |
+
},
|
| 1141 |
+
{
|
| 1142 |
+
"id": "msg_0163",
|
| 1143 |
+
"case_id": "case_008",
|
| 1144 |
+
"sender": "ai",
|
| 1145 |
+
"content": "I am here to help you with any medical concerns. Are you experiencing any symptoms or do you need assistance with something?",
|
| 1146 |
+
"timestamp": "2025-10-18T12:17:04.626478"
|
| 1147 |
+
},
|
| 1148 |
+
{
|
| 1149 |
+
"id": "msg_0164",
|
| 1150 |
+
"case_id": "case_008",
|
| 1151 |
+
"sender": "ai",
|
| 1152 |
+
"content": "I am here to help you with any medical concerns. Are you experiencing any symptoms or do you need assistance with something?",
|
| 1153 |
+
"timestamp": "2025-10-18T12:17:04.738044"
|
| 1154 |
+
},
|
| 1155 |
+
{
|
| 1156 |
+
"id": "msg_0165",
|
| 1157 |
+
"case_id": "case_009",
|
| 1158 |
+
"sender": "user",
|
| 1159 |
+
"content": "assalomu alaykum. vaalaykum assalom.",
|
| 1160 |
+
"timestamp": "2025-10-18T12:22:35.880941"
|
| 1161 |
+
},
|
| 1162 |
+
{
|
| 1163 |
+
"id": "msg_0166",
|
| 1164 |
+
"case_id": "case_009",
|
| 1165 |
+
"sender": "ai",
|
| 1166 |
+
"content": "Men \"Help.me\" tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 1167 |
+
"timestamp": "2025-10-18T12:22:38.834347"
|
| 1168 |
+
},
|
| 1169 |
+
{
|
| 1170 |
+
"id": "msg_0167",
|
| 1171 |
+
"case_id": "case_009",
|
| 1172 |
+
"sender": "ai",
|
| 1173 |
+
"content": "Men \"Help.me\" tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman?",
|
| 1174 |
+
"timestamp": "2025-10-18T12:22:38.966171"
|
| 1175 |
+
},
|
| 1176 |
+
{
|
| 1177 |
+
"id": "msg_0168",
|
| 1178 |
+
"case_id": "case_009",
|
| 1179 |
+
"sender": "user",
|
| 1180 |
+
"content": "meni yuragim og'riq otidi. shunga tez yordam chaqirib bera olasizlarmi?",
|
| 1181 |
+
"timestamp": "2025-10-18T12:23:01.433126"
|
| 1182 |
+
},
|
| 1183 |
+
{
|
| 1184 |
+
"id": "msg_0169",
|
| 1185 |
+
"case_id": "case_009",
|
| 1186 |
+
"sender": "ai",
|
| 1187 |
+
"content": "Bu jiddiy holat! Tezda yordam yuboramiz. Qaysi tumanda yashaysiz?",
|
| 1188 |
+
"timestamp": "2025-10-18T12:23:04.806685"
|
| 1189 |
+
},
|
| 1190 |
+
{
|
| 1191 |
+
"id": "msg_0170",
|
| 1192 |
+
"case_id": "case_009",
|
| 1193 |
+
"sender": "ai",
|
| 1194 |
+
"content": "Bu jiddiy holat! Tezda yordam yuboramiz. Qaysi tumanda yashaysiz?",
|
| 1195 |
+
"timestamp": "2025-10-18T12:23:04.887644"
|
| 1196 |
+
},
|
| 1197 |
+
{
|
| 1198 |
+
"id": "msg_0171",
|
| 1199 |
+
"case_id": "case_009",
|
| 1200 |
+
"sender": "user",
|
| 1201 |
+
"content": "olmazor tumanida",
|
| 1202 |
+
"timestamp": "2025-10-18T12:23:18.440004"
|
| 1203 |
+
},
|
| 1204 |
+
{
|
| 1205 |
+
"id": "msg_0172",
|
| 1206 |
+
"case_id": "case_009",
|
| 1207 |
+
"sender": "ai",
|
| 1208 |
+
"content": "Tushundim. Olmazor tumanida qaysi mahallada yashaysiz?",
|
| 1209 |
+
"timestamp": "2025-10-18T12:23:20.593784"
|
| 1210 |
+
},
|
| 1211 |
+
{
|
| 1212 |
+
"id": "msg_0173",
|
| 1213 |
+
"case_id": "case_009",
|
| 1214 |
+
"sender": "ai",
|
| 1215 |
+
"content": "Tushundim, Olmazor tumani. Iltimos, mahallangizni ayting.",
|
| 1216 |
+
"timestamp": "2025-10-18T12:23:20.738011"
|
| 1217 |
+
},
|
| 1218 |
+
{
|
| 1219 |
+
"id": "msg_0174",
|
| 1220 |
+
"case_id": "case_009",
|
| 1221 |
+
"sender": "user",
|
| 1222 |
+
"content": "mahalla abdulla qodiriy",
|
| 1223 |
+
"timestamp": "2025-10-18T12:23:35.258620"
|
| 1224 |
+
},
|
| 1225 |
+
{
|
| 1226 |
+
"id": "msg_0175",
|
| 1227 |
+
"case_id": "case_009",
|
| 1228 |
+
"sender": "user",
|
| 1229 |
+
"content": "kamolon mahallasi",
|
| 1230 |
+
"timestamp": "2025-10-18T12:40:18.132519"
|
| 1231 |
+
},
|
| 1232 |
+
{
|
| 1233 |
+
"id": "msg_0176",
|
| 1234 |
+
"case_id": "case_009",
|
| 1235 |
+
"sender": "ai",
|
| 1236 |
+
"content": "Tushundim. Brigada Olmazor tumani, Kamolon mahallasi manziliga yuborildi. Iltimos, to'liq ism-familiyangizni ayting.",
|
| 1237 |
+
"timestamp": "2025-10-18T12:40:21.570156"
|
| 1238 |
+
},
|
| 1239 |
+
{
|
| 1240 |
+
"id": "msg_0177",
|
| 1241 |
+
"case_id": "case_009",
|
| 1242 |
+
"sender": "ai",
|
| 1243 |
+
"content": "Tushundim, Olmazor tumani. Iltimos, mahallangizni ayting.",
|
| 1244 |
+
"timestamp": "2025-10-18T12:40:21.695313"
|
| 1245 |
+
},
|
| 1246 |
+
{
|
| 1247 |
+
"id": "msg_0178",
|
| 1248 |
+
"case_id": "case_009",
|
| 1249 |
+
"sender": "user",
|
| 1250 |
+
"content": "kamolon mahallasi",
|
| 1251 |
+
"timestamp": "2025-10-18T12:40:30.372065"
|
| 1252 |
+
},
|
| 1253 |
+
{
|
| 1254 |
+
"id": "msg_0179",
|
| 1255 |
+
"case_id": "case_010",
|
| 1256 |
+
"sender": "user",
|
| 1257 |
+
"content": "salom",
|
| 1258 |
+
"timestamp": "2025-10-19T01:32:56.991520"
|
| 1259 |
+
},
|
| 1260 |
+
{
|
| 1261 |
+
"id": "msg_0180",
|
| 1262 |
+
"case_id": "case_010",
|
| 1263 |
+
"sender": "ai",
|
| 1264 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima shikoyatingiz bor?",
|
| 1265 |
+
"timestamp": "2025-10-19T01:33:00.153281"
|
| 1266 |
+
},
|
| 1267 |
+
{
|
| 1268 |
+
"id": "msg_0181",
|
| 1269 |
+
"case_id": "case_010",
|
| 1270 |
+
"sender": "ai",
|
| 1271 |
+
"content": "Assalomu alaykum! Men Help.me tez yordam tizimining virtual yordamchisiman. Sizga qanday yordam bera olaman? Nima shikoyatingiz bor?",
|
| 1272 |
+
"timestamp": "2025-10-19T01:33:00.259685"
|
| 1273 |
+
}
|
| 1274 |
+
]
|
| 1275 |
+
}
|
data/patient_history.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"patients": [
|
| 3 |
+
{
|
| 4 |
+
"id": "pat_001",
|
| 5 |
+
"full_name": "Karimov Anvar Sobirovich",
|
| 6 |
+
"history": [
|
| 7 |
+
{
|
| 8 |
+
"date": "2024-08-15",
|
| 9 |
+
"diagnosis": "Gipertoniya (yuqori qon bosimi)",
|
| 10 |
+
"notes": "Doimiy nazorat talab etiladi. Anaprilin qabul qiladi."
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"date": "2023-11-20",
|
| 14 |
+
"diagnosis": "O'tkir bronxit",
|
| 15 |
+
"notes": "Statsionar davolangan."
|
| 16 |
+
}
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"id": "pat_002",
|
| 21 |
+
"full_name": "Yusupova Dilnoza Baxtiyorovna",
|
| 22 |
+
"history": [
|
| 23 |
+
{
|
| 24 |
+
"date": "2025-01-10",
|
| 25 |
+
"diagnosis": "Penitsillinga allergiya",
|
| 26 |
+
"notes": "Qo'llash qat'iyan man etiladi. Anafilaktik shok xavfi bor."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"date": "2024-05-03",
|
| 30 |
+
"diagnosis": "O't pufagi yallig'lanishi",
|
| 31 |
+
"notes": "Parhezga rioya qiladi."
|
| 32 |
+
}
|
| 33 |
+
]
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"id": "pat_003",
|
| 37 |
+
"full_name": "Tursunov Olim Akramovich",
|
| 38 |
+
"history": [
|
| 39 |
+
{
|
| 40 |
+
"date": "2024-09-01",
|
| 41 |
+
"diagnosis": "Qandli diabet (2-turi)",
|
| 42 |
+
"notes": "Metformin preparatini qabul qiladi. Qondagi qand miqdorini doimiy o'lchaydi."
|
| 43 |
+
}
|
| 44 |
+
]
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "pat_004",
|
| 48 |
+
"full_name": "Hakimova Lobar Rustamovna",
|
| 49 |
+
"history": [
|
| 50 |
+
{
|
| 51 |
+
"date": "2023-02-18",
|
| 52 |
+
"diagnosis": "Yurak ishemik kasalligi",
|
| 53 |
+
"notes": "Stenokardiya xurujlari bo'lib turadi. Nitroglitserin qabul qiladi."
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"date": "2022-07-22",
|
| 57 |
+
"diagnosis": "Bo'yindagi osteoxondroz",
|
| 58 |
+
"notes": "Bosh aylanishiga shikoyat qiladi."
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
]
|
| 63 |
+
}
|
data/tashkent_districts.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"districts": [
|
| 3 |
+
{
|
| 4 |
+
"id": "chilonzor",
|
| 5 |
+
"name": "Chilonzor tumani",
|
| 6 |
+
"center_lat": 41.2767,
|
| 7 |
+
"center_lon": 69.2095
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "yunusobod",
|
| 11 |
+
"name": "Yunusobod tumani",
|
| 12 |
+
"center_lat": 41.3356,
|
| 13 |
+
"center_lon": 69.2830
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"id": "mirzo_ulugbek",
|
| 17 |
+
"name": "Mirzo Ulug'bek tumani",
|
| 18 |
+
"center_lat": 41.3158,
|
| 19 |
+
"center_lon": 69.3321
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "shayxontohur",
|
| 23 |
+
"name": "Shayxontohur tumani",
|
| 24 |
+
"center_lat": 41.3111,
|
| 25 |
+
"center_lon": 69.2611
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"id": "yakkasaroy",
|
| 29 |
+
"name": "Yakkasaroy tumani",
|
| 30 |
+
"center_lat": 41.2889,
|
| 31 |
+
"center_lon": 69.2739
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "mirobod",
|
| 35 |
+
"name": "Mirobod tumani",
|
| 36 |
+
"center_lat": 41.2742,
|
| 37 |
+
"center_lon": 69.2894
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "yashnobod",
|
| 41 |
+
"name": "Yashnobod tumani",
|
| 42 |
+
"center_lat": 41.2512,
|
| 43 |
+
"center_lon": 69.3262
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "sergeli",
|
| 47 |
+
"name": "Sergeli tumani",
|
| 48 |
+
"center_lat": 41.2223,
|
| 49 |
+
"center_lon": 69.2345
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"id": "bektemir",
|
| 53 |
+
"name": "Bektemir tumani",
|
| 54 |
+
"center_lat": 41.2078,
|
| 55 |
+
"center_lon": 69.3345
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "uchtepa",
|
| 59 |
+
"name": "Uchtepa tumani",
|
| 60 |
+
"center_lat": 41.3034,
|
| 61 |
+
"center_lon": 69.1734
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "olmazor",
|
| 65 |
+
"name": "Olmazor tumani",
|
| 66 |
+
"center_lat": 41.3356,
|
| 67 |
+
"center_lon": 69.2145
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"id": "yangihayot",
|
| 71 |
+
"name": "Yangihayot tumani",
|
| 72 |
+
"center_lat": 41.2534,
|
| 73 |
+
"center_lon": 69.3478
|
| 74 |
+
}
|
| 75 |
+
]
|
| 76 |
+
}
|
data/uploads/doc_001.jpg
ADDED
|