Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import sqlite3 | |
| import datetime | |
| import requests | |
| import random | |
| import traceback | |
| from flask import Flask, render_template, request, jsonify, send_from_directory | |
| from werkzeug.utils import secure_filename | |
| app = Flask(__name__) | |
| app.secret_key = os.urandom(24) | |
| # Configuration | |
| API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi") | |
| BASE_URL = "https://api.siliconflow.cn/v1/chat/completions" | |
| # Use absolute path for DB and Uploads to avoid CWD issues | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| DB_PATH = os.path.join(BASE_DIR, 'edu_spark.db') | |
| UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads') | |
| app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER | |
| app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB Max Limit | |
| # Ensure instance folders exist | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| # Error Handlers | |
| def page_not_found(e): | |
| return render_template('index.html'), 404 | |
| def internal_server_error(e): | |
| # Log the full traceback for debugging | |
| error_trace = traceback.format_exc() | |
| print(f"ERROR: Internal Server Error:\n{error_trace}") | |
| # Return the actual error message in details if possible, otherwise generic | |
| return jsonify(error="Internal Server Error", details=error_trace if error_trace else str(e)), 500 | |
| def request_entity_too_large(e): | |
| return jsonify(error="File too large (Max 16MB)"), 413 | |
| # Database Helper with Retry | |
| def get_db_connection(max_retries=3): | |
| import time | |
| for i in range(max_retries): | |
| try: | |
| conn = sqlite3.connect(DB_PATH) | |
| return conn | |
| except sqlite3.OperationalError as e: | |
| if "locked" in str(e): | |
| time.sleep(0.1 * (i + 1)) | |
| else: | |
| raise e | |
| raise sqlite3.OperationalError("Database locked after retries") | |
| # Database Initialization | |
| def init_db(): | |
| try: | |
| # Use get_db_connection logic manually here or just connect | |
| with sqlite3.connect(DB_PATH) as conn: | |
| c = conn.cursor() | |
| c.execute('''CREATE TABLE IF NOT EXISTS courses ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| title TEXT NOT NULL, | |
| topic TEXT, | |
| level TEXT, | |
| content JSON, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| )''') | |
| c.execute('''CREATE TABLE IF NOT EXISTS users ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| name TEXT, | |
| skills JSON, | |
| goals JSON | |
| )''') | |
| # Check if empty and add default data | |
| c.execute("SELECT count(*) FROM courses") | |
| if c.fetchone()[0] == 0: | |
| default_courses = [ | |
| ("Python 基础入门", "Python", "Beginner", json.dumps({ | |
| "title": "Python 基础入门", | |
| "description": "零基础学习 Python 编程语言。", | |
| "modules": [ | |
| {"title": "环境搭建", "topics": ["安装 Python", "IDE 配置"], "duration": "30分钟"}, | |
| {"title": "基础语法", "topics": ["变量", "数据类型", "控制流"], "duration": "2小时"} | |
| ], | |
| "quiz": [] | |
| })), | |
| ("数据结构与算法", "Computer Science", "Intermediate", json.dumps({ | |
| "title": "数据结构与算法", | |
| "description": "掌握核心编程算法。", | |
| "modules": [ | |
| {"title": "线性表", "topics": ["数组", "链表"], "duration": "3小时"}, | |
| {"title": "树与图", "topics": ["二叉树", "DFS/BFS"], "duration": "5小时"} | |
| ], | |
| "quiz": [] | |
| })) | |
| ] | |
| c.executemany("INSERT INTO courses (title, topic, level, content) VALUES (?, ?, ?, ?)", default_courses) | |
| print("Database initialized with default data.") | |
| except Exception as e: | |
| print(f"Database Initialization Error: {e}") | |
| traceback.print_exc() | |
| init_db() | |
| # Helper: SiliconFlow API Call | |
| def call_ai(messages, model="Qwen/Qwen2.5-7B-Instruct"): | |
| headers = { | |
| "Authorization": f"Bearer {API_KEY}", | |
| "Content-Type": "application/json" | |
| } | |
| payload = { | |
| "model": model, | |
| "messages": messages, | |
| "temperature": 0.7, | |
| "max_tokens": 2048 | |
| } | |
| try: | |
| response = requests.post(BASE_URL, headers=headers, json=payload, timeout=10) | |
| response.raise_for_status() | |
| return response.json()['choices'][0]['message']['content'] | |
| except Exception as e: | |
| print(f"AI Error: {e}") | |
| # Mock Fallback | |
| return None | |
| def index(): | |
| return render_template('index.html') | |
| def get_stats(): | |
| # Mock data for Radar Chart (User Skills) & Line Chart (Activity) | |
| return jsonify({ | |
| "skills": [ | |
| {"name": "Python", "max": 100, "value": 85}, | |
| {"name": "Math", "max": 100, "value": 60}, | |
| {"name": "Logic", "max": 100, "value": 90}, | |
| {"name": "AI Theory", "max": 100, "value": 40}, | |
| {"name": "Data Analysis", "max": 100, "value": 75}, | |
| {"name": "Creativity", "max": 100, "value": 70} | |
| ], | |
| "activity": { | |
| "labels": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"], | |
| "data": [2, 4, 1.5, 5, 3.5, 6, 4.5] | |
| }, | |
| "learning_path": [ | |
| {"stage": "基础", "status": "completed"}, | |
| {"stage": "进阶", "status": "in_progress"}, | |
| {"stage": "实战", "status": "pending"} | |
| ] | |
| }) | |
| def upload_file(): | |
| if 'file' not in request.files: | |
| return jsonify({"error": "No file part"}), 400 | |
| file = request.files['file'] | |
| if file.filename == '': | |
| return jsonify({"error": "No selected file"}), 400 | |
| if file: | |
| filename = secure_filename(file.filename) | |
| # Mock processing | |
| file_size = len(file.read()) | |
| # file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) # Optional: Actually save | |
| return jsonify({ | |
| "message": f"文件 '{filename}' 上传成功!", | |
| "details": f"大小: {file_size} bytes. 系统已自动解析并存入知识库。" | |
| }) | |
| return jsonify({"error": "Upload failed"}), 500 | |
| def chat(): | |
| try: | |
| data = request.json | |
| if not data: | |
| return jsonify({"error": "Invalid JSON or Content-Type"}), 400 | |
| user_msg = data.get('message', '') | |
| history = data.get('history', []) | |
| # Context Construction | |
| messages = [ | |
| {"role": "system", "content": "你是 Edu Spark Agent,一个专业的个性化教育 AI 助手。你的目标是帮助用户制定学习计划、生成课程内容和解答问题。请用中文回答。如果用户要求生成课程,请按结构化 Markdown 格式输出。"} | |
| ] | |
| for msg in history: | |
| messages.append({"role": msg['role'], "content": msg['content']}) | |
| messages.append({"role": "user", "content": user_msg}) | |
| ai_reply = call_ai(messages) | |
| if ai_reply: | |
| return jsonify({"response": ai_reply}) | |
| else: | |
| # Mock Response if API fails | |
| return jsonify({"response": "**AI 服务暂时不可用 (Mock Mode)**\n\n但我可以为你提供一些建议:\n1. 尝试制定明确的学习目标。\n2. 分解任务为小块。\n3. 坚持每天练习。\n\n(请检查 API Key 或网络连接)"}) | |
| except Exception as e: | |
| print(f"Chat Error: {e}") | |
| traceback.print_exc() | |
| return jsonify(error="Chat Processing Error", details=str(e)), 500 | |
| def generate_course(): | |
| try: | |
| data = request.json | |
| if not data: | |
| return jsonify({"error": "Invalid JSON or Content-Type"}), 400 | |
| topic = data.get('topic', 'General') | |
| level = data.get('level', 'Beginner') | |
| prompt = f""" | |
| 请为主题 "{topic}" (难度: {level}) 生成一个详细的课程大纲。 | |
| 请严格以 JSON 格式输出,不要包含 Markdown 代码块标记(如 ```json),直接返回 JSON 字符串。 | |
| JSON 结构如下: | |
| {{ | |
| "title": "课程标题", | |
| "description": "课程简介", | |
| "modules": [ | |
| {{ | |
| "title": "模块 1 标题", | |
| "topics": ["知识点 1", "知识点 2"], | |
| "duration": "2小时" | |
| }} | |
| ], | |
| "quiz": [ | |
| {{ | |
| "question": "测试题 1", | |
| "options": ["A", "B", "C", "D"], | |
| "answer": "A" | |
| }} | |
| ] | |
| }} | |
| """ | |
| messages = [{"role": "user", "content": prompt}] | |
| ai_reply = call_ai(messages) | |
| course_data = {} | |
| if ai_reply: | |
| try: | |
| # Clean up potential markdown code blocks | |
| cleaned_reply = ai_reply.replace("```json", "").replace("```", "").strip() | |
| course_data = json.loads(cleaned_reply) | |
| except: | |
| print("JSON Parse Error, using fallback") | |
| course_data = _get_mock_course(topic) | |
| else: | |
| course_data = _get_mock_course(topic) | |
| # Save to DB | |
| try: | |
| with get_db_connection() as conn: | |
| c = conn.cursor() | |
| c.execute("INSERT INTO courses (title, topic, level, content) VALUES (?, ?, ?, ?)", | |
| (course_data.get('title', topic), topic, level, json.dumps(course_data))) | |
| conn.commit() | |
| except Exception as e: | |
| print(f"DB Error: {e}") | |
| traceback.print_exc() | |
| return jsonify(course_data) | |
| except Exception as e: | |
| print(f"Generate Course Error: {e}") | |
| traceback.print_exc() | |
| return jsonify(error="Generation Error", details=str(e)), 500 | |
| def _get_mock_course(topic): | |
| return { | |
| "title": f"{topic} 快速入门 (Mock)", | |
| "description": "这是一个自动生成的模拟课程大纲(AI 服务未响应)。", | |
| "modules": [ | |
| {"title": "基础概念", "topics": ["定义与历史", "核心原理"], "duration": "1小时"}, | |
| {"title": "进阶应用", "topics": ["实战案例", "常见陷阱"], "duration": "2小时"} | |
| ], | |
| "quiz": [ | |
| {"question": "这是一个模拟问题吗?", "options": ["是", "否"], "answer": "是"} | |
| ] | |
| } | |
| def list_courses(): | |
| try: | |
| with sqlite3.connect(DB_PATH) as conn: | |
| c = conn.cursor() | |
| c.execute("SELECT id, title, topic, level, created_at FROM courses ORDER BY id DESC") | |
| rows = c.fetchall() | |
| courses = [] | |
| for r in rows: | |
| courses.append({ | |
| "id": r[0], | |
| "title": r[1], | |
| "topic": r[2], | |
| "level": r[3], | |
| "created_at": r[4] | |
| }) | |
| return jsonify(courses) | |
| except Exception as e: | |
| print(f"List Courses Error: {e}") | |
| return jsonify(error="Failed to list courses"), 500 | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=7860, debug=True) | |