Asem75 commited on
Commit
119ac91
·
verified ·
1 Parent(s): d4b0c8f

Create learning_loss_manager.pylearning_loss_manager.py

Browse files
learning_loss_manager.pylearning_loss_manager.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # learning_loss_manager.py
2
+
3
+ import os
4
+ import json
5
+ from datetime import datetime
6
+
7
+
8
+ class LearningLossManager:
9
+ """
10
+ مدير الفاقد التعليمي.
11
+
12
+ وظيفته:
13
+ - يستقبل تقرير فحص المستوى.
14
+ - يستخرج الفجوات.
15
+ - يختار أول فجوة مهمة.
16
+ - يبني خطة علاجية.
17
+ - يجهز Prompt للشرح القصير بواسطة Qwen لاحقًا.
18
+ - يجهز Prompt للكويز بواسطة Phi لاحقًا.
19
+
20
+ ملاحظة:
21
+ هذا الملف لا يشغّل Qwen ولا Phi مباشرة.
22
+ هو فقط يجهز الخطة والطلبات حتى لا نضغط على ذاكرة المساحة الآن.
23
+ """
24
+
25
+ def __init__(self):
26
+ self.version = "learning_loss_manager_v1_0"
27
+ self.storage_dir = "learning_loss_sessions"
28
+ os.makedirs(self.storage_dir, exist_ok=True)
29
+
30
+ def now(self):
31
+ return datetime.utcnow().isoformat()
32
+
33
+ def safe_text(self, value, default=""):
34
+ text = str(value or "").strip()
35
+ return text if text else default
36
+
37
+ def slug(self, text):
38
+ text = str(text or "").strip()
39
+ keep = []
40
+
41
+ for ch in text:
42
+ if ch.isalnum() or ch in ["_", "-"]:
43
+ keep.append(ch)
44
+ elif "\u0600" <= ch <= "\u06FF":
45
+ keep.append(ch)
46
+ else:
47
+ keep.append("_")
48
+
49
+ result = "".join(keep).strip("_")
50
+ return result[:80] or "item"
51
+
52
+ def make_plan_id(self, diagnostic_report):
53
+ subject = diagnostic_report.get("subject_key") or diagnostic_report.get("subject") or "subject"
54
+ identity = diagnostic_report.get("identity") or "student"
55
+ stamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
56
+ return f"loss_{self.slug(subject)}_{self.slug(identity)}_{stamp}"
57
+
58
+ def normalize_report(self, diagnostic_data):
59
+ """
60
+ يقبل:
61
+ - report النهائي مباشرة.
62
+ - state يحتوي report.
63
+ - dict يحتوي planning_payload.
64
+ """
65
+
66
+ if not isinstance(diagnostic_data, dict):
67
+ return {
68
+ "success": False,
69
+ "message": "بيانات الفحص غير صالحة.",
70
+ "report": {}
71
+ }
72
+
73
+ if isinstance(diagnostic_data.get("report"), dict):
74
+ return {
75
+ "success": True,
76
+ "message": "تم استخراج التقرير من حالة الفحص.",
77
+ "report": diagnostic_data.get("report")
78
+ }
79
+
80
+ if isinstance(diagnostic_data.get("planning_payload"), dict):
81
+ return {
82
+ "success": True,
83
+ "message": "البيانات هي تقرير أو تحتوي مخرجات التخطيط.",
84
+ "report": diagnostic_data
85
+ }
86
+
87
+ if diagnostic_data.get("badge_results") or diagnostic_data.get("history"):
88
+ return {
89
+ "success": True,
90
+ "message": "البيانات تبدو كتقرير فحص مباشر.",
91
+ "report": diagnostic_data
92
+ }
93
+
94
+ return {
95
+ "success": False,
96
+ "message": "لم أجد تقرير فحص أو planning_payload.",
97
+ "report": {}
98
+ }
99
+
100
+ def extract_planning_payload(self, report):
101
+ if not isinstance(report, dict):
102
+ return {}
103
+
104
+ payload = report.get("planning_payload")
105
+
106
+ if isinstance(payload, dict):
107
+ return payload
108
+
109
+ gap_badges = []
110
+ mastered_badges = []
111
+
112
+ for item in report.get("badge_results", []):
113
+ if item.get("status") == "mastered":
114
+ mastered_badges.append({
115
+ "badge_id": item.get("badge_id"),
116
+ "badge_title": item.get("badge_title"),
117
+ "strengths": item.get("strengths", []),
118
+ })
119
+ else:
120
+ gap_badges.append({
121
+ "badge_id": item.get("badge_id"),
122
+ "badge_title": item.get("badge_title"),
123
+ "status": item.get("status"),
124
+ "decision": item.get("decision"),
125
+ "first_gap": item.get("first_gap"),
126
+ "gaps": item.get("gaps", []),
127
+ "planner_instruction": item.get("planner_instruction"),
128
+ })
129
+
130
+ return {
131
+ "identity": report.get("identity"),
132
+ "subject": report.get("subject"),
133
+ "subject_key": report.get("subject_key"),
134
+ "grade": report.get("grade"),
135
+ "grade_number": report.get("grade_number"),
136
+ "mastered_badges": mastered_badges,
137
+ "gap_badges": gap_badges,
138
+ "planner_note": "تم بناء planning_payload داخل LearningLossManager.",
139
+ }
140
+
141
+ def choose_priority_gap(self, planning_payload):
142
+ gaps = planning_payload.get("gap_badges", [])
143
+
144
+ if not gaps:
145
+ return None
146
+
147
+ priority_order = {
148
+ "foundation_gap": 1,
149
+ "partial_gap": 2,
150
+ "needs_more_evidence": 3,
151
+ "near_mastery": 4,
152
+ }
153
+
154
+ def gap_score(item):
155
+ status = item.get("status", "needs_more_evidence")
156
+ return priority_order.get(status, 99)
157
+
158
+ sorted_gaps = sorted(gaps, key=gap_score)
159
+ return sorted_gaps[0]
160
+
161
+ def get_first_gap_detail(self, priority_gap):
162
+ if not isinstance(priority_gap, dict):
163
+ return {}
164
+
165
+ first_gap = priority_gap.get("first_gap")
166
+
167
+ if isinstance(first_gap, dict) and first_gap:
168
+ return first_gap
169
+
170
+ gaps = priority_gap.get("gaps", [])
171
+
172
+ if isinstance(gaps, list) and gaps:
173
+ if isinstance(gaps[0], dict):
174
+ return gaps[0]
175
+
176
+ return {
177
+ "micro_skill": "",
178
+ "micro_skill_title": "المهارة الضعيفة غير محددة بعد",
179
+ "reason": "تحتاج أدلة أكثر",
180
+ }
181
+
182
+ def build_plan_from_diagnostic_report(self, diagnostic_data):
183
+ normalized = self.normalize_report(diagnostic_data)
184
+
185
+ if not normalized.get("success"):
186
+ return {
187
+ "success": False,
188
+ "message": normalized.get("message"),
189
+ "plan": None,
190
+ }
191
+
192
+ report = normalized.get("report", {})
193
+ planning_payload = self.extract_planning_payload(report)
194
+ priority_gap = self.choose_priority_gap(planning_payload)
195
+
196
+ plan_id = self.make_plan_id(report)
197
+
198
+ if not priority_gap:
199
+ plan = {
200
+ "success": True,
201
+ "version": self.version,
202
+ "plan_id": plan_id,
203
+ "created_at": self.now(),
204
+ "updated_at": self.now(),
205
+ "source_test_id": report.get("test_id"),
206
+ "identity": planning_payload.get("identity") or report.get("identity"),
207
+ "subject": planning_payload.get("subject") or report.get("subject"),
208
+ "subject_key": planning_payload.get("subject_key") or report.get("subject_key"),
209
+ "grade": planning_payload.get("grade") or report.get("grade"),
210
+ "grade_number": planning_payload.get("grade_number") or report.get("grade_number"),
211
+ "status": "no_clear_gap",
212
+ "locked_until_done": False,
213
+ "message": "لا توجد فجوة واضحة من تقرير الفحص الحالي.",
214
+ "priority_gap": None,
215
+ "steps": [],
216
+ "qwen_prompt": "",
217
+ "phi_quiz_prompt": "",
218
+ "planning_payload": planning_payload,
219
+ }
220
+
221
+ self.save_plan(plan)
222
+ return {
223
+ "success": True,
224
+ "message": "لا توجد فجوة واضحة، وتم إنشاء سجل فارغ.",
225
+ "plan": plan,
226
+ }
227
+
228
+ first_gap = self.get_first_gap_detail(priority_gap)
229
+ steps = self.build_learning_loss_queue(priority_gap, first_gap)
230
+
231
+ plan = {
232
+ "success": True,
233
+ "version": self.version,
234
+ "plan_id": plan_id,
235
+ "created_at": self.now(),
236
+ "updated_at": self.now(),
237
+ "source_test_id": report.get("test_id"),
238
+ "identity": planning_payload.get("identity") or report.get("identity"),
239
+ "subject": planning_payload.get("subject") or report.get("subject"),
240
+ "subject_key": planning_payload.get("subject_key") or report.get("subject_key"),
241
+ "grade": planning_payload.get("grade") or report.get("grade"),
242
+ "grade_number": planning_payload.get("grade_number") or report.get("grade_number"),
243
+ "status": "open",
244
+ "locked_until_done": True,
245
+ "message": "تم إنشاء خطة فاقد تعليمي من تقرير فحص المستوى.",
246
+ "priority_gap": priority_gap,
247
+ "first_gap": first_gap,
248
+ "steps": steps,
249
+ "current_step_index": 0,
250
+ "qwen_prompt": "",
251
+ "phi_quiz_prompt": "",
252
+ "planning_payload": planning_payload,
253
+ }
254
+
255
+ plan["qwen_prompt"] = self.build_qwen_prompt(plan)
256
+ plan["phi_quiz_prompt"] = self.build_phi_quiz_prompt(plan)
257
+
258
+ self.save_plan(plan)
259
+
260
+ return {
261
+ "success": True,
262
+ "message": "تم بناء خطة الفاقد التعليمي.",
263
+ "plan": plan,
264
+ }
265
+
266
+ def build_learning_loss_queue(self, priority_gap, first_gap):
267
+ badge_title = priority_gap.get("badge_title", "وسام غير محدد")
268
+ status = priority_gap.get("status", "partial_gap")
269
+ micro_skill_title = first_gap.get("micro_skill_title", "المهارة الضعيفة")
270
+
271
+ return [
272
+ {
273
+ "step_id": "short_lesson",
274
+ "title": "شرح علاجي قصير",
275
+ "engine": "qwen",
276
+ "status": "pending",
277
+ "locked": False,
278
+ "goal": f"شرح مبسط للمهارة: {micro_skill_title}",
279
+ "badge_title": badge_title,
280
+ "gap_status": status,
281
+ "required_to_unlock_next": True,
282
+ },
283
+ {
284
+ "step_id": "guided_practice",
285
+ "title": "تدريب موجّه",
286
+ "engine": "qwen",
287
+ "status": "pending",
288
+ "locked": True,
289
+ "goal": "حل مثالين مع الطالب خطوة بخطوة دون كشف الحل مباشرة.",
290
+ "badge_title": badge_title,
291
+ "gap_status": status,
292
+ "required_to_unlock_next": True,
293
+ },
294
+ {
295
+ "step_id": "worksheet",
296
+ "title": "ورقة عمل قصيرة",
297
+ "engine": "local_or_qwen",
298
+ "status": "pending",
299
+ "locked": True,
300
+ "goal": "إنشاء 5 أسئلة متدرجة على نفس المهارة.",
301
+ "badge_title": badge_title,
302
+ "gap_status": status,
303
+ "required_to_unlock_next": True,
304
+ },
305
+ {
306
+ "step_id": "educational_game",
307
+ "title": "لعبة تعليمية",
308
+ "engine": "games_manager",
309
+ "status": "pending",
310
+ "locked": True,
311
+ "goal": "اختيار لعبة مناسبة لترسيخ المهارة بعد الشرح والورقة.",
312
+ "badge_title": badge_title,
313
+ "gap_status": status,
314
+ "required_to_unlock_next": False,
315
+ },
316
+ {
317
+ "step_id": "homework",
318
+ "title": "واجب قصير",
319
+ "engine": "local_or_qwen",
320
+ "status": "pending",
321
+ "locked": True,
322
+ "goal": "واجب من 3 أسئلة للتثبيت المنزلي.",
323
+ "badge_title": badge_title,
324
+ "gap_status": status,
325
+ "required_to_unlock_next": True,
326
+ },
327
+ {
328
+ "step_id": "phi_quiz",
329
+ "title": "كويز تحقق",
330
+ "engine": "phi",
331
+ "status": "pending",
332
+ "locked": True,
333
+ "goal": "كويز جديد لا يكرر سؤال الفحص.",
334
+ "badge_title": badge_title,
335
+ "gap_status": status,
336
+ "required_to_unlock_next": True,
337
+ },
338
+ {
339
+ "step_id": "reassessment",
340
+ "title": "إعادة تقييم المهارة",
341
+ "engine": "phi",
342
+ "status": "pending",
343
+ "locked": True,
344
+ "goal": "تحديد هل أُغلقت الفجوة أم تحتاج دورة علاجية ثانية.",
345
+ "badge_title": badge_title,
346
+ "gap_status": status,
347
+ "required_to_unlock_next": True,
348
+ },
349
+ ]
350
+
351
+ def build_qwen_prompt(self, plan):
352
+ priority_gap = plan.get("priority_gap") or {}
353
+ first_gap = plan.get("first_gap") or {}
354
+
355
+ subject = plan.get("subject") or plan.get("subject_key") or "المادة"
356
+ grade = plan.get("grade") or plan.get("grade_number") or "غير محدد"
357
+ badge_title = priority_gap.get("badge_title", "وسام غير محدد")
358
+ status = priority_gap.get("status", "partial_gap")
359
+ micro_skill_title = first_gap.get("micro_skill_title", "المهارة الضعيفة")
360
+ reason = first_gap.get("reason", "إجابة خاطئة أو لا أعلم")
361
+
362
+ payload = {
363
+ "role": "Qwen teacher",
364
+ "task": "نفذ شرحًا علاجيًا قصيرًا لفجوة تعليمية محددة.",
365
+ "student_context": {
366
+ "identity": plan.get("identity"),
367
+ "subject": subject,
368
+ "grade": grade,
369
+ },
370
+ "gap": {
371
+ "badge_title": badge_title,
372
+ "status": status,
373
+ "micro_skill_title": micro_skill_title,
374
+ "reason": reason,
375
+ "planner_instruction": priority_gap.get("planner_instruction"),
376
+ },
377
+ "teaching_rules": [
378
+ "ابدأ بسؤال افتتاحي قصير للتأكد من حضور الطالب.",
379
+ "اشرح المهارة ببساطة وبأمثلة قليلة.",
380
+ "لا تنتقل للخطوة التالية قبل سؤال تحقق.",
381
+ "إذا أجاب الطالب خطأ، أعد الشرح بطريقة أبسط.",
382
+ "لا تجعل الحصة طويلة.",
383
+ "لا تعطِ كويز نهائي، لأن الكويز سيبنيه Phi لاحقًا.",
384
+ "استخدم العربية الواضحة، ومع الإنجليز�� اذكر ترجمة عربية عند الحاجة.",
385
+ ],
386
+ "required_output": {
387
+ "opening_question": "سؤال حضور بسيط",
388
+ "short_explanation": "شرح قصير",
389
+ "example_1": "مثال محلول جزئيًا",
390
+ "check_question": "سؤال تحقق بعد الشرح",
391
+ "next_action": "انتظار إجابة الطالب",
392
+ },
393
+ }
394
+
395
+ return json.dumps(payload, ensure_ascii=False, indent=2)
396
+
397
+ def build_phi_quiz_prompt(self, plan):
398
+ priority_gap = plan.get("priority_gap") or {}
399
+ first_gap = plan.get("first_gap") or {}
400
+
401
+ payload = {
402
+ "role": "Phi evaluator",
403
+ "task": "اصنع كويز تحقق قصير بعد الحصة العلاجية.",
404
+ "student_context": {
405
+ "identity": plan.get("identity"),
406
+ "subject": plan.get("subject"),
407
+ "subject_key": plan.get("subject_key"),
408
+ "grade": plan.get("grade"),
409
+ "grade_number": plan.get("grade_number"),
410
+ },
411
+ "gap": {
412
+ "badge_id": priority_gap.get("badge_id"),
413
+ "badge_title": priority_gap.get("badge_title"),
414
+ "status": priority_gap.get("status"),
415
+ "micro_skill": first_gap.get("micro_skill"),
416
+ "micro_skill_title": first_gap.get("micro_skill_title"),
417
+ "reason": first_gap.get("reason"),
418
+ },
419
+ "rules": [
420
+ "أرجع JSON فقط.",
421
+ "اصنع 3 أسئلة جديدة لا تكرر سؤال الفحص.",
422
+ "كل سؤال له 4 اختيارات.",
423
+ "answer_index رقم من 0 إلى 3.",
424
+ "لا تكشف الحل للطالب قبل الإجابة.",
425
+ "بعد التصحيح أعط قرارًا: gap_closed أو needs_more_practice.",
426
+ ],
427
+ "required_json": {
428
+ "quiz_id": "quiz_unique_id",
429
+ "questions": [
430
+ {
431
+ "question_id": "q1",
432
+ "question": "نص السؤال",
433
+ "choices": ["أ", "ب", "ج", "د"],
434
+ "answer_index": 0,
435
+ "correct_answer": "أ",
436
+ "micro_skill_title": "اسم المهارة",
437
+ }
438
+ ],
439
+ "closing_rule": "gap_closed if score >= 2/3 else needs_more_practice",
440
+ },
441
+ }
442
+
443
+ return json.dumps(payload, ensure_ascii=False, indent=2)
444
+
445
+ def get_current_step(self, plan):
446
+ steps = plan.get("steps", [])
447
+ index = int(plan.get("current_step_index", 0))
448
+
449
+ if index < 0 or index >= len(steps):
450
+ return None
451
+
452
+ return steps[index]
453
+
454
+ def complete_current_step(self, plan, result=None):
455
+ if not isinstance(plan, dict):
456
+ return {
457
+ "success": False,
458
+ "message": "الخطة غير صالحة.",
459
+ "plan": plan,
460
+ }
461
+
462
+ steps = plan.get("steps", [])
463
+ index = int(plan.get("current_step_index", 0))
464
+
465
+ if index < 0 or index >= len(steps):
466
+ plan["status"] = "completed"
467
+ plan["locked_until_done"] = False
468
+ plan["updated_at"] = self.now()
469
+ self.save_plan(plan)
470
+
471
+ return {
472
+ "success": True,
473
+ "message": "الخطة مكتملة.",
474
+ "plan": plan,
475
+ }
476
+
477
+ steps[index]["status"] = "completed"
478
+ steps[index]["completed_at"] = self.now()
479
+ steps[index]["result"] = result or {}
480
+
481
+ next_index = index + 1
482
+
483
+ if next_index < len(steps):
484
+ steps[next_index]["locked"] = False
485
+ plan["current_step_index"] = next_index
486
+ plan["status"] = "open"
487
+ message = f"تم إنهاء الخطوة، والانتقال إلى: {steps[next_index].get('title')}"
488
+ else:
489
+ plan["current_step_index"] = next_index
490
+ plan["status"] = "completed"
491
+ plan["locked_until_done"] = False
492
+ message = "تم إنهاء خطة الفاقد التعليمي."
493
+
494
+ plan["steps"] = steps
495
+ plan["updated_at"] = self.now()
496
+ self.save_plan(plan)
497
+
498
+ return {
499
+ "success": True,
500
+ "message": message,
501
+ "plan": plan,
502
+ }
503
+
504
+ def save_plan(self, plan):
505
+ try:
506
+ plan_id = plan.get("plan_id") or "learning_loss_plan"
507
+ path = os.path.join(self.storage_dir, f"{self.slug(plan_id)}.json")
508
+
509
+ with open(path, "w", encoding="utf-8") as f:
510
+ json.dump(plan, f, ensure_ascii=False, indent=2)
511
+
512
+ plan["saved_path"] = path
513
+ except Exception:
514
+ pass
515
+
516
+ return plan
517
+
518
+ def load_plan(self, plan_id):
519
+ path = os.path.join(self.storage_dir, f"{self.slug(plan_id)}.json")
520
+
521
+ if not os.path.exists(path):
522
+ return None
523
+
524
+ try:
525
+ with open(path, "r", encoding="utf-8") as f:
526
+ return json.load(f)
527
+ except Exception:
528
+ return None
529
+
530
+ def format_plan(self, plan):
531
+ if not isinstance(plan, dict):
532
+ return "لا توجد خطة فاقد تعليمي."
533
+
534
+ if plan.get("status") == "no_clear_gap":
535
+ return f"""
536
+ # الفاقد التعليمي
537
+
538
+ ✅ لا توجد فجوة واضحة حاليًا.
539
+
540
+ المادة: {plan.get('subject')}
541
+ الصف: {plan.get('grade')}
542
+
543
+ الرسالة:
544
+ {plan.get('message')}
545
+ """.strip()
546
+
547
+ priority_gap = plan.get("priority_gap") or {}
548
+ first_gap = plan.get("first_gap") or {}
549
+ current_step = self.get_current_step(plan)
550
+
551
+ text = f"""
552
+ # خطة الفاقد التعليمي
553
+
554
+ الحالة: {plan.get('status')}
555
+ الخطة مقفلة حتى الإكمال: {plan.get('locked_until_done')}
556
+
557
+ المادة: {plan.get('subject')}
558
+ الصف: {plan.get('grade')}
559
+
560
+ ## الفجوة المختارة أولًا
561
+
562
+ الوسام: {priority_gap.get('badge_title')}
563
+ الحالة: {priority_gap.get('status')}
564
+ المهارة الصغيرة: {first_gap.get('micro_skill_title')}
565
+ سبب الفجوة: {first_gap.get('reason')}
566
+ تعليمات التخطيط: {priority_gap.get('planner_instruction')}
567
+
568
+ ## الخطوة الحالية
569
+
570
+ {current_step.get('title') if current_step else 'لا توجد خطوة حالية'}
571
+
572
+ ## خطوات الخطة
573
+ """.strip()
574
+
575
+ for i, step in enumerate(plan.get("steps", []), start=1):
576
+ lock_text = "مقفلة" if step.get("locked") else "مفتوحة"
577
+ text += f"""
578
+
579
+ {i}. {step.get('title')}
580
+ - المحرك: {step.get('engine')}
581
+ - الحالة