430 lines
16 KiB
Python
430 lines
16 KiB
Python
from flask import Flask, redirect, session, request, jsonify, render_template, send_from_directory, url_for
|
||
from flask_cors import CORS
|
||
from flask_socketio import SocketIO, join_room, emit, Namespace
|
||
import markdown
|
||
import os
|
||
import uuid
|
||
import shutil
|
||
import sys
|
||
import json
|
||
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||
sys.path.insert(0, parent_dir)
|
||
student_workspace_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../study'))
|
||
|
||
current_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))
|
||
sys.path.insert(0, current_dir)
|
||
from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager
|
||
|
||
|
||
import configparser
|
||
|
||
from db.user_list import UserList
|
||
from db.user import User, load_user_from_json, create_user_json
|
||
from db.course_list import CourseList
|
||
from db.course import Course, load_course_from_json
|
||
GLOBAL_CONFIG = configparser.ConfigParser()
|
||
GLOBAL_CONFIG.read('config.ini')
|
||
VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url']
|
||
USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir']
|
||
COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir']
|
||
|
||
app = Flask(__name__)
|
||
socketio = SocketIO(app, cors_allowed_origins="*",ping_timeout=60, ping_interval=5)
|
||
# socketio = SocketIO(app, cors_allowed_origins="http://localhost:9888") # 设置跨域支持
|
||
import logging
|
||
app.secret_key = 'cakebaker'
|
||
app.logger.setLevel(logging.DEBUG)
|
||
# 配置 CORS
|
||
CORS(app, resources={r"/*": {"origins": VSCODE_WEB_URL},}, supports_credentials=True)
|
||
|
||
|
||
userid_recorder = {} # user_id&path -> session['user_id']
|
||
|
||
'''
|
||
Backboard
|
||
'''
|
||
from backboardManager import BackBoardManager, Backboard
|
||
|
||
backboard_manager = BackBoardManager()
|
||
uuid2username = {}
|
||
username2uuid = {}
|
||
|
||
|
||
|
||
# 定义命名空间:用于和 VSCode 插件交流
|
||
class VSCodeNamespace(Namespace):
|
||
def on_login(self,data):
|
||
print("VSCode client connected")
|
||
print(data)
|
||
dataconfig = data['config']
|
||
user_id = dataconfig.get('user_id')
|
||
path = dataconfig.get('path')
|
||
course_id = dataconfig.get('course_id')
|
||
lesson_id = dataconfig.get('chapter_id')
|
||
print(f"User {user_id} connected with path: {path}")
|
||
useruuid = username2uuid[user_id]
|
||
join_room(useruuid, namespace='/vscode')
|
||
if backboard_manager.get_backboard(useruuid) == None:
|
||
backboard_manager.add_backboard(useruuid,user_id, course_id, lesson_id, path)
|
||
|
||
|
||
def on_message(self, data):
|
||
print(f"Received from VSCode client: {data}")
|
||
dataconfig = data['config']
|
||
realtime_response(dataconfig, data)
|
||
# emit('response', {'message': 'Config data received and connection established'})
|
||
def on_disconnect(self, data):
|
||
print("VSCode client disconnected")
|
||
print("Disconnect reason:"+str(data))
|
||
|
||
|
||
def realtime_response(config, realtime_action):
|
||
user_id = config['user_id']
|
||
folder_path = config['path']
|
||
useruuid = username2uuid[user_id]
|
||
bb = backboard_manager.get_backboard(useruuid)
|
||
assert type(bb) == Backboard
|
||
|
||
bb.add_history(realtime_action)
|
||
|
||
if realtime_action['type'] == 'workspaceFolders':
|
||
bb.file_tree = realtime_action['fileTree']
|
||
|
||
if realtime_action['type'] == 'activeFile':
|
||
file_path = realtime_action['filePath']
|
||
assert type(file_path) == str
|
||
with open(f"{file_path}") as f:
|
||
bb.active_file_content = f.read()
|
||
|
||
if realtime_action['type'] == 'paste':
|
||
file_path = realtime_action['filePath']
|
||
assert type(file_path) == str
|
||
bb.pasted_file_path = file_path
|
||
bb.pasted_content = realtime_action['content']
|
||
bb.active_file_path = file_path
|
||
with open(f"{file_path}") as f:
|
||
bb.active_file_content = f.read()
|
||
|
||
|
||
if realtime_action['type'] == 'fileEdit':
|
||
file_path = realtime_action['filePath']
|
||
assert type(file_path) == str
|
||
bb.active_file_path = file_path
|
||
with open(f"{file_path}") as f:
|
||
bb.active_file_content = f.read()
|
||
|
||
|
||
|
||
|
||
print("config"+str(config))
|
||
print("realtime_action"+str(realtime_action))
|
||
pass
|
||
|
||
|
||
'''
|
||
Agent and Chat
|
||
'''
|
||
agent_manager = AgentManager(app = app, socketio = socketio)
|
||
user_threads = {}
|
||
import threading
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
# 初始化线程池
|
||
executor = ThreadPoolExecutor(max_workers=10)
|
||
class AgentNamespace(Namespace):
|
||
def on_login(self, data):
|
||
data = json.loads(data)
|
||
user = data['username']
|
||
course_id = data['course_id']
|
||
chapter_id = data['chapter_id']
|
||
user_uuid = username2uuid[user]
|
||
session['user_id'] = user_uuid
|
||
print(f'User connected with session user_id: {user_uuid}')
|
||
# 从markdown、markdown_prompts、score_prompts中各读取数据folder.md并进行new_agent
|
||
with open(f'books/markdown/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fmd,\
|
||
open(f'books/markdown_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8')as fmdp,\
|
||
open(f'books/score_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fsp:
|
||
markdown = fmd.read()
|
||
markdown_prompts = fmdp.read()
|
||
score_prompts = fsp.read()
|
||
user_uuid, agent = agent_manager.new_agent(course_id, chapter_id, markdown, markdown_prompts, score_prompts, id=user_uuid,
|
||
root_path=f'../../study/{user}/{course_id}/{chapter_id}')
|
||
print(user_uuid)
|
||
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
|
||
backboard_manager.add_backboard(user_uuid, user, course_id, chapter_id, root_path=f'../../study/{user}/{course_id}/{chapter_id}')
|
||
|
||
def on_language(self, language):
|
||
id = session.get('user_id')
|
||
agent_manager.change_language(id, language)
|
||
|
||
def on_message(self, data):
|
||
print(f"Message from client: {data}")
|
||
id = session.get('user_id')
|
||
if (type(data)==str):
|
||
data = json.loads(data)
|
||
print(id)
|
||
if data['type'] == 'text':
|
||
res = agent_manager.invoke(id, data['data'], backboard_manager.get_backboard(id).get_info_prompt())
|
||
print('=*='*20)
|
||
print(res.content)
|
||
reply = f"{res.content['speak']}"
|
||
with app.app_context():
|
||
emit('message',reply, room=id, namespace='/agent')
|
||
emit('request_function',res.content['function'], room=id, namespace='/agent')
|
||
|
||
if data['type'] == 'function':
|
||
agent_manager.function_call(id, data['data'])
|
||
|
||
def on_initiative(self,data):
|
||
print("User active function call")
|
||
user_id = session['user_id']
|
||
if data['name'] == 'sample_judge':
|
||
agent_manager.sample_judge(user_id, backboard_manager.get_backboard(user_id))
|
||
if data['name'] == 'judge':
|
||
agent_manager.judge(user_id, backboard_manager.get_backboard(user_id))
|
||
|
||
|
||
|
||
def on_disconnect(self,data):
|
||
print("VSCode client disconnected")
|
||
print("Disconnect reason:"+str(data))
|
||
|
||
|
||
|
||
'''
|
||
Markdown to HTML
|
||
'''
|
||
# 配置文件路径
|
||
MARKDOWN_DIR = 'books/markdown' # 存放 markdown 文件的文件夹
|
||
STATIC_DIR = 'static' # 存放生成的 HTML 和图片资源的文件夹
|
||
IMAGE_DIR = 'image' # 图片资源相对于 markdown 的位置
|
||
|
||
|
||
|
||
@app.route('/<course_id>-<filename>-markdown', methods=['GET'])
|
||
def convert_md(course_id, filename):
|
||
md_file_path = os.path.join(MARKDOWN_DIR,course_id, f'{filename}.md')
|
||
|
||
if not os.path.exists(md_file_path):
|
||
return jsonify({'error': 'Markdown file not found'}), 404
|
||
|
||
# 读取 markdown 文件内容
|
||
with open(md_file_path, 'r', encoding='utf-8') as file:
|
||
md_content = file.read()
|
||
|
||
# 将 markdown 转换为 HTML
|
||
html_content = markdown.markdown(md_content)
|
||
# 插入 CSS 样式,限制图片最大宽度为 100%,高度自动调整
|
||
html_with_styles = f"""
|
||
<html>
|
||
<head>
|
||
<style>
|
||
img {{
|
||
max-width: 100%;
|
||
height: auto;
|
||
}}
|
||
</style>
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||
</head>
|
||
<body>
|
||
{html_content}
|
||
</body>
|
||
</html>
|
||
"""
|
||
# 保存 HTML 文件到 static 文件夹
|
||
html_output_path = os.path.join(STATIC_DIR, f'{filename}.html')
|
||
with open(html_output_path, 'w', encoding='utf-8') as html_file:
|
||
html_file.write(html_with_styles)
|
||
|
||
# 处理图片资源,将 image/xxx 文件夹拷贝到 static/image/xxx
|
||
image_source_dir = os.path.join(MARKDOWN_DIR,course_id, IMAGE_DIR, filename)
|
||
image_target_dir = os.path.join(STATIC_DIR, IMAGE_DIR, filename)
|
||
print(f"Copying image resources from {image_source_dir} to {image_target_dir}")
|
||
if os.path.exists(image_source_dir):
|
||
# 确保 static/image/xxx 目录存在
|
||
if not os.path.exists(image_target_dir):
|
||
os.makedirs(image_target_dir)
|
||
|
||
# 拷贝图片资源
|
||
for image_file in os.listdir(image_source_dir):
|
||
full_image_file_path = os.path.join(image_source_dir, image_file)
|
||
if os.path.isfile(full_image_file_path):
|
||
shutil.copy(full_image_file_path, image_target_dir)
|
||
|
||
return jsonify({'html_url': f'/static/{filename}.html'})
|
||
|
||
# 静态文件(HTML 和图片资源)的访问
|
||
@app.route('/static/<path:filename>')
|
||
def serve_static(filename):
|
||
return send_from_directory(STATIC_DIR, filename)
|
||
|
||
|
||
'''
|
||
Vscode
|
||
'''
|
||
@app.route('/desktop/<user_id>/<course_id>/<chapter_id>')
|
||
def desktop(user_id, course_id, chapter_id):
|
||
if 'user_id' not in session:
|
||
# return redirect(url_for('login'))
|
||
session['user_id'] = 'user_' + str(uuid.uuid4())
|
||
print("user "+ user_id + "uuid is"+ session['user_id'])
|
||
username2uuid[user_id] = session['user_id']
|
||
uuid2username[session['user_id']] = user_id
|
||
userid_recorder[user_id+'&'+course_id] = session['user_id']
|
||
|
||
# 在学习目录下创建一个名为 user_id_path 的文件夹
|
||
path_dir = os.path.join(student_workspace_root_path, user_id, course_id,chapter_id)
|
||
os.makedirs(path_dir, exist_ok=True)
|
||
# 在此文件夹内部创建一个.config文件,并写入 user_id=user_id\n path=path
|
||
config_path = os.path.join(path_dir, '.config')
|
||
if GLOBAL_CONFIG['VSCODE_WEB_PATH']['is_wsl']: # 如果是在 WSL 中运行,则将路径转换为 WSL 路径
|
||
path_dir = path_dir.replace('\\', '/')
|
||
path_dir = path_dir.replace(GLOBAL_CONFIG['VSCODE_WEB_PATH']['windows_path'] , GLOBAL_CONFIG['VSCODE_WEB_PATH']['wsl_path'])
|
||
with open(config_path, 'w', encoding='utf-8') as f:
|
||
tmpd = {'user_id': user_id, 'course_id': course_id, 'chapter_id': chapter_id, 'path': path_dir}
|
||
json.dump(tmpd, f)
|
||
print('---------------------')
|
||
print(config_path)
|
||
return render_template('desktop.html', vscode_web_url=VSCODE_WEB_URL, user_id=user_id, course_id=course_id, chapter_id=chapter_id, workspace_path=path_dir)
|
||
|
||
@app.route('/desktop_nouser/<course_id>/<chapter_id>')
|
||
def desktop_nouser(course_id, chapter_id):
|
||
if 'user_id' not in session:
|
||
return redirect(url_for('login'))
|
||
user_id = session['user_id']
|
||
username = uuid2username[user_id]
|
||
return redirect(url_for('desktop', user_id=username, course_id=course_id, chapter_id=chapter_id))
|
||
|
||
@app.route('/vscode_data', methods=['POST'])
|
||
def vscode_data():
|
||
data = request.json
|
||
config = data['config']
|
||
realtime_response(config,data)
|
||
print(f"Received data from VSCode: {data}")
|
||
return jsonify({"status": "success", "received": data})
|
||
|
||
@app.route('/get_session')
|
||
def get_session():
|
||
user_session = session.get('user_id', 'default_session')
|
||
return jsonify({"session": user_session})
|
||
|
||
|
||
|
||
'''
|
||
Login
|
||
|
||
'''
|
||
users_list = UserList()
|
||
course_list = CourseList()
|
||
|
||
@app.route('/register')
|
||
def register():
|
||
return render_template('register.html')
|
||
|
||
@app.route('/register_post', methods=['POST'])
|
||
def register_post():
|
||
data = request.get_json()
|
||
username = data.get('username')
|
||
password = data.get('password')
|
||
if users_list.has_user(username) is not None:
|
||
users_list.add_user(username, password)
|
||
create_user_json(username, USER_DATA_DIR)
|
||
return jsonify({'success': True, 'message': '注册成功'})
|
||
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
|
||
|
||
|
||
@app.route('/login')
|
||
def login():
|
||
return render_template('login.html')
|
||
if 'user_id' not in session:
|
||
return render_template('login.html')
|
||
else:
|
||
return redirect(url_for('/'))
|
||
|
||
@app.route('/login_post', methods=['POST'])
|
||
def login_post():
|
||
# 获取请求的 JSON 数据
|
||
data = request.get_json()
|
||
|
||
username = data.get('username')
|
||
password = data.get('password')
|
||
|
||
pswd = users_list.get_user_pswd(username)
|
||
|
||
if pswd is None:
|
||
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
||
if pswd != password:
|
||
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
||
|
||
session['user_id'] = 'user_' + str(uuid.uuid4())
|
||
print("user "+ username + "uuid is"+ session['user_id'])
|
||
username2uuid[username] = session['user_id']
|
||
uuid2username[session['user_id']] = username
|
||
return jsonify({'success': True, 'message': '登录成功'})
|
||
|
||
user_id2UserClass = {}
|
||
'''
|
||
DashBoard
|
||
'''
|
||
@app.route('/dashboard')
|
||
def dashboard():
|
||
if 'user_id' not in session or session['user_id'] not in uuid2username:
|
||
return redirect(url_for('login'))
|
||
user_id = session['user_id']
|
||
username = uuid2username[user_id]
|
||
if (user_id not in user_id2UserClass):
|
||
user_id2UserClass[user_id] = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
|
||
user_course_data = []
|
||
for course_id in user_id2UserClass[user_id].select_course:
|
||
course_brief_info = course_list.get_course_brief_info(course_id, load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR))
|
||
user_course_data.append(course_brief_info)
|
||
return render_template('dashboard.html',user_data = user_id2UserClass[user_id].to_json_without_dialog(), user_course_data = json.dumps(user_course_data))
|
||
|
||
'''
|
||
Course
|
||
'''
|
||
|
||
@app.route('/course/<course_id>')
|
||
def course(course_id):
|
||
c = load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR)
|
||
return render_template('course.html', course_id=course_id, course_data=c)
|
||
|
||
@app.route('/select_course', methods=['POST'])
|
||
def select_course():
|
||
if ('user_id' not in session or session['user_id'] not in user_id2UserClass):return redirect(url_for('login'))
|
||
user = user_id2UserClass[session['user_id']]
|
||
data = request.get_json()
|
||
course_id = data.get('course_id')
|
||
if course_id:
|
||
user.select_new_course(course_id, load_course_from_json(course_id, COURSE_DATA_DIR))
|
||
return jsonify({'success': True, 'message': '课程选择成功'})
|
||
|
||
|
||
@app.route('/')
|
||
def home_index():
|
||
selected_courses=[]
|
||
if ('user_id' in session):
|
||
if (session['user_id'] not in user_id2UserClass): return redirect(url_for('login'))
|
||
user = user_id2UserClass[session['user_id']]
|
||
for course_id in user.select_course:
|
||
selected_courses.append(course_id)
|
||
|
||
return render_template('index.html', courses_data = course_list, selected_courses =selected_courses)
|
||
|
||
# 一些app辅助函数,主要提供给Agent与数据库的交互能力
|
||
|
||
class MyFunction:
|
||
def save_chapter_memory(self, id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal):
|
||
print("-=-=-=-=-=-=-=-")
|
||
print(id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
|
||
username = uuid2username[id]
|
||
u = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
|
||
u.save_chapter_memory(course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
|
||
|
||
app.my_function = MyFunction()
|
||
if __name__ == '__main__':
|
||
socketio.on_namespace(VSCodeNamespace('/vscode'))
|
||
socketio.on_namespace(AgentNamespace('/agent'))
|
||
socketio.run(app, host='0.0.0.0', port=5550, debug=False,allow_unsafe_werkzeug=True)
|
||
|