structural-bim-assistant / src /streamlit_app.py
ahmednssr's picture
Update src/streamlit_app.py
b731e69 verified
import streamlit as st
import pandas as pd
from sentence_transformers import SentenceTransformer
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import requests
import os
# Page config
st.set_page_config(
page_title="Structural BIM Assistant",
page_icon="🏗️",
layout="wide"
)
# Custom CSS for Arabic support and styling
st.markdown("""
<style>
.stApp {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.main-header {
background: linear-gradient(90deg, #1e40af 0%, #3b82f6 100%);
padding: 2rem;
border-radius: 10px;
color: white;
text-align: center;
margin-bottom: 2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.question-card {
background: white;
padding: 1.5rem;
border-radius: 10px;
margin: 1rem 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-right: 4px solid #3b82f6;
}
.answer-card {
background: #f0f9ff;
padding: 1.5rem;
border-radius: 10px;
margin: 1rem 0;
border-right: 4px solid #10b981;
}
.category-badge {
display: inline-block;
background: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.85rem;
margin: 0.25rem;
}
.rarity-badge {
display: inline-block;
background: #dcfce7;
color: #166534;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.85rem;
margin: 0.25rem;
}
.stButton>button {
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 8px;
font-weight: bold;
}
.arabic-text {
direction: rtl;
text-align: right;
font-size: 1.1rem;
line-height: 1.8;
}
</style>
""", unsafe_allow_html=True)
# Initialize session state
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
# Load FAQ Data
@st.cache_data
def load_faq_data():
"""Load FAQ data from local CSV file"""
try:
# Try to read from local file first
if os.path.exists('faq_data.csv'):
df = pd.read_csv('faq_data.csv', skiprows=1)
elif os.path.exists('src/faq_data.csv'):
df = pd.read_csv('src/faq_data.csv', skiprows=1)
else:
st.error("❌ faq_data.csv not found. Please upload the CSV file.")
return None
# Set proper column names
df.columns = ['No', 'Category', 'Rarety', 'Questions', 'Additional Questions Data (image)',
'Answers', 'Additional data_01 (image)', 'Additional data_02 (video links or extra links)']
# Clean and filter
df.columns = df.columns.str.strip()
df = df[df['Questions'].notna() & (df['Questions'] != '') & (df['Questions'].str.len() > 5)]
st.success(f"✅ تم تحميل {len(df)} سؤال بنجاح!")
return df
except Exception as e:
st.error(f"Error loading FAQ data: {e}")
return None
# Load embedding model
@st.cache_resource
def load_embedding_model():
"""Load sentence transformer model for semantic search"""
try:
model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
return model
except Exception as e:
st.error(f"Error loading model: {e}")
return None
# Hugging Face API call
def query_huggingface(prompt, api_token):
"""Query Hugging Face API with free model"""
API_URL = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.2"
headers = {"Authorization": f"Bearer {api_token}"}
payload = {
"inputs": prompt,
"parameters": {
"max_new_tokens": 500,
"temperature": 0.7,
"top_p": 0.9,
"return_full_text": False
}
}
try:
response = requests.post(API_URL, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
if isinstance(result, list) and len(result) > 0:
return result[0].get('generated_text', '').strip()
elif response.status_code == 503:
return "⏳ النموذج يتم تحميله حالياً، جرب مرة أخرى بعد 20 ثانية..."
else:
return f"خطأ في الاتصال بالنموذج: {response.status_code}"
except Exception as e:
return f"خطأ: {str(e)}"
return "عذراً، لم أتمكن من الحصول على إجابة"
# Search FAQ database
def search_faq(question, df, model, threshold=0.6):
"""Search FAQ database using semantic similarity"""
if df is None or model is None:
return None, 0
try:
question_embedding = model.encode([question])
faq_questions = df['Questions'].tolist()
faq_embeddings = model.encode(faq_questions)
similarities = cosine_similarity(question_embedding, faq_embeddings)[0]
max_idx = np.argmax(similarities)
max_similarity = similarities[max_idx]
if max_similarity >= threshold:
return df.iloc[max_idx], max_similarity
return None, max_similarity
except Exception as e:
st.error(f"Error in search: {e}")
return None, 0
# Main app
def main():
# Header
st.markdown("""
<div class="main-header">
<h1>🏗️ Structural BIM Assistant</h1>
<p>مساعدك الذكي لأسئلة Revit والهندسة الإنشائية</p>
</div>
""", unsafe_allow_html=True)
# Sidebar
with st.sidebar:
st.header("⚙️ الإعدادات")
# Try to get token from environment
default_token = os.getenv("HUGGINGFACE_TOKEN", "")
# API Token input
api_token = st.text_input(
"🔑 Hugging Face Token",
type="password",
value=default_token,
help="سيتم استخدام التوكن من الإعدادات تلقائياً"
)
if not api_token:
st.warning("⚠️ يرجى إضافة Hugging Face Token في Settings → Repository secrets")
# Search threshold
threshold = st.slider(
"🎯 دقة البحث",
min_value=0.3,
max_value=0.9,
value=0.6,
step=0.1,
help="كلما زادت القيمة، كلما كانت النتائج أدق"
)
st.divider()
# Statistics
df = load_faq_data()
if df is not None:
st.metric("📚 عدد الأسئلة في قاعدة البيانات", len(df))
st.subheader("📊 التصنيفات")
category_counts = df['Category'].value_counts()
for cat, count in category_counts.items():
if pd.notna(cat):
st.write(f"• {cat}: {count}")
st.divider()
if st.button("🗑️ مسح المحادثة"):
st.session_state.chat_history = []
st.rerun()
# Load models
df = load_faq_data()
model = load_embedding_model()
if df is None:
st.error("⚠️ فشل تحميل قاعدة البيانات")
return
if model is None:
st.error("⚠️ فشل تحميل نموذج البحث")
return
# Example questions
with st.expander("💡 أسئلة شائعة - اضغط لاختيار"):
col1, col2 = st.columns(2)
examples = df.head(6)['Questions'].tolist()
for idx, example in enumerate(examples):
col = col1 if idx % 2 == 0 else col2
with col:
if st.button(example[:80] + "...", key=f"ex_{idx}"):
st.session_state.user_question = example
# Chat interface
st.markdown("### 💬 اسأل سؤالك")
user_question = st.text_area(
"اكتب سؤالك هنا:",
height=100,
key="question_input",
placeholder="مثال: كيف أعمل Level جديد في Revit؟"
)
col1, col2 = st.columns([1, 5])
with col1:
ask_button = st.button("🚀 إرسال", use_container_width=True)
# Process question
if ask_button and user_question.strip():
with st.spinner("🔍 جاري البحث عن الإجابة..."):
result, similarity = search_faq(user_question, df, model, threshold)
st.markdown(f"""
<div class="question-card">
<strong>❓ سؤالك:</strong>
<p class="arabic-text">{user_question}</p>
</div>
""", unsafe_allow_html=True)
if result is not None:
st.success(f"✅ وجدت إجابة في قاعدة البيانات (دقة: {similarity*100:.1f}%)")
st.markdown(f"""
<div class="answer-card">
<div>
<span class="category-badge">📁 {result['Category']}</span>
<span class="rarity-badge">⭐ {result['Rarety']}</span>
</div>
<br>
<strong>✅ الإجابة:</strong>
<p class="arabic-text">{result['Answers']}</p>
</div>
""", unsafe_allow_html=True)
if pd.notna(result.get('Additional data_02 (video links or extra links)')):
links = result['Additional data_02 (video links or extra links)']
st.info(f"🔗 روابط إضافية: {links}")
with st.expander("📝 السؤال الأصلي في قاعدة البيانات"):
st.write(result['Questions'])
else:
st.warning(f"⚠️ لم أجد إجابة مطابقة في قاعدة البيانات (أقرب تطابق: {similarity*100:.1f}%)")
if api_token:
st.info("🤖 سأحاول الإجابة باستخدام الذكاء الاصطناعي...")
prompt = f"""أنت مساعد متخصص في Structural BIM و Revit.
أجب على السؤال التالي بشكل مختصر ومفيد باللغة العربية:
السؤال: {user_question}
الإجابة:"""
ai_response = query_huggingface(prompt, api_token)
st.markdown(f"""
<div class="answer-card">
<strong>🤖 إجابة الذكاء الاصطناعي:</strong>
<p class="arabic-text">{ai_response}</p>
<br>
<em style="color: #6b7280; font-size: 0.9rem;">
⚠️ هذه الإجابة من الذكاء الاصطناعي وليست من قاعدة البيانات.
يُرجى التحقق من دقتها.
</em>
</div>
""", unsafe_allow_html=True)
else:
st.error("⚠️ يرجى إضافة Hugging Face Token")
ai_response = "لا يوجد إجابة متاحة"
st.session_state.chat_history.append({
'question': user_question,
'answer': result['Answers'] if result is not None else ai_response,
'source': 'database' if result is not None else 'ai',
'similarity': similarity
})
# Chat history
if st.session_state.chat_history:
st.markdown("---")
st.markdown("### 📜 سجل المحادثات")
for idx, chat in enumerate(reversed(st.session_state.chat_history[-5:])):
with st.expander(f"💬 {chat['question'][:60]}..."):
st.markdown(f"**السؤال:** {chat['question']}")
st.markdown(f"**الإجابة:** {chat['answer']}")
source_emoji = "📚" if chat['source'] == 'database' else "🤖"
st.caption(f"{source_emoji} المصدر: {'قاعدة البيانات' if chat['source'] == 'database' else 'الذكاء الاصطناعي'}")
if __name__ == "__main__":
main()