Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| # <div> isn't parsed by Gradio's renderer. | |
| body = f'<a href="{url}" target="_blank" rel="noopener" class="source-link">{title}</a>' if url else title | |
| lines.append(f'<div class="source-card"><b>{i}.</b> {body}<br><i>{meta}</i></div>') | |
| 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( | |
| '<div class="header-box">' | |
| '<h1 style="margin:0; font-size:30px;">⚽ <span class="accent">FilGoal</span>Bot</h1>' | |
| '<p style="margin:8px 0 0; font-size:15px; direction:rtl;">' | |
| 'مساعد ذكي لأخبار كرة القدم المصرية والعربية والعالمية' | |
| '</p></div>' | |
| ) | |
| 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( | |
| '<div style="text-align:center; padding:16px 0 4px; color:var(--text-muted); font-size:12px; direction:rtl;">' | |
| '<span style="color:var(--accent); font-weight:700;">FilGoal</span>Bot · Hugging Face Space demo' | |
| '</div>' | |
| ) | |
| 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() | |