EGYADMIN commited on
Commit
4c143b5
·
verified ·
1 Parent(s): c258a55

Upload 10 files

Browse files
modules/ai_assistant/ai_app.py CHANGED
The diff for this file is too large to render. See raw diff
 
modules/document_analysis/analyzer.py CHANGED
@@ -9,10 +9,6 @@ import threading
9
  from pathlib import Path
10
  import datetime
11
  import json
12
- import base64
13
- import time
14
- from PIL import Image
15
- import io
16
 
17
  # تهيئة السجل
18
  logging.basicConfig(
@@ -23,33 +19,33 @@ logger = logging.getLogger('document_analysis')
23
 
24
  class DocumentAnalyzer:
25
  """فئة تحليل المستندات"""
26
-
27
  def __init__(self, config=None):
28
  """تهيئة محلل المستندات"""
29
  self.config = config
30
  self.analysis_in_progress = False
31
  self.current_document = None
32
  self.analysis_results = {}
33
-
34
  # إنشاء مجلد المستندات إذا لم يكن موجوداً
35
  if config and hasattr(config, 'DOCUMENTS_PATH'):
36
  self.documents_path = Path(config.DOCUMENTS_PATH)
37
  else:
38
  self.documents_path = Path('data/documents')
39
-
40
  if not self.documents_path.exists():
41
  self.documents_path.mkdir(parents=True, exist_ok=True)
42
-
43
  def analyze_document(self, document_path, document_type="tender", callback=None):
44
  """تحليل مستند"""
45
  if self.analysis_in_progress:
46
  logger.warning("هناك عملية تحليل جارية بالفعل")
47
  return False
48
-
49
  if not os.path.exists(document_path):
50
  logger.error(f"المستند غير موجود: {document_path}")
51
  return False
52
-
53
  self.analysis_in_progress = True
54
  self.current_document = document_path
55
  self.analysis_results = {
@@ -63,7 +59,7 @@ class DocumentAnalyzer:
63
  "amounts": [],
64
  "risks": []
65
  }
66
-
67
  # بدء التحليل في خيط منفصل
68
  thread = threading.Thread(
69
  target=self._analyze_document_thread,
@@ -71,15 +67,15 @@ class DocumentAnalyzer:
71
  )
72
  thread.daemon = True
73
  thread.start()
74
-
75
  return True
76
-
77
  def _analyze_document_thread(self, document_path, document_type, callback):
78
  """خيط تحليل المستند"""
79
  try:
80
  # تحديد نوع المستند
81
  file_extension = os.path.splitext(document_path)[1].lower()
82
-
83
  if file_extension == '.pdf':
84
  self.analysis_results = self._analyze_pdf(document_path, document_type)
85
  elif file_extension == '.docx':
@@ -92,38 +88,34 @@ class DocumentAnalyzer:
92
  logger.error(f"نوع المستند غير مدعوم: {file_extension}")
93
  self.analysis_results["status"] = "فشل التحليل"
94
  self.analysis_results["error"] = "نوع المستند غير مدعوم"
95
-
96
  # تحديث حالة التحليل
97
  if self.analysis_results["status"] != "فشل التحليل":
98
  self.analysis_results["status"] = "اكتمل التحليل"
99
  self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
100
-
101
  logger.info(f"اكتمل تحليل المستند: {document_path}")
102
-
103
  except Exception as e:
104
  logger.error(f"خطأ في تحليل المستند: {str(e)}")
105
  self.analysis_results["status"] = "فشل التحليل"
106
  self.analysis_results["error"] = str(e)
107
-
108
  finally:
109
  self.analysis_in_progress = False
110
-
111
  # استدعاء دالة الاستجابة إذا تم توفيرها
112
  if callback and callable(callback):
113
  callback(self.analysis_results)
114
-
115
  def _analyze_pdf(self, document_path, document_type):
116
  """تحليل مستند PDF باستخدام الذكاء الاصطناعي"""
117
  try:
118
  # استخراج النص من PDF
119
  text = self._extract_text_from_pdf(document_path)
120
-
121
  # تحليل متقدم للمستند
122
  analysis = {
123
- "document_path": document_path,
124
- "document_type": document_type,
125
- "analysis_start_time": self.analysis_results["analysis_start_time"],
126
- "status": "جاري التحليل",
127
  "file_info": {
128
  "name": os.path.basename(document_path),
129
  "type": "PDF",
@@ -173,1519 +165,116 @@ class DocumentAnalyzer:
173
  except Exception as e:
174
  logger.error(f"خطأ في تحليل PDF: {str(e)}")
175
  raise
176
-
177
- def extract_document_metadata(self, document_path):
178
- """استخراج البيانات الوصفية للمستند"""
179
- try:
180
- # تحديد نوع المستند
181
- file_extension = os.path.splitext(document_path)[1].lower()
182
-
183
- metadata = {
184
- "filename": os.path.basename(document_path),
185
- "file_type": file_extension.replace('.', '').upper(),
186
- "file_size": os.path.getsize(document_path),
187
- "creation_date": "غير متوفر",
188
- "modification_date": time.ctime(os.path.getmtime(document_path)),
189
- "author": "غير متوفر",
190
- "title": "غير متوفر"
191
- }
192
-
193
- # استخراج البيانات الوصفية حسب نوع المستند
194
- if file_extension == '.pdf':
195
- pdf_metadata = self._extract_pdf_metadata(document_path)
196
- metadata.update(pdf_metadata)
197
- elif file_extension == '.docx':
198
- docx_metadata = self._extract_docx_metadata(document_path)
199
- metadata.update(docx_metadata)
200
- elif file_extension == '.xlsx':
201
- xlsx_metadata = self._extract_xlsx_metadata(document_path)
202
- metadata.update(xlsx_metadata)
203
-
204
- return metadata
205
-
206
- except Exception as e:
207
- logger.error(f"خطأ في استخراج البيانات الوصفية: {str(e)}")
208
- return None
209
-
210
- def _extract_pdf_metadata(self, document_path):
211
- """استخراج البيانات الوصفية من ملف PDF"""
212
- try:
213
- import PyPDF2
214
-
215
- metadata = {}
216
-
217
- with open(document_path, 'rb') as file:
218
- reader = PyPDF2.PdfReader(file)
219
-
220
- # استخراج البيانات الوصفية المتاحة
221
- if reader.metadata:
222
- if '/Title' in reader.metadata:
223
- metadata["title"] = reader.metadata['/Title']
224
- if '/Author' in reader.metadata:
225
- metadata["author"] = reader.metadata['/Author']
226
- if '/CreationDate' in reader.metadata:
227
- metadata["creation_date"] = reader.metadata['/CreationDate']
228
- if '/ModDate' in reader.metadata:
229
- metadata["modification_date"] = reader.metadata['/ModDate']
230
- if '/Producer' in reader.metadata:
231
- metadata["producer"] = reader.metadata['/Producer']
232
- if '/Creator' in reader.metadata:
233
- metadata["creator"] = reader.metadata['/Creator']
234
-
235
- # إضافة عدد الصفحات
236
- metadata["pages"] = len(reader.pages)
237
-
238
- return metadata
239
-
240
- except Exception as e:
241
- logger.error(f"خطأ في استخراج البيانات الوصفية من PDF: {str(e)}")
242
- return {}
243
-
244
- def compare_documents(self, document_path1, document_path2):
245
- """مقارنة مستندين"""
246
- try:
247
- # تحليل المستندين
248
- self.analyze_document(document_path1)
249
- analysis1 = self.get_analysis_results()
250
-
251
- self.analyze_document(document_path2)
252
- analysis2 = self.get_analysis_results()
253
-
254
- # مقارنة نتائج التحليل
255
- comparison = {
256
- "document1": {
257
- "path": document_path1,
258
- "file_info": analysis1.get("file_info", {})
259
- },
260
- "document2": {
261
- "path": document_path2,
262
- "file_info": analysis2.get("file_info", {})
263
- },
264
- "differences": self._find_document_differences(analysis1, analysis2),
265
- "similarity_score": self._calculate_similarity_score(analysis1, analysis2)
266
- }
267
-
268
- return comparison
269
-
270
- except Exception as e:
271
- logger.error(f"خطأ في مقارنة المستندات: {str(e)}")
272
- return None
273
-
274
- def _find_document_differences(self, analysis1, analysis2):
275
- """العثور على الاختلافات بين تحليلين"""
276
- differences = {}
277
-
278
- # مقارنة البنود
279
- if "items" in analysis1 and "items" in analysis2:
280
- items1 = set(item["name"] for item in analysis1["items"] if "name" in item)
281
- items2 = set(item["name"] for item in analysis2["items"] if "name" in item)
282
-
283
- differences["items"] = {
284
- "only_in_doc1": list(items1 - items2),
285
- "only_in_doc2": list(items2 - items1),
286
- "common": list(items1.intersection(items2))
287
- }
288
-
289
- # مقارنة الكيانات
290
- if "entities" in analysis1 and "entities" in analysis2:
291
- entities1 = set(entity for entity in analysis1["entities"])
292
- entities2 = set(entity for entity in analysis2["entities"])
293
-
294
- differences["entities"] = {
295
- "only_in_doc1": list(entities1 - entities2),
296
- "only_in_doc2": list(entities2 - entities1),
297
- "common": list(entities1.intersection(entities2))
298
- }
299
-
300
- # مقارنة التواريخ
301
- if "dates" in analysis1 and "dates" in analysis2:
302
- dates1 = set(date for date in analysis1["dates"])
303
- dates2 = set(date for date in analysis2["dates"])
304
-
305
- differences["dates"] = {
306
- "only_in_doc1": list(dates1 - dates2),
307
- "only_in_doc2": list(dates2 - dates1),
308
- "common": list(dates1.intersection(dates2))
309
- }
310
-
311
- # مقارنة المبالغ
312
- if "amounts" in analysis1 and "amounts" in analysis2:
313
- amounts1 = set(amount["amount"] for amount in analysis1["amounts"] if "amount" in amount)
314
- amounts2 = set(amount["amount"] for amount in analysis2["amounts"] if "amount" in amount)
315
-
316
- differences["amounts"] = {
317
- "only_in_doc1": list(amounts1 - amounts2),
318
- "only_in_doc2": list(amounts2 - amounts1),
319
- "common": list(amounts1.intersection(amounts2))
320
- }
321
-
322
- return differences
323
-
324
- def _calculate_similarity_score(self, analysis1, analysis2):
325
- """حساب درجة التشابه بين تحليلين"""
326
- # محاكاة بسيطة لحساب درجة التشابه
327
- similarity_score = 0
328
- total_factors = 0
329
-
330
- # التشابه في البنود
331
- if "items" in analysis1 and "items" in analysis2:
332
- items1 = set(item["name"] for item in analysis1["items"] if "name" in item)
333
- items2 = set(item["name"] for item in analysis2["items"] if "name" in item)
334
-
335
- if items1 or items2: # تجنب القسمة على صفر
336
- similarity_score += len(items1.intersection(items2)) / max(len(items1.union(items2)), 1)
337
- total_factors += 1
338
-
339
- # التشابه في الكيانات
340
- if "entities" in analysis1 and "entities" in analysis2:
341
- entities1 = set(entity for entity in analysis1["entities"])
342
- entities2 = set(entity for entity in analysis2["entities"])
343
-
344
- if entities1 or entities2:
345
- similarity_score += len(entities1.intersection(entities2)) / max(len(entities1.union(entities2)), 1)
346
- total_factors += 1
347
-
348
- # التشابه في التواريخ
349
- if "dates" in analysis1 and "dates" in analysis2:
350
- dates1 = set(date for date in analysis1["dates"])
351
- dates2 = set(date for date in analysis2["dates"])
352
-
353
- if dates1 or dates2:
354
- similarity_score += len(dates1.intersection(dates2)) / max(len(dates1.union(dates2)), 1)
355
- total_factors += 1
356
-
357
- # التشابه في المبالغ
358
- if "amounts" in analysis1 and "amounts" in analysis2:
359
- amounts1 = set(amount["amount"] for amount in analysis1["amounts"] if "amount" in amount)
360
- amounts2 = set(amount["amount"] for amount in analysis2["amounts"] if "amount" in amount)
361
-
362
- if amounts1 or amounts2:
363
- similarity_score += len(amounts1.intersection(amounts2)) / max(len(amounts1.union(amounts2)), 1)
364
- total_factors += 1
365
-
366
- # حساب المتوسط
367
- if total_factors > 0:
368
- similarity_percentage = (similarity_score / total_factors) * 100
369
- return round(similarity_percentage, 2)
370
- else:
371
- return 0.0
372
-
373
- def generate_report(self, analysis_results=None, report_format="html"):
374
- """توليد تقرير من نتائج التحليل"""
375
- try:
376
- # استخدام نتائج التحليل الحالية إذا لم يتم توفير نتائج
377
- if analysis_results is None:
378
- analysis_results = self.analysis_results
379
-
380
- if not analysis_results:
381
- logger.warning("لا توجد نتائج تحليل لتوليد تقرير")
382
- return None
383
-
384
- # توليد التقرير حسب الصيغة المطلوبة
385
- if report_format.lower() == "html":
386
- return self._generate_html_report(analysis_results)
387
- elif report_format.lower() == "pdf":
388
- return self._
389
-
390
- def _extract_docx_metadata(self, document_path):
391
- """استخراج البيانات الوصفية من ملف Word"""
392
- try:
393
- import docx
394
-
395
- metadata = {}
396
-
397
- doc = docx.Document(document_path)
398
-
399
- # استخراج البيانات الوصفية المتاحة
400
- core_properties = doc.core_properties
401
-
402
- if core_properties.title:
403
- metadata["title"] = core_properties.title
404
- if core_properties.author:
405
- metadata["author"] = core_properties.author
406
- if core_properties.created:
407
- metadata["creation_date"] = str(core_properties.created)
408
- if core_properties.modified:
409
- metadata["modification_date"] = str(core_properties.modified)
410
- if core_properties.last_modified_by:
411
- metadata["last_modified_by"] = core_properties.last_modified_by
412
- if core_properties.revision:
413
- metadata["revision"] = core_properties.revision
414
-
415
- # إضافة عدد الصفحات (تقريبي)
416
- text_length = sum(len(paragraph.text) for paragraph in doc.paragraphs)
417
- estimated_pages = max(1, text_length // 3000)
418
- metadata["pages"] = estimated_pages
419
-
420
- return metadata
421
-
422
- except Exception as e:
423
- logger.error(f"خطأ في استخراج البيانات الوصفية من Word: {str(e)}")
424
- return {}
425
-
426
- def _extract_xlsx_metadata(self, document_path):
427
- """استخراج البيانات الوصفية من ملف Excel"""
428
- try:
429
- import openpyxl
430
-
431
- metadata = {}
432
-
433
- workbook = openpyxl.load_workbook(document_path, read_only=True)
434
-
435
- # استخراج البيانات الوصفية المتاحة
436
- if workbook.properties:
437
- if workbook.properties.title:
438
- metadata["title"] = workbook.properties.title
439
- if workbook.properties.creator:
440
- metadata["author"] = workbook.properties.creator
441
- if workbook.properties.created:
442
- metadata["creation_date"] = str(workbook.properties.created)
443
- if workbook.properties.modified:
444
- metadata["modification_date"] = str(workbook.properties.modified)
445
- if workbook.properties.lastModifiedBy:
446
- metadata["last_modified_by"] = workbook.properties.lastModifiedBy
447
- if workbook.properties.revision:
448
- metadata["revision"] = workbook.properties.revision
449
-
450
- # إضافة عدد الأوراق
451
- metadata["sheets"] = len(workbook.sheetnames)
452
- metadata["sheet_names"] = workbook.sheetnames
453
-
454
- return metadata
455
-
456
- except Exception as e:
457
- logger.error(f"خطأ في استخراج البيانات الوصفية من Excel: {str(e)}")
458
- return {}
459
 
460
  def _extract_text_from_pdf(self, document_path):
461
- """استخراج النص من ملف PDF"""
462
- try:
463
- import PyPDF2
464
- text = ""
465
- with open(document_path, 'rb') as file:
466
- reader = PyPDF2.PdfReader(file)
467
- for page in reader.pages:
468
- text += page.extract_text() + "\n"
469
- return text
470
- except Exception as e:
471
- logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
472
- raise
473
 
474
  def _analyze_contract_terms(self, text):
475
  """تحليل بنود العقد"""
476
- terms = []
477
- sections = text.split('\n\n')
478
- for section in sections:
479
- if any(keyword in section.lower() for keyword in ['شروط', 'بند', 'يلتزم', 'يجب']):
480
- terms.append(section.strip())
481
- return terms
482
 
483
  def _analyze_financial_terms(self, text):
484
  """تحليل الجزء المالي"""
485
- financial_terms = []
486
-
487
- # البحث عن الأقسام المالية
488
- sections = text.split('\n\n')
489
- for section in sections:
490
- if any(keyword in section.lower() for keyword in ['مالي', 'تكلفة', 'سعر', 'ميزانية', 'دفع']):
491
- financial_terms.append(section.strip())
492
-
493
- # استخراج المبالغ المالية
494
- amounts = self._extract_monetary_amounts(text)
495
-
496
- return {
497
- "sections": financial_terms,
498
- "amounts": amounts,
499
- "payment_terms": self._extract_payment_terms(text),
500
- "budget_allocation": self._extract_budget_allocation(text)
501
- }
502
-
503
- def _extract_monetary_amounts(self, text):
504
- """استخراج المبالغ المالية من النص"""
505
- import re
506
- # نمط للبحث عن المبالغ المالية بالريال السعودي والدولار الأمريكي
507
- pattern = r'(\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*(?:ريال|دولار|SAR|USD|ر\.س|\$)'
508
- matches = re.findall(pattern, text)
509
- return [float(amount.replace(',', '')) for amount in matches]
510
-
511
- def _extract_payment_terms(self, text):
512
- """استخراج شروط الدفع"""
513
- payment_terms = []
514
- sections = text.split('\n\n')
515
- for section in sections:
516
- if any(keyword in section.lower() for keyword in ['دفع', 'سداد', 'أقساط', 'مستحقات']):
517
- payment_terms.append(section.strip())
518
- return payment_terms
519
-
520
- def _extract_budget_allocation(self, text):
521
- """استخراج تخصيص الميزانية"""
522
- # هذه وظيفة بسيطة لاستخراج تخصيص الميزانية
523
- # في التطبيق الحقيقي، قد تحتاج إلى تحليل أكثر تعقيدًا
524
- budget_items = []
525
- sections = text.split('\n\n')
526
- for section in sections:
527
- if any(keyword in section.lower() for keyword in ['ميزانية', 'تخصيص', 'تمويل']):
528
- budget_items.append(section.strip())
529
- return budget_items
530
 
531
  def _analyze_legal_terms(self, text):
532
  """تحليل القانوني للعقد"""
533
- legal_terms = []
534
-
535
- # البحث عن الأقسام القانونية
536
- sections = text.split('\n\n')
537
- for section in sections:
538
- if any(keyword in section.lower() for keyword in ['قانون', 'تشريع', 'نظام', 'حكم', 'قضاء', 'محكمة']):
539
- legal_terms.append(section.strip())
540
-
541
- return {
542
- "sections": legal_terms,
543
- "liability_clauses": self._extract_liability_clauses(text),
544
- "dispute_resolution": self._extract_dispute_resolution(text),
545
- "legal_references": self._extract_legal_references(text)
546
- }
547
-
548
- def _extract_liability_clauses(self, text):
549
- """استخراج بنود المسؤولية"""
550
- liability_clauses = []
551
- sections = text.split('\n\n')
552
- for section in sections:
553
- if any(keyword in section.lower() for keyword in ['مسؤولية', 'التزام', 'ضمان', 'تعويض']):
554
- liability_clauses.append(section.strip())
555
- return liability_clauses
556
 
557
- def _extract_dispute_resolution(self, text):
558
- """استخراج آلية فض النزاعات"""
559
- dispute_clauses = []
560
- sections = text.split('\n\n')
561
- for section in sections:
562
- if any(keyword in section.lower() for keyword in ['نزاع', 'خلاف', 'تحكيم', 'قضاء', 'تسوية']):
563
- dispute_clauses.append(section.strip())
564
- return dispute_clauses
565
-
566
- def _extract_legal_references(self, text):
567
- """استخراج المراجع القانونية"""
568
- import re
569
- # نمط للبحث عن المراجع القانونية مثل أرقام القوانين واللوائح
570
- pattern = r'قانون رقم \d+|لائحة \d+|نظام \d+|مرسوم \d+'
571
- return re.findall(pattern, text)
572
-
573
  def _analyze_risks(self, text):
574
  """تحليل المخاطر"""
575
- risk_factors = []
576
-
577
- # البحث عن الأقسام المتعلقة بالمخاطر
578
- sections = text.split('\n\n')
579
- for section in sections:
580
- if any(keyword in section.lower() for keyword in ['مخاطر', 'خطر', 'تهديد', 'ضرر', 'إخلال']):
581
- risk_factors.append(section.strip())
582
-
583
- # تصنيف المخاطر
584
- risk_categories = {
585
- "financial_risks": self._extract_financial_risks(text),
586
- "operational_risks": self._extract_operational_risks(text),
587
- "legal_risks": self._extract_legal_risks(text),
588
- "technical_risks": self._extract_technical_risks(text)
589
- }
590
-
591
- # تقييم شدة المخاطر
592
- risk_severity = self._assess_risk_severity(risk_factors)
593
-
594
- return {
595
- "risk_factors": risk_factors,
596
- "risk_categories": risk_categories,
597
- "risk_severity": risk_severity,
598
- "mitigation_suggestions": self._suggest_risk_mitigation(risk_factors)
599
- }
600
-
601
- def _extract_financial_risks(self, text):
602
- """استخراج المخاطر المالية"""
603
- financial_risks = []
604
- sections = text.split('\n\n')
605
- for section in sections:
606
- if any(keyword in section.lower() for keyword in ['مخاطر مالية', 'خسارة', 'تكلفة إضافية', 'غرامة']):
607
- financial_risks.append(section.strip())
608
- return financial_risks
609
-
610
- def _extract_operational_risks(self, text):
611
- """استخراج المخاطر التشغيلية"""
612
- operational_risks = []
613
- sections = text.split('\n\n')
614
- for section in sections:
615
- if any(keyword in section.lower() for keyword in ['مخاطر تشغيلية', 'توقف', 'تعطل', 'تأخير']):
616
- operational_risks.append(section.strip())
617
- return operational_risks
618
-
619
- def _extract_legal_risks(self, text):
620
- """استخراج المخاطر القانونية"""
621
- legal_risks = []
622
- sections = text.split('\n\n')
623
- for section in sections:
624
- if any(keyword in section.lower() for keyword in ['مخاطر قانونية', 'نزاع', 'مخالفة', 'تقاضي']):
625
- legal_risks.append(section.strip())
626
- return legal_risks
627
-
628
- def _extract_technical_risks(self, text):
629
- """استخراج المخاطر الفنية"""
630
- technical_risks = []
631
- sections = text.split('\n\n')
632
- for section in sections:
633
- if any(keyword in section.lower() for keyword in ['مخاطر فنية', 'عطل', 'خلل', 'تقني']):
634
- technical_risks.append(section.strip())
635
- return technical_risks
636
-
637
- def _assess_risk_severity(self, risk_factors):
638
- """تقييم شدة المخاطر"""
639
- # في التطبيق الحقيقي، ستحتاج إلى تحليل أكثر تعقيدًا
640
- # هذه مجرد محاكاة بسيطة
641
- severity_scores = []
642
- for risk in risk_factors:
643
- # تقييم بسيط بناءً على طول النص والكلمات الرئيسية
644
- score = len(risk) / 100 # كلما كان النص أطول، كلما كانت المخاطر أكثر تفصيلاً
645
-
646
- # زيادة الدرجة بناءً على كلمات مفتاحية تدل على شدة الخطر
647
- severe_keywords = ['خطير', 'شديد', 'كبير', 'جسيم', 'عالي']
648
- for keyword in severe_keywords:
649
- if keyword in risk.lower():
650
- score += 1
651
-
652
- severity_scores.append(min(score, 10)) # تحديد سقف للدرجة
653
-
654
- # متوسط درجة الشدة
655
- average_severity = sum(severity_scores) / len(severity_scores) if severity_scores else 0
656
-
657
- # تصنيف المخاطر بناءً على متوسط الشدة
658
- if average_severity >= 7:
659
- return "عالية"
660
- elif average_severity >= 4:
661
- return "متوسطة"
662
- else:
663
- return "منخفضة"
664
-
665
- def _suggest_risk_mitigation(self, risk_factors):
666
- """اقتراح آليات تخفيف المخاطر"""
667
- mitigations = []
668
-
669
- # في التطبيق الحقيقي، ستحتاج إلى محرك استدلال أكثر تعقيدًا
670
- # هذه مجرد اقتراحات عامة
671
-
672
- if any("مالي" in risk for risk in risk_factors):
673
- mitigations.append("ضمانات مالية وتأمين لتغطية المخاطر المالية")
674
-
675
- if any("تأخير" in risk for risk in risk_factors):
676
- mitigations.append("وضع جداول زمنية مرنة وخطط بديلة للطوارئ")
677
-
678
- if any("قانوني" in risk for risk in risk_factors):
679
- mitigations.append("مراجعة قانونية شاملة للعقد وبنوده")
680
-
681
- if any("فني" in risk for risk in risk_factors):
682
- mitigations.append("اختبارات فنية مسبقة وضمانات للأداء الفني")
683
-
684
- # إضافة توصيات عامة إذا لم يتم العثور على مخاطر محددة
685
- if not mitigations:
686
- mitigations = [
687
- "وضع خطة إدارة مخاطر شاملة",
688
- "تحديد مسؤوليات الأطراف بوضوح",
689
- "وضع آليات للمتابعة والتقييم الدوري",
690
- "توفير ضمانات مالية وفنية كافية"
691
- ]
692
-
693
- return mitigations
694
 
695
  def _analyze_conditions(self, text):
696
  """دراسة كراسة الشروط"""
697
- conditions = []
698
-
699
- # البحث عن الأقسام المتعلقة بالشروط
700
- sections = text.split('\n\n')
701
- for section in sections:
702
- if any(keyword in section.lower() for keyword in ['شروط', 'متطلبات', 'معايير', 'مواصفات']):
703
- conditions.append(section.strip())
704
-
705
- # تصنيف الشروط
706
- categorized_conditions = {
707
- "general_conditions": self._extract_general_conditions(text),
708
- "technical_conditions": self._extract_technical_conditions(text),
709
- "administrative_conditions": self._extract_administrative_conditions(text),
710
- "financial_conditions": self._extract_financial_conditions(text)
711
- }
712
-
713
- # تقييم مدى اكتمال الشروط ووضوحها
714
- completeness_score = self._assess_conditions_completeness(conditions)
715
- clarity_score = self._assess_conditions_clarity(conditions)
716
-
717
- return {
718
- "conditions_list": conditions,
719
- "categorized_conditions": categorized_conditions,
720
- "completeness_score": completeness_score,
721
- "clarity_score": clarity_score,
722
- "improvement_suggestions": self._suggest_conditions_improvements(conditions)
723
- }
724
-
725
- def _extract_general_conditions(self, text):
726
- """استخراج الشروط العامة"""
727
- general_conditions = []
728
- sections = text.split('\n\n')
729
- for section in sections:
730
- if any(keyword in section.lower() for keyword in ['شروط عامة', 'أحكام عامة']):
731
- general_conditions.append(section.strip())
732
- return general_conditions
733
-
734
- def _extract_technical_conditions(self, text):
735
- """استخراج الشروط الفنية"""
736
- technical_conditions = []
737
- sections = text.split('\n\n')
738
- for section in sections:
739
- if any(keyword in section.lower() for keyword in ['شروط فنية', 'مواصفات فنية', 'متطلبات فنية']):
740
- technical_conditions.append(section.strip())
741
- return technical_conditions
742
-
743
- def _extract_administrative_conditions(self, text):
744
- """استخراج الشروط الإدارية"""
745
- admin_conditions = []
746
- sections = text.split('\n\n')
747
- for section in sections:
748
- if any(keyword in section.lower() for keyword in ['شروط إدارية', 'متطلبات إدارية']):
749
- admin_conditions.append(section.strip())
750
- return admin_conditions
751
-
752
- def _extract_financial_conditions(self, text):
753
- """استخراج الشروط المالية"""
754
- financial_conditions = []
755
- sections = text.split('\n\n')
756
- for section in sections:
757
- if any(keyword in section.lower() for keyword in ['شروط مالية', 'متطلبات مالية']):
758
- financial_conditions.append(section.strip())
759
- return financial_conditions
760
-
761
- def _assess_conditions_completeness(self, conditions):
762
- """تقييم اكتمال الشروط"""
763
- # تحقق من وجود جميع أنواع الشروط الرئيسية
764
- required_categories = ['عامة', 'فنية', 'إدارية', 'مالية']
765
-
766
- coverage = 0
767
- for category in required_categories:
768
- if any(category in condition.lower() for condition in conditions):
769
- coverage += 1
770
-
771
- # حساب نسبة التغطية
772
- completeness_score = (coverage / len(required_categories)) * 10
773
-
774
- return min(round(completeness_score, 1), 10) # تحديد سقف للدرجة
775
-
776
- def _assess_conditions_clarity(self, conditions):
777
- """تقييم وضوح الشروط"""
778
- # في التطبيق الحقيقي، ستحتاج إلى تحليل لغوي أكثر تعقيدًا
779
- # هذه مجرد محاكاة بسيطة
780
-
781
- clarity_scores = []
782
- for condition in conditions:
783
- # تقييم بسيط بناءً على وضوح النص
784
- score = 10 # نبدأ بدرجة كاملة
785
-
786
- # تقليل الدرجة بناءً على كلمات غامضة
787
- ambiguous_terms = ['ربما', 'قد', 'يمكن', 'محتمل', 'حسب الاقتضاء', 'في بعض الحالات']
788
- for term in ambiguous_terms:
789
- if term in condition.lower():
790
- score -= 1
791
-
792
- # تقليل الدرجة إذا كان النص طويلًا جدًا
793
- if len(condition) > 500:
794
- score -= 2
795
-
796
- clarity_scores.append(max(score, 1)) # الحد الأدنى للدرجة هو 1
797
-
798
- # متوسط درجة الوضوح
799
- average_clarity = sum(clarity_scores) / len(clarity_scores) if clarity_scores else 0
800
-
801
- return round(average_clarity, 1)
802
-
803
- def _suggest_conditions_improvements(self, conditions):
804
- """اقتراح تحسينات للشروط"""
805
- suggestions = []
806
-
807
- # اقتراحات عامة لتحسين الشروط
808
- if not any('عامة' in condition.lower() for condition in conditions):
809
- suggestions.append("إضافة قسم للشروط العامة يوضح نطاق العمل والمسؤوليات العامة")
810
-
811
- if not any('فنية' in condition.lower() for condition in conditions):
812
- suggestions.append("إضافة قسم للشروط الفنية يحدد المواصفات والمتطلبات الفنية بدقة")
813
-
814
- if not any('إدارية' in condition.lower() for condition in conditions):
815
- suggestions.append("إضافة قسم للشروط الإدارية يوضح الإجراءات والمتطلبات الإدارية")
816
-
817
- if not any('مالية' in condition.lower() for condition in conditions):
818
- suggestions.append("إضافة قسم للشروط المالية يحدد الالتزامات المالية وآليات الدفع")
819
-
820
- # اقتراحات للشروط الموجودة
821
- ambiguous_conditions = []
822
- for condition in conditions:
823
- if any(term in condition.lower() for term in ['ربما', 'قد', 'يمكن', 'محتمل']):
824
- ambiguous_conditions.append(condition)
825
-
826
- if ambiguous_conditions:
827
- suggestions.append("توضيح الشروط الغامضة وتحديد المتطلبات بدقة أكبر")
828
-
829
- # إضافة توصيات عامة إذا لم يتم العثور على مشاكل محددة
830
- if not suggestions:
831
- suggestions = [
832
- "تنظيم الشروط في أقسام منفصلة وواضحة",
833
- "استخدام لغة بسيطة ومباشرة في صياغة الشروط",
834
- "تحديد المعايير الكمية والنوعية بدقة",
835
- "تضمين آليات لحل النزاعات في حالة الاختلاف حول تفسير الشروط"
836
- ]
837
-
838
- return suggestions
839
 
840
  def _generate_summary(self, text):
841
  """توليد ملخص"""
842
- # في التطبيق الحقيقي، ستحتاج إلى استخدام تقنيات معالجة اللغة الطبيعية
843
- # لتلخيص النص بشكل ذكي. هذه مجرد محاكاة بسيطة.
844
-
845
- # استخراج الجمل المهمة من النص
846
- important_sentences = []
847
- sentences = text.split('.')
848
-
849
- # البحث عن جمل مهمة بناءً على كلمات مفتاحية
850
- key_terms = ['شروط', 'بنود', 'التزامات', 'متطلبات', 'تكلفة', 'مدة', 'ضمان', 'غرامة']
851
- for sentence in sentences:
852
- if any(term in sentence.lower() for term in key_terms):
853
- important_sentences.append(sentence.strip())
854
-
855
- # اختيار عدد محدود من الجمل للملخص
856
- max_sentences = min(10, len(important_sentences))
857
- summary_sentences = important_sentences[:max_sentences]
858
-
859
- # دمج الجمل في ملخص
860
- summary = '. '.join(summary_sentences)
861
-
862
- # إضافة خاتمة موجزة
863
- summary += f"\n\nيتكون المستند من {len(sentences)} جملة وتم تلخيصه في {len(summary_sentences)} جمل رئيسية."
864
-
865
- return summary
866
 
867
  def _generate_recommendations(self, text):
868
  """توليد التوصيات"""
869
- # في التطبيق الحقيقي، ستحتاج إلى تحليل أكثر تعقيدًا
870
- # هذه مجرد توصيات عامة بناءً على المحتوى
871
-
872
- recommendations = []
873
-
874
- # توصيات بناءً على وجود أو غياب أقسام معينة
875
- if 'شروط' not in text.lower():
876
- recommendations.append("إضافة قسم واضح للشروط العامة والخاصة")
877
-
878
- if 'مواصفات فنية' not in text.lower():
879
- recommendations.append("توضيح المواصفات الفنية المطلوبة بشكل مفصل")
880
-
881
- if 'غرامات' not in text.lower():
882
- recommendations.append("تحديد الغرامات والجزاءات بوضوح في حالة عدم الالتزام")
883
-
884
- if 'ضمان' not in text.lower():
885
- recommendations.append("تضمين بنود الضمان والصيانة بشكل واضح")
886
-
887
- # توصيات بناءً على تحليل المخاطر
888
- risks = self._analyze_risks(text)
889
- if risks["risk_severity"] == "عالية":
890
- recommendations.append("مراجعة بنود العقد للتقليل من المخاطر العالية المحددة في التحليل")
891
-
892
- # توصيات بناءً على تحليل الشروط
893
- conditions = self._analyze_conditions(text)
894
- if conditions["clarity_score"] < 7:
895
- recommendations.append("تحسين صياغة الشروط لزيادة الوضوح وتقليل الغموض")
896
-
897
- # توصيات عامة
898
- general_recommendations = [
899
- "مراجعة العقد من قبل مستشار قانوني متخصص",
900
- "التأكد من توافق البنود مع الأنظمة واللوائح الحالية",
901
- "تضمين آليات واضحة لحل النزاعات",
902
- "تحديد مسؤوليات كل طرف بشكل صريح",
903
- "وضع جداول زمنية واضحة للتنفيذ ومؤشرات للأداء"
904
- ]
905
-
906
- # دمج التوصيات
907
- recommendations.extend(general_recommendations)
908
-
909
- return recommendations
910
-
911
- def _analyze_tender_specifics(self, text):
912
- """تحليل خاص بالمناقصات"""
913
- return {
914
- "eligibility_criteria": self._extract_eligibility_criteria(text),
915
- "submission_requirements": self._extract_submission_requirements(text),
916
- "evaluation_criteria": self._extract_evaluation_criteria(text),
917
- "timeline": self._extract_tender_timeline(text)
918
- }
919
-
920
- def _extract_eligibility_criteria(self, text):
921
- """استخراج معايير الأهلية"""
922
- criteria = []
923
- sections = text.split('\n\n')
924
- for section in sections:
925
- if any(keyword in section.lower() for keyword in ['أهلية', 'شروط المشاركة', 'متطلبات التأهيل']):
926
- criteria.append(section.strip())
927
- return criteria
928
-
929
- def _extract_submission_requirements(self, text):
930
- """استخراج متطلبات تقديم العروض"""
931
- requirements = []
932
- sections = text.split('\n\n')
933
- for section in sections:
934
- if any(keyword in section.lower() for keyword in ['تقديم العروض', 'متطلبات العرض', 'مستندات']):
935
- requirements.append(section.strip())
936
- return requirements
937
-
938
- def _extract_evaluation_criteria(self, text):
939
- """استخراج معايير التقييم"""
940
- criteria = []
941
- sections = text.split('\n\n')
942
- for section in sections:
943
- if any(keyword in section.lower() for keyword in ['معايير التقييم', 'آلية التقييم', 'ترسية']):
944
- criteria.append(section.strip())
945
- return criteria
946
-
947
- def _extract_tender_timeline(self, text):
948
- """استخراج الجدول الزمني للمناقصة"""
949
- import re
950
-
951
- timeline = {}
952
-
953
- # البحث عن تواريخ محددة مثل تاريخ الإعلان، تاريخ الإغلاق، إلخ.
954
- date_pattern = r'(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})'
955
-
956
- # تاريخ الإعلان
957
- announcement_match = re.search(r'تاريخ الإعلان\s*[:؛]\s*' + date_pattern, text)
958
- if announcement_match:
959
- timeline["announcement_date"] = announcement_match.group(1)
960
-
961
- # تاريخ بدء استلام العروض
962
- start_submission_match = re.search(r'بدء استلام العروض\s*[:؛]\s*' + date_pattern, text)
963
- if start_submission_match:
964
- timeline["submission_start_date"] = start_submission_match.group(1)
965
-
966
- # تاريخ إغلاق استلام العروض
967
- end_submission_match = re.search(r'إغلاق استلام العروض\s*[:؛]\s*' + date_pattern, text)
968
- if end_submission_match:
969
- timeline["submission_end_date"] = end_submission_match.group(1)
970
-
971
- # تاريخ فتح المظاريف
972
- opening_match = re.search(r'فتح المظاريف\s*[:؛]\s*' + date_pattern, text)
973
- if opening_match:
974
- timeline["opening_date"] = opening_match.group(1)
975
-
976
- # تاريخ التقييم
977
- evaluation_match = re.search(r'تاريخ التقييم\s*[:؛]\s*' + date_pattern, text)
978
- if evaluation_match:
979
- timeline["evaluation_date"] = evaluation_match.group(1)
980
-
981
- # تاريخ الترسية
982
- award_match = re.search(r'تاريخ الترسية\s*[:؛]\s*' + date_pattern, text)
983
- if award_match:
984
- timeline["award_date"] = award_match.group(1)
985
-
986
- return timeline
987
-
988
- def _analyze_contract_specifics(self, text):
989
- """تحليل خاص بالعقود"""
990
- return {
991
- "parties": self._extract_contract_parties(text),
992
- "duration": self._extract_contract_duration(text),
993
- "termination_conditions": self._extract_termination_conditions(text),
994
- "penalties": self._extract_penalties(text),
995
- "warranties": self._extract_warranties(text)
996
- }
997
-
998
- def _extract_contract_parties(self, text):
999
- """استخراج أطراف العقد"""
1000
- parties = {}
1001
-
1002
- # البحث عن الطرف الأول
1003
- first_party_match = re.search(r'الطرف الأول\s*[:؛]\s*([^\n]+)', text)
1004
- if first_party_match:
1005
- parties["first_party"] = first_party_match.group(1).strip()
1006
-
1007
- # البحث عن الطرف الثاني
1008
- second_party_match = re.search(r'الطرف الثاني\s*[:؛]\s*([^\n]+)', text)
1009
- if second_party_match:
1010
- parties["second_party"] = second_party_match.group(1).strip()
1011
-
1012
- return parties
1013
-
1014
- def _extract_contract_duration(self, text):
1015
- """استخراج مدة العقد"""
1016
- duration = {}
1017
-
1018
- # البحث عن مدة العقد
1019
- duration_match = re.search(r'مدة العقد\s*[:؛]\s*([^\n]+)', text)
1020
- if duration_match:
1021
- duration["text"] = duration_match.group(1).strip()
1022
-
1023
- # البحث عن تاريخ بداية العقد
1024
- start_date_match = re.search(r'تاريخ بداية العقد\s*[:؛]\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})', text)
1025
- if start_date_match:
1026
- duration["start_date"] = start_date_match.group(1)
1027
-
1028
- # البحث عن تاريخ نهاية العقد
1029
- end_date_match = re.search(r'تاريخ نهاية العقد\s*[:؛]\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})', text)
1030
- if end_date_match:
1031
- duration["end_date"] = end_date_match.group(1)
1032
-
1033
- return duration
1034
-
1035
- def _extract_termination_conditions(self, text):
1036
- """استخراج شروط إنهاء العقد"""
1037
- conditions = []
1038
- sections = text.split('\n\n')
1039
- for section in sections:
1040
- if any(keyword in section.lower() for keyword in ['إنهاء العقد', 'فسخ العقد', 'إلغاء العقد']):
1041
- conditions.append(section.strip())
1042
- return conditions
1043
-
1044
- def _extract_penalties(self, text):
1045
- """استخراج الغرامات والجزاءات"""
1046
- penalties = []
1047
- sections = text.split('\n\n')
1048
- for section in sections:
1049
- if any(keyword in section.lower() for keyword in ['غرامة', 'جزاء', 'عقوبة', 'تعويض']):
1050
- penalties.append(section.strip())
1051
- return penalties
1052
-
1053
- def _extract_warranties(self, text):
1054
- """استخراج الضمانات"""
1055
- warranties = []
1056
- sections = text.split('\n\n')
1057
- for section in sections:
1058
- if any(keyword in section.lower() for keyword in ['ضمان', 'كفالة', 'تأمين']):
1059
- warranties.append(section.strip())
1060
- return warranties
1061
-
1062
- def _analyze_technical_specifics(self, text):
1063
- """تحليل خاص بالمستندات الفنية"""
1064
- return {
1065
- "specifications": self._extract_technical_specifications(text),
1066
- "standards": self._extract_technical_standards(text),
1067
- "testing_procedures": self._extract_testing_procedures(text),
1068
- "quality_requirements": self._extract_quality_requirements(text)
1069
- }
1070
-
1071
- def _extract_technical_specifications(self, text):
1072
- """استخراج المواصفات الفنية"""
1073
- specifications = []
1074
- sections = text.split('\n\n')
1075
- for section in sections:
1076
- if any(keyword in section.lower() for keyword in ['مواصفات فنية', 'خصائص', 'متطلبات فنية']):
1077
- specifications.append(section.strip())
1078
- return specifications
1079
-
1080
- def _extract_technical_standards(self, text):
1081
- """استخراج المعايير الفنية"""
1082
- standards = []
1083
- sections = text.split('\n\n')
1084
- for section in sections:
1085
- if any(keyword in section.lower() for keyword in ['معايير', 'مقاييس', 'مواصفات قياسية']):
1086
- standards.append(section.strip())
1087
- return standards
1088
-
1089
- def _extract_testing_procedures(self, text):
1090
- """استخراج إجراءات الاختبار"""
1091
- procedures = []
1092
- sections = text.split('\n\n')
1093
- for section in sections:
1094
- if any(keyword in section.lower() for keyword in ['اختبار', 'فحص', 'تجربة']):
1095
- procedures.append(section.strip())
1096
- return procedures
1097
-
1098
- def _extract_quality_requirements(self, text):
1099
- """استخراج متطلبات الجودة"""
1100
- requirements = []
1101
- sections = text.split('\n\n')
1102
- for section in sections:
1103
- if any(keyword in section.lower() for keyword in ['جودة', 'ضمان الجودة', 'رقابة']):
1104
- requirements.append(section.strip())
1105
- return requirements
1106
 
1107
- def _extract_entities(self, text):
1108
- """استخراج الكيانات من النص"""
1109
- entities = {
1110
- "organizations": self._extract_organizations(text),
1111
- "people": self._extract_people(text),
1112
- "locations": self._extract_locations(text)
1113
- }
1114
- return entities
1115
-
1116
- def _extract_organizations(self, text):
1117
- """استخراج المنظمات والشركات"""
1118
- import re
1119
- # نمط بسيط للبحث عن المنظمات والشركات
1120
- org_pattern = r'شركة [\u0600-\u06FF\s]+|مؤسسة [\u0600-\u06FF\s]+|وزارة [\u0600-\u06FF\s]+|هيئة [\u0600-\u06FF\s]+'
1121
- return list(set(re.findall(org_pattern, text)))
1122
-
1123
- def _extract_people(self, text):
1124
- """استخراج أسماء الأشخاص"""
1125
- # في التطبيق الحقيقي، ستحتاج إلى استخدام تقنيات التعرف على الكيانات
1126
- # هذه مجرد محاكاة بسيطة
1127
- return []
1128
-
1129
- def _extract_locations(self, text):
1130
- """استخراج المواقع"""
1131
- import re
1132
- # نمط بسيط للبحث عن المواقع
1133
- location_pattern = r'مدينة [\u0600-\u06FF\s]+|منطقة [\u0600-\u06FF\s]+|محافظة [\u0600-\u06FF\s]+'
1134
- return list(set(re.findall(location_pattern, text)))
1135
 
1136
- def _extract_materials(self, text):
1137
- """استخراج المواد"""
1138
- materials = []
1139
- # في التطبيق الحقيقي، ستحتاج إلى قائمة بالمواد الشائعة للبحث عنها
1140
- common_materials = ['حديد', 'خشب', 'زجاج', 'ألمنيوم', 'نحاس', 'بلاستيك', 'خرسانة']
1141
- for material in common_materials:
1142
- if material in text.lower():
1143
- # البحث عن السياق المحيط بالمادة
1144
- pattern = r'[^.]*\b' + material + r'\b[^.]*\.'
1145
- material_contexts = re.findall(pattern, text)
1146
- for context in material_contexts:
1147
- materials.append(context.strip())
1148
- return materials
1149
-
1150
- def _extract_measurements(self, text):
1151
- """استخراج القياسات"""
1152
- import re
1153
- # البحث عن القياسات مثل الطول والعرض والوزن وغيرها
1154
- measurement_pattern = r'\d+(?:\.\d+)?\s*(?:متر|سم|مم|كجم|طن|لتر|مل)'
1155
- return re.findall(measurement_pattern, text)
1156
-
1157
- def _extract_standards(self, text):
1158
- """استخراج المعايير"""
1159
- standards = []
1160
- # معايير شائعة للبحث عنها
1161
- common_standards = ['ISO', 'SASO', 'ASTM', 'BS', 'DIN', 'IEC']
1162
- for standard in common_standards:
1163
- if standard in text:
1164
- # البحث عن المعيار مع رقمه
1165
- pattern = r'\b' + standard + r'\s*\d+\b'
1166
- standard_matches = re.findall(pattern, text)
1167
- standards.extend(standard_matches)
1168
- return standards
1169
-
1170
- def _analyze_topics(self, text):
1171
- """تحليل المواضيع الرئيسية"""
1172
- # في التطبيق الحقيقي، ستحتاج إلى استخدام تقنيات تحليل المواضيع مثل LDA
1173
- # هذه مجرد محاكاة بسيطة
1174
-
1175
- topics = {}
1176
-
1177
- # مواضيع شائعة في المناقصات والعقود
1178
- common_topics = {
1179
- "financial": ['سعر', 'تكلفة', 'ميزانية', 'دفع', 'مالي'],
1180
- "technical": ['فني', 'مواصفات', 'معايير', 'تقني'],
1181
- "legal": ['قانوني', 'شرط', 'بند', 'التزام', 'حق'],
1182
- "administrative": ['إداري', 'إجراء', 'تنظيم', 'إشراف'],
1183
- "time": ['مدة', 'فترة', 'موعد', 'تاريخ', 'جدول زمني']
1184
- }
1185
-
1186
- # حساب تكرار كلمات كل موضوع في النص
1187
- word_count = len(text.split())
1188
-
1189
- for topic, keywords in common_topics.items():
1190
- topic_count = 0
1191
- for keyword in keywords:
1192
- # عدد مرات ظهور الكلمة المفتاحية في النص
1193
- topic_count += len(re.findall(r'\b' + keyword + r'\w*\b', text))
1194
-
1195
- # حساب النسبة المئوية للموضوع
1196
- if word_count > 0:
1197
- topic_percentage = (topic_count / word_count) * 100
1198
- topics[topic] = round(topic_percentage, 2)
1199
- else:
1200
- topics[topic] = 0
1201
-
1202
- return topics
1203
-
1204
- def _check_required_terms(self, text):
1205
- """التحقق من وجود المصطلحات المطلوبة"""
1206
- required_terms = {
1207
- "general": ['نطاق العمل', 'مدة التنفيذ', 'الشروط العامة'],
1208
- "financial": ['قيمة العقد', 'طريقة الدفع', 'الضمان المالي'],
1209
- "legal": ['حل النزاعات', 'الإنهاء', 'التعويضات'],
1210
- "technical": ['المواصفات الفنية', 'ضمان الجودة', 'معايير القبول']
1211
- }
1212
-
1213
- found_terms = {}
1214
-
1215
- for category, terms in required_terms.items():
1216
- found_in_category = []
1217
- for term in terms:
1218
- if term in text:
1219
- found_in_category.append(term)
1220
-
1221
- found_terms[category] = found_in_category
1222
-
1223
- return found_terms
1224
-
1225
- def _calculate_compliance_score(self, text):
1226
- """حساب درجة الامتثال"""
1227
- # التحقق من وجود الأقسام المطلوبة
1228
- missing_sections = self._check_missing_sections(text)
1229
- required_terms = self._check_required_terms(text)
1230
-
1231
- # حساب درجة الامتثال
1232
- total_required_terms = sum(len(terms) for terms in required_terms.values())
1233
- found_terms = sum(len(found) for found in required_terms.values())
1234
-
1235
- if total_required_terms > 0:
1236
- compliance_percentage = (found_terms / total_required_terms) * 100
1237
-
1238
- # تقليل الدرجة بناءً على الأقسام المفقودة
1239
- compliance_percentage -= len(missing_sections) * 5
1240
-
1241
- # ضمان أن الدرجة في النطاق المناسب
1242
- compliance_percentage = max(0, min(100, compliance_percentage))
1243
-
1244
- return round(compliance_percentage, 1)
1245
- else:
1246
- return 0
1247
-
1248
- def _get_version_info(self, document_path):
1249
- """الحصول على معلومات الإصدار"""
1250
- # في التطبيق الحقيقي، قد تحتاج لاستخراج معلومات الإصدار من الملف
1251
- version_info = {
1252
- "filename": os.path.basename(document_path),
1253
- "last_modified": time.ctime(os.path.getmtime(document_path))
1254
- }
1255
-
1256
- # البحث عن رقم الإصدار في اسم الملف
1257
- match = re.search(r'[vV](\d+(?:\.\d+)*)', os.path.basename(document_path))
1258
- if match:
1259
- version_info["version_number"] = match.group(1)
1260
- else:
1261
- version_info["version_number"] = "غير محدد"
1262
-
1263
- return version_info
1264
 
1265
  def _analyze_docx(self, document_path, document_type):
1266
  """تحليل مستند Word"""
1267
  try:
1268
- # استخراج النص من ملف Word
1269
- text = self._extract_text_from_docx(document_path)
1270
-
1271
- # استخدام نفس آلية تحليل PDF للتحليل
1272
- analysis = self._analyze_pdf(document_path, document_type)
1273
-
1274
- # تحديث نوع الملف
1275
- analysis["file_info"]["type"] = "DOCX"
 
 
 
 
 
1276
 
1277
- return analysis
 
1278
 
1279
  except Exception as e:
1280
  logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
1281
  raise
1282
-
1283
- def _extract_text_from_docx(self, document_path):
1284
- """استخراج النص من ملف Word"""
1285
- try:
1286
- import docx
1287
-
1288
- doc = docx.Document(document_path)
1289
- text = ""
1290
-
1291
- for paragraph in doc.paragraphs:
1292
- text += paragraph.text + "\n"
1293
-
1294
- for table in doc.tables:
1295
- for row in table.rows:
1296
- for cell in row.cells:
1297
- text += cell.text + " "
1298
- text += "\n"
1299
-
1300
- return text
1301
-
1302
- except Exception as e:
1303
- logger.error(f"خطأ في استخراج النص من Word: {str(e)}")
1304
- raise
1305
-
1306
  def _analyze_xlsx(self, document_path, document_type):
1307
  """تحليل مستند Excel"""
1308
  try:
1309
- # استخراج البيانات من ملف Excel
1310
- data = self._extract_data_from_xlsx(document_path)
1311
 
1312
- # إنشاء تحليل مخصص لملفات Excel
1313
- analysis = {
1314
- "document_path": document_path,
1315
- "document_type": document_type,
1316
- "analysis_start_time": self.analysis_results["analysis_start_time"],
1317
- "status": "جاري التحليل",
1318
- "file_info": {
1319
- "name": os.path.basename(document_path),
1320
- "type": "XLSX",
1321
- "size": os.path.getsize(document_path),
1322
- "sheets": self._count_sheets(document_path),
1323
- "create_date": "غير متوفر",
1324
- "modify_date": time.ctime(os.path.getmtime(document_path))
1325
- },
1326
- "data_analysis": {
1327
- "sheet_summary": data["sheet_summary"],
1328
- "total_rows": data["total_rows"],
1329
- "total_columns": data["total_columns"],
1330
- "numeric_columns": data["numeric_columns"],
1331
- "text_columns": data["text_columns"],
1332
- "date_columns": data["date_columns"]
1333
- }
1334
- }
1335
 
1336
- # إضافة تحليلات إضافية حسب نوع المستند
1337
- if document_type == "tender":
1338
- analysis["tender_analysis"] = self._analyze_excel_tender(data)
1339
- elif document_type == "financial":
1340
- analysis["financial_analysis"] = self._analyze_excel_financial(data)
 
1341
 
1342
- return analysis
 
 
 
 
 
1343
 
1344
  except Exception as e:
1345
  logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
1346
  raise
1347
-
1348
- def _extract_data_from_xlsx(self, document_path):
1349
- """استخراج البيانات من ملف Excel"""
1350
- try:
1351
- import pandas as pd
1352
-
1353
- # قراءة جميع الأوراق في الملف
1354
- excel_file = pd.ExcelFile(document_path)
1355
- sheet_names = excel_file.sheet_names
1356
-
1357
- data = {
1358
- "sheet_summary": {},
1359
- "total_rows": 0,
1360
- "total_columns": 0,
1361
- "numeric_columns": 0,
1362
- "text_columns": 0,
1363
- "date_columns": 0,
1364
- "sheets": {}
1365
- }
1366
-
1367
- for sheet_name in sheet_names:
1368
- # قراءة الورقة إلى DataFrame
1369
- df = pd.read_excel(excel_file, sheet_name=sheet_name)
1370
-
1371
- # تحليل أنواع البيانات في الأعمدة
1372
- column_types = {}
1373
- numeric_columns = 0
1374
- text_columns = 0
1375
- date_columns = 0
1376
-
1377
- for column in df.columns:
1378
- if pd.api.types.is_numeric_dtype(df[column]):
1379
- column_types[column] = "numeric"
1380
- numeric_columns += 1
1381
- elif pd.api.types.is_datetime64_dtype(df[column]):
1382
- column_types[column] = "date"
1383
- date_columns += 1
1384
- else:
1385
- column_types[column] = "text"
1386
- text_columns += 1
1387
-
1388
- # تحديث ملخص الورقة
1389
- data["sheet_summary"][sheet_name] = {
1390
- "rows": len(df),
1391
- "columns": len(df.columns),
1392
- "column_types": column_types
1393
- }
1394
-
1395
- # تحديث الإحصائيات الإجمالية
1396
- data["total_rows"] += len(df)
1397
- data["total_columns"] += len(df.columns)
1398
- data["numeric_columns"] += numeric_columns
1399
- data["text_columns"] += text_columns
1400
- data["date_columns"] += date_columns
1401
-
1402
- # تخزين البيانات (مع حد أقصى للصفوف للتحكم في الحجم)
1403
- max_rows = 100
1404
- data["sheets"][sheet_name] = df.head(max_rows).to_dict(orient="records")
1405
-
1406
- return data
1407
-
1408
- except Exception as e:
1409
- logger.error(f"خطأ في استخراج البيانات من Excel: {str(e)}")
1410
- raise
1411
-
1412
- def _count_sheets(self, document_path):
1413
- """حساب عدد الأوراق في ملف Excel"""
1414
- try:
1415
- import pandas as pd
1416
-
1417
- excel_file = pd.ExcelFile(document_path)
1418
- return len(excel_file.sheet_names)
1419
-
1420
- except Exception as e:
1421
- logger.error(f"خطأ في حساب عدد الأوراق: {str(e)}")
1422
- return 0
1423
-
1424
- def _analyze_excel_tender(self, data):
1425
- """تحليل بيانات المناقصة من ملف Excel"""
1426
- # تحليل بسيط لملف Excel خاص بالمناقصة
1427
- analysis = {
1428
- "items": self._extract_tender_items(data),
1429
- "quantities": self._extract_tender_quantities(data),
1430
- "pricing": self._extract_tender_pricing(data)
1431
- }
1432
-
1433
- return analysis
1434
-
1435
- def _extract_tender_items(self, data):
1436
- """استخراج البنود من بيانات المناقصة"""
1437
- items = []
1438
-
1439
- # البحث عن الأوراق التي تحتوي على بنود المناقصة
1440
- for sheet_name, sheet_data in data["sheets"].items():
1441
- if not sheet_data:
1442
- continue
1443
-
1444
- # البحث عن الأعمدة التي قد تحتوي على أسماء البنود
1445
- possible_item_columns = ["البند", "الوصف", "المادة", "البيان", "item", "description"]
1446
-
1447
- for row in sheet_data:
1448
- item_found = False
1449
-
1450
- # البحث عن أسماء البنود
1451
- for column in possible_item_columns:
1452
- if column in row and row[column]:
1453
- # تحقق من وجود أعمدة الكمية والوحدة
1454
- quantity = None
1455
- unit = None
1456
-
1457
- for qty_col in ["الكمية", "العدد", "quantity", "qty"]:
1458
- if qty_col in row and row[qty_col]:
1459
- quantity = row[qty_col]
1460
- break
1461
-
1462
- for unit_col in ["الوحدة", "unit", "uom"]:
1463
- if unit_col in row and row[unit_col]:
1464
- unit = row[unit_col]
1465
- break
1466
-
1467
- # إضافة البند إلى القائمة
1468
- items.append({
1469
- "name": row[column],
1470
- "quantity": quantity,
1471
- "unit": unit
1472
- })
1473
-
1474
- item_found = True
1475
- break
1476
-
1477
- if item_found:
1478
- break
1479
-
1480
- return items
1481
-
1482
- def _extract_tender_quantities(self, data):
1483
- """استخراج الكميات من بيانات المناقصة"""
1484
- quantities = {}
1485
-
1486
- # البحث عن الأوراق التي تحتوي على كميات المناقصة
1487
- for sheet_name, sheet_data in data["sheets"].items():
1488
- if not sheet_data:
1489
- continue
1490
-
1491
- # البحث عن الأعمدة التي قد تحتوي على الكميات
1492
- quantity_columns = ["الكمية", "العدد", "quantity", "qty"]
1493
- item_columns = ["البند", "الوصف", "المادة", "البيان", "item", "description"]
1494
-
1495
- for row in sheet_data:
1496
- item_name = None
1497
- quantity = None
1498
-
1499
- # البحث عن اسم البند
1500
- for col in item_columns:
1501
- if col in row and row[col]:
1502
- item_name = row[col]
1503
- break
1504
-
1505
- # البحث عن الكمية
1506
- for col in quantity_columns:
1507
- if col in row and row[col]:
1508
- quantity = row[col]
1509
- break
1510
-
1511
- # تخزين الكمية إذا وجدت
1512
- if item_name and quantity:
1513
- quantities[item_name] = quantity
1514
-
1515
- return quantities
1516
-
1517
- def _extract_tender_pricing(self, data):
1518
- """استخراج الأسعار من بيانات المناقصة"""
1519
- pricing = {}
1520
-
1521
- # البحث عن الأوراق التي تحتوي على أسعار المناقصة
1522
- for sheet_name, sheet_data in data["sheets"].items():
1523
- if not sheet_data:
1524
- continue
1525
-
1526
- # البحث عن الأعمدة التي قد تحتوي على الأسعار
1527
- price_columns = ["السعر", "التكلفة", "المبلغ", "price", "cost", "amount"]
1528
- item_columns = ["البند", "الوصف", "المادة", "البيان", "item", "description"]
1529
-
1530
- for row in sheet_data:
1531
- item_name = None
1532
- price = None
1533
-
1534
- # البحث عن اسم البند
1535
- for col in item_columns:
1536
- if col in row and row[col]:
1537
- item_name = row[col]
1538
- break
1539
-
1540
- # البحث عن السعر
1541
- for col in price_columns:
1542
- if col in row and row[col]:
1543
- price = row[col]
1544
- break
1545
-
1546
- # تخزين السعر إذا وجد
1547
- if item_name and price:
1548
- pricing[item_name] = price
1549
-
1550
- return pricing
1551
-
1552
- def _analyze_excel_financial(self, data):
1553
- """تحليل البيانات المالية من ملف Excel"""
1554
- # تحليل بسيط لملف Excel مالي
1555
- analysis = {
1556
- "total_amount": self._calculate_total_amount(data),
1557
- "budget_breakdown": self._extract_budget_breakdown(data),
1558
- "payment_schedule": self._extract_payment_schedule(data)
1559
- }
1560
-
1561
- return analysis
1562
-
1563
- def _calculate_total_amount(self, data):
1564
- """حساب المبلغ الإجمالي من البيانات المالية"""
1565
- total = 0
1566
-
1567
- # البحث عن الأوراق التي تحتوي على بيانات مالية
1568
- for sheet_name, sheet_data in data["sheets"].items():
1569
- if not sheet_data:
1570
- continue
1571
-
1572
- # البحث عن الأعمدة التي قد تحتوي على مبالغ
1573
- amount_columns = ["المبلغ", "الإجمالي", "المجموع", "amount", "total", "sum"]
1574
-
1575
- for row in sheet_data:
1576
- for col in amount_columns:
1577
- if col in row and row[col] and isinstance(row[col], (int, float)):
1578
- total += row[col]
1579
-
1580
- return total
1581
-
1582
- def _extract_budget_breakdown(self, data):
1583
- """استخراج تفاصيل الميزانية من البيانات المالية"""
1584
- breakdown = {}
1585
-
1586
- # البحث عن الأوراق التي تحتوي على تفاصيل الميزانية
1587
- for sheet_name, sheet_data in data["sheets"].items():
1588
- if not sheet_data:
1589
- continue
1590
-
1591
- # البحث عن الأعمدة التي قد تحتوي على بنود الميزانية
1592
- category_columns = ["البند", "الفئة", "القسم", "category", "item"]
1593
- amount_columns = ["المبلغ", "التكلفة", "القيمة", "amount", "cost", "value"]
1594
-
1595
- for row in sheet_data:
1596
- category = None
1597
- amount = None
1598
-
1599
- # البحث عن فئة الميزانية
1600
- for col in category_columns:
1601
- if col in row and row[col]:
1602
- category = row[col]
1603
- break
1604
-
1605
- # البحث عن المبلغ
1606
- for col in amount_columns:
1607
- if col in row and row[col] and isinstance(row[col], (int, float)):
1608
- amount = row[col]
1609
- break
1610
-
1611
- # تخزين بند الميزانية إذا وجد
1612
- if category and amount:
1613
- breakdown[category] = amount
1614
-
1615
- return breakdown
1616
-
1617
- def _extract_payment_schedule(self, data):
1618
- """استخراج جدول الدفعات من البيانات المالية"""
1619
- schedule = []
1620
-
1621
- # البحث عن الأوراق التي تحتوي على جدول الدفعات
1622
- for sheet_name, sheet_data in data["sheets"].items():
1623
- if not sheet_data:
1624
- continue
1625
-
1626
- # البحث عن الأعمدة التي قد تحتوي على معلومات الدفعات
1627
- date_columns = ["التاريخ", "الموعد", "date", "schedule"]
1628
- amount_columns = ["المبلغ", "الدفعة", "القيمة", "amount", "payment", "value"]
1629
- description_columns = ["الوصف", "البيان", "description", "details"]
1630
-
1631
- for row in sheet_data:
1632
- date = None
1633
- amount = None
1634
- description = None
1635
-
1636
- # البحث عن تاريخ الدفعة
1637
- for col in date_columns:
1638
- if col in row and row[col]:
1639
- date = row[col]
1640
- break
1641
-
1642
- # البحث عن مبلغ الدفعة
1643
- for col in amount_columns:
1644
- if col in row and row[col]:
1645
- amount = row[col]
1646
- break
1647
-
1648
- # البحث عن وصف الدفعة
1649
- for col in description_columns:
1650
- if col in row and row[col]:
1651
- description = row[col]
1652
- break
1653
-
1654
- # تخزين الدفعة إذا وجدت
1655
- if date and amount:
1656
- schedule.append({
1657
- "date": date,
1658
- "amount": amount,
1659
- "description": description
1660
- })
1661
-
1662
- return schedule
1663
-
1664
  def _analyze_txt(self, document_path, document_type):
1665
  """تحليل مستند نصي"""
1666
  try:
1667
- # قراءة محتوى الملف النصي
1668
- with open(document_path, 'r', encoding='utf-8') as file:
1669
- text = file.read()
1670
-
1671
- # استخدام نفس آلية تحليل PDF
1672
- analysis = self._analyze_pdf(document_path, document_type)
1673
 
1674
- # تحديث نوع الملف
1675
- analysis["file_info"]["type"] = "TXT"
1676
- analysis["file_info"]["pages"] = self._estimate_pages(text)
1677
 
1678
- return analysis
 
1679
 
1680
  except Exception as e:
1681
  logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
1682
  raise
1683
-
1684
- def _estimate_pages(self, text):
1685
- """تقدير عدد الصفحات في النص"""
1686
- # تقدير بسيط: كل 3000 حرف تعادل صفحة واحدة تقريبًا
1687
- return max(1, len(text) // 3000)
1688
-
1689
  def get_analysis_status(self):
1690
  """الحصول على حالة التحليل الحالي"""
1691
  if not self.analysis_in_progress:
@@ -1693,53 +282,53 @@ class DocumentAnalyzer:
1693
  return {"status": "لا يوجد تحليل جارٍ"}
1694
  else:
1695
  return {"status": self.analysis_results.get("status", "غير معروف")}
1696
-
1697
  return {
1698
  "status": "جاري التحليل",
1699
  "document_path": self.current_document,
1700
  "start_time": self.analysis_results.get("analysis_start_time")
1701
  }
1702
-
1703
  def get_analysis_results(self):
1704
  """الحصول على نتائج التحليل"""
1705
  return self.analysis_results
1706
-
1707
  def export_analysis_results(self, output_path=None):
1708
  """تصدير نتائج التحليل إلى ملف JSON"""
1709
  if not self.analysis_results:
1710
  logger.warning("لا توجد نتائج تحليل للتصدير")
1711
  return None
1712
-
1713
  if not output_path:
1714
  # إنشاء اسم ملف افتراضي
1715
  timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
1716
  filename = f"analysis_results_{timestamp}.json"
1717
  output_path = os.path.join(self.documents_path, filename)
1718
-
1719
  try:
1720
  with open(output_path, 'w', encoding='utf-8') as f:
1721
  json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
1722
-
1723
  logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
1724
  return output_path
1725
-
1726
  except Exception as e:
1727
  logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
1728
  return None
1729
-
1730
  def import_analysis_results(self, input_path):
1731
  """استيراد نتائج التحليل من ملف JSON"""
1732
  if not os.path.exists(input_path):
1733
  logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
1734
  return False
1735
-
1736
  try:
1737
  with open(input_path, 'r', encoding='utf-8') as f:
1738
  self.analysis_results = json.load(f)
1739
-
1740
  logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
1741
  return True
1742
-
1743
  except Exception as e:
1744
  logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
1745
  return False
@@ -1788,11 +377,11 @@ class DocumentAnalyzer:
1788
  # البحث عن القيم النقدية
1789
  currency_pattern = r'[\d,]+\.?\d*\s*(?:ريال|دولار|SAR|USD)'
1790
  currencies = re.findall(currency_pattern, text)
1791
-
1792
  # البحث عن النسب المئوية
1793
  percentage_pattern = r'\d+\.?\d*\s*%'
1794
  percentages = re.findall(percentage_pattern, text)
1795
-
1796
  return {
1797
  "currencies": currencies,
1798
  "percentages": percentages
@@ -1806,16 +395,10 @@ class DocumentAnalyzer:
1806
  def _calculate_complexity(self, text):
1807
  """حساب مستوى تعقيد النص"""
1808
  words = text.split()
1809
- if not words:
1810
- return 0
1811
-
1812
  avg_word_length = sum(len(word) for word in words) / len(words)
1813
  sentences = text.split('.')
1814
- if not sentences:
1815
- return 0
1816
-
1817
  avg_sentence_length = len(words) / len(sentences)
1818
-
1819
  # حساب درجة التعقيد (1-10)
1820
  complexity = min((avg_word_length * 0.5 + avg_sentence_length * 0.2), 10)
1821
  return round(complexity, 2)
@@ -1830,12 +413,12 @@ class DocumentAnalyzer:
1830
  "الغرامات",
1831
  "شروط الدفع"
1832
  ]
1833
-
1834
  missing = []
1835
  for section in required_sections:
1836
  if section not in text:
1837
  missing.append(section)
1838
-
1839
  return missing
1840
 
1841
  def _find_related_documents(self, document_path):
@@ -1843,56 +426,9 @@ class DocumentAnalyzer:
1843
  directory = os.path.dirname(document_path)
1844
  base_name = os.path.basename(document_path)
1845
  related = []
1846
-
1847
  for file in os.listdir(directory):
1848
  if file != base_name and file.startswith(base_name.split('_')[0]):
1849
  related.append(file)
1850
-
1851
  return related
1852
-
1853
- def process_image(self, image_path):
1854
- """معالجة وضغط الصورة"""
1855
- try:
1856
- # فتح الصورة
1857
- with Image.open(image_path) as img:
1858
- # تحويل الصورة إلى RGB إذا كانت RGBA
1859
- if img.mode == 'RGBA':
1860
- img = img.convert('RGB')
1861
-
1862
- # البدء بجودة عالية وتقليلها تدريجياً حتى نصل للحجم المطلوب
1863
- quality = 95
1864
- max_size = (1200, 1200)
1865
-
1866
- while True:
1867
- img.thumbnail(max_size, Image.Resampling.LANCZOS)
1868
- buffer = io.BytesIO()
1869
- img.save(buffer, format='JPEG', quality=quality, optimize=True)
1870
- size = len(buffer.getvalue())
1871
-
1872
- # إذا كان الحجم أقل من 5 ميجابايت، نخرج من الحلقة
1873
- if size <= 5000000:
1874
- break
1875
-
1876
- # تقليل الجودة والحجم
1877
- quality = max(quality - 10, 20) # لا نقلل الجودة عن 20
1878
- max_size = (int(max_size[0] * 0.8), int(max_size[1] * 0.8))
1879
-
1880
- # إذا وصلنا للحد الأدنى من الجودة والحجم ولم نصل للحجم المطلوب
1881
- if quality == 20 and max_size[0] < 400:
1882
- raise ValueError("لا يمكن ضغط الصورة للحجم المطلوب")
1883
-
1884
- # تحويل الصورة المضغوطة إلى base64
1885
- return base64.b64encode(buffer.getvalue()).decode('utf-8')
1886
- except Exception as e:
1887
- logger.error(f"خطأ في معالجة الصورة: {str(e)}")
1888
- raise
1889
-
1890
- def convert_pdf_to_images(self, pdf_path):
1891
- """تحويل PDF إلى صور"""
1892
- try:
1893
- from pdf2image import convert_from_path
1894
- images = convert_from_path(pdf_path)
1895
- return images
1896
- except Exception as e:
1897
- logger.error(f"فشل في تحويل ملف PDF إلى صورة: {str(e)}")
1898
- raise
 
9
  from pathlib import Path
10
  import datetime
11
  import json
 
 
 
 
12
 
13
  # تهيئة السجل
14
  logging.basicConfig(
 
19
 
20
  class DocumentAnalyzer:
21
  """فئة تحليل المستندات"""
22
+
23
  def __init__(self, config=None):
24
  """تهيئة محلل المستندات"""
25
  self.config = config
26
  self.analysis_in_progress = False
27
  self.current_document = None
28
  self.analysis_results = {}
29
+
30
  # إنشاء مجلد المستندات إذا لم يكن موجوداً
31
  if config and hasattr(config, 'DOCUMENTS_PATH'):
32
  self.documents_path = Path(config.DOCUMENTS_PATH)
33
  else:
34
  self.documents_path = Path('data/documents')
35
+
36
  if not self.documents_path.exists():
37
  self.documents_path.mkdir(parents=True, exist_ok=True)
38
+
39
  def analyze_document(self, document_path, document_type="tender", callback=None):
40
  """تحليل مستند"""
41
  if self.analysis_in_progress:
42
  logger.warning("هناك عملية تحليل جارية بالفعل")
43
  return False
44
+
45
  if not os.path.exists(document_path):
46
  logger.error(f"المستند غير موجود: {document_path}")
47
  return False
48
+
49
  self.analysis_in_progress = True
50
  self.current_document = document_path
51
  self.analysis_results = {
 
59
  "amounts": [],
60
  "risks": []
61
  }
62
+
63
  # بدء التحليل في خيط منفصل
64
  thread = threading.Thread(
65
  target=self._analyze_document_thread,
 
67
  )
68
  thread.daemon = True
69
  thread.start()
70
+
71
  return True
72
+
73
  def _analyze_document_thread(self, document_path, document_type, callback):
74
  """خيط تحليل المستند"""
75
  try:
76
  # تحديد نوع المستند
77
  file_extension = os.path.splitext(document_path)[1].lower()
78
+
79
  if file_extension == '.pdf':
80
  self.analysis_results = self._analyze_pdf(document_path, document_type)
81
  elif file_extension == '.docx':
 
88
  logger.error(f"نوع المستند غير مدعوم: {file_extension}")
89
  self.analysis_results["status"] = "فشل التحليل"
90
  self.analysis_results["error"] = "نوع المستند غير مدعوم"
91
+
92
  # تحديث حالة التحليل
93
  if self.analysis_results["status"] != "فشل التحليل":
94
  self.analysis_results["status"] = "اكتمل التحليل"
95
  self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
96
+
97
  logger.info(f"اكتمل تحليل المستند: {document_path}")
98
+
99
  except Exception as e:
100
  logger.error(f"خطأ في تحليل المستند: {str(e)}")
101
  self.analysis_results["status"] = "فشل التحليل"
102
  self.analysis_results["error"] = str(e)
103
+
104
  finally:
105
  self.analysis_in_progress = False
106
+
107
  # استدعاء دالة الاستجابة إذا تم توفيرها
108
  if callback and callable(callback):
109
  callback(self.analysis_results)
110
+
111
  def _analyze_pdf(self, document_path, document_type):
112
  """تحليل مستند PDF باستخدام الذكاء الاصطناعي"""
113
  try:
114
  # استخراج النص من PDF
115
  text = self._extract_text_from_pdf(document_path)
116
+
117
  # تحليل متقدم للمستند
118
  analysis = {
 
 
 
 
119
  "file_info": {
120
  "name": os.path.basename(document_path),
121
  "type": "PDF",
 
165
  except Exception as e:
166
  logger.error(f"خطأ في تحليل PDF: {str(e)}")
167
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  def _extract_text_from_pdf(self, document_path):
170
+ """استخراج النص من ملف PDF (تحتاج إلى مكتبة مثل PyPDF2 أو pdfplumber)"""
171
+ # Implementation using a PDF processing library like PyPDF2 or pdfplumber is needed here.
172
+ # This is a placeholder. Replace with actual PDF text extraction.
173
+ return "Placeholder text extracted from PDF"
 
 
 
 
 
 
 
 
174
 
175
  def _analyze_contract_terms(self, text):
176
  """تحليل بنود العقد"""
177
+ # Implementation for contract term analysis is needed here. This is a placeholder.
178
+ return "Placeholder contract terms analysis"
 
 
 
 
179
 
180
  def _analyze_financial_terms(self, text):
181
  """تحليل الجزء المالي"""
182
+ # Implementation for financial term analysis is needed here. This is a placeholder.
183
+ return "Placeholder financial terms analysis"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  def _analyze_legal_terms(self, text):
186
  """تحليل القانوني للعقد"""
187
+ # Implementation for legal term analysis is needed here. This is a placeholder.
188
+ return "Placeholder legal terms analysis"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  def _analyze_risks(self, text):
191
  """تحليل المخاطر"""
192
+ # Implementation for risk analysis is needed here. This is a placeholder.
193
+ return "Placeholder risk analysis"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  def _analyze_conditions(self, text):
196
  """دراسة كراسة الشروط"""
197
+ # Implementation for conditions analysis is needed here. This is a placeholder.
198
+ return "Placeholder conditions analysis"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  def _generate_summary(self, text):
201
  """توليد ملخص"""
202
+ # Implementation for summary generation is needed here. This is a placeholder.
203
+ return "Placeholder summary"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  def _generate_recommendations(self, text):
206
  """توليد التوصيات"""
207
+ # Implementation for recommendation generation is needed here. This is a placeholder.
208
+ return "Placeholder recommendations"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  def _analyze_docx(self, document_path, document_type):
213
  """تحليل مستند Word"""
214
  try:
215
+ # محاكاة تحليل مستند Word
216
+ logger.info(f"تحليل مستند Word: {document_path}")
217
+
218
+ # في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
219
+ # لاستخراج النص من ملف Word وتحليله
220
+
221
+ # محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
222
+ # (مشابه لتحليل PDF)
223
+ self.analysis_results["items"] = [
224
+ {"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
225
+ {"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
226
+ {"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
227
+ ]
228
 
229
+ # محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
230
+ # (مشابه لتحليل PDF)
231
 
232
  except Exception as e:
233
  logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
234
  raise
235
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  def _analyze_xlsx(self, document_path, document_type):
237
  """تحليل مستند Excel"""
238
  try:
239
+ # محاكاة تحليل مستند Excel
240
+ logger.info(f"تحليل مستند Excel: {document_path}")
241
 
242
+ # في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
243
+ # لاستخراج البيانات من ملف Excel وتحليلها
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ # محاكاة استخراج البنود
246
+ self.analysis_results["items"] = [
247
+ {"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
248
+ {"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
249
+ {"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
250
+ ]
251
 
252
+ # محاكاة استخراج المبالغ
253
+ self.analysis_results["amounts"] = [
254
+ {"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
255
+ {"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
256
+ {"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
257
+ ]
258
 
259
  except Exception as e:
260
  logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
261
  raise
262
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  def _analyze_txt(self, document_path, document_type):
264
  """تحليل مستند نصي"""
265
  try:
266
+ # محاكاة تحليل مستند نصي
267
+ logger.info(f"تحليل مستند نصي: {document_path}")
 
 
 
 
268
 
269
+ # في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
 
 
270
 
271
+ # محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
272
+ # (مشابه للتحليلات الأخرى)
273
 
274
  except Exception as e:
275
  logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
276
  raise
277
+
 
 
 
 
 
278
  def get_analysis_status(self):
279
  """الحصول على حالة التحليل الحالي"""
280
  if not self.analysis_in_progress:
 
282
  return {"status": "لا يوجد تحليل جارٍ"}
283
  else:
284
  return {"status": self.analysis_results.get("status", "غير معروف")}
285
+
286
  return {
287
  "status": "جاري التحليل",
288
  "document_path": self.current_document,
289
  "start_time": self.analysis_results.get("analysis_start_time")
290
  }
291
+
292
  def get_analysis_results(self):
293
  """الحصول على نتائج التحليل"""
294
  return self.analysis_results
295
+
296
  def export_analysis_results(self, output_path=None):
297
  """تصدير نتائج التحليل إلى ملف JSON"""
298
  if not self.analysis_results:
299
  logger.warning("لا توجد نتائج تحليل للتصدير")
300
  return None
301
+
302
  if not output_path:
303
  # إنشاء اسم ملف افتراضي
304
  timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
305
  filename = f"analysis_results_{timestamp}.json"
306
  output_path = os.path.join(self.documents_path, filename)
307
+
308
  try:
309
  with open(output_path, 'w', encoding='utf-8') as f:
310
  json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
311
+
312
  logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
313
  return output_path
314
+
315
  except Exception as e:
316
  logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
317
  return None
318
+
319
  def import_analysis_results(self, input_path):
320
  """استيراد نتائج التحليل من ملف JSON"""
321
  if not os.path.exists(input_path):
322
  logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
323
  return False
324
+
325
  try:
326
  with open(input_path, 'r', encoding='utf-8') as f:
327
  self.analysis_results = json.load(f)
328
+
329
  logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
330
  return True
331
+
332
  except Exception as e:
333
  logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
334
  return False
 
377
  # البحث عن القيم النقدية
378
  currency_pattern = r'[\d,]+\.?\d*\s*(?:ريال|دولار|SAR|USD)'
379
  currencies = re.findall(currency_pattern, text)
380
+
381
  # البحث عن النسب المئوية
382
  percentage_pattern = r'\d+\.?\d*\s*%'
383
  percentages = re.findall(percentage_pattern, text)
384
+
385
  return {
386
  "currencies": currencies,
387
  "percentages": percentages
 
395
  def _calculate_complexity(self, text):
396
  """حساب مستوى تعقيد النص"""
397
  words = text.split()
 
 
 
398
  avg_word_length = sum(len(word) for word in words) / len(words)
399
  sentences = text.split('.')
 
 
 
400
  avg_sentence_length = len(words) / len(sentences)
401
+
402
  # حساب درجة التعقيد (1-10)
403
  complexity = min((avg_word_length * 0.5 + avg_sentence_length * 0.2), 10)
404
  return round(complexity, 2)
 
413
  "الغرامات",
414
  "شروط الدفع"
415
  ]
416
+
417
  missing = []
418
  for section in required_sections:
419
  if section not in text:
420
  missing.append(section)
421
+
422
  return missing
423
 
424
  def _find_related_documents(self, document_path):
 
426
  directory = os.path.dirname(document_path)
427
  base_name = os.path.basename(document_path)
428
  related = []
429
+
430
  for file in os.listdir(directory):
431
  if file != base_name and file.startswith(base_name.split('_')[0]):
432
  related.append(file)
433
+
434
  return related