From 0fd548edcd5ad6ead986b6bc3256ae22b7078af8 Mon Sep 17 00:00:00 2001 From: CakeCN Date: Mon, 17 Nov 2025 23:14:55 +0800 Subject: [PATCH] save_load_learned --- Html/apps/config.py | 1 + Html/apps/services/course_service.py | 138 +++++++++++++++++++++++++- Html/apps/sockets/namespaces.py | 62 ++++++++++-- Html/apps/static/js/chatbox.js | 143 +-------------------------- Html/config.ini | 1 + 5 files changed, 194 insertions(+), 151 deletions(-) diff --git a/Html/apps/config.py b/Html/apps/config.py index a06fb17..fe33684 100644 --- a/Html/apps/config.py +++ b/Html/apps/config.py @@ -48,4 +48,5 @@ class Config: } ASE_ENGINE_URL = GLOBAL_CONFIG['ASE_ENGINE']['url'] + ASE_ENGINE_URL_TOKEN = GLOBAL_CONFIG['ASE_ENGINE']['url_token'] ASE_ENGINE_NAMESPACE = GLOBAL_CONFIG['ASE_ENGINE']['namespace'] \ No newline at end of file diff --git a/Html/apps/services/course_service.py b/Html/apps/services/course_service.py index 108b5ec..5780a56 100644 --- a/Html/apps/services/course_service.py +++ b/Html/apps/services/course_service.py @@ -1,6 +1,8 @@ # myapp/services/course_service.py import re -from typing import Dict, List +import requests +import json +from typing import Dict, List, Optional from flask import current_app from urllib.parse import urlparse from datetime import datetime @@ -450,10 +452,10 @@ def _default_markdowns(lesson_name: str): ## 阶段一:示例阶段 ### 步骤A -请基于“步骤A”的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。 +请基于"步骤A"的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。 ### 步骤B -请基于“步骤B”的教学目标与材料,生成指导性提示词。 +请基于"步骤B"的教学目标与材料,生成指导性提示词。 """ score_prompt_md = f"""# {lesson_name} @@ -463,8 +465,134 @@ def _default_markdowns(lesson_name: str): ## 阶段一:示例阶段 ### 步骤A -对学员在“步骤A”的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": , "reasons": "", "advices": ""}}``` +对学员在"步骤A"的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": , "reasons": "", "advices": ""}}``` ### 步骤B -同上,对“步骤B”输出进行评分与反馈。 +同上,对"步骤B"输出进行评分与反馈。 """ return lesson_md, prompt_md, score_prompt_md + +def save_learning_progress(user_uuid: str, chatmanager, user_id: str) -> bool: + """ + 保存用户学习进度到MongoDB + + 参数: + - user_uuid: 用户UUID + - chatmanager: ChatManager实例,包含学习进度数据 + - user_id: 用户ID + + 返回: + - bool: 保存是否成功 + """ + try: + # 获取MongoDB连接 + mongo = current_app.extensions["mongo"] + + # 获取MultiAgents框架中的对话列表 + dialog_list = get_multiagents_dialog_list(user_id) + + # 构建学习进度数据 + progress_data = { + "user_uuid": user_uuid, + "user_id": user_id, + "material_id": chatmanager.material_id, + "chapter_name": chatmanager.chapter_name, + "lesson_name": chatmanager.lesson_name, + "chapter_chain_now": chatmanager.chapter_chain_now, + "chat_historys": chatmanager.chat_historys, + "scores": chatmanager.scores, + "multiagents_dialogs": dialog_list, + "updated_at": datetime.now() + } + + # 保存到MongoDB,使用upsert操作(如果存在则更新,不存在则插入) + mongo.db.learning_progress.update_one( + { + "user_uuid": user_uuid, + "material_id": chatmanager.material_id, + "chapter_name": chatmanager.chapter_name, + "lesson_name": chatmanager.lesson_name + }, + {"$set": progress_data}, + upsert=True + ) + + current_app.logger.info(f"学习进度保存成功: user_uuid={user_uuid}, lesson={chatmanager.lesson_name}") + return True + except Exception as e: + current_app.logger.error(f"保存学习进度失败: {str(e)}") + return False + +def get_multiagents_dialog_list(user_id: str) -> List[Dict]: + """ + 从MultiAgents框架获取用户对话列表 + + 参数: + - user_id: 用户ID + + 返回: + - List[Dict]: 对话列表 + """ + try: + # 从配置中获取ASE引擎URL和token + config = current_app.config + url_token = config.get("ASE_ENGINE_URL_TOKEN", "") + + # 构建请求URL + base_url = config.get("ASE_ENGINE_URL", "") + api_url = f"{base_url}/api/sync/download" + + # 构建请求参数 + payload = { + "namespace_url": url_token, # 使用配置中的token作为namespace_url + "user_id": user_id + } + + # 发送POST请求 + response = requests.post( + api_url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=10 + ) + + # 检查响应 + if response.status_code == 200: + return response.json() + else: + current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}") + return [] + except Exception as e: + current_app.logger.error(f"获取MultiAgents对话列表异常: {str(e)}") + return [] + +def load_learning_progress(user_uuid: str, material_id: str, chapter_name: str, lesson_name: str) -> Optional[Dict]: + """ + 加载用户学习进度 + + 参数: + - user_uuid: 用户UUID + - material_id: 课程ID + - chapter_name: 章节名 + - lesson_name: 课时名 + + 返回: + - Optional[Dict]: 学习进度数据,如果不存在则返回None + """ + try: + mongo = current_app.extensions["mongo"] + progress = mongo.db.learning_progress.find_one({ + "user_uuid": user_uuid, + "material_id": material_id, + "chapter_name": chapter_name, + "lesson_name": lesson_name + }) + + if progress: + # 转换ObjectId为字符串 + if "_id" in progress: + progress["_id"] = str(progress["_id"]) + return progress + return None + except Exception as e: + current_app.logger.error(f"加载学习进度失败: {str(e)}") + return None diff --git a/Html/apps/sockets/namespaces.py b/Html/apps/sockets/namespaces.py index f31f128..2224ea7 100644 --- a/Html/apps/sockets/namespaces.py +++ b/Html/apps/sockets/namespaces.py @@ -7,7 +7,7 @@ from flask_socketio import Namespace, join_room, leave_room, emit from ..function_tools import judge from ..function_tools import next_chapter from ..services.backboard_service import realtime_response -from ..services.markdown_service import load_full_markdown_file +from ..services.course_service import load_full_markdown_file, save_learning_progress from ..extension_ase.ase_client import HSAEngineClient from ..services.user_service import get_or_load_current_user @@ -71,8 +71,32 @@ class AgentNamespace(Namespace): chatmanager = user_uuid2chatmanager[user_uuid] chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'}) chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid}) - chatmanager.chat_historys = [] - chatmanager.scores = [] + + # 尝试加载用户学习进度 + from ..services.course_service import load_learning_progress + progress = load_learning_progress(user_uuid, course_id, chapter_name, lesson_name) + need_skip_chapters = 0 + + if progress: + # 恢复学习进度数据 + if 'scores' in progress: + # 根据scores的数量确定需要跳过的章节数 + need_skip_chapters = len(progress['scores']) + chatmanager.scores = progress['scores'] + print(f"恢复用户学习进度: {user_uuid}, 已完成 {need_skip_chapters} 个章节") + + # 恢复聊天历史 + if 'chat_historys' in progress: + chatmanager.chat_historys = progress['chat_historys'] + else: + chatmanager.chat_historys = [] + + # 恢复当前章节链 + if 'chapter_chain_now' in progress: + chatmanager.chapter_chain_now = progress['chapter_chain_now'] + else: + chatmanager.chat_historys = [] + chatmanager.scores = [] fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name) user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager) @@ -80,6 +104,10 @@ class AgentNamespace(Namespace): backboard_manager.add_backboard(user_uuid, user_id, course_id, lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}') chatmanager.bb = backboard_manager.get_backboard(user_uuid) chatmanager.next_chapter() + + # 如果有保存的进度,需要跳过已经完成的章节 + for _ in range(need_skip_chapters): + chatmanager.next_chapter() self.chatmanager = chatmanager self.ase_client = user_uuid2ase_client[user_uuid] user_uuid2ase_client[user_uuid].register_on_connect_entry( @@ -221,7 +249,7 @@ class AgentNamespace(Namespace): if len(chatmanager.scores) >= chatmanager.chapter_chain_now: chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None) else: chatmanager.scores.append(data.get('data', None)) - user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.material_id, chatmanager.chapter_name, chatmanager.lesson_name) + user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.course_id, chatmanager.chapter_name, chatmanager.lesson_name) chatmanager.load_next_chapter() time.sleep(0.5) chatmanager.ase_client.send_text("chapter-start", "") @@ -241,10 +269,32 @@ class AgentNamespace(Namespace): def on_disconnect(self,data): + print(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}') ex = current_app.extensions uuid = session.get('user_uuid') - user_uuid2chatmanager = ex["user_uuid2chatmanager"] - user_uuid2chatmanager[uuid].disconnect(uuid) + user_uuid2chatmanager = ex.get("user_uuid2chatmanager", {}) + + # 保存学习进度到MongoDB + try: + # 获取用户ID + uuid2username = ex.get("uuid2username", {}) + user_id = uuid2username.get(uuid, "unknown") + + # 调用保存学习进度功能 + chatmanager = user_uuid2chatmanager.get(uuid) + if chatmanager: + save_result = save_learning_progress(uuid, chatmanager, user_id) + print(f"保存学习进度结果: {save_result}, user_uuid={uuid}") + except Exception as e: + print(f"保存学习进度时发生异常: {str(e)}") + + # 执行原有的断开连接逻辑 + if uuid in user_uuid2chatmanager: + user_uuid2chatmanager[uuid].disconnect(uuid) + + # 离开房间 + if uuid: + leave_room(uuid, namespace='/agent') print("VSCode client disconnected") print("Disconnect reason:"+str(data)) \ No newline at end of file diff --git a/Html/apps/static/js/chatbox.js b/Html/apps/static/js/chatbox.js index 8c6dc34..7ba5155 100644 --- a/Html/apps/static/js/chatbox.js +++ b/Html/apps/static/js/chatbox.js @@ -105,29 +105,7 @@ document.addEventListener('DOMContentLoaded', function () { document.getElementById('chatbox').appendChild(requestMessage); document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; } - // for (let i = 0; i < data.length; i++) { - // const element = data[i] - // const requestMessage = document.createElement('div'); - // requestMessage.className = 'chat-message server-message'; - // const approveButton = document.createElement('button'); - // approveButton.innerText = '批准'; - // approveButton.data = element; - // approveButton.style.width = '100%'; - // approveButton.style.borderRadius = '4px'; - // approveButton.onclick = function(event) { - // console.log(element) - // socket.emit('message', JSON.stringify({type:'function', data:element})); - // event.currentTarget.disabled = true; - // event.currentTarget.style.backgroundColor = 'gray'; - // event.currentTarget.style.color = 'white'; // 可选:设置文字颜色为白色,以便更清晰地显示 - // event.currentTarget.style.border = 'none'; // 可选:去掉边框 - // }; - // // 将按钮添加到消息气泡中 - // requestMessage.innerHTML = `Function: ${element.name}(${JSON.stringify(element.arguments)})
`; - // requestMessage.appendChild(approveButton); - // document.getElementById('chatbox').appendChild(requestMessage); - // document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; - // } + } catch (error) { console.log(error) } @@ -250,124 +228,9 @@ function sendMessage() { document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; } -// let voice_stream = null; -// let audio_chunks = []; -// let isRecording = false; -// let recordingInterval = null; -// //鼠标点击终止事件 -// function handleMouseClick(event) { -// event.stopPropagation(); -// stopRecording(); -// } -// //发送语音 -// const startAudioRecording = async () => { -// try { -// const stream = await navigator.mediaDevices.getUserMedia({ -// audio: { -// sampleRate: 16000, -// channelCount: 1, -// echoCancellation: true, -// noiseSuppression: true -// } -// }); -// const mediaRecorder = new MediaRecorder(stream); -// const audioContext = new AudioContext({ -// sampleRate: 16000 -// }); - -// // Load the AudioWorklet -// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js'); - -// const source = audioContext.createMediaStreamSource(stream); -// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor'); -// let audioChunks = []; - -// workletNode.port.onmessage = (e) => { -// audioChunks.push(new Float32Array(e.data)); - -// if (audioChunks.length >= 16) { -// // 合并所有音频块 -// const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0); -// const mergedData = new Float32Array(totalLength); -// let offset = 0; -// for (const chunk of audioChunks) { -// mergedData.set(chunk, offset); -// offset += chunk.length; -// } - -// const audioMessage = { -// id: window.data.user_uuid, -// type: 'audio', -// data: mergedData, -// sampleRate: 16000, -// timestamp: Date.now() -// }; -// socket.emit('voice', audioMessage); -// console.log("voice sended") -// audioChunks = []; -// } -// }; - -// source.connect(workletNode); -// workletNode.connect(audioContext.destination); - -// mediaRecorder.value = { -// stop: () => { -// source.disconnect(); -// workletNode.disconnect(); -// stream.getTracks().forEach(track => track.stop()); -// audioContext.close(); -// } -// } - -// isRecording.value = true; -// } catch (error) { -// console.error('Error starting audio recording:', error); -// } -// }; -// function sendVoiceMessage() { -// startAudioRecording() -// //变换录音中图标 -// document.getElementById('recordIcon').className = 'bi bi-record-circle'; -// document.addEventListener('click', handleMouseClick); -// } -// //停止录音 -// function stopRecording() { -// if (!isRecording) return; -// isRecording = false; -// //取消监听 -// document.removeEventListener('click', handleMouseClick); -// //取消定时器 -// if (recordingInterval) { -// clearInterval(recordingInterval); -// recordingInterval = null; -// } -// //变换图标 -// document.getElementById('recordIcon').className = 'bi bi-mic'; -// //发送剩下的数据 -// if (audio_chunks.length > 0) { -// const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0)); -// let offset = 0; -// for (const chunk of audio_chunks) { -// merged_data.set(new Uint8Array(chunk), offset); -// offset += chunk.length; -// } -// socket.emit('VoiceMessage', merged_data, { binary: true }); -// audio_chunks = []; -// } -// overlay.style.display = 'none'; -// //关闭音频流 -// if (voice_stream) { -// voice_stream.stop_stream(); -// voice_stream.close(); -// voice_stream = null; -// } -// if (p) { -// p.terminate(); -// p = null; -// } -// } + + //语音这一块// diff --git a/Html/config.ini b/Html/config.ini index 5ad9972..8efda60 100644 --- a/Html/config.ini +++ b/Html/config.ini @@ -29,3 +29,4 @@ region = ap-guangzhou [ASE_ENGINE] url = https://asengine.net namespace = /socket.io/7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1 +url_token = 7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1