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()