bahi-bh commited on
Commit
abab9cb
·
verified ·
1 Parent(s): 321065a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +941 -0
app.py CHANGED
@@ -2901,4 +2901,945 @@ async def simple_chat_stream(chat_req: ChatRequest, request: Request):
2901
  "Connection": "keep-alive",
2902
  "X-Accel-Buffering": "no",
2903
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2904
  )
 
2901
  "Connection": "keep-alive",
2902
  "X-Accel-Buffering": "no",
2903
  }
2904
+ )
2905
+ # =====================================================
2906
+ # BACKGROUND CLEANUP TASK
2907
+ # =====================================================
2908
+ async def cleanup_task():
2909
+ """مهمة تنظيف دورية للجلسات القديمة"""
2910
+ while True:
2911
+ try:
2912
+ await asyncio.sleep(3600) # كل ساعة
2913
+ CONTEXT_STORE.cleanup_old_sessions(max_age_hours=24)
2914
+ logger.info("[Cleanup] Old sessions cleaned")
2915
+ except Exception as e:
2916
+ logger.error(f"[Cleanup] Error: {e}")
2917
+
2918
+ @app.on_event("startup")
2919
+ async def startup_event():
2920
+ """تشغيل المهام عند بدء التطبيق"""
2921
+ asyncio.create_task(cleanup_task())
2922
+ logger.info("=" * 60)
2923
+ logger.info("🚀 G4F Execution Engine v6.0.0 Started")
2924
+ logger.info(f"🍪 Cookies: {COOKIE_STATUS}")
2925
+ logger.info(f"🔌 Providers: {list(REAL_PROVIDERS.keys())}")
2926
+ logger.info("🔧 Execution Engine: ACTIVE")
2927
+ logger.info("✅ Problems Fixed:")
2928
+ logger.info(" 1. Smart Watcher Syndrome → ResponseAnalyzer")
2929
+ logger.info(" 2. Lost in Translation → JSON Cleaner")
2930
+ logger.info(" 3. Missing Final Push → ExecutionEnforcer")
2931
+ logger.info(" 4. Identity Confusion → EXECUTOR mode")
2932
+ logger.info("=" * 60)
2933
+
2934
+ @app.on_event("shutdown")
2935
+ async def shutdown_event():
2936
+ """تنظيف عند إغلاق التطبيق"""
2937
+ logger.info("[Shutdown] G4F Execution Engine stopping...")
2938
+ active_sessions = len(CONTEXT_STORE._store)
2939
+ logger.info(f"[Shutdown] Active sessions at shutdown: {active_sessions}")
2940
+
2941
+ # =====================================================
2942
+ # نقطة تشخيص: عرض حالة النظام الكاملة
2943
+ # =====================================================
2944
+ @app.get("/debug/system-status")
2945
+ async def debug_system_status(request: Request):
2946
+ """عرض حالة النظام الكاملة للتشخيص"""
2947
+ verify_api_key(request)
2948
+
2949
+ # إحصائيات الجلسات
2950
+ session_stats = {
2951
+ "total_sessions": 0,
2952
+ "sessions_with_pending_tools": 0,
2953
+ "sessions_with_description_failures": 0,
2954
+ "sessions_completed": 0,
2955
+ "total_tool_executions": 0,
2956
+ "total_description_failures": 0,
2957
+ }
2958
+
2959
+ with CONTEXT_STORE._lock:
2960
+ session_stats["total_sessions"] = len(CONTEXT_STORE._store)
2961
+ for sid, ctx in CONTEXT_STORE._store.items():
2962
+ pending = len(ctx.get("pending_tool_calls", []))
2963
+ desc_failures = ctx.get("consecutive_description_count", 0)
2964
+ executed = len(ctx.get("executed_tool_calls", []))
2965
+ state = ctx.get("current_state", "")
2966
+
2967
+ if pending > 0:
2968
+ session_stats["sessions_with_pending_tools"] += 1
2969
+ if desc_failures > 0:
2970
+ session_stats["sessions_with_description_failures"] += 1
2971
+ if state == ExecutionState.COMPLETED:
2972
+ session_stats["sessions_completed"] += 1
2973
+
2974
+ session_stats["total_tool_executions"] += executed
2975
+ session_stats["total_description_failures"] += desc_failures
2976
+
2977
+ # إحصائيات المزودين
2978
+ provider_stats = {}
2979
+ for pname, pobj in REAL_PROVIDERS.items():
2980
+ models = discover_provider_models(pobj, pname)
2981
+ provider_stats[pname] = {
2982
+ "available": True,
2983
+ "model_count": len(models),
2984
+ "models": models[:3], # أول 3 نماذج فقط
2985
+ }
2986
+
2987
+ # إحصائيات محرك التنفيذ
2988
+ engine_stats = {
2989
+ "description_patterns_count": len(ResponseAnalyzer.DESCRIPTION_PHRASES),
2990
+ "tool_call_start_markers": len(ToolCallParser.START_MARKERS),
2991
+ "max_execution_retries": ExecutionEnforcer.MAX_EXECUTION_RETRIES,
2992
+ "cache_size": len(CACHE.cache),
2993
+ "cache_max_size": CACHE.max_size,
2994
+ "cache_ttl_seconds": CACHE.ttl,
2995
+ }
2996
+
2997
+ return {
2998
+ "status": "operational",
2999
+ "version": "6.0.0",
3000
+ "uptime_info": {
3001
+ "cookie_status": COOKIE_STATUS,
3002
+ "providers_loaded": len(REAL_PROVIDERS),
3003
+ },
3004
+ "session_statistics": session_stats,
3005
+ "provider_statistics": provider_stats,
3006
+ "execution_engine_statistics": engine_stats,
3007
+ "execution_states": {
3008
+ "IDLE": ExecutionState.IDLE,
3009
+ "PLANNING": ExecutionState.PLANNING,
3010
+ "TOOL_PENDING": ExecutionState.TOOL_PENDING,
3011
+ "TOOL_EXECUTING": ExecutionState.TOOL_EXECUTING,
3012
+ "TOOL_DONE": ExecutionState.TOOL_DONE,
3013
+ "AWAITING_NEXT": ExecutionState.AWAITING_NEXT,
3014
+ "COMPLETED": ExecutionState.COMPLETED,
3015
+ },
3016
+ "timestamp": int(time.time())
3017
+ }
3018
+
3019
+ # =====================================================
3020
+ # نقطة تشخيص: اختبار مزود محدد
3021
+ # =====================================================
3022
+ @app.post("/debug/test-provider")
3023
+ async def debug_test_provider(request: Request):
3024
+ """اختبار مزود محدد بشكل مباشر"""
3025
+ verify_api_key(request)
3026
+ body = await request.json()
3027
+ provider_name = body.get("provider", "Blackbox")
3028
+ model_name = body.get("model", "gpt-4o")
3029
+ test_message = body.get("message", "Say 'hello' in one word.")
3030
+ timeout_seconds = body.get("timeout", 30)
3031
+
3032
+ pobj = REAL_PROVIDERS.get(provider_name)
3033
+ if not pobj:
3034
+ return {
3035
+ "success": False,
3036
+ "error": f"Provider '{provider_name}' not found",
3037
+ "available_providers": list(REAL_PROVIDERS.keys())
3038
+ }
3039
+
3040
+ start_time = time.time()
3041
+ response_chunks = []
3042
+ error_msg = None
3043
+
3044
+ try:
3045
+ msgs = [{"role": "user", "content": test_message}]
3046
+ stream = g4f.ChatCompletion.create(
3047
+ model=model_name,
3048
+ provider=pobj,
3049
+ messages=msgs,
3050
+ stream=True,
3051
+ timeout=timeout_seconds
3052
+ )
3053
+
3054
+ for chunk in stream:
3055
+ c = clean_stream(chunk)
3056
+ if c:
3057
+ response_chunks.append(c)
3058
+ # توقف بعد 500 حرف للاختبار
3059
+ if sum(len(r) for r in response_chunks) > 500:
3060
+ break
3061
+
3062
+ except Exception as e:
3063
+ error_msg = str(e)
3064
+
3065
+ elapsed = time.time() - start_time
3066
+ full_response = "".join(response_chunks)
3067
+
3068
+ return {
3069
+ "success": bool(full_response and not error_msg),
3070
+ "provider": provider_name,
3071
+ "model": model_name,
3072
+ "test_message": test_message,
3073
+ "response_preview": full_response[:300] if full_response else "",
3074
+ "response_length": len(full_response),
3075
+ "elapsed_seconds": round(elapsed, 2),
3076
+ "error": error_msg,
3077
+ "timestamp": int(time.time())
3078
+ }
3079
+
3080
+ # =====================================================
3081
+ # نقطة تشخيص: اختبار دورة كاملة مع أداة
3082
+ # =====================================================
3083
+ @app.post("/debug/test-full-tool-cycle")
3084
+ async def debug_test_full_tool_cycle(request: Request):
3085
+ """
3086
+ اختبار دورة كاملة: رسالة → كشف أداة → تنفيذ → نتيجة
3087
+ يُظهر بوضوح هل المحرك يعمل أم لا
3088
+ """
3089
+ verify_api_key(request)
3090
+ body = await request.json()
3091
+ test_message = body.get(
3092
+ "message",
3093
+ "Create a file called 'hello.py' with content: print('Hello World')"
3094
+ )
3095
+ model = body.get("model", "gpt-4o")
3096
+
3097
+ test_tools = [
3098
+ {
3099
+ "name": "write_file",
3100
+ "description": "Write content to a file on disk",
3101
+ "input_schema": {
3102
+ "type": "object",
3103
+ "properties": {
3104
+ "path": {
3105
+ "type": "string",
3106
+ "description": "The file path to write to"
3107
+ },
3108
+ "content": {
3109
+ "type": "string",
3110
+ "description": "The content to write to the file"
3111
+ }
3112
+ },
3113
+ "required": ["path", "content"]
3114
+ }
3115
+ }
3116
+ ]
3117
+
3118
+ test_session = f"test_cycle_{uuid.uuid4().hex[:8]}"
3119
+ results = {
3120
+ "test_session": test_session,
3121
+ "test_message": test_message,
3122
+ "model": model,
3123
+ "steps": [],
3124
+ "final_tool_calls": [],
3125
+ "success": False,
3126
+ "description_attempts": 0,
3127
+ "execution_attempts": 0,
3128
+ }
3129
+
3130
+ # المرحلة 1: تحويل الرسالة
3131
+ messages = [{"role": "user", "content": test_message}]
3132
+ full_message, history = MessageConverter.convert_messages(
3133
+ messages, "", test_tools, "auto", test_session
3134
+ )
3135
+
3136
+ results["steps"].append({
3137
+ "step": 1,
3138
+ "name": "Message Conversion",
3139
+ "message_length": len(full_message),
3140
+ "has_tool_instructions": "tool_call" in full_message.lower(),
3141
+ "has_executor_rule": "EXECUTOR" in full_message,
3142
+ })
3143
+
3144
+ # المرحلة 2: إرسال للنموذج
3145
+ step2_start = time.time()
3146
+ full_response = ""
3147
+ try:
3148
+ for chunk in ask(
3149
+ full_message, history, "Blackbox", model,
3150
+ tools=test_tools, session_id=test_session
3151
+ ):
3152
+ full_response += chunk
3153
+ except Exception as e:
3154
+ results["steps"].append({
3155
+ "step": 2,
3156
+ "name": "Model Request",
3157
+ "error": str(e)
3158
+ })
3159
+ return results
3160
+
3161
+ step2_elapsed = time.time() - step2_start
3162
+
3163
+ # المرحلة 3: تحليل الرد
3164
+ analysis = ResponseAnalyzer.analyze(full_response, test_tools)
3165
+ clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, test_tools)
3166
+
3167
+ results["steps"].append({
3168
+ "step": 2,
3169
+ "name": "Model Request",
3170
+ "response_length": len(full_response),
3171
+ "response_preview": full_response[:200],
3172
+ "elapsed_seconds": round(step2_elapsed, 2),
3173
+ "has_actual_tool_call": analysis["has_actual_tool_call"],
3174
+ "has_description_without_execution": analysis["has_description_without_execution"],
3175
+ "tool_calls_found": len(tool_calls),
3176
+ "described_actions": analysis["described_actions"][:3],
3177
+ "intended_tool": analysis.get("intended_tool_name"),
3178
+ })
3179
+
3180
+ if analysis["has_description_without_execution"]:
3181
+ results["description_attempts"] += 1
3182
+
3183
+ # المرحلة 4: تطبيق محرك التنفيذ إذا لزم
3184
+ if not tool_calls and analysis["has_description_without_execution"]:
3185
+ enforcement = ExecutionEnforcer.post_process_response(
3186
+ full_response, test_session, test_tools, tool_calls
3187
+ )
3188
+
3189
+ results["steps"].append({
3190
+ "step": 3,
3191
+ "name": "Execution Enforcer",
3192
+ "needs_retry": enforcement["needs_retry"],
3193
+ "retry_message_preview": enforcement["retry_message"][:200],
3194
+ })
3195
+
3196
+ if enforcement["needs_retry"]:
3197
+ results["execution_attempts"] += 1
3198
+ retry_response = ""
3199
+ retry_start = time.time()
3200
+
3201
+ try:
3202
+ for chunk in ask(
3203
+ enforcement["retry_message"], [], "Blackbox", model,
3204
+ tools=test_tools, session_id=test_session
3205
+ ):
3206
+ retry_response += chunk
3207
+ except Exception as e:
3208
+ results["steps"].append({
3209
+ "step": 4,
3210
+ "name": "Retry Request",
3211
+ "error": str(e)
3212
+ })
3213
+
3214
+ retry_elapsed = time.time() - retry_start
3215
+ clean_text2, tool_calls2 = ToolCallParser.parse_tool_calls(
3216
+ retry_response, test_tools
3217
+ )
3218
+
3219
+ results["steps"].append({
3220
+ "step": 4,
3221
+ "name": "Retry Request",
3222
+ "response_length": len(retry_response),
3223
+ "response_preview": retry_response[:200],
3224
+ "elapsed_seconds": round(retry_elapsed, 2),
3225
+ "tool_calls_found": len(tool_calls2),
3226
+ })
3227
+
3228
+ if tool_calls2:
3229
+ tool_calls = tool_calls2
3230
+
3231
+ # النتيجة النهائية
3232
+ results["final_tool_calls"] = tool_calls
3233
+ results["success"] = len(tool_calls) > 0
3234
+
3235
+ # تنظيف الجلسة التجريبية
3236
+ with CONTEXT_STORE._lock:
3237
+ if test_session in CONTEXT_STORE._store:
3238
+ del CONTEXT_STORE._store[test_session]
3239
+
3240
+ results["summary"] = {
3241
+ "total_steps": len(results["steps"]),
3242
+ "description_attempts": results["description_attempts"],
3243
+ "execution_attempts": results["execution_attempts"],
3244
+ "tool_calls_extracted": len(results["final_tool_calls"]),
3245
+ "verdict": (
3246
+ "✅ SUCCESS: Tool was actually called"
3247
+ if results["success"]
3248
+ else "❌ FAILURE: Model described but didn't execute"
3249
+ )
3250
+ }
3251
+
3252
+ return results
3253
+
3254
+ # =====================================================
3255
+ # نقطة: مسح كل الجلسات (للتطوير فقط)
3256
+ # =====================================================
3257
+ @app.delete("/context/all/clear")
3258
+ async def clear_all_sessions(request: Request):
3259
+ """مسح جميع الجلسات - للتطوير فقط"""
3260
+ verify_api_key(request)
3261
+ with CONTEXT_STORE._lock:
3262
+ count = len(CONTEXT_STORE._store)
3263
+ CONTEXT_STORE._store.clear()
3264
+ return {
3265
+ "message": f"Cleared {count} sessions",
3266
+ "timestamp": int(time.time())
3267
+ }
3268
+
3269
+ # =====================================================
3270
+ # نقطة: إضافة نتيجة أداة يدوياً (للاختبار)
3271
+ # =====================================================
3272
+ @app.post("/context/{session_id}/add-tool-result")
3273
+ async def add_tool_result_manually(
3274
+ session_id: str,
3275
+ request: Request
3276
+ ):
3277
+ """إضافة نتيجة أداة يدوياً - مفيد للاختبار"""
3278
+ verify_api_key(request)
3279
+ body = await request.json()
3280
+ tool_name = body.get("tool_name", "unknown")
3281
+ tool_id = body.get("tool_id", ToolCallParser.generate_tool_id())
3282
+ result = body.get("result", "")
3283
+
3284
+ CONTEXT_STORE.add_tool_result(session_id, tool_name, tool_id, result)
3285
+
3286
+ return {
3287
+ "message": f"Tool result added for session {session_id}",
3288
+ "tool_name": tool_name,
3289
+ "tool_id": tool_id,
3290
+ "result_preview": result[:100],
3291
+ "session_state": CONTEXT_STORE.get_or_create_session(session_id).get(
3292
+ "current_state", ExecutionState.IDLE
3293
+ )
3294
+ }
3295
+
3296
+ # =====================================================
3297
+ # نقطة: تحديث حالة الجلسة يدوياً
3298
+ # =====================================================
3299
+ @app.post("/context/{session_id}/set-state")
3300
+ async def set_session_state(session_id: str, request: Request):
3301
+ """تحديث حالة الجلسة يدوياً"""
3302
+ verify_api_key(request)
3303
+ body = await request.json()
3304
+ new_state = body.get("state", ExecutionState.IDLE)
3305
+ next_action = body.get("next_required_action", "")
3306
+
3307
+ valid_states = [
3308
+ ExecutionState.IDLE,
3309
+ ExecutionState.PLANNING,
3310
+ ExecutionState.TOOL_PENDING,
3311
+ ExecutionState.TOOL_EXECUTING,
3312
+ ExecutionState.TOOL_DONE,
3313
+ ExecutionState.AWAITING_NEXT,
3314
+ ExecutionState.COMPLETED,
3315
+ ]
3316
+
3317
+ if new_state not in valid_states:
3318
+ raise HTTPException(
3319
+ status_code=400,
3320
+ detail=f"Invalid state. Valid states: {valid_states}"
3321
+ )
3322
+
3323
+ CONTEXT_STORE.update_session(
3324
+ session_id,
3325
+ current_state=new_state,
3326
+ next_required_action=next_action
3327
+ )
3328
+
3329
+ return {
3330
+ "message": f"Session {session_id} state updated",
3331
+ "new_state": new_state,
3332
+ "next_required_action": next_action,
3333
+ "execution_directive": CONTEXT_STORE.get_execution_directive(session_id)
3334
+ }
3335
+
3336
+ # =====================================================
3337
+ # نقطة: تسجيل أداة معلّقة يدوياً
3338
+ # =====================================================
3339
+ @app.post("/context/{session_id}/register-pending-tool")
3340
+ async def register_pending_tool_manually(
3341
+ session_id: str,
3342
+ request: Request
3343
+ ):
3344
+ """تسجيل أداة معلّقة يدوياً - لإجبار البوت على تنفيذها"""
3345
+ verify_api_key(request)
3346
+ body = await request.json()
3347
+ tool_name = body.get("name", "")
3348
+ tool_args = body.get("arguments", {})
3349
+ reason = body.get("reason", "Manually registered")
3350
+
3351
+ if not tool_name:
3352
+ raise HTTPException(status_code=400, detail="Tool name is required")
3353
+
3354
+ tool_id = ToolCallParser.generate_tool_id()
3355
+ pending_tool = PendingToolCall(
3356
+ name=tool_name,
3357
+ arguments=tool_args,
3358
+ tool_id=tool_id,
3359
+ reason=reason
3360
+ )
3361
+
3362
+ CONTEXT_STORE.register_pending_tool(session_id, pending_tool)
3363
+
3364
+ return {
3365
+ "message": f"Pending tool '{tool_name}' registered for session {session_id}",
3366
+ "tool_id": tool_id,
3367
+ "directive": CONTEXT_STORE.get_execution_directive(session_id)
3368
+ }
3369
+
3370
+ # =====================================================
3371
+ # نقطة: اختبار JSON cleaner
3372
+ # =====================================================
3373
+ @app.post("/debug/test-json-cleaner")
3374
+ async def debug_test_json_cleaner(request: Request):
3375
+ """اختبار مُصلح JSON على حالات شائعة"""
3376
+ verify_api_key(request)
3377
+
3378
+ test_cases = [
3379
+ {
3380
+ "name": "Trailing comma",
3381
+ "input": '{"name": "write_file", "arguments": {"path": "test.py",}}',
3382
+ "expected_valid": True
3383
+ },
3384
+ {
3385
+ "name": "Curly quotes",
3386
+ "input": '{\u201cname\u201d: \u201cwrite_file\u201d, \u201carguments\u201d: {}}',
3387
+ "expected_valid": True
3388
+ },
3389
+ {
3390
+ "name": "Missing closing brace",
3391
+ "input": '{"name": "write_file", "arguments": {"path": "test.py"',
3392
+ "expected_valid": True
3393
+ },
3394
+ {
3395
+ "name": "Valid JSON",
3396
+ "input": '{"name": "read_file", "arguments": {"path": "config.json"}}',
3397
+ "expected_valid": True
3398
+ },
3399
+ {
3400
+ "name": "Double trailing commas",
3401
+ "input": '{"name": "tool",, "arguments": {}}',
3402
+ "expected_valid": False # هذا صعب الإصلاح
3403
+ },
3404
+ ]
3405
+
3406
+ results = []
3407
+ for tc in test_cases:
3408
+ cleaned = ToolCallParser._clean_json_string(tc["input"])
3409
+ parse_success = False
3410
+ parse_result = None
3411
+ error_msg = None
3412
+
3413
+ try:
3414
+ parse_result = json.loads(cleaned)
3415
+ parse_success = True
3416
+ except json.JSONDecodeError as e:
3417
+ error_msg = str(e)
3418
+ # محاولة الإصلاح التلقائي
3419
+ fixed = ToolCallParser._try_fix_json(tc["input"])
3420
+ if fixed:
3421
+ parse_result = fixed
3422
+ parse_success = True
3423
+ error_msg = None
3424
+
3425
+ results.append({
3426
+ "test_name": tc["name"],
3427
+ "original_input": tc["input"],
3428
+ "cleaned_output": cleaned,
3429
+ "parse_success": parse_success,
3430
+ "parse_result": parse_result,
3431
+ "error": error_msg,
3432
+ "expected_valid": tc["expected_valid"],
3433
+ "passed": parse_success == tc["expected_valid"]
3434
+ })
3435
+
3436
+ passed = sum(1 for r in results if r["passed"])
3437
+ return {
3438
+ "test_results": results,
3439
+ "summary": {
3440
+ "passed": passed,
3441
+ "failed": len(results) - passed,
3442
+ "total": len(results)
3443
+ }
3444
+ }
3445
+
3446
+ # =====================================================
3447
+ # نقطة: اختبار ResponseAnalyzer على نص مخصص
3448
+ # =====================================================
3449
+ @app.post("/debug/analyze-response")
3450
+ async def debug_analyze_response(request: Request):
3451
+ """تحليل نص مخصص بواسطة ResponseAnalyzer"""
3452
+ verify_api_key(request)
3453
+ body = await request.json()
3454
+ response_text = body.get("response", "")
3455
+ tools = body.get("tools", [])
3456
+
3457
+ if not response_text:
3458
+ raise HTTPException(status_code=400, detail="response field is required")
3459
+
3460
+ analysis = ResponseAnalyzer.analyze(response_text, tools)
3461
+
3462
+ # محاكاة قرار المحرك
3463
+ test_session = f"analyze_{uuid.uuid4().hex[:8]}"
3464
+ enforcement = ExecutionEnforcer.post_process_response(
3465
+ response_text, test_session, tools, analysis["tool_calls_found"]
3466
+ )
3467
+
3468
+ # تنظيف
3469
+ with CONTEXT_STORE._lock:
3470
+ if test_session in CONTEXT_STORE._store:
3471
+ del CONTEXT_STORE._store[test_session]
3472
+
3473
+ return {
3474
+ "input_response_preview": response_text[:300],
3475
+ "analysis": {
3476
+ "has_actual_tool_call": analysis["has_actual_tool_call"],
3477
+ "has_description_without_execution": (
3478
+ analysis["has_description_without_execution"]
3479
+ ),
3480
+ "needs_execution_push": analysis["needs_execution_push"],
3481
+ "described_actions_found": analysis["described_actions"],
3482
+ "intended_tool_name": analysis.get("intended_tool_name"),
3483
+ "tool_calls_extracted": len(analysis["tool_calls_found"]),
3484
+ "tool_calls_details": analysis["tool_calls_found"],
3485
+ },
3486
+ "enforcer_decision": {
3487
+ "needs_retry": enforcement["needs_retry"],
3488
+ "retry_message": enforcement["retry_message"],
3489
+ },
3490
+ "verdict": (
3491
+ "✅ EXECUTOR: Model actually called a tool"
3492
+ if analysis["has_actual_tool_call"]
3493
+ else (
3494
+ "⚠️ DESCRIBER: Model described without executing"
3495
+ if analysis["has_description_without_execution"]
3496
+ else "ℹ️ NEUTRAL: Regular text response (no tools involved)"
3497
+ )
3498
+ )
3499
+ }
3500
+
3501
+ # =====================================================
3502
+ # نقطة: مراقبة الكاش
3503
+ # =====================================================
3504
+ @app.get("/debug/cache-stats")
3505
+ async def debug_cache_stats(request: Request):
3506
+ """إحصائيات الكاش"""
3507
+ verify_api_key(request)
3508
+ with CACHE._lock:
3509
+ cache_size = len(CACHE.cache)
3510
+ now = time.time()
3511
+ expired_count = sum(
3512
+ 1 for _, ts in CACHE.cache.values()
3513
+ if now - ts > CACHE.ttl
3514
+ )
3515
+ entries = []
3516
+ for key, (value, ts) in list(CACHE.cache.items())[:10]:
3517
+ entries.append({
3518
+ "key_preview": key[:50],
3519
+ "value_length": len(value),
3520
+ "age_seconds": round(now - ts),
3521
+ "expired": now - ts > CACHE.ttl
3522
+ })
3523
+
3524
+ return {
3525
+ "cache_size": cache_size,
3526
+ "max_size": CACHE.max_size,
3527
+ "ttl_seconds": CACHE.ttl,
3528
+ "expired_entries": expired_count,
3529
+ "active_entries": cache_size - expired_count,
3530
+ "sample_entries": entries,
3531
+ "timestamp": int(time.time())
3532
+ }
3533
+
3534
+ # =====================================================
3535
+ # نقطة: تصدير سياق جلسة كاملة
3536
+ # =====================================================
3537
+ @app.get("/context/{session_id}/export")
3538
+ async def export_session_context(session_id: str, request: Request):
3539
+ """تصدير سياق الجلسة كاملاً بصيغة JSON"""
3540
+ verify_api_key(request)
3541
+
3542
+ ctx = CONTEXT_STORE.get_or_create_session(session_id)
3543
+ summary = CONTEXT_STORE.get_context_summary(session_id)
3544
+ directive = CONTEXT_STORE.get_execution_directive(session_id)
3545
+
3546
+ export_data = {
3547
+ "session_id": session_id,
3548
+ "exported_at": int(time.time()),
3549
+ "context": {
3550
+ "original_task": ctx.get("original_task", ""),
3551
+ "current_state": ctx.get("current_state", ExecutionState.IDLE),
3552
+ "message_count": ctx.get("message_count", 0),
3553
+ "completed_steps": ctx.get("completed_steps", []),
3554
+ "tool_results": ctx.get("tool_results", []),
3555
+ "pending_tool_calls": ctx.get("pending_tool_calls", []),
3556
+ "executed_tool_calls": ctx.get("executed_tool_calls", []),
3557
+ "described_but_not_executed": ctx.get("described_but_not_executed", []),
3558
+ "consecutive_description_count": ctx.get(
3559
+ "consecutive_description_count", 0
3560
+ ),
3561
+ "next_required_action": ctx.get("next_required_action", ""),
3562
+ "provider_history": ctx.get("provider_history", [])[-5:],
3563
+ "created_at": ctx.get("created_at", 0),
3564
+ "last_updated": ctx.get("last_updated", 0),
3565
+ },
3566
+ "human_readable_summary": summary,
3567
+ "current_execution_directive": directive,
3568
+ }
3569
+
3570
+ return export_data
3571
+
3572
+ # =====================================================
3573
+ # نقطة: استيراد سياق جلسة
3574
+ # =====================================================
3575
+ @app.post("/context/{session_id}/import")
3576
+ async def import_session_context(session_id: str, request: Request):
3577
+ """استيراد سياق جلسة من بيانات JSON"""
3578
+ verify_api_key(request)
3579
+ body = await request.json()
3580
+ context_data = body.get("context", {})
3581
+
3582
+ if not context_data:
3583
+ raise HTTPException(status_code=400, detail="context field is required")
3584
+
3585
+ # التحقق من الحقول المطلوبة وتعيين القيم الافتراضية
3586
+ safe_context = {
3587
+ "original_task": context_data.get("original_task", ""),
3588
+ "task_breakdown": context_data.get("task_breakdown", []),
3589
+ "completed_steps": context_data.get("completed_steps", []),
3590
+ "tool_results": context_data.get("tool_results", []),
3591
+ "pending_tool_calls": context_data.get("pending_tool_calls", []),
3592
+ "executed_tool_calls": context_data.get("executed_tool_calls", []),
3593
+ "current_state": context_data.get("current_state", ExecutionState.IDLE),
3594
+ "next_required_action": context_data.get("next_required_action", ""),
3595
+ "created_at": context_data.get("created_at", time.time()),
3596
+ "last_updated": time.time(),
3597
+ "message_count": context_data.get("message_count", 0),
3598
+ "provider_history": context_data.get("provider_history", []),
3599
+ "last_model_response": context_data.get("last_model_response", ""),
3600
+ "described_but_not_executed": context_data.get("described_but_not_executed", []),
3601
+ "consecutive_description_count": context_data.get(
3602
+ "consecutive_description_count", 0
3603
+ ),
3604
+ }
3605
+
3606
+ with CONTEXT_STORE._lock:
3607
+ CONTEXT_STORE._store[session_id] = safe_context
3608
+
3609
+ return {
3610
+ "message": f"Context imported for session {session_id}",
3611
+ "session_id": session_id,
3612
+ "state": safe_context["current_state"],
3613
+ "directive": CONTEXT_STORE.get_execution_directive(session_id)
3614
+ }
3615
+
3616
+ # =====================================================
3617
+ # نقطة: اختبار pattern matching للأنماط المحظورة
3618
+ # =====================================================
3619
+ @app.post("/debug/test-patterns")
3620
+ async def debug_test_patterns(request: Request):
3621
+ """اختبار أنماط كشف الوصف بدون تنفيذ"""
3622
+ verify_api_key(request)
3623
+ body = await request.json()
3624
+ test_texts = body.get("texts", [
3625
+ "سأقوم بإنشاء الملف الآن",
3626
+ "I will create the file",
3627
+ "Let me write the code",
3628
+ "I'm going to use write_file",
3629
+ "Here is the result: 42",
3630
+ "سأستخدم أداة write_file",
3631
+ "Now I'll create the Python file",
3632
+ "The file has been created successfully",
3633
+ "I'll use the write_file tool to save your code",
3634
+ "Using the write_file tool to create the file",
3635
+ ])
3636
+
3637
+ results = []
3638
+ for text in test_texts:
3639
+ matched_patterns = []
3640
+ for pattern in ResponseAnalyzer.DESCRIPTION_PHRASES:
3641
+ matches = re.findall(pattern, text, re.IGNORECASE)
3642
+ if matches:
3643
+ matched_patterns.append({
3644
+ "pattern": pattern,
3645
+ "matches": matches
3646
+ })
3647
+
3648
+ is_description = len(matched_patterns) > 0
3649
+ results.append({
3650
+ "text": text,
3651
+ "is_description_without_execution": is_description,
3652
+ "matched_patterns_count": len(matched_patterns),
3653
+ "matched_patterns": matched_patterns[:3], # أول 3 فقط
3654
+ "verdict": (
3655
+ "⚠️ DESCRIPTION (needs enforcement)"
3656
+ if is_description
3657
+ else "✅ OK (not a description pattern)"
3658
+ )
3659
+ })
3660
+
3661
+ description_count = sum(1 for r in results if r["is_description_without_execution"])
3662
+ return {
3663
+ "test_results": results,
3664
+ "summary": {
3665
+ "total_texts": len(results),
3666
+ "description_patterns_found": description_count,
3667
+ "clean_texts": len(results) - description_count,
3668
+ "total_patterns_available": len(ResponseAnalyzer.DESCRIPTION_PHRASES),
3669
+ }
3670
+ }
3671
+
3672
+ # =====================================================
3673
+ # نقطة: إحصائيات المزودين مع الأداء
3674
+ # =====================================================
3675
+ @app.get("/providers/stats")
3676
+ async def get_provider_stats(request: Request):
3677
+ """إحصائيات المزودين مع معلومات الأداء من التاريخ"""
3678
+ verify_api_key(request)
3679
+
3680
+ provider_usage = {}
3681
+ for pname in REAL_PROVIDERS.keys():
3682
+ provider_usage[pname] = {
3683
+ "total_uses": 0,
3684
+ "recent_uses": 0,
3685
+ "models_used": set(),
3686
+ }
3687
+
3688
+ # تحليل تاريخ الاستخدام من الجلسات
3689
+ with CONTEXT_STORE._lock:
3690
+ for sid, ctx in CONTEXT_STORE._store.items():
3691
+ for ph in ctx.get("provider_history", []):
3692
+ pname = ph.get("provider", "")
3693
+ model = ph.get("model", "")
3694
+ ts = ph.get("timestamp", 0)
3695
+
3696
+ if pname in provider_usage:
3697
+ provider_usage[pname]["total_uses"] += 1
3698
+ if model:
3699
+ provider_usage[pname]["models_used"].add(model)
3700
+ # استخدام خلال آخر ساعة
3701
+ if time.time() - ts < 3600:
3702
+ provider_usage[pname]["recent_uses"] += 1
3703
+
3704
+ # تحويل sets إلى lists للـ JSON
3705
+ result = {}
3706
+ for pname, stats in provider_usage.items():
3707
+ pobj = REAL_PROVIDERS.get(pname)
3708
+ available_models = discover_provider_models(pobj, pname) if pobj else []
3709
+ result[pname] = {
3710
+ "available": True,
3711
+ "total_uses": stats["total_uses"],
3712
+ "recent_uses_last_hour": stats["recent_uses"],
3713
+ "models_actually_used": list(stats["models_used"]),
3714
+ "all_available_models": available_models,
3715
+ }
3716
+
3717
+ return {
3718
+ "providers": result,
3719
+ "timestamp": int(time.time())
3720
+ }
3721
+
3722
+ # =====================================================
3723
+ # نقطة: اختبار stream buffer مع أداة كاملة
3724
+ # =====================================================
3725
+ @app.post("/debug/test-stream-with-tool")
3726
+ async def debug_test_stream_with_tool(request: Request):
3727
+ """اختبار stream buffer مع أداة كاملة مقسّمة على chunks"""
3728
+ verify_api_key(request)
3729
+ body = await request.json()
3730
+ chunk_size = body.get("chunk_size", 5)
3731
+
3732
+ test_tools = [{
3733
+ "name": "write_file",
3734
+ "description": "Write content to a file",
3735
+ "input_schema": {
3736
+ "type": "object",
3737
+ "properties": {
3738
+ "path": {"type": "string"},
3739
+ "content": {"type": "string"}
3740
+ },
3741
+ "required": ["path", "content"]
3742
+ }
3743
+ }]
3744
+
3745
+ # نص يحتوي على tool call كاملة
3746
+ test_text = (
3747
+ 'I will create the file for you.\n'
3748
+ '<tool_call>\n'
3749
+ '{\n'
3750
+ ' "name": "write_file",\n'
3751
+ ' "arguments": {\n'
3752
+ ' "path": "hello.py",\n'
3753
+ ' "content": "print(\'Hello World\')"\n'
3754
+ ' }\n'
3755
+ '}\n'
3756
+ '</tool_call>\n'
3757
+ 'The file has been created.'
3758
+ )
3759
+
3760
+ buffer = StreamToolBuffer(available_tools=test_tools)
3761
+ all_events = []
3762
+ text_events = []
3763
+ tool_events = []
3764
+
3765
+ # محاكاة الـ streaming chunk by chunk
3766
+ for i in range(0, len(test_text), chunk_size):
3767
+ chunk = test_text[i:i + chunk_size]
3768
+ events = buffer.feed(chunk)
3769
+ for event in events:
3770
+ all_events.append({
3771
+ "chunk_index": i // chunk_size,
3772
+ "chunk_preview": chunk[:10],
3773
+ "event_type": event.get("type", "tool_use"),
3774
+ "event_preview": (
3775
+ event.get("text", "")[:30]
3776
+ if event.get("type") == "text"
3777
+ else f"tool: {event.get('name', '')}"
3778
+ )
3779
+ })
3780
+ if event.get("type") == "text":
3781
+ text_events.append(event["text"])
3782
+ else:
3783
+ tool_events.append(event)
3784
+
3785
+ # تفريغ ما تبقى
3786
+ remaining = buffer.flush()
3787
+ for event in remaining:
3788
+ all_events.append({
3789
+ "chunk_index": "flush",
3790
+ "event_type": event.get("type", "tool_use"),
3791
+ "event_preview": (
3792
+ event.get("text", "")[:30]
3793
+ if event.get("type") == "text"
3794
+ else f"tool: {event.get('name', '')}"
3795
+ )
3796
+ })
3797
+ if event.get("type") == "text":
3798
+ text_events.append(event["text"])
3799
+ else:
3800
+ tool_events.append(event)
3801
+
3802
+ return {
3803
+ "input_text_length": len(test_text),
3804
+ "chunk_size": chunk_size,
3805
+ "total_chunks": (len(test_text) + chunk_size - 1) // chunk_size,
3806
+ "total_events": len(all_events),
3807
+ "text_events_count": len(text_events),
3808
+ "tool_events_count": len(tool_events),
3809
+ "reconstructed_text": "".join(text_events),
3810
+ "tool_calls_detected": tool_events,
3811
+ "success": len(tool_events) > 0,
3812
+ "verdict": (
3813
+ "✅ Stream buffer correctly detected tool call"
3814
+ if len(tool_events) > 0
3815
+ else "❌ Stream buffer missed the tool call"
3816
+ ),
3817
+ "all_events_sample": all_events[:20]
3818
+ }
3819
+
3820
+ # =====================================================
3821
+ # MAIN ENTRY POINT
3822
+ # =====================================================
3823
+ if __name__ == "__main__":
3824
+ import uvicorn
3825
+
3826
+ port = int(os.getenv("PORT", 8000))
3827
+ host = os.getenv("HOST", "0.0.0.0")
3828
+ workers = int(os.getenv("WORKERS", 1))
3829
+ log_level = os.getenv("LOG_LEVEL", "info").lower()
3830
+
3831
+ logger.info(f"Starting G4F Execution Engine v6.0.0")
3832
+ logger.info(f"Host: {host}, Port: {port}")
3833
+ logger.info(f"Workers: {workers}")
3834
+ logger.info(f"Cookie Status: {COOKIE_STATUS}")
3835
+ logger.info(f"Providers: {list(REAL_PROVIDERS.keys())}")
3836
+
3837
+ uvicorn.run(
3838
+ "main:app",
3839
+ host=host,
3840
+ port=port,
3841
+ workers=workers,
3842
+ log_level=log_level,
3843
+ reload=False,
3844
+ access_log=True,
3845
  )