EGYADMIN commited on
Commit
a3b5c66
·
verified ·
1 Parent(s): 069756f

Create analysis/requirement_analyzer.py

Browse files
Files changed (1) hide show
  1. analysis/requirement_analyzer.py +881 -0
analysis/requirement_analyzer.py ADDED
@@ -0,0 +1,881 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # إذا كانت الجملة الأولى طويلة، اختصرها
2
+ if len(first_sentence) > 50:
3
+ if ":" in first_sentence:
4
+ # استخدام النص قبل العلامة : كعنوان
5
+ title = first_sentence.split(":")[0].strip()
6
+ else:
7
+ # اختصار الجملة الأولى
8
+ words = first_sentence.split()
9
+ title = " ".join(words[:7]) + "..."
10
+ else:
11
+ title = first_sentence
12
+
13
+ return title
14
+
15
+ def _categorize_requirements(self, requirements: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
16
+ """
17
+ تصنيف المتطلبات إلى فئات
18
+
19
+ المعاملات:
20
+ ----------
21
+ requirements : List[Dict[str, Any]]
22
+ قائمة المتطلبات
23
+
24
+ المخرجات:
25
+ --------
26
+ Dict[str, List[Dict[str, Any]]]
27
+ المتطلبات مصنفة حسب الفئة
28
+ """
29
+ categorized = {
30
+ "technical": [],
31
+ "financial": [],
32
+ "legal": []
33
+ }
34
+
35
+ for req in requirements:
36
+ description = req.get("description", "").lower()
37
+ title = req.get("title", "").lower()
38
+ text = title + " " + description
39
+
40
+ # تحديد الفئة بناءً على الكلمات المفتاحية
41
+ if any(keyword in text for keyword in ["فني", "تقني", "هندسي", "مواصفات", "جودة", "معايير", "أداء", "تصميم", "مخطط"]):
42
+ req["category"] = "technical"
43
+ categorized["technical"].append(req)
44
+ elif any(keyword in text for keyword in ["مالي", "سعر", "تكلفة", "دفع", "ضمان", "تأمين", "غرامة", "ميزانية", "ضريبة", "محاسبة"]):
45
+ req["category"] = "financial"
46
+ categorized["financial"].append(req)
47
+ elif any(keyword in text for keyword in ["قانوني", "شرط", "عقد", "التزام", "تعاقد", "نظام", "لائحة", "تشريع", "ترخيص", "حقوق"]):
48
+ req["category"] = "legal"
49
+ categorized["legal"].append(req)
50
+ else:
51
+ # إذا لم يتطابق مع أي فئة، افترض أنه متطلب فني
52
+ req["category"] = "technical"
53
+ categorized["technical"].append(req)
54
+
55
+ return categorized
56
+
57
+ def _analyze_importance_difficulty(self, categorized_requirements: Dict[str, List[Dict[str, Any]]]) -> Dict[str, List[Dict[str, Any]]]:
58
+ """
59
+ تحليل أهمية وصعوبة المتطلبات
60
+
61
+ المعاملات:
62
+ ----------
63
+ categorized_requirements : Dict[str, List[Dict[str, Any]]]
64
+ المتطلبات مصنفة حسب الفئة
65
+
66
+ المخرجات:
67
+ --------
68
+ Dict[str, List[Dict[str, Any]]]
69
+ المتطلبات بعد تحليل الأهمية والصعوبة
70
+ """
71
+ analyzed = {}
72
+
73
+ for category, reqs in categorized_requirements.items():
74
+ analyzed_reqs = []
75
+
76
+ for req in reqs:
77
+ # تحليل الأهمية
78
+ req["importance"] = self._determine_importance(req)
79
+
80
+ # تحليل صعوبة التنفيذ
81
+ req["difficulty"] = self._determine_difficulty(req)
82
+
83
+ # تحديد الفجوات
84
+ req["gaps"] = self._identify_gaps(req)
85
+
86
+ # اقتراح تحسينات
87
+ req["improvements"] = self._suggest_improvements(req)
88
+
89
+ analyzed_reqs.append(req)
90
+
91
+ analyzed[category] = analyzed_reqs
92
+
93
+ return analyzed
94
+
95
+ def _determine_importance(self, requirement: Dict[str, Any]) -> str:
96
+ """
97
+ تحديد أهمية المتطلب
98
+
99
+ المعاملات:
100
+ ----------
101
+ requirement : Dict[str, Any]
102
+ المتطلب
103
+
104
+ المخرجات:
105
+ --------
106
+ str
107
+ درجة الأهمية
108
+ """
109
+ text = requirement.get("description", "").lower()
110
+
111
+ # البحث عن كلمات تدل على الإلزامية
112
+ if any(keyword in text for keyword in ["يجب", "إلزامي", "ضروري", "لا بد", "لابد", "مطلوب", "يلتزم", "ملزم"]):
113
+ return "إلزامي"
114
+
115
+ # البحث عن كلمات تدل على الأهمية الثانوية
116
+ elif any(keyword in text for keyword in ["يفضل", "مرغوب", "محبذ", "يحبذ", "يستحسن"]):
117
+ return "ثانوي"
118
+
119
+ # إذا لم يتطابق مع أي حالة، افترض أنه متطلب عا��ي
120
+ else:
121
+ return "عادي"
122
+
123
+ def _determine_difficulty(self, requirement: Dict[str, Any]) -> int:
124
+ """
125
+ تحديد صعوبة تنفيذ المتطلب
126
+
127
+ المعاملات:
128
+ ----------
129
+ requirement : Dict[str, Any]
130
+ المتطلب
131
+
132
+ المخرجات:
133
+ --------
134
+ int
135
+ درجة الصعوبة (1-5)
136
+ """
137
+ text = requirement.get("description", "").lower()
138
+ category = requirement.get("category", "")
139
+ importance = requirement.get("importance", "")
140
+
141
+ # تعيين درجة صعوبة أساسية
142
+ base_difficulty = 3 # متوسط
143
+
144
+ # زيادة الصعوبة للمتطلبات الإلزامية
145
+ if importance == "إلزامي":
146
+ base_difficulty += 1
147
+
148
+ # زيادة الصعوبة بناءً على طول النص
149
+ if len(text) > 200:
150
+ base_difficulty += 0.5
151
+
152
+ # زيادة الصعوبة بناءً على الكلمات المفتاحية
153
+ if any(keyword in text for keyword in ["معقد", "صعب", "متقدم", "عالي", "متطور", "خبير", "مخصص", "خاص"]):
154
+ base_difficulty += 1
155
+
156
+ # تخفيض الصعوبة للمتطلبات البسيطة
157
+ if any(keyword in text for keyword in ["بسيط", "سهل", "عادي", "أساسي", "تقليدي"]):
158
+ base_difficulty -= 1
159
+
160
+ # ضبط الصعوبة في النطاق 1-5
161
+ return max(1, min(5, round(base_difficulty)))
162
+
163
+ def _identify_gaps(self, requirement: Dict[str, Any]) -> str:
164
+ """
165
+ تحديد الفجوات في المتطلب
166
+
167
+ المعاملات:
168
+ ----------
169
+ requirement : Dict[str, Any]
170
+ المتطلب
171
+
172
+ المخرجات:
173
+ --------
174
+ str
175
+ الفجوات المحددة
176
+ """
177
+ text = requirement.get("description", "")
178
+
179
+ gaps = []
180
+
181
+ # التحقق من وضوح المتطلب
182
+ if len(text) < 50:
183
+ gaps.append("وصف قصير وغير مفصل")
184
+
185
+ # التحقق من وجود معايير قياس
186
+ if not any(keyword in text.lower() for keyword in ["معيار", "قياس", "مؤشر", "مستوى", "نسبة", "كمية", "عدد"]):
187
+ gaps.append("لا يحتوي على معايير قياس واضحة")
188
+
189
+ # التحقق من وجود توصيف زمني
190
+ if not any(keyword in text.lower() for keyword in ["مدة", "فترة", "خلال", "زمن", "تاريخ", "موعد", "يوم", "أسبوع", "شهر"]):
191
+ gaps.append("لا يحدد إطار زمني")
192
+
193
+ # التحقق من وجود مسؤولية تنفيذ
194
+ if not any(keyword in text.lower() for keyword in ["مسؤول", "مسؤولية", "مقاول", "مورد", "مقدم", "طرف", "مكلف"]):
195
+ gaps.append("لا يحدد المسؤولية عن التنفيذ")
196
+
197
+ # إرجاع الفجوات
198
+ if gaps:
199
+ return "، ".join(gaps)
200
+ else:
201
+ return "لا توجد فجوات واضحة"
202
+
203
+ def _suggest_improvements(self, requirement: Dict[str, Any]) -> str:
204
+ """
205
+ اقتراح تحسينات للمتطلب
206
+
207
+ المعاملات:
208
+ ----------
209
+ requirement : Dict[str, Any]
210
+ المتطلب
211
+
212
+ المخرجات:
213
+ --------
214
+ str
215
+ التحسينات المقترحة
216
+ """
217
+ gaps = requirement.get("gaps", "")
218
+ category = requirement.get("category", "")
219
+
220
+ improvements = []
221
+
222
+ # اقتراح تحسينات بناءً على الفجوات
223
+ if "وصف قصير" in gaps:
224
+ improvements.append("توضيح المتطلب بشكل أكثر تفصيلاً")
225
+
226
+ if "معايير قياس" in gaps:
227
+ improvements.append("إضافة معايير قياس كمية ونوعية")
228
+
229
+ if "إطار زمني" in gaps:
230
+ improvements.append("تحديد إطار زمني واضح للتنفيذ")
231
+
232
+ if "المسؤولية" in gaps:
233
+ improvements.append("تحديد المسؤولية عن التنفيذ بوضوح")
234
+
235
+ # اقتراح تحسينات بناءً على الفئة
236
+ if category == "technical":
237
+ if not improvements:
238
+ improvements.append("إضافة مراجع للمعايير الفنية والمواصفات")
239
+ elif category == "financial":
240
+ if not improvements:
241
+ improvements.append("توضيح آلية الدفع والتسعير")
242
+ elif category == "legal":
243
+ if not improvements:
244
+ improvements.append("إضافة المرجعية القانونية والتنظيمية")
245
+
246
+ # إرجاع التحسينات
247
+ if improvements:
248
+ return "، ".join(improvements)
249
+ else:
250
+ return "المتطلب واضح ومكتمل"
251
+
252
+ def _extract_local_content_requirements(self, text: str) -> List[Dict[str, Any]]:
253
+ """
254
+ استخراج متطلبات المحتوى المحلي
255
+
256
+ المعاملات:
257
+ ----------
258
+ text : str
259
+ النص المستخرج من المناقصة
260
+
261
+ المخرجات:
262
+ --------
263
+ List[Dict[str, Any]]
264
+ قائمة متطلبات المحتوى المحلي
265
+ """
266
+ # الكلمات المفتاحية للمحتوى المحلي
267
+ local_content_keywords = [
268
+ "المحتوى المحلي", "التوطين", "السعودة", "المنتجات المحلية",
269
+ "الصناعة المحلية", "نسبة المحتوى", "المكون المحلي", "منشأ محلي",
270
+ "نطاقات", "توظيف السعوديين", "توظيف المواطنين", "القيمة المضافة المحلية"
271
+ ]
272
+
273
+ local_content_requirements = []
274
+
275
+ # البحث عن أقسام المحتوى المحلي
276
+ for keyword in local_content_keywords:
277
+ pattern = rf"((?:{keyword}|{keyword.upper()}).*?(?:\n\n|\Z))"
278
+ matches = re.finditer(pattern, text, re.DOTALL)
279
+
280
+ for match in matches:
281
+ section_text = match.group(1).strip()
282
+
283
+ # استخراج النسب المئوية
284
+ percentage_matches = re.findall(r'(\d+(?:\.\d+)?)(?:\s*%|\s*في المائة|\s*بالمائة)', section_text)
285
+
286
+ if percentage_matches:
287
+ for percentage in percentage_matches:
288
+ requirement = {
289
+ "title": f"متطلب المحتوى المحلي - {percentage}%",
290
+ "description": section_text,
291
+ "category": "local_content",
292
+ "importance": "إلزامي",
293
+ "percentage": float(percentage),
294
+ "difficulty": 4, # افتراضي: صعوبة عالية
295
+ "gaps": self._identify_gaps({"description": section_text}),
296
+ "improvements": "توضيح آلية حساب وتوثيق نسبة المحتوى المحلي"
297
+ }
298
+ local_content_requirements.append(requirement)
299
+ else:
300
+ # إذا لم يتم العثور على نسب، أضف المتطلب بدون نسبة
301
+ requirement = {
302
+ "title": "متطلب المحتوى المحلي",
303
+ "description": section_text,
304
+ "category": "local_content",
305
+ "importance": "إلزامي",
306
+ "difficulty": 3, # افتراضي: صعوبة متوسطة
307
+ "gaps": "لا يحدد نسبة محتوى محلي واضحة",
308
+ "improvements": "تحديد نسبة المحتوى المحلي المطلوبة بشكل صريح"
309
+ }
310
+ local_content_requirements.append(requirement)
311
+
312
+ # إزالة التكرارات
313
+ unique_requirements = []
314
+ descriptions = set()
315
+
316
+ for req in local_content_requirements:
317
+ desc = req["description"]
318
+ if desc not in descriptions:
319
+ descriptions.add(desc)
320
+ unique_requirements.append(req)
321
+
322
+ return unique_requirements
323
+
324
+ def _generate_requirements_summary(self, categorized_requirements: Dict[str, List[Dict[str, Any]]],
325
+ local_content_requirements: List[Dict[str, Any]],
326
+ total_count: int, mandatory_count: int, avg_difficulty: float) -> str:
327
+ """
328
+ إعداد ملخص المتطلبات
329
+
330
+ المعاملات:
331
+ ----------
332
+ categorized_requirements : Dict[str, List[Dict[str, Any]]]
333
+ المتطلبات مصنفة حسب الفئة
334
+ local_content_requirements : List[Dict[str, Any]]
335
+ متطلبات المحتوى المحلي
336
+ total_count : int
337
+ إجمالي عدد المتطلبات
338
+ mandatory_count : int
339
+ عدد المتطلبات الإلزامية
340
+ avg_difficulty : float
341
+ متوسط صعوبة التنفيذ
342
+
343
+ المخرجات:
344
+ --------
345
+ str
346
+ ملخص المتطلبات
347
+ """
348
+ # إعداد الملخص
349
+ summary = f"تم تحليل {total_count} متطلب، منها {mandatory_count} متطلب إلزامي. "
350
+
351
+ # توزيع المتطلبات حسب الفئة
352
+ tech_count = len(categorized_requirements.get("technical", []))
353
+ fin_count = len(categorized_requirements.get("financial", []))
354
+ legal_count = len(categorized_requirements.get("legal", []))
355
+ local_count = len(local_content_requirements)
356
+
357
+ summary += f"تتوزع المتطلبات إلى {tech_count} متطلب فني، و{fin_count} متطلب مالي، و{legal_count} متطلب قانوني، و{local_count} متطلب للمحتوى المحلي. "
358
+
359
+ # صعوبة التنفيذ
360
+ if avg_difficulty >= 4:
361
+ summary += "متوسط صعوبة تنفيذ المتطلبات مرتفع، مما يشير إلى تعقيد المشروع. "
362
+ elif avg_difficulty >= 3:
363
+ summary += "متوسط صعوبة تنفيذ المتطلبات متوسط. "
364
+ else:
365
+ summary += "متوسط صعوبة تنفيذ المتطلبات منخفض، مما يشير إلى إمكانية تنفيذ المشروع بسهولة نسبية. "
366
+
367
+ # متطلبات المحتوى المحلي
368
+ if local_count > 0:
369
+ local_percentages = [req.get("percentage", 0) for req in local_content_requirements if "percentage" in req]
370
+ if local_percentages:
371
+ max_percentage = max(local_percentages)
372
+ summary += f"يتطلب المشروع تحقيق نسبة محتوى محلي لا تقل عن {max_percentage}%. "
373
+ else:
374
+ summary += "يتضمن المشروع متطلبات للمحتوى المحلي، لكن النسبة المطلوبة غير محددة بوضوح. "
375
+ else:
376
+ summary += "لم يتم تحديد متطلبات واضحة للمحتوى المحلي. "
377
+
378
+ # تقييم عام
379
+ if mandatory_count / total_count > 0.7:
380
+ summary += "نسبة المتطلبات الإلزامية مرتفعة، مما يشير إلى تشدد في شروط المناقصة. "
381
+
382
+ if avg_difficulty > 3.5 and mandatory_count / total_count > 0.6:
383
+ summary += "يجب الانتباه إلى ارتفاع صعوبة التنفيذ ونسبة المتطلبات الإلزامية، مما قد يزيد من مخاطر المشروع."
384
+
385
+ return summary
386
+
387
+ def _generate_template_requirements(self) -> List[Dict[str, Any]]:
388
+ """
389
+ إنشاء متطلبات افتراضية من القوالب
390
+
391
+ المخرجات:
392
+ --------
393
+ List[Dict[str, Any]]
394
+ قائمة المتطلبات الافتراضية
395
+ """
396
+ # إنشاء نسخة من قوالب المتطلبات
397
+ requirements = []
398
+
399
+ # إضافة متطلبات من كل فئة
400
+ for category, templates in self.requirement_templates.items():
401
+ for template in templates:
402
+ requirement = template.copy()
403
+ requirement["category"] = category
404
+ requirements.append(requirement)
405
+
406
+ return requirements
407
+
408
+ def _clean_requirements(self, requirements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
409
+ """
410
+ تنظيف وتوحيد المتطلبات
411
+
412
+ المعاملات:
413
+ ----------
414
+ requirements : List[Dict[str, Any]]
415
+ قائمة المتطلبات
416
+
417
+ المخرجات:
418
+ --------
419
+ List[Dict[str, Any]]
420
+ قائمة المتطلبات المنظفة
421
+ """
422
+ cleaned = []
423
+ descriptions = set()
424
+
425
+ for req in requirements:
426
+ # تنظيف الوصف
427
+ if "description" in req:
428
+ req["description"] = req["description"].strip()
429
+
430
+ # تجاهل المتطلبات المكررة
431
+ if req.get("description", "") in descriptions:
432
+ continue
433
+
434
+ # إضافة الوصف إلى مجموعة الأوصاف
435
+ descriptions.add(req.get("description", ""))
436
+
437
+ # التأكد من وجود عنوان
438
+ if "title" not in req or not req["title"]:
439
+ req["title"] = self._extract_requirement_title(req.get("description", ""))
440
+
441
+ # إضافة المتطلب إلى القائمة المنظفة
442
+ cleaned.append(req)
443
+
444
+ return cleaned
445
+
446
+ def _load_requirement_templates(self) -> Dict[str, List[Dict[str, Any]]]:
447
+ """
448
+ تحميل قوالب المتطلبات
449
+
450
+ المخرجات:
451
+ --------
452
+ Dict[str, List[Dict[str, Any]]]
453
+ قوالب المتطلبات
454
+ """
455
+ try:
456
+ file_path = 'data/templates/requirement_templates.json'
457
+ if os.path.exists(file_path):
458
+ with open(file_path, 'r', encoding='utf-8') as f:
459
+ return json.load(f)
460
+ else:
461
+ logger.warning(f"ملف قوالب المتطلبات غير موجود: {file_path}")
462
+ # إنشاء قوالب افتراضية
463
+ return self._create_default_requirement_templates()
464
+ except Exception as e:
465
+ logger.error(f"فشل في تحميل قوالب المتطلبات: {str(e)}")
466
+ return self._create_default_requirement_templates()
467
+
468
+ def _create_default_requirement_templates(self) -> Dict[str, List[Dict[str, Any]]]:
469
+ """
470
+ إنشاء قوالب متطلبات افتراضية
471
+
472
+ المخرجات:
473
+ --------
474
+ Dict[str, List[Dict[str, Any]]]
475
+ قوالب المتطلبات الافتراضية
476
+ """
477
+ templates = {
478
+ "technical": [
479
+ {
480
+ "title": "مؤهلات وخبرات الفريق الفني",
481
+ "description": "يجب توفير فريق فني مؤهل ذو خبرة لا تقل عن 5 سنوات في مجال المشروع، مع تقديم السير الذاتية للمهندسين الرئيسيين.",
482
+ "importance": "إلزامي",
483
+ "difficulty": 3
484
+ },
485
+ {
486
+ "title": "جودة المواد والمعدات",
487
+ "description": "يجب استخدام مواد ومعدات ذات جودة عالية ومطابقة للمواصفات القياسية السعودية والعالمية، مع تقديم شهادات الجودة والمنشأ.",
488
+ "importance": "إلزامي",
489
+ "difficulty": 4
490
+ },
491
+ {
492
+ "title": "خطة العمل والجدول الزمني",
493
+ "description": "تقديم خطة عمل تفصيلية وجدول زمني لتنفيذ المشروع، موضحاً المراحل الرئيسية والمدد الزمنية لكل مرحلة.",
494
+ "importance": "إلزامي",
495
+ "difficulty": 3
496
+ },
497
+ {
498
+ "title": "ضمان الأعمال",
499
+ "description": "تقديم ضمان للأعمال المنفذة لمدة لا تقل عن سنة من تاريخ الاستلام النهائي، مع الالتزام بإصلاح أي عيوب خلال فترة الضمان.",
500
+ "importance": "إلزامي",
501
+ "difficulty": 2
502
+ },
503
+ {
504
+ "title": "الصيانة الدورية",
505
+ "description": "يفضل تقديم برنامج للصيانة الدورية بعد انتهاء فترة الضمان، مع تحديد التكاليف والخدمات المشمولة.",
506
+ "importance": "ثانوي",
507
+ "difficulty": 2
508
+ }
509
+ ],
510
+ "financial": [
511
+ {
512
+ "title": "العرض المالي",
513
+ "description": "تقديم عرض مالي مفصل يوضح تكاليف كل بند من بنود المشروع، مع بيان القيمة الإجمالية شاملة ضريبة القيمة المضافة.",
514
+ "importance": "إلزامي",
515
+ "difficulty": 3
516
+ },
517
+ {
518
+ "title": "الضمان الابتدائي",
519
+ "description": "تقديم ضمان ابتدائي بنسبة 2% من قيمة العرض، ساري المفعول لمدة 90 يوماً من تاريخ فتح المظاريف.",
520
+ "importance": "إلزامي",
521
+ "difficulty": 2
522
+ },
523
+ {
524
+ "title": "الضمان النهائي",
525
+ "description": "تقديم ضمان نهائي بنسبة 5% من قيمة العقد عند الترسية، ساري المفعول حتى انتهاء فترة الضمان.",
526
+ "importance": "إلزامي",
527
+ "difficulty": 2
528
+ },
529
+ {
530
+ "title": "شروط الدفع",
531
+ "description": "يتم الدفع على دفعات مرحلية حسب نسب الإنجاز، مع احتجاز نسبة 10% من كل دفعة كضمان حسن التنفيذ.",
532
+ "importance": "إلزامي",
533
+ "difficulty": 2
534
+ },
535
+ {
536
+ "title": "غرامات التأخير",
537
+ "description": "تفرض غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير، بحد أقصى 10% من القيمة الإجمالية للعقد.",
538
+ "importance": "إلزامي",
539
+ "difficulty": 3
540
+ }
541
+ ],
542
+ "legal": [
543
+ {
544
+ "title": "السجل التجاري",
545
+ "description": "يجب أن يكون المتقدم حاصلاً على سجل تجاري ساري المفعول في نفس مجال المناقصة.",
546
+ "importance": "إلزامي",
547
+ "difficulty": 1
548
+ },
549
+ {
550
+ "title": "تصنيف المقاولين",
551
+ "description": "يجب أن يكون المقاول مصنفاً لدى وزارة الشؤون البلدية والقروية والإسكان في المجال المطلوب وبدرجة لا تقل عن الثالثة.",
552
+ "importance": "إلزامي",
553
+ "difficulty": 2
554
+ },
555
+ {
556
+ "title": "التأمينات",
557
+ "description": "تقديم شهادة من المؤسسة العامة للتأمينات الاجتماعية تثبت الوفاء بالالتزامات تجاه المؤسسة.",
558
+ "importance": "إلزامي",
559
+ "difficulty": 1
560
+ },
561
+ {
562
+ "title": "الزكاة والدخل",
563
+ "description": "تقديم شهادة من هيئة الزكاة والضريبة والجمارك تثبت سداد المستحقات.",
564
+ "importance": "إلزامي",
565
+ "difficulty": 1
566
+ },
567
+ {
568
+ "title": "نظام المنافسات والمشتريات",
569
+ "description": "الالتزام بأحكام نظام المنافسات والمشتريات الحكومية ولائحته التنفيذية.",
570
+ "importance": "إلزامي",
571
+ "difficulty": 2
572
+ }
573
+ ]
574
+ }
575
+
576
+ return templates"""
577
+ محلل متطلبات المناقصات
578
+ يقوم باستخراج وتحليل وتصنيف متطلبات المناقصات
579
+ """
580
+
581
+ import re
582
+ import logging
583
+ import json
584
+ import os
585
+ from typing import Dict, List, Any, Tuple, Optional, Union
586
+ import numpy as np
587
+
588
+ logger = logging.getLogger(__name__)
589
+
590
+ class RequirementAnalyzer:
591
+ """
592
+ محلل متطلبات المناقصات
593
+ """
594
+
595
+ def __init__(self, model_loader, arabic_nlp, config=None):
596
+ """
597
+ تهيئة محلل المتطلبات
598
+
599
+ المعاملات:
600
+ ----------
601
+ model_loader : ModelLoader
602
+ محمّل النماذج المستخدمة للتحليل
603
+ arabic_nlp : ArabicNLP
604
+ أدوات معالجة اللغة العربية
605
+ config : Dict, optional
606
+ إعدادات المحلل
607
+ """
608
+ self.config = config or {}
609
+ self.model_loader = model_loader
610
+ self.arabic_nlp = arabic_nlp
611
+
612
+ # تحميل النماذج
613
+ self.ner_model = None
614
+ if hasattr(model_loader, 'get_ner_model'):
615
+ try:
616
+ self.ner_model = model_loader.get_ner_model()
617
+ logger.info("تم تحميل نموذج التعرف على الكيانات المسماة")
618
+ except Exception as e:
619
+ logger.warning(f"فشل في تحميل نموذج التعرف على الكيانات المسماة: {str(e)}")
620
+
621
+ # تحميل قوالب المتطلبات
622
+ self.requirement_templates = self._load_requirement_templates()
623
+
624
+ logger.info("تم تهيئة محلل المتطلبات")
625
+
626
+ def analyze(self, text: str) -> Dict[str, Any]:
627
+ """
628
+ تحليل متطلبات المناقصة من النص
629
+
630
+ المعاملات:
631
+ ----------
632
+ text : str
633
+ النص المستخرج من المناقصة
634
+
635
+ المخرجات:
636
+ --------
637
+ Dict[str, Any]
638
+ نتائج تحليل المتطلبات
639
+ """
640
+ try:
641
+ logger.info("بدء تحليل متطلبات المناقصة")
642
+
643
+ # استخراج المتطلبات من النص
644
+ requirements = self._extract_requirements(text)
645
+
646
+ # تصنيف المتطلبات
647
+ categorized_requirements = self._categorize_requirements(requirements)
648
+
649
+ # تحليل الأهمية والصعوبة
650
+ analyzed_requirements = self._analyze_importance_difficulty(categorized_requirements)
651
+
652
+ # تحليل متطلبات المحتوى المحلي
653
+ local_content_requirements = self._extract_local_content_requirements(text)
654
+
655
+ # تحديد عدد المتطلبات الإلزامية
656
+ mandatory_count = sum(1 for req in requirements if req.get("importance", "") == "إلزامي")
657
+
658
+ # حساب متوسط صعوبة التنفيذ
659
+ difficulty_values = [req.get("difficulty", 3) for req in requirements if req.get("difficulty", 0) > 0]
660
+ avg_difficulty = np.mean(difficulty_values) if difficulty_values else 3.0
661
+
662
+ # حساب نسبة متطلبات المحتوى المحلي
663
+ total_count = len(requirements)
664
+ local_content_percentage = (len(local_content_requirements) / total_count * 100) if total_count > 0 else 0
665
+
666
+ # إعداد ملخص المتطلبات
667
+ summary = self._generate_requirements_summary(
668
+ analyzed_requirements, local_content_requirements, total_count, mandatory_count, avg_difficulty
669
+ )
670
+
671
+ # إعداد النتائج
672
+ results = {
673
+ "summary": summary,
674
+ "technical": analyzed_requirements.get("technical", []),
675
+ "financial": analyzed_requirements.get("financial", []),
676
+ "legal": analyzed_requirements.get("legal", []),
677
+ "local_content": local_content_requirements,
678
+ "total_count": total_count,
679
+ "mandatory_count": mandatory_count,
680
+ "avg_difficulty": avg_difficulty,
681
+ "local_content_percentage": local_content_percentage
682
+ }
683
+
684
+ logger.info(f"اكتمل تحليل المتطلبات: {total_count} متطلبات، {mandatory_count} إلزامية")
685
+ return results
686
+
687
+ except Exception as e:
688
+ logger.error(f"فشل في تحليل المتطلبات: {str(e)}")
689
+ return {
690
+ "summary": "حدث خطأ أثناء تحليل المتطلبات",
691
+ "technical": [],
692
+ "financial": [],
693
+ "legal": [],
694
+ "local_content": [],
695
+ "total_count": 0,
696
+ "mandatory_count": 0,
697
+ "avg_difficulty": 0,
698
+ "local_content_percentage": 0,
699
+ "error": str(e)
700
+ }
701
+
702
+ def _extract_requirements(self, text: str) -> List[Dict[str, Any]]:
703
+ """
704
+ استخراج المتطلبات من النص
705
+
706
+ المعاملات:
707
+ ----------
708
+ text : str
709
+ النص المستخرج من المناقصة
710
+
711
+ المخرجات:
712
+ --------
713
+ List[Dict[str, Any]]
714
+ قائمة المتطلبات المستخرجة
715
+ """
716
+ requirements = []
717
+
718
+ # البحث عن قسم المتطلبات أو الشروط
719
+ requirement_sections = self._find_requirement_sections(text)
720
+
721
+ # إذا لم يتم العثور على أقسام للمتطلبات، استخدم النص كاملاً
722
+ if not requirement_sections:
723
+ requirement_sections = [text]
724
+
725
+ # معالجة كل قسم
726
+ for section in requirement_sections:
727
+ # استخراج المتطلبات من النص
728
+ section_requirements = self._parse_requirements_text(section)
729
+
730
+ # دمج المتطلبات المستخرجة
731
+ requirements.extend(section_requirements)
732
+
733
+ # إذا لم يتم العثور على متطلبات، استخدم القوالب
734
+ if not requirements:
735
+ requirements = self._generate_template_requirements()
736
+
737
+ # تنظيف وتوحيد المتطلبات
738
+ requirements = self._clean_requirements(requirements)
739
+
740
+ return requirements
741
+
742
+ def _find_requirement_sections(self, text: str) -> List[str]:
743
+ """
744
+ البحث عن أقسام المتطلبات في النص
745
+
746
+ المعاملات:
747
+ ----------
748
+ text : str
749
+ النص المستخرج من المناقصة
750
+
751
+ المخرجات:
752
+ --------
753
+ List[str]
754
+ أقسام المتطلبات المستخرجة
755
+ """
756
+ sections = []
757
+
758
+ # الكلمات المفتاحية لأقسام المتطلبات
759
+ section_keywords = [
760
+ "المتطلبات", "الشروط", "المواصفات", "نطاق العمل",
761
+ "البنود", "المعايير", "الالتزامات", "المؤهلات",
762
+ "التأهيل", "الواجبات", "نطاق الأعمال", "الخدمات المطلوبة"
763
+ ]
764
+
765
+ # البحث عن أقسام المتطلبات
766
+ for keyword in section_keywords:
767
+ pattern = rf"((?:{keyword}|{keyword.upper()})[:\s].*?)(?:^(?:{section_keywords[0]}|{section_keywords[0].upper()})[:\s]|\Z)"
768
+ matches = re.finditer(pattern, text, re.MULTILINE | re.DOTALL)
769
+
770
+ for match in matches:
771
+ section_text = match.group(1).strip()
772
+ if len(section_text) > 50: # تجاهل الأقسام القصيرة جدًا
773
+ sections.append(section_text)
774
+
775
+ # البحث عن قو��ئم البنود
776
+ bullet_lists = re.findall(r'(?:^|\n)(?:[•\-*]\s+.*(?:\n|$))+', text, re.MULTILINE)
777
+
778
+ for bullet_list in bullet_lists:
779
+ if len(bullet_list) > 100: # تجاهل القوائم القصيرة
780
+ sections.append(bullet_list)
781
+
782
+ # البحث عن قوائم مرقمة
783
+ numbered_lists = re.findall(r'(?:^|\n)(?:\d+[.)\s]+.*(?:\n|$))+', text, re.MULTILINE)
784
+
785
+ for numbered_list in numbered_lists:
786
+ if len(numbered_list) > 100: # تجاهل القوائم القصيرة
787
+ sections.append(numbered_list)
788
+
789
+ return sections
790
+
791
+ def _parse_requirements_text(self, text: str) -> List[Dict[str, Any]]:
792
+ """
793
+ تحليل نص المتطلبات لاستخراج المتطلبات الفردية
794
+
795
+ المعاملات:
796
+ ----------
797
+ text : str
798
+ نص قسم المتطلبات
799
+
800
+ المخرجات:
801
+ --------
802
+ List[Dict[str, Any]]
803
+ قائمة المتطلبات المستخرجة
804
+ """
805
+ requirements = []
806
+
807
+ # استخراج المتطلبات من القوائم النقطية
808
+ bullet_items = re.findall(r'[•\-*]\s+(.*?)(?:\n[•\-*]|\n\n|\Z)', text, re.DOTALL)
809
+
810
+ for item in bullet_items:
811
+ item = item.strip()
812
+ if len(item) > 10: # تجاهل العناصر القصيرة جدًا
813
+ requirements.append({
814
+ "title": self._extract_requirement_title(item),
815
+ "description": item,
816
+ "source": "bullet_list"
817
+ })
818
+
819
+ # استخراج المتطلبات من القوائم المرقمة
820
+ numbered_items = re.findall(r'(\d+)[.)\s]+(.*?)(?:\n\d+[.)\s]|\n\n|\Z)', text, re.DOTALL)
821
+
822
+ for num, item in numbered_items:
823
+ item = item.strip()
824
+ if len(item) > 10: # تجاهل العناصر القصيرة جدًا
825
+ requirements.append({
826
+ "title": self._extract_requirement_title(item),
827
+ "description": item,
828
+ "source": "numbered_list",
829
+ "number": int(num)
830
+ })
831
+
832
+ # استخراج المتطلبات من الفقرات
833
+ if not requirements:
834
+ paragraphs = re.split(r'\n\s*\n', text)
835
+
836
+ for paragraph in paragraphs:
837
+ paragraph = paragraph.strip()
838
+ if len(paragraph) > 50 and len(paragraph) < 500: # تجاهل الفقرات القصيرة جدًا أو الطويلة جدًا
839
+ # تحقق مما إذا كانت الفقرة تحتوي على عبارات المتطلبات
840
+ if any(keyword in paragraph.lower() for keyword in ["يجب", "ضرورة", "إلزامي", "مطلوب", "يلتزم", "لا بد", "لابد", "شرط", "اشتراط"]):
841
+ requirements.append({
842
+ "title": self._extract_requirement_title(paragraph),
843
+ "description": paragraph,
844
+ "source": "paragraph"
845
+ })
846
+
847
+ return requirements
848
+
849
+ import re
850
+
851
+ def _extract_requirement_title(self, text: str) -> str:
852
+ """
853
+ استخراج عنوان المتطلب من النص.
854
+
855
+ المعاملات:
856
+ ----------
857
+ text : str
858
+ نص المتطلب.
859
+
860
+ المخرجات:
861
+ --------
862
+ str
863
+ عنوان المتطلب المستخرج.
864
+ """
865
+ # تنظيف النص وإزالة أي فراغات زائدة
866
+ text = text.strip()
867
+
868
+ # محاولة استخراج العنوان من بداية النص باستخدام الجملة الأولى
869
+ first_sentence = re.split(r'[.!?،؛]', text)[0].strip()
870
+
871
+ # إذا كان النص قصيرًا جدًا، نعيده كما هو
872
+ if len(first_sentence) < 5:
873
+ return text
874
+
875
+ # التحقق من أن العنوان لا يحتوي على أرقام أو رموز غير مفهومة
876
+ if re.search(r'\d', first_sentence):
877
+ return text # إذا كان هناك أرقام، نعيد النص الأصلي
878
+
879
+ return first_sentence
880
+
881
+