File size: 17,247 Bytes
339347e
 
e660779
ff61ae6
 
339347e
ff61ae6
7e9f512
e660779
 
4820df3
0483cf1
7e9f512
2a5b8f8
273d7ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8cb98fa
 
 
273d7ce
 
 
ff61ae6
 
 
 
 
 
 
 
 
273d7ce
 
8cb98fa
273d7ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8cb98fa
273d7ce
 
 
 
4820df3
8cb98fa
e660779
273d7ce
e660779
273d7ce
 
 
4820df3
e660779
4820df3
8cb98fa
 
 
 
 
 
e660779
 
8cb98fa
 
 
 
 
 
4820df3
273d7ce
4820df3
 
 
e660779
273d7ce
8cb98fa
 
273d7ce
8cb98fa
 
4820df3
8cb98fa
 
273d7ce
 
8cb98fa
273d7ce
8cb98fa
273d7ce
 
4820df3
273d7ce
8cb98fa
273d7ce
8cb98fa
 
 
 
273d7ce
4820df3
273d7ce
8cb98fa
 
273d7ce
8cb98fa
 
273d7ce
4820df3
8cb98fa
 
 
 
 
 
 
 
273d7ce
4820df3
8cb98fa
 
4820df3
 
 
 
 
273d7ce
4820df3
273d7ce
8cb98fa
 
ff61ae6
8cb98fa
273d7ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339347e
8cb98fa
 
ff61ae6
 
 
8cb98fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4820df3
8cb98fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a5b8f8
4820df3
339347e
2a5b8f8
 
 
 
 
 
 
4820df3
2a5b8f8
 
339347e
273d7ce
2a5b8f8
 
 
 
 
 
273d7ce
2a5b8f8
273d7ce
8cb98fa
273d7ce
e660779
2a5b8f8
273d7ce
339347e
8cb98fa
2a5b8f8
 
 
 
 
 
 
 
008eaa9
2a5b8f8
4820df3
2a5b8f8
4820df3
273d7ce
ff61ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4820df3
ff61ae6
 
4820df3
ff61ae6
 
 
 
 
 
 
4820df3
 
ff61ae6
 
 
 
 
4820df3
ff61ae6
 
4820df3
ff61ae6
 
 
 
 
 
4820df3
ff61ae6
 
 
 
 
 
273d7ce
339347e
 
273d7ce
af7432b
8cb98fa
273d7ce
 
ff61ae6
 
 
 
4820df3
ff61ae6
 
 
 
 
 
 
4820df3
ff61ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273d7ce
339347e
476a41c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import gradio as gr
from huggingface_hub import InferenceClient
import json, re, os, requests
import csv
from io import StringIO

# API Keys from Space settings
hf_token = os.getenv("HF_TOKEN")
google_key = os.getenv("GOOGLE_MAPS_API_KEY")

# Initialize Client
client = InferenceClient("Qwen/Qwen2.5-7B-Instruct", token=hf_token)

SYSTEM_PROMPT = """Analyze the Arabic map query and return ONLY a JSON object with these exact keys:

1. "location": City, area, or "near me" if not specified.
2. "category": The main place type. Use standard Arabic terms:
   - Food: مطعم, مقهى, فرن, مخبز, سوق, بقالة, مطعم شاورما, مطعم برجر
   - Shopping: مول, متجر, سوبرماركت, محل ملابس, صيدلية, محل إلكترونيات, محل أثاث
   - Services: بنك, صراف آلي, محطة وقود, مغسلة, كوافير, ورشة سيارات, مكتب بريد
   - Health: مستشفى, عيادة, صيدلية, طبيب أسنان, مركز صحي, طوارئ
   - Education: مدرسة, جامعة, مكتبة, حضانة, معهد
   - Entertainment: سينما, متحف, حديقة, ملعب, نادي رياضي, شاطئ, حديقة حيوان, منتزه
   - Transportation: مطار, محطة قطار, محطة حافلات, موقف سيارات, تأجير سيارات, وقود
   - Lodging: فندق, شقة فندقية, شاليه, مخيم, بيت ضيافة
   - Religious: مسجد, كنيسة, معبد, جامع
   - Government: ديوان بلدية, محكمة, مركز شرطة, سفارة, مركز حكومي
   - Outdoor: شاطئ, ممر مشاة, بحيرة, غابة, جبل, منتزه طبيعي

3. "sub_type": Specific type within category or null.
4. "features": A list of ALL descriptive tags mentioned.
5. "sort_by": ONE of: rating, price, distance, relevance, open_now.

Examples:
Input: "أرخص وأقرب مستشفى طوارئ في جدة"
Output: {"location": "جدة", "category": "مستشفى", "sub_type": "طوارئ", "features": ["أرخص", "أقرب"], "sort_by": "price"}
Input: "حديقة أطفال نظيفة في الرياض"
Output: {"location": "الرياض", "category": "حديقة", "sub_type": "أطفال", "features": ["نظيفة"], "sort_by": "relevance"}
Input: "أفضل فندق 5 نجوم قريب من المطار في دبي"
Output: {"location": "دبي", "category": "فندق", "sub_type": "5 نجوم", "features": ["أفضل", "قريب من المطار"], "sort_by": "rating"}
Input: "محطة وقود 24 ساعة رخيصة"
Output: {"location": "near me", "category": "محطة وقود", "sub_type": null, "features": ["24 ساعة", "رخيصة"], "sort_by": "price"}
Input: "مقهى هادئ للعمل في الرياض"
Output: {"location": "الرياض", "category": "مقهى", "sub_type": null, "features": ["هادئ", "للعمل"], "sort_by": "relevance"}"""

