Spaces:
Sleeping
Sleeping
# إذا كانت الجملة الأولى طويلة، اختصرها | |
if len(first_sentence) > 50: | |
if ":" in first_sentence: | |
# استخدام النص قبل العلامة : كعنوان | |
title = first_sentence.split(":")[0].strip() | |
else: | |
# اختصار الجملة الأولى | |
words = first_sentence.split() | |
title = " ".join(words[:7]) + "..." | |
else: | |
title = first_sentence | |
return title | |
def _categorize_requirements(self, requirements: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: | |
""" | |
تصنيف المتطلبات إلى فئات | |
المعاملات: | |
---------- | |
requirements : List[Dict[str, Any]] | |
قائمة المتطلبات | |
المخرجات: | |
-------- | |
Dict[str, List[Dict[str, Any]]] | |
المتطلبات مصنفة حسب الفئة | |
""" | |
categorized = { | |
"technical": [], | |
"financial": [], | |
"legal": [] | |
} | |
for req in requirements: | |
description = req.get("description", "").lower() | |
title = req.get("title", "").lower() | |
text = title + " " + description | |
# تحديد الفئة بناءً على الكلمات المفتاحية | |
if any(keyword in text for keyword in ["فني", "تقني", "هندسي", "مواصفات", "جودة", "معايير", "أداء", "تصميم", "مخطط"]): | |
req["category"] = "technical" | |
categorized["technical"].append(req) | |
elif any(keyword in text for keyword in ["مالي", "سعر", "تكلفة", "دفع", "ضمان", "تأمين", "غرامة", "ميزانية", "ضريبة", "محاسبة"]): | |
req["category"] = "financial" | |
categorized["financial"].append(req) | |
elif any(keyword in text for keyword in ["قانوني", "شرط", "عقد", "التزام", "تعاقد", "نظام", "لائحة", "تشريع", "ترخيص", "حقوق"]): | |
req["category"] = "legal" | |
categorized["legal"].append(req) | |
else: | |
# إذا لم يتطابق مع أي فئة، افترض أنه متطلب فني | |
req["category"] = "technical" | |
categorized["technical"].append(req) | |
return categorized | |
def _analyze_importance_difficulty(self, categorized_requirements: Dict[str, List[Dict[str, Any]]]) -> Dict[str, List[Dict[str, Any]]]: | |
""" | |
تحليل أهمية وصعوبة المتطلبات | |
المعاملات: | |
---------- | |
categorized_requirements : Dict[str, List[Dict[str, Any]]] | |
المتطلبات مصنفة حسب الفئة | |
المخرجات: | |
-------- | |
Dict[str, List[Dict[str, Any]]] | |
المتطلبات بعد تحليل الأهمية والصعوبة | |
""" | |
analyzed = {} | |
for category, reqs in categorized_requirements.items(): | |
analyzed_reqs = [] | |
for req in reqs: | |
# تحليل الأهمية | |
req["importance"] = self._determine_importance(req) | |
# تحليل صعوبة التنفيذ | |
req["difficulty"] = self._determine_difficulty(req) | |
# تحديد الفجوات | |
req["gaps"] = self._identify_gaps(req) | |
# اقتراح تحسينات | |
req["improvements"] = self._suggest_improvements(req) | |
analyzed_reqs.append(req) | |
analyzed[category] = analyzed_reqs | |
return analyzed | |
def _determine_importance(self, requirement: Dict[str, Any]) -> str: | |
""" | |
تحديد أهمية المتطلب | |
المعاملات: | |
---------- | |
requirement : Dict[str, Any] | |
المتطلب | |
المخرجات: | |
-------- | |
str | |
درجة الأهمية | |
""" | |
text = requirement.get("description", "").lower() | |
# البحث عن كلمات تدل على الإلزامية | |
if any(keyword in text for keyword in ["يجب", "إلزامي", "ضروري", "لا بد", "لابد", "مطلوب", "يلتزم", "ملزم"]): | |
return "إلزامي" | |
# البحث عن كلمات تدل على الأهمية الثانوية | |
elif any(keyword in text for keyword in ["يفضل", "مرغوب", "محبذ", "يحبذ", "يستحسن"]): | |
return "ثانوي" | |
# إذا لم يتطابق مع أي حالة، افترض أنه متطلب عادي | |
else: | |
return "عادي" | |
def _determine_difficulty(self, requirement: Dict[str, Any]) -> int: | |
""" | |
تحديد صعوبة تنفيذ المتطلب | |
المعاملات: | |
---------- | |
requirement : Dict[str, Any] | |
المتطلب | |
المخرجات: | |
-------- | |
int | |
درجة الصعوبة (1-5) | |
""" | |
text = requirement.get("description", "").lower() | |
category = requirement.get("category", "") | |
importance = requirement.get("importance", "") | |
# تعيين درجة صعوبة أساسية | |
base_difficulty = 3 # متوسط | |
# زيادة الصعوبة للمتطلبات الإلزامية | |
if importance == "إلزامي": | |
base_difficulty += 1 | |
# زيادة الصعوبة بناءً على طول النص | |
if len(text) > 200: | |
base_difficulty += 0.5 | |
# زيادة الصعوبة بناءً على الكلمات المفتاحية | |
if any(keyword in text for keyword in ["معقد", "صعب", "متقدم", "عالي", "متطور", "خبير", "مخصص", "خاص"]): | |
base_difficulty += 1 | |
# تخفيض الصعوبة للمتطلبات البسيطة | |
if any(keyword in text for keyword in ["بسيط", "سهل", "عادي", "أساسي", "تقليدي"]): | |
base_difficulty -= 1 | |
# ضبط الصعوبة في النطاق 1-5 | |
return max(1, min(5, round(base_difficulty))) | |
def _identify_gaps(self, requirement: Dict[str, Any]) -> str: | |
""" | |
تحديد الفجوات في المتطلب | |
المعاملات: | |
---------- | |
requirement : Dict[str, Any] | |
المتطلب | |
المخرجات: | |
-------- | |
str | |
الفجوات المحددة | |
""" | |
text = requirement.get("description", "") | |
gaps = [] | |
# التحقق من وضوح المتطلب | |
if len(text) < 50: | |
gaps.append("وصف قصير وغير مفصل") | |
# التحقق من وجود معايير قياس | |
if not any(keyword in text.lower() for keyword in ["معيار", "قياس", "مؤشر", "مستوى", "نسبة", "كمية", "عدد"]): | |
gaps.append("لا يحتوي على معايير قياس واضحة") | |
# التحقق من وجود توصيف زمني | |
if not any(keyword in text.lower() for keyword in ["مدة", "فترة", "خلال", "زمن", "تاريخ", "موعد", "يوم", "أسبوع", "شهر"]): | |
gaps.append("لا يحدد إطار زمني") | |
# التحقق من وجود مسؤولية تنفيذ | |
if not any(keyword in text.lower() for keyword in ["مسؤول", "مسؤولية", "مقاول", "مورد", "مقدم", "طرف", "مكلف"]): | |
gaps.append("لا يحدد المسؤولية عن التنفيذ") | |
# إرجاع الفجوات | |
if gaps: | |
return "، ".join(gaps) | |
else: | |
return "لا توجد فجوات واضحة" | |
def _suggest_improvements(self, requirement: Dict[str, Any]) -> str: | |
""" | |
اقتراح تحسينات للمتطلب | |
المعاملات: | |
---------- | |
requirement : Dict[str, Any] | |
المتطلب | |
المخرجات: | |
-------- | |
str | |
التحسينات المقترحة | |
""" | |
gaps = requirement.get("gaps", "") | |
category = requirement.get("category", "") | |
improvements = [] | |
# اقتراح تحسينات بناءً على الفجوات | |
if "وصف قصير" in gaps: | |
improvements.append("توضيح المتطلب بشكل أكثر تفصيلاً") | |
if "معايير قياس" in gaps: | |
improvements.append("إضافة معايير قياس كمية ونوعية") | |
if "إطار زمني" in gaps: | |
improvements.append("تحديد إطار زمني واضح للتنفيذ") | |
if "المسؤولية" in gaps: | |
improvements.append("تحديد المسؤولية عن التنفيذ بوضوح") | |
# اقتراح تحسينات بناءً على الفئة | |
if category == "technical": | |
if not improvements: | |
improvements.append("إضافة مراجع للمعايير الفنية والمواصفات") | |
elif category == "financial": | |
if not improvements: | |
improvements.append("توضيح آلية الدفع والتسعير") | |
elif category == "legal": | |
if not improvements: | |
improvements.append("إضافة المرجعية القانونية والتنظيمية") | |
# إرجاع التحسينات | |
if improvements: | |
return "، ".join(improvements) | |
else: | |
return "المتطلب واضح ومكتمل" | |
def _extract_local_content_requirements(self, text: str) -> List[Dict[str, Any]]: | |
""" | |
استخراج متطلبات المحتوى المحلي | |
المعاملات: | |
---------- | |
text : str | |
النص المستخرج من المناقصة | |
المخرجات: | |
-------- | |
List[Dict[str, Any]] | |
قائمة متطلبات المحتوى المحلي | |
""" | |
# الكلمات المفتاحية للمحتوى المحلي | |
local_content_keywords = [ | |
"المحتوى المحلي", "التوطين", "السعودة", "المنتجات المحلية", | |
"الصناعة المحلية", "نسبة المحتوى", "المكون المحلي", "منشأ محلي", | |
"نطاقات", "توظيف السعوديين", "توظيف المواطنين", "القيمة المضافة المحلية" | |
] | |
local_content_requirements = [] | |
# البحث عن أقسام المحتوى المحلي | |
for keyword in local_content_keywords: | |
pattern = rf"((?:{keyword}|{keyword.upper()}).*?(?:\n\n|\Z))" | |
matches = re.finditer(pattern, text, re.DOTALL) | |
for match in matches: | |
section_text = match.group(1).strip() | |
# استخراج النسب المئوية | |
percentage_matches = re.findall(r'(\d+(?:\.\d+)?)(?:\s*%|\s*في المائة|\s*بالمائة)', section_text) | |
if percentage_matches: | |
for percentage in percentage_matches: | |
requirement = { | |
"title": f"متطلب المحتوى المحلي - {percentage}%", | |
"description": section_text, | |
"category": "local_content", | |
"importance": "إلزامي", | |
"percentage": float(percentage), | |
"difficulty": 4, # افتراضي: صعوبة عالية | |
"gaps": self._identify_gaps({"description": section_text}), | |
"improvements": "توضيح آلية حساب وتوثيق نسبة المحتوى المحلي" | |
} | |
local_content_requirements.append(requirement) | |
else: | |
# إذا لم يتم العثور على نسب، أضف المتطلب بدون نسبة | |
requirement = { | |
"title": "متطلب المحتوى المحلي", | |
"description": section_text, | |
"category": "local_content", | |
"importance": "إلزامي", | |
"difficulty": 3, # افتراضي: صعوبة متوسطة | |
"gaps": "لا يحدد نسبة محتوى محلي واضحة", | |
"improvements": "تحديد نسبة المحتوى المحلي المطلوبة بشكل صريح" | |
} | |
local_content_requirements.append(requirement) | |
# إزالة التكرارات | |
unique_requirements = [] | |
descriptions = set() | |
for req in local_content_requirements: | |
desc = req["description"] | |
if desc not in descriptions: | |
descriptions.add(desc) | |
unique_requirements.append(req) | |
return unique_requirements | |
def _generate_requirements_summary(self, categorized_requirements: Dict[str, List[Dict[str, Any]]], | |
local_content_requirements: List[Dict[str, Any]], | |
total_count: int, mandatory_count: int, avg_difficulty: float) -> str: | |
""" | |
إعداد ملخص المتطلبات | |
المعاملات: | |
---------- | |
categorized_requirements : Dict[str, List[Dict[str, Any]]] | |
المتطلبات مصنفة حسب الفئة | |
local_content_requirements : List[Dict[str, Any]] | |
متطلبات المحتوى المحلي | |
total_count : int | |
إجمالي عدد المتطلبات | |
mandatory_count : int | |
عدد المتطلبات الإلزامية | |
avg_difficulty : float | |
متوسط صعوبة التنفيذ | |
المخرجات: | |
-------- | |
str | |
ملخص المتطلبات | |
""" | |
# إعداد الملخص | |
summary = f"تم تحليل {total_count} متطلب، منها {mandatory_count} متطلب إلزامي. " | |
# توزيع المتطلبات حسب الفئة | |
tech_count = len(categorized_requirements.get("technical", [])) | |
fin_count = len(categorized_requirements.get("financial", [])) | |
legal_count = len(categorized_requirements.get("legal", [])) | |
local_count = len(local_content_requirements) | |
summary += f"تتوزع المتطلبات إلى {tech_count} متطلب فني، و{fin_count} متطلب مالي، و{legal_count} متطلب قانوني، و{local_count} متطلب للمحتوى المحلي. " | |
# صعوبة التنفيذ | |
if avg_difficulty >= 4: | |
summary += "متوسط صعوبة تنفيذ المتطلبات مرتفع، مما يشير إلى تعقيد المشروع. " | |
elif avg_difficulty >= 3: | |
summary += "متوسط صعوبة تنفيذ المتطلبات متوسط. " | |
else: | |
summary += "متوسط صعوبة تنفيذ المتطلبات منخفض، مما يشير إلى إمكانية تنفيذ المشروع بسهولة نسبية. " | |
# متطلبات المحتوى المحلي | |
if local_count > 0: | |
local_percentages = [req.get("percentage", 0) for req in local_content_requirements if "percentage" in req] | |
if local_percentages: | |
max_percentage = max(local_percentages) | |
summary += f"يتطلب المشروع تحقيق نسبة محتوى محلي لا تقل عن {max_percentage}%. " | |
else: | |
summary += "يتضمن المشروع متطلبات للمحتوى المحلي، لكن النسبة المطلوبة غير محددة بوضوح. " | |
else: | |
summary += "لم يتم تحديد متطلبات واضحة للمحتوى المحلي. " | |
# تقييم عام | |
if mandatory_count / total_count > 0.7: | |
summary += "نسبة المتطلبات الإلزامية مرتفعة، مما يشير إلى تشدد في شروط المناقصة. " | |
if avg_difficulty > 3.5 and mandatory_count / total_count > 0.6: | |
summary += "يجب الانتباه إلى ارتفاع صعوبة التنفيذ ونسبة المتطلبات الإلزامية، مما قد يزيد من مخاطر المشروع." | |
return summary | |
def _generate_template_requirements(self) -> List[Dict[str, Any]]: | |
""" | |
إنشاء متطلبات افتراضية من القوالب | |
المخرجات: | |
-------- | |
List[Dict[str, Any]] | |
قائمة المتطلبات الافتراضية | |
""" | |
# إنشاء نسخة من قوالب المتطلبات | |
requirements = [] | |
# إضافة متطلبات من كل فئة | |
for category, templates in self.requirement_templates.items(): | |
for template in templates: | |
requirement = template.copy() | |
requirement["category"] = category | |
requirements.append(requirement) | |
return requirements | |
def _clean_requirements(self, requirements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: | |
""" | |
تنظيف وتوحيد المتطلبات | |
المعاملات: | |
---------- | |
requirements : List[Dict[str, Any]] | |
قائمة المتطلبات | |
المخرجات: | |
-------- | |
List[Dict[str, Any]] | |
قائمة المتطلبات المنظفة | |
""" | |
cleaned = [] | |
descriptions = set() | |
for req in requirements: | |
# تنظيف الوصف | |
if "description" in req: | |
req["description"] = req["description"].strip() | |
# تجاهل المتطلبات المكررة | |
if req.get("description", "") in descriptions: | |
continue | |
# إضافة الوصف إلى مجموعة الأوصاف | |
descriptions.add(req.get("description", "")) | |
# التأكد من وجود عنوان | |
if "title" not in req or not req["title"]: | |
req["title"] = self._extract_requirement_title(req.get("description", "")) | |
# إضافة المتطلب إلى القائمة المنظفة | |
cleaned.append(req) | |
return cleaned | |
def _load_requirement_templates(self) -> Dict[str, List[Dict[str, Any]]]: | |
""" | |
تحميل قوالب المتطلبات | |
المخرجات: | |
-------- | |
Dict[str, List[Dict[str, Any]]] | |
قوالب المتطلبات | |
""" | |
try: | |
file_path = 'data/templates/requirement_templates.json' | |
if os.path.exists(file_path): | |
with open(file_path, 'r', encoding='utf-8') as f: | |
return json.load(f) | |
else: | |
logger.warning(f"ملف قوالب المتطلبات غير موجود: {file_path}") | |
# إنشاء قوالب افتراضية | |
return self._create_default_requirement_templates() | |
except Exception as e: | |
logger.error(f"فشل في تحميل قوالب المتطلبات: {str(e)}") | |
return self._create_default_requirement_templates() | |
def _create_default_requirement_templates(self) -> Dict[str, List[Dict[str, Any]]]: | |
""" | |
إنشاء قوالب متطلبات افتراضية | |
المخرجات: | |
-------- | |
Dict[str, List[Dict[str, Any]]] | |
قوالب المتطلبات الافتراضية | |
""" | |
templates = { | |
"technical": [ | |
{ | |
"title": "مؤهلات وخبرات الفريق الفني", | |
"description": "يجب توفير فريق فني مؤهل ذو خبرة لا تقل عن 5 سنوات في مجال المشروع، مع تقديم السير الذاتية للمهندسين الرئيسيين.", | |
"importance": "إلزامي", | |
"difficulty": 3 | |
}, | |
{ | |
"title": "جودة المواد والمعدات", | |
"description": "يجب استخدام مواد ومعدات ذات جودة عالية ومطابقة للمواصفات القياسية السعودية والعالمية، مع تقديم شهادات الجودة والمنشأ.", | |
"importance": "إلزامي", | |
"difficulty": 4 | |
}, | |
{ | |
"title": "خطة العمل والجدول الزمني", | |
"description": "تقديم خطة عمل تفصيلية وجدول زمني لتنفيذ المشروع، موضحاً المراحل الرئيسية والمدد الزمنية لكل مرحلة.", | |
"importance": "إلزامي", | |
"difficulty": 3 | |
}, | |
{ | |
"title": "ضمان الأعمال", | |
"description": "تقديم ضمان للأعمال المنفذة لمدة لا تقل عن سنة من تاريخ الاستلام النهائي، مع الالتزام بإصلاح أي عيوب خلال فترة الضمان.", | |
"importance": "إلزامي", | |
"difficulty": 2 | |
}, | |
{ | |
"title": "الصيانة الدورية", | |
"description": "يفضل تقديم برنامج للصيانة الدورية بعد انتهاء فترة الضمان، مع تحديد التكاليف والخدمات المشمولة.", | |
"importance": "ثانوي", | |
"difficulty": 2 | |
} | |
], | |
"financial": [ | |
{ | |
"title": "العرض المالي", | |
"description": "تقديم عرض مالي مفصل يوضح تكاليف كل بند من بنود المشروع، مع بيان القيمة الإجمالية شاملة ضريبة القيمة المضافة.", | |
"importance": "إلزامي", | |
"difficulty": 3 | |
}, | |
{ | |
"title": "الضمان الابتدائي", | |
"description": "تقديم ضمان ابتدائي بنسبة 2% من قيمة العرض، ساري المفعول لمدة 90 يوماً من تاريخ فتح المظاريف.", | |
"importance": "إلزامي", | |
"difficulty": 2 | |
}, | |
{ | |
"title": "الضمان النهائي", | |
"description": "تقديم ضمان نهائي بنسبة 5% من قيمة العقد عند الترسية، ساري المفعول حتى انتهاء فترة الضمان.", | |
"importance": "إلزامي", | |
"difficulty": 2 | |
}, | |
{ | |
"title": "شروط الدفع", | |
"description": "يتم الدفع على دفعات مرحلية حسب نسب الإنجاز، مع احتجاز نسبة 10% من كل دفعة كضمان حسن التنفيذ.", | |
"importance": "إلزامي", | |
"difficulty": 2 | |
}, | |
{ | |
"title": "غرامات التأخير", | |
"description": "تفرض غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير، بحد أقصى 10% من القيمة الإجمالية للعقد.", | |
"importance": "إلزامي", | |
"difficulty": 3 | |
} | |
], | |
"legal": [ | |
{ | |
"title": "السجل التجاري", | |
"description": "يجب أن يكون المتقدم حاصلاً على سجل تجاري ساري المفعول في نفس مجال المناقصة.", | |
"importance": "إلزامي", | |
"difficulty": 1 | |
}, | |
{ | |
"title": "تصنيف المقاولين", | |
"description": "يجب أن يكون المقاول مصنفاً لدى وزارة الشؤون البلدية والقروية والإسكان في المجال المطلوب وبدرجة لا تقل عن الثالثة.", | |
"importance": "إلزامي", | |
"difficulty": 2 | |
}, | |
{ | |
"title": "التأمينات", | |
"description": "تقديم شهادة من المؤسسة العامة للتأمينات الاجتماعية تثبت الوفاء بالالتزامات تجاه المؤسسة.", | |
"importance": "إلزامي", | |
"difficulty": 1 | |
}, | |
{ | |
"title": "الزكاة والدخل", | |
"description": "تقديم شهادة من هيئة الزكاة والضريبة والجمارك تثبت سداد المستحقات.", | |
"importance": "إلزامي", | |
"difficulty": 1 | |
}, | |
{ | |
"title": "نظام المنافسات والمشتريات", | |
"description": "الالتزام بأحكام نظام المنافسات والمشتريات الحكومية ولائحته التنفيذية.", | |
"importance": "إلزامي", | |
"difficulty": 2 | |
} | |
] | |
} | |
return templates""" | |
محلل متطلبات المناقصات | |
يقوم باستخراج وتحليل وتصنيف متطلبات المناقصات | |
""" | |
import re | |
import logging | |
import json | |
import os | |
from typing import Dict, List, Any, Tuple, Optional, Union | |
import numpy as np | |
logger = logging.getLogger(__name__) | |
class RequirementAnalyzer: | |
""" | |
محلل متطلبات المناقصات | |
""" | |
def __init__(self, model_loader, arabic_nlp, config=None): | |
""" | |
تهيئة محلل المتطلبات | |
المعاملات: | |
---------- | |
model_loader : ModelLoader | |
محمّل النماذج المستخدمة للتحليل | |
arabic_nlp : ArabicNLP | |
أدوات معالجة اللغة العربية | |
config : Dict, optional | |
إعدادات المحلل | |
""" | |
self.config = config or {} | |
self.model_loader = model_loader | |
self.arabic_nlp = arabic_nlp | |
# تحميل النماذج | |
self.ner_model = None | |
if hasattr(model_loader, 'get_ner_model'): | |
try: | |
self.ner_model = model_loader.get_ner_model() | |
logger.info("تم تحميل نموذج التعرف على الكيانات المسماة") | |
except Exception as e: | |
logger.warning(f"فشل في تحميل نموذج التعرف على الكيانات المسماة: {str(e)}") | |
# تحميل قوالب المتطلبات | |
self.requirement_templates = self._load_requirement_templates() | |
logger.info("تم تهيئة محلل المتطلبات") | |
def analyze(self, text: str) -> Dict[str, Any]: | |
""" | |
تحليل متطلبات المناقصة من النص | |
المعاملات: | |
---------- | |
text : str | |
النص المستخرج من المناقصة | |
المخرجات: | |
-------- | |
Dict[str, Any] | |
نتائج تحليل المتطلبات | |
""" | |
try: | |
logger.info("بدء تحليل متطلبات المناقصة") | |
# استخراج المتطلبات من النص | |
requirements = self._extract_requirements(text) | |
# تصنيف المتطلبات | |
categorized_requirements = self._categorize_requirements(requirements) | |
# تحليل الأهمية والصعوبة | |
analyzed_requirements = self._analyze_importance_difficulty(categorized_requirements) | |
# تحليل متطلبات المحتوى المحلي | |
local_content_requirements = self._extract_local_content_requirements(text) | |
# تحديد عدد المتطلبات الإلزامية | |
mandatory_count = sum(1 for req in requirements if req.get("importance", "") == "إلزامي") | |
# حساب متوسط صعوبة التنفيذ | |
difficulty_values = [req.get("difficulty", 3) for req in requirements if req.get("difficulty", 0) > 0] | |
avg_difficulty = np.mean(difficulty_values) if difficulty_values else 3.0 | |
# حساب نسبة متطلبات المحتوى المحلي | |
total_count = len(requirements) | |
local_content_percentage = (len(local_content_requirements) / total_count * 100) if total_count > 0 else 0 | |
# إعداد ملخص المتطلبات | |
summary = self._generate_requirements_summary( | |
analyzed_requirements, local_content_requirements, total_count, mandatory_count, avg_difficulty | |
) | |
# إعداد النتائج | |
results = { | |
"summary": summary, | |
"technical": analyzed_requirements.get("technical", []), | |
"financial": analyzed_requirements.get("financial", []), | |
"legal": analyzed_requirements.get("legal", []), | |
"local_content": local_content_requirements, | |
"total_count": total_count, | |
"mandatory_count": mandatory_count, | |
"avg_difficulty": avg_difficulty, | |
"local_content_percentage": local_content_percentage | |
} | |
logger.info(f"اكتمل تحليل المتطلبات: {total_count} متطلبات، {mandatory_count} إلزامية") | |
return results | |
except Exception as e: | |
logger.error(f"فشل في تحليل المتطلبات: {str(e)}") | |
return { | |
"summary": "حدث خطأ أثناء تحليل المتطلبات", | |
"technical": [], | |
"financial": [], | |
"legal": [], | |
"local_content": [], | |
"total_count": 0, | |
"mandatory_count": 0, | |
"avg_difficulty": 0, | |
"local_content_percentage": 0, | |
"error": str(e) | |
} | |
def _extract_requirements(self, text: str) -> List[Dict[str, Any]]: | |
""" | |
استخراج المتطلبات من النص | |
المعاملات: | |
---------- | |
text : str | |
النص المستخرج من المناقصة | |
المخرجات: | |
-------- | |
List[Dict[str, Any]] | |
قائمة المتطلبات المستخرجة | |
""" | |
requirements = [] | |
# البحث عن قسم المتطلبات أو الشروط | |
requirement_sections = self._find_requirement_sections(text) | |
# إذا لم يتم العثور على أقسام للمتطلبات، استخدم النص كاملاً | |
if not requirement_sections: | |
requirement_sections = [text] | |
# معالجة كل قسم | |
for section in requirement_sections: | |
# استخراج المتطلبات من النص | |
section_requirements = self._parse_requirements_text(section) | |
# دمج المتطلبات المستخرجة | |
requirements.extend(section_requirements) | |
# إذا لم يتم العثور على متطلبات، استخدم القوالب | |
if not requirements: | |
requirements = self._generate_template_requirements() | |
# تنظيف وتوحيد المتطلبات | |
requirements = self._clean_requirements(requirements) | |
return requirements | |
def _find_requirement_sections(self, text: str) -> List[str]: | |
""" | |
البحث عن أقسام المتطلبات في النص | |
المعاملات: | |
---------- | |
text : str | |
النص المستخرج من المناقصة | |
المخرجات: | |
-------- | |
List[str] | |
أقسام المتطلبات المستخرجة | |
""" | |
sections = [] | |
# الكلمات المفتاحية لأقسام المتطلبات | |
section_keywords = [ | |
"المتطلبات", "الشروط", "المواصفات", "نطاق العمل", | |
"البنود", "المعايير", "الالتزامات", "المؤهلات", | |
"التأهيل", "الواجبات", "نطاق الأعمال", "الخدمات المطلوبة" | |
] | |
# البحث عن أقسام المتطلبات | |
for keyword in section_keywords: | |
pattern = rf"((?:{keyword}|{keyword.upper()})[:\s].*?)(?:^(?:{section_keywords[0]}|{section_keywords[0].upper()})[:\s]|\Z)" | |
matches = re.finditer(pattern, text, re.MULTILINE | re.DOTALL) | |
for match in matches: | |
section_text = match.group(1).strip() | |
if len(section_text) > 50: # تجاهل الأقسام القصيرة جدًا | |
sections.append(section_text) | |
# البحث عن قوائم البنود | |
bullet_lists = re.findall(r'(?:^|\n)(?:[•\-*]\s+.*(?:\n|$))+', text, re.MULTILINE) | |
for bullet_list in bullet_lists: | |
if len(bullet_list) > 100: # تجاهل القوائم القصيرة | |
sections.append(bullet_list) | |
# البحث عن قوائم مرقمة | |
numbered_lists = re.findall(r'(?:^|\n)(?:\d+[.)\s]+.*(?:\n|$))+', text, re.MULTILINE) | |
for numbered_list in numbered_lists: | |
if len(numbered_list) > 100: # تجاهل القوائم القصيرة | |
sections.append(numbered_list) | |
return sections | |
def _parse_requirements_text(self, text: str) -> List[Dict[str, Any]]: | |
""" | |
تحليل نص المتطلبات لاستخراج المتطلبات الفردية | |
المعاملات: | |
---------- | |
text : str | |
نص قسم المتطلبات | |
المخرجات: | |
-------- | |
List[Dict[str, Any]] | |
قائمة المتطلبات المستخرجة | |
""" | |
requirements = [] | |
# استخراج المتطلبات من القوائم النقطية | |
bullet_items = re.findall(r'[•\-*]\s+(.*?)(?:\n[•\-*]|\n\n|\Z)', text, re.DOTALL) | |
for item in bullet_items: | |
item = item.strip() | |
if len(item) > 10: # تجاهل العناصر القصيرة جدًا | |
requirements.append({ | |
"title": self._extract_requirement_title(item), | |
"description": item, | |
"source": "bullet_list" | |
}) | |
# استخراج المتطلبات من القوائم المرقمة | |
numbered_items = re.findall(r'(\d+)[.)\s]+(.*?)(?:\n\d+[.)\s]|\n\n|\Z)', text, re.DOTALL) | |
for num, item in numbered_items: | |
item = item.strip() | |
if len(item) > 10: # تجاهل العناصر القصيرة جدًا | |
requirements.append({ | |
"title": self._extract_requirement_title(item), | |
"description": item, | |
"source": "numbered_list", | |
"number": int(num) | |
}) | |
# استخراج المتطلبات من الفقرات | |
if not requirements: | |
paragraphs = re.split(r'\n\s*\n', text) | |
for paragraph in paragraphs: | |
paragraph = paragraph.strip() | |
if len(paragraph) > 50 and len(paragraph) < 500: # تجاهل الفقرات القصيرة جدًا أو الطويلة جدًا | |
# تحقق مما إذا كانت الفقرة تحتوي على عبارات المتطلبات | |
if any(keyword in paragraph.lower() for keyword in ["يجب", "ضرورة", "إلزامي", "مطلوب", "يلتزم", "لا بد", "لابد", "شرط", "اشتراط"]): | |
requirements.append({ | |
"title": self._extract_requirement_title(paragraph), | |
"description": paragraph, | |
"source": "paragraph" | |
}) | |
return requirements | |
import re | |
def _extract_requirement_title(self, text: str) -> str: | |
""" | |
استخراج عنوان المتطلب من النص. | |
المعاملات: | |
---------- | |
text : str | |
نص المتطلب. | |
المخرجات: | |
-------- | |
str | |
عنوان المتطلب المستخرج. | |
""" | |
# تنظيف النص وإزالة أي فراغات زائدة | |
text = text.strip() | |
# محاولة استخراج العنوان من بداية النص باستخدام الجملة الأولى | |
first_sentence = re.split(r'[.!?،؛]', text)[0].strip() | |
# إذا كان النص قصيرًا جدًا، نعيده كما هو | |
if len(first_sentence) < 5: | |
return text | |
# التحقق من أن العنوان لا يحتوي على أرقام أو رموز غير مفهومة | |
if re.search(r'\d', first_sentence): | |
return text # إذا كان هناك أرقام، نعيد النص الأصلي | |
return first_sentence | |