giyos1212 commited on
Commit
98b6d67
·
verified ·
1 Parent(s): da6bbfb

Upload 72 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +1 -0
  2. .gitattributes +7 -0
  3. .gitignore +21 -0
  4. README.md +115 -0
  5. app/__init__.py +0 -0
  6. app/__pycache__/__init__.cpython-313.pyc +0 -0
  7. app/api/__pycache__/dispatcher_routes.cpython-313.pyc +0 -0
  8. app/api/__pycache__/routes.cpython-313.pyc +0 -0
  9. app/api/dispatcher_routes.py +532 -0
  10. app/api/routes.py +1125 -0
  11. app/core/__pycache__/auth.cpython-313.pyc +0 -0
  12. app/core/__pycache__/config.cpython-313.pyc +0 -0
  13. app/core/__pycache__/connections.cpython-313.pyc +0 -0
  14. app/core/__pycache__/database.cpython-313.pyc +0 -0
  15. app/core/auth.py +102 -0
  16. app/core/config.py +283 -0
  17. app/core/connections.py +10 -0
  18. app/core/database.py +446 -0
  19. app/models/__pycache__/schemas.cpython-313.pyc +0 -0
  20. app/models/schemas.py +287 -0
  21. app/services/__pycache__/brigade_matcher.cpython-313.pyc +0 -0
  22. app/services/__pycache__/brigade_simulator.cpython-313.pyc +0 -0
  23. app/services/__pycache__/geocoding.cpython-313.pyc +0 -0
  24. app/services/__pycache__/location_validator.cpython-313.pyc +0 -0
  25. app/services/__pycache__/models.cpython-313.pyc +0 -0
  26. app/services/brigade_matcher.py +270 -0
  27. app/services/brigade_simulator.py +199 -0
  28. app/services/geocoding.py +262 -0
  29. app/services/location_validator.py +330 -0
  30. app/services/models.py +682 -0
  31. app/utils/__pycache__/demo_gps.cpython-313.pyc +0 -0
  32. app/utils/__pycache__/district_matcher.cpython-313.pyc +0 -0
  33. app/utils/__pycache__/mahalla_matcher.cpython-313.pyc +0 -0
  34. app/utils/__pycache__/translit.cpython-313.pyc +0 -0
  35. app/utils/demo_gps.py +214 -0
  36. app/utils/district_matcher.py +205 -0
  37. app/utils/mahalla_matcher.py +309 -0
  38. app/utils/translit.py +130 -0
  39. data/brigades.json +88 -0
  40. data/cases.json +339 -0
  41. data/clinics.json +461 -0
  42. data/dispatchers.json +11 -0
  43. data/doctors.json +328 -0
  44. data/doctors_photos.json +319 -0
  45. data/locations.json +28 -0
  46. data/locations_db.json +0 -0
  47. data/messages.json +1275 -0
  48. data/patient_history.json +63 -0
  49. data/tashkent_districts.json +76 -0
  50. 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