EGYADMIN commited on
Commit
864160b
·
verified ·
1 Parent(s): 8e13b9a

Update modules/cost_risk_analyzer.py

Browse files
Files changed (1) hide show
  1. modules/cost_risk_analyzer.py +832 -54
modules/cost_risk_analyzer.py CHANGED
@@ -1,38 +1,57 @@
 
 
 
 
1
  import os
2
  import json
3
  import re
4
  import requests
5
  import numpy as np
 
6
  from typing import Dict, List, Any, Union, Tuple, Optional
7
  from datetime import datetime
8
 
 
 
9
  class LLMProcessor:
10
  """
11
  فئة للتعامل مع نماذج اللغة الكبيرة (LLM) لتحليل المناقصات
12
  """
13
 
14
- def __init__(self, model_name: str = "claude-3-haiku-20240307", use_rag: bool = True):
15
  """
16
  تهيئة معالج نماذج اللغة الكبيرة
17
 
18
  المعاملات:
19
  ----------
20
- model_name : str, optional
21
- اسم النموذج المستخدم (افتراضي: "claude-3-haiku-20240307")
22
- use_rag : bool, optional
23
- استخدام تقنية RAG (Retrieval-Augmented Generation) (افتراضي: True)
24
  """
25
- self.model_name = model_name
26
- self.use_rag = use_rag
 
 
 
 
 
 
27
 
28
- # الحصول على مفتاح واجهة برمجة التطبيقات من متغيرات البيئة
29
- self.api_key = os.getenv("ANTHROPIC_API_KEY")
30
 
31
  # تهيئة قاعدة بيانات المتجهات إذا كان استخدام RAG مفعلاً
32
  if self.use_rag:
33
- self.vector_db = VectorDB()
 
 
 
 
 
 
 
 
34
 
35
- def analyze_requirements(self, requirements: List[Dict[str, Any]], context: Dict[str, Any]) -> Dict[str, Any]:
36
  """
37
  تحليل المتطلبات باستخدام نموذج اللغة الكبيرة
38
 
@@ -40,7 +59,7 @@ class LLMProcessor:
40
  ----------
41
  requirements : List[Dict[str, Any]]
42
  قائمة المتطلبات المستخرجة من المستندات
43
- context : Dict[str, Any]
44
  معلومات السياق الإضافية
45
 
46
  المخرجات:
@@ -48,18 +67,46 @@ class LLMProcessor:
48
  Dict[str, Any]
49
  نتائج تحليل المتطلبات
50
  """
51
- # إعداد الاستعلام بناءً على المتطلبات والسياق
52
- prompt = self._prepare_requirements_prompt(requirements, context)
53
 
