Files
hsa/Html/apps/extension_ase/ase_client/manager.py
2025-09-12 15:07:25 +08:00

235 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import signal
import sys
import os
import time
import uuid
current_directory = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_directory)
from flask_socketio import SocketIO, join_room, emit, Namespace
import os
from ...function_tools.function_manager import FunctionManager
from dataclasses import dataclass, field
from typing import Dict, Optional
import subprocess
import threading
from ...utils import *
EMPTY_CHAPTER_CHAIN = [ Chapter(1, CHAPTER_FOCUS, "本章是未打开某个具体章节时的默认章节。", "处于本章节时不会有任何章节跳转。请作为一名经验丰富的算法教师,回答用户的问题。") ]
@dataclass
class ProcRecord:
popen: subprocess.Popen
user_id: str
path_dir: str
port: int
deadline_ts: Optional[float] = None
timer: Optional[threading.Timer] = field(default=None, repr=False)
class ChatManager:
def __init__(self):
self._procs: Dict[str, ProcRecord] = {}
self._lock = threading.RLock()
def init(self,user_uuid,ase_client,raw_markdown,raw_markdown_prompts,raw_score_prompts,max_iter = 5, app=None, socketio=None):
self.chapter_chain_now = -1
self.ase_client = ase_client
self.app = app
self.socketio = socketio
self.user_uuid = user_uuid
self.raw_markdown = raw_markdown
self.raw_markdown_prompts = raw_markdown_prompts
self.raw_score_prompts = raw_score_prompts
self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts)
self.function_manager = FunctionManager()
self.bb = None
def disconnect(self, uuid):
self.stop(uuid)
self.ase_client.disconnect()
def next_chapter(self):
assert self.chapter_chain_now + 1 < len(self.chapter_chain), "chapter_chain_now out of range"
self.chapter_chain_now += 1
self.focus_chapter(self.chapter_chain_now)
def focus_chapter(self, chapter_index):
assert chapter_index < len(self.chapter_chain), "chapter_index out of range"
self.chapter_chain_now = chapter_index
self.chapter_chain[chapter_index].Focus()
self.ase_client.loadmarkdown(self.chapter_chain[chapter_index].chapter, self.chapter_chain[chapter_index].chapter_prompt, self.chapter_chain[chapter_index].score_prompt)
def upload_bb(self):
self.ase_client.send_text('backboard-in', self.bb.get_info_prompt())
def message_pass(self, messagetype, message):
print("message_pass", self.user_uuid, messagetype, message)
with self.app.app_context():
try:
self.socketio.emit(messagetype, message, room=self.user_uuid, namespace='/agent')
except Exception as e:
print("message_pass",e)
def system_message(self, message):
with self.app.app_context():
try:
self.socketio.emit('system_message', message, room=self.user_uuid, namespace='/agent')
except Exception as e:
print("ERROR: system_message",e)
def save_chapter_memory(self, agent,course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal):
self.app.my_function.save_chapter_memory(self.user_uuid, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
def function_call(self, function_name, arg_function):
return self.function_manager.call_function(function_name, arg_function)
def change_language(self, id, language):
self.ase_client.send_text('language', language)
def sample_judge(self, id, bb):
self.function_manager.call_function('sample_judge', {'bb': bb})
with self.app.app_context():
try:
print(bb.get_active_file_reletive_path())
assert bb is not None and bb.active_file_path != "" , "no path specified"
if bb.get_active_file_reletive_path().endswith(".py"):
self.socketio.emit('terminal', {'data': f"python {bb.get_active_file_reletive_path()}"}, room=id, namespace='/vscode')
self.socketio.emit('system_message', "代码试运行尚在建设,推荐直接通过命令行执行!", room=id, namespace='/agent')
else:
self.socketio.emit('system_message', "代码试运行尚仅支持python文件", room=id, namespace='/agent')
except Exception as e:
print(e)
if (str(e) == "no path specified"):
self.socketio.emit("system_message", "未加载IDE文件同步请尝试重新打开IDE文件或刷新页面并确保插件已连接", room=id, namespace='/agent')
def judge(self, id, bb):
agent = self.agents[id]
with self.app.app_context():
try:
if (agent.chapter_chain[agent.chapter_chain_now].append_tools is not None and len(agent.chapter_chain[agent.chapter_chain_now].append_tools)):
for tool, tool_kwargs in agent.chapter_chain[agent.chapter_chain_now].append_tools:
if tool.__name__ == "judge":
print(bb.get_active_file_reletive_path())
assert bb is not None and bb.active_file_path != "" , "no path specified"
if bb.get_active_file_reletive_path().endswith(".py"):
# self.socketio.emit('terminal', {'data': f"python {bb.get_active_file_reletive_path()}"}, room=id, namespace='/vscode')
self.socketio.emit('system_message', "代码评测中,请稍后", room=id, namespace='/agent')
agent.function_call({'name':'judge','arguments':{'filepath': bb.get_active_file_reletive_path()}})
else:
self.socketio.emit('system_message', "代码评测尚仅支持python文件", room=id, namespace='/agent')
else:
with self.app.app_context():
self.socketio.emit("system_message", "本章学习无需评测", room=id, namespace='/agent')
else:
with self.app.app_context():
self.socketio.emit("system_message", "本章学习无需评测", room=id, namespace='/agent')
except Exception as e:
if (str(e) == "no path specified"):
self.socketio.emit("system_message", "未加载IDE文件同步请尝试重新打开IDE文件或刷新页面并确保插件已连接", room=id, namespace='/agent')
def start_code_server(self, key: str, user_id: str, path_dir: str, code_server_port: int, ttl_seconds: Optional[int] = None) -> ProcRecord:
# key is user_uuid
# 注意:建议使用 start_new_session=True 方便后续 kill 进程组
# 如果必须 sudo最好加 -n非交互避免卡住也可以考虑用 systemd-run 或专用守护
cmd = [
"sudo", "-u", user_id,
"code-server",
"--bind-addr", f"0.0.0.0:{code_server_port}",
"--auth", "none",
path_dir,
"--extensions-dir", "/share/extension/"
]
pop = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
start_new_session=True # 等价 setsid新建进程组方便整体结束
)
record = ProcRecord(
popen=pop,
user_id=user_id,
path_dir=path_dir,
port=code_server_port,
deadline_ts=(time.time() + ttl_seconds) if ttl_seconds else None
)
# 设置了到期停
if ttl_seconds:
t = threading.Timer(ttl_seconds, self.stop, kwargs={"key": key})
t.daemon = True
t.start()
record.timer = t
with self._lock:
self._procs[key] = record
return record
def stop(self, key: str, grace_seconds: int = 5) -> bool:
with self._lock:
rec = self._procs.pop(key, None)
if not rec:
return False
# 先取消到期定时器(如果是手动停)
if rec.timer:
try:
rec.timer.cancel()
except Exception:
pass
pop = rec.popen
if pop.poll() is not None:
return True # 已退出
try:
# 优雅:向进程组发 SIGTERM
os.killpg(pop.pid, signal.SIGTERM)
except ProcessLookupError:
return True
# 等待优雅退出
try:
pop.wait(timeout=grace_seconds)
return True
except subprocess.TimeoutExpired:
pass
# 兜底:强杀进程组
try:
os.killpg(pop.pid, signal.SIGKILL)
except ProcessLookupError:
pass
finally:
try:
pop.wait(timeout=1)
except Exception:
pass
return True
def get(self, key: str) -> Optional[ProcRecord]:
with self._lock:
return self._procs.get(key)
def stop_all(self):
with self._lock:
keys = list(self._procs.keys())
for k in keys:
self.stop(k)