Spaces:
Sleeping
Sleeping
Create analysis/requirement_analyzer.py
Browse files- 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 |
+
|