def build_search_query(parsed_data):
    """بناء جملة بحث محسّنة"""
    parts = []
    
    category = parsed_data.get("category", "")
    if category:
        parts.append(category)
    
    sub_type = parsed_data.get("sub_type")
    if sub_type and sub_type != category:
        parts.append(str(sub_type))
    
    location = parsed_data.get("location", "")
    if location and location != "near me":
        parts.append(f"في {location}")
    
    features = parsed_data.get("features", [])
    search_features = [f for f in features if any(keyword in f for keyword in 
        ["24", "طوارئ", "أطفال", "مجاني", "مفتوح", "فاخر", "اقتصادي", "رخيص"])]
    parts.extend(search_features)
    
    return " ".join(parts)

def search_google_maps_new_api(parsed_data):
    """استخدام Places API (New) v1"""
    if not google_key:
        return "⚠️ الرجاء إضافة GOOGLE_MAPS_API_KEY في إعدادات الـ Space."
    
    query = build_search_query(parsed_data)
    
    if not query.strip():
        query = f"{parsed_data.get("category", "")} {parsed_data.get("location", "")}"
    
    # Places API (New) - Text Search v1
    url = "https://places.googleapis.com/v1/places:searchText"
    
    headers = {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": google_key,
        "X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.rating,places.userRatingCount,places.priceLevel,places.currentOpeningHours,places.types,places.location"
    }
    
    data = {
        "textQuery": query,
        "languageCode": "ar",
        "maxResultCount": 10
    }
    
    # إضافة تفضيل الموقع إذا كان الترتيب حسب المسافة
    sort_by = parsed_data.get("sort_by", "relevance")
    if sort_by == "distance" and parsed_data.get("location") != "near me":
        # يمكن إضافة locationBias لاحقاً
        pass
    
    try:
        response = requests.post(url, headers=headers, json=data, timeout=15)
        result = response.json()
        
        if response.status_code != 200:
            error_msg = result.get("error", {}).get("message", "Unknown error")
            # إذا كان الخطأ بسبب عدم تفعيل API، نحاول الـ Legacy كاحتياط
            if "not enabled" in error_msg.lower() or response.status_code == 403:
                return search_google_maps_legacy(parsed_data, query)
            return f"⚠️ خطأ في API: {error_msg}"
        
        places = result.get("places", [])
        
        if not places:
            return "❌ لم يتم العثور على نتائج. جرب تعديل البحث."
        
        # ترتيب النتائج
        if sort_by == "rating":
            places = sorted(places, key=lambda x: x.get("rating", 0), reverse=True)
        elif sort_by == "price":
            price_order = {"PRICE_LEVEL_UNSPECIFIED": 0, "PRICE_LEVEL_FREE": 1, 
                          "PRICE_LEVEL_INEXPENSIVE": 2, "PRICE_LEVEL_MODERATE": 3,
                          "PRICE_LEVEL_EXPENSIVE": 4, "PRICE_LEVEL_VERY_EXPENSIVE": 5}
            places = sorted(places, key=lambda x: price_order.get(x.get("priceLevel"), 99))
        
        # تنسيق النتائج
        output_list = []
        for i, place in enumerate(places[:6], 1):
            name = place.get("displayName", {}).get("text", "غير معروف")
            rating = place.get("rating", "غير متوفر")
            total_ratings = place.get("userRatingCount", 0)
            address = place.get("formattedAddress", "العنوان غير متوفر")
            
            # مستوى السعر
            price_level = place.get("priceLevel")
            price_map = {
                "PRICE_LEVEL_INEXPENSIVE": "💰",
                "PRICE_LEVEL_MODERATE": "💰💰",
                "PRICE_LEVEL_EXPENSIVE": "💰💰💰",
                "PRICE_LEVEL_VERY_EXPENSIVE": "💰💰💰💰"
            }
            price_str = price_map.get(price_level, "")
            
            # حالة الافتتاح
            hours = place.get("currentOpeningHours", {})
            open_now = hours.get("openNow")
            status = ""
            if open_now is True:
                status = " ✅ مفتوح الآن"
            elif open_now is False:
                status = " ❌ مغلق الآن"
            
            # الأنواع
            types = place.get("types", [])
            place_type = ", ".join([t.replace("_", " ").replace("restaurant", "مطعم").replace("cafe", "مقهى") 
                                   .replace("hospital", "مستشفى").replace("park", "حديقة") 
                                   .replace("store", "متجر").replace("gas_station", "محطة وقود") 
                                   for t in types[:2]])
            
            card = f"""### {i}. 📍 {name}
⭐ **التقييم:** {rating}/5 ({total_ratings} تقييم) {price_str}
🏠 **العنوان:** {address}{status}
🏷️ **النوع:** {place_type}
---"""
            output_list.append(card)
        
        summary = f"🔍 **نتائج البحث عن:** `{query}` | **الترتيب حسب:** {sort_by}\n\n"
        return summary + "\n".join(output_list)
        
    except requests.Timeout:
        return "⏱️ انتهى وقت الانتظار. حاول مرة أخرى."
    except Exception as e:
        return f"❌ خطأ في الاتصال: {str(e)}"

