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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +651 -396
app.py CHANGED
@@ -62,6 +62,140 @@ def load_cookies() -> str:
62
 
63
  COOKIE_STATUS = load_cookies()
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # =====================================================
66
  # CACHE محسن
67
  # =====================================================
@@ -208,10 +342,6 @@ def extract_text_from_content(content) -> str:
208
  elif item.get("type") == "tool_result":
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:
@@ -224,22 +354,17 @@ def extract_text_from_content(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}) "
242
- f"with input: {json.dumps(tool_input, ensure_ascii=False)}]"
243
  )
244
  elif "text" in item:
245
  parts.append(item["text"])
@@ -259,29 +384,6 @@ class ToolCallParser:
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',
@@ -589,12 +691,11 @@ class ToolCallParser:
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,9 +705,6 @@ class StreamToolBuffer:
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()
@@ -659,9 +757,6 @@ class StreamToolBuffer:
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
 
@@ -751,96 +846,54 @@ class StreamToolBuffer:
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(
@@ -848,36 +901,16 @@ class StreamToolBuffer:
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
 
@@ -885,7 +918,6 @@ class StreamToolBuffer:
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 = ""
@@ -895,38 +927,32 @@ class StreamToolBuffer:
895
 
896
 
897
  # =====================================================
898
- # TOOLS FORMATTER
899
  # =====================================================
900
  class ToolsFormatter:
901
- """تحويل أدوات Anthropic إلى نص للنموذج"""
902
 
903
  @staticmethod
904
  def format_tools_for_prompt(tools: List[Dict], tool_choice: Any = None) -> str:
 
 
 
 
905
  if not tools:
906
  return ""
907
 
908
  lines = []
909
  lines.append("# Available Tools")
910
  lines.append("")
911
- lines.append(
912
- "You have access to the following tools. "
913
- "To use a tool, respond with a tool_call block."
914
- )
915
- lines.append(
916
- "IMPORTANT: When you need to use a tool, you MUST format "
917
- "your response EXACTLY like this:"
918
- )
919
  lines.append("")
920
  lines.append("<tool_call>")
921
  lines.append('{"name": "tool_name", "arguments": {"param1": "value1"}}')
922
  lines.append("</tool_call>")
923
  lines.append("")
924
- lines.append(
925
- "You can include text explanation before and/or after the tool_call block."
926
- )
927
- lines.append(
928
- "You can make multiple tool calls by using multiple <tool_call> blocks."
929
- )
930
  lines.append("")
931
  lines.append("## Tool Definitions:")
932
  lines.append("")
@@ -962,36 +988,29 @@ class ToolsFormatter:
962
 
963
  lines.append("")
964
 
 
965
  if tool_choice:
966
  if isinstance(tool_choice, dict):
967
  if tool_choice.get("type") == "tool":
968
  forced_tool = tool_choice.get("name", "")
969
  if forced_tool:
970
  lines.append(
971
- f"**IMPORTANT:** You MUST use the `{forced_tool}` tool."
972
  )
973
  elif tool_choice.get("type") == "any":
974
  lines.append(
975
- "**IMPORTANT:** You MUST use at least one tool."
976
  )
977
- elif tool_choice == "auto":
978
- lines.append("Use tools when appropriate.")
979
  elif tool_choice == "any":
980
- lines.append("**IMPORTANT:** You MUST use at least one tool.")
981
 
982
  lines.append("")
983
- lines.append("Remember: Always use <tool_call>JSON</tool_call> format.")
984
- lines.append("The JSON inside must have 'name' and 'arguments' keys.")
985
- lines.append("")
986
 
987
  return "\n".join(lines)
988
 
989
  @staticmethod
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):
997
  result_text = content
