Files

233 lines
9.6 KiB
Python

from app.tools.base_interpreter import BaseCodeInterpreter
from app.tools.notebook_serializer import NotebookSerializer
import jupyter_client
from app.utils.log_util import logger
import os
from app.services.redis_manager import redis_manager
from app.schemas.response import (
OutputItem,
ResultModel,
StdErrModel,
SystemMessage,
)
class LocalCodeInterpreter(BaseCodeInterpreter):
def __init__(
self,
task_id: str,
work_dir: str,
notebook_serializer: NotebookSerializer,
):
super().__init__(task_id, work_dir, notebook_serializer)
self.km, self.kc = None, None
self.interrupt_signal = False
async def initialize(self):
# 本地内核一般不需异步上传文件,直接切换目录即可
# 初始化 Jupyter 内核管理器和客户端
logger.info("初始化本地内核")
self.km, self.kc = jupyter_client.manager.start_new_kernel(
kernel_name="python3"
)
self._pre_execute_code()
def _pre_execute_code(self):
init_code = (
f"import os\n"
f"work_dir = r'{self.work_dir}'\n"
f"os.makedirs(work_dir, exist_ok=True)\n"
f"os.chdir(work_dir)\n"
f"print('当前工作目录:', os.getcwd())\n"
# f"import matplotlib.pyplot as plt\n"
# f"import matplotlib as mpl\n"
# # 更完整的中文字体配置
# f"plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', 'DejaVu Sans', 'sans-serif']\n"
# f"plt.rcParams['axes.unicode_minus'] = False\n"
# f"plt.rcParams['font.family'] = 'sans-serif'\n"
# f"mpl.rcParams['font.size'] = 12\n"
# f"mpl.rcParams['axes.labelsize'] = 12\n"
# f"mpl.rcParams['xtick.labelsize'] = 10\n"
# f"mpl.rcParams['ytick.labelsize'] = 10\n"
# # 设置DPI以获得更清晰的显示
)
self.execute_code_(init_code)
async def execute_code(self, code: str) -> tuple[str, bool, str]:
logger.info(f"执行代码: {code}")
# 添加代码到notebook
self.notebook_serializer.add_code_cell_to_notebook(code)
text_to_gpt: list[str] = []
content_to_display: list[OutputItem] | None = []
error_occurred: bool = False
error_message: str = ""
await redis_manager.publish_message(
self.task_id,
SystemMessage(content="开始执行代码"),
)
# 执行 Python 代码
logger.info("开始在本地执行代码...")
execution = self.execute_code_(code)
logger.info("代码执行完成,开始处理结果...")
await redis_manager.publish_message(
self.task_id,
SystemMessage(content="代码执行完成"),
)
for mark, out_str in execution:
if mark in ("stdout", "execute_result_text", "display_text"):
text_to_gpt.append(self._truncate_text(f"[{mark}]\n{out_str}"))
# 添加text到notebook
content_to_display.append(
ResultModel(type="result", format="text", msg=out_str)
)
self.notebook_serializer.add_code_cell_output_to_notebook(out_str)
elif mark in (
"execute_result_png",
"execute_result_jpeg",
"display_png",
"display_jpeg",
):
# TODO: 视觉模型解释图像
text_to_gpt.append(f"[{mark} 图片已生成,内容为 base64,未展示]")
# 添加image到notebook
if "png" in mark:
self.notebook_serializer.add_image_to_notebook(out_str, "image/png")
content_to_display.append(
ResultModel(type="result", format="png", msg=out_str)
)
else:
self.notebook_serializer.add_image_to_notebook(
out_str, "image/jpeg"
)
content_to_display.append(
ResultModel(type="result", format="jpeg", msg=out_str)
)
elif mark == "error":
error_occurred = True
error_message = self.delete_color_control_char(out_str)
error_message = self._truncate_text(error_message)
logger.error(f"执行错误: {error_message}")
text_to_gpt.append(error_message)
# 添加error到notebook
self.notebook_serializer.add_code_cell_error_to_notebook(out_str)
content_to_display.append(StdErrModel(msg=out_str))
logger.info(f"text_to_gpt: {text_to_gpt}")
combined_text = "\n".join(text_to_gpt)
await self._push_to_websocket(content_to_display)
return (
combined_text,
error_occurred,
error_message,
)
def execute_code_(self, code) -> list[tuple[str, str]]:
msg_id = self.kc.execute(code)
logger.info(f"执行代码: {code}")
# Get the output of the code
msg_list = []
while True:
try:
iopub_msg = self.kc.get_iopub_msg(timeout=1)
msg_list.append(iopub_msg)
if (
iopub_msg["msg_type"] == "status"
and iopub_msg["content"].get("execution_state") == "idle"
):
break
except:
if self.interrupt_signal:
self.km.interrupt_kernel()
self.interrupt_signal = False
continue
all_output: list[tuple[str, str]] = []
for iopub_msg in msg_list:
if iopub_msg["msg_type"] == "stream":
if iopub_msg["content"].get("name") == "stdout":
output = iopub_msg["content"]["text"]
all_output.append(("stdout", output))
elif iopub_msg["msg_type"] == "execute_result":
if "data" in iopub_msg["content"]:
if "text/plain" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["text/plain"]
all_output.append(("execute_result_text", output))
if "text/html" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["text/html"]
all_output.append(("execute_result_html", output))
if "image/png" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["image/png"]
all_output.append(("execute_result_png", output))
if "image/jpeg" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["image/jpeg"]
all_output.append(("execute_result_jpeg", output))
elif iopub_msg["msg_type"] == "display_data":
if "data" in iopub_msg["content"]:
if "text/plain" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["text/plain"]
all_output.append(("display_text", output))
if "text/html" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["text/html"]
all_output.append(("display_html", output))
if "image/png" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["image/png"]
all_output.append(("display_png", output))
if "image/jpeg" in iopub_msg["content"]["data"]:
output = iopub_msg["content"]["data"]["image/jpeg"]
all_output.append(("display_jpeg", output))
elif iopub_msg["msg_type"] == "error":
# TODO: 正确返回格式
if "traceback" in iopub_msg["content"]:
output = "\n".join(iopub_msg["content"]["traceback"])
cleaned_output = self.delete_color_control_char(output)
all_output.append(("error", cleaned_output))
return all_output
async def get_created_images(self, section: str) -> list[str]:
"""获取新创建的图片列表"""
current_images = set()
files = os.listdir(self.work_dir)
for file in files:
if file.endswith((".png", ".jpg", ".jpeg")):
current_images.add(file)
# 计算新增的图片
new_images = current_images - self.last_created_images
# 更新last_created_images为当前的图片集合
self.last_created_images = current_images
logger.info(f"新创建的图片列表: {new_images}")
return list(new_images) # 最后转换为list返回
async def cleanup(self):
# 关闭内核
self.kc.shutdown()
logger.info("关闭内核")
self.km.shutdown_kernel()
def send_interrupt_signal(self):
self.interrupt_signal = True
def restart_jupyter_kernel(self):
"""Restart the Jupyter kernel and recreate the work directory."""
self.kc.shutdown()
self.km, self.kc = jupyter_client.manager.start_new_kernel(
kernel_name="python3"
)
self.interrupt_signal = False
self._create_work_dir()
def _create_work_dir(self):
"""Ensure the working directory exists after a restart."""
os.makedirs(self.work_dir, exist_ok=True)