|
|
|
|
|
""" |
|
|
CMA Arabic Document Chatbot with Simplified RAG System |
|
|
Uses TF-IDF and cosine similarity for document retrieval |
|
|
""" |
|
|
|
|
|
import os |
|
|
import json |
|
|
import gradio as gr |
|
|
import openai |
|
|
from typing import List, Dict, Any, Optional, Tuple |
|
|
import numpy as np |
|
|
import pickle |
|
|
import logging |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
try: |
|
|
import fitz |
|
|
PYMUPDF_AVAILABLE = True |
|
|
except ImportError: |
|
|
PYMUPDF_AVAILABLE = False |
|
|
logger.warning("PyMuPDF not available. PDF processing disabled.") |
|
|
|
|
|
try: |
|
|
from sklearn.feature_extraction.text import TfidfVectorizer |
|
|
from sklearn.metrics.pairwise import cosine_similarity |
|
|
SKLEARN_AVAILABLE = True |
|
|
except ImportError: |
|
|
SKLEARN_AVAILABLE = False |
|
|
logger.warning("scikit-learn not available. Using simple text matching.") |
|
|
|
|
|
class SimplePDFProcessor: |
|
|
"""Process all CMA PDF documents and extract text""" |
|
|
|
|
|
def __init__(self, pdf_directory: str = "/home/ubuntu/upload"): |
|
|
self.pdf_directory = pdf_directory |
|
|
|
|
|
def extract_text_from_pdf(self, pdf_path: str) -> List[Dict[str, Any]]: |
|
|
"""Extract text from PDF with metadata""" |
|
|
if not PYMUPDF_AVAILABLE: |
|
|
logger.warning("PyMuPDF not available. Cannot process PDF files.") |
|
|
return [] |
|
|
|
|
|
try: |
|
|
doc = fitz.open(pdf_path) |
|
|
chunks = [] |
|
|
|
|
|
for page_num in range(len(doc)): |
|
|
page = doc.load_page(page_num) |
|
|
text = page.get_text() |
|
|
|
|
|
|
|
|
text = self._clean_arabic_text(text) |
|
|
|
|
|
if len(text.strip()) > 100: |
|
|
|
|
|
text_chunks = self._split_text(text, max_length=1000) |
|
|
|
|
|
for i, chunk in enumerate(text_chunks): |
|
|
chunks.append({ |
|
|
'text': chunk, |
|
|
'source': os.path.basename(pdf_path), |
|
|
'page': page_num + 1, |
|
|
'chunk_id': f"{os.path.basename(pdf_path)}_page_{page_num + 1}_chunk_{i + 1}" |
|
|
}) |
|
|
|
|
|
doc.close() |
|
|
return chunks |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error processing {pdf_path}: {e}") |
|
|
return [] |
|
|
|
|
|
def _clean_arabic_text(self, text: str) -> str: |
|
|
"""Clean and normalize Arabic text""" |
|
|
import re |
|
|
|
|
|
text = re.sub(r'\s+', ' ', text) |
|
|
|
|
|
|
|
|
text = re.sub(r'^\d+\s*$', '', text, flags=re.MULTILINE) |
|
|
|
|
|
|
|
|
text = '\n'.join([line.strip() for line in text.split('\n') if line.strip()]) |
|
|
|
|
|
return text.strip() |
|
|
|
|
|
def _split_text(self, text: str, max_length: int = 1000) -> List[str]: |
|
|
"""Split text into smaller chunks""" |
|
|
sentences = text.split('.') |
|
|
chunks = [] |
|
|
current_chunk = "" |
|
|
|
|
|
for sentence in sentences: |
|
|
if len(current_chunk + sentence) < max_length: |
|
|
current_chunk += sentence + "." |
|
|
else: |
|
|
if current_chunk: |
|
|
chunks.append(current_chunk.strip()) |
|
|
current_chunk = sentence + "." |
|
|
|
|
|
if current_chunk: |
|
|
chunks.append(current_chunk.strip()) |
|
|
|
|
|
return chunks |
|
|
|
|
|
def process_all_pdfs(self) -> List[Dict[str, Any]]: |
|
|
"""Process all PDF files in the directory""" |
|
|
all_chunks = [] |
|
|
|
|
|
|
|
|
if not os.path.exists(self.pdf_directory): |
|
|
logger.warning(f"PDF directory {self.pdf_directory} does not exist. Using fallback knowledge base.") |
|
|
return [] |
|
|
|
|
|
try: |
|
|
pdf_files = [f for f in os.listdir(self.pdf_directory) if f.endswith('.pdf')] |
|
|
except Exception as e: |
|
|
logger.error(f"Error accessing PDF directory: {e}") |
|
|
return [] |
|
|
|
|
|
if not pdf_files: |
|
|
logger.warning("No PDF files found. Using fallback knowledge base.") |
|
|
return [] |
|
|
|
|
|
logger.info(f"Found {len(pdf_files)} PDF files to process") |
|
|
|
|
|
for pdf_file in pdf_files: |
|
|
pdf_path = os.path.join(self.pdf_directory, pdf_file) |
|
|
logger.info(f"Processing: {pdf_file}") |
|
|
|
|
|
chunks = self.extract_text_from_pdf(pdf_path) |
|
|
all_chunks.extend(chunks) |
|
|
|
|
|
logger.info(f"Extracted {len(chunks)} chunks from {pdf_file}") |
|
|
|
|
|
logger.info(f"Total chunks extracted: {len(all_chunks)}") |
|
|
return all_chunks |
|
|
|
|
|
class SimpleVectorStore: |
|
|
"""TF-IDF based vector store for semantic search""" |
|
|
|
|
|
def __init__(self): |
|
|
if SKLEARN_AVAILABLE: |
|
|
self.vectorizer = TfidfVectorizer( |
|
|
max_features=5000, |
|
|
stop_words=None, |
|
|
ngram_range=(1, 2), |
|
|
min_df=1, |
|
|
max_df=0.95 |
|
|
) |
|
|
self.tfidf_matrix = None |
|
|
else: |
|
|
self.vectorizer = None |
|
|
self.tfidf_matrix = None |
|
|
|
|
|
self.documents = [] |
|
|
|
|
|
def build_index(self, documents: List[Dict[str, Any]]): |
|
|
"""Build TF-IDF index from documents""" |
|
|
self.documents = documents |
|
|
|
|
|
if not SKLEARN_AVAILABLE: |
|
|
logger.warning("scikit-learn not available. Using simple text matching.") |
|
|
return |
|
|
|
|
|
texts = [doc['text'] for doc in documents] |
|
|
|
|
|
logger.info("Building TF-IDF index...") |
|
|
self.tfidf_matrix = self.vectorizer.fit_transform(texts) |
|
|
logger.info(f"Built TF-IDF index with {self.tfidf_matrix.shape[0]} documents and {self.tfidf_matrix.shape[1]} features") |
|
|
|
|
|
def search(self, query: str, k: int = 5) -> List[Dict[str, Any]]: |
|
|
"""Search for similar documents using TF-IDF and cosine similarity""" |
|
|
if not self.documents: |
|
|
return [] |
|
|
|
|
|
if not SKLEARN_AVAILABLE or self.tfidf_matrix is None: |
|
|
|
|
|
return self._simple_text_search(query, k) |
|
|
|
|
|
|
|
|
query_vector = self.vectorizer.transform([query]) |
|
|
|
|
|
|
|
|
similarities = cosine_similarity(query_vector, self.tfidf_matrix).flatten() |
|
|
|
|
|
|
|
|
top_indices = similarities.argsort()[-k:][::-1] |
|
|
|
|
|
results = [] |
|
|
for i, idx in enumerate(top_indices): |
|
|
if similarities[idx] > 0: |
|
|
result = self.documents[idx].copy() |
|
|
result['similarity_score'] = float(similarities[idx]) |
|
|
result['rank'] = i + 1 |
|
|
results.append(result) |
|
|
|
|
|
return results |
|
|
|
|
|
def _simple_text_search(self, query: str, k: int = 5) -> List[Dict[str, Any]]: |
|
|
"""Simple text matching fallback""" |
|
|
query_words = query.lower().split() |
|
|
results = [] |
|
|
|
|
|
for i, doc in enumerate(self.documents): |
|
|
text_lower = doc['text'].lower() |
|
|
matches = sum(1 for word in query_words if word in text_lower) |
|
|
|
|
|
if matches > 0: |
|
|
result = doc.copy() |
|
|
result['similarity_score'] = matches / len(query_words) |
|
|
result['rank'] = len(results) + 1 |
|
|
results.append(result) |
|
|
|
|
|
|
|
|
results.sort(key=lambda x: x['similarity_score'], reverse=True) |
|
|
return results[:k] |
|
|
|
|
|
def save_index(self, filepath: str): |
|
|
"""Save the index and documents""" |
|
|
data = { |
|
|
'documents': self.documents, |
|
|
'vectorizer': self.vectorizer, |
|
|
'tfidf_matrix': self.tfidf_matrix |
|
|
} |
|
|
|
|
|
try: |
|
|
with open(f"{filepath}.pkl", 'wb') as f: |
|
|
pickle.dump(data, f) |
|
|
logger.info(f"Saved index to {filepath}") |
|
|
except Exception as e: |
|
|
logger.error(f"Failed to save index: {e}") |
|
|
|
|
|
def load_index(self, filepath: str) -> bool: |
|
|
"""Load the index and documents""" |
|
|
try: |
|
|
with open(f"{filepath}.pkl", 'rb') as f: |
|
|
data = pickle.load(f) |
|
|
|
|
|
self.documents = data['documents'] |
|
|
self.vectorizer = data['vectorizer'] |
|
|
self.tfidf_matrix = data['tfidf_matrix'] |
|
|
|
|
|
logger.info(f"Loaded index from {filepath}") |
|
|
return True |
|
|
except Exception as e: |
|
|
logger.error(f"Failed to load index: {e}") |
|
|
return False |
|
|
|
|
|
class CMASimpleRAGBot: |
|
|
"""CMA chatbot with simplified RAG system""" |
|
|
|
|
|
def __init__(self, pdf_directory: str = "/home/ubuntu/upload"): |
|
|
self.pdf_directory = pdf_directory |
|
|
self.vector_store = SimpleVectorStore() |
|
|
self.conversation_history = [] |
|
|
self.setup_openai() |
|
|
self.initialize_knowledge_base() |
|
|
|
|
|
def setup_openai(self): |
|
|
"""Configure OpenAI API""" |
|
|
api_key = os.getenv('OPENAI_API_KEY') |
|
|
|
|
|
if not api_key: |
|
|
self.client = None |
|
|
logger.warning("No OpenAI API key found. Using knowledge base only.") |
|
|
return |
|
|
|
|
|
try: |
|
|
openai.api_key = api_key |
|
|
self.client = openai |
|
|
logger.info("OpenAI API configured successfully") |
|
|
except Exception as e: |
|
|
self.client = None |
|
|
logger.error(f"Failed to configure OpenAI: {e}") |
|
|
|
|
|
def initialize_knowledge_base(self): |
|
|
"""Initialize or load the knowledge base""" |
|
|
index_path = "/tmp/cma_simple_index" |
|
|
|
|
|
|
|
|
if self.vector_store.load_index(index_path): |
|
|
logger.info("Loaded existing knowledge base") |
|
|
return |
|
|
|
|
|
|
|
|
logger.info("Building new knowledge base from PDFs...") |
|
|
processor = SimplePDFProcessor(self.pdf_directory) |
|
|
documents = processor.process_all_pdfs() |
|
|
|
|
|
if not documents: |
|
|
logger.warning("No documents found! Using fallback knowledge base.") |
|
|
self._create_fallback_knowledge_base() |
|
|
else: |
|
|
|
|
|
self.vector_store.build_index(documents) |
|
|
|
|
|
|
|
|
self.vector_store.save_index(index_path) |
|
|
logger.info("Knowledge base built and saved successfully") |
|
|
|
|
|
def _create_fallback_knowledge_base(self): |
|
|
"""Create a comprehensive fallback knowledge base""" |
|
|
fallback_docs = [ |
|
|
{ |
|
|
'text': 'أنظمة الاستثمار الجماعي هي ترتيبات استثمارية تجمع أموال عدد من المستثمرين لاستثمارها في محفظة متنوعة من الأوراق المالية أو الأصول الأخرى، وتديرها جهة متخصصة نيابة عن المستثمرين مقابل رسوم إدارة. وتشمل صناديق الاستثمار المفتوحة والمغلقة، وصناديق المؤشرات، وصناديق الاستثمار العقاري المتداولة.', |
|
|
'source': 'أنظمة الاستثمار الجماعي', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_collective_investment' |
|
|
}, |
|
|
{ |
|
|
'text': 'قواعد الإدراج هي مجموعة من الشروط والمتطلبات التي يجب على الشركات استيفاؤها للحصول على موافقة إدراج أوراقها المالية في البورصة. تشمل هذه القواعد الحد الأدنى لرأس المال المدفوع، ومتطلبات الحوكمة، والإفصاح المالي، وتوزيع الملكية، والامتثال للقوانين واللوائح ذات الصلة.', |
|
|
'source': 'قواعد الإدراج', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_listing_rules' |
|
|
}, |
|
|
{ |
|
|
'text': 'يجب على الشخص المرخص له الاحتفاظ بجميع بيانات العملاء والمعاملات لمدة لا تقل عن خمس سنوات من تاريخ انتهاء العلاقة التجارية أو إتمام المعاملة، أيهما أطول. كما يجب أن تكون هذه البيانات محفوظة بطريقة آمنة ومنظمة تسمح بالوصول إليها عند الحاجة للمراجعة أو التدقيق.', |
|
|
'source': 'أموال العملاء وأصولهم', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_kyc_retention' |
|
|
}, |
|
|
{ |
|
|
'text': 'يجب أن يكون التفويض القانوني صادراً من كاتب عدل أو موثق معتمد من وزارة العدل، ويجب أن يتضمن نصاً صريحاً بالصلاحيات المفوضة مثل فتح الحساب، تشغيله، إجراء الحوالات، أو بيع وشراء الأوراق المالية. لا يعتد بالتفويض الإلكتروني إلا إذا كان موثقاً ومعتمداً وفق قانون المعاملات الإلكترونية.', |
|
|
'source': 'أنشطة الأوراق المالية', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_legal_authorization' |
|
|
}, |
|
|
{ |
|
|
'text': 'يتعين على موظفي الشخص المرخص له الالتزام بالتعليمات والقيود المفروضة عليهم من قبل الهيئة، حيث يتعين إبلاغ مسؤول المطابقة والالتزام على الفور بأي صفقة (بيع أو شراء أوراق مالية محلية) يجريها عن نفسه أو بالإنابة عن أحد أقربائه أو عن شركة تابعة له أو لأحد أقربائه.', |
|
|
'source': 'أخلاقيات العمل', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_employee_trading' |
|
|
}, |
|
|
{ |
|
|
'text': 'يجب على الشركات المدرجة تطبيق قواعد الحوكمة التي تشمل: تشكيل مجلس إدارة مستقل وفعال، إنشاء لجان متخصصة (المراجعة، المكافآت، الترشيحات)، وضع سياسات واضحة لإدارة المخاطر، ضمان الشفافية في التقارير المالية، وحماية حقوق المساهمين.', |
|
|
'source': 'حوكمة الشركات', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_corporate_governance' |
|
|
}, |
|
|
{ |
|
|
'text': 'يجب على الأشخاص المرخص لهم تطبيق سياسات وإجراءات شاملة لمكافحة غسل الأموال تشمل: التحقق من هوية العملاء، مراقبة المعاملات المشبوهة، الاحتفاظ بالسجلات، التدريب المستمر للموظفين، والإبلاغ عن العمليات المشبوهة للسلطات المختصة.', |
|
|
'source': 'مكافحة غسل الأموال وتمويل الإرهاب', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_aml_cft' |
|
|
}, |
|
|
{ |
|
|
'text': 'التقنيات المالية (FinTech) تشمل استخدام التكنولوجيا لتحسين وأتمتة تقديم واستخدام الخدمات المالية. يجب على مقدمي خدمات التقنيات المالية الحصول على التراخيص المناسبة من هيئة أسواق المال والامتثال لجميع اللوائح ذات الصلة بحماية المستهلك وأمن البيانات.', |
|
|
'source': 'التقنيات المالية', |
|
|
'page': 1, |
|
|
'chunk_id': 'fallback_fintech' |
|
|
} |
|
|
] |
|
|
|
|
|
self.vector_store.build_index(fallback_docs) |
|
|
logger.info("Fallback knowledge base created with comprehensive CMA content") |
|
|
|
|
|
def generate_response(self, question: str) -> str: |
|
|
"""Generate response using simplified RAG system""" |
|
|
if not question.strip(): |
|
|
return "يرجى كتابة سؤالك أو استفسارك." |
|
|
|
|
|
|
|
|
if any(greeting in question.lower() for greeting in ['سلام', 'مرحبا', 'أهلا']): |
|
|
return "وعليكم السلام ورحمة الله وبركاته. أهلاً وسهلاً بك في مستشار هيئة أسواق المال الكويتية. أنا مستشار ذكي مدرب على جميع وثائق هيئة أسواق المال الكويتية باستخدام تقنيات الذكاء الاصطناعي المتقدمة. كيف يمكنني مساعدتك اليوم؟" |
|
|
|
|
|
|
|
|
relevant_docs = self.vector_store.search(question, k=3) |
|
|
|
|
|
if not relevant_docs: |
|
|
return self._no_information_response() |
|
|
|
|
|
|
|
|
if self.client: |
|
|
return self._generate_ai_response(question, relevant_docs) |
|
|
else: |
|
|
return self._generate_fallback_response(question, relevant_docs) |
|
|
|
|
|
def _generate_ai_response(self, question: str, context_docs: List[Dict]) -> str: |
|
|
"""Generate AI response with retrieved context""" |
|
|
try: |
|
|
|
|
|
context = "معلومات مسترجعة من وثائق هيئة أسواق المال الكويتية:\n\n" |
|
|
|
|
|
for i, doc in enumerate(context_docs[:3], 1): |
|
|
context += f"المصدر {i}: {doc['source']} (صفحة {doc['page']})\n" |
|
|
context += f"المحتوى: {doc['text'][:800]}...\n" |
|
|
context += f"درجة الصلة: {doc['similarity_score']:.3f}\n\n" |
|
|
|
|
|
prompt = f"""أنت مستشار متخصص في قوانين هيئة أسواق المال الكويتية (CMA). |
|
|
|
|
|
{context} |
|
|
|
|
|
السؤال: {question} |
|
|
|
|
|
تعليمات: |
|
|
- استخدم المعلومات المسترجعة من الوثائق الرسمية |
|
|
- أجب باللغة العربية الفصحى |
|
|
- كن دقيقاً ومفصلاً |
|
|
- اذكر المصادر المستخدمة |
|
|
- إذا لم تكن المعلومات كافية، اعترف بذلك |
|
|
|
|
|
الإجابة:""" |
|
|
|
|
|
response = self.client.ChatCompletion.create( |
|
|
model="gpt-3.5-turbo", |
|
|
messages=[ |
|
|
{"role": "system", "content": "أنت مستشار متخصص في قوانين هيئة أسواق المال الكويتية. أجب باللغة العربية فقط واستخدم المعلومات المقدمة."}, |
|
|
{"role": "user", "content": prompt} |
|
|
], |
|
|
max_tokens=800, |
|
|
temperature=0.2 |
|
|
) |
|
|
|
|
|
ai_response = response.choices[0].message.content.strip() |
|
|
|
|
|
|
|
|
sources = "\n\n📚 **المصادر المسترجعة:**\n" |
|
|
for doc in context_docs[:3]: |
|
|
sources += f"• {doc['source']} (صفحة {doc['page']}) - درجة الصلة: {doc['similarity_score']:.3f}\n" |
|
|
|
|
|
final_response = ai_response + sources |
|
|
|
|
|
|
|
|
self.conversation_history.append({ |
|
|
'question': question, |
|
|
'answer': final_response, |
|
|
'source': 'rag_ai' |
|
|
}) |
|
|
|
|
|
return final_response |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error with AI response: {e}") |
|
|
return self._generate_fallback_response(question, context_docs) |
|
|
|
|
|
def _generate_fallback_response(self, question: str, context_docs: List[Dict]) -> str: |
|
|
"""Generate fallback response using retrieved documents only""" |
|
|
if not context_docs: |
|
|
return self._no_information_response() |
|
|
|
|
|
best_doc = context_docs[0] |
|
|
|
|
|
response = f"بناءً على الوثائق المسترجعة من قاعدة المعرفة:\n\n" |
|
|
response += f"{best_doc['text']}\n\n" |
|
|
|
|
|
|
|
|
response += "📚 **المصادر المسترجعة:**\n" |
|
|
for i, doc in enumerate(context_docs[:3], 1): |
|
|
response += f"{i}. {doc['source']} (صفحة {doc['page']}) - درجة الصلة: {doc['similarity_score']:.3f}\n" |
|
|
|
|
|
response += "\n💡 للحصول على معلومات أكثر تفصيلاً، يرجى الرجوع إلى الوثائق الرسمية لهيئة أسواق المال الكويتية." |
|
|
|
|
|
|
|
|
self.conversation_history.append({ |
|
|
'question': question, |
|
|
'answer': response, |
|
|
'source': 'rag_fallback' |
|
|
}) |
|
|
|
|
|
return response |
|
|
|
|
|
def _no_information_response(self) -> str: |
|
|
"""Response when no relevant information is found""" |
|
|
return """عذراً، لم أتمكن من العثور على معلومات ذات صلة في قاعدة المعرفة الحالية للإجابة على هذا السؤال. |
|
|
|
|
|
يرجى: |
|
|
• إعادة صياغة السؤال بطريقة أخرى |
|
|
• التأكد من أن السؤال متعلق بقوانين هيئة أسواق المال الكويتية |
|
|
• الرجوع إلى الموقع الرسمي: cma.gov.kw |
|
|
• التواصل مع الهيئة مباشرة للحصول على معلومات دقيقة |
|
|
|
|
|
🔍 **اقتراحات للأسئلة:** |
|
|
• ما هي أنظمة الاستثمار الجماعي؟ |
|
|
• ما هي قواعد الإدراج؟ |
|
|
• ما هي متطلبات حوكمة الشركات؟ |
|
|
• ما هي المدة القانونية للاحتفاظ ببيانات KYC؟""" |
|
|
|
|
|
def get_knowledge_base_stats(self) -> Dict[str, Any]: |
|
|
"""Get statistics about the knowledge base""" |
|
|
if not self.vector_store.documents: |
|
|
return {'total_documents': 0, 'total_sources': 0, 'sources': []} |
|
|
|
|
|
sources = set(doc['source'] for doc in self.vector_store.documents) |
|
|
|
|
|
return { |
|
|
'total_documents': len(self.vector_store.documents), |
|
|
'total_sources': len(sources), |
|
|
'sources': list(sources) |
|
|
} |
|
|
|
|
|
def clear_conversation(self): |
|
|
"""Clear conversation history""" |
|
|
self.conversation_history = [] |
|
|
|
|
|
def create_simple_rag_interface(): |
|
|
"""Create Gradio interface with simplified RAG system""" |
|
|
|
|
|
bot = CMASimpleRAGBot() |
|
|
stats = bot.get_knowledge_base_stats() |
|
|
|
|
|
def chat_response(message: str, history: List[Tuple[str, str]]) -> Tuple[List[Tuple[str, str]], str]: |
|
|
"""Handle chat responses""" |
|
|
if not message.strip(): |
|
|
return history if history else [], "" |
|
|
|
|
|
response = bot.generate_response(message) |
|
|
|
|
|
if history is None: |
|
|
history = [] |
|
|
|
|
|
history.append((message, response)) |
|
|
return history, "" |
|
|
|
|
|
def clear_chat(): |
|
|
"""Clear chat and conversation history""" |
|
|
bot.clear_conversation() |
|
|
return [], "" |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="مستشار هيئة أسواق المال الكويتية - نظام RAG المبسط", |
|
|
theme=gr.themes.Soft(primary_hue="blue"), |
|
|
css=""" |
|
|
.gradio-container { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
direction: rtl; |
|
|
text-align: right; |
|
|
} |
|
|
""" |
|
|
) as interface: |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div style="text-align: center; padding: 30px; background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: white; border-radius: 15px; margin-bottom: 25px;"> |
|
|
<h1 style="margin: 0; font-size: 2.5rem;">🤖 مستشار هيئة أسواق المال الكويتية</h1> |
|
|
<p style="margin: 15px 0 0 0; font-size: 1.2rem;">نظام RAG مع معالجة شاملة لجميع وثائق PDF</p> |
|
|
<p style="margin: 10px 0 0 0; font-size: 1rem; opacity: 0.9;">مدرب على جميع وثائق هيئة أسواق المال الكويتية باستخدام TF-IDF والبحث الدلالي</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
search_type = "TF-IDF + Cosine" if SKLEARN_AVAILABLE else "Simple Text" |
|
|
gr.HTML(f""" |
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;"> |
|
|
<div style="background: #e3f2fd; padding: 20px; border-radius: 10px; text-align: center;"> |
|
|
<h3 style="margin: 0; color: #1565c0;">📄 المستندات</h3> |
|
|
<p style="margin: 5px 0 0 0; font-size: 1.5rem; font-weight: bold; color: #1565c0;">{stats['total_documents']:,}</p> |
|
|
</div> |
|
|
<div style="background: #e8f5e8; padding: 20px; border-radius: 10px; text-align: center;"> |
|
|
<h3 style="margin: 0; color: #2e7d32;">📚 المصادر</h3> |
|
|
<p style="margin: 5px 0 0 0; font-size: 1.5rem; font-weight: bold; color: #2e7d32;">{stats['total_sources']}</p> |
|
|
</div> |
|
|
<div style="background: #fff3e0; padding: 20px; border-radius: 10px; text-align: center;"> |
|
|
<h3 style="margin: 0; color: #f57c00;">🔍 نوع البحث</h3> |
|
|
<p style="margin: 5px 0 0 0; font-size: 1.2rem; font-weight: bold; color: #f57c00;">{search_type}</p> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
chatbot = gr.Chatbot( |
|
|
label="💬 المحادثة مع المستشار", |
|
|
height=500 |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
msg = gr.Textbox( |
|
|
label="✍️ اكتب سؤالك هنا", |
|
|
placeholder="مثال: ما هي أنظمة الاستثمار الجماعي؟", |
|
|
lines=2, |
|
|
scale=4 |
|
|
) |
|
|
submit_btn = gr.Button("📤 إرسال", variant="primary", scale=1) |
|
|
|
|
|
clear_btn = gr.Button("🗑️ مسح المحادثة", variant="secondary") |
|
|
|
|
|
|
|
|
with gr.Accordion("💡 أمثلة على الأسئلة", open=True): |
|
|
examples = [ |
|
|
"السلام عليكم", |
|
|
"ما هي أنظمة الاستثمار الجماعي؟", |
|
|
"عرف قواعد الإدراج", |
|
|
"ما هي المدة القانونية للاحتفاظ ببيانات KYC؟", |
|
|
"ما هي متطلبات حوكمة الشركات؟", |
|
|
"ما هي شروط التفويض القانوني؟", |
|
|
"ما هي قيود تداولات الموظفين؟" |
|
|
] |
|
|
|
|
|
example_buttons = [] |
|
|
for example in examples: |
|
|
btn = gr.Button(example, variant="secondary", size="sm") |
|
|
example_buttons.append(btn) |
|
|
btn.click(lambda x=example: x, None, msg) |
|
|
|
|
|
|
|
|
with gr.Accordion("🔧 التفاصيل التقنية", open=False): |
|
|
gr.HTML(f""" |
|
|
<div style="padding: 20px; background: #f8f9fa; border-radius: 10px;"> |
|
|
<h3>⚙️ مواصفات النظام:</h3> |
|
|
<ul style="text-align: right;"> |
|
|
<li><strong>نوع النظام:</strong> RAG (Retrieval-Augmented Generation)</li> |
|
|
<li><strong>فهرسة المتجهات:</strong> {"TF-IDF" if SKLEARN_AVAILABLE else "Simple Text Matching"}</li> |
|
|
<li><strong>البحث الدلالي:</strong> {"Cosine Similarity" if SKLEARN_AVAILABLE else "Keyword Matching"}</li> |
|
|
<li><strong>نموذج الذكاء الاصطناعي:</strong> OpenAI GPT-3.5 Turbo</li> |
|
|
<li><strong>معالجة النصوص:</strong> {"PyMuPDF + Arabic Processing" if PYMUPDF_AVAILABLE else "Text Processing Only"}</li> |
|
|
<li><strong>تقسيم النصوص:</strong> Intelligent Chunking</li> |
|
|
</ul> |
|
|
|
|
|
<h3>📊 إحصائيات قاعدة المعرفة:</h3> |
|
|
<ul style="text-align: right;"> |
|
|
<li><strong>إجمالي المستندات:</strong> {stats['total_documents']:,} مستند</li> |
|
|
<li><strong>عدد المصادر:</strong> {stats['total_sources']} مصدر</li> |
|
|
<li><strong>طريقة الاسترجاع:</strong> Top-K Similarity Search</li> |
|
|
<li><strong>دعم اللغة العربية:</strong> كامل مع RTL</li> |
|
|
</ul> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
msg.submit(chat_response, [msg, chatbot], [chatbot, msg]) |
|
|
submit_btn.click(chat_response, [msg, chatbot], [chatbot, msg]) |
|
|
clear_btn.click(clear_chat, None, [chatbot, msg]) |
|
|
|
|
|
|
|
|
msg.submit(lambda: "", None, msg) |
|
|
submit_btn.click(lambda: "", None, msg) |
|
|
|
|
|
return interface |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
print("🚀 Starting CMA Simple RAG Chatbot...") |
|
|
print("📚 Processing all PDF documents with TF-IDF indexing...") |
|
|
|
|
|
interface = create_simple_rag_interface() |
|
|
interface.launch( |
|
|
share=False, |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
show_error=True |
|
|
) |
|
|
|