54
- # استدعاء النموذج
55
- response = self._call_llm(prompt)
56
-
57
- # معالجة الاستجابة
58
- analysis = self._parse_requirements_response(response)
59
-
60
- return analysis
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- def analyze_local_content(self, local_content_data: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
63
  """
64
  تحليل بيانات المحتوى المحلي باستخدام نموذج اللغة الكبيرة
65
 
@@ -67,7 +114,7 @@ class LLMProcessor:
67
  ----------
68
  local_content_data : Dict[str, Any]
69
  بيانات المحتوى المحلي المستخرجة
70
- context : Dict[str, Any]
71
  معلومات السياق الإضافية
72
 
73
  المخرجات:
@@ -75,18 +122,40 @@ class LLMProcessor:
75
  Dict[str, Any]
76
  نتائج تحليل المحتوى المحلي
77
  """
78
- # إعداد الاستعلام بناءً على بيانات المحتوى المحلي والسياق
79
- prompt = self._prepare_local_content_prompt(local_content_data, context)
80
-
81
- # استدعاء النموذج
82
- response = self._call_llm(prompt)
83
-
84
- # معالجة الاستجابة
85
- analysis = self._parse_local_content_response(response)
86
 
87
- return analysis
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- def analyze_supply_chain(self, supply_chain_data: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
90
  """
91
  تحليل بيانات سلسلة الإمداد باستخدام نموذج اللغة الكبيرة
92
 
@@ -94,7 +163,7 @@ class LLMProcessor:
94
  ----------
95
  supply_chain_data : Dict[str, Any]
96
  بيانات سلسلة الإمداد المستخرجة
97
- context : Dict[str, Any]
98
  معلومات السياق الإضافية
99
 
100
  المخرجات:
@@ -102,16 +171,93 @@ class LLMProcessor:
102
  Dict[str, Any]
103
  نتائج تحليل سلسلة الإمداد
104
  """
105
- # إعداد الاستعلام بناءً على بيانات سلسلة الإمداد والسياق
106
- prompt = self._prepare_supply_chain_prompt(supply_chain_data, context)
107
 
108
- # استدعاء النموذج
109
- response = self._call_llm(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- # معالجة الاستجابة
112
- analysis = self._parse_supply_chain_response(response)
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- return analysis
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def generate_summary(self, extracted_data: Dict[str, Any], analysis_results: Dict[str, Any]) -> Dict[str, Any]:
117
  """
@@ -129,16 +275,87 @@ class LLMProcessor:
129
  Dict[str, Any]
130
  الملخص والتوصيات
131
  """
132
- # إعداد الاستعلام بناءً على البيانات المستخرجة ونتائج التحليل
133
- prompt = self._prepare_summary_prompt(extracted_data, analysis_results)
134
-
135
- # استدعاء النموذج
136
- response = self._call_llm(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- # معالجة الاستجابة
139
- summary = self._parse_summary_response(response)
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- return summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  def _prepare_requirements_prompt(self, requirements: List[Dict[str, Any]], context: Dict[str, Any]) -> str:
144
  """
@@ -154,8 +371,569 @@ class LLMProcessor:
154
  prompt += f"\n{i+1}. {req.get('title', 'متطلب')}: {req.get('description', '')}"
155
  prompt += f"\n الفئة: {req.get('category', 'عامة')}, الأهمية: {req.get('importance', 'عادية')}"
156
 
157
- prompt += """
158
- الرجاء تقديم تحليل تفصيلي لتكاليف المشروع والمخاطر المحتملة.
159
- يجب أن يشمل التحليل التكاليف المباشرة وغير المباشرة، وتأثير المخاطر على الميزانية.
160
- إذا كانت هناك أي توصيات لتخفيف المخاطر، يرجى توضيحها.
161
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ فئة للتعامل مع نماذج اللغة الكبيرة (LLM) لتحليل المناقصات
3
+ """
4
+
5
  import os
6
  import json
7
  import re
8
  import requests
9
  import numpy as np
10
+ import logging
11
  from typing import Dict, List, Any, Union, Tuple, Optional
12
  from datetime import datetime
13
 
14
+ logger = logging.getLogger(__name__)
15
+
16
  class LLMProcessor:
17
  """
18
  فئة للتعامل مع نماذج اللغة الكبيرة (LLM) لتحليل المناقصات
19
  """
20
 
21
+ def __init__(self, config: Dict[str, Any] = None):
22
  """
23
  تهيئة معالج نماذج اللغة الكبيرة
24
 
25
  المعاملات:
26
  ----------
27
+ config : Dict[str, Any], optional
28
+ إعدادات معالج نماذج اللغة الكبيرة
 
 
29
  """
30
+ self.config = config or {}
31
+ self.model_name = self.config.get('model_name', "claude-3-haiku-20240307")
32
+ self.use_rag = self.config.get('use_rag', True)
33
+ self.temperature = self.config.get('temperature', 0.7)
34
+ self.max_tokens = self.config.get('max_tokens', 4096)
35
+
36
+ # الحصول على مفتاح واجهة برمجة التطبيقات من متغيرات البيئة أو الإعدادات
37
+ self.api_key = self.config.get('api_key') or os.getenv("ANTHROPIC_API_KEY")
38
 
39
+ if not self.api_key:
40
+ logger.warning("لم يتم تحديد مفتاح واجهة برمجة التطبيقات لـ Anthropic. لن تعمل وظائف LLM.")
41
 
42
  # تهيئة قاعدة بيانات المتجهات إذا كان استخدام RAG مفعلاً
43
  if self.use_rag:
44
+ try:
45
+ from utils.vector_db import VectorDB
46
+ self.vector_db = VectorDB(self.config.get('vector_db_config', {}))
47
+ logger.info("تم تهيئة قاعدة بيانات المتجهات بنجاح.")
48
+ except ImportError as e:
49
+ logger.error(f"فشل في تهيئة قاعدة بيانات المتجهات: {str(e)}")
50
+ self.use_rag = False
51
+
52
+ logger.info(f"تم تهيئة معالج نماذج اللغة الكبيرة: {self.model_name}, RAG: {self.use_rag}")
53
 
54
+ def analyze_requirements(self, requirements: List[Dict[str, Any]], context: Dict[str, Any] = None) -> Dict[str, Any]:
55
  """
56
  تحليل المتطلبات باستخدام نموذج اللغة الكبيرة
57
 
 
59
  ----------
60
  requirements : List[Dict[str, Any]]
61
  قائمة المتطلبات المستخرجة من المستندات
62
+ context : Dict[str, Any], optional
63
  معلومات السياق الإضافية
64
 
65
  المخرجات:
 
67
  Dict[str, Any]
68
  نتائج تحليل المتطلبات
69
  """
70
+ context = context or {}
 
71
 
72
+ try:
73
+ # إعداد الاستعلام بناءً على المتطلبات والسياق
74
+ prompt = self._prepare_requirements_prompt(requirements, context)
75
+
76
+ # استرجاع معلومات إضافية من قاعدة البيانات إذا كان استخدام RAG مفعلاً
77
+ if self.use_rag:
78
+ retrieval_results = self.vector_db.search(
79
+ query=self._create_search_query(requirements),
80
+ collection="requirements",
81
+ limit=5
82
+ )
83
+ prompt += "\n\nمعلومات إضافية مسترجعة:\n" + self._format_retrieval_results(retrieval_results)
84
+
85
+ # استدعاء النموذج
86
+ response = self._call_llm(prompt)
87
+
88
+ # معالجة الاستجابة
89
+ analysis = self._parse_requirements_response(response)
90
+
91
+ logger.info("تم تحليل المتطلبات بنجاح.")
92
+ return analysis
93
+
94
+ except Exception as e:
95
+ logger.error(f"فشل في تحليل المتطلبات: {str(e)}")
96
+ return {
97
+ "summary": "حدث خطأ أثناء تحليل المتطلبات",
98
+ "error": str(e),
99
+ "technical": [],
100
+ "financial": [],
101
+ "legal": [],
102
+ "local_content": [],
103
+ "total_count": len(requirements),
104
+ "mandatory_count": 0,
105
+ "avg_difficulty": 0,
106
+ "local_content_percentage": 0
107
+ }
108
 
109
+ def analyze_local_content(self, local_content_data: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
110
  """
111
  تحليل بيانات المحتوى المحلي باستخدام نموذج اللغة الكبيرة
112
 
 
114
  ----------
115
  local_content_data : Dict[str, Any]
116
  بيانات المحتوى المحلي المستخرجة
117
+ context : Dict[str, Any], optional
118
  معلومات السياق الإضافية
119
 
120
  المخرجات:
 
122
  Dict[str, Any]
123
  نتائج تحليل المحتوى المحلي
124
  """
125
+ context = context or {}
 
 
 
 
 
 
 
126
 
127
+ try:
128
+ # إعداد الاستعلام بناءً على بيانات المحتوى المحلي والسياق
129
+ prompt = self._prepare_local_content_prompt(local_content_data, context)
130
+
131
+ # استرجاع معلومات إضافية من قاعدة البيانات إذا كان استخدام RAG مفعلاً
132
+ if self.use_rag:
133
+ retrieval_results = self.vector_db.search(
134
+ query=self._create_search_query(local_content_data),
135
+ collection="local_content",
136
+ limit=5
137
+ )
138
+ prompt += "\n\nمعلومات إضافية مسترجعة:\n" + self._format_retrieval_results(retrieval_results)
139
+
140
+ # استدعاء النموذج
141
+ response = self._call_llm(prompt)
142
+
143
+ # معالجة الاستجابة
144
+ analysis = self._parse_local_content_response(response)
145
+
146
+ logger.info("تم تحليل بيانات المحتوى المحلي بنجاح.")
147
+ return analysis
148
+
149
+ except Exception as e:
150
+ logger.error(f"فشل في تحليل بيانات المحتوى المحلي: {str(e)}")
151
+ return {
152
+ "estimated_local_content": 0,
153
+ "required_local_content": 0,
154
+ "required_materials": [],
155
+ "improvement_strategies": []
156
+ }
157
 
158
+ def analyze_supply_chain(self, supply_chain_data: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
159
  """
160
  تحليل بيانات سلسلة الإمداد باستخدام نموذج اللغة الكبيرة
161
 
 
163
  ----------
164
  supply_chain_data : Dict[str, Any]
165
  بيانات سلسلة الإمداد المستخرجة
166
+ context : Dict[str, Any], optional
167
  معلومات السياق الإضافية
168
 
169
  المخرجات:
 
171
  Dict[str, Any]
172
  نتائج تحليل سلسلة الإمداد
173
  """
174
+ context = context or {}
 
175
 
176
+ try:
177
+ # إعداد الاستعلام بناءً على بيانات سلسلة الإمداد والسياق
178
+ prompt = self._prepare_supply_chain_prompt(supply_chain_data, context)
179
+
180
+ # استرجاع معلومات إ��افية من قاعدة البيانات إذا كان استخدام RAG مفعلاً
181
+ if self.use_rag:
182
+ retrieval_results = self.vector_db.search(
183
+ query=self._create_search_query(supply_chain_data),
184
+ collection="supply_chain",
185
+ limit=5
186
+ )
187
+ prompt += "\n\nمعلومات إضافية مسترجعة:\n" + self._format_retrieval_results(retrieval_results)
188
+
189
+ # استدعاء النموذج
190
+ response = self._call_llm(prompt)
191
+
192
+ # معالجة الاستجابة
193
+ analysis = self._parse_supply_chain_response(response)
194
+
195
+ logger.info("تم تحليل بيانات سلسلة الإمداد بنجاح.")
196
+ return analysis
197
+
198
+ except Exception as e:
199
+ logger.error(f"فشل في تحليل بيانات سلسلة الإمداد: {str(e)}")
200
+ return {
201
+ "supply_chain_risks": [],
202
+ "optimizations": [],
203
+ "local_suppliers_availability": 0
204
+ }
205
+
206
+ def analyze_risks(self, extracted_text: str, context: Dict[str, Any] = None) -> Dict[str, Any]:
207
+ """
208
+ تحليل المخاطر من نص المناقصة
209
 
210
+ المعاملات:
211
+ ----------
212
+ extracted_text : str
213
+ النص المستخرج من المناقصة
214
+ context : Dict[str, Any], optional
215
+ معلومات السياق الإضافية
216
+
217
+ المخرجات:
218
+ --------
219
+ Dict[str, Any]
220
+ نتائج تحليل المخاطر
221
+ """
222
+ context = context or {}
223
 
224
+ try:
225
+ # إعداد الاستعلام
226
+ prompt = self._prepare_risk_analysis_prompt(extracted_text, context)
227
+
228
+ # استرجاع معلومات إضافية من قاعدة البيانات إذا كان استخدام RAG مفعلاً
229
+ if self.use_rag:
230
+ # استخراج كلمات مفتاحية من النص
231
+ keywords = self._extract_keywords(extracted_text)
232
+ query = " ".join(keywords[:10]) # استخدام أهم 10 كلمات مفتاحية
233
+
234
+ retrieval_results = self.vector_db.search(
235
+ query=query,
236
+ collection="risks",
237
+ limit=5
238
+ )
239
+ prompt += "\n\nمعلومات إضافية مسترجعة:\n" + self._format_retrieval_results(retrieval_results)
240
+
241
+ # استدعاء النموذج
242
+ response = self._call_llm(prompt)
243
+
244
+ # معالجة الاستجابة
245
+ analysis = self._parse_risk_response(response)
246
+
247
+ logger.info("تم تحليل المخاطر بنجاح.")
248
+ return analysis
249
+
250
+ except Exception as e:
251
+ logger.error(f"فشل في تحليل المخاطر: {str(e)}")
252
+ return {
253
+ "all_risks": [],
254
+ "technical_risks": [],
255
+ "financial_risks": [],
256
+ "supply_chain_risks": [],
257
+ "legal_risks": [],
258
+ "mitigation_plan": [],
259
+ "avg_severity": 0
260
+ }
261
 
262
  def generate_summary(self, extracted_data: Dict[str, Any], analysis_results: Dict[str, Any]) -> Dict[str, Any]:
263
  """
 
275
  Dict[str, Any]
276
  الملخص والتوصيات
277
  """
278
+ try:
279
+ # إعداد الاستعلام بناءً على البيانات المستخرجة ونتائج التحليل
280
+ prompt = self._prepare_summary_prompt(extracted_data, analysis_results)
281
+
282
+ # استرجاع معلومات إضافية من قاعدة البيانات إذا كان استخدام RAG مفعلاً
283
+ if self.use_rag:
284
+ # إنشاء استعلام متجه من البيانات المستخرجة
285
+ query = self._create_search_query(extracted_data)
286
+
287
+ retrieval_results = self.vector_db.search(
288
+ query=query,
289
+ collection="summaries",
290
+ limit=3
291
+ )
292
+ prompt += "\n\nأمثلة مشابهة من المناقصات السابقة:\n" + self._format_retrieval_results(retrieval_results)
293
+
294
+ # استدعاء ��لنموذج
295
+ response = self._call_llm(prompt)
296
+
297
+ # معالجة الاستجابة
298
+ summary = self._parse_summary_response(response)
299
+
300
+ logger.info("تم إعداد الملخص بنجاح.")
301
+ return summary
302
+
303
+ except Exception as e:
304
+ logger.error(f"فشل في إعداد الملخص: {str(e)}")
305
+ return {
306
+ "executive_summary": "حدث خطأ أثناء إعداد الملخص",
307
+ "recommendations": [],
308
+ "bid_strategy": {},
309
+ "error": str(e)
310
+ }
311
+
312
+ def _call_llm(self, prompt: str) -> str:
313
+ """
314
+ استدعاء نموذج اللغة الكبيرة
315
 
316
+ المعاملات:
317
+ ----------
318
+ prompt : str
319
+ الاستعلام المُرسل للنموذج
320
+
321
+ المخرجات:
322
+ --------
323
+ str
324
+ استجابة النموذج
325
+ """
326
+ if not self.api_key:
327
+ logger.warning("لم يتم تحديد مفتاح واجهة برمجة التطبيقات.")
328
+ return "لم يتم تحديد مفتاح واجهة برمجة التطبيقات. يرجى تكوين مفتاح API في الإعدادات."
329
 
330
+ try:
331
+ headers = {
332
+ "x-api-key": self.api_key,
333
+ "content-type": "application/json",
334
+ "anthropic-version": "2023-06-01"
335
+ }
336
+
337
+ data = {
338
+ "model": self.model_name,
339
+ "messages": [{"role": "user", "content": prompt}],
340
+ "temperature": self.temperature,
341
+ "max_tokens": self.max_tokens
342
+ }
343
+
344
+ response = requests.post(
345
+ "https://api.anthropic.com/v1/messages",
346
+ headers=headers,
347
+ json=data
348
+ )
349
+
350
+ if response.status_code == 200:
351
+ return response.json()["content"][0]["text"]
352
+ else:
353
+ logger.error(f"فشل في استدعاء النموذج: {response.status_code} - {response.text}")
354
+ return f"فشل في استدعاء النموذج: {response.status_code}"
355
+
356
+ except Exception as e:
357
+ logger.error(f"خطأ في استدعاء النموذج: {str(e)}")
358
+ return f"خطأ في استدعاء النموذج: {str(e)}"
359
 
360
  def _prepare_requirements_prompt(self, requirements: List[Dict[str, Any]], context: Dict[str, Any]) -> str:
361
  """
 
371
  prompt += f"\n{i+1}. {req.get('title', 'متطلب')}: {req.get('description', '')}"
372
  prompt += f"\n الفئة: {req.get('category', 'عامة')}, الأهمية: {req.get('importance', 'عادية')}"
373
 
374
+ prompt += """
375
+
376
+ يرجى تقديم التحليل التالي:
377
+ 1. ملخص شامل للمتطلبات وجودتها العامة
378
+ 2. تصنيف المتطلبات إلى الفئات التالية:
379
+ - متطلبات فنية
380
+ - متطلبات مالية
381
+ - متطلبات قانونية
382
+ - متطلبات المحتوى المحلي
383
+ 3. تحديد عدد المتطلبات الإلزامية
384
+ 4. تقييم متوسط صعوبة التنفيذ (على مقياس من 1 إلى 5)
385
+ 5. تقدير نسبة متطلبات المحتوى المحلي من إجمالي المتطلبات
386
+ 6. تحديد أي فجوات أو تناقضات في المتطلبات
387
+ 7. اقتراح تحسينات للمتطلبات غير الواضحة
388
+
389
+ قدم الإجابة بتنسيق JSON التالي:
390
+ {
391
+ "summary": "ملخص شامل للمتطلبات",
392
+ "technical": [{"title": "عنوان المتطلب", "description": "وصف المتطلب", "analysis": "تحليل المتطلب", "importance": "الأهمية", "difficulty": 3, "gaps": "الفجوات", "improvements": "التحسينات المقترحة"}],
393
+ "financial": [...],
394
+ "legal": [...],
395
+ "local_content": [...],
396
+ "total_count": 10,
397
+ "mandatory_count": 5,
398
+ "avg_difficulty": 3.5,
399
+ "local_content_percentage": 25
400
+ }
401
+ """
402
+
403
+ if context:
404
+ prompt += "\n\nمعلوم��ت سياقية إضافية:\n"
405
+ for key, value in context.items():
406
+ prompt += f"{key}: {value}\n"
407
+
408
+ return prompt
409
+
410
+ def _prepare_local_content_prompt(self, local_content_data: Dict[str, Any], context: Dict[str, Any]) -> str:
411
+ """
412
+ إعداد استعلام لتحليل المحتوى المحلي
413
+ """
414
+ prompt = """
415
+ أنت خبير في تحليل المحتوى المحلي وسلاسل الإمداد في المملكة العربية السعودية. يرجى تحليل البيانات التالية وتقديم توصيات لتحسين نسبة المحتوى المحلي.
416
+
417
+ بيانات المحتوى المحلي:
418
+ """
419
+
420
+ prompt += f"\nنسبة المحتوى المحلي المطلوبة: {local_content_data.get('required_percentage', 'غير محدد')}"
421
+ prompt += f"\nالفئة: {local_content_data.get('category', 'غير محدد')}"
422
+
423
+ if 'materials' in local_content_data:
424
+ prompt += "\n\nالمواد والمنتجات المطلوبة:"
425
+ for material in local_content_data['materials']:
426
+ prompt += f"\n- {material.get('name', '')} - الكمية: {material.get('quantity', '')}, وحدة القياس: {material.get('unit', '')}"
427
+
428
+ if 'services' in local_content_data:
429
+ prompt += "\n\nالخدمات المطلوبة:"
430
+ for service in local_content_data['services']:
431
+ prompt += f"\n- {service.get('name', '')} - {service.get('description', '')}"
432
+
433
+ prompt += """
434
+
435
+ يرجى تقديم التحليل التالي:
436
+ 1. تقدير نسبة المحتوى المحلي المتوقعة بناءً على البيانات المقدمة
437
+ 2. تحليل مدى توافق النسبة المتوقعة مع النسبة المطلوبة
438
+ 3. تحديد المواد والمنتجات التي يمكن توريدها محلياً
439
+ 4. اقتراح استراتيجيات لزيادة نسبة المحتوى المحلي
440
+ 5. تحديد الموردين المحليين المحتملين (إن وجدت معلومات)
441
+
442
+ قدم الإجابة بتنسيق JSON التالي:
443
+ {
444
+ "estimated_local_content": 35.5,
445
+ "required_local_content": 40,
446
+ "required_materials": [
447
+ {"name": "اسم المادة", "quantity": "الكمية", "unit": "وحدة القياس", "local_availability": "متوفر محلياً؟", "potential_suppliers": ["مورد 1", "مورد 2"]}
448
+ ],
449
+ "improvement_strategies": ["استراتيجية 1", "استراتيجية 2"]
450
+ }
451
+ """
452
+
453
+ if context:
454
+ prompt += "\n\nمعلومات سياقية إضافية:\n"
455
+ for key, value in context.items():
456
+ prompt += f"{key}: {value}\n"
457
+
458
+ return prompt
459
+
460
+ def _prepare_supply_chain_prompt(self, supply_chain_data: Dict[str, Any], context: Dict[str, Any]) -> str:
461
+ """
462
+ إعداد استعلام لتحليل سلسلة الإمداد
463
+ """
464
+ prompt = """
465
+ أنت خبير في سلاسل الإمداد والمشتريات. يرجى تحليل بيانات سلسلة الإمداد التالية وتقديم توصيات لتحسين الكفاءة وتقليل المخاطر.
466
+
467
+ بيانات سلسلة الإمداد:
468
+ """
469
+
470
+ if 'materials' in supply_chain_data:
471
+ prompt += "\n\nالمواد والمنتجات:"
472
+ for material in supply_chain_data['materials']:
473
+ prompt += f"\n- {material.get('name', '')} - المصدر: {material.get('source', '')}, وقت التوريد: {material.get('lead_time', '')}"
474
+
475
+ if 'suppliers' in supply_chain_data:
476
+ prompt += "\n\nالموردين:"
477
+ for supplier in supply_chain_data['suppliers']:
478
+ prompt += f"\n- {supplier.get('name', '')} - الموقع: {supplier.get('location', '')}, التصنيف: {supplier.get('rating', '')}"
479
+
480
+ if 'logistics' in supply_chain_data:
481
+ prompt += "\n\nالخدمات اللوجستية:"
482
+ for logistic in supply_chain_data['logistics']:
483
+ prompt += f"\n- {logistic.get('type', '')} - المدة: {logistic.get('duration', '')}, التكلفة: {logistic.get('cost', '')}"
484
+
485
+ prompt += """
486
+
487
+ يرجى تقديم التحليل التالي:
488
+ 1. تحليل المخاطر المحتملة في سلسلة الإمداد
489
+ 2. اقتراح تحسينات لتقليل المخاطر وزيادة الكفاءة
490
+ 3. تقييم مدى توفر الموردين المحليين للمواد المطلوبة
491
+ 4. اقتراح استراتيجية مشتريات فعالة
492
+
493
+ قدم الإجابة بتنسيق JSON ��لتالي:
494
+ {
495
+ "supply_chain_risks": [
496
+ {"risk": "وصف المخاطرة", "severity": 4, "probability": 3, "impact": "تأثير المخاطرة", "mitigation": "إجراءات التخفيف"}
497
+ ],
498
+ "optimizations": ["تحسين 1", "تحسين 2"],
499
+ "local_suppliers_availability": 65.5,
500
+ "procurement_strategy": {
501
+ "approach": "نهج المشتريات",
502
+ "timeline": "الجدول الزمني",
503
+ "key_considerations": ["اعتبار 1", "اعتبار 2"]
504
+ }
505
+ }
506
+ """
507
+
508
+ if context:
509
+ prompt += "\n\nمعلومات سياقية إضافية:\n"
510
+ for key, value in context.items():
511
+ prompt += f"{key}: {value}\n"
512
+
513
+ return prompt
514
+
515
+ def _prepare_risk_analysis_prompt(self, extracted_text: str, context: Dict[str, Any]) -> str:
516
+ """
517
+ إعداد استعلام لتحليل المخاطر
518
+ """
519
+ # اختصار النص المستخرج إذا كان طويلاً جدًا
520
+ max_chars = 15000 # الحد الأقصى لطول النص
521
+ if len(extracted_text) > max_chars:
522
+ extracted_text = extracted_text[:max_chars] + "... (تم اختصار النص)"
523
+
524
+ prompt = """
525
+ أنت خبير في تحليل المخاطر في المناقصات والمشاريع. يرجى تحليل النص التالي من وثيقة المناقصة وتحديد المخاطر المحتملة.
526
+
527
+ نص المناقصة:
528
+ """
529
+
530
+ prompt += f"\n{extracted_text}"
531
+
532
+ prompt += """
533
+
534
+ يرجى تحديد وتحليل المخاطر التالية:
535
+ 1. المخاطر التقنية
536
+ 2. المخاطر المالية
537
+ 3. مخاطر سلسلة الإمداد
538
+ 4. المخاطر القانونية
539
+
540
+ لكل مخاطرة، يرجى تقديم:
541
+ - وصف المخاطرة
542
+ - درجة الخطورة (على مقياس من 1 إلى 5)
543
+ - احتمالية الحدوث (على مقياس من 1 إلى 5)
544
+ - التأثير المحتمل
545
+ - إجراءات مقترحة للتخفيف
546
+
547
+ يرجى أيضًا تقديم خطة شاملة للتخفيف من المخاطر العالية.
548
+
549
+ قدم الإجابة بتنسيق JSON التالي:
550
+ {
551
+ "all_risks": [
552
+ {"risk": "وصف المخاطرة", "type": "نوع المخاطرة", "severity": 4, "probability": 3, "impact": "تأثير المخاطرة", "mitigation": "إجراءات التخفيف"}
553
+ ],
554
+ "technical_risks": [...],
555
+ "financial_risks": [...],
556
+ "supply_chain_risks": [...],
557
+ "legal_risks": [...],
558
+ "mitigation_plan": ["خطوة 1", "خطوة 2"],
559
+ "avg_severity": 3.5
560
+ }
561
+ """
562
+
563
+ if context:
564
+ prompt += "\n\nمعلومات سياقية إضافية:\n"
565
+ for key, value in context.items():
566
+ prompt += f"{key}: {value}\n"
567
+
568
+ return prompt
569
+
570
+ def _prepare_summary_prompt(self, extracted_data: Dict[str, Any], analysis_results: Dict[str, Any]) -> str:
571
+ """
572
+ إعداد استعلام لإعداد ملخص شامل
573
+ """
574
+ prompt = """
575
+ أنت مستشار استراتيجي في مجال المناقصات والمشاريع. يرجى إعداد ملخص تنفيذي شامل وتوصيات استراتيجية بناءً على البيانات والتحليلات التالية.
576
+
577
+ البيانات المستخرجة:
578
+ """
579
+
580
+ # إضافة البيانات المستخرجة
581
+ for key, value in extracted_data.items():
582
+ if isinstance(value, dict) or isinstance(value, list):
583
+ prompt += f"\n{key}: {json.dumps(value, ensure_ascii=False)[:500]}..."
584
+ else:
585
+ prompt += f"\n{key}: {str(value)[:500]}..."
586
+
587
+ prompt += "\n\nنتائج التحليلات:"
588
+
589
+ # إضافة نتائج التحليلات
590
+ for key, value in analysis_results.items():
591
+ if isinstance(value, dict) or isinstance(value, list):
592
+ prompt += f"\n{key}: {json.dumps(value, ensure_ascii=False)[:500]}..."
593
+ else:
594
+ prompt += f"\n{key}: {str(value)[:500]}..."
595
+
596
+ prompt += """
597
+
598
+ يرجى إعداد:
599
+ 1. ملخص تنفيذي شامل للمناقصة
600
+ 2. توصيات استراتيجية للتقدم للمناقصة
601
+ 3. استراتيجية تقديم العطاء، بما في ذلك النقاط التنافسية والأسعار المقترحة
602
+ 4. خطة عمل لتحسين فرص الفوز بالمناقصة
603
+
604
+ قدم الإجابة ب��نسيق JSON التالي:
605
+ {
606
+ "executive_summary": "ملخص تنفيذي شامل للمناقصة",
607
+ "recommendations": ["توصية 1", "توصية 2", "توصية 3"],
608
+ "bid_strategy": {
609
+ "competitive_points": ["نقطة 1", "نقطة 2"],
610
+ "proposed_price": "السعر المقترح",
611
+ "profit_margin": "هامش الربح المقترح"
612
+ },
613
+ "action_plan": ["خطوة 1", "خطوة 2", "خطوة 3"]
614
+ }
615
+ """
616
+
617
+ return prompt
618
+
619
+ def _parse_requirements_response(self, response: str) -> Dict[str, Any]:
620
+ """
621
+ تحليل استجابة المتطلبات من النموذج
622
+ """
623
+ try:
624
+ # البحث عن كتلة JSON في الاستجابة
625
+ json_match = re.search(r'```json\s*(.*?)```', response, re.DOTALL)
626
+ if not json_match:
627
+ json_match = re.search(r'{.*}', response, re.DOTALL)
628
+
629
+ if json_match:
630
+ json_str = json_match.group(1) if json_match.groups() else json_match.group(0)
631
+ return json.loads(json_str)
632
+ else:
633
+ logger.warning("لم يتم العثور على JSON في استجابة تحليل المتطلبات. استخدام قيم افتراضية.")
634
+ return {
635
+ "summary": "لم يتم تحليل المتطلبات بشكل صحيح",
636
+ "technical": [],
637
+ "financial": [],
638
+ "legal": [],
639
+ "local_content": [],
640
+ "total_count": 0,
641
+ "mandatory_count": 0,
642
+ "avg_difficulty": 0,
643
+ "local_content_percentage": 0
644
+ }
645
+ except json.JSONDecodeError as e:
646
+ logger.error(f"خطأ في تحليل استجابة JSON للمتطلبات: {str(e)}")
647
+ return {
648
+ "summary": "حدث خطأ في تحليل استجابة JSON",
649
+ "error": str(e),
650
+ "raw_response": response[:1000] + "..." if len(response) > 1000 else response,
651
+ "technical": [],
652
+ "financial": [],
653
+ "legal": [],
654
+ "local_content": [],
655
+ "total_count": 0,
656
+ "mandatory_count": 0,
657
+ "avg_difficulty": 0,
658
+ "local_content_percentage": 0
659
+ }
660
+
661
+ def _parse_local_content_response(self, response: str) -> Dict[str, Any]:
662
+ """
663
+ تحليل استجابة المحتوى المحلي من النموذج
664
+ """
665
+ try:
666
+ # البحث عن كتلة JSON في الاستجابة
667
+ json_match = re.search(r'```json\s*(.*?)```', response, re.DOTALL)
668
+ if not json_match:
669
+ json_match = re.search(r'{.*}', response, re.DOTALL)
670
+
671
+ if json_match:
672
+ json_str = json_match.group(1) if json_match.groups() else json_match.group(0)
673
+ return json.loads(json_str)
674
+ else:
675
+ logger.warning("لم يتم العثور على JSON في استجابة تحليل المحتوى المحلي. استخدام قيم افتراضية.")
676
+ return {
677
+ "estimated_local_content": 0,
678
+ "required_local_content": 0,
679
+ "required_materials": [],
680
+ "improvement_strategies": []
681
+ }
682
+ except json.JSONDecodeError as e:
683
+ logger.error(f"خطأ في تحليل استجابة JSON للمحتوى المحلي: {str(e)}")
684
+ return {
685
+ "estimated_local_content": 0,
686
+ "required_local_content": 0,
687
+ "required_materials": [],
688
+ "improvement_strategies": [],
689
+ "error": str(e),
690
+ "raw_response": response[:1000] + "..." if len(response) > 1000 else response
691
+ }
692
+
693
+ def _parse_supply_chain_response(self, response: str) -> Dict[str, Any]:
694
+ """
695
+ تحليل استجابة سلسلة الإمداد من النموذج
696
+ """
697
+ try:
698
+ # البحث عن كتلة JSON في الاستجابة
699
+ json_match = re.search(r'```json\s*(.*?)```', response, re.DOTALL)
700
+ if not json_match:
701
+ json_match = re.search(r'{.*}', response, re.DOTALL)
702
+
703
+ if json_match:
704
+ json_str = json_match.group(1) if json_match.groups() else json_match.group(0)
705
+ return json.loads(json_str)
706
+ else:
707
+ logger.warning("لم يتم العثور على JSON في استجابة تحليل سلسلة الإمداد. استخدام قيم افتراضية.")
708
+ return {
709
+ "supply_chain_risks": [],
710
+ "optimizations": [],
711
+ "local_suppliers_availability": 0,
712
+ "procurement_strategy": {}
713
+ }
714
+ except json.JSONDecodeError as e:
715
+ logger.error(f"خطأ في تحليل استجابة JSON لسلسلة الإمداد: {str(e)}")
716
+ return {
717
+ "supply_chain_risks": [],
718
+ "optimizations": [],
719
+ "local_suppliers_availability": 0,
720
+ "procurement_strategy": {},
721
+ "error": str(e),
722
+ "raw_response": response[:1000] + "..." if len(response) > 1000 else response
723
+ }
724
+
725
+ def _parse_risk_response(self, response: str) -> Dict[str, Any]:
726
+ """
727
+ تحليل استجابة تحليل المخاطر من النموذج
728
+ """
729
+ try:
730
+ # البحث عن كتلة JSON في الاستجابة
731
+ json_match = re.search(r'```json\s*(.*?)```', response, re.DOTALL)
732
+ if not json_match:
733
+ json_match = re.search(r'{.*}', response, re.DOTALL)
734
+
735
+ if json_match:
736
+ json_str = json_match.group(1) if json_match.groups() else json_match.group(0)
737
+ return json.loads(json_str)
738
+ else:
739
+ logger.warning("لم يتم العثور على JSON في استجابة تحليل المخاطر. استخدام قيم افتراضية.")
740
+ return {
741
+ "all_risks": [],
742
+ "technical_risks": [],
743
+ "financial_risks": [],
744
+ "supply_chain_risks": [],
745
+ "legal_risks": [],
746
+ "mitigation_plan": [],
747
+ "avg_severity": 0
748
+ }
749
+ except json.JSONDecodeError as e:
750
+ logger.error(f"خطأ في تحليل استجابة JSON للمخاطر: {str(e)}")
751
+ return {
752
+ "all_risks": [],
753
+ "technical_risks": [],
754
+ "financial_risks": [],
755
+ "supply_chain_risks": [],
756
+ "legal_risks": [],
757
+ "mitigation_plan": [],
758
+ "avg_severity": 0,
759
+ "error": str(e),
760
+ "raw_response": response[:1000] + "..." if len(response) > 1000 else response
761
+ }
762
+
763
+ def _parse_summary_response(self, response: str) -> Dict[str, Any]:
764
+ """
765
+ تحليل استجابة الملخص من النموذج
766
+ """
767
+ try:
768
+ # البحث عن كتلة JSON في الاستجابة
769
+ json_match = re.search(r'```json\s*(.*?)```', response, re.DOTALL)
770
+ if not json_match:
771
+ json_match = re.search(r'{.*}', response, re.DOTALL)
772
+
773
+ if json_match:
774
+ json_str = json_match.group(1) if json_match.groups() else json_match.group(0)
775
+ return json.loads(json_str)
776
+ else:
777
+ logger.warning("لم يتم العثور على JSON في استجابة الملخص. استخدام قيم افتراضية.")
778
+ return {
779
+ "executive_summary": "لم يتم إعداد الملخص بشكل صحيح",
780
+ "recommendations": [],
781
+ "bid_strategy": {},
782
+ "action_plan": []
783
+ }
784
+ except json.JSONDecodeError as e:
785
+ logger.error(f"خطأ في تحليل استجابة JSON للملخص: {str(e)}")
786
+ return {
787
+ "executive_summary": "حدث خطأ في تحليل استجابة JSON",
788
+ "recommendations": [],
789
+ "bid_strategy": {},
790
+ "action_plan": [],
791
+ "error": str(e),
792
+ "raw_response": response[:1000] + "..." if len(response) > 1000 else response
793
+ }
794
+
795
+ def _create_search_query(self, data) -> str:
796
+ """
797
+ إنشاء استعلام بحث من البيانات
798
+ """
799
+ if isinstance(data, list):
800
+ # إذا كانت البيانات قائمة، جمع النصوص من العناصر
801
+ texts = []
802
+ for item in data:
803
+ if isinstance(item, dict):
804
+ texts.extend([str(v) for k, v in item.items() if isinstance(v, (str, int, float))])
805
+ else:
806
+ texts.append(str(item))
807
+ return " ".join(texts[:20]) # استخدام أول 20 عنصر فقط
808
+
809
+ elif isinstance(data, dict):
810
+ # إذا كانت البيانات قاموس، جمع القيم النصية
811
+ texts = [str(v) for k, v in data.items() if isinstance(v, (str, int, float))]
812
+ return " ".join(texts[:20]) # استخدام أول 20 عنصر فقط
813
+
814
+ else:
815
+ # إذا كانت البيانات نص أو قيمة أخرى
816
+ return str(data)
817
+
818
+ def _format_retrieval_results(self, results: List[Dict[str, Any]]) -> str:
819
+ """
820
+ تنسيق نتائج الاسترجاع من قاعدة ��لبيانات
821
+ """
822
+ if not results:
823
+ return "لا توجد نتائج مسترجعة."
824
+
825
+ formatted = ""
826
+ for i, result in enumerate(results):
827
+ formatted += f"\n{i+1}. "
828
+ if 'title' in result:
829
+ formatted += f"{result['title']}: "
830
+ if 'content' in result:
831
+ content = result['content']
832
+ # اقتصار المحتوى إذا كان طويلاً
833
+ if len(content) > 300:
834
+ content = content[:300] + "..."
835
+ formatted += content
836
+ if 'metadata' in result and isinstance(result['metadata'], dict):
837
+ formatted += f"\n معلومات إضافية: {', '.join([f'{k}: {v}' for k, v in result['metadata'].items() if k != 'text'])}"
838
+ formatted += "\n"
839
+
840
+ return formatted
841
+
842
+ def _extract_keywords(self, text: str, top_n: int = 20) -> List[str]:
843
+ """
844
+ استخراج الكلمات المفتاحية من النص
845
+
846
+ المعاملات:
847
+ ----------
848
+ text : str
849
+ النص المراد استخراج الكلمات المفتاحية منه
850
+ top_n : int, optional
851
+ عدد الكلمات المفتاحية المراد استخراجها
852
+
853
+ المخرجات:
854
+ --------
855
+ List[str]
856
+ قائمة بالكلمات المفتاحية
857
+ """
858
+ # قائمة الكلمات الغير مهمة (stop words) باللغة العربية
859
+ arabic_stop_words = ['من', 'الى', 'إلى', 'عن', 'على', 'في', 'و', 'ا', 'ان', 'أن', 'لا', 'ما', 'هذا', 'هذه', 'ذلك', 'تلك', 'هناك', 'هنالك', 'هو', 'هي', 'هم']
860
+
861
+ # تنظيف النص وتقسيمه إلى كلمات
862
+ words = re.findall(r'[\u0600-\u06FF]+|[a-zA-Z]+', text)
863
+
864
+ # تحويل الكلمات إلى أحرف صغيرة وإزالة الكلمات الغير مهمة
865
+ filtered_words = [word.lower() for word in words if word.lower() not in arabic_stop_words and len(word) > 2]
866
+
867
+ # حساب تكرار الكلمات
868
+ word_counts = {}
869
+ for word in filtered_words:
870
+ word_counts[word] = word_counts.get(word, 0) + 1
871
+
872
+ # ترتيب الكلمات حسب التكرار والحصول على أهم الكلمات
873
+ sorted_words = [word for word, count in sorted(word_counts.items(), key=lambda x: x[1], reverse=True)]
874
+
875
+ return sorted_words[:top_n]
876
+
877
+ def _split_text_into_chunks(self, text: str, max_length: int = 4000) -> List[str]:
878
+ """
879
+ تقسيم النص إلى أجزاء أصغر لمعالجتها بشكل منفصل
880
+
881
+ المعاملات:
882
+ ----------
883
+ text : str
884
+ النص المراد تقسيمه
885
+ max_length : int, optional
886
+ الحد الأقصى لطول كل جزء
887
+
888
+ المخرجات:
889
+ --------
890
+ List[str]
891
+ قائمة بأجزاء النص
892
+ """
893
+ # إذا كان النص قصيراً، إرجاعه كما هو
894
+ if len(text) <= max_length:
895
+ return [text]
896
+
897
+ # تقسيم النص إلى فقرات
898
+ paragraphs = text.split('\n')
899
+
900
+ chunks = []
901
+ current_chunk = ""
902
+
903
+ for paragraph in paragraphs:
904
+ # إذا كانت إضافة الفقرة ستجعل الجزء الحالي أطول من الحد الأقصى
905
+ if len(current_chunk) + len(paragraph) > max_length:
906
+ # إذا كان الجزء الحالي غير فارغ، إضافته إلى القائمة
907
+ if current_chunk:
908
+ chunks.append(current_chunk)
909
+
910
+ # إذا كانت الفقرة نفسها أطول من الحد الأقصى، تقسيمها
911
+ if len(paragraph) > max_length:
912
+ # تقسيم الفقرة إلى جمل
913
+ sentences = re.split(r'(?<=[.!?])\s+', paragraph)
914
+
915
+ current_chunk = ""
916
+ for sentence in sentences:
917
+ if len(current_chunk) + len(sentence) > max_length:
918
+ if current_chunk:
919
+ chunks.append(current_chunk)
920
+
921
+ # إذا كانت الجملة نفسها أطول من الحد الأقصى، تقسيمها
922
+ if len(sentence) > max_length:
923
+ # تقسيم الجملة إلى أجزاء بطول الحد الأقصى
924
+ for i in range(0, len(sentence), max_length):
925
+ chunks.append(sentence[i:i+max_length])
926
+ else:
927
+ current_chunk = sentence
928
+ else:
929
+ current_chunk += " " + sentence if current_chunk else sentence
930
+ else:
931
+ current_chunk = paragraph
932
+ else:
933
+ current_chunk += "\n" + paragraph if current_chunk else paragraph
934
+
935
+ # إضافة الجزء الأخير إذا لم يكن فارغاً
936
+ if current_chunk:
937
+ chunks.append(current_chunk)
938
+
939
+ return chunks