Spaces:
Sleeping
Sleeping
File size: 12,521 Bytes
d9bd342 5a45a46 d9bd342 5a45a46 d9bd342 5a45a46 d9bd342 5a45a46 b911d8c 5a45a46 d9bd342 5a45a46 425af9c 5a45a46 425af9c 5a45a46 425af9c 5a45a46 425af9c 5a45a46 425af9c 5a45a46 425af9c 5a45a46 425af9c 5a45a46 d9bd342 5a45a46 d9bd342 0de31bd d9bd342 99c7e89 d9bd342 6b842bc d9bd342 99c7e89 d9bd342 99c7e89 d9bd342 20a6be7 d9bd342 99c7e89 d9bd342 20a6be7 99c7e89 d9bd342 470975e d9bd342 5a45a46 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# Access site: https://binkhoale1812-tutorbot.hf.space
import os
import time
import uvicorn
import tempfile
import psutil
import logging
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from google import genai
from gradio_client import Client, handle_file
# ———————— Logging —————————
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s — %(name)s — %(levelname)s — %(message)s", force=True)
logger = logging.getLogger("tutor-chatbot")
logger.setLevel(logging.DEBUG)
logger.info("🚀 Starting Tutor Chatbot API...")
# —————— Environment ———————
gemini_flash_api_key = os.getenv("FlashAPI")
if not gemini_flash_api_key:
raise ValueError("❌ Missing Gemini Flash API key!")
# —————— System Check ——————
def check_system_resources():
memory = psutil.virtual_memory()
cpu = psutil.cpu_percent(interval=1)
disk = psutil.disk_usage("/")
logger.info(f"🔍 RAM: {memory.percent}%, CPU: {cpu}%, Disk: {disk.percent}%")
check_system_resources()
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["TOKENIZERS_PARALLELISM"] = "false"
# —————— FastAPI Setup —————
app = FastAPI(title="Tutor Chatbot API")
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://localhost:3000",
"https://ai-tutor-beta-topaz.vercel.app",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# —————— Gemini 2.5 API Call ——————
def gemini_flash_completion(prompt, model="gemini-2.5-flash-preview-04-17", temperature=0.7):
client = genai.Client(api_key=gemini_flash_api_key)
try:
response = client.models.generate_content(model=model, contents=prompt)
return response.text
except Exception as e:
logger.error(f"❌ Gemini error: {e}")
return "Error generating response from Gemini."
# —— Qwen 2.5 VL Client Setup —————
logger.info("[Qwen] Using remote API via Gradio Client")
# Read and reasoning on image data sending over
def qwen_image_summary(image_file: UploadFile, subject: str, level: str) -> str:
from gradio_client import Client, handle_file
import tempfile, os
from fastapi import HTTPException
# Not accepted format
if image_file.content_type not in {"image/png", "image/jpeg", "image/jpg"}:
raise HTTPException(415, "Only PNG or JPEG images are supported")
# Write and save image sending over on cache
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
tmp.write(image_file.file.read())
tmp_path = tmp.name
# Engineered prompting
instruction = f"""
You are an academic tutor.
The student has submitted an image that may contain multiple exam-style questions or study material. Your task is to:
1. Carefully extract **each individual question** from the image (if visible), even if they are numbered (e.g., 1., 2., 3.).
2. If any question contains **multiple-choice options** (e.g., a), b), c), d)), include them exactly as shown.
3. Preserve the original structure and wording as much as possible — DO NOT paraphrase.
4. Do not include commentary, analysis, or summaries — just return the extracted question(s) cleanly.
Format your output as:
1. Question 1 text
a) option A
b) option B
c) option C
d) option D
2. Question 2 text
a) ... (if applicable)
Only include what appears in the image. Be accurate and neat.
"""
# ——— 1️⃣ Primary: 32B Model (Qwen/Qwen2.5-VL-32B-Instruct) ———
try:
logger.info("[Qwen32B] Using /predict ...")
client32 = Client("Qwen/Qwen2.5-VL-32B-Instruct")
# Payload handler
_chatbot_payload = [
(None, instruction.strip()),
(None, {"file": tmp_path})
]
# Call client
result = client32.predict(_chatbot=_chatbot_payload, api_name="/predict")
# Clean result
if isinstance(result, (list, tuple)) and result:
assistant_reply = (result[0] or "").strip()
else:
assistant_reply = str(result).strip()
# Primary success
if assistant_reply:
logger.info("[Qwen32B] ✅ Successfully transcribed.")
os.remove(tmp_path)
return assistant_reply
# Empty return
raise ValueError("Empty result from 32B")
# Fail on primary
except Exception as e_32b:
logger.warning(f"[Qwen32B] ❌ Failed: {e_32b} — falling back to Qwen 7B")
# ——— 2️⃣ Fallback: 7B Model (prithivMLmods/Qwen2.5-VL) ———
try:
logger.info("[Qwen7B] Using /generate_image fallback ...")
client7 = Client("prithivMLmods/Qwen2.5-VL")
# Fallback client calling
result = client7.predict(
model_name="Qwen2.5-VL-7B-Instruct",
text=instruction.strip(),
image=handle_file(tmp_path),
max_new_tokens=1024,
temperature=0.6,
top_p=0.9,
top_k=50,
repetition_penalty=1.2,
api_name="/generate_image"
)
# Clean result
result = (result or "").strip()
os.remove(tmp_path)
# Extract fallback result
if result:
logger.info("[Qwen7B] ✅ Fallback succeeded.")
return result
# Empty return
raise ValueError("Empty result from 7B fallback")
# Fail on both
except Exception as e_7b:
logger.error(f"[Qwen7B] ❌ Fallback also failed: {e_7b}")
raise HTTPException(500, "❌ Both Qwen image models failed to process the image.")
# ————— Unified Chat Endpoint —————
@app.post("/chat")
async def chat_endpoint(
query: str = Form(""),
subject: str = Form("general"),
level: str = Form("secondary"),
lang: str = Form("EN"),
image: UploadFile = File(None)
):
start_time = time.time()
image_context = ""
# Step 1: If image is present, get transcription from Qwen
if image:
logger.info("[Router] 📸 Image uploaded — using Qwen2.5-VL for transcription")
try:
image_context = qwen_image_summary(image, subject, level)
except HTTPException as e:
return JSONResponse(status_code=e.status_code, content={"response": e.detail})
# Step 2: Build prompt for Gemini depending on presence of text and/or image
if query and image_context:
# Case: image + query
prompt = f"""
You are an academic tutor specialized in **{subject}** at **{level}** level.
Below is an image submitted by a student and transcribed by a vision model:
--- BEGIN IMAGE CONTEXT ---
{image_context}
--- END IMAGE CONTEXT ---
The student asked the following:
**Question:** {query}
Respond appropriately using markdown:
- **Bold** key ideas
- *Italic* for reasoning
- Provide examples if useful
**Response Language:** {lang}
"""
elif image_context and not query:
# Case: image only — auto-answer based on content
prompt = f"""
You are an academic tutor specialized in **{subject}** at **{level}** level.
A student submitted an image with no question. Below is the vision model’s transcription:
--- BEGIN IMAGE CONTENT ---
{image_context}
--- END IMAGE CONTENT ---
Based on this image, explain its key ideas and help the student understand it.
Assume it's part of their study material.
Respond using markdown:
- **Bold** key terms
- *Italic* for explanations
- Give brief insights or examples
**Response Language:** {lang}
"""
elif query and not image_context:
# Case: text only
prompt = f"""
You are an academic tutor specialized in **{subject}** at **{level}** level.
**Question:** {query}
Answer clearly using markdown:
- **Bold** key terms
- *Italic* for explanations
- Include examples if helpful
**Response Language:** {lang}
"""
else:
# Nothing was sent
return JSONResponse(content={"response": "❌ Please provide either a query, an image, or both."})
# Step 3: Call Gemini
response_text = gemini_flash_completion(prompt)
end_time = time.time()
response_text += f"\n\n*(Response time: {end_time - start_time:.2f} seconds)*"
return JSONResponse(content={"response": response_text})
# ————— Clsr Pydantic Schema —————
from pydantic import BaseModel, Field, validator
from typing import Optional, List, Literal
# Dynamic cls
class StudyPreferences(BaseModel):
daysPerWeek: int = Field(..., ge=1, le=7)
hoursPerSession: float = Field(..., ge=0.5, le=4)
numberWeekTotal: float = Field(..., ge=1, le=52)
learningStyle: Literal["step-by-step", "conceptual", "visual"]
# Dynamic cls
class ClassroomRequest(BaseModel):
id: str
name: str = Field(..., min_length=2)
role: Literal["tutor", "student"]
subject: str
gradeLevel: str
notice: Optional[str] = None
textbookUrl: Optional[str] = None
syllabusUrl: Optional[str] = None
studyPreferences: StudyPreferences
# —————— Time table creator ——————
import json
from fastapi import Body
@app.post("/api/generate-timetable")
async def create_classroom(payload: ClassroomRequest = Body(...)):
"""
Generate a detailed study timetable based on classroom parameters.
"""
# ---------- Build prompt for Gemini 2.5 ----------
prefs = payload.studyPreferences
prompt = f"""
You are an expert academic coordinator.
Create a **{prefs.numberWeekTotal}-week study timetable** for a classroom with the following settings:
- Subject: {payload.subject}
- Grade level: {payload.gradeLevel}
- Instruction (Optional): {payload.notice}
- Study days per week: {prefs.daysPerWeek}
- Hours per session: {prefs.hoursPerSession}
- Preferred learning style: {prefs.learningStyle}
- Role perspective: {payload.role}
{"- Textbook URL: " + payload.textbookUrl if payload.textbookUrl else "Not Available"}
{"- Syllabus URL: " + payload.syllabusUrl if payload.syllabusUrl else "Not Available"}
Requirements:
1. Divide each week into exactly {prefs.daysPerWeek} sessions (label Day 1 … Day {prefs.daysPerWeek}).
2. For **each session**, return:
- `week` (1-{prefs.numberWeekTotal})
- `day` (1-{prefs.daysPerWeek})
- `durationHours` (fixed: {prefs.hoursPerSession})
- `topic` (max 15 words)
- `activities` (array of 2-3 bullet strings)
- `materials` (array of links/titles; include textbook chapters if URL given, else suggesting external textbook/document for referencing)
- `homework` (concise task ≤ 50 words)
3. **Output pure JSON only** using the schema:
```json
{{
"classroom_id": "<same id as request>",
"timetable": [{{session objects as listed}}]
}}
Do not wrap JSON in markdown fences or commentary.
"""
raw = gemini_flash_completion(prompt).strip()
# ---------- Attempt to parse JSON ----------
try:
timetable_json = json.loads(raw)
except json.JSONDecodeError:
logger.warning("Gemini returned invalid JSON; sending raw text.")
return JSONResponse(content={"classroom_id": payload.id, "timetable_raw": raw})
# Ensure id is echoed (fallback if model forgot)
timetable_json["classroom_id"] = payload.id
return JSONResponse(content=timetable_json)
# —————— Launch Server ———————
if __name__ == "__main__":
logger.info("✅ Launching FastAPI server...")
try:
uvicorn.run(app, host="0.0.0.0", port=7860, log_level="debug")
except Exception as e:
logger.error(f"❌ Server startup failed: {e}")
exit(1)
|