""" FilGoalBot — Hugging Face Spaces entry point ============================================= Single-process Gradio app that loads FilGoalRAG directly (no FastAPI hop). For local dev with the FastAPI backend split, run `python -m frontend.app`. Required Space secret: GROQ_API_KEY Optional Space variable: FILGOAL_FORCE_SMALL_MODEL=1 (route everything to 8B) """ import logging import time import gradio as gr from dotenv import load_dotenv from qa_engine.rag_pipeline import FilGoalRAG load_dotenv() logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") log = logging.getLogger("space") # ── Bootstrap pipeline at module load. HF Spaces calls this once at boot, then # keeps the worker warm — subsequent requests reuse the same loaded models. log.info("Loading FilGoalRAG...") bot = FilGoalRAG() bot.load() log.info("FilGoalRAG ready") INTENT_LABELS = { "match_result": "🏆 نتيجة مباراة", "lineup": "📋 تشكيلة", "player_info": "⚽ معلومات لاعب", "team_news": "📰 أخبار الفريق", "transfer_news": "🔄 ميركاتو", "general_football": "🌍 كرة القدم", } EXAMPLE_QUESTIONS = [ "ما نتيجة مباراة بيراميدز والجيش الملكي؟", "ما تشكيل الأهلي أمام المقاولون العرب؟", "آخر أخبار محمد صلاح في ليفربول", "آخر صفقات الزمالك في الميركاتو؟", "من سجل في مباراة الأهلي والاتحاد؟", "أخبار مران الزمالك قبل مباراة أبطال إفريقيا", "نتيجة مباراة برشلونة وريال مدريد؟", "هل عاد دي بروين لتدريبات نابولي؟", ] def format_sources(sources): if not sources: return "" lines = [] for i, s in enumerate(sources, 1): title = s.get("title", "")[:70] url = s.get("url", "") date = (s.get("pub_date", "") or "")[:10] league = s.get("league", "") or "" meta_parts = [p for p in (date, league if league != "other" else "") if p] meta = " · ".join(meta_parts) # HTML anchor (not markdown) — markdown link syntax inside an HTML #
isn't parsed by Gradio's renderer. body = f'{title}' if url else title lines.append(f'
{i}. {body}
{meta}
') return "\n".join(lines) def chat(query, history, show_sources, show_intent): if not (query or "").strip(): return history, history, "", "", "" start = time.monotonic() try: result = bot.answer(query.strip()) except Exception as e: log.error(f"RAG error: {e}", exc_info=True) history.append({"role": "user", "content": query}) history.append({"role": "assistant", "content": "❌ حدث خطأ، يرجى المحاولة مرة أخرى"}) return history, history, "", "", "" latency = int((time.monotonic() - start) * 1000) answer = result.get("answer", "لا توجد إجابة") intent = result.get("intent", "") sources = result.get("sources", []) intent_text = INTENT_LABELS.get(intent, intent) if show_intent else "" sources_md = format_sources(sources) if show_sources and sources else "" history.append({"role": "user", "content": query}) history.append({"role": "assistant", "content": answer}) return history, history, intent_text, sources_md, f"{latency}ms" def clear_chat(): return [], [], "", "", "" # ── CSS — FilGoal-style theme with light/dark toggle ───────────────────────── CSS = """ :root, html[data-theme="dark"] { /* Custom tokens used by our own elements (header, sources, etc.) */ --bg: #0f1115; --bg-elevated: #181b22; --panel: #1e2129; --panel-strong: #252834; --border: #2a2e3a; --text: #f1f3f7; --text-muted: #9aa0aa; --accent: #f5a623; --accent-hover: #ffb84d; --accent-soft: rgba(245, 166, 35, 0.12); --user-bubble: #2a2e3a; --bot-bubble: #1e2129; --shadow: 0 4px 12px rgba(0,0,0,0.3); /* Override Gradio's own theme tokens so its built-in components (Chatbot, gr.Group, gr.Accordion, etc.) follow the same palette. */ --body-background-fill: #0f1115 !important; --background-fill-primary: #181b22 !important; --background-fill-secondary: #1e2129 !important; --block-background-fill: #1e2129 !important; --panel-background-fill: #1e2129 !important; --input-background-fill: #181b22 !important; --block-border-color: #2a2e3a !important; --border-color-primary: #2a2e3a !important; --color-text-primary: #f1f3f7 !important; --body-text-color: #f1f3f7 !important; } html[data-theme="light"] { --bg: #f5f6fa; --bg-elevated: #ffffff; --panel: #ffffff; --panel-strong: #f0f1f5; --border: #e2e4ea; --text: #1a1d24; --text-muted: #5c6370; --accent: #e6951a; --accent-hover: #c97f12; --accent-soft: rgba(230, 149, 26, 0.10); --user-bubble: #f0f1f5; --bot-bubble: #ffffff; --shadow: 0 2px 8px rgba(0,0,0,0.06); --body-background-fill: #f5f6fa !important; --background-fill-primary: #ffffff !important; --background-fill-secondary: #f0f1f5 !important; --block-background-fill: #ffffff !important; --panel-background-fill: #ffffff !important; --input-background-fill: #ffffff !important; --block-border-color: #e2e4ea !important; --border-color-primary: #e2e4ea !important; --color-text-primary: #1a1d24 !important; --body-text-color: #1a1d24 !important; } /* Source link colour (anchor inside .source-card) — picks up the accent in either theme. */ .source-link, .source-link:visited { color: var(--accent); text-decoration: none; font-weight: 600; } .source-link:hover { text-decoration: underline; color: var(--accent-hover); } /* Force text colour on Gradio-managed nested elements (chatbot bubbles, accordion bodies, group panels, inputs, source cards). Without this, light theme leaves white-on-white text. The :not(a):not(button) keeps anchor accents and per-button colours intact. */ .gradio-container #chatbot *:not(a), .gradio-container .gr-form *:not(a):not(button), .gradio-container .gr-group *:not(a):not(button), .gradio-container .gr-accordion *:not(a):not(button), .gradio-container .source-card *:not(a), .gradio-container .prose *:not(a), .gradio-container textarea, .gradio-container input { color: var(--text) !important; } body, gradio-app, .gradio-container { background: var(--bg) !important; color: var(--text) !important; font-family: 'Cairo', 'Segoe UI', 'Noto Naskh Arabic', sans-serif !important; } .gradio-container * { border-color: var(--border) !important; } .rtl-text, .rtl-text * { direction: rtl; text-align: right; } .header-box { background: linear-gradient(135deg, var(--bg-elevated) 0%, var(--panel) 100%); border-radius: 14px; padding: 28px 24px; margin-bottom: 18px; text-align: center; border: 1px solid var(--border); box-shadow: var(--shadow); position: relative; overflow: hidden; } .header-box::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, transparent, var(--accent), transparent); } .header-box h1 { color: var(--text) !important; font-weight: 800 !important; } .header-box .accent { color: var(--accent); } .header-box p { color: var(--text-muted) !important; } #theme-toggle { background: var(--panel) !important; border: 1px solid var(--border) !important; color: var(--text) !important; font-weight: 600 !important; } #theme-toggle:hover { background: var(--accent-soft) !important; border-color: var(--accent) !important; } .intent-badge { display: inline-block; background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent); border-radius: 18px; padding: 4px 14px; font-size: 13px; font-weight: 600; direction: rtl; } .source-card { background: var(--panel-strong); border-right: 3px solid var(--accent); border-radius: 6px; padding: 10px 14px; margin: 6px 0; direction: rtl; color: var(--text); } #chatbot { background: var(--bg-elevated) !important; border: 1px solid var(--border) !important; border-radius: 12px !important; } #chatbot .message-wrap { direction: rtl; } #chatbot .message.user, #chatbot .user { background: var(--user-bubble) !important; color: var(--text) !important; border: 1px solid var(--border) !important; } #chatbot .message.bot, #chatbot .bot { background: var(--bot-bubble) !important; color: var(--text) !important; border: 1px solid var(--border) !important; } #query-input textarea { direction: rtl; text-align: right; font-size: 15px; background: var(--panel) !important; color: var(--text) !important; border: 1px solid var(--border) !important; } #query-input textarea:focus { border-color: var(--accent) !important; box-shadow: 0 0 0 2px var(--accent-soft) !important; } #submit-btn { background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%) !important; color: #0f1115 !important; border: none !important; font-weight: 700 !important; box-shadow: 0 2px 8px var(--accent-soft); } #submit-btn:hover { filter: brightness(1.1); transform: translateY(-1px); } .example-btn { direction: rtl; text-align: right; background: var(--panel) !important; color: var(--text) !important; border: 1px solid var(--border) !important; } .example-btn:hover { background: var(--accent-soft) !important; border-color: var(--accent) !important; } /* Gradio 4.x renders gr.Group/gr.Accordion/gr.Form with a mix of .block, .form, .gradio-* and svelte-hashed classes. List them all so the right sidebar follows the theme. */ .gradio-container .block, .gradio-container .form, .gradio-container .gr-form, .gradio-container .gr-group, .gradio-container .gradio-group, .gradio-container .gr-accordion, .gradio-container .gradio-accordion, .gradio-container .panel, .gradio-container .padded { background: var(--bg-elevated) !important; border-color: var(--border) !important; } label, .gradio-container label { color: var(--text-muted) !important; } """ with gr.Blocks( css=CSS, title="FilGoalRAG — مساعد كرة القدم", theme=gr.themes.Base( primary_hue="orange", neutral_hue="slate", font=gr.themes.GoogleFont("Cairo"), ), ) as demo: chat_history = gr.State([]) # Header + theme toggle with gr.Row(): with gr.Column(scale=10): gr.HTML( '
' '

FilGoalBot

' '

' 'مساعد ذكي لأخبار كرة القدم المصرية والعربية والعالمية' '

' ) with gr.Column(scale=1, min_width=120): theme_toggle = gr.Button("☀️ فاتح", elem_id="theme-toggle", size="sm") theme_toggle.click( fn=None, inputs=None, outputs=theme_toggle, js=""" () => { const cur = document.documentElement.getAttribute('data-theme') || 'dark'; const next = cur === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); return next === 'dark' ? '☀️ فاتح' : '🌙 مظلم'; } """, ) # Main layout with gr.Row(): with gr.Column(scale=3): chatbot = gr.Chatbot( elem_id="chatbot", label="المحادثة", height=480, show_label=False, rtl=True, type="messages", ) with gr.Row(): query_input = gr.Textbox( elem_id="query-input", placeholder="اكتب سؤالك هنا... مثال: من سجل هدف الأهلي؟", show_label=False, scale=5, lines=1, max_lines=3, rtl=True, ) submit_btn = gr.Button("إرسال ➤", elem_id="submit-btn", scale=1, variant="primary") with gr.Row(): clear_btn = gr.Button("🗑️ مسح المحادثة", size="sm", variant="secondary") with gr.Column(scale=1): with gr.Group(): gr.Markdown("### معلومات الإجابة", elem_classes=["rtl-text"]) intent_display = gr.Markdown(elem_classes=["rtl-text"]) latency_display = gr.Markdown(elem_classes=["rtl-text"]) with gr.Accordion("📰 المصادر", open=True): sources_display = gr.Markdown(elem_classes=["rtl-text"]) with gr.Accordion("⚙️ الإعدادات", open=False): show_sources = gr.Checkbox(label="عرض المصادر", value=True) show_intent = gr.Checkbox(label="عرض نوع السؤال", value=True) gr.Markdown("### 💡 أسئلة مقترحة", elem_classes=["rtl-text"]) with gr.Row(): for q in EXAMPLE_QUESTIONS[:4]: gr.Button(q, size="sm", elem_classes=["example-btn"]).click( fn=lambda x=q: x, outputs=query_input, ) with gr.Row(): for q in EXAMPLE_QUESTIONS[4:]: gr.Button(q, size="sm", elem_classes=["example-btn"]).click( fn=lambda x=q: x, outputs=query_input, ) gr.HTML( '
' 'FilGoalBot · Hugging Face Space demo' '
' ) submit_inputs = [query_input, chat_history, show_sources, show_intent] submit_outputs = [chatbot, chat_history, intent_display, sources_display, latency_display] submit_btn.click(fn=chat, inputs=submit_inputs, outputs=submit_outputs)\ .then(fn=lambda: "", outputs=query_input) query_input.submit(fn=chat, inputs=submit_inputs, outputs=submit_outputs)\ .then(fn=lambda: "", outputs=query_input) clear_btn.click(fn=clear_chat, outputs=submit_outputs) if __name__ == "__main__": demo.launch()