|
|
|
""" |
|
وحدة المساعد الذكي |
|
|
|
هذا الملف يحتوي على الفئة الرئيسية لتطبيق المساعد الذكي مع دعم نموذج Claude AI. |
|
""" |
|
|
|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
import plotly.express as px |
|
import requests |
|
import json |
|
import time |
|
import base64 |
|
import logging |
|
import os |
|
from datetime import datetime, timedelta |
|
import io |
|
import tempfile |
|
import random |
|
from io import BytesIO |
|
from tempfile import NamedTemporaryFile |
|
from PIL import Image |
|
|
|
class ClaudeAIService: |
|
""" |
|
فئة خدمة Claude AI للتحليل الذكي |
|
""" |
|
def __init__(self): |
|
"""تهيئة خدمة Claude AI""" |
|
self.api_url = "https://api.anthropic.com/v1/messages" |
|
|
|
def get_api_key(self): |
|
"""الحصول على مفتاح API من متغيرات البيئة""" |
|
api_key = os.environ.get("ANTHROPIC_API_KEY") |
|
if not api_key: |
|
raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة") |
|
return api_key |
|
|
|
def get_available_models(self): |
|
""" |
|
الحصول على قائمة بالنماذج المتاحة |
|
|
|
العوائد: |
|
dict: قائمة بالنماذج مع وصفها |
|
""" |
|
return { |
|
"claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة", |
|
"claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية" |
|
} |
|
|
|
def get_model_full_name(self, short_name): |
|
""" |
|
تحويل الاسم المختصر للنموذج إلى الاسم الكامل |
|
|
|
المعلمات: |
|
short_name: الاسم المختصر للنموذج |
|
|
|
العوائد: |
|
str: الاسم الكامل للنموذج |
|
""" |
|
valid_models = { |
|
"claude-3-7-sonnet": "claude-3-7-sonnet-20250219", |
|
"claude-3-5-haiku": "claude-3-5-haiku-20240307" |
|
} |
|
|
|
return valid_models.get(short_name, short_name) |
|
|
|
def analyze_image(self, image_path, prompt, model_name="claude-3-7-sonnet"): |
|
""" |
|
تحليل صورة باستخدام نموذج Claude AI |
|
|
|
المعلمات: |
|
image_path: مسار الصورة المراد تحليلها |
|
prompt: التوجيه للنموذج |
|
model_name: اسم نموذج Claude المراد استخدامه |
|
|
|
العوائد: |
|
dict: نتائج التحليل |
|
""" |
|
try: |
|
|
|
api_key = self.get_api_key() |
|
|
|
|
|
file_size = os.path.getsize(image_path) |
|
_, ext = os.path.splitext(image_path) |
|
ext = ext.lower() |
|
|
|
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp'] and file_size > 5 * 1024 * 1024: |
|
|
|
try: |
|
with Image.open(image_path) as img: |
|
|
|
compressed_img_buffer = io.BytesIO() |
|
|
|
|
|
quality = min(95, int((5 * 1024 * 1024 / file_size) * 100)) |
|
|
|
|
|
img.save(compressed_img_buffer, format='JPEG', quality=quality) |
|
compressed_img_buffer.seek(0) |
|
|
|
|
|
file_content = compressed_img_buffer.read() |
|
logging.info(f"تم ضغط الصورة من {file_size/1024/1024:.2f} ميجابايت إلى {len(file_content)/1024/1024:.2f} ميجابايت") |
|
except Exception as e: |
|
logging.error(f"خطأ أثناء ضغط الصورة: {str(e)}") |
|
with open(image_path, 'rb') as f: |
|
file_content = f.read() |
|
else: |
|
|
|
with open(image_path, 'rb') as f: |
|
file_content = f.read() |
|
|
|
|
|
if len(file_content) > 5 * 1024 * 1024: |
|
raise ValueError(f"حجم الملف ({len(file_content)/1024/1024:.2f} ميجابايت) يتجاوز الحد الأقصى (5 ميجابايت). يرجى تقليل حجم الملف قبل الرفع.") |
|
|
|
|
|
file_base64 = base64.b64encode(file_content).decode('utf-8') |
|
|
|
|
|
_, ext = os.path.splitext(image_path) |
|
ext = ext.lower() |
|
|
|
if ext in ('.jpg', '.jpeg'): |
|
file_type = "image/jpeg" |
|
elif ext == '.png': |
|
file_type = "image/png" |
|
elif ext == '.gif': |
|
file_type = "image/gif" |
|
elif ext == '.webp': |
|
file_type = "image/webp" |
|
else: |
|
file_type = "image/jpeg" |
|
|
|
|
|
model_name = self.get_model_full_name(model_name) |
|
|
|
|
|
headers = { |
|
"Content-Type": "application/json", |
|
"x-api-key": api_key, |
|
"anthropic-version": "2023-06-01" |
|
} |
|
|
|
payload = { |
|
"model": model_name, |
|
"max_tokens": 4096, |
|
"messages": [ |
|
{ |
|
"role": "user", |
|
"content": [ |
|
{"type": "text", "text": prompt}, |
|
{ |
|
"type": "image", |
|
"source": { |
|
"type": "base64", |
|
"media_type": file_type, |
|
"data": file_base64 |
|
} |
|
|
|
|
|
quantities_df = pd.DataFrame(quantities_data) |
|
quantities_df['النسبة المئوية'] = quantities_df['السعر التقديري'] / quantities_df['السعر التقديري'].sum() * 100 |
|
quantities_df['النسبة المئوية'] = quantities_df['النسبة المئوية'].round(1) |
|
|
|
|
|
st.dataframe(quantities_df, use_container_width=True) |
|
|
|
|
|
fig = px.bar( |
|
quantities_df, |
|
x='البند', |
|
y='السعر التقديري', |
|
title='التكاليف التقديرية حسب البند', |
|
labels={'السعر التقديري': 'التكلفة التقديرية (ريال)', 'البند': 'بنود الأعمال'} |
|
) |
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("### الكلمات المفتاحية المستخرجة") |
|
|
|
keywords = [ |
|
{"word": "غرامة التأخير", "count": 3, "relevance": 0.85}, |
|
{"word": "المحتوى المحلي", "count": 5, "relevance": 0.92}, |
|
{"word": "الدفعة المقدمة", "count": 2, "relevance": 0.78}, |
|
{"word": "مدة التنفيذ", "count": 4, "relevance": 0.88}, |
|
{"word": "الضمان", "count": 3, "relevance": 0.75}, |
|
{"word": "مواقف سيارات", "count": 2, "relevance": 0.70}, |
|
{"word": "تكييف", "count": 3, "relevance": 0.65}, |
|
{"word": "سلامة", "count": 2, "relevance": 0.60} |
|
] |
|
|
|
keywords_df = pd.DataFrame(keywords) |
|
|
|
|
|
from matplotlib.pyplot import figure |
|
|
|
fig, ax = plt.subplots(figsize=(10, 5)) |
|
|
|
|
|
for i, kw in enumerate(keywords): |
|
size = 10 + (kw["relevance"] * 30) |
|
color = plt.cm.viridis(kw["relevance"]) |
|
x = random.uniform(0.1, 0.9) |
|
y = random.uniform(0.1, 0.9) |
|
ax.text(x, y, kw["word"], fontsize=size, color=color, |
|
ha='center', va='center', rotation=random.randint(-30, 30)) |
|
|
|
ax.set_xlim(0, 1) |
|
ax.set_ylim(0, 1) |
|
ax.axis('off') |
|
|
|
st.pyplot(fig) |
|
|
|
|
|
st.markdown("### التوصيات") |
|
|
|
st.info(""" |
|
بناءً على تحليل مستند المناقصة، نوصي بالآتي: |
|
|
|
1. **التركيز على المحتوى المحلي**: تقديم خطة تفصيلية لتحقيق نسبة 60% من المحتوى المحلي. |
|
|
|
2. **مراجعة شروط الدفعات**: التأكد من توفير السيولة المالية اللازمة نظرًا لنظام الدفعات الشهرية. |
|
|
|
3. **الانتباه لغرامات التأخير**: وضع جدول زمني واقعي مع هوامش كافية لتجنب غرامات التأخير. |
|
|
|
4. **الاستعانة بالمتخصصين**: تكوين فريق فني متكامل يلبي متطلبات الخبرة المطلوبة. |
|
|
|
5. **مراجعة تفصيلية لجدول الكميات**: التأكد من شمولية الأسعار لجميع متطلبات المشروع. |
|
""") |
|
|
|
def _render_local_content_tab(self): |
|
"""عرض تبويب المحتوى المحلي""" |
|
st.markdown("### حاسبة المحتوى المحلي") |
|
|
|
st.markdown(""" |
|
استخدم هذه الأداة لحساب نسبة المحتوى المحلي للمشروع حسب متطلبات هيئة المحتوى المحلي. |
|
""") |
|
|
|
|
|
st.markdown("#### القسم الأول: العمالة") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
local_labor_cost = st.number_input( |
|
"تكلفة العمالة المحلية", |
|
min_value=0, |
|
value=500000, |
|
step=10000, |
|
format="%d", |
|
key="local_labor_cost" |
|
) |
|
|
|
foreign_labor_cost = st.number_input( |
|
"تكلفة العمالة الأجنبية", |
|
min_value=0, |
|
value=300000, |
|
step=10000, |
|
format="%d", |
|
key="foreign_labor_cost" |
|
) |
|
|
|
with col2: |
|
local_labor_weight = st.slider( |
|
"وزن العمالة المحلية", |
|
min_value=0.0, |
|
max_value=1.0, |
|
value=0.4, |
|
step=0.1, |
|
format="%.1f", |
|
key="local_labor_weight" |
|
) |
|
|
|
|
|
st.markdown("#### القسم الثاني: المواد") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
local_materials_cost = st.number_input( |
|
"تكلفة المواد المحلية", |
|
min_value=0, |
|
value=800000, |
|
step=10000, |
|
format="%d", |
|
key="local_materials_cost" |
|
) |
|
|
|
imported_materials_cost = st.number_input( |
|
"تكلفة المواد المستوردة", |
|
min_value=0, |
|
value=1200000, |
|
step=10000, |
|
format="%d", |
|
key="imported_materials_cost" |
|
) |
|
|
|
with col2: |
|
materials_weight = st.slider( |
|
"وزن المواد", |
|
min_value=0.0, |
|
max_value=1.0, |
|
value=0.3, |
|
step=0.1, |
|
format="%.1f", |
|
key="materials_weight" |
|
) |
|
|
|
|
|
st.markdown("#### القسم الثالث: المعدات") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
local_equipment_cost = st.number_input( |
|
"تكلفة المعدات المحلية", |
|
min_value=0, |
|
value=300000, |
|
step=10000, |
|
format="%d", |
|
key="local_equipment_cost" |
|
) |
|
|
|
imported_equipment_cost = st.number_input( |
|
"تكلفة المعدات المستوردة", |
|
min_value=0, |
|
value=700000, |
|
step=10000, |
|
format="%d", |
|
key="imported_equipment_cost" |
|
) |
|
|
|
with col2: |
|
equipment_weight = st.slider( |
|
"وزن المعدات", |
|
min_value=0.0, |
|
max_value=1.0, |
|
value=0.2, |
|
step=0.1, |
|
format="%.1f", |
|
key="equipment_weight" |
|
) |
|
|
|
|
|
st.markdown("#### القسم الرابع: الخدمات") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
local_services_cost = st.number_input( |
|
"تكلفة الخدمات المحلية", |
|
min_value=0, |
|
value=400000, |
|
step=10000, |
|
format="%d", |
|
key="local_services_cost" |
|
) |
|
|
|
foreign_services_cost = st.number_input( |
|
"تكلفة الخدمات الأجنبية", |
|
min_value=0, |
|
value=200000, |
|
step=10000, |
|
format="%d", |
|
key="foreign_services_cost" |
|
) |
|
|
|
with col2: |
|
services_weight = st.slider( |
|
"وزن الخدمات", |
|
min_value=0.0, |
|
max_value=1.0, |
|
value=0.1, |
|
step=0.1, |
|
format="%.1f", |
|
key="services_weight" |
|
) |
|
|
|
|
|
total_weight = local_labor_weight + materials_weight + equipment_weight + services_weight |
|
|
|
if total_weight != 1.0: |
|
st.warning(f"إجمالي الأوزان يجب أن يساوي 1.0. الإجمالي الحالي هو {total_weight}.") |
|
|
|
|
|
if st.button("حساب المحتوى المحلي", key="calculate_local_content_button"): |
|
|
|
with st.spinner("جاري حساب نسبة المحتوى المحلي..."): |
|
|
|
time.sleep(1) |
|
|
|
|
|
labor_total = local_labor_cost + foreign_labor_cost |
|
labor_local_percentage = (local_labor_cost / labor_total) * 100 if labor_total > 0 else 0 |
|
|
|
materials_total = local_materials_cost + imported_materials_cost |
|
materials_local_percentage = (local_materials_cost / materials_total) * 100 if materials_total > 0 else 0 |
|
|
|
equipment_total = local_equipment_cost + imported_equipment_cost |
|
equipment_local_percentage = (local_equipment_cost / equipment_total) * 100 if equipment_total > 0 else 0 |
|
|
|
services_total = local_services_cost + foreign_services_cost |
|
services_local_percentage = (local_services_cost / services_total) * 100 if services_total > 0 else 0 |
|
|
|
|
|
weighted_local_content = ( |
|
(labor_local_percentage * local_labor_weight) + |
|
(materials_local_percentage * materials_weight) + |
|
(equipment_local_percentage * equipment_weight) + |
|
(services_local_percentage * services_weight) |
|
) |
|
|
|
|
|
target_percentage = 60 |
|
|
|
if weighted_local_content >= target_percentage: |
|
status = "مطابق" |
|
status_color = "green" |
|
elif weighted_local_content >= target_percentage * 0.9: |
|
status = "قريب" |
|
status_color = "orange" |
|
else: |
|
status = "غير مطابق" |
|
status_color = "red" |
|
|
|
|
|
st.success("تم حساب نسبة المحتوى المحلي بنجاح!") |
|
|
|
|
|
st.markdown("#### نتائج المحتوى المحلي") |
|
|
|
col1, col2, col3 = st.columns([2, 1, 1]) |
|
|
|
with col1: |
|
st.metric( |
|
"نسبة المحتوى المحلي الإجمالية", |
|
f"{weighted_local_content:.1f}%", |
|
f"{weighted_local_content - target_percentage:.1f}%" if weighted_local_content != target_percentage else "0%" |
|
) |
|
|
|
with col2: |
|
st.metric("النسبة المستهدفة", f"{target_percentage}%") |
|
|
|
with col3: |
|
st.markdown(f""" |
|
<div style=" |
|
padding: 10px; |
|
border-radius: 5px; |
|
text-align: center; |
|
background-color: {'#8BC34A' if status == 'مطابق' else '#FF9800' if status == 'قريب' else '#F44336'}; |
|
color: white; |
|
font-weight: bold; |
|
margin-top: 21px; |
|
"> |
|
{status} |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("#### تفاصيل حساب المحتوى المحلي") |
|
|
|
|
|
details_data = { |
|
'القسم': ['العمالة', 'المواد', 'المعدات', 'الخدمات', 'الإجمالي'], |
|
'التكلفة المحلية': [ |
|
local_labor_cost, |
|
local_materials_cost, |
|
local_equipment_cost, |
|
local_services_cost, |
|
local_labor_cost + local_materials_cost + local_equipment_cost + local_services_cost |
|
], |
|
'التكلفة الإجمالية': [ |
|
labor_total, |
|
materials_total, |
|
equipment_total, |
|
services_total, |
|
labor_total + materials_total + equipment_total + services_total |
|
], |
|
'النسبة المحلية': [ |
|
f"{labor_local_percentage:.1f}%", |
|
f"{materials_local_percentage:.1f}%", |
|
f"{equipment_local_percentage:.1f}%", |
|
f"{services_local_percentage:.1f}%", |
|
f"{weighted_local_content:.1f}%" |
|
], |
|
'الوزن': [ |
|
f"{local_labor_weight:.1f}", |
|
f"{materials_weight:.1f}", |
|
f"{equipment_weight:.1f}", |
|
f"{services_weight:.1f}", |
|
"1.0" |
|
], |
|
'المساهمة المرجحة': [ |
|
f"{labor_local_percentage * local_labor_weight:.1f}%", |
|
f"{materials_local_percentage * materials_weight:.1f}%", |
|
f"{equipment_local_percentage * equipment_weight:.1f}%", |
|
f"{services_local_percentage * services_weight:.1f}%", |
|
f"{weighted_local_content:.1f}%" |
|
] |
|
} |
|
|
|
details_df = pd.DataFrame(details_data) |
|
|
|
|
|
st.dataframe(details_df, use_container_width=True) |
|
|
|
|
|
st.markdown("#### مساهمة كل قسم في المحتوى المحلي") |
|
|
|
chart_data = details_df.iloc[:-1].copy() |
|
|
|
fig = px.bar( |
|
chart_data, |
|
x='القسم', |
|
y=[local_labor_weight * labor_local_percentage, |
|
materials_weight * materials_local_percentage, |
|
equipment_weight * equipment_local_percentage, |
|
services_weight * services_local_percentage], |
|
labels={'value': 'المساهمة المرجحة (%)', 'variable': 'القسم'}, |
|
title='مساهمة كل قسم في إجمالي المحتوى المحلي', |
|
color_discrete_sequence=px.colors.qualitative.Set3 |
|
) |
|
|
|
|
|
fig.add_hline(y=target_percentage, line_dash="dash", line_color="red", |
|
annotation_text=f"النسبة المستهدفة ({target_percentage}%)", |
|
annotation_position="top right") |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### توصيات لتحسين المحتوى المحلي") |
|
|
|
if weighted_local_content < target_percentage: |
|
st.warning(""" |
|
للوصول إلى النسبة المستهدفة للمحتوى المحلي (60%)، نوصي بما يلي: |
|
""") |
|
|
|
recommendations = [] |
|
|
|
if labor_local_percentage < 70: |
|
recommendations.append("زيادة نسبة توظيف العمالة المحلية والاستفادة من برامج دعم التوطين.") |
|
|
|
if materials_local_percentage < 50: |
|
recommendations.append("البحث عن موردين محليين للمواد وإعطاء الأولوية للمنتجات المحلية.") |
|
|
|
if equipment_local_percentage < 40: |
|
recommendations.append("استئجار المعدات من شركات محلية بدلاً من الاستيراد المباشر.") |
|
|
|
if services_local_percentage < 60: |
|
recommendations.append("التعاقد مع مقدمي خدمات محليين والاستعانة بالشركات المحلية للخدمات المساندة.") |
|
|
|
for i, rec in enumerate(recommendations): |
|
st.markdown(f"{i+1}. {rec}") |
|
|
|
|
|
st.markdown("#### محاكاة تحسين المحتوى المحلي") |
|
|
|
st.info(""" |
|
استخدم المحاكاة أدناه لتجربة تعديل النسب والوصول إلى المحتوى المحلي المستهدف. |
|
""") |
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
with col1: |
|
labor_improvement = st.slider( |
|
"تحسين العمالة المحلية", |
|
min_value=0, |
|
max_value=100, |
|
value=10, |
|
step=5, |
|
format="%d%%", |
|
key="labor_improvement" |
|
) / 100 |
|
|
|
with col2: |
|
materials_improvement = st.slider( |
|
"تحسين المواد المحلية", |
|
min_value=0, |
|
max_value=100, |
|
value=15, |
|
step=5, |
|
format="%d%%", |
|
key="materials_improvement" |
|
) / 100 |
|
|
|
with col3: |
|
equipment_improvement = st.slider( |
|
"تحسين المعدات المحلية", |
|
min_value=0, |
|
max_value=100, |
|
value=10, |
|
step=5, |
|
format="%d%%", |
|
key="equipment_improvement" |
|
) / 100 |
|
|
|
with col4: |
|
services_improvement = st.slider( |
|
"تحسين الخدمات المحلية", |
|
min_value=0, |
|
max_value=100, |
|
value=5, |
|
step=5, |
|
format="%d%%", |
|
key="services_improvement" |
|
) / 100 |
|
|
|
|
|
improved_labor_percentage = min(100, labor_local_percentage + (labor_improvement * 100)) |
|
improved_materials_percentage = min(100, materials_local_percentage + (materials_improvement * 100)) |
|
improved_equipment_percentage = min(100, equipment_local_percentage + (equipment_improvement * 100)) |
|
improved_services_percentage = min(100, services_local_percentage + (services_improvement * 100)) |
|
|
|
improved_local_content = ( |
|
(improved_labor_percentage * local_labor_weight) + |
|
(improved_materials_percentage * materials_weight) + |
|
(improved_equipment_percentage * equipment_weight) + |
|
(improved_services_percentage * services_weight) |
|
) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
st.metric( |
|
"المحتوى المحلي الحالي", |
|
f"{weighted_local_content:.1f}%" |
|
) |
|
|
|
with col2: |
|
st.metric( |
|
"المحتوى المحلي بعد التحسين", |
|
f"{improved_local_content:.1f}%", |
|
f"{improved_local_content - weighted_local_content:.1f}%" |
|
) |
|
|
|
|
|
additional_cost = ( |
|
(labor_improvement * foreign_labor_cost) + |
|
(materials_improvement * imported_materials_cost) + |
|
(equipment_improvement * imported_equipment_cost) + |
|
(services_improvement * foreign_services_cost) |
|
) * 0.15 |
|
|
|
st.metric( |
|
"التكلفة الإضافية المقدرة للتحسين", |
|
f"{additional_cost:,.2f} ريال" |
|
) |
|
|
|
|
|
if improved_local_content >= target_percentage: |
|
st.success(f"بعد التحسين، سيصبح المحتوى المحلي {improved_local_content:.1f}% وهو أعلى من النسبة المستهدفة ({target_percentage}%).") |
|
else: |
|
st.warning(f"التحسين المقترح غير كافٍ. المحتوى المحلي سيصبح {improved_local_content:.1f}% وهو أقل من النسبة المستهدفة ({target_percentage}%).") |
|
else: |
|
st.success(f""" |
|
تهانينا! المشروع يحقق نسبة محتوى محلي {weighted_local_content:.1f}% وهي أعلى من النسبة المستهدفة ({target_percentage}%). |
|
|
|
للحفاظ على هذا المستوى أو تحسينه، نوصي بما يلي: |
|
|
|
1. توثيق جميع المشتريات المحلية والتأكد من حصولها على شهادات المحتوى المحلي. |
|
2. متابعة تحديثات هيئة المحتوى المحلي للاستفادة من الحوافز المتاحة. |
|
3. بناء علاقات استراتيجية مع الموردين المحليين لضمان استدامة سلسلة التوريد. |
|
4. المشاركة في برامج تطوير الموردين المحليين لتعزيز القدرات المحلية. |
|
""") |
|
|
|
def _render_faq_tab(self): |
|
"""عرض تبويب الأسئلة الشائعة""" |
|
st.markdown("### الأسئلة الشائعة") |
|
|
|
st.markdown(""" |
|
فيما يلي قائمة بالأسئلة الشائعة حول نظام تسعير المناقصات والإجابات عليها. |
|
""") |
|
|
|
|
|
search_query = st.text_input("ابحث في الأسئلة الشائعة", key="faq_search") |
|
|
|
filtered_faqs = self.faqs |
|
if search_query: |
|
filtered_faqs = [faq for faq in self.faqs if search_query.lower() in faq["question"].lower()] |
|
|
|
|
|
if not filtered_faqs: |
|
st.info("لا توجد نتائج مطابقة لبحثك. يرجى تعديل كلمات البحث.") |
|
else: |
|
for i, faq in enumerate(filtered_faqs): |
|
with st.expander(faq["question"]): |
|
st.markdown(faq["answer"]) |
|
|
|
|
|
st.markdown("### لم تجد إجابة لسؤالك؟") |
|
|
|
new_question = st.text_area("اكتب سؤالك هنا", key="new_question") |
|
|
|
if st.button("إرسال السؤال", key="submit_question_button"): |
|
if new_question: |
|
st.success("تم استلام سؤالك بنجاح! سيتم الرد عليه في أقرب وقت ممكن.") |
|
|
|
|
|
|
|
else: |
|
st.warning("يرجى كتابة سؤالك قبل الإرسال.") |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
app = AIAssistantApp() |
|
app.render() |
|
} |
|
] |
|
} |
|
] |
|
} |
|
|
|
|
|
response = requests.post( |
|
self.api_url, |
|
headers=headers, |
|
json=payload, |
|
timeout=60 |
|
) |
|
|
|
|
|
if response.status_code != 200: |
|
error_message = f"فشل طلب API: {response.status_code}" |
|
try: |
|
error_details = response.json() |
|
error_message += f"\nتفاصيل: {error_details}" |
|
except: |
|
error_message += f"\nتفاصيل: {response.text}" |
|
|
|
return {"error": error_message} |
|
|
|
|
|
result = response.json() |
|
|
|
return { |
|
"success": True, |
|
"content": result["content"][0]["text"], |
|
"model": result["model"], |
|
"usage": result.get("usage", {}) |
|
} |
|
|
|
except Exception as e: |
|
logging.error(f"خطأ أثناء تحليل الصورة: {str(e)}") |
|
import traceback |
|
stack_trace = traceback.format_exc() |
|
return {"error": f"فشل في تحليل الصورة: {str(e)}\n{stack_trace}"} |
|
|
|
def chat_completion(self, messages, model_name="claude-3-7-sonnet"): |
|
""" |
|
إكمال محادثة باستخدام نموذج Claude AI |
|
|
|
المعلمات: |
|
messages: سجل المحادثة |
|
model_name: اسم نموذج Claude المراد استخدامه |
|
|
|
العوائد: |
|
dict: نتائج الإكمال |
|
""" |
|
try: |
|
|
|
api_key = self.get_api_key() |
|
|
|
|
|
claude_messages = [] |
|
for msg in messages: |
|
claude_messages.append({ |
|
"role": msg["role"], |
|
"content": msg["content"] |
|
}) |
|
|
|
|
|
model_name = self.get_model_full_name(model_name) |
|
|
|
|
|
headers = { |
|
"Content-Type": "application/json", |
|
"x-api-key": api_key, |
|
"anthropic-version": "2023-06-01" |
|
} |
|
|
|
payload = { |
|
"model": model_name, |
|
"max_tokens": 4096, |
|
"messages": claude_messages, |
|
"temperature": 0.7 |
|
} |
|
|
|
|
|
response = requests.post( |
|
self.api_url, |
|
headers=headers, |
|
json=payload, |
|
timeout=60 |
|
) |
|
|
|
|
|
if response.status_code != 200: |
|
error_message = f"فشل طلب API: {response.status_code}" |
|
try: |
|
error_details = response.json() |
|
error_message += f"\nتفاصيل: {error_details}" |
|
except: |
|
error_message += f"\nتفاصيل: {response.text}" |
|
|
|
return {"error": error_message} |
|
|
|
|
|
result = response.json() |
|
|
|
return { |
|
"success": True, |
|
"content": result["content"][0]["text"], |
|
"model": result["model"], |
|
"usage": result.get("usage", {}) |
|
} |
|
|
|
except Exception as e: |
|
logging.error(f"خطأ أثناء إكمال المحادثة: {str(e)}") |
|
import traceback |
|
stack_trace = traceback.format_exc() |
|
return {"error": f"فشل في إكمال المحادثة: {str(e)}\n{stack_trace}"} |
|
|
|
|
|
class AIAssistantApp: |
|
"""وحدة المساعد الذكي""" |
|
|
|
def __init__(self): |
|
"""تهيئة وحدة المساعد الذكي""" |
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
|
|
|
try: |
|
from pdf2image import convert_from_path |
|
self.pdf_conversion_available = True |
|
self.convert_from_path = convert_from_path |
|
except ImportError: |
|
self.pdf_conversion_available = False |
|
logging.warning("مكتبة pdf2image غير متوفرة. لن يتم دعم تحويل ملفات PDF.") |
|
|
|
|
|
self.cost_model = self._load_cost_prediction_model() |
|
self.document_model = self._load_document_classifier_model() |
|
self.risk_model = self._load_risk_assessment_model() |
|
self.local_content_model = self._load_local_content_model() |
|
self.entity_model = self._load_entity_recognition_model() |
|
|
|
|
|
self.claude_service = ClaudeAIService() |
|
|
|
|
|
self.faqs = [ |
|
{ |
|
"question": "كيف يمكنني إضافة مشروع جديد؟", |
|
"answer": "يمكنك إضافة مشروع جديد من خلال الانتقال إلى وحدة إدارة المشاريع، ثم النقر على زر 'إضافة مشروع جديد'، وملء النموذج بالبيانات المطلوبة." |
|
}, |
|
{ |
|
"question": "ما هي خطوات تسعير المناقصة؟", |
|
"answer": "تتضمن خطوات تسعير المناقصة: 1) تحليل مستندات المناقصة، 2) تحديد بنود العمل، 3) تقدير التكاليف المباشرة، 4) إضافة المصاريف العامة والأرباح، 5) احتساب المحتوى المحلي، 6) مراجعة النتائج النهائية." |
|
}, |
|
{ |
|
"question": "كيف يتم حساب المحتوى المحلي؟", |
|
"answer": "يتم حساب المحتوى المحلي بتحديد نسبة المنتجات والخدمات والقوى العاملة المحلية من إجمالي التكاليف. يتم استخدام قاعدة بيانات الموردين المعتمدين وتطبيق معادلات خاصة حسب متطلبات هيئة المحتوى المحلي." |
|
}, |
|
{ |
|
"question": "كيف يمكنني تصدير التقارير؟", |
|
"answer": "يمكنك تصدير التقارير من وحدة التقارير والتحليلات، حيث يوجد زر 'تصدير' في كل تقرير. يمكن تصدير التقارير بتنسيقات مختلفة مثل Excel و PDF و CSV." |
|
}, |
|
{ |
|
"question": "كيف يمكنني تقييم المخاطر للمشروع؟", |
|
"answer": "يمكنك تقييم المخاطر للمشروع من خلال وحدة المخاطر، حيث يمكنك إضافة المخاطر المحتملة وتقييم تأثيرها واحتماليتها، ثم وضع خطة الاستجابة المناسبة." |
|
}, |
|
{ |
|
"question": "ما هي طرق التسعير المتاحة في النظام؟", |
|
"answer": "يوفر النظام أربع طرق للتسعير: 1) التسعير القياسي، 2) التسعير غير المتزن، 3) التسعير التنافسي، 4) التسعير الموجه بالربحية. يمكنك اختيار الطريقة المناسبة حسب طبيعة المشروع واستراتيجية الشركة." |
|
}, |
|
{ |
|
"question": "كيف يمكنني معالجة مستندات المناقصة ضخمة الحجم؟", |
|
"answer": "يمكنك استخدام وحدة تحليل المستندات لمعالجة مستندات المناقصة ضخمة الحجم، حيث تقوم الوحدة بتحليل المستندات واستخراج المعلومات المهمة مثل مواصفات المشروع ومتطلباته وشروطه تلقائياً." |
|
} |
|
] |
|
|
|
def _load_cost_prediction_model(self): |
|
"""تحميل نموذج التنبؤ بالتكاليف""" |
|
|
|
|
|
logging.info("جاري تحميل نموذج التنبؤ بالتكاليف...") |
|
return {"name": "cost_prediction_model", "version": "1.0.0", "status": "loaded"} |
|
|
|
def _load_document_classifier_model(self): |
|
"""تحميل نموذج تصنيف المستندات""" |
|
logging.info("جاري تحميل نموذج تصنيف المستندات...") |
|
return {"name": "document_classifier_model", "version": "1.0.0", "status": "loaded"} |
|
|
|
def _load_risk_assessment_model(self): |
|
"""تحميل نموذج تقييم المخاطر""" |
|
logging.info("جاري تحميل نموذج تقييم المخاطر...") |
|
return {"name": "risk_assessment_model", "version": "1.0.0", "status": "loaded"} |
|
|
|
def _load_local_content_model(self): |
|
"""تحميل نموذج تحليل المحتوى المحلي""" |
|
logging.info("جاري تحميل نموذج تحليل المحتوى المحلي...") |
|
return {"name": "local_content_model", "version": "1.0.0", "status": "loaded"} |
|
|
|
def _load_entity_recognition_model(self): |
|
"""تحميل نموذج التعرف على الكيانات""" |
|
logging.info("جاري تحميل نموذج التعرف على الكيانات...") |
|
return {"name": "entity_recognition_model", "version": "1.0.0", "status": "loaded"} |
|
|
|
def render(self): |
|
"""عرض واجهة وحدة المساعد الذكي""" |
|
|
|
st.markdown("<h1 class='module-title'>وحدة المساعد الذكي</h1>", unsafe_allow_html=True) |
|
|
|
tabs = st.tabs([ |
|
"المساعد الذكي", |
|
"التنبؤ بالتكاليف", |
|
"تحليل المخاطر", |
|
"تحليل المستندات", |
|
"المحتوى المحلي", |
|
"الأسئلة الشائعة" |
|
]) |
|
|
|
with tabs[0]: |
|
self._render_ai_assistant_tab() |
|
|
|
with tabs[1]: |
|
self._render_cost_prediction_tab() |
|
|
|
with tabs[2]: |
|
self._render_risk_analysis_tab() |
|
|
|
with tabs[3]: |
|
self._render_document_analysis_tab() |
|
|
|
with tabs[4]: |
|
self._render_local_content_tab() |
|
|
|
with tabs[5]: |
|
self._render_faq_tab() |
|
|
|
def _render_ai_assistant_tab(self): |
|
"""عرض تبويب المساعد الذكي مع دعم Claude AI""" |
|
|
|
st.markdown("### المساعد الذكي لتسعير المناقصات") |
|
|
|
|
|
claude_models = self.claude_service.get_available_models() |
|
|
|
selected_model = st.radio( |
|
"اختر نموذج الذكاء الاصطناعي", |
|
options=list(claude_models.keys()), |
|
format_func=lambda x: claude_models[x], |
|
horizontal=True, |
|
key="assistant_ai_model" |
|
) |
|
|
|
|
|
st.markdown(""" |
|
<div class="chat-container"> |
|
<div class="chat-header"> |
|
<h4>المساعد الذكي</h4> |
|
<p>تحدث مع المساعد الذكي للحصول على المساعدة في تسعير المناقصات وتحليل البيانات</p> |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if 'ai_assistant_messages' not in st.session_state: |
|
st.session_state.ai_assistant_messages = [ |
|
{"role": "assistant", "content": "مرحباً! أنا المساعد الذكي لنظام تسعير المناقصات. كيف يمكنني مساعدتك اليوم؟"} |
|
] |
|
|
|
|
|
chat_container = st.container() |
|
with chat_container: |
|
st.markdown(""" |
|
<style> |
|
.chat-container { |
|
max-width: 800px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
} |
|
.message { |
|
display: flex; |
|
margin-bottom: 20px; |
|
align-items: flex-start; |
|
} |
|
.user-message { |
|
justify-content: flex-end; |
|
} |
|
.assistant-message { |
|
justify-content: flex-start; |
|
} |
|
.avatar { |
|
width: 40px; |
|
height: 40px; |
|
border-radius: 50%; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-weight: bold; |
|
color: white; |
|
margin: 0 10px; |
|
} |
|
.user-avatar { |
|
background-color: #2196F3; |
|
} |
|
.assistant-avatar { |
|
background-color: #4CAF50; |
|
} |
|
.message-content { |
|
padding: 15px; |
|
border-radius: 15px; |
|
max-width: 70%; |
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
} |
|
.user-content { |
|
background-color: #E3F2FD; |
|
border-top-right-radius: 5px; |
|
} |
|
.assistant-content { |
|
background-color: #F5F5F5; |
|
border-top-left-radius: 5px; |
|
} |
|
.message-time { |
|
font-size: 0.8em; |
|
color: #757575; |
|
margin-top: 5px; |
|
text-align: right; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
for message in st.session_state.ai_assistant_messages: |
|
if message["role"] == "user": |
|
st.markdown(f""" |
|
<div class="message user-message"> |
|
<div class="message-content user-content"> |
|
{message["content"]} |
|
<div class="message-time">أنت • الآن</div> |
|
</div> |
|
<div class="avatar user-avatar">أ</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
else: |
|
st.markdown(f""" |
|
<div class="message assistant-message"> |
|
<div class="avatar assistant-avatar">م</div> |
|
<div class="message-content assistant-content"> |
|
{message["content"]} |
|
<div class="message-time">المساعد • الآن</div> |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
uploaded_file = st.file_uploader( |
|
"اختياري: ارفع ملفًا للمساعدة (صورة، PDF)", |
|
type=["jpg", "jpeg", "png", "pdf"], |
|
key="assistant_file_upload" |
|
) |
|
|
|
|
|
user_input = st.text_input("اكتب رسالتك هنا", key="ai_assistant_input") |
|
|
|
|
|
if st.button("إرسال", key="send_message_button"): |
|
|
|
api_available = True |
|
try: |
|
self.claude_service.get_api_key() |
|
except ValueError: |
|
api_available = False |
|
st.warning("مفتاح API لـ Claude غير متوفر. يرجى إضافته في إعدادات النظام.") |
|
|
|
|
|
if user_input and api_available: |
|
|
|
st.session_state.ai_assistant_messages.append({"role": "user", "content": user_input}) |
|
|
|
|
|
if uploaded_file is not None: |
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp_file: |
|
tmp_file.write(uploaded_file.getvalue()) |
|
temp_file_path = tmp_file.name |
|
|
|
try: |
|
|
|
if uploaded_file.name.lower().endswith('.pdf') and self.pdf_conversion_available: |
|
|
|
images = self.convert_from_path(temp_file_path, first_page=1, last_page=1) |
|
if images: |
|
|
|
image_path = f"{temp_file_path}.jpg" |
|
images[0].save(image_path, 'JPEG') |
|
|
|
|
|
result = self.claude_service.analyze_image( |
|
image_path=image_path, |
|
prompt=f"المستخدم رفع ملف PDF وسأل: {user_input}. يرجى تحليل هذه الصفحة من الملف وتقديم إجابة مناسبة.", |
|
model_name=selected_model |
|
) |
|
|
|
|
|
os.remove(image_path) |
|
else: |
|
result = {"error": "فشل في تحويل ملف PDF إلى صورة."} |
|
else: |
|
|
|
result = self.claude_service.analyze_image( |
|
image_path=temp_file_path, |
|
prompt=f"المستخدم رفع صورة وسأل: {user_input}. يرجى تحليل الصورة وتقديم إجابة مناسبة.", |
|
model_name=selected_model |
|
) |
|
except Exception as e: |
|
result = {"error": f"حدث خطأ أثناء معالجة الملف: {str(e)}"} |
|
|
|
|
|
os.remove(temp_file_path) |
|
|
|
|
|
if "error" in result: |
|
assistant_response = f"عذراً، حدث خطأ أثناء معالجة الملف: {result['error']}" |
|
else: |
|
assistant_response = result["content"] |
|
else: |
|
|
|
result = self.claude_service.chat_completion( |
|
messages=st.session_state.ai_assistant_messages, |
|
model_name=selected_model |
|
) |
|
|
|
|
|
if "error" in result: |
|
assistant_response = f"عذراً، حدث خطأ أثناء الاتصال بالمساعد الذكي: {result['error']}" |
|
else: |
|
assistant_response = result["content"] |
|
|
|
|
|
st.session_state.ai_assistant_messages.append({"role": "assistant", "content": assistant_response}) |
|
|
|
|
|
st.experimental_rerun() |
|
|
|
def _render_cost_prediction_tab(self): |
|
"""عرض تبويب التنبؤ بالتكاليف""" |
|
st.markdown("### التنبؤ بالتكاليف") |
|
|
|
st.markdown(""" |
|
استخدم هذه الأداة للتنبؤ بتكاليف المشروع بناءً على البيانات التاريخية والمعايير المشابهة. |
|
""") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
project_type = st.selectbox( |
|
"نوع المشروع", |
|
["بناء", "صيانة", "تصميم", "استشارات", "توريد"], |
|
key="cost_project_type" |
|
) |
|
|
|
project_size = st.select_slider( |
|
"حجم المشروع", |
|
options=["صغير", "متوسط", "كبير", "ضخم"], |
|
key="cost_project_size" |
|
) |
|
|
|
duration = st.number_input( |
|
"المدة المتوقعة (بالأشهر)", |
|
min_value=1, |
|
max_value=60, |
|
value=12, |
|
key="cost_duration" |
|
) |
|
|
|
with col2: |
|
location = st.selectbox( |
|
"الموقع", |
|
["المنطقة الشرقية", "المنطقة الغربية", "المنطقة الوسطى", "المنطقة الشمالية", "المنطقة الجنوبية"], |
|
key="cost_location" |
|
) |
|
|
|
complexity = st.select_slider( |
|
"درجة التعقيد", |
|
options=["منخفضة", "متوسطة", "عالية", "معقدة جداً"], |
|
key="cost_complexity" |
|
) |
|
|
|
resources = st.multiselect( |
|
"الموارد المطلوبة", |
|
["عمالة محلية", "عمالة أجنبية", "معدات ثقيلة", "تكنولوجيا متقدمة", "مواد أولية مستوردة"], |
|
default=["عمالة محلية"], |
|
key="cost_resources" |
|
) |
|
|
|
|
|
if st.button("تنبؤ بالتكاليف", key="predict_cost_button"): |
|
|
|
with st.spinner("جاري حساب التكاليف المتوقعة..."): |
|
|
|
time.sleep(1.5) |
|
|
|
|
|
base_cost = 0 |
|
if project_size == "صغير": |
|
base_cost = random.uniform(100000, 500000) |
|
elif project_size == "متوسط": |
|
base_cost = random.uniform(500000, 2000000) |
|
elif project_size == "كبير": |
|
base_cost = random.uniform(2000000, 10000000) |
|
else: |
|
base_cost = random.uniform(10000000, 50000000) |
|
|
|
|
|
complexity_factor = 1.0 |
|
if complexity == "منخفضة": |
|
complexity_factor = 0.8 |
|
elif complexity == "متوسطة": |
|
complexity_factor = 1.0 |
|
elif complexity == "عالية": |
|
complexity_factor = 1.3 |
|
else: |
|
complexity_factor = 1.6 |
|
|
|
|
|
duration_factor = 1.0 + (duration / 60) |
|
|
|
|
|
total_cost = base_cost * complexity_factor * duration_factor |
|
|
|
|
|
labor_cost = total_cost * random.uniform(0.3, 0.5) |
|
materials_cost = total_cost * random.uniform(0.2, 0.4) |
|
equipment_cost = total_cost * random.uniform(0.1, 0.2) |
|
overhead_cost = total_cost - labor_cost - materials_cost - equipment_cost |
|
|
|
|
|
st.success("تم حساب التكاليف المتوقعة بنجاح!") |
|
|
|
|
|
st.markdown("#### ملخص التكاليف المتوقعة") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
st.metric("التكلفة الإجمالية المتوقعة", f"{total_cost:,.2f} ريال") |
|
st.metric("تكلفة العمالة", f"{labor_cost:,.2f} ريال") |
|
st.metric("تكلفة المواد", f"{materials_cost:,.2f} ريال") |
|
|
|
with col2: |
|
st.metric("تكلفة المعدات", f"{equipment_cost:,.2f} ريال") |
|
st.metric("المصاريف العامة", f"{overhead_cost:,.2f} ريال") |
|
st.metric("مؤشر الثقة", f"{random.uniform(70, 95):.1f}%") |
|
|
|
|
|
cost_data = { |
|
'البند': ['عمالة', 'مواد', 'معدات', 'مصاريف عامة'], |
|
'التكلفة': [labor_cost, materials_cost, equipment_cost, overhead_cost] |
|
} |
|
cost_df = pd.DataFrame(cost_data) |
|
|
|
fig = px.pie(cost_df, values='التكلفة', names='البند', title='توزيع التكاليف المتوقعة') |
|
st.plotly_chart(fig) |
|
|
|
|
|
st.markdown("#### نصائح لتحسين التكلفة") |
|
st.info(""" |
|
* استخدام عمالة محلية لتقليل تكاليف العمالة والاستفادة من برامج دعم التوطين. |
|
* شراء المواد مباشرة من المورد الرئيسي لتقليل تكاليف الوسطاء. |
|
* استئجار المعدات بدلاً من شرائها إذا كانت مدة استخدامها قصيرة. |
|
* الاستفادة من برامج المحتوى المحلي للحصول على حوافز السعر. |
|
""") |
|
|
|
def _render_risk_analysis_tab(self): |
|
"""عرض تبويب تحليل المخاطر""" |
|
st.markdown("### تحليل المخاطر للمشاريع") |
|
|
|
st.markdown(""" |
|
استخدم هذه الأداة لتحديد وتقييم المخاطر المحتملة للمشروع، ووضع خطط الاستجابة المناسبة. |
|
""") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
project_type = st.selectbox( |
|
"نوع المشروع", |
|
["بناء", "صيانة", "تصميم", "استشارات", "توريد"], |
|
key="risk_project_type" |
|
) |
|
|
|
duration = st.number_input( |
|
"المدة المتوقعة (بالأشهر)", |
|
min_value=1, |
|
max_value=60, |
|
value=12, |
|
key="risk_duration" |
|
) |
|
|
|
with col2: |
|
complexity = st.select_slider( |
|
"درجة التعقيد", |
|
options=["منخفضة", "متوسطة", "عالية", "معقدة جداً"], |
|
key="risk_complexity" |
|
) |
|
|
|
value = st.number_input( |
|
"قيمة المشروع (بالريال)", |
|
min_value=100000, |
|
max_value=100000000, |
|
value=1000000, |
|
step=100000, |
|
format="%d", |
|
key="risk_value" |
|
) |
|
|
|
|
|
if st.button("تحليل المخاطر", key="analyze_risk_button"): |
|
|
|
with st.spinner("جاري تحليل المخاطر المحتملة..."): |
|
|
|
time.sleep(1.5) |
|
|
|
|
|
risk_data = [ |
|
{ |
|
"category": "مالية", |
|
"risk": "تغيرات في أسعار المواد", |
|
"impact": random.uniform(3, 5), |
|
"probability": random.uniform(2, 4), |
|
"severity": 0, |
|
"mitigation": "إضافة بند تعديل الأسعار في العقد، وتأمين المواد الرئيسية مبكرًا" |
|
}, |
|
{ |
|
"category": "فنية", |
|
"risk": "صعوبات في التنفيذ", |
|
"impact": random.uniform(2, 5), |
|
"probability": random.uniform(1, 4), |
|
"severity": 0, |
|
"mitigation": "إجراء دراسة فنية مفصلة قبل البدء، والاستعانة بخبراء متخصصين" |
|
}, |
|
{ |
|
"category": "إدارية", |
|
"risk": "تأخر في الجدول الزمني", |
|
"impact": random.uniform(3, 5), |
|
"probability": random.uniform(2, 5), |
|
"severity": 0, |
|
"mitigation": "وضع خطة زمنية واقعية مع هوامش احتياطية، ومتابعة التنفيذ أسبوعيًا" |
|
}, |
|
{ |
|
"category": "تنظيمية", |
|
"risk": "تغييرات في اللوائح والأنظمة", |
|
"impact": random.uniform(2, 4), |
|
"probability": random.uniform(1, 3), |
|
"severity": 0, |
|
"mitigation": "متابعة التحديثات التنظيمية، والتنسيق المستمر مع الجهات المعنية" |
|
}, |
|
{ |
|
"category": "بيئية", |
|
"risk": "ظروف مناخية غير متوقعة", |
|
"impact": random.uniform(1, 4), |
|
"probability": random.uniform(1, 3), |
|
"severity": 0, |
|
"mitigation": "جدولة الأنشطة الحساسة في المواسم المناسبة، وتوفير بدائل للظروف الطارئة" |
|
} |
|
] |
|
|
|
|
|
for risk in risk_data: |
|
risk["severity"] = risk["impact"] * risk["probability"] |
|
|
|
|
|
risk_data = sorted(risk_data, key=lambda x: x["severity"], reverse=True) |
|
|
|
|
|
risk_df = pd.DataFrame(risk_data) |
|
|
|
|
|
st.success("تم تحليل المخاطر المحتملة بنجاح!") |
|
|
|
|
|
st.markdown("#### سجل المخاطر المحتملة") |
|
|
|
|
|
risk_display = risk_df.copy() |
|
risk_display.columns = ["الفئة", "المخاطرة", "التأثير", "الاحتمالية", "الشدة", "إجراءات التخفيف"] |
|
risk_display["التأثير"] = risk_display["التأثير"].round(1) |
|
risk_display["الاحتمالية"] = risk_display["الاحتمالية"].round(1) |
|
risk_display["الشدة"] = risk_display["الشدة"].round(1) |
|
|
|
st.dataframe(risk_display, use_container_width=True) |
|
|
|
|
|
st.markdown("#### مصفوفة المخاطر") |
|
|
|
|
|
fig, ax = plt.subplots(figsize=(10, 8)) |
|
|
|
|
|
ax.set_xlim(0.5, 5.5) |
|
ax.set_ylim(0.5, 5.5) |
|
|
|
|
|
risk_levels = np.zeros((5, 5)) |
|
for i in range(5): |
|
for j in range(5): |
|
risk_levels[i, j] = (i + 1) * (j + 1) |
|
|
|
|
|
cmap = plt.cm.RdYlGn_r |
|
risk_levels_normalized = risk_levels / 25.0 |
|
plt.imshow(risk_levels_normalized, cmap=cmap, extent=[0.5, 5.5, 0.5, 5.5], alpha=0.3, origin='lower') |
|
|
|
|
|
for i in range(6): |
|
plt.axhline(y=i + 0.5, color='gray', linestyle='-', alpha=0.3) |
|
plt.axvline(x=i + 0.5, color='gray', linestyle='-', alpha=0.3) |
|
|
|
|
|
for i, risk in enumerate(risk_data): |
|
ax.scatter(risk["probability"], risk["impact"], s=300, alpha=0.8, |
|
color=plt.cm.cool(i/len(risk_data)), |
|
edgecolor='black', linewidth=1.5) |
|
ax.text(risk["probability"], risk["impact"], str(i+1), |
|
ha='center', va='center', fontweight='bold') |
|
|
|
|
|
ax.set_xlabel('الاحتمالية', fontsize=14) |
|
ax.set_ylabel('التأثير', fontsize=14) |
|
ax.set_title('مصفوفة المخاطر', fontsize=16) |
|
|
|
|
|
legend_text = "\n".join([f"{i+1}. {risk['risk']}" for i, risk in enumerate(risk_data)]) |
|
props = dict(boxstyle='round', facecolor='white', alpha=0.5) |
|
ax.text(1.05, 0.95, legend_text, transform=ax.transAxes, fontsize=10, |
|
verticalalignment='top', bbox=props) |
|
|
|
st.pyplot(fig) |
|
|
|
|
|
st.markdown("#### توصيات إدارة المخاطر") |
|
|
|
high_risks = [risk for risk in risk_data if risk["severity"] > 12] |
|
medium_risks = [risk for risk in risk_data if 6 < risk["severity"] <= 12] |
|
low_risks = [risk for risk in risk_data if risk["severity"] <= 6] |
|
|
|
st.warning(f""" |
|
**المخاطر العالية ({len(high_risks)}):** |
|
يجب وضع خطة استجابة تفصيلية لكل مخاطرة عالية وتخصيص مسؤول متابعة لها. |
|
""") |
|
|
|
st.info(f""" |
|
**المخاطر المتوسطة ({len(medium_risks)}):** |
|
مراقبة هذه المخاطر بشكل دوري والتأكد من تنفيذ إجراءات التخفيف. |
|
""") |
|
|
|
st.success(f""" |
|
**المخاطر المنخفضة ({len(low_risks)}):** |
|
يمكن قبول هذه المخاطر مع المراقبة الدورية. |
|
""") |
|
|
|
|
|
st.markdown(""" |
|
#### إجراءات موصى بها: |
|
|
|
1. تخصيص احتياطي للمخاطر بقيمة تتراوح بين 5-10% من قيمة المشروع. |
|
2. تعيين مسؤول إدارة مخاطر للمشروع. |
|
3. مراجعة سجل المخاطر بشكل أسبوعي. |
|
4. توثيق الدروس المستفادة من المخاطر التي تحققت. |
|
""") |
|
|
|
def _render_document_analysis_tab(self): |
|
"""عرض تبويب تحليل المستندات""" |
|
st.markdown("### تحليل مستندات المناقصة") |
|
|
|
st.markdown(""" |
|
استخدم هذه الأداة لتحليل مستندات المناقصة واستخراج المعلومات المهمة منها. |
|
""") |
|
|
|
|
|
upload_method = st.radio( |
|
"طريقة تحميل المستندات", |
|
["تحميل ملف", "نسخ ولصق النص"], |
|
horizontal=True, |
|
key="doc_upload_method" |
|
) |
|
|
|
document_text = "" |
|
|
|
if upload_method == "تحميل ملف": |
|
uploaded_file = st.file_uploader( |
|
"ارفع مستند المناقصة (PDF، DOCX، TXT)", |
|
type=["pdf", "docx", "txt"], |
|
key="document_file_upload" |
|
) |
|
|
|
if uploaded_file is not None: |
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp_file: |
|
tmp_file.write(uploaded_file.getvalue()) |
|
temp_file_path = tmp_file.name |
|
|
|
|
|
try: |
|
st.info("تم استلام الملف، جاري معالجته...") |
|
|
|
time.sleep(1) |
|
|
|
|
|
document_text = "هذا نموذج لنص مستند المناقصة تم استخراجه من الملف المرفوع.\n\n" |
|
document_text += "مناقصة رقم: 2025/123\n" |
|
document_text += "اسم المشروع: إنشاء مباني إدارية\n\n" |
|
document_text += "متطلبات المشروع:\n" |
|
document_text += "1. تصميم وتنفيذ مبنى إداري مكون من 5 طوابق\n" |
|
document_text += "2. إنشاء مواقف سيارات بسعة 200 سيارة\n" |
|
document_text += "3. توفير أنظمة تكييف وتهوية متطورة\n\n" |
|
document_text += "الشروط العامة:\n" |
|
document_text += "- مدة التنفيذ: 18 شهرًا من تاريخ استلام الموقع\n" |
|
document_text += "- غرامة التأخير: 0.5% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%\n" |
|
document_text += "- الدفعة المقدمة: 10% من قيمة العقد مقابل ضمان بنكي\n" |
|
document_text += "- محتوى محلي: لا يقل عن 60% من إجمالي العقد\n" |
|
|
|
st.success("تم معالجة الملف بنجاح") |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء معالجة الملف: {str(e)}") |
|
|
|
|
|
try: |
|
os.remove(temp_file_path) |
|
except: |
|
pass |
|
else: |
|
document_text = st.text_area( |
|
"الصق نص المناقصة هنا", |
|
height=300, |
|
key="document_text_input" |
|
) |
|
|
|
|
|
st.markdown("### خيارات التحليل") |
|
|
|
analysis_options = st.multiselect( |
|
"اختر عناصر التحليل", |
|
[ |
|
"المعلومات الأساسية", |
|
"الشروط العامة", |
|
"المتطلبات الفنية", |
|
"المحتوى المحلي", |
|
"مواصفات المشروع", |
|
"جدول الكميات" |
|
], |
|
default=["المعلومات الأساسية", "الشروط العامة"], |
|
key="doc_analysis_options" |
|
) |
|
|
|
|
|
if st.button("تحليل المستند", key="analyze_document_button") and document_text: |
|
|
|
with st.spinner("جاري تحليل المستند..."): |
|
|
|
time.sleep(2) |
|
|
|
|
|
st.success("تم تحليل المستند بنجاح!") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
|
|
if "المعلومات الأساسية" in analysis_options: |
|
st.markdown("#### المعلومات الأساسية") |
|
st.info(""" |
|
**رقم المناقصة:** 2025/123 |
|
|
|
**اسم المشروع:** إنشاء مباني إدارية |
|
|
|
**الجهة المالكة:** وزارة الأشغال العامة |
|
|
|
**تاريخ الطرح:** 15/03/2025 |
|
|
|
**تاريخ الإقفال:** 15/04/2025 |
|
|
|
**مدة التنفيذ:** 18 شهرًا |
|
""") |
|
|
|
|
|
if "الشروط العامة" in analysis_options: |
|
st.markdown("#### الشروط العامة") |
|
st.info(""" |
|
**الضمان الابتدائي:** 2% من قيمة العطاء |
|
|
|
**الضمان النهائي:** 5% من قيمة العقد |
|
|
|
**غرامة التأخير:** 0.5% أسبوعيًا بحد أقصى 10% |
|
|
|
**نظام الدفعات:** شهرية حسب الإنجاز |
|
|
|
**الدفعة المقدمة:** 10% مقابل ضمان بنكي |
|
|
|
**متطلبات التأمين:** تأمين شامل ضد الأخطار + تأمين مسؤولية مدنية |
|
""") |
|
|
|
|
|
if "المتطلبات الفنية" in analysis_options: |
|
st.markdown("#### المتطلبات الفنية") |
|
st.info(""" |
|
**الشهادات المطلوبة:** |
|
- ISO 9001 لنظام إدارة الجودة |
|
- ISO 14001 لنظام الإدارة البيئية |
|
- شهادة تصنيف المقاولين (درجة أولى) |
|
|
|
**الخبرات المطلوبة:** |
|
- 3 مشاريع مماثلة في آخر 5 سنوات |
|
- خبرة لا تقل عن 10 سنوات في المجال |
|
|
|
**الكادر الفني:** |
|
- مدير مشروع (خبرة 15 سنة) |
|
- مهندس مدني (خبرة 10 سنوات) |
|
- مهندس معماري (خبرة 8 سنوات) |
|
- مهندس كهرباء (خبرة 8 سنوات) |
|
- مهندس ميكانيكا (خبرة 8 سنوات) |
|
""") |
|
|
|
with col2: |
|
|
|
if "المحتوى المحلي" in analysis_options: |
|
st.markdown("#### متطلبات المحتوى المحلي") |
|
st.info(""" |
|
**نسبة المحتوى المحلي المطلوبة:** 60% |
|
|
|
**العناصر المحددة:** |
|
- توظيف 70% من القوى العاملة محلية |
|
- استخدام 50% من المواد من الصناعة المحلية |
|
- التعاقد مع موردين محليين بنسبة 40% |
|
|
|
**المستندات المطلوبة:** |
|
- شهادة المحتوى المحلي للمنتجات |
|
- خطة تفصيلية لتحقيق نسبة المحتوى المحلي |
|
- التزام بتقديم تقارير دورية |
|
""") |
|
|
|
|
|
if "مواصفات المشروع" in analysis_options: |
|
st.markdown("#### مواصفات المشروع") |
|
st.info(""" |
|
**المباني:** |
|
- مبنى إداري رئيسي (5 طوابق، 15,000 م²) |
|
- مبنى خدمات (طابقين، 2,000 م²) |
|
- مواقف سيارات (200 سيارة) |
|
|
|
**التشطيبات:** |
|
- واجهات زجاجية عاكسة |
|
- أرضيات رخام في المداخل والردهات |
|
- أنظمة تكييف مركزية موفرة للطاقة |
|
- أنظمة الأمن والسلامة متطورة |
|
|
|
**المعايير:** |
|
- تصميم موفر للطاقة حسب متطلبات LEED |
|
- مراعاة متطلبات الوصول الشامل |
|
- الالتزام بكود البناء السعودي |
|
""") |
|
|
|
|
|
if "جدول الكميات" in analysis_options: |
|
st.markdown("#### تحليل جدول الكميات") |
|
|
|
|
|
quantities_data = { |
|
'البند': [ |
|
'أعمال الحفر والردم', |
|
'أعمال الخرسانة', |
|
'أعمال البناء', |
|
'أعمال التشطيبات', |
|
'أعمال الكهرباء', |
|
'أعمال السباكة', |
|
'أعمال التكييف', |
|
'أعمال السلامة' |
|
], |
|
'الكمية': [ |
|
15000, |
|
7500, |
|
8000, |
|
17000, |
|
1, |
|
1, |
|
1, |
|
1 |
|
], |
|
'الوحدة': [ |
|
'م³', |
|
'م³', |
|
'م²', |
|
'م²', |
|
'مقطوعية', |
|
'مقطوعية', |
|
'مقطوعية', |
|
'مقطوعية' |
|
], |
|
'السعر التقديري': [ |
|
800000, |
|
3000000, |
|
2500000, |
|
4000000, |
|
1500000, |
|
1200000, |
|
2000000, |
|
1000000 |
|
] |
|
} |