edu-spark-agent / app.py
EduSparkBot
fix: robust error handling for json payload and db retry
f55a222
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
@app.errorhandler(404)
def page_not_found(e):
return render_template('index.html'), 404
@app.errorhandler(500)
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
@app.errorhandler(413)
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
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/stats')
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"}
]
})
@app.route('/api/upload', methods=['POST'])
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
@app.route('/api/chat', methods=['POST'])
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
@app.route('/api/generate_course', methods=['POST'])
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": "是"}
]
}
@app.route('/api/courses')
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)