@@ -1013,37 +1032,39 @@ class ToolsFormatter:
1013
  else:
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"""
 
 
 
 
 
1032
 
1033
  @staticmethod
1034
  def convert_messages(
1035
  messages: List[Dict],
1036
  system_prompt: str = "",
1037
  tools: Optional[List[Dict]] = None,
1038
- tool_choice: Any = None
 
1039
  ) -> Tuple[str, List[Dict]]:
1040
  """تحويل رسائل Anthropic إلى (full_message, history) لـ g4f"""
1041
  history = []
1042
 
1043
- full_system = ""
1044
- if system_prompt:
1045
- full_system = system_prompt
1046
 
 
1047
  if tools:
1048
  tools_text = ToolsFormatter.format_tools_for_prompt(tools, tool_choice)
1049
  if full_system:
@@ -1051,6 +1072,19 @@ class MessageConverter:
1051
  else:
1052
  full_system = tools_text
1053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  for msg in messages:
1055
  role = msg.get("role", "user")
1056
  content = msg.get("content", "")
@@ -1069,48 +1103,14 @@ class MessageConverter:
1069
  g4f_role = "user" if role == "user" else "assistant"
1070
  history.append({"role": g4f_role, "content": converted_text})
1071
 
 
1072
  if history:
1073
  last_msg = history.pop()
1074
  user_message = last_msg["content"]
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"
@@ -1123,7 +1123,7 @@ class MessageConverter:
1123
 
1124
  @staticmethod
1125
  def _convert_content(content: Any, role: str) -> str:
1126
- """تحويل محتوى رسالة واحدة"""
1127
  if isinstance(content, str):
1128
  return content
1129
 
@@ -1151,9 +1151,6 @@ class MessageConverter:
1151
  )
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", "")
1159
  is_error = block.get("is_error", False)
@@ -1182,52 +1179,160 @@ class MessageConverter:
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 ""
1202
  return
1203
 
1204
- key = f"{provider_name}|{model_name}|{message[:200]}"
1205
- cached = CACHE.get(key)
1206
- if cached:
1207
- yield cached
1208
- return
 
 
 
 
1209
 
 
1210
  msgs = []
1211
  try:
1212
  if history:
1213
  if isinstance(history, list) and len(history) > 0:
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 "
1228
- f"(under threshold)"
1229
- )
1230
-
1231
  for item in smart_history:
1232
  role = item.get("role")
1233
  content = item.get("content")
@@ -1240,6 +1345,7 @@ def ask(
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:
@@ -1252,59 +1358,34 @@ def ask(
1252
  if item[1]:
1253
  msgs.append({"role": "assistant", "content": str(item[1])})
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 = [
1299
- provider_name,
1300
- "Blackbox",
1301
- "DeepSeek",
1302
- "Perplexity",
1303
- "Copilot",
1304
- "You",
1305
- "Bing",
1306
- "Qwen"
1307
- ]
1308
  used = []
1309
 
1310
  for pname in fallback_providers:
@@ -1321,34 +1402,64 @@ def ask(
1321
  logger.warning(f"[Fallback] No models for provider {pname}")
1322
  continue
1323
 
 
1324
  if model_name in models_list:
1325
  model_candidates = [model_name] + [m for m in models_list if m != model_name]
1326
  else:
1327
  model_candidates = models_list
1328
 
1329
- for m in model_candidates[:10]:
1330
  try:
1331
  logger.info(f"[Fallback] Trying provider {pname} with model {m}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1332
  stream = g4f.ChatCompletion.create(
1333
  model=m,
1334
  provider=pobj,
1335
  messages=msgs,
1336
  stream=True,
1337
- timeout=45
1338
  )
1339
  buffer = []
 
1340
  for chunk in stream:
1341
  if stop_flag and stop_flag.is_set():
1342
  return
1343
  c = clean_stream(chunk)
1344
  if not c:
1345
  continue
 
1346
  buffer.append(c)
1347
  yield c
 
1348
  full = "".join(buffer)
1349
- if full.strip():
1350
- CACHE.set(key, full)
 
 
 
 
 
 
 
 
1351
  return
 
1352
  except Exception as e:
1353
  logger.warning(
1354
  f"[Fallback] Provider {pname} model {m} failed: {str(e)[:200]}"
@@ -1358,12 +1469,60 @@ def ask(
1358
  yield "❌ فشلت جميع المزودات. تأكد من اتصال الإنترنت أو حاول لاحقاً."
1359
 
1360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1361
  # =====================================================
1362
  # FASTAPI
1363
  # =====================================================
1364
  app = FastAPI(
1365
  title="G4F Smart Router",
1366
- description="AI Gateway - Protocol Translator for Anthropic"
1367
  )
1368
 
1369
  app.add_middleware(
@@ -1382,6 +1541,7 @@ class ChatRequest(BaseModel):
1382
  provider: str = "Blackbox"
1383
  model: str = "gpt-4o"
1384
  history: List[Any] = []
 
1385
 
1386
 
1387
  # =====================================================
@@ -1471,10 +1631,14 @@ async def v1_messages(request: Request):
1471
  verify_api_key(request)
1472
  body = await request.json()
1473
 
 
 
 
1474
  logger.info(
1475
  f"[API] /v1/messages - model={body.get('model')}, "
1476
  f"stream={body.get('stream')}, "
1477
- f"tools_count={len(body.get('tools', []))}"
 
1478
  )
1479
 
1480
  messages = body.get("messages", [])
@@ -1499,8 +1663,23 @@ async def v1_messages(request: Request):
1499
  tool_choice = body.get("tool_choice", "auto")
1500
  metadata = body.get("metadata", {})
1501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1502
  full_message, history = MessageConverter.convert_messages(
1503
- messages, system_prompt, tools, tool_choice
1504
  )
1505
 
1506
  logger.info(
@@ -1511,12 +1690,16 @@ async def v1_messages(request: Request):
1511
 
1512
  if is_stream:
1513
  return await _handle_anthropic_stream(
1514
- full_message, history, model, max_tokens, tools, tool_choice, metadata
 
1515
  )
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)
@@ -1544,6 +1727,12 @@ async def v1_messages(request: Request):
1544
  logger.info(
1545
  f"[API] Tool call in response: {tc['name']} (id: {tool_id})"
1546
  )
 
 
 
 
 
 
1547
 
1548
  if not content_blocks:
1549
  content_blocks.append({
@@ -1578,7 +1767,8 @@ async def _handle_anthropic_stream(
1578
  max_tokens: int,
1579
  tools: List[Dict] = None,
1580
  tool_choice: Any = None,
1581
- metadata: Dict = None
 
1582
  ):
1583
  async def generate_stream():
1584
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
@@ -1611,6 +1801,7 @@ async def _handle_anthropic_stream(
1611
  text_block_open = False
1612
  output_tokens = 0
1613
  has_tool_calls = False
 
1614
 
1615
  def make_text_block_start(index: int) -> str:
1616
  block_start = {
@@ -1692,19 +1883,24 @@ async def _handle_anthropic_stream(
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:
@@ -1723,11 +1919,12 @@ async def _handle_anthropic_stream(
1723
  text_block_open = False
1724
 
1725
  has_tool_calls = True
 
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:
@@ -1746,8 +1943,9 @@ async def _handle_anthropic_stream(
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)
@@ -1760,6 +1958,7 @@ async def _handle_anthropic_stream(
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:
@@ -1767,6 +1966,15 @@ async def _handle_anthropic_stream(
1767
  yield make_text_delta(0, "")
1768
  yield make_block_stop(0)
1769
 
 
 
 
 
 
 
 
 
 
1770
  stop_reason = "tool_use" if has_tool_calls else "end_turn"
1771
 
1772
  msg_delta = {
@@ -1803,6 +2011,8 @@ async def v1_messages_stream(request: Request):
1803
  verify_api_key(request)
1804
  body = await request.json()
1805
 
 
 
1806
  messages = body.get("messages", [])
1807
  if not messages:
1808
  raise HTTPException(status_code=400, detail="No messages provided")
@@ -1823,11 +2033,12 @@ async def v1_messages_stream(request: Request):
1823
  tool_choice = body.get("tool_choice", "auto")
1824
 
1825
  full_message, history = MessageConverter.convert_messages(
1826
- messages, system_prompt, tools, tool_choice
1827
  )
1828
 
1829
  return await _handle_anthropic_stream(
1830
- full_message, history, model, max_tokens, tools, tool_choice
 
1831
  )
1832
 
1833
 
@@ -1839,6 +2050,8 @@ async def v1_chat_completions(request: Request):
1839
  verify_api_key(request)
1840
  body = await request.json()
1841
 
 
 
1842
  messages = body.get("messages", [])
1843
  if not messages:
1844
  raise HTTPException(status_code=400, detail="No messages provided")
@@ -1856,11 +2069,16 @@ async def v1_chat_completions(request: Request):
1856
  if role and content:
1857
  history.append({"role": role, "content": content})
1858
 
 
 
 
1859
  completion_id = f"chatcmpl-{int(time.time())}_{os.urandom(4).hex()}"
1860
 
1861
  if is_stream:
1862
  async def openai_stream():
1863
- for chunk in ask(user_message, history, "Blackbox", model):
 
 
1864
  if chunk:
1865
  data = {
1866
  "id": completion_id,
@@ -1900,7 +2118,9 @@ async def v1_chat_completions(request: Request):
1900
  )
1901
 
1902
  full_response = ""
1903
- for chunk in ask(user_message, history, "Blackbox", model):
 
 
1904
  full_response += chunk
1905
 
1906
  return {
@@ -1930,13 +2150,17 @@ async def v1_chat_completions(request: Request):
1930
  @app.get("/")
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": {
@@ -1949,8 +2173,8 @@ async def root():
1949
  "POST /chat": "Simple chat (AUTH)",
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,15 +2185,20 @@ async def root():
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
  }
@@ -1988,6 +2217,52 @@ async def get_providers(request: Request):
1988
  return {"providers": result}
1989
 
1990
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1991
  # =====================================================
1992
  # نقطة تشخيص: اختبار تحليل tool calls
1993
  # =====================================================
@@ -2043,22 +2318,6 @@ async def debug_test_tool_parse(request: Request):
2043
  '</tool_call>'
2044
  ),
2045
  },
2046
- {
2047
- "name": "Case insensitive detection",
2048
- "input": (
2049
- 'Using uppercase:\n'
2050
- '<TOOL_CALL>{"name": "list_files", "arguments": {"path": "."}}'
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,18 +2327,6 @@ async def debug_test_tool_parse(request: Request):
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": (
@@ -2091,8 +2338,6 @@ async def debug_test_tool_parse(request: Request):
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}
@@ -2133,91 +2378,79 @@ async def debug_test_stream_buffer(request: Request):
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)
 
2206
  result = ""
2207
  for chunk in ask(
2208
- chat_req.message, chat_req.history, chat_req.provider, chat_req.model
 
 
 
 
2209
  ):
2210
  result += chunk
2211
- return JSONResponse({"response": result})
2212
 
2213
 
2214
  @app.post("/chat/stream")
2215
  async def chat_stream(request: Request, chat_req: ChatRequest):
2216
  verify_api_key(request)
 
2217
 
2218
  async def generate():
2219
  for chunk in ask(
2220
- chat_req.message, chat_req.history, chat_req.provider, chat_req.model
 
 
 
 
2221
  ):
2222
  yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
2223
  yield "data: [DONE]\n\n"
@@ -2312,18 +2545,40 @@ async def rate_limit_handler(request: Request, exc: HTTPException):
2312
  )
2313
 
2314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2315
  # =====================================================
2316
  # التشغيل
2317
  # =====================================================
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)
 
62
 
63
  COOKIE_STATUS = load_cookies()
64
 
65
+ # =====================================================
66
+ # PERSISTENT CONTEXT STORE
67
+ # يحفظ سياق المهام بين النماذج المختلفة ويمنع النسيان
68
+ # =====================================================
69
+ class PersistentContextStore:
70
+ """
71
+ مخزن السياق الدائم - يحفظ:
72
+ 1. المهمة الأصلية (Original Task)
73
+ 2. الخطوات المكتملة (Completed Steps)
74
+ 3. نتائج الأدوات (Tool Results)
75
+ 4. الحالة الحالية (Current State)
76
+ """
77
+ def __init__(self):
78
+ self._store: Dict[str, Dict] = {}
79
+ self._lock = threading.Lock()
80
+
81
+ def get_or_create_session(self, session_id: str) -> Dict:
82
+ with self._lock:
83
+ if session_id not in self._store:
84
+ self._store[session_id] = {
85
+ "original_task": "",
86
+ "completed_steps": [],
87
+ "tool_results": [],
88
+ "pending_tool_calls": [],
89
+ "current_state": "idle",
90
+ "created_at": time.time(),
91
+ "last_updated": time.time(),
92
+ "message_count": 0,
93
+ "provider_history": [],
94
+ }
95
+ return self._store[session_id]
96
+
97
+ def update_session(self, session_id: str, **kwargs):
98
+ with self._lock:
99
+ if session_id not in self._store:
100
+ self.get_or_create_session(session_id)
101
+ self._store[session_id].update(kwargs)
102
+ self._store[session_id]["last_updated"] = time.time()
103
+
104
+ def add_tool_result(self, session_id: str, tool_name: str, tool_id: str, result: str):
105
+ with self._lock:
106
+ if session_id not in self._store:
107
+ self.get_or_create_session(session_id)
108
+ self._store[session_id]["tool_results"].append({
109
+ "tool_name": tool_name,
110
+ "tool_id": tool_id,
111
+ "result": result,
112
+ "timestamp": time.time()
113
+ })
114
+ self._store[session_id]["last_updated"] = time.time()
115
+
116
+ def add_completed_step(self, session_id: str, step: str):
117
+ with self._lock:
118
+ if session_id not in self._store:
119
+ self.get_or_create_session(session_id)
120
+ self._store[session_id]["completed_steps"].append({
121
+ "step": step,
122
+ "timestamp": time.time()
123
+ })
124
+ self._store[session_id]["last_updated"] = time.time()
125
+
126
+ def get_context_summary(self, session_id: str) -> str:
127
+ """
128
+ يولّد ملخصاً للسياق يُحقن في كل رسالة
129
+ هذا هو سر منع النسيان - السياق يُعاد حقنه في كل مرة
130
+ """
131
+ with self._lock:
132
+ if session_id not in self._store:
133
+ return ""
134
+ ctx = self._store[session_id]
135
+
136
+ parts = []
137
+
138
+ if ctx.get("original_task"):
139
+ parts.append(f"## Original Task\n{ctx['original_task']}")
140
+
141
+ if ctx.get("completed_steps"):
142
+ steps_text = "\n".join(
143
+ f" - {s['step']}" for s in ctx["completed_steps"][-10:]
144
+ )
145
+ parts.append(f"## Completed Steps\n{steps_text}")
146
+
147
+ if ctx.get("tool_results"):
148
+ # آخر 5 نتائج فقط لتجنب الحجم الكبير
149
+ recent_results = ctx["tool_results"][-5:]
150
+ results_text = "\n".join(
151
+ f" - [{r['tool_name']}]: {r['result'][:300]}..."
152
+ if len(r['result']) > 300
153
+ else f" - [{r['tool_name']}]: {r['result']}"
154
+ for r in recent_results
155
+ )
156
+ parts.append(f"## Tool Results So Far\n{results_text}")
157
+
158
+ if ctx.get("current_state") and ctx["current_state"] != "idle":
159
+ parts.append(f"## Current State\n{ctx['current_state']}")
160
+
161
+ if not parts:
162
+ return ""
163
+
164
+ return "# Task Context (DO NOT FORGET)\n" + "\n\n".join(parts)
165
+
166
+ def cleanup_old_sessions(self, max_age_hours: int = 24):
167
+ """تنظيف الجلسات القديمة"""
168
+ with self._lock:
169
+ now = time.time()
170
+ old_sessions = [
171
+ sid for sid, data in self._store.items()
172
+ if now - data.get("last_updated", 0) > max_age_hours * 3600
173
+ ]
174
+ for sid in old_sessions:
175
+ del self._store[sid]
176
+ if old_sessions:
177
+ logger.info(f"[Context] Cleaned {len(old_sessions)} old sessions")
178
+
179
+ def extract_task_from_messages(self, messages: List[Dict]) -> str:
180
+ """استخراج المهمة الأصلية من أول رسالة مستخدم"""
181
+ for msg in messages:
182
+ if msg.get("role") == "user":
183
+ content = msg.get("content", "")
184
+ if isinstance(content, str) and len(content) > 10:
185
+ # أ��ل 500 حرف من أول رسالة مستخدم = المهمة الأصلية
186
+ return content[:500]
187
+ elif isinstance(content, list):
188
+ for item in content:
189
+ if isinstance(item, dict) and item.get("type") == "text":
190
+ text = item.get("text", "")
191
+ if len(text) > 10:
192
+ return text[:500]
193
+ return ""
194
+
195
+
196
+ # المخزن العالمي للسياق
197
+ CONTEXT_STORE = PersistentContextStore()
198
+
199
  # =====================================================
200
  # CACHE محسن
201
  # =====================================================
 
342
  elif item.get("type") == "tool_result":
343
  tool_content = item.get("content", "")
344
  tool_use_id = item.get("tool_use_id", "")
 
 
 
 
345
  if isinstance(tool_content, list):
346
  result_texts = []
347
  for tc in tool_content:
 
354
  result_str = tool_content
355
  else:
356
  result_str = str(tool_content)
357
+ # صيغة واضحة ومنظمة لنتيجة الأداة
 
 
358
  parts.append(
359
+ f"[Tool Result for {tool_use_id}]:\n{result_str}"
 
 
 
360
  )
361
  elif item.get("type") == "tool_use":
362
  tool_name = item.get("name", "unknown")
363
  tool_input = item.get("input", {})
364
  tool_id = item.get("id", "")
365
  parts.append(
366
+ f"[Tool Called: {tool_name} (id:{tool_id}) "
367
+ f"with: {json.dumps(tool_input, ensure_ascii=False)}]"
368
  )
369
  elif "text" in item:
370
  parts.append(item["text"])
 
384
  يدعم أنماطاً متعددة مع regex محسّن يقبل المسافات والأسطر الجديدة.
385
  """
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  START_MARKERS = [
388
  '<tool_call>',
389
  '```tool_call',
 
691
 
692
 
693
  # =====================================================
694
+ # STREAM TOOL BUFFER
695
  # =====================================================
696
  class StreamToolBuffer:
697
  """
698
  Buffer ذكي يجمع chunks الـ stream ويكتشف tool calls.
 
699
  """
700
 
701
  def __init__(self, available_tools: Optional[List[Dict]] = None):
 
705
  self.available_tools = available_tools or []
706
  self.pending_text = ""
707
  self.tool_call_depth = 0
 
 
 
708
  self._active_start_marker = ""
709
  self._active_end_marker = ""
710
  self._stream_started_at = time.time()
 
757
  self.buffer = self.buffer[earliest_pos:]
758
  self.in_tool_call = True
759
  self.tool_call_buffer = ""
 
 
 
760
  self._active_start_marker = earliest_marker
761
  self._set_active_end_marker(earliest_marker)
762
 
 
846
  return events
847
 
848
  def flush(self) -> List[Dict]:
849
+ """تفريغ أي محتوى متبقي في الـ buffer مع إصلاح الوسوم غير المكتملة."""
 
 
 
 
 
850
  events = []
851
 
852
  if self.in_tool_call and self.buffer:
 
 
 
 
853
  logger.warning(
854
+ f"[Buffer] Stream ended with unclosed tool_call tag. "
855
  f"Active marker: '{self._active_start_marker}'. "
856
  f"Attempting manual closure..."
857
  )
858
 
 
859
  end_marker = self._active_end_marker or "</tool_call>"
 
 
860
  buffer_with_close = self.buffer + end_marker
 
 
 
861
 
862
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(
863
  buffer_with_close, self.available_tools
864
  )
865
 
866
  if tool_calls:
 
867
  logger.info(
868
+ f"[Buffer] Manual closure SUCCESS: recovered {len(tool_calls)} tool call(s)"
 
869
  )
870
  for tc in tool_calls:
871
  if not tc.get("id"):
872
  tc["id"] = ToolCallParser.generate_tool_id()
873
  events.append(tc)
 
 
 
 
874
  if clean_text.strip():
875
  events.append({"type": "text", "text": clean_text})
876
  else:
877
+ # محاولة إصلاح JSON الناقص
 
 
 
 
 
 
 
 
 
878
  raw_buffer = self.buffer
 
879
  if self._active_start_marker:
880
  start_lower = self._active_start_marker.lower()
881
  raw_lower = raw_buffer.lower()
882
  if raw_lower.startswith(start_lower):
883
  raw_buffer = raw_buffer[len(self._active_start_marker):]
884
 
 
885
  json_start = raw_buffer.find('{')
886
  if json_start != -1:
887
  json_fragment = raw_buffer[json_start:].strip()
 
 
888
  open_braces = json_fragment.count('{') - json_fragment.count('}')
889
  if open_braces > 0:
890
  json_fragment += '}' * open_braces
 
 
 
 
 
891
  open_brackets = json_fragment.count('[') - json_fragment.count(']')
892
  if open_brackets > 0:
893
  json_fragment += ']' * open_brackets
894
 
 
895
  repaired_text = (
896
+ self._active_start_marker + json_fragment + end_marker
 
 
897
  )
898
 
899
  clean_text2, tool_calls2 = ToolCallParser.parse_tool_calls(
 
901
  )
902
 
903
  if tool_calls2:
 
 
 
 
904
  for tc in tool_calls2:
905
  if not tc.get("id"):
906
  tc["id"] = ToolCallParser.generate_tool_id()
907
  events.append(tc)
 
 
 
 
908
  if clean_text2.strip():
909
  events.append({"type": "text", "text": clean_text2})
910
  else:
 
 
 
 
 
911
  if self.buffer.strip():
912
+ events.append({"type": "text", "text": self.buffer})
 
 
 
 
 
 
913
  else:
 
914
  if self.buffer.strip():
915
  events.append({"type": "text", "text": self.buffer})
916
 
 
918
  if self.buffer.strip():
919
  events.append({"type": "text", "text": self.buffer})
920
 
 
921
  self.buffer = ""
922
  self.in_tool_call = False
923
  self._active_start_marker = ""
 
927
 
928
 
929
  # =====================================================
930
+ # TOOLS FORMATTER - نسخة متوازنة (لا تشدد مفرط)
931
  # =====================================================
932
  class ToolsFormatter:
933
+ """تحويل أدوات Anthropic إلى نص للنموذج - صيغة متوازنة"""
934
 
935
  @staticmethod
936
  def format_tools_for_prompt(tools: List[Dict], tool_choice: Any = None) -> str:
937
+ """
938
+ تنسيق الأدوات بصيغة واضحة دون ضغط مفرط.
939
+ الضغط المفرط كان سبب تجاهل النموذج للأوامر.
940
+ """
941
  if not tools:
942
  return ""
943
 
944
  lines = []
945
  lines.append("# Available Tools")
946
  lines.append("")
947
+ lines.append("You have access to the following tools.")
948
+ lines.append("When you need to use a tool, format your response like this:")
 
 
 
 
 
 
949
  lines.append("")
950
  lines.append("<tool_call>")
951
  lines.append('{"name": "tool_name", "arguments": {"param1": "value1"}}')
952
  lines.append("</tool_call>")
953
  lines.append("")
954
+ lines.append("You can include text before or after the tool_call block.")
955
+ lines.append("You can make multiple tool calls using multiple <tool_call> blocks.")
 
 
 
 
956
  lines.append("")
957
  lines.append("## Tool Definitions:")
958
  lines.append("")
 
988
 
989
  lines.append("")
990
 
991
+ # معالجة tool_choice بشكل معتدل
992
  if tool_choice:
993
  if isinstance(tool_choice, dict):
994
  if tool_choice.get("type") == "tool":
995
  forced_tool = tool_choice.get("name", "")
996
  if forced_tool:
997
  lines.append(
998
+ f"**Note:** Please use the `{forced_tool}` tool for this request."
999
  )
1000
  elif tool_choice.get("type") == "any":
1001
  lines.append(
1002
+ "**Note:** Please use at least one tool to complete this request."
1003
  )
 
 
1004
  elif tool_choice == "any":
1005
+ lines.append("**Note:** Please use at least one tool.")
1006
 
1007
  lines.append("")
 
 
 
1008
 
1009
  return "\n".join(lines)
1010
 
1011
  @staticmethod
1012
  def format_tool_result_for_message(tool_use_id: str, content: Any) -> str:
1013
+ """تحويل نتيجة أداة إلى نص مفهوم للنموذج."""
 
 
 
1014
  result_text = ""
1015
  if isinstance(content, str):
1016
  result_text = content
 
1032
  else:
1033
  result_text = str(content) if content else ""
1034
 
 
 
 
 
1035
  return (
1036
+ f"[Tool Result - ID: {tool_use_id}]\n"
1037
+ f"{result_text}\n"
1038
+ f"[End Tool Result]"
1039
  )
1040
 
1041
 
1042
  # =====================================================
1043
+ # MESSAGE CONVERTER - محسّن للحفاظ على السياق الكامل
1044
  # =====================================================
1045
  class MessageConverter:
1046
+ """
1047
+ تحويل رسائل Anthropic إلى صيغة g4f مع الحفاظ الكامل على السياق.
1048
+
1049
+ الإصلاح الجوهري: لا نحذف أي رسائل من التاريخ إلا عند الضرورة القصوى،
1050
+ والسياق يُحفظ في PersistentContextStore ويُحقن في كل طلب.
1051
+ """
1052
 
1053
  @staticmethod
1054
  def convert_messages(
1055
  messages: List[Dict],
1056
  system_prompt: str = "",
1057
  tools: Optional[List[Dict]] = None,
1058
+ tool_choice: Any = None,
1059
+ session_id: str = ""
1060
  ) -> Tuple[str, List[Dict]]:
1061
  """تحويل رسائل Anthropic إلى (full_message, history) لـ g4f"""
1062
  history = []
1063
 
1064
+ # بناء system prompt
1065
+ full_system = system_prompt if system_prompt else ""
 
1066
 
1067
+ # إضافة تعريف الأدوات إذا وجدت
1068
  if tools:
1069
  tools_text = ToolsFormatter.format_tools_for_prompt(tools, tool_choice)
1070
  if full_system:
 
1072
  else:
1073
  full_system = tools_text
1074
 
1075
+ # حقن سياق المهمة المحفوظ (منع النسيان عند تبديل النماذج)
1076
+ if session_id:
1077
+ context_summary = CONTEXT_STORE.get_context_summary(session_id)
1078
+ if context_summary:
1079
+ if full_system:
1080
+ full_system = f"{full_system}\n\n{context_summary}"
1081
+ else:
1082
+ full_system = context_summary
1083
+ logger.info(
1084
+ f"[Context] Injected persistent context for session {session_id[:8]}..."
1085
+ )
1086
+
1087
+ # معالجة الرسائل وبناء التاريخ
1088
  for msg in messages:
1089
  role = msg.get("role", "user")
1090
  content = msg.get("content", "")
 
1103
  g4f_role = "user" if role == "user" else "assistant"
1104
  history.append({"role": g4f_role, "content": converted_text})
1105
 
1106
+ # استخراج الرسالة الأخيرة
1107
  if history:
1108
  last_msg = history.pop()
1109
  user_message = last_msg["content"]
1110
  else:
1111
  user_message = ""
1112
 
1113
+ # بناء الرسالة الكاملة مع system prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  if full_system:
1115
  full_message = (
1116
  f"[System Instructions]\n{full_system}\n"
 
1123
 
1124
  @staticmethod
1125
  def _convert_content(content: Any, role: str) -> str:
1126
+ """تحويل محتوى رسالة واحدة مع الحفاظ على كل المعلومات"""
1127
  if isinstance(content, str):
1128
  return content
1129
 
 
1151
  )
1152
 
1153
  elif block_type == "tool_result":
 
 
 
1154
  tool_use_id = block.get("tool_use_id", "")
1155
  result_content = block.get("content", "")
1156
  is_error = block.get("is_error", False)
 
1179
 
1180
 
1181
  # =====================================================
1182
+ # SMART HISTORY MANAGER
1183
+ # يدير التاريخ بذكاء للحفاظ على السياق الكامل
1184
+ # =====================================================
1185
+ class SmartHistoryManager:
1186
+ """
1187
+ مدير التاريخ الذكي - يحل مشكلة النسيان بشكل نهائي.
1188
+
1189
+ الاستراتيجية:
1190
+ 1. يحتفظ دائماً بأول N رسالة (تحتوي المهمة الأصلية)
1191
+ 2. يحتفظ دائماً بآخر M رسالة (السياق الحالي)
1192
+ 3. يضغط الرسائل الوسطى بدلاً من حذفها
1193
+ 4. يستخرج ويحفظ نتائج الأدوات في PersistentContextStore
1194
+ """
1195
+
1196
+ FIRST_MESSAGES_TO_KEEP = 5 # أول 5 رسائل (المهمة الأصلية)
1197
+ LAST_MESSAGES_TO_KEEP = 20 # آخر 20 رسالة (السياق الحالي)
1198
+ MAX_TOTAL_MESSAGES = 30 # الحد الأقصى الإجمالي
1199
+
1200
+ @classmethod
1201
+ def process_history(
1202
+ cls,
1203
+ history: List[Dict],
1204
+ session_id: str = ""
1205
+ ) -> List[Dict]:
1206
+ """
1207
+ معالجة التاريخ بذكاء مع استخراج وحفظ المعلومات المهمة.
1208
+ """
1209
+ if not history:
1210
+ return []
1211
+
1212
+ # استخراج نتائج الأدوات وحفظها في المخزن الدائم
1213
+ if session_id:
1214
+ cls._extract_and_store_tool_results(history, session_id)
1215
+
1216
+ total = len(history)
1217
+
1218
+ # إذا كان التاريخ صغيراً، نحتفظ بكله
1219
+ if total <= cls.MAX_TOTAL_MESSAGES:
1220
+ logger.info(f"[History] Full history: {total} messages (under limit)")
1221
+ return history
1222
+
1223
+ # التاريخ كبير: نطبق الاستراتيجية الذكية
1224
+ first_part = history[:cls.FIRST_MESSAGES_TO_KEEP]
1225
+ last_part = history[-cls.LAST_MESSAGES_TO_KEEP:]
1226
+
1227
+ # إنشاء ملخص للجزء الوسط
1228
+ middle_part = history[cls.FIRST_MESSAGES_TO_KEEP:-cls.LAST_MESSAGES_TO_KEEP]
1229
+ if middle_part:
1230
+ summary = cls._summarize_middle(middle_part)
1231
+ if summary:
1232
+ summary_msg = {
1233
+ "role": "assistant",
1234
+ "content": f"[Context Summary - {len(middle_part)} messages compressed]\n{summary}"
1235
+ }
1236
+ smart_history = first_part + [summary_msg] + last_part
1237
+ else:
1238
+ smart_history = first_part + last_part
1239
+ else:
1240
+ smart_history = first_part + last_part
1241
+
1242
+ logger.info(
1243
+ f"[History] Smart compression: {total} → {len(smart_history)} messages "
1244
+ f"(kept first {cls.FIRST_MESSAGES_TO_KEEP} + last {cls.LAST_MESSAGES_TO_KEEP})"
1245
+ )
1246
+
1247
+ return smart_history
1248
+
1249
+ @classmethod
1250
+ def _extract_and_store_tool_results(
1251
+ cls,
1252
+ history: List[Dict],
1253
+ session_id: str
1254
+ ):
1255
+ """استخراج نتائج الأدوات من التاريخ وحفظها في المخزن الدائم"""
1256
+ for msg in history:
1257
+ content = msg.get("content", "")
1258
+ if isinstance(content, str):
1259
+ # البحث عن نتائج الأدوات في النص
1260
+ if "[Tool Result" in content or "[SYSTEM]: Research results" in content:
1261
+ # استخراج اسم الأداة والنتيجة
1262
+ tool_match = re.search(
1263
+ r'\[Tool Result - ID: ([^\]]+)\]\n(.*?)\n\[End Tool Result\]',
1264
+ content,
1265
+ re.DOTALL
1266
+ )
1267
+ if tool_match:
1268
+ tool_id = tool_match.group(1)
1269
+ result = tool_match.group(2)[:500] # أول 500 حرف
1270
+ CONTEXT_STORE.add_tool_result(
1271
+ session_id, "tool", tool_id, result
1272
+ )
1273
+
1274
+ @classmethod
1275
+ def _summarize_middle(cls, messages: List[Dict]) -> str:
1276
+ """إنشاء ملخص نصي للرسائل الوسطى"""
1277
+ if not messages:
1278
+ return ""
1279
+
1280
+ summary_parts = []
1281
+ for msg in messages:
1282
+ role = msg.get("role", "")
1283
+ content = str(msg.get("content", ""))[:200] # أول 200 حرف
1284
+ summary_parts.append(f"[{role}]: {content}...")
1285
+
1286
+ return "\n".join(summary_parts[:10]) # أول 10 رسائل من الملخص
1287
+
1288
+
1289
+ # =====================================================
1290
+ # CHAT LOGIC - الإصلاح الجوهري
1291
  # =====================================================
1292
  def ask(
1293
  message: str,
1294
+ history: List,
1295
  provider_name: str,
1296
  model_name: str,
1297
  stop_flag=None,
1298
+ tools: Optional[List[Dict]] = None,
1299
+ session_id: str = ""
1300
  ):
1301
  """
1302
+ دالة الدردشة الرئيسية - مُصلحة بشكل جوهري.
1303
+
1304
+ الإصلاحات:
1305
+ 1. لا STRICT MODE مفرط - فقط تعليمات واضحة ومعقولة
1306
+ 2. حفظ السياق في PersistentContextStore
1307
+ 3. SmartHistoryManager يدير التاريخ بذكاء
1308
+ 4. Fallback يبدأ من المزود المطلوب فعلاً
1309
+ 5. لا حقن هدف مفرط يربك النموذج
1310
  """
1311
  message = (message or "").strip()
1312
  if not message:
1313
  yield ""
1314
  return
1315
 
1316
+ # لا نستخدم الكاش عند وجود أدوات لأن الأدوات تتطلب استجابة جديدة دائماً
1317
+ if not tools:
1318
+ key = f"{provider_name}|{model_name}|{message[:200]}"
1319
+ cached = CACHE.get(key)
1320
+ if cached:
1321
+ yield cached
1322
+ return
1323
+ else:
1324
+ key = None
1325
 
1326
+ # معالجة التاريخ بذكاء
1327
  msgs = []
1328
  try:
1329
  if history:
1330
  if isinstance(history, list) and len(history) > 0:
1331
  if isinstance(history[0], dict):
1332
+ # استخدام SmartHistoryManager
1333
+ smart_history = SmartHistoryManager.process_history(
1334
+ history, session_id
1335
+ )
 
 
 
 
 
 
 
 
 
 
 
 
1336
  for item in smart_history:
1337
  role = item.get("role")
1338
  content = item.get("content")
 
1345
  if text:
1346
  msgs.append({"role": str(role), "content": text})
1347
  else:
1348
+ # تنسيق tuples القديم
1349
  if len(history) > 30:
1350
  smart_history_tuples = history[:5] + history[-25:]
1351
  else:
 
1358
  if item[1]:
1359
  msgs.append({"role": "assistant", "content": str(item[1])})
1360
  except Exception as e:
1361
+ logger.warning(f"[History] Error processing history: {e}")
1362
+
1363
+ # إضافة الرسالة الحالية
1364
+ msgs.append({"role": "user", "content": message})
1365
+
1366
+ # تحديث المخزن الدائم
1367
+ if session_id:
1368
+ ctx = CONTEXT_STORE.get_or_create_session(session_id)
1369
+ # حفظ المهمة الأصلية إذا لم تُحفظ بعد
1370
+ if not ctx.get("original_task") and len(msgs) <= 2:
1371
+ CONTEXT_STORE.update_session(
1372
+ session_id,
1373
+ original_task=message[:500],
1374
+ current_state="processing"
 
 
 
 
 
 
 
 
1375
  )
1376
+ ctx["message_count"] = ctx.get("message_count", 0) + 1
1377
+
1378
+ # قائمة المزودين للـ fallback - يبدأ من المزود المطلوب
1379
+ all_provider_names = list(REAL_PROVIDERS.keys())
1380
+
1381
+ # ترتيب المزودين: المطلوب أولاً، ثم الباقين
1382
+ if provider_name in all_provider_names:
1383
+ fallback_providers = [provider_name] + [
1384
+ p for p in all_provider_names if p != provider_name
1385
+ ]
 
 
 
 
1386
  else:
1387
+ fallback_providers = all_provider_names
1388
+
 
 
 
 
 
 
 
 
 
 
 
 
 
1389
  used = []
1390
 
1391
  for pname in fallback_providers:
 
1402
  logger.warning(f"[Fallback] No models for provider {pname}")
1403
  continue
1404
 
1405
+ # ترتيب النماذج: المطلوب أولاً
1406
  if model_name in models_list:
1407
  model_candidates = [model_name] + [m for m in models_list if m != model_name]
1408
  else:
1409
  model_candidates = models_list
1410
 
1411
+ for m in model_candidates[:5]:
1412
  try:
1413
  logger.info(f"[Fallback] Trying provider {pname} with model {m}")
1414
+
1415
+ # تسجيل المزود المستخدم في المخزن الدائم
1416
+ if session_id:
1417
+ CONTEXT_STORE.get_or_create_session(session_id)
1418
+ provider_history = CONTEXT_STORE._store[session_id].get(
1419
+ "provider_history", []
1420
+ )
1421
+ provider_history.append({
1422
+ "provider": pname,
1423
+ "model": m,
1424
+ "timestamp": time.time()
1425
+ })
1426
+ # الاحتفاظ بآخر 20 مزود فقط
1427
+ CONTEXT_STORE._store[session_id]["provider_history"] = (
1428
+ provider_history[-20:]
1429
+ )
1430
+
1431
  stream = g4f.ChatCompletion.create(
1432
  model=m,
1433
  provider=pobj,
1434
  messages=msgs,
1435
  stream=True,
1436
+ timeout=60
1437
  )
1438
  buffer = []
1439
+ got_response = False
1440
  for chunk in stream:
1441
  if stop_flag and stop_flag.is_set():
1442
  return
1443
  c = clean_stream(chunk)
1444
  if not c:
1445
  continue
1446
+ got_response = True
1447
  buffer.append(c)
1448
  yield c
1449
+
1450
  full = "".join(buffer)
1451
+ if full.strip() and got_response:
1452
+ # حفظ في الكاش فقط إذا لم تكن هناك أدوات
1453
+ if key:
1454
+ CACHE.set(key, full)
1455
+ # تحديث حالة الجلسة
1456
+ if session_id:
1457
+ CONTEXT_STORE.update_session(
1458
+ session_id,
1459
+ current_state="completed_step"
1460
+ )
1461
  return
1462
+
1463
  except Exception as e:
1464
  logger.warning(
1465
  f"[Fallback] Provider {pname} model {m} failed: {str(e)[:200]}"
 
1469
  yield "❌ فشلت جميع المزودات. تأكد من اتصال الإنترنت أو حاول لاحقاً."
1470
 
1471
 
1472
+ # =====================================================
1473
+ # SESSION ID EXTRACTOR
1474
+ # استخراج معرف الجلسة من الطلب
1475
+ # =====================================================
1476
+ def extract_session_id(request: Request, body: Dict) -> str:
1477
+ """
1478
+ استخراج أو توليد معرف جلسة فريد.
1479
+ يستخدم لربط الطلبات المتعددة بنفس المحادثة.
1480
+ """
1481
+ # محاولة استخراج من الـ headers
1482
+ session_id = (
1483
+ request.headers.get("X-Session-ID", "") or
1484
+ request.headers.get("x-session-id", "") or
1485
+ request.headers.get("X-Conversation-ID", "") or
1486
+ ""
1487
+ )
1488
+
1489
+ if session_id:
1490
+ return session_id
1491
+
1492
+ # محاولة استخراج من الـ metadata
1493
+ metadata = body.get("metadata", {})
1494
+ if isinstance(metadata, dict):
1495
+ session_id = (
1496
+ metadata.get("session_id", "") or
1497
+ metadata.get("conversation_id", "") or
1498
+ metadata.get("user_id", "") or
1499
+ ""
1500
+ )
1501
+
1502
+ if session_id:
1503
+ return session_id
1504
+
1505
+ # توليد معرف من محتوى الرسائل (ثابت لنفس المحادثة)
1506
+ messages = body.get("messages", [])
1507
+ if messages:
1508
+ # استخدام أول رسالة لتوليد معرف ثابت
1509
+ first_msg = messages[0]
1510
+ content = str(first_msg.get("content", ""))[:100]
1511
+ # معرف شبه ثابت بناءً على المحتوى
1512
+ import hashlib
1513
+ hash_val = hashlib.md5(content.encode()).hexdigest()[:16]
1514
+ return f"auto_{hash_val}"
1515
+
1516
+ # توليد معرف عشوائي كحل أخير
1517
+ return f"rand_{uuid.uuid4().hex[:16]}"
1518
+
1519
+
1520
  # =====================================================
1521
  # FASTAPI
1522
  # =====================================================
1523
  app = FastAPI(
1524
  title="G4F Smart Router",
1525
+ description="AI Gateway - Protocol Translator for Anthropic (Fixed Version)"
1526
  )
1527
 
1528
  app.add_middleware(
 
1541
  provider: str = "Blackbox"
1542
  model: str = "gpt-4o"
1543
  history: List[Any] = []
1544
+ session_id: str = ""
1545
 
1546
 
1547
  # =====================================================
 
1631
  verify_api_key(request)
1632
  body = await request.json()
1633
 
1634
+ # استخراج معرف الجلسة
1635
+ session_id = extract_session_id(request, body)
1636
+
1637
  logger.info(
1638
  f"[API] /v1/messages - model={body.get('model')}, "
1639
  f"stream={body.get('stream')}, "
1640
+ f"tools_count={len(body.get('tools', []))}, "
1641
+ f"session={session_id[:8]}..."
1642
  )
1643
 
1644
  messages = body.get("messages", [])
 
1663
  tool_choice = body.get("tool_choice", "auto")
1664
  metadata = body.get("metadata", {})
1665
 
1666
+ # استخراج المهمة الأصلية وحفظها
1667
+ ctx = CONTEXT_STORE.get_or_create_session(session_id)
1668
+ if not ctx.get("original_task"):
1669
+ original_task = CONTEXT_STORE.extract_task_from_messages(messages)
1670
+ if original_task:
1671
+ CONTEXT_STORE.update_session(
1672
+ session_id,
1673
+ original_task=original_task
1674
+ )
1675
+ logger.info(
1676
+ f"[Context] Stored original task for session {session_id[:8]}: "
1677
+ f"{original_task[:50]}..."
1678
+ )
1679
+
1680
+ # تحويل الرسائل مع حقن السياق
1681
  full_message, history = MessageConverter.convert_messages(
1682
+ messages, system_prompt, tools, tool_choice, session_id
1683
  )
1684
 
1685
  logger.info(
 
1690
 
1691
  if is_stream:
1692
  return await _handle_anthropic_stream(
1693
+ full_message, history, model, max_tokens, tools, tool_choice,
1694
+ metadata, session_id
1695
  )
1696
 
1697
  # Non-stream: تجميع الرد كاملاً
1698
  full_response = ""
1699
+ for chunk in ask(
1700
+ full_message, history, "Blackbox", model,
1701
+ tools=tools, session_id=session_id
1702
+ ):
1703
  full_response += chunk
1704
 
1705
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, tools)
 
1727
  logger.info(
1728
  f"[API] Tool call in response: {tc['name']} (id: {tool_id})"
1729
  )
1730
+ # حفظ خطوة الأداة في المخزن الدائم
1731
+ if session_id:
1732
+ CONTEXT_STORE.add_completed_step(
1733
+ session_id,
1734
+ f"Called tool: {tc['name']} with {json.dumps(tc.get('input', {}))[:100]}"
1735
+ )
1736
 
1737
  if not content_blocks:
1738
  content_blocks.append({
 
1767
  max_tokens: int,
1768
  tools: List[Dict] = None,
1769
  tool_choice: Any = None,
1770
+ metadata: Dict = None,
1771
+ session_id: str = ""
1772
  ):
1773
  async def generate_stream():
1774
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
 
1801
  text_block_open = False
1802
  output_tokens = 0
1803
  has_tool_calls = False
1804
+ detected_tool_calls = []
1805
 
1806
  def make_text_block_start(index: int) -> str:
1807
  block_start = {
 
1883
 
1884
  # ===== معالجة الـ Stream =====
1885
  try:
1886
+ for chunk in ask(
1887
+ full_message, history, "Blackbox", model,
1888
+ tools=tools_list, session_id=session_id
1889
+ ):
1890
  if not chunk:
1891
  continue
1892
 
1893
  output_tokens += max(1, len(chunk) // 4)
1894
 
1895
  if not tools_list:
1896
+ # بدون أدوات: إرسال مباشر
1897
  if not text_block_open:
1898
  yield make_text_block_start(current_block_index)
1899
  text_block_open = True
1900
  yield make_text_delta(current_block_index, chunk)
1901
  continue
1902
 
1903
+ # مع أدوات: معالجة عبر الـ buffer
1904
  events = tool_buffer.feed(chunk)
1905
 
1906
  for event in events:
 
1919
  text_block_open = False
1920
 
1921
  has_tool_calls = True
1922
+ detected_tool_calls.append(event)
1923
  yield make_tool_use_events(current_block_index, event)
1924
  current_block_index += 1
1925
 
1926
+ # ===== تفريغ الـ buffer المتبقي =====
1927
+ logger.info("[Stream] Flushing remaining buffer...")
1928
  remaining_events = tool_buffer.flush()
1929
 
1930
  for event in remaining_events:
 
1943
  text_block_open = False
1944
 
1945
  has_tool_calls = True
1946
+ detected_tool_calls.append(event)
1947
  logger.info(
1948
+ f"[Stream] Recovered tool call from flush: "
1949
  f"{event.get('name')}"
1950
  )
1951
  yield make_tool_use_events(current_block_index, event)
 
1958
  text_block_open = True
1959
  yield make_text_delta(current_block_index, f"\n\n[Error: {str(e)}]")
1960
 
1961
+ # إغلاق الـ blocks المفتوحة
1962
  if text_block_open:
1963
  yield make_block_stop(current_block_index)
1964
  elif current_block_index == 0 and not has_tool_calls:
 
1966
  yield make_text_delta(0, "")
1967
  yield make_block_stop(0)
1968
 
1969
+ # حفظ الأدوات المكتشفة في المخزن الدائم
1970
+ if session_id and detected_tool_calls:
1971
+ for tc in detected_tool_calls:
1972
+ CONTEXT_STORE.add_completed_step(
1973
+ session_id,
1974
+ f"Called tool: {tc.get('name', 'unknown')} "
1975
+ f"with {json.dumps(tc.get('input', {}))[:100]}"
1976
+ )
1977
+
1978
  stop_reason = "tool_use" if has_tool_calls else "end_turn"
1979
 
1980
  msg_delta = {
 
2011
  verify_api_key(request)
2012
  body = await request.json()
2013
 
2014
+ session_id = extract_session_id(request, body)
2015
+
2016
  messages = body.get("messages", [])
2017
  if not messages:
2018
  raise HTTPException(status_code=400, detail="No messages provided")
 
2033
  tool_choice = body.get("tool_choice", "auto")
2034
 
2035
  full_message, history = MessageConverter.convert_messages(
2036
+ messages, system_prompt, tools, tool_choice, session_id
2037
  )
2038
 
2039
  return await _handle_anthropic_stream(
2040
+ full_message, history, model, max_tokens, tools, tool_choice,
2041
+ {}, session_id
2042
  )
2043
 
2044
 
 
2050
  verify_api_key(request)
2051
  body = await request.json()
2052
 
2053
+ session_id = extract_session_id(request, body)
2054
+
2055
  messages = body.get("messages", [])
2056
  if not messages:
2057
  raise HTTPException(status_code=400, detail="No messages provided")
 
2069
  if role and content:
2070
  history.append({"role": role, "content": content})
2071
 
2072
+ # تطبيق SmartHistoryManager
2073
+ history = SmartHistoryManager.process_history(history, session_id)
2074
+
2075
  completion_id = f"chatcmpl-{int(time.time())}_{os.urandom(4).hex()}"
2076
 
2077
  if is_stream:
2078
  async def openai_stream():
2079
+ for chunk in ask(
2080
+ user_message, history, "Blackbox", model, session_id=session_id
2081
+ ):
2082
  if chunk:
2083
  data = {
2084
  "id": completion_id,
 
2118
  )
2119
 
2120
  full_response = ""
2121
+ for chunk in ask(
2122
+ user_message, history, "Blackbox", model, session_id=session_id
2123
+ ):
2124
  full_response += chunk
2125
 
2126
  return {
 
2150
  @app.get("/")
2151
  async def root():
2152
  return {
2153
+ "message": "G4F Smart Router - Fixed Version",
2154
+ "version": "4.0.0",
2155
  "fixes_applied": [
2156
+ "Removed STRICT MODE (was causing model to ignore all commands)",
2157
+ "Added PersistentContextStore (prevents forgetting between model switches)",
2158
+ "Added SmartHistoryManager (intelligent history compression)",
2159
+ "Fixed Fallback order (starts from requested provider, not always Blackbox)",
2160
+ "Balanced tool instructions (clear but not aggressive)",
2161
+ "Added session_id support for context persistence",
2162
+ "Removed excessive goal injection (was confusing the model)",
2163
+ "Stream tag auto-close preserved from v3"
2164
  ],
2165
  "providers": list(REAL_PROVIDERS.keys()),
2166
  "endpoints": {
 
2173
  "POST /chat": "Simple chat (AUTH)",
2174
  "POST /chat/stream": "Simple stream (AUTH)",
2175
  "GET /providers": "Providers list (AUTH)",
2176
+ "GET /context/{session_id}": "View session context (AUTH)",
2177
+ "DELETE /context/{session_id}": "Clear session context (AUTH)",
2178
  },
2179
  "cookies": COOKIE_STATUS,
2180
  "status": "✅ Server is working"
 
2185
  async def health():
2186
  return {
2187
  "status": "ok",
2188
+ "version": "4.0.0",
2189
  "cookies": COOKIE_STATUS,
2190
  "providers": list(REAL_PROVIDERS.keys()),
2191
  "provider_count": len(REAL_PROVIDERS),
2192
+ "context_store": {
2193
+ "active_sessions": len(CONTEXT_STORE._store),
2194
+ },
2195
  "fixes": {
2196
+ "persistent_context_store": True,
2197
+ "smart_history_manager": True,
2198
+ "balanced_tool_instructions": True,
2199
+ "correct_fallback_order": True,
2200
+ "stream_tag_auto_close": True,
2201
+ "removed_strict_mode": True,
2202
  },
2203
  "timestamp": int(time.time())
2204
  }
 
2217
  return {"providers": result}
2218
 
2219
 
2220
+ # =====================================================
2221
+ # نقطة عرض سياق الجلسة
2222
+ # =====================================================
2223
+ @app.get("/context/{session_id}")
2224
+ async def get_session_context(session_id: str, request: Request):
2225
+ """عرض سياق جلسة محددة - مفيد للتشخيص"""
2226
+ verify_api_key(request)
2227
+ ctx = CONTEXT_STORE.get_or_create_session(session_id)
2228
+ return {
2229
+ "session_id": session_id,
2230
+ "context": ctx,
2231
+ "summary": CONTEXT_STORE.get_context_summary(session_id)
2232
+ }
2233
+
2234
+
2235
+ @app.delete("/context/{session_id}")
2236
+ async def clear_session_context(session_id: str, request: Request):
2237
+ """مسح سياق جلسة محددة"""
2238
+ verify_api_key(request)
2239
+ with CONTEXT_STORE._lock:
2240
+ if session_id in CONTEXT_STORE._store:
2241
+ del CONTEXT_STORE._store[session_id]
2242
+ return {"message": f"Session {session_id} cleared"}
2243
+ return {"message": f"Session {session_id} not found"}
2244
+
2245
+
2246
+ @app.get("/context")
2247
+ async def list_sessions(request: Request):
2248
+ """عرض قائمة الجلسات النشطة"""
2249
+ verify_api_key(request)
2250
+ sessions = []
2251
+ with CONTEXT_STORE._lock:
2252
+ for sid, ctx in CONTEXT_STORE._store.items():
2253
+ sessions.append({
2254
+ "session_id": sid,
2255
+ "message_count": ctx.get("message_count", 0),
2256
+ "original_task_preview": ctx.get("original_task", "")[:100],
2257
+ "current_state": ctx.get("current_state", "idle"),
2258
+ "completed_steps": len(ctx.get("completed_steps", [])),
2259
+ "tool_results": len(ctx.get("tool_results", [])),
2260
+ "last_updated": ctx.get("last_updated", 0),
2261
+ "provider_count": len(ctx.get("provider_history", [])),
2262
+ })
2263
+ return {"sessions": sessions, "total": len(sessions)}
2264
+
2265
+
2266
  # =====================================================
2267
  # نقطة تشخيص: اختبار تحليل tool calls
2268
  # =====================================================
 
2318
  '</tool_call>'
2319
  ),
2320
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2321
  {
2322
  "name": "No tool calls (plain text)",
2323
  "input": "This is just a regular response with no tool calls.",
 
2327
  results = []
2328
  for tc in test_cases:
2329
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(tc["input"])
 
 
 
 
 
 
 
 
 
 
 
 
2330
  results.append({
2331
  "test_name": tc["name"],
2332
  "input_preview": (
 
2338
  "tool_calls_found": len(tool_calls),
2339
  "tool_calls": tool_calls,
2340
  "has_tool_call_marker": ToolCallParser.might_contain_tool_call(tc["input"]),
 
 
2341
  })
2342
 
2343
  return {"test_results": results}
 
2378
 
2379
 
2380
  # =====================================================
2381
+ # نقطة تشخيص: اختبار PersistentContextStore
2382
  # =====================================================
2383
+ @app.post("/debug/test-context-store")
2384
+ async def debug_test_context_store(request: Request):
2385
  verify_api_key(request)
2386
  body = await request.json()
2387
 
2388
+ test_session_id = f"test_{uuid.uuid4().hex[:8]}"
 
 
2389
 
2390
+ # اختبار إنشاء جلسة
2391
+ ctx = CONTEXT_STORE.get_or_create_session(test_session_id)
2392
+ CONTEXT_STORE.update_session(
2393
+ test_session_id,
2394
+ original_task="Create a Python web scraper",
2395
+ current_state="processing"
2396
+ )
2397
 
2398
+ # اختبار إضافة خطوات
2399
+ CONTEXT_STORE.add_completed_step(test_session_id, "Searched for Python scraping libraries")
2400
+ CONTEXT_STORE.add_completed_step(test_session_id, "Found BeautifulSoup and requests")
 
 
 
 
 
 
 
 
 
 
 
 
2401
 
2402
+ # اختبار إضافة نتائج أدوات
2403
+ CONTEXT_STORE.add_tool_result(
2404
+ test_session_id,
2405
+ "web_search",
2406
+ "toolu_test123",
2407
+ "BeautifulSoup4, requests, scrapy are popular Python scraping libraries"
2408
  )
2409
 
2410
+ # الحصول على ملخص السياق
2411
+ summary = CONTEXT_STORE.get_context_summary(test_session_id)
2412
+
2413
+ # تنظيف الجلسة التجريبية
2414
+ with CONTEXT_STORE._lock:
2415
+ if test_session_id in CONTEXT_STORE._store:
2416
+ del CONTEXT_STORE._store[test_session_id]
 
 
 
 
 
 
 
 
 
 
 
 
2417
 
2418
  return {
2419
+ "test_session_id": test_session_id,
2420
+ "context_summary": summary,
2421
+ "test_passed": len(summary) > 0,
2422
+ "summary_length": len(summary)
 
 
2423
  }
2424
 
2425
 
2426
  @app.post("/chat")
2427
  async def chat(request: Request, chat_req: ChatRequest):
2428
  verify_api_key(request)
2429
+ session_id = chat_req.session_id or f"chat_{uuid.uuid4().hex[:16]}"
2430
  result = ""
2431
  for chunk in ask(
2432
+ chat_req.message,
2433
+ chat_req.history,
2434
+ chat_req.provider,
2435
+ chat_req.model,
2436
+ session_id=session_id
2437
  ):
2438
  result += chunk
2439
+ return JSONResponse({"response": result, "session_id": session_id})
2440
 
2441
 
2442
  @app.post("/chat/stream")
2443
  async def chat_stream(request: Request, chat_req: ChatRequest):
2444
  verify_api_key(request)
2445
+ session_id = chat_req.session_id or f"chat_{uuid.uuid4().hex[:16]}"
2446
 
2447
  async def generate():
2448
  for chunk in ask(
2449
+ chat_req.message,
2450
+ chat_req.history,
2451
+ chat_req.provider,
2452
+ chat_req.model,
2453
+ session_id=session_id
2454
  ):
2455
  yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
2456
  yield "data: [DONE]\n\n"
 
2545
  )
2546
 
2547
 
2548
+ # =====================================================
2549
+ # مهمة تنظيف دورية للجلسات القديمة
2550
+ # =====================================================
2551
+ @app.on_event("startup")
2552
+ async def startup_event():
2553
+ """تشغيل مهام عند بدء الخادم"""
2554
+ logger.info("Starting G4F Smart Router v4.0 (Fixed Version)")
2555
+ logger.info(f"Cookies: {COOKIE_STATUS}")
2556
+ logger.info(f"Providers: {list(REAL_PROVIDERS.keys())}")
2557
+ logger.info("Key Fixes:")
2558
+ logger.info(" ✅ Removed STRICT MODE (was breaking model responses)")
2559
+ logger.info(" ✅ Added PersistentContextStore (prevents forgetting)")
2560
+ logger.info(" ✅ Added SmartHistoryManager (intelligent compression)")
2561
+ logger.info(" ✅ Fixed Fallback order (uses requested provider first)")
2562
+ logger.info(" ✅ Balanced tool instructions (clear, not aggressive)")
2563
+ logger.info(" ✅ Added session tracking across model switches")
2564
+
2565
+ # بدء مهمة التنظيف الدورية
2566
+ asyncio.create_task(periodic_cleanup())
2567
+
2568
+
2569
+ async def periodic_cleanup():
2570
+ """تنظيف دوري للجلسات القديمة كل ساعة"""
2571
+ while True:
2572
+ await asyncio.sleep(3600) # كل ساعة
2573
+ CONTEXT_STORE.cleanup_old_sessions(max_age_hours=24)
2574
+ logger.info("[Cleanup] Periodic cleanup completed")
2575
+
2576
+
2577
  # =====================================================
2578
  # التشغيل
2579
  # =====================================================
2580
  if __name__ == "__main__":
2581
  import uvicorn
2582
  port = int(os.getenv("PORT", 7860))
2583
+ logger.info(f"Starting G4F Smart Router v4.0 on port {port}")
 
 
 
 
 
 
 
2584
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)