diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -21,7 +21,7 @@ logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
-logger = logging.getLogger("g4f-smart-router")
+logger = logging.getLogger("g4f-execution-engine")
# =====================================================
# COOKIES
@@ -62,18 +62,42 @@ def load_cookies() -> str:
COOKIE_STATUS = load_cookies()
+# =====================================================
+# EXECUTION STATE MACHINE
+# =====================================================
+class ExecutionState:
+ IDLE = "idle"
+ PLANNING = "planning"
+ TOOL_PENDING = "tool_pending"
+ TOOL_EXECUTING = "tool_executing"
+ TOOL_DONE = "tool_done"
+ AWAITING_NEXT = "awaiting_next"
+ COMPLETED = "completed"
+
+class PendingToolCall:
+ def __init__(self, name: str, arguments: Dict, tool_id: str, reason: str = ""):
+ self.name = name
+ self.arguments = arguments
+ self.tool_id = tool_id
+ self.reason = reason
+ self.created_at = time.time()
+ self.attempts = 0
+ self.max_attempts = 3
+
+ def to_dict(self) -> Dict:
+ return {
+ "name": self.name,
+ "arguments": self.arguments,
+ "tool_id": self.tool_id,
+ "reason": self.reason,
+ "created_at": self.created_at,
+ "attempts": self.attempts
+ }
+
# =====================================================
# PERSISTENT CONTEXT STORE
-# يحفظ سياق المهام بين النماذج المختلفة ويمنع النسيان
# =====================================================
class PersistentContextStore:
- """
- مخزن السياق الدائم - يحفظ:
- 1. المهمة الأصلية (Original Task)
- 2. الخطوات المكتملة (Completed Steps)
- 3. نتائج الأدوات (Tool Results)
- 4. الحالة الحالية (Current State)
- """
def __init__(self):
self._store: Dict[str, Dict] = {}
self._lock = threading.Lock()
@@ -83,14 +107,20 @@ class PersistentContextStore:
if session_id not in self._store:
self._store[session_id] = {
"original_task": "",
+ "task_breakdown": [],
"completed_steps": [],
"tool_results": [],
"pending_tool_calls": [],
- "current_state": "idle",
+ "executed_tool_calls": [],
+ "current_state": ExecutionState.IDLE,
+ "next_required_action": "",
"created_at": time.time(),
"last_updated": time.time(),
"message_count": 0,
"provider_history": [],
+ "last_model_response": "",
+ "described_but_not_executed": [],
+ "consecutive_description_count": 0,
}
return self._store[session_id]
@@ -105,13 +135,30 @@ class PersistentContextStore:
with self._lock:
if session_id not in self._store:
self.get_or_create_session(session_id)
+ # إزالة من المعلّقة
+ self._store[session_id]["pending_tool_calls"] = [
+ p for p in self._store[session_id].get("pending_tool_calls", [])
+ if p.get("tool_id") != tool_id
+ ]
+ # إضافة للمنفَّذة
+ self._store[session_id]["executed_tool_calls"].append({
+ "tool_name": tool_name,
+ "tool_id": tool_id,
+ "timestamp": time.time()
+ })
+ # حفظ النتيجة
self._store[session_id]["tool_results"].append({
"tool_name": tool_name,
"tool_id": tool_id,
"result": result,
"timestamp": time.time()
})
+ # إعادة ضبط عداد الوصف
+ self._store[session_id]["consecutive_description_count"] = 0
+ self._store[session_id]["described_but_not_executed"] = []
self._store[session_id]["last_updated"] = time.time()
+ if self._store[session_id].get("current_state") == ExecutionState.TOOL_EXECUTING:
+ self._store[session_id]["current_state"] = ExecutionState.TOOL_DONE
def add_completed_step(self, session_id: str, step: str):
with self._lock:
@@ -123,16 +170,142 @@ class PersistentContextStore:
})
self._store[session_id]["last_updated"] = time.time()
- def get_context_summary(self, session_id: str) -> str:
+ def register_pending_tool(self, session_id: str, tool_call: PendingToolCall):
+ with self._lock:
+ if session_id not in self._store:
+ self.get_or_create_session(session_id)
+ existing_ids = [
+ p.get("tool_id")
+ for p in self._store[session_id].get("pending_tool_calls", [])
+ ]
+ if tool_call.tool_id not in existing_ids:
+ self._store[session_id]["pending_tool_calls"].append(tool_call.to_dict())
+ self._store[session_id]["current_state"] = ExecutionState.TOOL_PENDING
+ self._store[session_id]["last_updated"] = time.time()
+
+ def get_pending_tools(self, session_id: str) -> List[Dict]:
+ with self._lock:
+ if session_id not in self._store:
+ return []
+ return self._store[session_id].get("pending_tool_calls", [])
+
+ def increment_description_count(self, session_id: str, response_text: str) -> int:
+ """
+ يزيد عداد "الوصف بدون تنفيذ" ويعيد العدد الحالي.
+ هذا يُصلح مشكلة "المشاهد الذكي" بتتبع دقيق لعدد المرات.
+ """
+ with self._lock:
+ if session_id not in self._store:
+ self.get_or_create_session(session_id)
+ self._store[session_id]["described_but_not_executed"].append({
+ "response_preview": response_text[:300],
+ "timestamp": time.time()
+ })
+ count = self._store[session_id].get("consecutive_description_count", 0) + 1
+ self._store[session_id]["consecutive_description_count"] = count
+ self._store[session_id]["last_updated"] = time.time()
+ return count
+
+ def get_execution_directive(self, session_id: str) -> str:
"""
- يولّد ملخصاً للسياق يُحقن في كل رسالة
- هذا هو سر منع النسيان - السياق يُعاد حقنه في كل مرة
+ *** القلب الحقيقي للحل ***
+ يولّد توجيهاً إجبارياً يحدد ما يجب فعله الآن.
+ هذا يحل مشكلة "غياب الدفعة الأخيرة".
"""
with self._lock:
if session_id not in self._store:
return ""
ctx = self._store[session_id]
+ parts = []
+ state = ctx.get("current_state", ExecutionState.IDLE)
+ pending = ctx.get("pending_tool_calls", [])
+ executed = ctx.get("executed_tool_calls", [])
+ desc_count = ctx.get("consecutive_description_count", 0)
+ tool_results = ctx.get("tool_results", [])
+
+ # ===== الحالة 1: أداة معلّقة - أعلى أولوية =====
+ if pending and state == ExecutionState.TOOL_PENDING:
+ pending_tool = pending[0]
+ tool_name = pending_tool.get("name", "unknown")
+ tool_args = pending_tool.get("arguments", {})
+ tool_id = pending_tool.get("tool_id", "")
+ reason = pending_tool.get("reason", "")
+
+ parts.append("## ⚡ MANDATORY IMMEDIATE ACTION")
+ parts.append("")
+ parts.append(
+ f"Tool `{tool_name}` was registered as pending but NOT executed."
+ )
+ if reason:
+ parts.append(f"Reason: {reason}")
+ parts.append("")
+ parts.append("**COPY THIS EXACTLY INTO YOUR RESPONSE:**")
+ parts.append("")
+ parts.append("")
+ call_obj = {"name": tool_name, "arguments": tool_args}
+ if tool_id:
+ call_obj["id"] = tool_id
+ parts.append(json.dumps(call_obj, ensure_ascii=False, indent=2))
+ parts.append("")
+ parts.append("")
+ parts.append("NO other text is needed. Just the tool_call block above.")
+ return "\n".join(parts)
+
+ # ===== الحالة 2: وصف متكرر بدون تنفيذ =====
+ if desc_count >= 1:
+ parts.append("## 🚨 CRITICAL: EXECUTION FAILURE PATTERN DETECTED")
+ parts.append("")
+ parts.append(
+ f"You have described actions {desc_count} time(s) without executing them."
+ )
+ parts.append("")
+ parts.append("**THE ONLY ACCEPTABLE RESPONSE FORMAT:**")
+ parts.append("")
+ parts.append("```")
+ parts.append("")
+ parts.append('{"name": "TOOL_NAME", "arguments": {ACTUAL_ARGUMENTS}}')
+ parts.append("")
+ parts.append("```")
+ parts.append("")
+ parts.append("**FORBIDDEN PATTERNS (will be rejected):**")
+ parts.append('❌ "I will create..." → FORBIDDEN')
+ parts.append('❌ "I\'m going to use..." → FORBIDDEN')
+ parts.append('❌ "Let me write..." → FORBIDDEN')
+ parts.append('❌ "I\'ll use the tool..." → FORBIDDEN')
+ parts.append('❌ "سأقوم بـ..." → FORBIDDEN')
+ parts.append('❌ "سأستخدم أداة..." → FORBIDDEN')
+ parts.append("")
+ parts.append("**EXECUTE NOW. No descriptions. No explanations.**")
+ return "\n".join(parts)
+
+ # ===== الحالة 3: أداة منفَّذة، تحقق من الاكتمال =====
+ if tool_results and state == ExecutionState.TOOL_DONE:
+ last_result = tool_results[-1]
+ tool_name = last_result.get("tool_name", "")
+ next_action = ctx.get("next_required_action", "")
+
+ parts.append(f"## ✅ Tool `{tool_name}` Completed Successfully")
+ parts.append("")
+ if next_action:
+ parts.append(f"**Next Required Action:** {next_action}")
+ parts.append("Execute it now using the appropriate tool.")
+ else:
+ parts.append(
+ "Determine if the original task is now complete. "
+ "If more tools are needed, call them now. "
+ "If done, provide a brief confirmation."
+ )
+ return "\n".join(parts)
+
+ return ""
+
+ def get_context_summary(self, session_id: str) -> str:
+ with self._lock:
+ if session_id not in self._store:
+ return ""
+ ctx = self._store[session_id]
+
parts = []
if ctx.get("original_task"):
@@ -140,31 +313,49 @@ class PersistentContextStore:
if ctx.get("completed_steps"):
steps_text = "\n".join(
- f" - {s['step']}" for s in ctx["completed_steps"][-10:]
+ f" ✅ {s['step']}"
+ for s in ctx["completed_steps"][-10:]
)
parts.append(f"## Completed Steps\n{steps_text}")
+ if ctx.get("executed_tool_calls"):
+ exec_text = "\n".join(
+ f" 🔧 {e['tool_name']} (id: {e['tool_id'][:12]}...)"
+ for e in ctx["executed_tool_calls"][-5:]
+ )
+ parts.append(f"## Tools Already Executed\n{exec_text}")
+
if ctx.get("tool_results"):
- # آخر 5 نتائج فقط لتجنب الحجم الكبير
- recent_results = ctx["tool_results"][-5:]
+ recent_results = ctx["tool_results"][-3:]
results_text = "\n".join(
- f" - [{r['tool_name']}]: {r['result'][:300]}..."
- if len(r['result']) > 300
- else f" - [{r['tool_name']}]: {r['result']}"
+ f" - [{r['tool_name']}]: "
+ f"{r['result'][:300]}{'...' if len(r['result']) > 300 else ''}"
for r in recent_results
)
- parts.append(f"## Tool Results So Far\n{results_text}")
+ parts.append(f"## Tool Results Available\n{results_text}")
+
+ if ctx.get("pending_tool_calls"):
+ pending_text = "\n".join(
+ f" ⏳ {p['name']} (NOT YET EXECUTED)"
+ for p in ctx["pending_tool_calls"]
+ )
+ parts.append(f"## ⚠️ PENDING TOOLS (Must Execute)\n{pending_text}")
- if ctx.get("current_state") and ctx["current_state"] != "idle":
- parts.append(f"## Current State\n{ctx['current_state']}")
+ # التوجيه التنفيذي - الأهم دائماً
+ directive = self.get_execution_directive(session_id)
+ if directive:
+ parts.append(directive)
if not parts:
return ""
- return "# Task Context (DO NOT FORGET)\n" + "\n\n".join(parts)
+ return (
+ "# Task Context & Execution State\n"
+ "# (This is your memory - use it to continue the task)\n\n"
+ + "\n\n".join(parts)
+ )
def cleanup_old_sessions(self, max_age_hours: int = 24):
- """تنظيف الجلسات القديمة"""
with self._lock:
now = time.time()
old_sessions = [
@@ -177,12 +368,10 @@ class PersistentContextStore:
logger.info(f"[Context] Cleaned {len(old_sessions)} old sessions")
def extract_task_from_messages(self, messages: List[Dict]) -> str:
- """استخراج المهمة الأصلية من أول رسالة مستخدم"""
for msg in messages:
if msg.get("role") == "user":
content = msg.get("content", "")
if isinstance(content, str) and len(content) > 10:
- # أول 500 حرف من أول رسالة مستخدم = المهمة الأصلية
return content[:500]
elif isinstance(content, list):
for item in content:
@@ -192,12 +381,10 @@ class PersistentContextStore:
return text[:500]
return ""
-
-# المخزن العالمي للسياق
CONTEXT_STORE = PersistentContextStore()
# =====================================================
-# CACHE محسن
+# CACHE
# =====================================================
class TTLCache:
def __init__(self, max_size: int = 100, ttl_seconds: int = 300):
@@ -315,10 +502,10 @@ def clean_stream(chunk):
pass
if '\\' in chunk:
chunk = chunk.replace('\\n', '\n')
- if '\\r' in chunk:
- chunk = chunk.replace('\\r', '\r')
- if '\\t' in chunk:
- chunk = chunk.replace('\\t', ' ')
+ if '\\r' in chunk:
+ chunk = chunk.replace('\\r', '\r')
+ if '\\t' in chunk:
+ chunk = chunk.replace('\\t', ' ')
return chunk
return str(chunk)
except Exception as e:
@@ -326,7 +513,7 @@ def clean_stream(chunk):
return ""
# =====================================================
-# استخراج النص من content
+# استخراج النص
# =====================================================
def extract_text_from_content(content) -> str:
if isinstance(content, str):
@@ -354,10 +541,7 @@ def extract_text_from_content(content) -> str:
result_str = tool_content
else:
result_str = str(tool_content)
- # صيغة واضحة ومنظمة لنتيجة الأداة
- parts.append(
- f"[Tool Result for {tool_use_id}]:\n{result_str}"
- )
+ parts.append(f"[Tool Result for {tool_use_id}]:\n{result_str}")
elif item.get("type") == "tool_use":
tool_name = item.get("name", "unknown")
tool_input = item.get("input", {})
@@ -376,12 +560,94 @@ def extract_text_from_content(content) -> str:
return ""
# =====================================================
-# TOOL CALL PARSER
+# RESPONSE ANALYZER - محسّن جذرياً
+# =====================================================
+class ResponseAnalyzer:
+ """
+ يحلل رد النموذج لاكتشاف:
+ 1. هل وصف فعلاً بدون تنفيذ؟
+ 2. هل يحتوي على tool call فعلي؟
+ 3. ما هي الأداة المقصودة؟
+ """
+
+ # أنماط "الوصف بدون تنفيذ" - شاملة ودقيقة
+ DESCRIPTION_PHRASES = [
+ # عربي
+ r"سأقوم\s+ب",
+ r"سأستخدم\s+(?:أداة|الأداة|tool)",
+ r"سأكتب\s+(?:الكود|الملف|الآن)",
+ r"سأنشئ\s+(?:ملف|الملف|كود)",
+ r"سأحفظ\s+(?:الملف|الكود|الآن)",
+ r"سأبدأ\s+(?:الآن|بـ)",
+ r"دعني\s+(?:أنشئ|أكتب|أستخدم|أحفظ)",
+ # إنجليزي - I will
+ r"I\s+will\s+(?:now\s+)?(?:create|write|save|use|call|make|build|generate|execute)",
+ r"I\s+will\s+(?:now\s+)?(?:use|call)\s+the\s+\w+\s+tool",
+ # إنجليزي - I'm going to
+ r"I(?:'m|'m)\s+going\s+to\s+(?:create|write|save|use|call|make|build|generate)",
+ # إنجليزي - Let me
+ r"Let\s+me\s+(?:create|write|save|use|call|make|build|generate|now)",
+ # إنجليزي - Now I'll
+ r"Now\s+I(?:'ll|'m\s+going\s+to)\s+(?:create|write|save|use|call|make|build)",
+ # إنجليزي - I'll
+ r"I(?:'ll|'ll)\s+(?:now\s+)?(?:use|call)\s+the\s+\w+\s+(?:tool|function)",
+ r"I(?:'ll|'ll)\s+(?:now\s+)?(?:create|write|save|make|build|generate)",
+ # إنجليزي - using the tool
+ r"using\s+the\s+(?:write_file|create_file|save_file|search|browse|read_file)\s+tool",
+ # إنجليزي - To create/write
+ r"To\s+(?:create|write|save)\s+this\s+(?:file|code|content)",
+ # إنجليزي - I need to use
+ r"I\s+need\s+to\s+(?:use|call)\s+the\s+\w+\s+(?:tool|function)",
+ # الوصف في code block
+ r"```json\n\{[^}]*\"name\"\s*:",
+ ]
+
+ @classmethod
+ def analyze(cls, response: str, available_tools: List[Dict] = None) -> Dict:
+ result = {
+ "has_actual_tool_call": False,
+ "has_description_without_execution": False,
+ "described_actions": [],
+ "needs_execution_push": False,
+ "tool_calls_found": [],
+ "intended_tool_name": None,
+ }
+
+ # التحقق من وجود tool call فعلي
+ clean_text, tool_calls = ToolCallParser.parse_tool_calls(response, available_tools)
+ result["has_actual_tool_call"] = len(tool_calls) > 0
+ result["tool_calls_found"] = tool_calls
+
+ # البحث عن عبارات الوصف
+ descriptions_found = []
+ for pattern in cls.DESCRIPTION_PHRASES:
+ matches = re.findall(pattern, response, re.IGNORECASE)
+ descriptions_found.extend(matches)
+
+ result["described_actions"] = descriptions_found
+
+ # الحكم النهائي
+ if descriptions_found and not result["has_actual_tool_call"]:
+ result["has_description_without_execution"] = True
+ result["needs_execution_push"] = True
+
+ # محاولة استخراج اسم الأداة المقصودة
+ if available_tools:
+ for tool in available_tools:
+ tool_name = tool.get("name", "")
+ if tool_name and tool_name.lower() in response.lower():
+ result["intended_tool_name"] = tool_name
+ break
+
+ return result
+
+# =====================================================
+# TOOL CALL PARSER - محسّن مع تسامح أعلى
# =====================================================
class ToolCallParser:
"""
- يكتشف ويحلل وسوم Tool Call من استجابة النموذج.
- يدعم أنماطاً متعددة مع regex محسّن يقبل المسافات والأسطر الجديدة.
+ يكتشف ويحلل وسوم Tool Call.
+ محسّن للتعامل مع التنسيقات المختلفة والأخطاء الطفيفة.
"""
START_MARKERS = [
@@ -399,12 +665,10 @@ class ToolCallParser:
@staticmethod
def generate_tool_id() -> str:
- """توليد معرف فريد للأداة بصيغة Anthropic"""
return f"toolu_{uuid.uuid4().hex[:24]}"
@classmethod
def might_contain_tool_call(cls, text: str) -> bool:
- """فحص سريع: هل النص قد يحتوي على بداية tool call؟"""
text_stripped = text.strip().lower()
for marker in cls.START_MARKERS:
if marker.lower() in text_stripped:
@@ -419,11 +683,11 @@ class ToolCallParser:
text: str,
available_tools: Optional[List[Dict]] = None
) -> Tuple[str, List[Dict]]:
- """تحليل النص واستخراج tool calls منه."""
tool_calls = []
clean_text = text
- # ---- النمط 1: JSON ----
+ # النمط 1: JSON
+ # مرن جداً - يتحمل مسافات وأسطر إضافية
pattern1 = re.compile(
r'\s*\s*(\{.*?\})\s*\s*',
re.DOTALL | re.IGNORECASE
@@ -431,13 +695,15 @@ class ToolCallParser:
for match in pattern1.finditer(text):
try:
raw_json = match.group(1).strip()
+ # تنظيف JSON من الأخطاء الشائعة
+ raw_json = cls._clean_json_string(raw_json)
parsed = json.loads(raw_json)
tool_call = cls._normalize_tool_call(parsed, available_tools)
if tool_call:
tool_calls.append(tool_call)
clean_text = clean_text.replace(match.group(0), " ", 1)
except json.JSONDecodeError as e:
- logger.warning(f"[Parser] Failed to parse tool_call JSON: {e}")
+ logger.warning(f"[Parser] JSON parse error: {e}")
fixed = cls._try_fix_json(match.group(1).strip())
if fixed:
tool_call = cls._normalize_tool_call(fixed, available_tools)
@@ -445,7 +711,7 @@ class ToolCallParser:
tool_calls.append(tool_call)
clean_text = clean_text.replace(match.group(0), " ", 1)
- # ---- النمط 2: ```tool_call\nJSON\n``` ----
+ # النمط 2: ```tool_call\nJSON\n```
if not tool_calls:
pattern2 = re.compile(
r'```(?:tool_call|json)?\s*\n?\s*(\{.*?"(?:name|function)".*?\})\s*\n?\s*```',
@@ -454,6 +720,7 @@ class ToolCallParser:
for match in pattern2.finditer(text):
try:
raw_json = match.group(1).strip()
+ raw_json = cls._clean_json_string(raw_json)
parsed = json.loads(raw_json)
tool_call = cls._normalize_tool_call(parsed, available_tools)
if tool_call:
@@ -462,7 +729,7 @@ class ToolCallParser:
except json.JSONDecodeError:
pass
- # ---- النمط 3: JSON ----
+ # النمط 3: JSON
if not tool_calls:
pattern3 = re.compile(
r'\s*\s*(\{.*?\})\s*\s*',
@@ -486,7 +753,7 @@ class ToolCallParser:
except json.JSONDecodeError:
pass
- # ---- النمط 4: [TOOL_CALL]...[/TOOL_CALL] ----
+ # النمط 4: [TOOL_CALL]...[/TOOL_CALL]
if not tool_calls:
pattern4 = re.compile(
r'\s*\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]\s*',
@@ -518,7 +785,7 @@ class ToolCallParser:
except json.JSONDecodeError:
pass
- # ---- النمط 5: ✿FUNCTION✿ ----
+ # النمط 5: ✿FUNCTION✿
if not tool_calls:
pattern5 = re.compile(
r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
@@ -539,7 +806,7 @@ class ToolCallParser:
except json.JSONDecodeError:
pass
- # ---- النمط 6: JSON مباشر يحتوي tool_calls ----
+ # النمط 6: JSON مباشر يحتوي tool_calls
if not tool_calls:
try:
json_pattern = re.compile(
@@ -553,7 +820,10 @@ class ToolCallParser:
for tc in parsed["tool_calls"]:
func_data = tc.get("function", tc)
name = func_data.get("name", "")
- args = func_data.get("arguments", func_data.get("input", {}))
+ args = func_data.get(
+ "arguments",
+ func_data.get("input", {})
+ )
if isinstance(args, str):
try:
args = json.loads(args)
@@ -567,7 +837,7 @@ class ToolCallParser:
"input": args
}
tool_calls.append(tool_call)
- clean_text = clean_text.replace(jm.group(0), " ", 1)
+ clean_text = clean_text.replace(jm.group(0), " ", 1)
except json.JSONDecodeError:
pass
except Exception:
@@ -575,17 +845,30 @@ class ToolCallParser:
clean_text = clean_text.strip()
clean_text = re.sub(r'\n{3,}', '\n\n', clean_text)
- clean_text = re.sub(r' +', ' ', clean_text)
-
+ clean_text = re.sub(r' +', ' ', clean_text)
return clean_text, tool_calls
+ @classmethod
+ def _clean_json_string(cls, json_str: str) -> str:
+ """
+ تنظيف JSON من الأخطاء الشائعة.
+ هذا يحل مشكلة "ضياع الأوامر في الترجمة".
+ """
+ # إزالة trailing commas
+ json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
+ # إصلاح علامات الاقتباس المنحنية
+ json_str = json_str.replace('\u201c', '"').replace('\u201d', '"')
+ json_str = json_str.replace('\u2018', "'").replace('\u2019', "'")
+ # إزالة BOM
+ json_str = json_str.lstrip('\ufeff')
+ return json_str
+
@classmethod
def _normalize_tool_call(
cls,
parsed: Dict,
available_tools: Optional[List[Dict]] = None
) -> Optional[Dict]:
- """توحيد صيغة tool call من أشكال مختلفة إلى صيغة Anthropic"""
name = None
arguments = {}
@@ -595,11 +878,11 @@ class ToolCallParser:
elif "name" in parsed:
name = parsed["name"]
arguments = (
- parsed.get("arguments") or
- parsed.get("parameters") or
- parsed.get("input") or
- parsed.get("args") or
- {}
+ parsed.get("arguments")
+ or parsed.get("parameters")
+ or parsed.get("input")
+ or parsed.get("args")
+ or {}
)
elif "tool" in parsed:
name = parsed["tool"]
@@ -632,7 +915,6 @@ class ToolCallParser:
tool_call: Dict,
available_tools: List[Dict]
) -> Optional[Dict]:
- """التحقق من أن الأداة موجودة في القائمة المتاحة"""
requested_name = tool_call["name"]
tool_names = []
@@ -641,7 +923,6 @@ class ToolCallParser:
if not tool_name and "function" in tool:
tool_name = tool["function"].get("name", "")
tool_names.append(tool_name)
-
if tool_name == requested_name:
return tool_call
@@ -655,13 +936,15 @@ class ToolCallParser:
tool_call["name"] = tool_name
return tool_call
- logger.warning(f"[Validator] Tool '{requested_name}' not found in: {tool_names}")
+ logger.warning(f"[Validator] Tool '{requested_name}' not in: {tool_names}")
return tool_call
@classmethod
def _try_fix_json(cls, broken_json: str) -> Optional[Dict]:
- """محاولة إصلاح JSON مكسور"""
- fixed = re.sub(r',\s*([}\]])', r'\1', broken_json)
+ # تنظيف أولي
+ fixed = cls._clean_json_string(broken_json)
+
+ # إغلاق الأقواس المفتوحة
open_braces = fixed.count('{') - fixed.count('}')
if open_braces > 0:
fixed += '}' * open_braces
@@ -674,6 +957,7 @@ class ToolCallParser:
except json.JSONDecodeError:
pass
+ # محاولة استخراج JSON من وسط النص
try:
start = fixed.index('{')
depth = 0
@@ -689,15 +973,10 @@ class ToolCallParser:
return None
-
# =====================================================
# STREAM TOOL BUFFER
# =====================================================
class StreamToolBuffer:
- """
- Buffer ذكي يجمع chunks الـ stream ويكتشف tool calls.
- """
-
def __init__(self, available_tools: Optional[List[Dict]] = None):
self.buffer = ""
self.in_tool_call = False
@@ -710,10 +989,8 @@ class StreamToolBuffer:
self._stream_started_at = time.time()
def feed(self, chunk: str) -> List[Dict]:
- """إطعام chunk جديد للـ buffer."""
events = []
self.buffer += chunk
-
while self.buffer:
if self.in_tool_call:
events.extend(self._process_tool_call_mode())
@@ -724,11 +1001,9 @@ class StreamToolBuffer:
if self.in_tool_call:
continue
break
-
return events
def _process_text_mode(self) -> List[Dict]:
- """معالجة النص في النمط العادي."""
events = []
buffer_lower = self.buffer.lower()
earliest_pos = -1
@@ -753,7 +1028,6 @@ class StreamToolBuffer:
events.append({"type": "text", "text": text_before})
elif text_before and text_before.isspace():
events.append({"type": "text", "text": " "})
-
self.buffer = self.buffer[earliest_pos:]
self.in_tool_call = True
self.tool_call_buffer = ""
@@ -763,7 +1037,6 @@ class StreamToolBuffer:
return events
def _set_active_end_marker(self, start_marker: str):
- """تحديد علامة النهاية المقابلة لعلامة البداية"""
start_lower = start_marker.lower()
if start_lower == '':
self._active_end_marker = ''
@@ -779,9 +1052,7 @@ class StreamToolBuffer:
self._active_end_marker = ''
def _process_tool_call_mode(self) -> List[Dict]:
- """معالجة النص في نمط tool call."""
events = []
-
end_markers_pairs = [
('', ''),
('```', '```tool_call'),
@@ -790,28 +1061,22 @@ class StreamToolBuffer:
('[/tool_call]', '[tool_call]'),
('[/TOOL_CALL]', '[TOOL_CALL]'),
]
-
buffer_lower = self.buffer.lower()
found_end = False
for end_marker, start_marker in end_markers_pairs:
end_lower = end_marker.lower()
start_lower = start_marker.lower()
-
if not buffer_lower.startswith(start_lower):
continue
-
search_from = len(start_marker)
end_pos = buffer_lower.find(end_lower, search_from)
-
if end_pos != -1:
full_tool_text = self.buffer[:end_pos + len(end_marker)]
remaining = self.buffer[end_pos + len(end_marker):]
-
clean_text, tool_calls = ToolCallParser.parse_tool_calls(
full_tool_text, self.available_tools
)
-
for tc in tool_calls:
if not tc.get("id"):
tc["id"] = ToolCallParser.generate_tool_id()
@@ -820,10 +1085,8 @@ class StreamToolBuffer:
f"(id: {tc['id']})"
)
events.append(tc)
-
if clean_text.strip():
events.append({"type": "text", "text": clean_text})
-
self.buffer = remaining
self.in_tool_call = False
self._active_start_marker = ""
@@ -834,8 +1097,7 @@ class StreamToolBuffer:
if not found_end:
if len(self.buffer) > 5000:
logger.warning(
- "[Buffer] Tool call buffer too large without closing tag, "
- "treating as text"
+ "[Buffer] Tool call buffer too large, treating as text"
)
events.append({"type": "text", "text": self.buffer})
self.buffer = ""
@@ -846,26 +1108,20 @@ class StreamToolBuffer:
return events
def flush(self) -> List[Dict]:
- """تفريغ أي محتوى متبقي في الـ buffer مع إصلاح الوسوم غير المكتملة."""
events = []
-
if self.in_tool_call and self.buffer:
logger.warning(
- f"[Buffer] Stream ended with unclosed tool_call tag. "
- f"Active marker: '{self._active_start_marker}'. "
+ f"[Buffer] Stream ended with unclosed tool_call. "
f"Attempting manual closure..."
)
-
end_marker = self._active_end_marker or ""
buffer_with_close = self.buffer + end_marker
-
clean_text, tool_calls = ToolCallParser.parse_tool_calls(
buffer_with_close, self.available_tools
)
-
if tool_calls:
logger.info(
- f"[Buffer] Manual closure SUCCESS: recovered {len(tool_calls)} tool call(s)"
+ f"[Buffer] Manual closure SUCCESS: {len(tool_calls)} tool call(s)"
)
for tc in tool_calls:
if not tc.get("id"):
@@ -874,14 +1130,12 @@ class StreamToolBuffer:
if clean_text.strip():
events.append({"type": "text", "text": clean_text})
else:
- # محاولة إصلاح JSON الناقص
raw_buffer = self.buffer
if self._active_start_marker:
start_lower = self._active_start_marker.lower()
raw_lower = raw_buffer.lower()
if raw_lower.startswith(start_lower):
raw_buffer = raw_buffer[len(self._active_start_marker):]
-
json_start = raw_buffer.find('{')
if json_start != -1:
json_fragment = raw_buffer[json_start:].strip()
@@ -891,15 +1145,12 @@ class StreamToolBuffer:
open_brackets = json_fragment.count('[') - json_fragment.count(']')
if open_brackets > 0:
json_fragment += ']' * open_brackets
-
repaired_text = (
self._active_start_marker + json_fragment + end_marker
)
-
clean_text2, tool_calls2 = ToolCallParser.parse_tool_calls(
repaired_text, self.available_tools
)
-
if tool_calls2:
for tc in tool_calls2:
if not tc.get("id"):
@@ -913,7 +1164,6 @@ class StreamToolBuffer:
else:
if self.buffer.strip():
events.append({"type": "text", "text": self.buffer})
-
elif self.buffer:
if self.buffer.strip():
events.append({"type": "text", "text": self.buffer})
@@ -922,39 +1172,40 @@ class StreamToolBuffer:
self.in_tool_call = False
self._active_start_marker = ""
self._active_end_marker = ""
-
return events
-
# =====================================================
-# TOOLS FORMATTER - نسخة متوازنة (لا تشدد مفرط)
+# TOOLS FORMATTER - محسّن
# =====================================================
class ToolsFormatter:
- """تحويل أدوات Anthropic إلى نص للنموذج - صيغة متوازنة"""
-
@staticmethod
def format_tools_for_prompt(tools: List[Dict], tool_choice: Any = None) -> str:
- """
- تنسيق الأدوات بصيغة واضحة دون ضغط مفرط.
- الضغط المفرط كان سبب تجاهل النموذج للأوامر.
- """
if not tools:
return ""
lines = []
- lines.append("# Available Tools")
+ lines.append("# Tools Available")
lines.append("")
- lines.append("You have access to the following tools.")
- lines.append("When you need to use a tool, format your response like this:")
+ lines.append(
+ "**CRITICAL RULE**: You are an EXECUTOR, not a DESCRIBER. "
+ "When you decide to use a tool, you MUST call it immediately "
+ "using the format below. "
+ "NEVER write 'I will use...' or 'I'm going to...' - just DO IT."
+ )
lines.append("")
+ lines.append("## ✅ CORRECT - How to call a tool:")
lines.append("")
- lines.append('{"name": "tool_name", "arguments": {"param1": "value1"}}')
+ lines.append('{"name": "tool_name", "arguments": {"param": "value"}}')
lines.append("")
lines.append("")
- lines.append("You can include text before or after the tool_call block.")
- lines.append("You can make multiple tool calls using multiple blocks.")
+ lines.append("## ❌ WRONG - What NOT to do:")
+ lines.append(' "I will now use the write_file tool to..." → FORBIDDEN')
+ lines.append(' "Let me create the file..." → FORBIDDEN')
+ lines.append(' "I\'ll use the tool to..." → FORBIDDEN')
+ lines.append("")
+ lines.append("---")
lines.append("")
- lines.append("## Tool Definitions:")
+ lines.append("## Available Tools:")
lines.append("")
for i, tool in enumerate(tools, 1):
@@ -963,54 +1214,71 @@ class ToolsFormatter:
input_schema = tool.get("input_schema", {})
lines.append(f"### {i}. `{name}`")
- lines.append(f"**Description:** {description}")
+ lines.append(f"**Purpose:** {description}")
properties = input_schema.get("properties", {})
required = input_schema.get("required", [])
+ example_args = {}
if properties:
lines.append("**Parameters:**")
for param_name, param_info in properties.items():
param_type = param_info.get("type", "any")
param_desc = param_info.get("description", "")
is_required = param_name in required
- req_marker = " (required)" if is_required else " (optional)"
+ req_marker = " *(required)*" if is_required else " *(optional)*"
lines.append(
f" - `{param_name}` ({param_type}{req_marker}): {param_desc}"
)
if "enum" in param_info:
lines.append(
- f" Allowed values: "
- f"{', '.join(str(v) for v in param_info['enum'])}"
+ f" Values: {', '.join(str(v) for v in param_info['enum'])}"
)
- else:
- lines.append("**Parameters:** None")
+ if param_name in required:
+ if param_type == "string":
+ example_args[param_name] = f"<{param_name}_value>"
+ elif param_type == "integer":
+ example_args[param_name] = 0
+ elif param_type == "boolean":
+ example_args[param_name] = True
+ elif param_type == "array":
+ example_args[param_name] = []
+ else:
+ example_args[param_name] = f"<{param_name}>"
+ lines.append(f"**Call it like this:**")
+ lines.append("")
+ example = {"name": name, "arguments": example_args}
+ lines.append(json.dumps(example, ensure_ascii=False, indent=2))
+ lines.append("")
lines.append("")
- # معالجة tool_choice بشكل معتدل
+ # معالجة tool_choice
if tool_choice:
if isinstance(tool_choice, dict):
if tool_choice.get("type") == "tool":
forced_tool = tool_choice.get("name", "")
if forced_tool:
+ lines.append("---")
lines.append(
- f"**Note:** Please use the `{forced_tool}` tool for this request."
+ f"**⚡ MANDATORY:** You MUST call `{forced_tool}` right now. "
+ f"Use the format above."
)
elif tool_choice.get("type") == "any":
+ lines.append("---")
lines.append(
- "**Note:** Please use at least one tool to complete this request."
+ "**⚡ MANDATORY:** You MUST call at least one tool. "
+ "Use the format above."
)
elif tool_choice == "any":
- lines.append("**Note:** Please use at least one tool.")
+ lines.append("---")
+ lines.append("**⚡ MANDATORY:** Use at least one tool now.")
lines.append("")
-
return "\n".join(lines)
@staticmethod
def format_tool_result_for_message(tool_use_id: str, content: Any) -> str:
- """تحويل نتيجة أداة إلى نص مفهوم للنموذج."""
result_text = ""
if isinstance(content, str):
result_text = content
@@ -1038,18 +1306,10 @@ class ToolsFormatter:
f"[End Tool Result]"
)
-
# =====================================================
-# MESSAGE CONVERTER - محسّن للحفاظ على السياق الكامل
+# MESSAGE CONVERTER
# =====================================================
class MessageConverter:
- """
- تحويل رسائل Anthropic إلى صيغة g4f مع الحفاظ الكامل على السياق.
-
- الإصلاح الجوهري: لا نحذف أي رسائل من التاريخ إلا عند الضرورة القصوى،
- والسياق يُحفظ في PersistentContextStore ويُحقن في كل طلب.
- """
-
@staticmethod
def convert_messages(
messages: List[Dict],
@@ -1058,13 +1318,10 @@ class MessageConverter:
tool_choice: Any = None,
session_id: str = ""
) -> Tuple[str, List[Dict]]:
- """تحويل رسائل Anthropic إلى (full_message, history) لـ g4f"""
history = []
-
- # بناء system prompt
full_system = system_prompt if system_prompt else ""
- # إضافة تعريف الأدوات إذا وجدت
+ # إضافة تعريف الأدوات
if tools:
tools_text = ToolsFormatter.format_tools_for_prompt(tools, tool_choice)
if full_system:
@@ -1072,7 +1329,7 @@ class MessageConverter:
else:
full_system = tools_text
- # حقن سياق المهمة المحفوظ (منع النسيان عند تبديل النماذج)
+ # حقن سياق المهمة مع التوجيه التنفيذي
if session_id:
context_summary = CONTEXT_STORE.get_context_summary(session_id)
if context_summary:
@@ -1081,10 +1338,10 @@ class MessageConverter:
else:
full_system = context_summary
logger.info(
- f"[Context] Injected persistent context for session {session_id[:8]}..."
+ f"[Context] Injected context+directive for session {session_id[:8]}..."
)
- # معالجة الرسائل وبناء التاريخ
+ # معالجة الرسائل
for msg in messages:
role = msg.get("role", "user")
content = msg.get("content", "")
@@ -1098,7 +1355,6 @@ class MessageConverter:
continue
converted_text = MessageConverter._convert_content(content, role)
-
if converted_text:
g4f_role = "user" if role == "user" else "assistant"
history.append({"role": g4f_role, "content": converted_text})
@@ -1110,7 +1366,7 @@ class MessageConverter:
else:
user_message = ""
- # بناء الرسالة الكاملة مع system prompt
+ # بناء الرسالة الكاملة
if full_system:
full_message = (
f"[System Instructions]\n{full_system}\n"
@@ -1123,7 +1379,6 @@ class MessageConverter:
@staticmethod
def _convert_content(content: Any, role: str) -> str:
- """تحويل محتوى رسالة واحدة مع الحفاظ على كل المعلومات"""
if isinstance(content, str):
return content
@@ -1134,10 +1389,8 @@ class MessageConverter:
parts.append(block)
elif isinstance(block, dict):
block_type = block.get("type", "")
-
if block_type == "text":
parts.append(block.get("text", ""))
-
elif block_type == "tool_use":
name = block.get("name", "unknown")
input_data = block.get("input", {})
@@ -1149,53 +1402,36 @@ class MessageConverter:
f'"arguments": {json.dumps(input_data, ensure_ascii=False)}}}'
f""
)
-
elif block_type == "tool_result":
tool_use_id = block.get("tool_use_id", "")
result_content = block.get("content", "")
is_error = block.get("is_error", False)
-
result_text = ToolsFormatter.format_tool_result_for_message(
tool_use_id, result_content
)
if is_error:
result_text = f"[ERROR] {result_text}"
parts.append(result_text)
-
elif block_type == "image":
parts.append("[Image content - not supported in text mode]")
-
else:
if "text" in block:
parts.append(block["text"])
else:
parts.append(json.dumps(block, ensure_ascii=False))
-
return "\n".join(parts)
if content is not None:
return str(content)
return ""
-
# =====================================================
# SMART HISTORY MANAGER
-# يدير التاريخ بذكاء للحفاظ على السياق الكامل
# =====================================================
class SmartHistoryManager:
- """
- مدير التاريخ الذكي - يحل مشكلة النسيان بشكل نهائي.
-
- الاستراتيجية:
- 1. يحتفظ دائماً بأول N رسالة (تحتوي المهمة الأصلية)
- 2. يحتفظ دائماً بآخر M رسالة (السياق الحالي)
- 3. يضغط الرسائل الوسطى بدلاً من حذفها
- 4. يستخرج ويحفظ نتائج الأدوات في PersistentContextStore
- """
-
- FIRST_MESSAGES_TO_KEEP = 5 # أول 5 رسائل (المهمة الأصلية)
- LAST_MESSAGES_TO_KEEP = 20 # آخر 20 رسالة (السياق الحالي)
- MAX_TOTAL_MESSAGES = 30 # الحد الأقصى الإجمالي
+ FIRST_MESSAGES_TO_KEEP = 5
+ LAST_MESSAGES_TO_KEEP = 20
+ MAX_TOTAL_MESSAGES = 30
@classmethod
def process_history(
@@ -1203,35 +1439,30 @@ class SmartHistoryManager:
history: List[Dict],
session_id: str = ""
) -> List[Dict]:
- """
- معالجة التاريخ بذكاء مع استخراج وحفظ المعلومات المهمة.
- """
if not history:
return []
- # استخراج نتائج الأدوات وحفظها في المخزن الدائم
if session_id:
cls._extract_and_store_tool_results(history, session_id)
total = len(history)
-
- # إذا كان التاريخ صغيراً، نحتفظ بكله
if total <= cls.MAX_TOTAL_MESSAGES:
- logger.info(f"[History] Full history: {total} messages (under limit)")
+ logger.info(f"[History] Full history: {total} messages")
return history
- # التاريخ كبير: نطبق الاستراتيجية الذكية
first_part = history[:cls.FIRST_MESSAGES_TO_KEEP]
last_part = history[-cls.LAST_MESSAGES_TO_KEEP:]
-
- # إنشاء ملخص للجزء الوسط
middle_part = history[cls.FIRST_MESSAGES_TO_KEEP:-cls.LAST_MESSAGES_TO_KEEP]
+
if middle_part:
summary = cls._summarize_middle(middle_part)
if summary:
summary_msg = {
"role": "assistant",
- "content": f"[Context Summary - {len(middle_part)} messages compressed]\n{summary}"
+ "content": (
+ f"[Context Summary - {len(middle_part)} messages compressed]\n"
+ f"{summary}"
+ )
}
smart_history = first_part + [summary_msg] + last_part
else:
@@ -1240,10 +1471,8 @@ class SmartHistoryManager:
smart_history = first_part + last_part
logger.info(
- f"[History] Smart compression: {total} → {len(smart_history)} messages "
- f"(kept first {cls.FIRST_MESSAGES_TO_KEEP} + last {cls.LAST_MESSAGES_TO_KEEP})"
+ f"[History] Smart compression: {total} → {len(smart_history)} messages"
)
-
return smart_history
@classmethod
@@ -1252,13 +1481,10 @@ class SmartHistoryManager:
history: List[Dict],
session_id: str
):
- """استخراج نتائج الأدوات من التاريخ وحفظها في المخزن الدائم"""
for msg in history:
content = msg.get("content", "")
if isinstance(content, str):
- # البحث عن نتائج الأدوات في النص
- if "[Tool Result" in content or "[SYSTEM]: Research results" in content:
- # استخراج اسم الأداة والنتيجة
+ if "[Tool Result" in content:
tool_match = re.search(
r'\[Tool Result - ID: ([^\]]+)\]\n(.*?)\n\[End Tool Result\]',
content,
@@ -1266,28 +1492,208 @@ class SmartHistoryManager:
)
if tool_match:
tool_id = tool_match.group(1)
- result = tool_match.group(2)[:500] # أول 500 حرف
+ result = tool_match.group(2)[:500]
CONTEXT_STORE.add_tool_result(
session_id, "tool", tool_id, result
)
@classmethod
def _summarize_middle(cls, messages: List[Dict]) -> str:
- """إنشاء ملخص نصي للرسائل الوسطى"""
if not messages:
return ""
-
summary_parts = []
for msg in messages:
role = msg.get("role", "")
- content = str(msg.get("content", ""))[:200] # أول 200 حرف
+ content = str(msg.get("content", ""))[:200]
summary_parts.append(f"[{role}]: {content}...")
+ return "\n".join(summary_parts[:10])
- return "\n".join(summary_parts[:10]) # أول 10 رسائل من الملخص
+# =====================================================
+# EXECUTION ENFORCER - القلب الحقيقي للحل
+# =====================================================
+class ExecutionEnforcer:
+ """
+ المحرك التنفيذي الإجباري.
+ يحل المشاكل الأربع:
+ 1. "المشاهد الذكي" → يكتشف الوصف ويُجبر على التنفيذ
+ 2. "ضياع الأوامر" → يُعيد الطلب بتنسيق صحيح ومضمون
+ 3. "غياب الدفعة الأخيرة" → يُرسل رسالة إجبارية للتنفيذ
+ 4. "تشتت الهوية" → يُعرّف النموذج كـ EXECUTOR لا DESCRIBER
+ """
+
+ MAX_EXECUTION_RETRIES = 2
+
+ @classmethod
+ def build_forced_execution_message(
+ cls,
+ tool_name: str,
+ tool_args: Dict,
+ reason: str = ""
+ ) -> str:
+ """
+ يبني رسالة تُجبر النموذج على تنفيذ أداة محددة.
+ هذا هو "الدفعة الأخيرة" المفقودة.
+ """
+ tool_call_json = json.dumps(
+ {"name": tool_name, "arguments": tool_args},
+ ensure_ascii=False,
+ indent=2
+ )
+
+ msg_parts = [
+ "EXECUTE THIS TOOL CALL NOW.",
+ "",
+ ]
+
+ if reason:
+ msg_parts.append(f"Reason: {reason}")
+ msg_parts.append("")
+
+ msg_parts.extend([
+ "Copy this EXACTLY:",
+ "",
+ "",
+ tool_call_json,
+ "",
+ "",
+ "No other text needed. Just the tool_call block."
+ ])
+
+ return "\n".join(msg_parts)
+
+ @classmethod
+ def pre_process_request(
+ cls,
+ message: str,
+ session_id: str,
+ tools: List[Dict]
+ ) -> str:
+ """
+ معالجة الطلب قبل إرساله.
+ يضيف توجيهات إجبارية إذا كانت هناك أداة معلّقة.
+ """
+ if not session_id or not tools:
+ return message
+
+ ctx = CONTEXT_STORE.get_or_create_session(session_id)
+ state = ctx.get("current_state", ExecutionState.IDLE)
+ pending = ctx.get("pending_tool_calls", [])
+ desc_count = ctx.get("consecutive_description_count", 0)
+
+ # إذا كانت هناك أداة معلّقة
+ if pending and state == ExecutionState.TOOL_PENDING:
+ pending_tool = pending[0]
+ tool_name = pending_tool.get("name", "unknown")
+ tool_args = pending_tool.get("arguments", {})
+
+ reminder = (
+ f"\n\n[EXECUTION REMINDER]: "
+ f"You must call `{tool_name}` using:\n"
+ f"\n"
+ f"{json.dumps({'name': tool_name, 'arguments': tool_args}, ensure_ascii=False, indent=2)}\n"
+ f""
+ )
+ return message + reminder
+
+ # إذا وصف بدون تنفيذ مرات متعددة
+ if desc_count >= 2:
+ warning = (
+ f"\n\n[SYSTEM WARNING - EXECUTION REQUIRED]: "
+ f"You have described actions {desc_count} times without executing them. "
+ f"Your response MUST contain a block. "
+ f"Descriptions are not acceptable."
+ )
+ return message + warning
+
+ return message
+
+ @classmethod
+ def post_process_response(
+ cls,
+ response: str,
+ session_id: str,
+ tools: List[Dict],
+ tool_calls_found: List[Dict]
+ ) -> Dict:
+ """
+ تحليل الرد بعد استلامه.
+ يقرر هل يحتاج "دفعة" أخرى.
+ """
+ if not session_id:
+ return {"needs_retry": False, "retry_message": "", "analysis": {}}
+
+ analysis = ResponseAnalyzer.analyze(response, tools)
+
+ # إذا وجد tool calls فعلية - نجاح
+ if tool_calls_found:
+ for tc in tool_calls_found:
+ CONTEXT_STORE.add_completed_step(
+ session_id,
+ f"Executed tool: {tc.get('name', 'unknown')} "
+ f"with args: {json.dumps(tc.get('input', {}))[:100]}"
+ )
+ # إعادة ضبط عداد الوصف
+ CONTEXT_STORE.update_session(
+ session_id,
+ described_but_not_executed=[],
+ consecutive_description_count=0,
+ current_state=ExecutionState.TOOL_EXECUTING
+ )
+ return {"needs_retry": False, "retry_message": "", "analysis": analysis}
+
+ # إذا وصف بدون تنفيذ
+ if analysis["has_description_without_execution"]:
+ desc_count = CONTEXT_STORE.increment_description_count(session_id, response)
+
+ if desc_count <= cls.MAX_EXECUTION_RETRIES:
+ intended_tool = analysis.get("intended_tool_name")
+
+ if intended_tool:
+ # بناء رسالة إجبارية للأداة المقصودة
+ tool_def = next(
+ (t for t in tools if t.get("name") == intended_tool),
+ None
+ )
+ example_args = {}
+ if tool_def:
+ schema = tool_def.get("input_schema", {})
+ for param_name in schema.get("required", []):
+ param_info = schema.get("properties", {}).get(param_name, {})
+ param_type = param_info.get("type", "string")
+ if param_type == "string":
+ example_args[param_name] = f"<{param_name}>"
+ elif param_type == "integer":
+ example_args[param_name] = 0
+ elif param_type == "boolean":
+ example_args[param_name] = True
+ else:
+ example_args[param_name] = None
+
+ retry_message = cls.build_forced_execution_message(
+ intended_tool,
+ example_args,
+ f"You mentioned using {intended_tool} but didn't call it"
+ )
+ else:
+ retry_message = (
+ "You described an action without executing it. "
+ "Use a block now:\n\n"
+ "\n"
+ '{"name": "TOOL_NAME", "arguments": {ARGUMENTS}}\n'
+ ""
+ )
+
+ return {
+ "needs_retry": True,
+ "retry_message": retry_message,
+ "analysis": analysis
+ }
+
+ return {"needs_retry": False, "retry_message": "", "analysis": analysis}
# =====================================================
-# CHAT LOGIC - الإصلاح الجوهري
+# CHAT LOGIC - مع محرك التنفيذ الكامل
# =====================================================
def ask(
message: str,
@@ -1299,21 +1705,18 @@ def ask(
session_id: str = ""
):
"""
- دالة الدردشة الرئيسية - مُصلحة بشكل جوهري.
-
- الإصلاحات:
- 1. لا STRICT MODE مفرط - فقط تعليمات واضحة ومعقولة
- 2. حفظ السياق في PersistentContextStore
- 3. SmartHistoryManager يدير التاريخ بذكاء
- 4. Fallback يبدأ من المزود المطلوب فعلاً
- 5. لا حقن هدف مفرط يربك النموذج
+ دالة الدردشة الرئيسية مع محرك التنفيذ الإجباري.
"""
message = (message or "").strip()
if not message:
yield ""
return
- # لا نستخدم الكاش عند وجود أدوات لأن الأدوات تتطلب استجابة جديدة دائماً
+ # تطبيق المعالجة المسبقة للتنفيذ
+ if tools and session_id:
+ message = ExecutionEnforcer.pre_process_request(message, session_id, tools)
+
+ # لا كاش عند وجود أدوات
if not tools:
key = f"{provider_name}|{model_name}|{message[:200]}"
cached = CACHE.get(key)
@@ -1323,13 +1726,12 @@ def ask(
else:
key = None
- # معالجة التاريخ بذكاء
+ # معالجة التاريخ
msgs = []
try:
if history:
if isinstance(history, list) and len(history) > 0:
if isinstance(history[0], dict):
- # استخدام SmartHistoryManager
smart_history = SmartHistoryManager.process_history(
history, session_id
)
@@ -1345,12 +1747,10 @@ def ask(
if text:
msgs.append({"role": str(role), "content": text})
else:
- # تنسيق tuples القديم
if len(history) > 30:
smart_history_tuples = history[:5] + history[-25:]
else:
smart_history_tuples = history
-
for item in smart_history_tuples:
if isinstance(item, (list, tuple)) and len(item) == 2:
if item[0]:
@@ -1358,27 +1758,23 @@ def ask(
if item[1]:
msgs.append({"role": "assistant", "content": str(item[1])})
except Exception as e:
- logger.warning(f"[History] Error processing history: {e}")
+ logger.warning(f"[History] Error: {e}")
- # إضافة الرسالة الحالية
msgs.append({"role": "user", "content": message})
# تحديث المخزن الدائم
if session_id:
ctx = CONTEXT_STORE.get_or_create_session(session_id)
- # حفظ المهمة الأصلية إذا لم تُحفظ بعد
if not ctx.get("original_task") and len(msgs) <= 2:
CONTEXT_STORE.update_session(
session_id,
original_task=message[:500],
- current_state="processing"
+ current_state=ExecutionState.PLANNING
)
ctx["message_count"] = ctx.get("message_count", 0) + 1
- # قائمة المزودين للـ fallback - يبدأ من المزود المطلوب
+ # قائمة المزودين
all_provider_names = list(REAL_PROVIDERS.keys())
-
- # ترتيب المزودين: المطلوب أولاً، ثم الباقين
if provider_name in all_provider_names:
fallback_providers = [provider_name] + [
p for p in all_provider_names if p != provider_name
@@ -1387,22 +1783,17 @@ def ask(
fallback_providers = all_provider_names
used = []
-
for pname in fallback_providers:
if pname in used:
continue
used.append(pname)
pobj = REAL_PROVIDERS.get(pname)
if not pobj:
- logger.info(f"[Fallback] Provider {pname} not available, skipping")
continue
-
models_list = discover_provider_models(pobj, pname)
if not models_list:
- logger.warning(f"[Fallback] No models for provider {pname}")
continue
- # ترتيب النماذج: المطلوب أولاً
if model_name in models_list:
model_candidates = [model_name] + [m for m in models_list if m != model_name]
else:
@@ -1410,20 +1801,17 @@ def ask(
for m in model_candidates[:5]:
try:
- logger.info(f"[Fallback] Trying provider {pname} with model {m}")
+ logger.info(f"[Fallback] Trying {pname}/{m}")
- # تسجيل المزود المستخدم في المخزن الدائم
if session_id:
- CONTEXT_STORE.get_or_create_session(session_id)
- provider_history = CONTEXT_STORE._store[session_id].get(
- "provider_history", []
- )
+ provider_history = CONTEXT_STORE._store.get(
+ session_id, {}
+ ).get("provider_history", [])
provider_history.append({
"provider": pname,
"model": m,
"timestamp": time.time()
})
- # الاحتفاظ بآخر 20 مزود فقط
CONTEXT_STORE._store[session_id]["provider_history"] = (
provider_history[-20:]
)
@@ -1435,6 +1823,7 @@ def ask(
stream=True,
timeout=60
)
+
buffer = []
got_response = False
for chunk in stream:
@@ -1449,80 +1838,64 @@ def ask(
full = "".join(buffer)
if full.strip() and got_response:
- # حفظ في الكاش فقط إذا لم تكن هناك أدوات
if key:
CACHE.set(key, full)
- # تحديث حالة الجلسة
if session_id:
CONTEXT_STORE.update_session(
session_id,
- current_state="completed_step"
+ last_model_response=full[:500],
+ current_state=ExecutionState.AWAITING_NEXT
)
return
except Exception as e:
logger.warning(
- f"[Fallback] Provider {pname} model {m} failed: {str(e)[:200]}"
+ f"[Fallback] {pname}/{m} failed: {str(e)[:200]}"
)
continue
yield "❌ فشلت جميع المزودات. تأكد من اتصال الإنترنت أو حاول لاحقاً."
-
# =====================================================
# SESSION ID EXTRACTOR
-# استخراج معرف الجلسة من الطلب
# =====================================================
def extract_session_id(request: Request, body: Dict) -> str:
- """
- استخراج أو توليد معرف جلسة فريد.
- يستخدم لربط الطلبات المتعددة بنفس المحادثة.
- """
- # محاولة استخراج من الـ headers
session_id = (
- request.headers.get("X-Session-ID", "") or
- request.headers.get("x-session-id", "") or
- request.headers.get("X-Conversation-ID", "") or
- ""
+ request.headers.get("X-Session-ID", "")
+ or request.headers.get("x-session-id", "")
+ or request.headers.get("X-Conversation-ID", "")
+ or ""
)
-
if session_id:
return session_id
- # محاولة استخراج من الـ metadata
metadata = body.get("metadata", {})
if isinstance(metadata, dict):
session_id = (
- metadata.get("session_id", "") or
- metadata.get("conversation_id", "") or
- metadata.get("user_id", "") or
- ""
+ metadata.get("session_id", "")
+ or metadata.get("conversation_id", "")
+ or metadata.get("user_id", "")
+ or ""
)
-
if session_id:
return session_id
- # توليد معرف من محتوى الرسائل (ثابت لنفس المحادثة)
messages = body.get("messages", [])
if messages:
- # استخدام أول رسالة لتوليد معرف ثابت
first_msg = messages[0]
content = str(first_msg.get("content", ""))[:100]
- # معرف شبه ثابت بناءً على المحتوى
import hashlib
hash_val = hashlib.md5(content.encode()).hexdigest()[:16]
return f"auto_{hash_val}"
- # توليد معرف عشوائي كحل أخير
return f"rand_{uuid.uuid4().hex[:16]}"
-
# =====================================================
# FASTAPI
# =====================================================
app = FastAPI(
- title="G4F Smart Router",
- description="AI Gateway - Protocol Translator for Anthropic (Fixed Version)"
+ title="G4F Execution Engine",
+ description="AI Gateway with Execution Enforcement"
)
app.add_middleware(
@@ -1535,7 +1908,6 @@ app.add_middleware(
API_KEY = os.getenv("API_KEY", "mysecretkey123")
-
class ChatRequest(BaseModel):
message: str
provider: str = "Blackbox"
@@ -1543,10 +1915,6 @@ class ChatRequest(BaseModel):
history: List[Any] = []
session_id: str = ""
-
-# =====================================================
-# التحقق من المفتاح
-# =====================================================
def verify_api_key(request: Request):
auth = request.headers.get("Authorization", "").strip()
x_key = request.headers.get("X-API-Key", "").strip()
@@ -1566,7 +1934,6 @@ def verify_api_key(request: Request):
detail="Invalid API key. Use 'Authorization: Bearer KEY' or 'X-API-Key: KEY'"
)
-
# =====================================================
# دعم HEAD
# =====================================================
@@ -1590,9 +1957,8 @@ async def head_messages():
async def head_chat_completions():
return Response(status_code=200)
-
# =====================================================
-# نقاط نهاية متوافقة مع Anthropic API
+# نقاط نهاية Anthropic API
# =====================================================
@app.get("/v1/models")
async def v1_models(request: Request):
@@ -1622,16 +1988,10 @@ async def v1_models(request: Request):
}]
return {"object": "list", "data": models}
-
-# =====================================================
-# نقطة /v1/messages - بروتوكول Anthropic الكامل
-# =====================================================
@app.post("/v1/messages")
async def v1_messages(request: Request):
verify_api_key(request)
body = await request.json()
-
- # استخراج معرف الجلسة
session_id = extract_session_id(request, body)
logger.info(
@@ -1647,7 +2007,6 @@ async def v1_messages(request: Request):
model = body.get("model", "gpt-4o")
system_prompt = body.get("system", "")
-
if isinstance(system_prompt, list):
sys_parts = []
for sp in system_prompt:
@@ -1663,21 +2022,14 @@ async def v1_messages(request: Request):
tool_choice = body.get("tool_choice", "auto")
metadata = body.get("metadata", {})
- # استخراج المهمة الأصلية وحفظها
+ # حفظ المهمة الأصلية
ctx = CONTEXT_STORE.get_or_create_session(session_id)
if not ctx.get("original_task"):
original_task = CONTEXT_STORE.extract_task_from_messages(messages)
if original_task:
- CONTEXT_STORE.update_session(
- session_id,
- original_task=original_task
- )
- logger.info(
- f"[Context] Stored original task for session {session_id[:8]}: "
- f"{original_task[:50]}..."
- )
+ CONTEXT_STORE.update_session(session_id, original_task=original_task)
- # تحويل الرسائل مع حقن السياق
+ # تحويل الرسائل
full_message, history = MessageConverter.convert_messages(
messages, system_prompt, tools, tool_choice, session_id
)
@@ -1690,11 +2042,11 @@ async def v1_messages(request: Request):
if is_stream:
return await _handle_anthropic_stream(
- full_message, history, model, max_tokens, tools, tool_choice,
- metadata, session_id
+ full_message, history, model, max_tokens,
+ tools, tool_choice, metadata, session_id
)
- # Non-stream: تجميع الرد كاملاً
+ # Non-stream
full_response = ""
for chunk in ask(
full_message, history, "Blackbox", model,
@@ -1704,17 +2056,40 @@ async def v1_messages(request: Request):
clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, tools)
+ # تطبيق محرك التنفيذ
+ if tools and session_id:
+ enforcement = ExecutionEnforcer.post_process_response(
+ full_response, session_id, tools, tool_calls
+ )
+ if enforcement["needs_retry"]:
+ logger.info(
+ f"[Enforcer] Retry needed: {enforcement['retry_message'][:100]}"
+ )
+ retry_response = ""
+ for chunk in ask(
+ enforcement["retry_message"], [], "Blackbox", model,
+ tools=tools, session_id=session_id
+ ):
+ retry_response += chunk
+
+ if retry_response:
+ clean_text2, tool_calls2 = ToolCallParser.parse_tool_calls(
+ retry_response, tools
+ )
+ if tool_calls2:
+ tool_calls = tool_calls2
+ clean_text = clean_text2
+ logger.info(
+ f"[Enforcer] Retry SUCCESS: {len(tool_calls2)} tool calls"
+ )
+
message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
input_tokens = max(1, len(full_message) // 4)
output_tokens = max(1, len(full_response) // 4)
content_blocks = []
-
if clean_text.strip():
- content_blocks.append({
- "type": "text",
- "text": clean_text.strip()
- })
+ content_blocks.append({"type": "text", "text": clean_text.strip()})
for tc in tool_calls:
tool_id = tc.get("id") or ToolCallParser.generate_tool_id()
@@ -1724,10 +2099,7 @@ async def v1_messages(request: Request):
"name": tc["name"],
"input": tc.get("input", {})
})
- logger.info(
- f"[API] Tool call in response: {tc['name']} (id: {tool_id})"
- )
- # حفظ خطوة الأداة في المخزن الدائم
+ logger.info(f"[API] Tool call: {tc['name']} (id: {tool_id})")
if session_id:
CONTEXT_STORE.add_completed_step(
session_id,
@@ -1735,10 +2107,7 @@ async def v1_messages(request: Request):
)
if not content_blocks:
- content_blocks.append({
- "type": "text",
- "text": full_response or ""
- })
+ content_blocks.append({"type": "text", "text": full_response or ""})
stop_reason = "tool_use" if tool_calls else "end_turn"
@@ -1756,9 +2125,8 @@ async def v1_messages(request: Request):
}
}
-
# =====================================================
-# معالج الـ streaming بصيغة Anthropic SSE
+# معالج الـ streaming
# =====================================================
async def _handle_anthropic_stream(
full_message: str,
@@ -1796,12 +2164,12 @@ async def _handle_anthropic_stream(
)
tool_buffer = StreamToolBuffer(available_tools=tools_list)
-
current_block_index = 0
text_block_open = False
output_tokens = 0
has_tool_calls = False
detected_tool_calls = []
+ full_response_buffer = []
def make_text_block_start(index: int) -> str:
block_start = {
@@ -1826,25 +2194,19 @@ async def _handle_anthropic_stream(
)
def make_block_stop(index: int) -> str:
- block_stop = {
- "type": "content_block_stop",
- "index": index
- }
+ block_stop = {"type": "content_block_stop", "index": index}
return (
f"event: content_block_stop\n"
f"data: {json.dumps(block_stop, ensure_ascii=False)}\n\n"
)
def make_tool_use_events(index: int, tool_call: Dict) -> str:
- """إنشاء أحداث tool_use للـ stream."""
events_str = ""
tool_id = tool_call.get("id") or ToolCallParser.generate_tool_id()
tool_name = tool_call.get("name", "unknown")
tool_input = tool_call.get("input", {})
- logger.info(
- f"[Stream] Sending tool_use: {tool_name} (id: {tool_id})"
- )
+ logger.info(f"[Stream] Sending tool_use: {tool_name} (id: {tool_id})")
block_start = {
"type": "content_block_start",
@@ -1881,7 +2243,7 @@ async def _handle_anthropic_stream(
events_str += make_block_stop(index)
return events_str
- # ===== معالجة الـ Stream =====
+ # معالجة الـ Stream
try:
for chunk in ask(
full_message, history, "Blackbox", model,
@@ -1890,19 +2252,17 @@ async def _handle_anthropic_stream(
if not chunk:
continue
+ full_response_buffer.append(chunk)
output_tokens += max(1, len(chunk) // 4)
if not tools_list:
- # بدون أدوات: إرسال مباشر
if not text_block_open:
yield make_text_block_start(current_block_index)
text_block_open = True
yield make_text_delta(current_block_index, chunk)
continue
- # مع أدوات: معالجة عبر الـ buffer
events = tool_buffer.feed(chunk)
-
for event in events:
if event.get("type") == "text":
text = event["text"]
@@ -1911,22 +2271,19 @@ async def _handle_anthropic_stream(
yield make_text_block_start(current_block_index)
text_block_open = True
yield make_text_delta(current_block_index, text)
-
elif event.get("type") == "tool_use":
if text_block_open:
yield make_block_stop(current_block_index)
current_block_index += 1
text_block_open = False
-
has_tool_calls = True
detected_tool_calls.append(event)
yield make_tool_use_events(current_block_index, event)
current_block_index += 1
- # ===== تفريغ الـ buffer المتبقي =====
- logger.info("[Stream] Flushing remaining buffer...")
+ # تفريغ الـ buffer
+ logger.info("[Stream] Flushing buffer...")
remaining_events = tool_buffer.flush()
-
for event in remaining_events:
if event.get("type") == "text":
text = event["text"]
@@ -1935,22 +2292,75 @@ async def _handle_anthropic_stream(
yield make_text_block_start(current_block_index)
text_block_open = True
yield make_text_delta(current_block_index, text)
-
elif event.get("type") == "tool_use":
if text_block_open:
yield make_block_stop(current_block_index)
current_block_index += 1
text_block_open = False
-
has_tool_calls = True
detected_tool_calls.append(event)
logger.info(
- f"[Stream] Recovered tool call from flush: "
- f"{event.get('name')}"
+ f"[Stream] Recovered tool call: {event.get('name')}"
)
yield make_tool_use_events(current_block_index, event)
current_block_index += 1
+ # ===== تطبيق محرك التنفيذ على الـ stream =====
+ if tools_list and session_id and not has_tool_calls:
+ full_response = "".join(full_response_buffer)
+ enforcement = ExecutionEnforcer.post_process_response(
+ full_response, session_id, tools_list, detected_tool_calls
+ )
+
+ if enforcement["needs_retry"]:
+ logger.info(
+ f"[Stream Enforcer] Sending execution push..."
+ )
+ retry_tool_buffer = StreamToolBuffer(available_tools=tools_list)
+
+ for retry_chunk in ask(
+ enforcement["retry_message"], [], "Blackbox", model,
+ tools=tools_list, session_id=session_id
+ ):
+ if not retry_chunk:
+ continue
+
+ retry_events = retry_tool_buffer.feed(retry_chunk)
+ for event in retry_events:
+ if event.get("type") == "text":
+ text = event["text"]
+ if text:
+ if not text_block_open:
+ yield make_text_block_start(current_block_index)
+ text_block_open = True
+ yield make_text_delta(current_block_index, text)
+ elif event.get("type") == "tool_use":
+ if text_block_open:
+ yield make_block_stop(current_block_index)
+ current_block_index += 1
+ text_block_open = False
+ has_tool_calls = True
+ detected_tool_calls.append(event)
+ logger.info(
+ f"[Stream Enforcer] Tool call in retry: "
+ f"{event.get('name')}"
+ )
+ yield make_tool_use_events(current_block_index, event)
+ current_block_index += 1
+
+ # تفريغ retry buffer
+ retry_remaining = retry_tool_buffer.flush()
+ for event in retry_remaining:
+ if event.get("type") == "tool_use":
+ if text_block_open:
+ yield make_block_stop(current_block_index)
+ current_block_index += 1
+ text_block_open = False
+ has_tool_calls = True
+ detected_tool_calls.append(event)
+ yield make_tool_use_events(current_block_index, event)
+ current_block_index += 1
+
except Exception as e:
logger.error(f"[Stream] Error: {str(e)}")
if not text_block_open:
@@ -1966,7 +2376,7 @@ async def _handle_anthropic_stream(
yield make_text_delta(0, "")
yield make_block_stop(0)
- # حفظ الأدوات المكتشفة في المخزن الدائم
+ # حفظ الأدوات المكتشفة
if session_id and detected_tool_calls:
for tc in detected_tool_calls:
CONTEXT_STORE.add_completed_step(
@@ -1976,7 +2386,6 @@ async def _handle_anthropic_stream(
)
stop_reason = "tool_use" if has_tool_calls else "end_turn"
-
msg_delta = {
"type": "message_delta",
"delta": {
@@ -1989,7 +2398,6 @@ async def _handle_anthropic_stream(
f"event: message_delta\n"
f"data: {json.dumps(msg_delta, ensure_ascii=False)}\n\n"
)
-
yield 'event: message_stop\ndata: {"type": "message_stop"}\n\n'
return StreamingResponse(
@@ -2002,7 +2410,6 @@ async def _handle_anthropic_stream(
}
)
-
# =====================================================
# نقطة /v1/messages/stream
# =====================================================
@@ -2010,7 +2417,6 @@ async def _handle_anthropic_stream(
async def v1_messages_stream(request: Request):
verify_api_key(request)
body = await request.json()
-
session_id = extract_session_id(request, body)
messages = body.get("messages", [])
@@ -2037,19 +2443,17 @@ async def v1_messages_stream(request: Request):
)
return await _handle_anthropic_stream(
- full_message, history, model, max_tokens, tools, tool_choice,
- {}, session_id
+ full_message, history, model, max_tokens,
+ tools, tool_choice, {}, session_id
)
-
# =====================================================
-# نقطة /v1/chat/completions (صيغة OpenAI)
+# نقطة /v1/chat/completions (OpenAI)
# =====================================================
@app.post("/v1/chat/completions")
async def v1_chat_completions(request: Request):
verify_api_key(request)
body = await request.json()
-
session_id = extract_session_id(request, body)
messages = body.get("messages", [])
@@ -2069,15 +2473,14 @@ async def v1_chat_completions(request: Request):
if role and content:
history.append({"role": role, "content": content})
- # تطبيق SmartHistoryManager
history = SmartHistoryManager.process_history(history, session_id)
-
completion_id = f"chatcmpl-{int(time.time())}_{os.urandom(4).hex()}"
if is_stream:
async def openai_stream():
for chunk in ask(
- user_message, history, "Blackbox", model, session_id=session_id
+ user_message, history, "Blackbox", model,
+ session_id=session_id
):
if chunk:
data = {
@@ -2119,7 +2522,8 @@ async def v1_chat_completions(request: Request):
full_response = ""
for chunk in ask(
- user_message, history, "Blackbox", model, session_id=session_id
+ user_message, history, "Blackbox", model,
+ session_id=session_id
):
full_response += chunk
@@ -2143,98 +2547,80 @@ async def v1_chat_completions(request: Request):
}
}
-
# =====================================================
# نقاط نهاية إضافية
# =====================================================
@app.get("/")
async def root():
return {
- "message": "G4F Smart Router - Fixed Version",
- "version": "4.0.0",
- "fixes_applied": [
- "Removed STRICT MODE (was causing model to ignore all commands)",
- "Added PersistentContextStore (prevents forgetting between model switches)",
- "Added SmartHistoryManager (intelligent history compression)",
- "Fixed Fallback order (starts from requested provider, not always Blackbox)",
- "Balanced tool instructions (clear but not aggressive)",
- "Added session_id support for context persistence",
- "Removed excessive goal injection (was confusing the model)",
- "Stream tag auto-close preserved from v3"
- ],
- "providers": list(REAL_PROVIDERS.keys()),
- "endpoints": {
- "GET /": "Home",
- "GET /health": "Health check",
- "GET /v1/models": "List models",
- "POST /v1/messages": "Anthropic format (AUTH)",
- "POST /v1/messages/stream": "Anthropic stream (AUTH)",
- "POST /v1/chat/completions": "OpenAI format (AUTH)",
- "POST /chat": "Simple chat (AUTH)",
- "POST /chat/stream": "Simple stream (AUTH)",
- "GET /providers": "Providers list (AUTH)",
- "GET /context/{session_id}": "View session context (AUTH)",
- "DELETE /context/{session_id}": "Clear session context (AUTH)",
+ "message": "G4F Execution Engine",
+ "version": "6.0.0",
+ "problems_solved": {
+ "1_smart_watcher_syndrome": (
+ "ResponseAnalyzer detects 'I will...' patterns "
+ "and flags them for immediate retry"
+ ),
+ "2_lost_in_translation": (
+ "ToolCallParser._clean_json_string() fixes common JSON errors "
+ "before parsing - tolerant of extra spaces and characters"
+ ),
+ "3_missing_final_push": (
+ "ExecutionEnforcer.build_forced_execution_message() "
+ "sends a mandatory execution message when description detected"
+ ),
+ "4_identity_confusion": (
+ "ToolsFormatter defines the model as EXECUTOR not DESCRIBER "
+ "with clear forbidden patterns list"
+ ),
},
+ "providers": list(REAL_PROVIDERS.keys()),
"cookies": COOKIE_STATUS,
- "status": "✅ Server is working"
+ "status": "✅ Execution Engine v6 Active"
}
-
@app.get("/health")
async def health():
return {
"status": "ok",
- "version": "4.0.0",
+ "version": "6.0.0",
"cookies": COOKIE_STATUS,
"providers": list(REAL_PROVIDERS.keys()),
"provider_count": len(REAL_PROVIDERS),
"context_store": {
"active_sessions": len(CONTEXT_STORE._store),
},
- "fixes": {
- "persistent_context_store": True,
- "smart_history_manager": True,
- "balanced_tool_instructions": True,
- "correct_fallback_order": True,
- "stream_tag_auto_close": True,
- "removed_strict_mode": True,
+ "execution_engine": {
+ "response_analyzer": True,
+ "execution_enforcer": True,
+ "stream_retry": True,
+ "json_cleaner": True,
+ "description_patterns": len(ResponseAnalyzer.DESCRIPTION_PHRASES),
},
"timestamp": int(time.time())
}
-
@app.get("/providers")
async def get_providers(request: Request):
verify_api_key(request)
result = {}
for pname, pobj in REAL_PROVIDERS.items():
models = discover_provider_models(pobj, pname)
- result[pname] = {
- "available": True,
- "models": models
- }
+ result[pname] = {"available": True, "models": models}
return {"providers": result}
-
-# =====================================================
-# نقطة عرض سياق الجلسة
-# =====================================================
@app.get("/context/{session_id}")
async def get_session_context(session_id: str, request: Request):
- """عرض سياق جلسة محددة - مفيد للتشخيص"""
verify_api_key(request)
ctx = CONTEXT_STORE.get_or_create_session(session_id)
return {
"session_id": session_id,
"context": ctx,
- "summary": CONTEXT_STORE.get_context_summary(session_id)
+ "summary": CONTEXT_STORE.get_context_summary(session_id),
+ "execution_directive": CONTEXT_STORE.get_execution_directive(session_id)
}
-
@app.delete("/context/{session_id}")
async def clear_session_context(session_id: str, request: Request):
- """مسح سياق جلسة محددة"""
verify_api_key(request)
with CONTEXT_STORE._lock:
if session_id in CONTEXT_STORE._store:
@@ -2242,10 +2628,8 @@ async def clear_session_context(session_id: str, request: Request):
return {"message": f"Session {session_id} cleared"}
return {"message": f"Session {session_id} not found"}
-
@app.get("/context")
async def list_sessions(request: Request):
- """عرض قائمة الجلسات النشطة"""
verify_api_key(request)
sessions = []
with CONTEXT_STORE._lock:
@@ -2254,18 +2638,115 @@ async def list_sessions(request: Request):
"session_id": sid,
"message_count": ctx.get("message_count", 0),
"original_task_preview": ctx.get("original_task", "")[:100],
- "current_state": ctx.get("current_state", "idle"),
+ "current_state": ctx.get("current_state", ExecutionState.IDLE),
"completed_steps": len(ctx.get("completed_steps", [])),
"tool_results": len(ctx.get("tool_results", [])),
+ "pending_tools": len(ctx.get("pending_tool_calls", [])),
+ "executed_tools": len(ctx.get("executed_tool_calls", [])),
+ "description_count": ctx.get("consecutive_description_count", 0),
"last_updated": ctx.get("last_updated", 0),
- "provider_count": len(ctx.get("provider_history", [])),
})
return {"sessions": sessions, "total": len(sessions)}
+@app.post("/debug/test-execution-enforcer")
+async def debug_test_execution_enforcer(request: Request):
+ verify_api_key(request)
+
+ test_cases = [
+ {
+ "name": "Arabic description without execution",
+ "response": "سأقوم الآن بإنشاء الملف باستخدام أداة write_file.",
+ "expected_needs_retry": True
+ },
+ {
+ "name": "English description without execution",
+ "response": "I will now create the file using the write_file tool.",
+ "expected_needs_retry": True
+ },
+ {
+ "name": "Actual tool call",
+ "response": (
+ "Creating the file now.\n"
+ "\n"
+ '{"name": "write_file", "arguments": {"path": "test.py", "content": "print(1)"}}\n'
+ ""
+ ),
+ "expected_needs_retry": False
+ },
+ {
+ "name": "Let me create pattern",
+ "response": "Let me create a Python file with the requested functionality.",
+ "expected_needs_retry": True
+ },
+ {
+ "name": "Regular response (no tools needed)",
+ "response": "The answer to your question is 42.",
+ "expected_needs_retry": False
+ },
+ {
+ "name": "I'll use the tool pattern",
+ "response": "I'll use the write_file tool to save your code.",
+ "expected_needs_retry": True
+ },
+ ]
+
+ test_tools = [{
+ "name": "write_file",
+ "description": "Write content to a file",
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "File path"},
+ "content": {"type": "string", "description": "File content"}
+ },
+ "required": ["path", "content"]
+ }
+ }]
+
+ results = []
+ for tc in test_cases:
+ analysis = ResponseAnalyzer.analyze(tc["response"], test_tools)
+ test_session = f"test_{uuid.uuid4().hex[:8]}"
+ enforcement = ExecutionEnforcer.post_process_response(
+ tc["response"], test_session, test_tools, []
+ )
+ # تنظيف الجلسة التجريبية
+ with CONTEXT_STORE._lock:
+ if test_session in CONTEXT_STORE._store:
+ del CONTEXT_STORE._store[test_session]
+
+ results.append({
+ "test_name": tc["name"],
+ "response_preview": tc["response"][:100],
+ "expected_needs_retry": tc["expected_needs_retry"],
+ "actual_needs_retry": enforcement["needs_retry"],
+ "passed": enforcement["needs_retry"] == tc["expected_needs_retry"],
+ "analysis": {
+ "has_actual_tool_call": analysis["has_actual_tool_call"],
+ "has_description_without_execution": (
+ analysis["has_description_without_execution"]
+ ),
+ "intended_tool": analysis.get("intended_tool_name"),
+ },
+ "retry_message_preview": (
+ enforcement["retry_message"][:150]
+ if enforcement["retry_message"]
+ else ""
+ )
+ })
+
+ passed = sum(1 for r in results if r["passed"])
+ total = len(results)
+ return {
+ "test_results": results,
+ "summary": {
+ "passed": passed,
+ "failed": total - passed,
+ "total": total,
+ "success_rate": f"{passed/total*100:.1f}%"
+ }
+ }
-# =====================================================
-# نقطة تشخيص: اختبار تحليل tool calls
-# =====================================================
@app.get("/debug/test-tool-parse")
async def debug_test_tool_parse(request: Request):
verify_api_key(request)
@@ -2276,15 +2757,15 @@ async def debug_test_tool_parse(request: Request):
"input": (
'I will create the file now.\n'
'{"name": "write_file", '
- '"arguments": {"path": "test.py", "content": "print(\'hello\')"}}'
+ '"arguments": {"path": "test.py", "content": "print(\'hello\')"}}'
''
),
},
{
- "name": "Tool call with leading whitespace/newlines",
+ "name": "Tool call with whitespace",
"input": (
'Let me help you.\n\n\n'
- ' \n'
+ ' \n'
'{"name": "read_file", "arguments": {"path": "config.json"}}\n'
''
),
@@ -2299,12 +2780,11 @@ async def debug_test_tool_parse(request: Request):
),
},
{
- "name": "Function tag format",
+ "name": "JSON with trailing comma (common error)",
"input": (
- 'Creating file:\n'
- ''
- '{"path": "app.js", "content": "console.log(1)"}'
- ''
+ ''
+ '{"name": "write_file", "arguments": {"path": "test.py", "content": "data",}}'
+ ''
),
},
{
@@ -2342,15 +2822,10 @@ async def debug_test_tool_parse(request: Request):
return {"test_results": results}
-
-# =====================================================
-# نقطة تشخيص: اختبار stream buffer
-# =====================================================
@app.post("/debug/test-stream-buffer")
async def debug_test_stream_buffer(request: Request):
verify_api_key(request)
body = await request.json()
-
text = body.get("text", "")
tools = body.get("tools", [])
chunk_size = body.get("chunk_size", 10)
@@ -2376,58 +2851,15 @@ async def debug_test_stream_buffer(request: Request):
"events": all_events
}
-
# =====================================================
-# نقطة تشخيص: اختبار PersistentContextStore
+# نقطة الدردشة البسيطة
# =====================================================
-@app.post("/debug/test-context-store")
-async def debug_test_context_store(request: Request):
- verify_api_key(request)
- body = await request.json()
-
- test_session_id = f"test_{uuid.uuid4().hex[:8]}"
-
- # اختبار إنشاء جلسة
- ctx = CONTEXT_STORE.get_or_create_session(test_session_id)
- CONTEXT_STORE.update_session(
- test_session_id,
- original_task="Create a Python web scraper",
- current_state="processing"
- )
-
- # اختبار إضافة خطوات
- CONTEXT_STORE.add_completed_step(test_session_id, "Searched for Python scraping libraries")
- CONTEXT_STORE.add_completed_step(test_session_id, "Found BeautifulSoup and requests")
-
- # اختبار إضافة نتائج أدوات
- CONTEXT_STORE.add_tool_result(
- test_session_id,
- "web_search",
- "toolu_test123",
- "BeautifulSoup4, requests, scrapy are popular Python scraping libraries"
- )
-
- # الحصول على ملخص السياق
- summary = CONTEXT_STORE.get_context_summary(test_session_id)
-
- # تنظيف الجلسة التجريبية
- with CONTEXT_STORE._lock:
- if test_session_id in CONTEXT_STORE._store:
- del CONTEXT_STORE._store[test_session_id]
-
- return {
- "test_session_id": test_session_id,
- "context_summary": summary,
- "test_passed": len(summary) > 0,
- "summary_length": len(summary)
- }
-
-
@app.post("/chat")
-async def chat(request: Request, chat_req: ChatRequest):
+async def simple_chat(chat_req: ChatRequest, request: Request):
verify_api_key(request)
- session_id = chat_req.session_id or f"chat_{uuid.uuid4().hex[:16]}"
- result = ""
+ session_id = chat_req.session_id or extract_session_id(request, {})
+
+ full_response = ""
for chunk in ask(
chat_req.message,
chat_req.history,
@@ -2435,14 +2867,19 @@ async def chat(request: Request, chat_req: ChatRequest):
chat_req.model,
session_id=session_id
):
- result += chunk
- return JSONResponse({"response": result, "session_id": session_id})
+ full_response += chunk
+ return {
+ "response": full_response,
+ "session_id": session_id,
+ "provider": chat_req.provider,
+ "model": chat_req.model
+ }
@app.post("/chat/stream")
-async def chat_stream(request: Request, chat_req: ChatRequest):
+async def simple_chat_stream(chat_req: ChatRequest, request: Request):
verify_api_key(request)
- session_id = chat_req.session_id or f"chat_{uuid.uuid4().hex[:16]}"
+ session_id = chat_req.session_id or extract_session_id(request, {})
async def generate():
for chunk in ask(
@@ -2452,7 +2889,8 @@ async def chat_stream(request: Request, chat_req: ChatRequest):
chat_req.model,
session_id=session_id
):
- yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
+ if chunk:
+ yield f"data: {json.dumps({'text': chunk}, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
@@ -2461,124 +2899,6 @@ async def chat_stream(request: Request, chat_req: ChatRequest):
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
+ "X-Accel-Buffering": "no",
}
- )
-
-
-# =====================================================
-# معالجة الأخطاء العامة
-# =====================================================
-@app.exception_handler(404)
-async def not_found_handler(request: Request, exc: HTTPException):
- return JSONResponse(
- status_code=404,
- content={
- "type": "error",
- "error": {
- "type": "not_found_error",
- "message": (
- f"Endpoint {request.method} {request.url.path} not found"
- )
- }
- }
- )
-
-
-@app.exception_handler(500)
-async def internal_error_handler(request: Request, exc: Exception):
- logger.error(f"Internal error: {str(exc)}")
- return JSONResponse(
- status_code=500,
- content={
- "type": "error",
- "error": {
- "type": "api_error",
- "message": "Internal server error"
- }
- }
- )
-
-
-@app.exception_handler(401)
-async def auth_error_handler(request: Request, exc: HTTPException):
- return JSONResponse(
- status_code=401,
- content={
- "type": "error",
- "error": {
- "type": "authentication_error",
- "message": (
- exc.detail if hasattr(exc, 'detail') else "Invalid API key"
- )
- }
- }
- )
-
-
-@app.exception_handler(400)
-async def bad_request_handler(request: Request, exc: HTTPException):
- return JSONResponse(
- status_code=400,
- content={
- "type": "error",
- "error": {
- "type": "invalid_request_error",
- "message": (
- exc.detail if hasattr(exc, 'detail') else "Bad request"
- )
- }
- }
- )
-
-
-@app.exception_handler(429)
-async def rate_limit_handler(request: Request, exc: HTTPException):
- return JSONResponse(
- status_code=429,
- content={
- "type": "error",
- "error": {
- "type": "rate_limit_error",
- "message": "Rate limit exceeded. Please retry after a brief wait."
- }
- }
- )
-
-
-# =====================================================
-# مهمة تنظيف دورية للجلسات القديمة
-# =====================================================
-@app.on_event("startup")
-async def startup_event():
- """تشغيل مهام عند بدء الخادم"""
- logger.info("Starting G4F Smart Router v4.0 (Fixed Version)")
- logger.info(f"Cookies: {COOKIE_STATUS}")
- logger.info(f"Providers: {list(REAL_PROVIDERS.keys())}")
- logger.info("Key Fixes:")
- logger.info(" ✅ Removed STRICT MODE (was breaking model responses)")
- logger.info(" ✅ Added PersistentContextStore (prevents forgetting)")
- logger.info(" ✅ Added SmartHistoryManager (intelligent compression)")
- logger.info(" ✅ Fixed Fallback order (uses requested provider first)")
- logger.info(" ✅ Balanced tool instructions (clear, not aggressive)")
- logger.info(" ✅ Added session tracking across model switches")
-
- # بدء مهمة التنظيف الدورية
- asyncio.create_task(periodic_cleanup())
-
-
-async def periodic_cleanup():
- """تنظيف دوري للجلسات القديمة كل ساعة"""
- while True:
- await asyncio.sleep(3600) # كل ساعة
- CONTEXT_STORE.cleanup_old_sessions(max_age_hours=24)
- logger.info("[Cleanup] Periodic cleanup completed")
-
-
-# =====================================================
-# التشغيل
-# =====================================================
-if __name__ == "__main__":
- import uvicorn
- port = int(os.getenv("PORT", 7860))
- logger.info(f"Starting G4F Smart Router v4.0 on port {port}")
- uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
+ )
\ No newline at end of file