Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,46 +1,226 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
| 2 |
import requests
|
| 3 |
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
try:
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
except Exception as e:
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
else:
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
with gr.Row():
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
demo.launch()
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
import json
|
| 5 |
import requests
|
| 6 |
import pandas as pd
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from html import escape
|
| 9 |
+
|
| 10 |
+
# ---------- Configuration / Environment ----------
|
| 11 |
+
HF_API_TOKEN = os.getenv("HF_API_TOKEN") # set this in Space Secrets
|
| 12 |
+
MODEL_ID = os.getenv("MODEL_ID", "meta-llama/Meta-Llama-3-8B-Instruct")
|
| 13 |
+
JOB_SHEET_CSV = os.getenv("JOB_SHEET_CSV", "") # optional: published Google Sheet CSV URL
|
| 14 |
+
|
| 15 |
+
HF_API_URL = f"https://api-inference.huggingface.co/models/{MODEL_ID}"
|
| 16 |
+
HEADERS = {"Authorization": f"Bearer {HF_API_TOKEN}"} if HF_API_TOKEN else {}
|
| 17 |
+
|
| 18 |
+
# ---------- Lightweight local fallback knowledge (prevents 'no answer') ----------
|
| 19 |
+
FALLBACK_KB = {
|
| 20 |
+
"gravity_en": "Gravity is a force that pulls objects toward each other. On Earth it makes things fall down and keeps the Moon orbiting Earth. In simple terms: mass attracts mass.",
|
| 21 |
+
"gravity_ur": "کشش ثقل ایک ایسی قوت ہے جو چیزوں کو ایک دوسرے کی طرف کھینچتی ہے۔ زمین پر یہی وجہ ہے کہ چیزیں نیچے گرتی ہیں اور چاند زمین کے گرد گھومتا ہے۔",
|
| 22 |
+
"hello_en": "Hello! I'm StudyMate AI — ask me about subjects, jobs, or study tips. Try: 'Explain gravity in Urdu' or 'Job ideas for graphic designer'.",
|
| 23 |
+
"hello_ur": "سلام! میں StudyMate AI ہوں — مجھ سے مضامین، نوکریاں، یا مطالعے کے مشورے پوچھیں۔"
|
| 24 |
+
}
|
| 25 |
|
| 26 |
+
# ---------- Helper utilities ----------
|
| 27 |
+
def detect_arabic_script(text: str) -> bool:
|
| 28 |
+
"""Rudimentary detection for Urdu/Sindhi/Arabic script presence."""
|
| 29 |
+
for ch in text:
|
| 30 |
+
if '\u0600' <= ch <= '\u06FF' or '\u0750' <= ch <= '\u077F':
|
| 31 |
+
return True
|
| 32 |
+
return False
|
| 33 |
|
| 34 |
+
def call_hf_inference(prompt: str, max_tokens: int = 300, retries: int = 2, wait: float = 1.0):
|
| 35 |
+
"""Call Hugging Face Inference API with retries and graceful error handling."""
|
| 36 |
+
if not HF_API_TOKEN:
|
| 37 |
+
return {"error": "No HF token configured."}
|
| 38 |
+
|
| 39 |
+
payload = {
|
| 40 |
+
"inputs": prompt,
|
| 41 |
+
"parameters": {"max_new_tokens": max_tokens, "temperature": 0.3, "do_sample": True},
|
| 42 |
+
}
|
| 43 |
+
last_err = None
|
| 44 |
+
for attempt in range(retries + 1):
|
| 45 |
try:
|
| 46 |
+
r = requests.post(HF_API_URL, headers=HEADERS, json=payload, timeout=30)
|
| 47 |
+
r.raise_for_status()
|
| 48 |
+
out = r.json()
|
| 49 |
+
# Inference API returns list or dict depending on model
|
| 50 |
+
if isinstance(out, list) and "generated_text" in out[0]:
|
| 51 |
+
return {"text": out[0]["generated_text"]}
|
| 52 |
+
if isinstance(out, dict) and "generated_text" in out:
|
| 53 |
+
return {"text": out["generated_text"]}
|
| 54 |
+
# some models return other structures
|
| 55 |
+
if isinstance(out, dict) and "error" in out:
|
| 56 |
+
last_err = out.get("error")
|
| 57 |
+
else:
|
| 58 |
+
# try to extract text heuristically
|
| 59 |
+
txt = json.dumps(out)[:2000]
|
| 60 |
+
return {"text": txt}
|
| 61 |
except Exception as e:
|
| 62 |
+
last_err = str(e)
|
| 63 |
+
time.sleep(wait)
|
| 64 |
+
return {"error": f"Inference failed after retries. Last error: {last_err}"}
|
| 65 |
+
|
| 66 |
+
def safe_answer(user_text: str, mode: str):
|
| 67 |
+
"""Main orchestrator: returns string to show user (tries inference, then fallback)."""
|
| 68 |
+
is_arabic = detect_arabic_script(user_text)
|
| 69 |
+
language_hint = "Urdu" if is_arabic else "English"
|
| 70 |
+
system_intro = (
|
| 71 |
+
f"You are StudyMate AI — a polite, highly helpful tutor and local helper. "
|
| 72 |
+
f"Answer the user's query clearly and concisely in {language_hint}. "
|
| 73 |
+
"If user asks for local resources or jobs, give practical steps and links if known. "
|
| 74 |
+
"Keep answers short (<= 250 words) and include 'Confidence: high/medium/low' at the end."
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Build the prompt
|
| 78 |
+
prompt = f"{system_intro}\n\nUser: {user_text}\n\nAssistant:"
|
| 79 |
+
|
| 80 |
+
# Try model
|
| 81 |
+
model_resp = call_hf_inference(prompt)
|
| 82 |
+
if model_resp.get("text"):
|
| 83 |
+
out = model_resp["text"]
|
| 84 |
+
# Clean up common repeats of 'Assistant:' from returned text
|
| 85 |
+
return out.strip()
|
| 86 |
else:
|
| 87 |
+
# fallback: try to map a few common Qs to local KB so user isn't disappointed
|
| 88 |
+
if "gravity" in user_text.lower() or "کشش" in user_text:
|
| 89 |
+
return FALLBACK_KB["gravity_ur"] if is_arabic else FALLBACK_KB["gravity_en"] \
|
| 90 |
+
+ "\n\nConfidence: high\nNext step: Ask for an example or practice question."
|
| 91 |
+
if any(greet in user_text.lower() for greet in ["hi", "hello", "salam", "سلام"]):
|
| 92 |
+
return FALLBACK_KB["hello_ur"] if is_arabic else FALLBACK_KB["hello_en"]
|
| 93 |
+
# final fallback message
|
| 94 |
+
err = model_resp.get("error", "Model unavailable.")
|
| 95 |
+
return (
|
| 96 |
+
f"Sorry — I couldn't generate a full answer right now. "
|
| 97 |
+
f"I can try a short summary: {FALLBACK_KB.get('hello_ur') if is_arabic else FALLBACK_KB.get('hello_en')}\n\n"
|
| 98 |
+
f"Error: {escape(err)}\n\nPlease try again or rephrase. Confidence: low."
|
| 99 |
+
)
|
| 100 |
|
| 101 |
+
# ---------- Job finder using published Google Sheet CSV ----------
|
| 102 |
+
def fetch_jobs_from_csv(csv_url: str):
|
| 103 |
+
"""Return a pandas DataFrame parsed from a public CSV URL or empty DataFrame on failure."""
|
| 104 |
+
if not csv_url:
|
| 105 |
+
return pd.DataFrame()
|
| 106 |
+
try:
|
| 107 |
+
df = pd.read_csv(csv_url)
|
| 108 |
+
return df
|
| 109 |
+
except Exception as e:
|
| 110 |
+
return pd.DataFrame()
|
| 111 |
|
| 112 |
+
def find_jobs_by_skill(skill_query: str, csv_url: str, limit: int = 6):
|
| 113 |
+
"""Very simple skill matching on job listings CSV (case-insensitive match on text columns)."""
|
| 114 |
+
df = fetch_jobs_from_csv(csv_url)
|
| 115 |
+
if df.empty:
|
| 116 |
+
return []
|
| 117 |
+
skill = skill_query.lower()
|
| 118 |
+
matches = []
|
| 119 |
+
# Search across all text columns for the skill word
|
| 120 |
+
text_cols = [c for c in df.columns if df[c].dtype == object]
|
| 121 |
+
for _, row in df.sample(min(len(df), 200)).iterrows():
|
| 122 |
+
row_text = " ".join(str(row[c]) for c in text_cols).lower()
|
| 123 |
+
if skill in row_text:
|
| 124 |
+
# Build a short job card
|
| 125 |
+
matches.append({
|
| 126 |
+
"title": str(row.get("Job Title", row.get("title", "Job"))),
|
| 127 |
+
"company": str(row.get("Company", row.get("company", ""))),
|
| 128 |
+
"location": str(row.get("Location", row.get("location", ""))),
|
| 129 |
+
"link": str(row.get("Apply Link", row.get("link", ""))),
|
| 130 |
+
"snippet": (str(row.get("Description", ""))[:180] + "...") if row.get("Description") else ""
|
| 131 |
+
})
|
| 132 |
+
if len(matches) >= limit:
|
| 133 |
+
break
|
| 134 |
+
return matches
|
| 135 |
+
|
| 136 |
+
# ---------- Gradio UI with Comet-like theme ----------
|
| 137 |
+
COMET_CSS = r"""
|
| 138 |
+
/* Comet-like dark glass theme */
|
| 139 |
+
body { background: radial-gradient(circle at 15% 20%, #0b1630 0%, #071028 20%, #000814 100%), url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=2000&auto=format&fit=crop'); background-size: cover; color: #e6eef8; }
|
| 140 |
+
.gradio-container { background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); border-radius: 18px; padding: 22px; backdrop-filter: blur(12px) saturate(140%); box-shadow: 0 10px 40px rgba(2,6,23,0.6); }
|
| 141 |
+
#title { color: #eaf4ff; font-weight:700; }
|
| 142 |
+
h2 { color: #dff0ff; }
|
| 143 |
+
.gr-box { border-radius: 12px!important; }
|
| 144 |
+
input, textarea { background: rgba(255,255,255,0.03) !important; color: #e6eef8 !important; border: 1px solid rgba(255,255,255,0.06) !important; }
|
| 145 |
+
button { background: linear-gradient(90deg,#6ad1ff,#7b6bff) !important; color: #041028 !important; border: none !important; box-shadow: 0 6px 18px rgba(75,60,255,0.14); }
|
| 146 |
+
footer { display:none !important; }
|
| 147 |
+
|
| 148 |
+
/* comet streak */
|
| 149 |
+
#comet {
|
| 150 |
+
position: absolute;
|
| 151 |
+
right: -20%;
|
| 152 |
+
top: -10%;
|
| 153 |
+
width: 60%;
|
| 154 |
+
height: 400px;
|
| 155 |
+
background: linear-gradient(120deg, rgba(255,255,255,0.03), rgba(120,80,255,0.1));
|
| 156 |
+
transform: rotate(-25deg);
|
| 157 |
+
filter: blur(30px);
|
| 158 |
+
pointer-events: none;
|
| 159 |
+
border-radius: 40%;
|
| 160 |
+
opacity: 0.6;
|
| 161 |
+
}
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
def format_jobs_md(jobs):
|
| 165 |
+
if not jobs:
|
| 166 |
+
return "No community job posts found. Share a job using the Google Form (link in README)."
|
| 167 |
+
md = ""
|
| 168 |
+
for j in jobs:
|
| 169 |
+
md += f"**{escape(j['title'])}** — {escape(j['company'])} — {escape(j['location'])}\n\n"
|
| 170 |
+
if j.get("snippet"):
|
| 171 |
+
md += f"{escape(j['snippet'])}\n\n"
|
| 172 |
+
if j.get("link"):
|
| 173 |
+
md += f"[Apply link]({escape(j['link'])})\n\n---\n"
|
| 174 |
+
return md
|
| 175 |
+
|
| 176 |
+
with gr.Blocks(css=COMET_CSS) as demo:
|
| 177 |
+
gr.HTML("<div id='comet'></div>")
|
| 178 |
+
gr.Markdown("<h2 id='title'>🚀 StudyMate AI — Comet Edition</h2>"
|
| 179 |
+
"<p>Learn, find jobs, and get local help in English, Urdu & Sindhi. Public & free.</p>")
|
| 180 |
with gr.Row():
|
| 181 |
+
with gr.Column(scale=3):
|
| 182 |
+
mode = gr.Radio(choices=["Study Help", "Job Finder", "General Chat"], value="Study Help", label="Mode")
|
| 183 |
+
user_input = gr.Textbox(placeholder="Ask in English or Urdu — e.g., 'کشش ثقل آسان الفاظ میں بتائیں' or 'job ideas for designer in karachi'", label="Your question", lines=3)
|
| 184 |
+
send = gr.Button("Ask StudyMate ✨")
|
| 185 |
+
output_chat = gr.Markdown("", label="Answer")
|
| 186 |
+
with gr.Column(scale=1):
|
| 187 |
+
gr.Markdown("### 🔎 Quick Actions")
|
| 188 |
+
daily_tip_btn = gr.Button("Daily Tip")
|
| 189 |
+
last_q_btn = gr.Button("Sample Qs")
|
| 190 |
+
gr.Markdown("### 📢 Community Jobs")
|
| 191 |
+
jobs_md = gr.Markdown("Jobs will appear here when you set JOB_SHEET_CSV in Space Secrets.")
|
| 192 |
+
# Event handlers
|
| 193 |
+
def on_ask(user_text, mode_sel):
|
| 194 |
+
user_text = (user_text or "").strip()
|
| 195 |
+
if not user_text:
|
| 196 |
+
return "Please type a question first."
|
| 197 |
+
if mode_sel == "Job Finder":
|
| 198 |
+
# use community CSV URL first
|
| 199 |
+
jobs = find_jobs_by_skill(user_text, JOB_SHEET_CSV)
|
| 200 |
+
if jobs:
|
| 201 |
+
return format_jobs_md(jobs)
|
| 202 |
+
# else fallback: ask model for job ideas
|
| 203 |
+
prompt = f"You are a career advisor. Suggest 6 practical job or gig ideas for someone with skill: {user_text}. Give short bullets and local ideas."
|
| 204 |
+
ans = safe_answer(prompt, mode_sel)
|
| 205 |
+
return ans
|
| 206 |
+
else:
|
| 207 |
+
ans = safe_answer(user_text, mode_sel)
|
| 208 |
+
return ans
|
| 209 |
+
|
| 210 |
+
def on_daily_tip():
|
| 211 |
+
tip_prompt = "Give one short practical study or career tip in 1-2 sentences and include a motivation line."
|
| 212 |
+
return safe_answer(tip_prompt, "General Chat")
|
| 213 |
+
|
| 214 |
+
def on_sample_qs():
|
| 215 |
+
return ("Try these:\n\n"
|
| 216 |
+
"- Explain Newton's 2nd law in simple Urdu.\n"
|
| 217 |
+
"- Give 5 flashcards about basic calculus.\n"
|
| 218 |
+
"- Suggest 3 low-cost freelance gigs for a graphic designer in Pakistan.")
|
| 219 |
+
|
| 220 |
+
send.click(on_ask, [user_input, mode], output_chat)
|
| 221 |
+
daily_tip_btn.click(lambda: on_daily_tip(), None, output_chat)
|
| 222 |
+
last_q_btn.click(lambda: on_sample_qs(), None, output_chat)
|
| 223 |
+
|
| 224 |
+
gr.Markdown("**Notes:** This app uses a remote model via Hugging Face Inference API. Add your `HF_API_TOKEN` as a Space Secret. If the model is unavailable, StudyMate will show helpful fallback messages so users are not left hanging.")
|
| 225 |
|
| 226 |
demo.launch()
|