def search_google_maps_legacy(parsed_data, query=None):
    """الرجوع إلى Legacy API كاحتياط"""
    if not google_key:
        return "⚠️ الرجاء إضافة GOOGLE_MAPS_API_KEY في إعدادات الـ Space."

    if not query:
        query = build_search_query(parsed_data)
    
    url = "https://maps.googleapis.com/maps/api/place/textsearch/json"
    params = {
        "query": query,
        "language": "ar",
        "key": google_key
    }
    
    try:
        response = requests.get(url, params=params, timeout=10)
        data = response.json()
        
        if data.get("status") != "OK":
            return f"⚠️ خطأ في Legacy API أيضاً: {data.get("status")}. الرجاء تفعيل Places API (New) في Google Cloud Console."
        
        results = data.get("results", [])
        
        if not results:
            return "❌ لم يتم العثور على نتائج."
        
        sort_by = parsed_data.get("sort_by", "relevance")
        if sort_by == "rating":
            results = sorted(results, key=lambda x: x.get("rating", 0), reverse=True)
        
        output_list = []
        for i, place in enumerate(results[:6], 1):
            name = place.get("name", "غير معروف")
            rating = place.get("rating", "غير متوفر")
            address = place.get("formatted_address", "العنوان غير متوفر")
            
            price_level = place.get("price_level")
            price_str = "💰" * price_level if price_level else ""
            
            open_now = place.get("opening_hours", {}).get("open_now")
            status = " ✅ مفتوح" if open_now else " ❌ مغلق" if open_now is False else ""
            
            card = f"""### {i}. 📍 {name}
⭐ **التقييم:** {rating}/5 {price_str}
🏠 **العنوان:** {address}{status}
---"""
            output_list.append(card)
        
        return "\n".join(output_list)
        
    except Exception as e:
        return f"❌ فشل في Legacy API أيضاً: {str(e)}"

def parse_arabic_query(user_query):
    """تحليل الاستعلام باستخدام LLM"""
    try:
        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_query}
        ]
        
        response = client.chat_completion(
            messages=messages,
            max_tokens=300,
            temperature=0.1,
            stream=False
        )
        
        if hasattr(response, 'choices') and response.choices:
            raw_content = response.choices[0].message.content
        elif isinstance(response, dict) and 'choices' in response:
            raw_content = response['choices'][0]['message']['content']
        else:
            raw_content = str(response)
        
        match = re.search(r'\{.*\}', raw_content, re.DOTALL)
        if not match:
            return {"error": "لم يتم العثور على JSON صالح", "raw_response": raw_content[:500]}
        
        parsed_data = json.loads(match.group(0))
        return parsed_data
        
    except Exception as e:
        return {"error": f"خطأ في التحليل: {str(e)}"}

def main_process(user_query):
    if not user_query.strip():
        return {"error": "الرجاء إدخال استعلام"}, "لا توجد نتائج"
    
    parsed_data = parse_arabic_query(user_query)
    
    if "error" in parsed_data:
        return parsed_data, f"⚠️ {parsed_data.get('error', 'خطأ غير معروف')}"
    
    map_results = search_google_maps_new_api(parsed_data)
    
    return parsed_data, map_results

