yahya1912's picture
تعديل الاسم
af7432b verified
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()