bahi-bh commited on
Commit
074ba56
·
verified ·
1 Parent(s): c7ecb89

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +400 -129
app.py CHANGED
@@ -209,22 +209,33 @@ def extract_text_from_content(content) -> str:
209
  tool_content = item.get("content", "")
210
  tool_use_id = item.get("tool_use_id", "")
211
  # ======================================================
212
- # [إصلاح #3] - ربط نتيجة الأداة بمعرفها tool_use_id
 
213
  # ======================================================
214
- if tool_use_id:
215
- parts.append(f"[Tool Result for {tool_use_id}]:")
216
  if isinstance(tool_content, list):
 
217
  for tc in tool_content:
218
  if isinstance(tc, dict) and tc.get("type") == "text":
219
- parts.append(tc.get("text", ""))
220
  elif isinstance(tc, str):
221
- parts.append(tc)
 
222
  elif isinstance(tool_content, str):
223
- parts.append(tool_content)
 
 
 
 
 
 
 
 
 
 
 
224
  elif item.get("type") == "tool_use":
225
  tool_name = item.get("name", "unknown")
226
  tool_input = item.get("input", {})
227
- # الحفاظ على tool_id لربط الاستجابة لاحقاً
228
  tool_id = item.get("id", "")
229
  parts.append(
230
  f"[Called tool: {tool_name} (id:{tool_id}) "
@@ -248,47 +259,35 @@ class ToolCallParser:
248
  يدعم أنماطاً متعددة مع regex محسّن يقبل المسافات والأسطر الجديدة.
249
  """
250
 
251
- # ======================================================
252
- # [إصلاح #2] - تحديث الـ Regex ليكون non-greedy
253
- # ويقبل المسافات البيضاء والأسطر الجديدة قبل وبعد الوسوم
254
- # ======================================================
255
  PATTERNS = [
256
- # النمط 1: <tool_call> مع مسافات مرنة حول الوسم والـ JSON
257
  re.compile(
258
  r'\s*<tool_call>\s*(\{.*?\})\s*</tool_call>\s*',
259
  re.DOTALL | re.IGNORECASE
260
  ),
261
- # النمط 2: ```tool_call أو ```json مع مسافات مرنة
262
  re.compile(
263
  r'```(?:tool_call|json)?\s*\n?\s*(\{.*?"(?:name|function)".*?\})\s*\n?\s*```',
264
  re.DOTALL | re.IGNORECASE
265
  ),
266
- # النمط 3: <function=name>JSON</function>
267
  re.compile(
268
  r'\s*<function=(\w+)>\s*(\{.*?\})\s*</function>\s*',
269
  re.DOTALL | re.IGNORECASE
270
  ),
271
- # النمط 4: [TOOL_CALL]...[/TOOL_CALL]
272
  re.compile(
273
  r'\s*\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]\s*',
274
  re.DOTALL | re.IGNORECASE
275
  ),
276
- # النمط 5: ✿FUNCTION✿
277
  re.compile(
278
  r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
279
  re.DOTALL
280
  ),
281
  ]
282
 
283
- # ======================================================
284
- # [إصلاح #2] - علامات البداية محدّثة لتشمل المسافات
285
- # ======================================================
286
  START_MARKERS = [
287
  '<tool_call>',
288
  '```tool_call',
289
  '```json',
290
  '<function=',
291
- '[tool_call]', # نسخة lowercase للمرونة
292
  '[TOOL_CALL]',
293
  '✿function✿',
294
  '✿FUNCTION✿',
@@ -303,16 +302,11 @@ class ToolCallParser:
303
 
304
  @classmethod
305
  def might_contain_tool_call(cls, text: str) -> bool:
306
- """
307
- فحص سريع: هل النص قد يحتوي على بداية tool call؟
308
- [إصلاح #2] - يتجاهل حالة الأحرف ويتعامل مع المسافات
309
- """
310
- # تنظيف النص من المسافات الزائدة ��لفحص
311
  text_stripped = text.strip().lower()
312
  for marker in cls.START_MARKERS:
313
  if marker.lower() in text_stripped:
314
  return True
315
- # فحص إضافي: هل يبدأ بـ { ويحتوي "name"؟ (JSON مباشر)
316
  if text_stripped.startswith('{') and '"name"' in text_stripped:
317
  return True
318
  return False
@@ -323,10 +317,7 @@ class ToolCallParser:
323
  text: str,
324
  available_tools: Optional[List[Dict]] = None
325
  ) -> Tuple[str, List[Dict]]:
326
- """
327
- تحليل النص واستخراج tool calls منه.
328
- [إصلاح #2] - regex محسّن مع non-greedy matching
329
- """
330
  tool_calls = []
331
  clean_text = text
332
 
@@ -480,7 +471,6 @@ class ToolCallParser:
480
  except Exception:
481
  pass
482
 
483
- # تنظيف النص
484
  clean_text = clean_text.strip()
485
  clean_text = re.sub(r'\n{3,}', '\n\n', clean_text)
486
  clean_text = re.sub(r' +', ' ', clean_text)
@@ -599,12 +589,12 @@ class ToolCallParser:
599
 
600
 
601
  # =====================================================
602
- # STREAM TOOL BUFFER - محسّن
603
  # =====================================================
604
  class StreamToolBuffer:
605
  """
606
  Buffer ذكي يجمع chunks الـ stream ويكتشف tool calls.
607
- [إصلاح #2] - تحديث _process_text_mode لاكتشاف أفضل
608
  """
609
 
610
  def __init__(self, available_tools: Optional[List[Dict]] = None):
@@ -614,12 +604,15 @@ class StreamToolBuffer:
614
  self.available_tools = available_tools or []
615
  self.pending_text = ""
616
  self.tool_call_depth = 0
 
 
 
 
 
 
617
 
618
  def feed(self, chunk: str) -> List[Dict]:
619
- """
620
- إطعام chunk جديد للـ buffer.
621
- يُرجع قائمة من الأحداث الجاهزة للإرسال.
622
- """
623
  events = []
624
  self.buffer += chunk
625
 
@@ -637,30 +630,19 @@ class StreamToolBuffer:
637
  return events
638
 
639
  def _process_text_mode(self) -> List[Dict]:
640
- """
641
- معالجة النص في النمط العادي.
642
- [إصلاح #2] - يتجاهل حالة الأحرف ويتعامل مع المسافات قبل الوسوم
643
- """
644
  events = []
645
-
646
- # ======================================================
647
- # [إصلاح #2] البحث بدون حساسية لحالة الأحرف
648
- # ======================================================
649
  buffer_lower = self.buffer.lower()
650
-
651
  earliest_pos = -1
652
  earliest_marker = ""
653
 
654
  for marker in ToolCallParser.START_MARKERS:
655
- # البحث بدون حساسية لحالة الأحرف
656
  pos = buffer_lower.find(marker.lower())
657
  if pos != -1 and (earliest_pos == -1 or pos < earliest_pos):
658
  earliest_pos = pos
659
  earliest_marker = marker
660
 
661
  if earliest_pos == -1:
662
- # لا يوجد tool call - احتفظ بـ 100 حرف كاحتياط (زيادة من 50)
663
- # لأن بعض الوسوم قد تكون أطول
664
  safe_length = len(self.buffer) - 100
665
  if safe_length > 0:
666
  text_to_send = self.buffer[:safe_length]
@@ -668,34 +650,43 @@ class StreamToolBuffer:
668
  if text_to_send:
669
  events.append({"type": "text", "text": text_to_send})
670
  else:
671
- # ======================================================
672
- # [إصلاح #2] تخطي المسافات البيضاء قبل الوسم
673
- # ======================================================
674
- # إرسال النص قبل الوسم (مع إزالة المسافات الزائدة قبله)
675
  text_before = self.buffer[:earliest_pos]
676
-
677
- # لا نرسل إذا كان النص قبله مجرد مسافات
678
  if text_before and not text_before.isspace():
679
  events.append({"type": "text", "text": text_before})
680
  elif text_before and text_before.isspace():
681
- # مسافات قبل الوسم - نرسلها كمسافة واحدة
682
  events.append({"type": "text", "text": " "})
683
 
684
- # الانتقال لنمط tool call
685
  self.buffer = self.buffer[earliest_pos:]
686
  self.in_tool_call = True
687
  self.tool_call_buffer = ""
 
 
 
 
 
688
 
689
  return events
690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  def _process_tool_call_mode(self) -> List[Dict]:
692
- """
693
- معالجة النص في نمط tool call.
694
- [إصلاح #2] - تحسين اكتشاف نهاية الوسوم مع المسافات
695
- """
696
  events = []
697
 
698
- # أزواج (نهاية الوسم، بداية الوسم) مع دعم حالة الأحرف
699
  end_markers_pairs = [
700
  ('</tool_call>', '<tool_call>'),
701
  ('```', '```tool_call'),
@@ -712,29 +703,21 @@ class StreamToolBuffer:
712
  end_lower = end_marker.lower()
713
  start_lower = start_marker.lower()
714
 
715
- # التحقق أن الـ buffer يبدأ بعلامة البداية (بدون حساسية)
716
  if not buffer_lower.startswith(start_lower):
717
  continue
718
 
719
- # البحث عن نهاية الوسم بعد البداية
720
  search_from = len(start_marker)
721
  end_pos = buffer_lower.find(end_lower, search_from)
722
 
723
  if end_pos != -1:
724
- # وجدنا النهاية - استخراج النص الكامل للوسم
725
  full_tool_text = self.buffer[:end_pos + len(end_marker)]
726
  remaining = self.buffer[end_pos + len(end_marker):]
727
 
728
- # تحليل الـ tool call
729
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(
730
  full_tool_text, self.available_tools
731
  )
732
 
733
- # ======================================================
734
- # [إصلاح #3] إرسال tool calls مع تأكيد المعرف
735
- # ======================================================
736
  for tc in tool_calls:
737
- # التأكد من وجود معرف صحيح
738
  if not tc.get("id"):
739
  tc["id"] = ToolCallParser.generate_tool_id()
740
  logger.info(
@@ -748,11 +731,12 @@ class StreamToolBuffer:
748
 
749
  self.buffer = remaining
750
  self.in_tool_call = False
 
 
751
  found_end = True
752
  break
753
 
754
  if not found_end:
755
- # لم نجد نهاية - تحقق من الحجم
756
  if len(self.buffer) > 5000:
757
  logger.warning(
758
  "[Buffer] Tool call buffer too large without closing tag, "
@@ -761,29 +745,152 @@ class StreamToolBuffer:
761
  events.append({"type": "text", "text": self.buffer})
762
  self.buffer = ""
763
  self.in_tool_call = False
 
 
764
 
765
  return events
766
 
767
  def flush(self) -> List[Dict]:
768
- """تفريغ أي محتوى متبقي في الـ buffer"""
 
 
 
 
 
769
  events = []
770
 
771
  if self.in_tool_call and self.buffer:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(
773
- self.buffer, self.available_tools
774
  )
775
- for tc in tool_calls:
776
- if not tc.get("id"):
777
- tc["id"] = ToolCallParser.generate_tool_id()
778
- events.append(tc)
779
- if clean_text.strip():
780
- events.append({"type": "text", "text": clean_text})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  elif self.buffer:
782
  if self.buffer.strip():
783
  events.append({"type": "text", "text": self.buffer})
784
 
 
785
  self.buffer = ""
786
  self.in_tool_call = False
 
 
 
787
  return events
788
 
789
 
@@ -883,7 +990,7 @@ class ToolsFormatter:
883
  def format_tool_result_for_message(tool_use_id: str, content: Any) -> str:
884
  """
885
  تحويل نتيجة أداة إلى نص مفهوم للنموذج.
886
- [إصلاح #3] - ربط النتيجة بمعرف الأداة tool_use_id
887
  """
888
  result_text = ""
889
  if isinstance(content, str):
@@ -907,19 +1014,18 @@ class ToolsFormatter:
907
  result_text = str(content) if content else ""
908
 
909
  # ======================================================
910
- # [إصلاح #3] - تأكيد ربط النتيجة بالمعرف الصحيح
 
911
  # ======================================================
912
  return (
913
- f"[Tool Result]\n"
914
- f"tool_use_id: {tool_use_id}\n"
915
- f"status: success\n"
916
- f"output:\n{result_text}\n"
917
- f"[/Tool Result]"
918
  )
919
 
920
 
921
  # =====================================================
922
- # MESSAGE CONVERTER
923
  # =====================================================
924
  class MessageConverter:
925
  """تحويل رسائل Anthropic إلى صيغة g4f"""
@@ -969,6 +1075,42 @@ class MessageConverter:
969
  else:
970
  user_message = ""
971
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972
  if full_system:
973
  full_message = (
974
  f"[System Instructions]\n{full_system}\n"
@@ -1010,7 +1152,7 @@ class MessageConverter:
1010
 
1011
  elif block_type == "tool_result":
1012
  # ======================================================
1013
- # [إصلاح #3] - ربط نتيجة الأداة بمعرفها الصحيح
1014
  # ======================================================
1015
  tool_use_id = block.get("tool_use_id", "")
1016
  result_content = block.get("content", "")
@@ -1040,9 +1182,20 @@ class MessageConverter:
1040
 
1041
 
1042
  # =====================================================
1043
- # CHAT LOGIC - مع إصلاح الذاكرة الانتقائية
1044
  # =====================================================
1045
- def ask(message: str, history, provider_name: str, model_name: str, stop_flag=None):
 
 
 
 
 
 
 
 
 
 
 
1046
  message = (message or "").strip()
1047
  if not message:
1048
  yield ""
@@ -1061,19 +1214,14 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1061
  if isinstance(history[0], dict):
1062
  # ======================================================
1063
  # [إصلاح #1] - الذاكرة الانتقائية (Selective History)
1064
- # الاحتفاظ بأول 10 رسائل (System Prompt + Tool Definitions)
1065
- # ودمجها مع آخر 50 رسالة للسياق الحالي
1066
- # السطر 1031 المشار إليه في التقرير
1067
  # ======================================================
1068
  if len(history) > 60:
1069
- # الاحتفاظ بأول 10 رسائل + آخر 50 رسالة
1070
  smart_history = history[:10] + history[-50:]
1071
  logger.info(
1072
  f"[Memory] Smart history: keeping first 10 + "
1073
  f"last 50 from {len(history)} total messages"
1074
  )
1075
  else:
1076
- # إذا كانت الرسائل أقل من 60، نأخذها كلها
1077
  smart_history = history
1078
  logger.info(
1079
  f"[Memory] Full history: {len(history)} messages "
@@ -1092,7 +1240,6 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1092
  if text:
1093
  msgs.append({"role": str(role), "content": text})
1094
  else:
1095
- # صيغة قديمة (list of tuples)
1096
  if len(history) > 30:
1097
  smart_history_tuples = history[:5] + history[-25:]
1098
  else:
@@ -1107,7 +1254,45 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1107
  except Exception as e:
1108
  logger.warning(f"[Memory] History error: {e}")
1109
 
1110
- msgs.append({"role": "user", "content": message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1111
 
1112
  # قائمة المزودات للـ fallback
1113
  fallback_providers = [
@@ -1331,7 +1516,7 @@ async def v1_messages(request: Request):
1331
 
1332
  # Non-stream: تجميع الرد كاملاً
1333
  full_response = ""
1334
- for chunk in ask(full_message, history, "Blackbox", model):
1335
  full_response += chunk
1336
 
1337
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, tools)
@@ -1348,9 +1533,6 @@ async def v1_messages(request: Request):
1348
  "text": clean_text.strip()
1349
  })
1350
 
1351
- # ======================================================
1352
- # [إصلاح #3] - إرسال tool calls مع معرفات صحيحة
1353
- # ======================================================
1354
  for tc in tool_calls:
1355
  tool_id = tc.get("id") or ToolCallParser.generate_tool_id()
1356
  content_blocks.append({
@@ -1402,7 +1584,6 @@ async def _handle_anthropic_stream(
1402
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
1403
  tools_list = tools or []
1404
 
1405
- # حدث بداية الرسالة
1406
  msg_start = {
1407
  "type": "message_start",
1408
  "message": {
@@ -1431,7 +1612,6 @@ async def _handle_anthropic_stream(
1431
  output_tokens = 0
1432
  has_tool_calls = False
1433
 
1434
- # ---- دوال مساعدة ----
1435
  def make_text_block_start(index: int) -> str:
1436
  block_start = {
1437
  "type": "content_block_start",
@@ -1465,14 +1645,8 @@ async def _handle_anthropic_stream(
1465
  )
1466
 
1467
  def make_tool_use_events(index: int, tool_call: Dict) -> str:
1468
- """
1469
- إنشاء أحداث tool_use للـ stream.
1470
- [إصلاح #3] - استخدام المعرف الصحيح من tool_call
1471
- """
1472
  events_str = ""
1473
- # ======================================================
1474
- # [إصلاح #3] - ضمان وجود معرف صحيح
1475
- # ======================================================
1476
  tool_id = tool_call.get("id") or ToolCallParser.generate_tool_id()
1477
  tool_name = tool_call.get("name", "unknown")
1478
  tool_input = tool_call.get("input", {})
@@ -1496,7 +1670,6 @@ async def _handle_anthropic_stream(
1496
  f"data: {json.dumps(block_start, ensure_ascii=False)}\n\n"
1497
  )
1498
 
1499
- # إرسال input كـ JSON delta
1500
  input_json = json.dumps(tool_input, ensure_ascii=False)
1501
  chunk_size = 100
1502
  for i in range(0, len(input_json), chunk_size):
@@ -1519,21 +1692,19 @@ async def _handle_anthropic_stream(
1519
 
1520
  # ===== معالجة الـ Stream =====
1521
  try:
1522
- for chunk in ask(full_message, history, "Blackbox", model):
1523
  if not chunk:
1524
  continue
1525
 
1526
  output_tokens += max(1, len(chunk) // 4)
1527
 
1528
  if not tools_list:
1529
- # لا أدوات - نرسل كنص مباشرة
1530
  if not text_block_open:
1531
  yield make_text_block_start(current_block_index)
1532
  text_block_open = True
1533
  yield make_text_delta(current_block_index, chunk)
1534
  continue
1535
 
1536
- # هناك أدوات - نستخدم الـ buffer
1537
  events = tool_buffer.feed(chunk)
1538
 
1539
  for event in events:
@@ -1546,7 +1717,6 @@ async def _handle_anthropic_stream(
1546
  yield make_text_delta(current_block_index, text)
1547
 
1548
  elif event.get("type") == "tool_use":
1549
- # إغلاق كتلة النص المفتوحة
1550
  if text_block_open:
1551
  yield make_block_stop(current_block_index)
1552
  current_block_index += 1
@@ -1556,8 +1726,10 @@ async def _handle_anthropic_stream(
1556
  yield make_tool_use_events(current_block_index, event)
1557
  current_block_index += 1
1558
 
1559
- # ===== تفريغ الـ buffer المتبقي =====
 
1560
  remaining_events = tool_buffer.flush()
 
1561
  for event in remaining_events:
1562
  if event.get("type") == "text":
1563
  text = event["text"]
@@ -1574,6 +1746,10 @@ async def _handle_anthropic_stream(
1574
  text_block_open = False
1575
 
1576
  has_tool_calls = True
 
 
 
 
1577
  yield make_tool_use_events(current_block_index, event)
1578
  current_block_index += 1
1579
 
@@ -1584,7 +1760,6 @@ async def _handle_anthropic_stream(
1584
  text_block_open = True
1585
  yield make_text_delta(current_block_index, f"\n\n[Error: {str(e)}]")
1586
 
1587
- # إغلاق آخر كتلة مفتوحة
1588
  if text_block_open:
1589
  yield make_block_stop(current_block_index)
1590
  elif current_block_index == 0 and not has_tool_calls:
@@ -1756,11 +1931,12 @@ async def v1_chat_completions(request: Request):
1756
  async def root():
1757
  return {
1758
  "message": "G4F Smart Router - Protocol Translator (Anthropic Compatible)",
1759
- "version": "2.1.0",
1760
  "fixes_applied": [
1761
- "[Fix #1] Selective Memory: history[:10] + history[-50:]",
1762
- "[Fix #2] Improved Regex: non-greedy + case-insensitive + whitespace handling",
1763
- "[Fix #3] Tool ID Binding: tool_use_id properly linked in results"
 
1764
  ],
1765
  "providers": list(REAL_PROVIDERS.keys()),
1766
  "endpoints": {
@@ -1774,6 +1950,7 @@ async def root():
1774
  "POST /chat/stream": "Simple stream (AUTH)",
1775
  "GET /providers": "Providers list (AUTH)",
1776
  "GET /debug/test-tool-parse": "Test tool parsing (AUTH)",
 
1777
  },
1778
  "cookies": COOKIE_STATUS,
1779
  "status": "✅ Server is working"
@@ -1784,14 +1961,15 @@ async def root():
1784
  async def health():
1785
  return {
1786
  "status": "ok",
1787
- "version": "2.1.0",
1788
  "cookies": COOKIE_STATUS,
1789
  "providers": list(REAL_PROVIDERS.keys()),
1790
  "provider_count": len(REAL_PROVIDERS),
1791
  "fixes": {
1792
- "selective_memory": True,
1793
- "improved_regex": True,
1794
- "tool_id_binding": True
 
1795
  },
1796
  "timestamp": int(time.time())
1797
  }
@@ -1873,6 +2051,14 @@ async def debug_test_tool_parse(request: Request):
1873
  '</TOOL_CALL>'
1874
  ),
1875
  },
 
 
 
 
 
 
 
 
1876
  {
1877
  "name": "No tool calls (plain text)",
1878
  "input": "This is just a regular response with no tool calls.",
@@ -1882,6 +2068,18 @@ async def debug_test_tool_parse(request: Request):
1882
  results = []
1883
  for tc in test_cases:
1884
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(tc["input"])
 
 
 
 
 
 
 
 
 
 
 
 
1885
  results.append({
1886
  "test_name": tc["name"],
1887
  "input_preview": (
@@ -1892,7 +2090,9 @@ async def debug_test_tool_parse(request: Request):
1892
  "clean_text": clean_text,
1893
  "tool_calls_found": len(tool_calls),
1894
  "tool_calls": tool_calls,
1895
- "has_tool_call_marker": ToolCallParser.might_contain_tool_call(tc["input"])
 
 
1896
  })
1897
 
1898
  return {"test_results": results}
@@ -1932,6 +2132,74 @@ async def debug_test_stream_buffer(request: Request):
1932
  }
1933
 
1934
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1935
  @app.post("/chat")
1936
  async def chat(request: Request, chat_req: ChatRequest):
1937
  verify_api_key(request)
@@ -2050,9 +2318,12 @@ async def rate_limit_handler(request: Request, exc: HTTPException):
2050
  if __name__ == "__main__":
2051
  import uvicorn
2052
  port = int(os.getenv("PORT", 7860))
2053
- logger.info(f"Starting G4F Smart Router v2.1 on port {port}")
2054
  logger.info(f"Cookies: {COOKIE_STATUS}")
2055
  logger.info(f"Providers: {list(REAL_PROVIDERS.keys())}")
2056
- logger.info("Fixes: [Memory] [Regex] [Tool-ID-Binding]")
 
 
 
 
2057
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
2058
-
 
209
  tool_content = item.get("content", "")
210
  tool_use_id = item.get("tool_use_id", "")
211
  # ======================================================
212
+ # [إصلاح #2] - تحويل tool_result إلى نص بسيط وواضح
213
+ # بدلاً من الصيغة المعقدة، نستخدم نصاً مباشراً يفهمه النموذج
214
  # ======================================================
 
 
215
  if isinstance(tool_content, list):
216
+ result_texts = []
217
  for tc in tool_content:
218
  if isinstance(tc, dict) and tc.get("type") == "text":
219
+ result_texts.append(tc.get("text", ""))
220
  elif isinstance(tc, str):
221
+ result_texts.append(tc)
222
+ result_str = "\n".join(result_texts)
223
  elif isinstance(tool_content, str):
224
+ result_str = tool_content
225
+ else:
226
+ result_str = str(tool_content)
227
+ # ======================================================
228
+ # [إصلاح #2] - صيغة مبسطة تأمر النموذج بالتنفيذ الفوري
229
+ # ======================================================
230
+ parts.append(
231
+ f"[SYSTEM]: Tool '{tool_use_id}' returned the following data:\n"
232
+ f"{result_str}\n"
233
+ f"[SYSTEM]: Research results obtained. Now use 'write_file' to save them. "
234
+ f"DO NOT explain. Execute immediately."
235
+ )
236
  elif item.get("type") == "tool_use":
237
  tool_name = item.get("name", "unknown")
238
  tool_input = item.get("input", {})
 
239
  tool_id = item.get("id", "")
240
  parts.append(
241
  f"[Called tool: {tool_name} (id:{tool_id}) "
 
259
  يدعم أنماطاً متعددة مع regex محسّن يقبل المسافات والأسطر الجديدة.
260
  """
261
 
 
 
 
 
262
  PATTERNS = [
 
263
  re.compile(
264
  r'\s*<tool_call>\s*(\{.*?\})\s*</tool_call>\s*',
265
  re.DOTALL | re.IGNORECASE
266
  ),
 
267
  re.compile(
268
  r'```(?:tool_call|json)?\s*\n?\s*(\{.*?"(?:name|function)".*?\})\s*\n?\s*```',
269
  re.DOTALL | re.IGNORECASE
270
  ),
 
271
  re.compile(
272
  r'\s*<function=(\w+)>\s*(\{.*?\})\s*</function>\s*',
273
  re.DOTALL | re.IGNORECASE
274
  ),
 
275
  re.compile(
276
  r'\s*\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]\s*',
277
  re.DOTALL | re.IGNORECASE
278
  ),
 
279
  re.compile(
280
  r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
281
  re.DOTALL
282
  ),
283
  ]
284
 
 
 
 
285
  START_MARKERS = [
286
  '<tool_call>',
287
  '```tool_call',
288
  '```json',
289
  '<function=',
290
+ '[tool_call]',
291
  '[TOOL_CALL]',
292
  '✿function✿',
293
  '✿FUNCTION✿',
 
302
 
303
  @classmethod
304
  def might_contain_tool_call(cls, text: str) -> bool:
305
+ """فحص سريع: هل النص قد يحتوي على بداية tool call؟"""
 
 
 
 
306
  text_stripped = text.strip().lower()
307
  for marker in cls.START_MARKERS:
308
  if marker.lower() in text_stripped:
309
  return True
 
310
  if text_stripped.startswith('{') and '"name"' in text_stripped:
311
  return True
312
  return False
 
317
  text: str,
318
  available_tools: Optional[List[Dict]] = None
319
  ) -> Tuple[str, List[Dict]]:
320
+ """تحليل النص واستخراج tool calls منه."""
 
 
 
321
  tool_calls = []
322
  clean_text = text
323
 
 
471
  except Exception:
472
  pass
473
 
 
474
  clean_text = clean_text.strip()
475
  clean_text = re.sub(r'\n{3,}', '\n\n', clean_text)
476
  clean_text = re.sub(r' +', ' ', clean_text)
 
589
 
590
 
591
  # =====================================================
592
+ # STREAM TOOL BUFFER - مع إصلاح #4 (إغلاق الوسوم يدوياً)
593
  # =====================================================
594
  class StreamToolBuffer:
595
  """
596
  Buffer ذكي يجمع chunks الـ stream ويكتشف tool calls.
597
+ [إصلاح #4] - إغلاق الوسوم يدوياً في flush() إذا لم تكتمل
598
  """
599
 
600
  def __init__(self, available_tools: Optional[List[Dict]] = None):
 
604
  self.available_tools = available_tools or []
605
  self.pending_text = ""
606
  self.tool_call_depth = 0
607
+ # ======================================================
608
+ # [إصلاح #4] - تتبع بداية الوسم لإغلاقه عند الضرورة
609
+ # ======================================================
610
+ self._active_start_marker = ""
611
+ self._active_end_marker = ""
612
+ self._stream_started_at = time.time()
613
 
614
  def feed(self, chunk: str) -> List[Dict]:
615
+ """إطعام chunk جديد للـ buffer."""
 
 
 
616
  events = []
617
  self.buffer += chunk
618
 
 
630
  return events
631
 
632
  def _process_text_mode(self) -> List[Dict]:
633
+ """معالجة النص في النمط العادي."""
 
 
 
634
  events = []
 
 
 
 
635
  buffer_lower = self.buffer.lower()
 
636
  earliest_pos = -1
637
  earliest_marker = ""
638
 
639
  for marker in ToolCallParser.START_MARKERS:
 
640
  pos = buffer_lower.find(marker.lower())
641
  if pos != -1 and (earliest_pos == -1 or pos < earliest_pos):
642
  earliest_pos = pos
643
  earliest_marker = marker
644
 
645
  if earliest_pos == -1:
 
 
646
  safe_length = len(self.buffer) - 100
647
  if safe_length > 0:
648
  text_to_send = self.buffer[:safe_length]
 
650
  if text_to_send:
651
  events.append({"type": "text", "text": text_to_send})
652
  else:
 
 
 
 
653
  text_before = self.buffer[:earliest_pos]
 
 
654
  if text_before and not text_before.isspace():
655
  events.append({"type": "text", "text": text_before})
656
  elif text_before and text_before.isspace():
 
657
  events.append({"type": "text", "text": " "})
658
 
 
659
  self.buffer = self.buffer[earliest_pos:]
660
  self.in_tool_call = True
661
  self.tool_call_buffer = ""
662
+ # ======================================================
663
+ # [إصلاح #4] - حفظ بداية الوسم النشطة لإغلاقه لاحقاً
664
+ # ======================================================
665
+ self._active_start_marker = earliest_marker
666
+ self._set_active_end_marker(earliest_marker)
667
 
668
  return events
669
 
670
+ def _set_active_end_marker(self, start_marker: str):
671
+ """تحديد علامة النهاية المقابلة لعلامة البداية"""
672
+ start_lower = start_marker.lower()
673
+ if start_lower == '<tool_call>':
674
+ self._active_end_marker = '</tool_call>'
675
+ elif start_lower in ('```tool_call', '```json', '```'):
676
+ self._active_end_marker = '```'
677
+ elif start_lower.startswith('<function='):
678
+ self._active_end_marker = '</function>'
679
+ elif start_lower in ('[tool_call]', '[tool_call]'):
680
+ self._active_end_marker = '[/TOOL_CALL]'
681
+ elif start_lower == '✿function✿':
682
+ self._active_end_marker = ''
683
+ else:
684
+ self._active_end_marker = '</tool_call>'
685
+
686
  def _process_tool_call_mode(self) -> List[Dict]:
687
+ """معالجة النص في نمط tool call."""
 
 
 
688
  events = []
689
 
 
690
  end_markers_pairs = [
691
  ('</tool_call>', '<tool_call>'),
692
  ('```', '```tool_call'),
 
703
  end_lower = end_marker.lower()
704
  start_lower = start_marker.lower()
705
 
 
706
  if not buffer_lower.startswith(start_lower):
707
  continue
708
 
 
709
  search_from = len(start_marker)
710
  end_pos = buffer_lower.find(end_lower, search_from)
711
 
712
  if end_pos != -1:
 
713
  full_tool_text = self.buffer[:end_pos + len(end_marker)]
714
  remaining = self.buffer[end_pos + len(end_marker):]
715
 
 
716
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(
717
  full_tool_text, self.available_tools
718
  )
719
 
 
 
 
720
  for tc in tool_calls:
 
721
  if not tc.get("id"):
722
  tc["id"] = ToolCallParser.generate_tool_id()
723
  logger.info(
 
731
 
732
  self.buffer = remaining
733
  self.in_tool_call = False
734
+ self._active_start_marker = ""
735
+ self._active_end_marker = ""
736
  found_end = True
737
  break
738
 
739
  if not found_end:
 
740
  if len(self.buffer) > 5000:
741
  logger.warning(
742
  "[Buffer] Tool call buffer too large without closing tag, "
 
745
  events.append({"type": "text", "text": self.buffer})
746
  self.buffer = ""
747
  self.in_tool_call = False
748
+ self._active_start_marker = ""
749
+ self._active_end_marker = ""
750
 
751
  return events
752
 
753
  def flush(self) -> List[Dict]:
754
+ """
755
+ تفريغ أي محتوى متبقي في الـ buffer.
756
+ ======================================================
757
+ [إصلاح #4] - إغلاق الوسوم يدوياً إذا بدأت ولم تنتهِ
758
+ ======================================================
759
+ """
760
  events = []
761
 
762
  if self.in_tool_call and self.buffer:
763
+ # ======================================================
764
+ # [إصلاح #4] الخطوة 1: محاولة إغلاق الوسم يدوياً
765
+ # إذا اكتشفنا أن الـ stream انقطع قبل إرسال وسم الإغلاق
766
+ # ======================================================
767
+ logger.warning(
768
+ f"[Buffer][Fix#4] Stream ended with unclosed tool_call tag. "
769
+ f"Active marker: '{self._active_start_marker}'. "
770
+ f"Attempting manual closure..."
771
+ )
772
+
773
+ # محاولة إضافة وسم الإغلاق المناسب
774
+ end_marker = self._active_end_marker or "</tool_call>"
775
+
776
+ # التحقق إذا كان الـ buffer يحتوي JSON ناقص
777
+ buffer_with_close = self.buffer + end_marker
778
+ logger.info(
779
+ f"[Buffer][Fix#4] Trying to parse with appended '{end_marker}'"
780
+ )
781
+
782
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(
783
+ buffer_with_close, self.available_tools
784
  )
785
+
786
+ if tool_calls:
787
+ # نجح الإغلاق اليدوي
788
+ logger.info(
789
+ f"[Buffer][Fix#4] Manual closure SUCCESS: "
790
+ f"recovered {len(tool_calls)} tool call(s)"
791
+ )
792
+ for tc in tool_calls:
793
+ if not tc.get("id"):
794
+ tc["id"] = ToolCallParser.generate_tool_id()
795
+ events.append(tc)
796
+ logger.info(
797
+ f"[Buffer][Fix#4] Recovered tool: {tc['name']} "
798
+ f"(id: {tc['id']})"
799
+ )
800
+ if clean_text.strip():
801
+ events.append({"type": "text", "text": clean_text})
802
+ else:
803
+ # ======================================================
804
+ # [إصلاح #4] الخطوة 2: محاولة إصلاح JSON الناقص
805
+ # قبل إضافة وسم الإغلاق
806
+ # ======================================================
807
+ logger.info(
808
+ "[Buffer][Fix#4] Direct closure failed. "
809
+ "Attempting JSON repair..."
810
+ )
811
+
812
+ # استخراج JSON من الـ buffer
813
+ raw_buffer = self.buffer
814
+ # إزالة علامة البداية إن وجدت
815
+ if self._active_start_marker:
816
+ start_lower = self._active_start_marker.lower()
817
+ raw_lower = raw_buffer.lower()
818
+ if raw_lower.startswith(start_lower):
819
+ raw_buffer = raw_buffer[len(self._active_start_marker):]
820
+
821
+ # محاولة إصلاح JSON الناقص
822
+ json_start = raw_buffer.find('{')
823
+ if json_start != -1:
824
+ json_fragment = raw_buffer[json_start:].strip()
825
+
826
+ # إضافة أقواس مفقودة
827
+ open_braces = json_fragment.count('{') - json_fragment.count('}')
828
+ if open_braces > 0:
829
+ json_fragment += '}' * open_braces
830
+ logger.info(
831
+ f"[Buffer][Fix#4] Added {open_braces} closing brace(s) "
832
+ f"to fix JSON"
833
+ )
834
+
835
+ open_brackets = json_fragment.count('[') - json_fragment.count(']')
836
+ if open_brackets > 0:
837
+ json_fragment += ']' * open_brackets
838
+
839
+ # إضافة وسم الإغلاق بعد إصلاح JSON
840
+ repaired_text = (
841
+ self._active_start_marker +
842
+ json_fragment +
843
+ end_marker
844
+ )
845
+
846
+ clean_text2, tool_calls2 = ToolCallParser.parse_tool_calls(
847
+ repaired_text, self.available_tools
848
+ )
849
+
850
+ if tool_calls2:
851
+ logger.info(
852
+ f"[Buffer][Fix#4] JSON repair SUCCESS: "
853
+ f"recovered {len(tool_calls2)} tool call(s)"
854
+ )
855
+ for tc in tool_calls2:
856
+ if not tc.get("id"):
857
+ tc["id"] = ToolCallParser.generate_tool_id()
858
+ events.append(tc)
859
+ logger.info(
860
+ f"[Buffer][Fix#4] Repaired tool: {tc['name']} "
861
+ f"(id: {tc['id']})"
862
+ )
863
+ if clean_text2.strip():
864
+ events.append({"type": "text", "text": clean_text2})
865
+ else:
866
+ # فشل كل شيء - إرسال كنص عادي مع تحذير
867
+ logger.error(
868
+ "[Buffer][Fix#4] All repair attempts failed. "
869
+ "Sending as raw text."
870
+ )
871
+ if self.buffer.strip():
872
+ events.append({
873
+ "type": "text",
874
+ "text": (
875
+ f"[⚠️ Incomplete tool call recovered]\n"
876
+ f"{self.buffer}"
877
+ )
878
+ })
879
+ else:
880
+ # لا يوجد JSON قابل للاستخراج
881
+ if self.buffer.strip():
882
+ events.append({"type": "text", "text": self.buffer})
883
+
884
  elif self.buffer:
885
  if self.buffer.strip():
886
  events.append({"type": "text", "text": self.buffer})
887
 
888
+ # تنظيف الحالة
889
  self.buffer = ""
890
  self.in_tool_call = False
891
+ self._active_start_marker = ""
892
+ self._active_end_marker = ""
893
+
894
  return events
895
 
896
 
 
990
  def format_tool_result_for_message(tool_use_id: str, content: Any) -> str:
991
  """
992
  تحويل نتيجة أداة إلى نص مفهوم للنموذج.
993
+ [إصلاح #2] - صيغة مبسطة تأمر النموذج بالتنفيذ الفوري
994
  """
995
  result_text = ""
996
  if isinstance(content, str):
 
1014
  result_text = str(content) if content else ""
1015
 
1016
  # ======================================================
1017
+ # [إصلاح #2] - صيغة مبسطة بدلاً من الصيغة المعقدة
1018
+ # تأمر النموذج بالتنفيذ الفوري بدون شرح
1019
  # ======================================================
1020
  return (
1021
+ f"[SYSTEM]: Research results obtained for tool_use_id={tool_use_id}.\n"
1022
+ f"Data:\n{result_text}\n"
1023
+ f"[SYSTEM]: Now use 'write_file' to save them. DO NOT explain. Execute immediately."
 
 
1024
  )
1025
 
1026
 
1027
  # =====================================================
1028
+ # MESSAGE CONVERTER - مع إصلاح #3 (فرض نمط الأداة)
1029
  # =====================================================
1030
  class MessageConverter:
1031
  """تحويل رسائل Anthropic إلى صيغة g4f"""
 
1075
  else:
1076
  user_message = ""
1077
 
1078
+ # ======================================================
1079
+ # [إصلاح #3] - فرض نمط الأداة (Forced Tool Mode)
1080
+ # إذا كانت هناك أدوات، نضيف STRICT MODE في بداية الـ system
1081
+ # ======================================================
1082
+ if tools and len(tools) > 0:
1083
+ tool_names = [t.get("name", "") for t in tools]
1084
+ has_file_tool = any(
1085
+ "file" in name.lower() or "write" in name.lower() or "create" in name.lower()
1086
+ for name in tool_names
1087
+ )
1088
+
1089
+ strict_mode_header = (
1090
+ "[STRICT MODE]: You are an execution engine, not a conversational AI.\n"
1091
+ "Output ONLY <tool_call> blocks when action is needed.\n"
1092
+ "Verbal explanations are FORBIDDEN unless explicitly requested.\n"
1093
+ "If you have data from a previous tool result, use it IMMEDIATELY "
1094
+ "to call the next required tool.\n"
1095
+ "DO NOT summarize, explain, or present data as text.\n"
1096
+ "EXECUTE the tool call NOW.\n"
1097
+ )
1098
+
1099
+ if has_file_tool:
1100
+ strict_mode_header += (
1101
+ "PRIORITY: If research/search results are available, "
1102
+ "call 'write_file' IMMEDIATELY with those results.\n"
1103
+ )
1104
+
1105
+ if full_system:
1106
+ full_system = f"{strict_mode_header}\n{full_system}"
1107
+ else:
1108
+ full_system = strict_mode_header
1109
+
1110
+ logger.info(
1111
+ f"[Fix#3] STRICT MODE activated for tools: {tool_names}"
1112
+ )
1113
+
1114
  if full_system:
1115
  full_message = (
1116
  f"[System Instructions]\n{full_system}\n"
 
1152
 
1153
  elif block_type == "tool_result":
1154
  # ======================================================
1155
+ # [إصلاح #2] - استخدام الصيغة المبسطة للنتائج
1156
  # ======================================================
1157
  tool_use_id = block.get("tool_use_id", "")
1158
  result_content = block.get("content", "")
 
1182
 
1183
 
1184
  # =====================================================
1185
+ # CHAT LOGIC - مع إصلاح #1 (حقن الهدف المستمر)
1186
  # =====================================================
1187
+ def ask(
1188
+ message: str,
1189
+ history,
1190
+ provider_name: str,
1191
+ model_name: str,
1192
+ stop_flag=None,
1193
+ tools: Optional[List[Dict]] = None
1194
+ ):
1195
+ """
1196
+ دالة الدردشة الرئيسية.
1197
+ [إصلاح #1] - حقن الهدف المستمر في كل رسالة مستخدم
1198
+ """
1199
  message = (message or "").strip()
1200
  if not message:
1201
  yield ""
 
1214
  if isinstance(history[0], dict):
1215
  # ======================================================
1216
  # [إصلاح #1] - الذاكرة الانتقائية (Selective History)
 
 
 
1217
  # ======================================================
1218
  if len(history) > 60:
 
1219
  smart_history = history[:10] + history[-50:]
1220
  logger.info(
1221
  f"[Memory] Smart history: keeping first 10 + "
1222
  f"last 50 from {len(history)} total messages"
1223
  )
1224
  else:
 
1225
  smart_history = history
1226
  logger.info(
1227
  f"[Memory] Full history: {len(history)} messages "
 
1240
  if text:
1241
  msgs.append({"role": str(role), "content": text})
1242
  else:
 
1243
  if len(history) > 30:
1244
  smart_history_tuples = history[:5] + history[-25:]
1245
  else:
 
1254
  except Exception as e:
1255
  logger.warning(f"[Memory] History error: {e}")
1256
 
1257
+ # ======================================================
1258
+ # [إصلاح #1] - حقن الهدف المستمر (Persistent Goal Injection)
1259
+ # إضافة تذكير بالمهمة الأصلية في كل رسالة مستخدم
1260
+ # ======================================================
1261
+ has_tools = tools and len(tools) > 0
1262
+ has_tool_result_in_history = any(
1263
+ "[SYSTEM]: Research results obtained" in str(item.get("content", ""))
1264
+ for item in msgs
1265
+ if isinstance(item, dict)
1266
+ )
1267
+
1268
+ if has_tools or has_tool_result_in_history:
1269
+ # فحص إذا كانت الرسالة تحتوي على نتائج أداة سابقة
1270
+ if has_tool_result_in_history or "tool result" in message.lower():
1271
+ goal_injection = (
1272
+ "\n\n[RE-ITERATION]: If you have gathered data from a previous tool, "
1273
+ "DO NOT present it as text. "
1274
+ "Use the 'write_file' tool IMMEDIATELY. "
1275
+ "Your primary mission is EXECUTION, not conversation. "
1276
+ "Output ONLY a <tool_call> block."
1277
+ )
1278
+ else:
1279
+ goal_injection = (
1280
+ "\n\n[RE-ITERATION]: Remember your primary mission is EXECUTION. "
1281
+ "If action is needed, use the appropriate tool via <tool_call> block. "
1282
+ "Do not explain what you will do - just DO IT."
1283
+ )
1284
+
1285
+ # إضافة حقن الهدف للرسالة الحالية
1286
+ message_with_injection = message + goal_injection
1287
+ logger.info(
1288
+ f"[Fix#1] Goal injection added to message. "
1289
+ f"Has tools: {has_tools}, "
1290
+ f"Has tool results: {has_tool_result_in_history}"
1291
+ )
1292
+ else:
1293
+ message_with_injection = message
1294
+
1295
+ msgs.append({"role": "user", "content": message_with_injection})
1296
 
1297
  # قائمة المزودات للـ fallback
1298
  fallback_providers = [
 
1516
 
1517
  # Non-stream: تجميع الرد كاملاً
1518
  full_response = ""
1519
+ for chunk in ask(full_message, history, "Blackbox", model, tools=tools):
1520
  full_response += chunk
1521
 
1522
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, tools)
 
1533
  "text": clean_text.strip()
1534
  })
1535
 
 
 
 
1536
  for tc in tool_calls:
1537
  tool_id = tc.get("id") or ToolCallParser.generate_tool_id()
1538
  content_blocks.append({
 
1584
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
1585
  tools_list = tools or []
1586
 
 
1587
  msg_start = {
1588
  "type": "message_start",
1589
  "message": {
 
1612
  output_tokens = 0
1613
  has_tool_calls = False
1614
 
 
1615
  def make_text_block_start(index: int) -> str:
1616
  block_start = {
1617
  "type": "content_block_start",
 
1645
  )
1646
 
1647
  def make_tool_use_events(index: int, tool_call: Dict) -> str:
1648
+ """إنشاء أحداث tool_use للـ stream."""
 
 
 
1649
  events_str = ""
 
 
 
1650
  tool_id = tool_call.get("id") or ToolCallParser.generate_tool_id()
1651
  tool_name = tool_call.get("name", "unknown")
1652
  tool_input = tool_call.get("input", {})
 
1670
  f"data: {json.dumps(block_start, ensure_ascii=False)}\n\n"
1671
  )
1672
 
 
1673
  input_json = json.dumps(tool_input, ensure_ascii=False)
1674
  chunk_size = 100
1675
  for i in range(0, len(input_json), chunk_size):
 
1692
 
1693
  # ===== معالجة الـ Stream =====
1694
  try:
1695
+ for chunk in ask(full_message, history, "Blackbox", model, tools=tools_list):
1696
  if not chunk:
1697
  continue
1698
 
1699
  output_tokens += max(1, len(chunk) // 4)
1700
 
1701
  if not tools_list:
 
1702
  if not text_block_open:
1703
  yield make_text_block_start(current_block_index)
1704
  text_block_open = True
1705
  yield make_text_delta(current_block_index, chunk)
1706
  continue
1707
 
 
1708
  events = tool_buffer.feed(chunk)
1709
 
1710
  for event in events:
 
1717
  yield make_text_delta(current_block_index, text)
1718
 
1719
  elif event.get("type") == "tool_use":
 
1720
  if text_block_open:
1721
  yield make_block_stop(current_block_index)
1722
  current_block_index += 1
 
1726
  yield make_tool_use_events(current_block_index, event)
1727
  current_block_index += 1
1728
 
1729
+ # ===== تفريغ الـ buffer المتبقي [إصلاح #4] =====
1730
+ logger.info("[Stream] Flushing remaining buffer (Fix#4 active)...")
1731
  remaining_events = tool_buffer.flush()
1732
+
1733
  for event in remaining_events:
1734
  if event.get("type") == "text":
1735
  text = event["text"]
 
1746
  text_block_open = False
1747
 
1748
  has_tool_calls = True
1749
+ logger.info(
1750
+ f"[Stream][Fix#4] Recovered tool call from flush: "
1751
+ f"{event.get('name')}"
1752
+ )
1753
  yield make_tool_use_events(current_block_index, event)
1754
  current_block_index += 1
1755
 
 
1760
  text_block_open = True
1761
  yield make_text_delta(current_block_index, f"\n\n[Error: {str(e)}]")
1762
 
 
1763
  if text_block_open:
1764
  yield make_block_stop(current_block_index)
1765
  elif current_block_index == 0 and not has_tool_calls:
 
1931
  async def root():
1932
  return {
1933
  "message": "G4F Smart Router - Protocol Translator (Anthropic Compatible)",
1934
+ "version": "3.0.0",
1935
  "fixes_applied": [
1936
+ "[Fix #1] Persistent Goal Injection: RE-ITERATION added to every user message",
1937
+ "[Fix #2] Virtual State Flattening: tool_results converted to simple [SYSTEM] text",
1938
+ "[Fix #3] Forced Tool Mode: STRICT MODE header injected when tools are present",
1939
+ "[Fix #4] Stream Tag Auto-Close: flush() repairs unclosed tool_call tags"
1940
  ],
1941
  "providers": list(REAL_PROVIDERS.keys()),
1942
  "endpoints": {
 
1950
  "POST /chat/stream": "Simple stream (AUTH)",
1951
  "GET /providers": "Providers list (AUTH)",
1952
  "GET /debug/test-tool-parse": "Test tool parsing (AUTH)",
1953
+ "POST /debug/test-stream-buffer": "Test stream buffer (AUTH)",
1954
  },
1955
  "cookies": COOKIE_STATUS,
1956
  "status": "✅ Server is working"
 
1961
  async def health():
1962
  return {
1963
  "status": "ok",
1964
+ "version": "3.0.0",
1965
  "cookies": COOKIE_STATUS,
1966
  "providers": list(REAL_PROVIDERS.keys()),
1967
  "provider_count": len(REAL_PROVIDERS),
1968
  "fixes": {
1969
+ "persistent_goal_injection": True,
1970
+ "virtual_state_flattening": True,
1971
+ "forced_tool_mode": True,
1972
+ "stream_tag_auto_close": True
1973
  },
1974
  "timestamp": int(time.time())
1975
  }
 
2051
  '</TOOL_CALL>'
2052
  ),
2053
  },
2054
+ {
2055
+ "name": "Unclosed tag (Fix#4 test)",
2056
+ "input": (
2057
+ 'Writing file now:\n'
2058
+ '<tool_call>{"name": "write_file", "arguments": {"path": "test.txt", "content": "hello"}'
2059
+ # لا يوجد </tool_call> - هذا يختبر إصلاح #4
2060
+ ),
2061
+ },
2062
  {
2063
  "name": "No tool calls (plain text)",
2064
  "input": "This is just a regular response with no tool calls.",
 
2068
  results = []
2069
  for tc in test_cases:
2070
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(tc["input"])
2071
+
2072
+ # اختبار إصلاح #4: محاكاة flush مع وسم غير مكتمل
2073
+ if "Unclosed" in tc["name"]:
2074
+ buf = StreamToolBuffer()
2075
+ buf.feed(tc["input"])
2076
+ flush_events = buf.flush()
2077
+ flush_tool_calls = [e for e in flush_events if e.get("type") != "text"]
2078
+ flush_texts = [e for e in flush_events if e.get("type") == "text"]
2079
+ else:
2080
+ flush_tool_calls = []
2081
+ flush_texts = []
2082
+
2083
  results.append({
2084
  "test_name": tc["name"],
2085
  "input_preview": (
 
2090
  "clean_text": clean_text,
2091
  "tool_calls_found": len(tool_calls),
2092
  "tool_calls": tool_calls,
2093
+ "has_tool_call_marker": ToolCallParser.might_contain_tool_call(tc["input"]),
2094
+ "fix4_flush_recovered": len(flush_tool_calls),
2095
+ "fix4_flush_tool_calls": flush_tool_calls,
2096
  })
2097
 
2098
  return {"test_results": results}
 
2132
  }
2133
 
2134
 
2135
+ # =====================================================
2136
+ # نقطة تشخيص: اختبار حقن الهدف المستمر
2137
+ # =====================================================
2138
+ @app.post("/debug/test-goal-injection")
2139
+ async def debug_test_goal_injection(request: Request):
2140
+ verify_api_key(request)
2141
+ body = await request.json()
2142
+
2143
+ message = body.get("message", "Create a file with the search results")
2144
+ has_tools = body.get("has_tools", True)
2145
+ has_tool_results = body.get("has_tool_results", True)
2146
+
2147
+ tools_list = [{"name": "write_file"}] if has_tools else []
2148
+ history = []
2149
+
2150
+ if has_tool_results:
2151
+ history = [
2152
+ {
2153
+ "role": "user",
2154
+ "content": "Search for Python tutorials"
2155
+ },
2156
+ {
2157
+ "role": "assistant",
2158
+ "content": (
2159
+ "[SYSTEM]: Research results obtained for tool_use_id=test123.\n"
2160
+ "Data:\nPython tutorial results here...\n"
2161
+ "[SYSTEM]: Now use 'write_file' to save them."
2162
+ )
2163
+ }
2164
+ ]
2165
+
2166
+ # محاكاة ما سيحدث في ask()
2167
+ has_tool_result_in_history = any(
2168
+ "[SYSTEM]: Research results obtained" in str(item.get("content", ""))
2169
+ for item in history
2170
+ if isinstance(item, dict)
2171
+ )
2172
+
2173
+ if has_tools or has_tool_result_in_history:
2174
+ if has_tool_result_in_history or "tool result" in message.lower():
2175
+ goal_injection = (
2176
+ "\n\n[RE-ITERATION]: If you have gathered data from a previous tool, "
2177
+ "DO NOT present it as text. "
2178
+ "Use the 'write_file' tool IMMEDIATELY. "
2179
+ "Your primary mission is EXECUTION, not conversation. "
2180
+ "Output ONLY a <tool_call> block."
2181
+ )
2182
+ else:
2183
+ goal_injection = (
2184
+ "\n\n[RE-ITERATION]: Remember your primary mission is EXECUTION. "
2185
+ "If action is needed, use the appropriate tool via <tool_call> block. "
2186
+ "Do not explain what you will do - just DO IT."
2187
+ )
2188
+ final_message = message + goal_injection
2189
+ else:
2190
+ final_message = message
2191
+ goal_injection = "(none - no tools or tool results detected)"
2192
+
2193
+ return {
2194
+ "original_message": message,
2195
+ "final_message_with_injection": final_message,
2196
+ "injection_added": goal_injection,
2197
+ "has_tools": has_tools,
2198
+ "has_tool_results_in_history": has_tool_result_in_history,
2199
+ "fix1_active": has_tools or has_tool_result_in_history
2200
+ }
2201
+
2202
+
2203
  @app.post("/chat")
2204
  async def chat(request: Request, chat_req: ChatRequest):
2205
  verify_api_key(request)
 
2318
  if __name__ == "__main__":
2319
  import uvicorn
2320
  port = int(os.getenv("PORT", 7860))
2321
+ logger.info(f"Starting G4F Smart Router v3.0 on port {port}")
2322
  logger.info(f"Cookies: {COOKIE_STATUS}")
2323
  logger.info(f"Providers: {list(REAL_PROVIDERS.keys())}")
2324
+ logger.info("Fixes Applied:")
2325
+ logger.info(" [Fix#1] Persistent Goal Injection")
2326
+ logger.info(" [Fix#2] Virtual State Flattening")
2327
+ logger.info(" [Fix#3] Forced Tool Mode (STRICT MODE)")
2328
+ logger.info(" [Fix#4] Stream Tag Auto-Close in flush()")
2329
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)