def process_batch_queries(file_obj, progress=gr.Progress()):
    if file_obj is None:
        return None, "❌ الرجاء تحميل ملف للاستعلامات الجماعية."

    queries = []
    try:
        with open(file_obj.name, 'r', encoding='utf-8') as f:
            for line in f:
                query = line.strip()
                if query:
                    queries.append(query)
    except Exception as e:
        return None, f"❌ خطأ في قراءة الملف: {str(e)}"

    if not queries:
        return None, "❌ الملف فارغ أو لا يحتوي على استعلامات صالحة."

    results = []
    for i, query in enumerate(progress.tqdm(queries, desc="معالجة الاستعلامات")):
        parsed_data, map_results = main_process(query)
        
        # Flatten parsed_data for CSV, handle errors
        parsed_json_str = json.dumps(parsed_data, ensure_ascii=False) if "error" not in parsed_data else parsed_data.get("error", "")
        
        # Extract top result name if available, otherwise indicate no results
        top_result_name = ""
        if "نتائج البحث عن" in map_results and "📍" in map_results:
            match = re.search(r'### \d+\. 📍 ([^\n]+)', map_results)
            if match:
                top_result_name = match.group(1).strip()
        elif "لم يتم العثور على نتائج" in map_results:
            top_result_name = "لا توجد نتائج"
        elif "خطأ" in map_results:
            top_result_name = f"خطأ في API: {map_results}"

        results.append({
            "Original Query": query,
            "Parsed JSON": parsed_json_str,
            "Top Map Result": top_result_name,
            "Full Map Results": map_results.replace('\n', '  ') # Replace newlines for CSV readability
        })

    # Write results to a CSV in memory
    output_buffer = StringIO()
    fieldnames = ["Original Query", "Parsed JSON", "Top Map Result", "Full Map Results"]
    writer = csv.DictWriter(output_buffer, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(results)
    
    # Save to a temporary file for Gradio to handle
    output_filepath = "./batch_results.csv"
    with open(output_filepath, 'w', encoding='utf-8') as f:
        f.write(output_buffer.getvalue())

    return output_filepath, "✅ تم الانتهاء من معالجة الدفعة. يمكنك تحميل النتائج."

# ==================== واجهة المستخدم ====================

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown("""
    # 🗺️ Arabic map parser
    
    """)
    
    with gr.Tab("استعلام فردي"):
        with gr.Row():
            input_text = gr.Textbox(
                label="📝 ماذا تبحث عنه؟",
                placeholder="مثال: أرخص مستشفى طوارئ في جدة، أو حديقة أطفال نظيفة في الرياض، أو فندق 5 نجوم في دبي",
                lines=2
            )
        
        btn = gr.Button("🔍 ابحث الآن", variant="primary")
        
        with gr.Row():
            with gr.Column(scale=1):
                output_json = gr.JSON(label="⚙️ تحليل الذكاء الاصطناعي")
            with gr.Column(scale=2):
                output_map = gr.Markdown(label="🗺️ نتائج Google Maps")
        
        gr.Markdown("### 💡 أمثلة للبحث:")
        examples = [
            "أرخص وأقرب مستشفى طوارئ في جدة",
            "حديقة أطفال نظيفة في الرياض",
            "أفضل فندق 5 نجوم في دبي",
            "مقهى هادئ للعمل في الخبر",
            "محطة وقود 24 ساعة رخيصة",
            "صيدلية قريبة مفتوحة الآن"
        ]
        
        with gr.Row():
            for ex in examples[:3]:
                btn_ex = gr.Button(ex, size="sm")
                btn_ex.click(lambda x=ex: x, None, input_text)
        
        with gr.Row():
            for ex in examples[3:]:
                btn_ex = gr.Button(ex, size="sm")
                btn_ex.click(lambda x=ex: x, None, input_text)
        
        btn.click(fn=main_process, inputs=input_text, outputs=[output_json, output_map])

    with gr.Tab("معالجة دفعة"):
        gr.Markdown("""
        ### ⬆️ تحميل ملف الاستعلامات
        قم بتحميل ملف نصي (.txt) أو CSV يحتوي على استعلام واحد في كل سطر.
        """)
        file_input = gr.File(label="ملف الاستعلامات (TXT/CSV)", file_count="single", type="filepath")
        batch_btn = gr.Button("🚀 ابدأ معالجة الدفعة", variant="primary")
        batch_output_file = gr.File(label="ملف نتائج الدفعة (CSV)")
        batch_status_message = gr.Markdown(label="الحالة")

        batch_btn.click(
            fn=process_batch_queries,
            inputs=file_input,
            outputs=[batch_output_file, batch_status_message]
        )

demo.launch()