Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -110,13 +110,6 @@ def index():
|
|
| 110 |
def favicon():
|
| 111 |
return Response('', status=204)
|
| 112 |
|
| 113 |
-
@app.route('/main.js')
|
| 114 |
-
def main_js():
|
| 115 |
-
js_path = os.path.join(TEMPLATE_DIR, 'main.js')
|
| 116 |
-
if os.path.isfile(js_path):
|
| 117 |
-
return send_file(js_path, mimetype='application/javascript')
|
| 118 |
-
return Response('// main.js placeholder\n', status=200, mimetype='application/javascript')
|
| 119 |
-
|
| 120 |
# ======================== أدوات مساعدة ========================
|
| 121 |
def as_text(v):
|
| 122 |
if v is None:
|
|
@@ -129,7 +122,7 @@ def as_text(v):
|
|
| 129 |
return v.get('text') or v.get('body') or v.get('content') or ""
|
| 130 |
return str(v)
|
| 131 |
|
| 132 |
-
TOOLS_PROMPT = """You have access to tools. To use a tool, output ONE single valid JSON object
|
| 133 |
|
| 134 |
Tool formats:
|
| 135 |
- Web search: {"tool":"search","query":"search terms"}
|
|
@@ -145,7 +138,6 @@ Document content schema:
|
|
| 145 |
Rules:
|
| 146 |
- Only emit tools when user explicitly asks
|
| 147 |
- Maximum ONE tool per response
|
| 148 |
-
- After tool JSON, add a short sentence
|
| 149 |
- Never fake tool results"""
|
| 150 |
|
| 151 |
def build_system_prompt(user, interests, think):
|
|
@@ -155,11 +147,7 @@ def build_system_prompt(user, interests, think):
|
|
| 155 |
]
|
| 156 |
user = user or {}
|
| 157 |
if user.get('name'):
|
| 158 |
-
lines.append(f"User's name is {user['name']}.
|
| 159 |
-
if user.get('dob'):
|
| 160 |
-
lines.append(f"User's birth date: {user['dob']}.")
|
| 161 |
-
if user.get('gender'):
|
| 162 |
-
lines.append(f"User's gender: {user['gender']}.")
|
| 163 |
if interests:
|
| 164 |
lines.append(f"User interests: {', '.join(interests[:15])}.")
|
| 165 |
|
|
@@ -260,14 +248,27 @@ def call_pollinations_text(messages, stream=False):
|
|
| 260 |
response.raise_for_status()
|
| 261 |
return response.json()
|
| 262 |
|
| 263 |
-
# ======================== أدوات البحث ========================
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
try:
|
| 266 |
r = requests.post(
|
| 267 |
"https://html.duckduckgo.com/html/",
|
| 268 |
data={"q": query},
|
| 269 |
-
headers={"User-Agent": "Mozilla/5.0
|
| 270 |
-
timeout=
|
| 271 |
)
|
| 272 |
results = []
|
| 273 |
pattern = re.compile(
|
|
@@ -284,26 +285,26 @@ def web_search_duckduckgo(query, n=10):
|
|
| 284 |
})
|
| 285 |
if len(results) >= n:
|
| 286 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
return results
|
| 288 |
except:
|
| 289 |
return []
|
| 290 |
|
| 291 |
-
def
|
| 292 |
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
| 293 |
-
future = executor.submit(web_search_duckduckgo, query)
|
| 294 |
-
try:
|
| 295 |
-
return future.result(timeout=timeout)
|
| 296 |
-
except concurrent.futures.TimeoutError:
|
| 297 |
-
logger.warning(f"[search] Timeout for: {query[:50]}")
|
| 298 |
-
return []
|
| 299 |
-
|
| 300 |
-
def image_search(query, n=8):
|
| 301 |
try:
|
| 302 |
r = requests.get(
|
| 303 |
"https://www.bing.com/images/search",
|
| 304 |
params={"q": query, "form": "HDRSC2"},
|
| 305 |
headers={"User-Agent": "Mozilla/5.0"},
|
| 306 |
-
timeout=
|
| 307 |
)
|
| 308 |
urls = re.findall(r'murl":"(.*?)"', r.text)
|
| 309 |
return [{"image": u} for u in urls[:n]]
|
|
@@ -322,44 +323,37 @@ def extract_tool_from_text(text):
|
|
| 322 |
pass
|
| 323 |
return None
|
| 324 |
|
| 325 |
-
def
|
| 326 |
-
"""تنفيذ
|
| 327 |
tool_type = tool_data.get('tool')
|
| 328 |
|
| 329 |
if tool_type == 'search':
|
| 330 |
query = tool_data.get('query', '')
|
| 331 |
-
|
| 332 |
-
results = web_search_with_timeout(query, timeout=5)
|
| 333 |
if results:
|
| 334 |
formatted = "\n\n### نتائج البحث:\n\n"
|
| 335 |
-
for i, r in enumerate(results
|
| 336 |
-
formatted += f"**{i}. {r
|
| 337 |
-
formatted += f" {r.get('snippet', '')[:200]}\n"
|
| 338 |
-
formatted += f" {r.get('url', '')}\n\n"
|
| 339 |
return formatted
|
| 340 |
-
return "\n\n
|
| 341 |
|
| 342 |
elif tool_type == 'image_search':
|
| 343 |
query = tool_data.get('query', '')
|
| 344 |
-
logger.info(f"[tool] Image search: {query[:100]}")
|
| 345 |
images = image_search(query)
|
| 346 |
if images:
|
| 347 |
formatted = "\n\n### الصور:\n\n"
|
| 348 |
for i, img in enumerate(images[:4], 1):
|
| 349 |
-
formatted += f"
|
| 356 |
-
return {"special": "image", "prompt": prompt}
|
| 357 |
|
| 358 |
elif tool_type == 'document':
|
| 359 |
-
|
| 360 |
-
return "\n\n📄 **جاري إنشاء المستند...**\n\n"
|
| 361 |
|
| 362 |
-
return
|
| 363 |
|
| 364 |
# ======================== المستندات ========================
|
| 365 |
def build_docx(cmd):
|
|
@@ -510,13 +504,49 @@ def chat():
|
|
| 510 |
interests = data.get('interests') or []
|
| 511 |
think = bool(data.get('think')) and model_type == 'pro'
|
| 512 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
system_prompt = build_system_prompt(user, interests, think)
|
| 514 |
full_messages = [{"role": "system", "content": system_prompt}] + normalize_messages(messages[-10:])
|
| 515 |
|
| 516 |
def generate():
|
| 517 |
attempt = 0
|
| 518 |
full_response = ""
|
| 519 |
-
tool_executed = False
|
| 520 |
|
| 521 |
while True:
|
| 522 |
attempt += 1
|
|
@@ -554,38 +584,31 @@ def chat():
|
|
| 554 |
except json.JSONDecodeError:
|
| 555 |
continue
|
| 556 |
|
| 557 |
-
# ا
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
|
|
|
| 563 |
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
yield f"data: {json.dumps({'delta': tool_result})}\n\n"
|
| 573 |
-
except Exception as tool_error:
|
| 574 |
-
logger.error(f"[tool] Error: {tool_error}")
|
| 575 |
-
error_msg = "\n\n⚠️ خطأ في تنفيذ الأداة: " + str(tool_error) + "\n\n"
|
| 576 |
-
yield f"data: {json.dumps({'delta': error_msg})}\n\n"
|
| 577 |
|
| 578 |
yield f"data: {json.dumps({'done': True})}\n\n"
|
| 579 |
return
|
| 580 |
|
| 581 |
except Exception as e:
|
| 582 |
logger.warning(f"[chat] attempt #{attempt} failed: {e}")
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
yield f"data: {json.dumps({'retrying': True, 'attempt': attempt})}\n\n"
|
| 588 |
-
time.sleep(RETRY_INTERVAL)
|
| 589 |
|
| 590 |
return Response(stream_with_context(generate()), mimetype='text/event-stream')
|
| 591 |
|
|
@@ -685,7 +708,7 @@ def search_ep():
|
|
| 685 |
q = _safe_json().get('query', '')
|
| 686 |
if not q:
|
| 687 |
return jsonify({'results': []})
|
| 688 |
-
results =
|
| 689 |
return jsonify({'results': results})
|
| 690 |
|
| 691 |
@app.route('/api/image-search', methods=['POST'])
|
|
@@ -712,8 +735,7 @@ def health():
|
|
| 712 |
'status': 'ok',
|
| 713 |
'model': FLASH_MODEL,
|
| 714 |
'provider': 'pollinations',
|
| 715 |
-
'search': 'local (
|
| 716 |
-
'retryInterval': RETRY_INTERVAL
|
| 717 |
})
|
| 718 |
|
| 719 |
# ======================== Error handlers ========================
|
|
@@ -727,5 +749,5 @@ def internal_error(e):
|
|
| 727 |
|
| 728 |
if __name__ == '__main__':
|
| 729 |
port = int(os.environ.get('PORT', 7860))
|
| 730 |
-
logger.info(f"Genisi running on port {port} | Flash: {FLASH_MODEL} | Search: local
|
| 731 |
app.run(host='0.0.0.0', port=port, threaded=True)
|
|
|
|
| 110 |
def favicon():
|
| 111 |
return Response('', status=204)
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
# ======================== أدوات مساعدة ========================
|
| 114 |
def as_text(v):
|
| 115 |
if v is None:
|
|
|
|
| 122 |
return v.get('text') or v.get('body') or v.get('content') or ""
|
| 123 |
return str(v)
|
| 124 |
|
| 125 |
+
TOOLS_PROMPT = """You have access to tools. To use a tool, output ONE single valid JSON object.
|
| 126 |
|
| 127 |
Tool formats:
|
| 128 |
- Web search: {"tool":"search","query":"search terms"}
|
|
|
|
| 138 |
Rules:
|
| 139 |
- Only emit tools when user explicitly asks
|
| 140 |
- Maximum ONE tool per response
|
|
|
|
| 141 |
- Never fake tool results"""
|
| 142 |
|
| 143 |
def build_system_prompt(user, interests, think):
|
|
|
|
| 147 |
]
|
| 148 |
user = user or {}
|
| 149 |
if user.get('name'):
|
| 150 |
+
lines.append(f"User's name is {user['name']}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
if interests:
|
| 152 |
lines.append(f"User interests: {', '.join(interests[:15])}.")
|
| 153 |
|
|
|
|
| 248 |
response.raise_for_status()
|
| 249 |
return response.json()
|
| 250 |
|
| 251 |
+
# ======================== أدوات البحث (سريعة) ========================
|
| 252 |
+
# كاش للبحث - يخزن النتائج لمدة 5 دقائق
|
| 253 |
+
search_cache = {}
|
| 254 |
+
CACHE_TTL = 300 # 5 دقائق
|
| 255 |
+
|
| 256 |
+
def web_search_duckduckgo(query, n=5):
|
| 257 |
+
"""بحث سريع مع كاش"""
|
| 258 |
+
# التحقق من الكاش
|
| 259 |
+
cache_key = query.lower().strip()
|
| 260 |
+
if cache_key in search_cache:
|
| 261 |
+
cached_time, cached_results = search_cache[cache_key]
|
| 262 |
+
if time.time() - cached_time < CACHE_TTL:
|
| 263 |
+
logger.info(f"[search] Cache hit for: {query[:50]}")
|
| 264 |
+
return cached_results
|
| 265 |
+
|
| 266 |
try:
|
| 267 |
r = requests.post(
|
| 268 |
"https://html.duckduckgo.com/html/",
|
| 269 |
data={"q": query},
|
| 270 |
+
headers={"User-Agent": "Mozilla/5.0"},
|
| 271 |
+
timeout=3, # timeout قصير
|
| 272 |
)
|
| 273 |
results = []
|
| 274 |
pattern = re.compile(
|
|
|
|
| 285 |
})
|
| 286 |
if len(results) >= n:
|
| 287 |
break
|
| 288 |
+
|
| 289 |
+
# تخزين في الكاش
|
| 290 |
+
if results:
|
| 291 |
+
search_cache[cache_key] = (time.time(), results)
|
| 292 |
+
# تنظيف الكاش القديم
|
| 293 |
+
for k in list(search_cache.keys()):
|
| 294 |
+
if time.time() - search_cache[k][0] > CACHE_TTL:
|
| 295 |
+
del search_cache[k]
|
| 296 |
+
|
| 297 |
return results
|
| 298 |
except:
|
| 299 |
return []
|
| 300 |
|
| 301 |
+
def image_search(query, n=6):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
try:
|
| 303 |
r = requests.get(
|
| 304 |
"https://www.bing.com/images/search",
|
| 305 |
params={"q": query, "form": "HDRSC2"},
|
| 306 |
headers={"User-Agent": "Mozilla/5.0"},
|
| 307 |
+
timeout=3,
|
| 308 |
)
|
| 309 |
urls = re.findall(r'murl":"(.*?)"', r.text)
|
| 310 |
return [{"image": u} for u in urls[:n]]
|
|
|
|
| 323 |
pass
|
| 324 |
return None
|
| 325 |
|
| 326 |
+
def execute_tool_fast(tool_data):
|
| 327 |
+
"""تنفيذ سريع للأداة"""
|
| 328 |
tool_type = tool_data.get('tool')
|
| 329 |
|
| 330 |
if tool_type == 'search':
|
| 331 |
query = tool_data.get('query', '')
|
| 332 |
+
results = web_search_duckduckgo(query, n=5)
|
|
|
|
| 333 |
if results:
|
| 334 |
formatted = "\n\n### نتائج البحث:\n\n"
|
| 335 |
+
for i, r in enumerate(results, 1):
|
| 336 |
+
formatted += f"**{i}. {r['title']}**\n{r['snippet'][:150]}\n{r['url']}\n\n"
|
|
|
|
|
|
|
| 337 |
return formatted
|
| 338 |
+
return "\n\nلم يتم العثور على نتائج.\n\n"
|
| 339 |
|
| 340 |
elif tool_type == 'image_search':
|
| 341 |
query = tool_data.get('query', '')
|
|
|
|
| 342 |
images = image_search(query)
|
| 343 |
if images:
|
| 344 |
formatted = "\n\n### الصور:\n\n"
|
| 345 |
for i, img in enumerate(images[:4], 1):
|
| 346 |
+
formatted += f"\n\n"
|
| 347 |
return formatted
|
| 348 |
+
return "\n\nلم يتم العثور على صور.\n\n"
|
| 349 |
|
| 350 |
elif tool_type == 'image':
|
| 351 |
+
return {"special": "image", "prompt": tool_data.get('prompt', '')}
|
|
|
|
|
|
|
| 352 |
|
| 353 |
elif tool_type == 'document':
|
| 354 |
+
return "\n\nجاري إنشاء المستند...\n\n"
|
|
|
|
| 355 |
|
| 356 |
+
return None
|
| 357 |
|
| 358 |
# ======================== المستندات ========================
|
| 359 |
def build_docx(cmd):
|
|
|
|
| 504 |
interests = data.get('interests') or []
|
| 505 |
think = bool(data.get('think')) and model_type == 'pro'
|
| 506 |
|
| 507 |
+
# ✨ فحص إذا كانت الرسالة الأخيرة طلب بحث مباشر
|
| 508 |
+
last_msg = messages[-1].get('content', '') if messages else ''
|
| 509 |
+
direct_search = False
|
| 510 |
+
search_query = ""
|
| 511 |
+
|
| 512 |
+
# كشف طلب البحث المباشر
|
| 513 |
+
search_patterns = [
|
| 514 |
+
r'ابحث (?:عن|لي) (.+)',
|
| 515 |
+
r'بحث (?:عن) (.+)',
|
| 516 |
+
r'search (?:for)? (.+)',
|
| 517 |
+
r'find (.+)',
|
| 518 |
+
r'معلومات عن (.+)',
|
| 519 |
+
r'اخر اخبار (.+)',
|
| 520 |
+
r'من هو (.+)',
|
| 521 |
+
r'ما هي (.+)',
|
| 522 |
+
r'ما هو (.+)',
|
| 523 |
+
]
|
| 524 |
+
|
| 525 |
+
for pattern in search_patterns:
|
| 526 |
+
match = re.search(pattern, last_msg, re.IGNORECASE)
|
| 527 |
+
if match:
|
| 528 |
+
direct_search = True
|
| 529 |
+
search_query = match.group(1).strip()
|
| 530 |
+
break
|
| 531 |
+
|
| 532 |
+
# إذا كان بحث مباشر، نجريه أولاً ونضيف النتائج للرسائل
|
| 533 |
+
if direct_search and search_query:
|
| 534 |
+
logger.info(f"[direct-search] Detected: {search_query}")
|
| 535 |
+
search_results = web_search_duckduckgo(search_query, n=5)
|
| 536 |
+
if search_results:
|
| 537 |
+
context = "\n\nمعلومات من الإنترنت:\n\n"
|
| 538 |
+
for r in search_results:
|
| 539 |
+
context += f"- {r['title']}: {r['snippet'][:200]}\n {r['url']}\n"
|
| 540 |
+
|
| 541 |
+
# إضافة السياق لآخر رسالة
|
| 542 |
+
messages[-1]['content'] = f"استخدم المعلومات التالية للإجابة:\n{context}\n\nسؤال المستخدم: {last_msg}"
|
| 543 |
+
|
| 544 |
system_prompt = build_system_prompt(user, interests, think)
|
| 545 |
full_messages = [{"role": "system", "content": system_prompt}] + normalize_messages(messages[-10:])
|
| 546 |
|
| 547 |
def generate():
|
| 548 |
attempt = 0
|
| 549 |
full_response = ""
|
|
|
|
| 550 |
|
| 551 |
while True:
|
| 552 |
attempt += 1
|
|
|
|
| 584 |
except json.JSONDecodeError:
|
| 585 |
continue
|
| 586 |
|
| 587 |
+
# ✨ معالجة سريعة للأداة بعد الرد
|
| 588 |
+
tool = extract_tool_from_text(full_response)
|
| 589 |
+
if tool and not direct_search: # لا نعالج الأدوات إذا كان بحث مباشر
|
| 590 |
+
logger.info(f"[tool] Executing: {tool.get('tool')}")
|
| 591 |
+
|
| 592 |
+
try:
|
| 593 |
+
tool_result = execute_tool_fast(tool)
|
| 594 |
|
| 595 |
+
if isinstance(tool_result, dict) and tool_result.get('special') == 'image':
|
| 596 |
+
yield f"data: {json.dumps({'tool': 'image', 'prompt': tool_result['prompt']})}\n\n"
|
| 597 |
+
elif tool_result:
|
| 598 |
+
yield f"data: {json.dumps({'delta': tool_result})}\n\n"
|
| 599 |
+
except Exception as tool_error:
|
| 600 |
+
logger.error(f"[tool] Error: {tool_error}")
|
| 601 |
+
error_msg = "\n\nخطأ: " + str(tool_error) + "\n\n"
|
| 602 |
+
yield f"data: {json.dumps({'delta': error_msg})}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
yield f"data: {json.dumps({'done': True})}\n\n"
|
| 605 |
return
|
| 606 |
|
| 607 |
except Exception as e:
|
| 608 |
logger.warning(f"[chat] attempt #{attempt} failed: {e}")
|
| 609 |
+
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
| 610 |
+
yield f"data: {json.dumps({'done': True})}\n\n"
|
| 611 |
+
return
|
|
|
|
|
|
|
|
|
|
| 612 |
|
| 613 |
return Response(stream_with_context(generate()), mimetype='text/event-stream')
|
| 614 |
|
|
|
|
| 708 |
q = _safe_json().get('query', '')
|
| 709 |
if not q:
|
| 710 |
return jsonify({'results': []})
|
| 711 |
+
results = web_search_duckduckgo(q, n=5)
|
| 712 |
return jsonify({'results': results})
|
| 713 |
|
| 714 |
@app.route('/api/image-search', methods=['POST'])
|
|
|
|
| 735 |
'status': 'ok',
|
| 736 |
'model': FLASH_MODEL,
|
| 737 |
'provider': 'pollinations',
|
| 738 |
+
'search': 'local (fast)',
|
|
|
|
| 739 |
})
|
| 740 |
|
| 741 |
# ======================== Error handlers ========================
|
|
|
|
| 749 |
|
| 750 |
if __name__ == '__main__':
|
| 751 |
port = int(os.environ.get('PORT', 7860))
|
| 752 |
+
logger.info(f"Genisi running on port {port} | Flash: {FLASH_MODEL} | Search: direct+local")
|
| 753 |
app.run(host='0.0.0.0', port=port, threaded=True)
|