Files

372 lines
14 KiB
Python

import os
from e2b_code_interpreter import AsyncSandbox
from app.schemas.response import (
ErrorModel,
OutputItem,
ResultModel,
StdErrModel,
StdOutModel,
SystemMessage,
)
from app.services.redis_manager import redis_manager
from app.tools.notebook_serializer import NotebookSerializer
from app.utils.log_util import logger
from app.config.setting import settings
import json
from app.tools.base_interpreter import BaseCodeInterpreter
class E2BCodeInterpreter(BaseCodeInterpreter):
def __init__(
self,
task_id: str,
work_dir: str,
notebook_serializer: NotebookSerializer,
):
super().__init__(task_id, work_dir, notebook_serializer)
self.sbx = None
@classmethod
async def create(
cls,
task_id: str,
work_dir: str,
notebook_serializer: NotebookSerializer,
) -> "E2BCodeInterpreter":
"""创建并初始化 E2BCodeInterpreter 实例"""
instance = cls(task_id, work_dir, notebook_serializer)
return instance
async def initialize(self, timeout: int = 3000):
"""异步初始化沙箱环境"""
try:
self.sbx = await AsyncSandbox.create(
api_key=settings.E2B_API_KEY, timeout=timeout
)
logger.info("沙箱环境初始化成功")
await self._pre_execute_code()
await self._upload_all_files()
except Exception as e:
logger.error(f"初始化沙箱环境失败: {str(e)}")
raise
async def _upload_all_files(self):
"""上传工作目录中的所有文件到沙箱"""
try:
logger.info(f"开始上传文件,工作目录: {self.work_dir}")
if not os.path.exists(self.work_dir):
logger.error(f"工作目录不存在: {self.work_dir}")
raise FileNotFoundError(f"工作目录不存在: {self.work_dir}")
files = [
f for f in os.listdir(self.work_dir) if f.endswith((".csv", ".xlsx"))
]
logger.info(f"工作目录中的文件列表: {files}")
for file in files:
file_path = os.path.join(self.work_dir, file)
if os.path.isfile(file_path):
try:
with open(file_path, "rb") as f:
content = f.read()
# 使用官方推荐的 files.write 方法
await self.sbx.files.write(f"/home/user/{file}", content)
logger.info(f"成功上传文件到沙箱: {file}")
except Exception as e:
logger.error(f"上传文件 {file} 失败: {str(e)}")
raise
except Exception as e:
logger.error(f"文件上传过程失败: {str(e)}")
raise
async def _pre_execute_code(self):
init_code = (
"import matplotlib.pyplot as plt\n"
# "plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Arial Unicode MS']\n"
# "plt.rcParams['axes.unicode_minus'] = False\n"
# "plt.rcParams['font.family'] = 'sans-serif'\n"
)
await self.execute_code(init_code)
async def execute_code(self, code: str) -> tuple[str, bool, str]:
"""执行代码并返回结果"""
if not self.sbx:
raise RuntimeError("沙箱环境未初始化")
logger.info(f"执行代码: {code}")
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 = await self.sbx.run_code(code) # 返回 Execution 对象
logger.info("代码执行完成,开始处理结果...")
await redis_manager.publish_message(
self.task_id,
SystemMessage(content="代码执行完成"),
)
# 处理执行错误
if execution.error:
error_occurred = True
error_message = f"Error: {execution.error.name}: {execution.error.value}\n{execution.error.traceback}"
error_message = self._truncate_text(error_message)
logger.error(f"执行错误: {error_message}")
text_to_gpt.append(self.delete_color_control_char(error_message))
content_to_display.append(
ErrorModel(
name=execution.error.name,
value=execution.error.value,
traceback=execution.error.traceback,
)
)
# 处理标准输出和标准错误
if execution.logs:
if execution.logs.stdout:
stdout_str = "\n".join(execution.logs.stdout)
stdout_str = self._truncate_text(stdout_str)
logger.info(f"标准输出: {stdout_str}")
text_to_gpt.append(stdout_str)
content_to_display.append(
StdOutModel(msg="\n".join(execution.logs.stdout))
)
self.notebook_serializer.add_code_cell_output_to_notebook(stdout_str)
if execution.logs.stderr:
stderr_str = "\n".join(execution.logs.stderr)
stderr_str = self._truncate_text(stderr_str)
logger.warning(f"标准错误: {stderr_str}")
text_to_gpt.append(stderr_str)
content_to_display.append(
StdErrModel(msg="\n".join(execution.logs.stderr))
)
# 处理执行结果
if execution.results:
for result in execution.results:
# 1. 文本格式
if str(result):
content_to_display.append(
ResultModel(type="result", format="text", msg=str(result))
)
# 2. HTML格式
if result._repr_html_():
content_to_display.append(
ResultModel(
type="result", format="html", msg=result._repr_html_()
)
)
# 3. Markdown格式
if result._repr_markdown_():
content_to_display.append(
ResultModel(
type="result",
format="markdown",
msg=result._repr_markdown_(),
)
)
# 4. PNG图片(base64字符串,前端可直接渲染)
if result._repr_png_():
content_to_display.append(
ResultModel(
type="result", format="png", msg=result._repr_png_()
)
)
# 5. JPEG图片
if result._repr_jpeg_():
content_to_display.append(
ResultModel(
type="result", format="jpeg", msg=result._repr_jpeg_()
)
)
# 6. SVG
if result._repr_svg_():
content_to_display.append(
ResultModel(
type="result", format="svg", msg=result._repr_svg_()
)
)
# 7. PDF
if result._repr_pdf_():
content_to_display.append(
ResultModel(
type="result", format="pdf", msg=result._repr_pdf_()
)
)
# 8. LaTeX
if result._repr_latex_():
content_to_display.append(
ResultModel(
type="result", format="latex", msg=result._repr_latex_()
)
)
# 9. JSON
if result._repr_json_():
content_to_display.append(
ResultModel(
type="result",
format="json",
msg=json.dumps(result._repr_json_()),
)
)
# 10. JavaScript
if result._repr_javascript_():
content_to_display.append(
ResultModel(
type="result",
format="javascript",
msg=result._repr_javascript_(),
)
)
# 处理主要结果
# if result.is_main_result and result.text:
# result_text = self._truncate_text(result.text)
# logger.info(f"主要结果: {result_text}")
# text_to_gpt.append(result_text)
# self.notebook_serializer.add_code_cell_output_to_notebook(
# result_text
# )
# 限制返回的文本总长度
for item in content_to_display:
if isinstance(item, dict):
if item.get("type") in ["stdout", "stderr", "error"]:
text_to_gpt.append(
self._truncate_text(
item.get("content") or item.get("value") or ""
)
)
elif isinstance(item, ResultModel):
if item.format in ["text", "html", "markdown", "json"]:
text_to_gpt.append(
self._truncate_text(f"[{item.format}]\n{item.msg}")
)
elif item.format in ["png", "jpeg", "svg", "pdf"]:
text_to_gpt.append(
f"[{item.format} 图片已生成,内容为 base64,未展示]"
)
logger.info(f"text_to_gpt: {text_to_gpt}")
combined_text = "\n".join(text_to_gpt)
# 在代码执行完成后,立即同步文件
try:
await self.download_all_files_from_sandbox()
logger.info("文件同步完成")
except Exception as e:
logger.error(f"文件同步失败: {str(e)}")
# 保存到分段内容
## TODO: Base64 等图像需要优化
await self._push_to_websocket(content_to_display)
return (
combined_text,
error_occurred,
error_message,
)
async def get_created_images(self, section: str) -> list[str]:
"""获取当前 section 创建的图片列表"""
if not self.sbx:
logger.warning("沙箱环境未初始化")
return []
try:
files = await self.sbx.files.list("./")
for file in files:
if file.path.endswith(".png") or file.path.endswith(".jpg"):
self.add_section(section)
self.section_output[section]["images"].append(file.name)
self.created_images = list(
set(self.section_output[section]["images"]) - set(self.created_images)
)
logger.info(f"{section}-获取创建的图片列表: {self.created_images}")
return self.created_images
except Exception as e:
logger.error(f"获取创建的图片列表失败: {str(e)}")
return []
async def cleanup(self):
"""清理资源并关闭沙箱"""
try:
if self.sbx:
if await self.sbx.is_running():
try:
await self.download_all_files_from_sandbox()
except Exception as e:
logger.error(f"下载文件失败: {str(e)}")
finally:
await self.sbx.kill()
logger.info("成功关闭沙箱环境")
else:
logger.warning("沙箱已经关闭,跳过清理步骤")
except Exception as e:
logger.error(f"清理沙箱环境失败: {str(e)}")
# 这里可以选择不抛出异常,因为这是清理步骤
async def download_all_files_from_sandbox(self) -> None:
"""从沙箱中下载所有文件并与本地同步"""
try:
# 获取沙箱中的文件列表
sandbox_files = await self.sbx.files.list("/home/user")
sandbox_files_dict = {f.name: f for f in sandbox_files}
# 获取本地文件列表
local_files = set()
if os.path.exists(self.work_dir):
local_files = set(os.listdir(self.work_dir))
# 下载新文件或更新已修改的文件
for file in sandbox_files:
try:
# 排除 .bash_logout、.bashrc 和 .profile 文件
if file.name in [".bash_logout", ".bashrc", ".profile"]:
continue
local_path = os.path.join(self.work_dir, file.name)
should_download = True
# 检查文件是否需要更新
if file.name in local_files:
# 这里可以添加文件修改时间或内容哈希的比较
# 暂时简单处理,有同名文件就更新
pass
if should_download:
# 使用 bytes 格式读取文件内容,确保正确处理二进制数据
content = await self.sbx.files.read(file.path, format="bytes")
# 确保目标目录存在
os.makedirs(self.work_dir, exist_ok=True)
# 写入文件
with open(local_path, "wb") as f:
f.write(content)
logger.info(f"同步文件: {file.name}")
except Exception as e:
logger.error(f"同步文件 {file.name} 失败: {str(e)}")
continue
logger.info("文件同步完成")
except Exception as e:
logger.error(f"文件同步失败: {str(e)}")