数据截至 (上游 commit 771b20c93db8)
富结果:IPython 显示格式器、DataFrame 与图表抽取
30 秒导读: 你在沙箱里跑
df(一个 pandas 表)或plt.plot(...)(一张 matplotlib 图),run_code返回的结 果里直接就有结构化的列数据、PNG 图片、甚至把图表拆成「点/柱/扇区」 的 JSON。这不是魔法——是 E2B 在 Jupyter 内核里偷偷注册了几个 IPython 显示格式器, 把富对象翻译成自定义 MIME 类型,一路流回客户端再还原成带类型的对象。本章讲透这条链。
本章是「内核桥」的下游:内核桥负责把 Jupyter 消息流回来, 本章负责讲那些消息里的富数据是怎么产生的、又是怎么被还原的。
1. 这是什么(零基础也能懂)
先看一个现象
同样一句 run_code,普通代码解释器只会给你一串文本;E2B 却能给你多种格式:
# 示意,非源码 —— 客户端视角
exec = sandbox.run_code("""
import pandas as pd
df = pd.DataFrame({"city": ["Tokyo", "Paris"], "pop": [37, 11]})
df # 最后一行的表
""")
result = exec.results[0]
result.text # 表的文本形式(给人看)
result.html # 表的 HTML(给浏览器渲染)
result.data # {"city": ["Tokyo","Paris"], "pop": [37,11]} ← 结构化!能直接喂给下游
再看图表:
# 示意,非源码
exec = sandbox.run_code("""
import matplotlib.pyplot as plt
plt.bar(["a","b","c"], [3,7,2])
plt.show()
""")
result = exec.results[0]
result.png # base64 的图片(给人看 / 存文件)
result.chart # BarChart 对象:.elements = [BarData(label="a", value=3), ...] ← 拆成了数据!
关键点: result.data 和 result.chart 里的东西,不是模型编出来的,是内核在你的
DataFrame / Figure 对象还活在内存里的时候,亲手把它拆出来的。对 AI agent 来说,这意味着
它拿到的是可信的结构化数据,不用再去 OCR 图片、也不用解析 print 出来的表格。
为什么要这么设计
给 agent 用代码解释器,最大的痛点是:模型看不见图,也读不懂乱七八糟的文本表格。
如果沙箱只回文本,agent 想「知道这张柱状图第二根柱子多高」就只能猜。E2B 的做法是——
在数据还是 Python 对象的时候就抽取结构,让 agent 直接拿到 value=7。
一句话直觉
把它想成内核里的一群「翻译官」:每当有个对象要被显示,IPython 就问在场的每个翻译官 「你能把它翻成你那门语言吗?」pandas 翻译官把 DataFrame 翻成
e2b/data列字典,matplotlib 翻译官把 Figure 翻成e2b/chart图表字典。这些「语言」就是 MIME 类型。
2. 顶层全景(富结果怎 么从对象变成客户端字段)
先理解一个 IPython 的基础机制:_repr_*_ 协议与 display_formatter。
- 在普通 Jupyter 里,一个对象能显示成 HTML,是因为它有个
_repr_html_()方法;能显示成图片, 是因为有_repr_png_()。IPython 显示一个对象时,会遍历所有已注册的格式器(formatter), 每个格式器对应一种 MIME 类型,挨个问对象「你支持我吗」,把支持的那些收集成一个 MIME bundle({"text/plain": ..., "text/html": ..., "image/png": ...})。 - E2B 的整个把戏就是:往这个格式器表里塞几个自定义 MIME 类型(
e2b/data、e2b/chart), 让 DataFrame / Figure 在被显示时,顺手也产出结构化数据。
一张图:从对象到客户端字段
怎么读:从上到下是一次显示的生命周期,左边是「东西是什么」,右边是「谁干的」。
用户代码最后一行: df (一个 pandas.DataFrame 还活在内核内存里)
│
▼
IPython display_formatter.format(df)
遍历所有已注册格式器,问 df「你支持吗」 ← IPython 内建机制
├─ text/plain → df 的 __repr__
├─ text/html → df._repr_html_()
├─ e2b/data → E2BDataFormatter(df) 把表转成列字典 ← 0002_data.py 注册的
└─ e2b/chart → E2BChartFormatter(df) 不是 Figure,跳过
│
▼
MIME bundle: {"text/plain":.., "text/html":.., "e2b/data": {"city":[..],"pop":[..]}}
│
▼ Jupyter execute_result / display_data 消息
│
沙箱内 FastAPI 服务 server/api/models/result.py Result.__init__
把每个 MIME key pop 进对应字段:
text/html → .html image/png → .png e2b/data → .data e2b/chart → .chart
│
▼ 流式 HTTP(见 01-sdk-client / 03-kernel-messaging)
│
客户端 SDK python/e2b_code_interpreter/models.py Result.__init__
同样把 dict 铺进字段;chart 字段额外 _deserialize_chart() → 带类型的 BarChart 对象
│
▼
你手里的 result.data / result.png / result.chart
部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
E2BDataFormatter | DataFrame → e2b/data 列字典 | template/startup_scripts/0002_data.py:8 |
E2BChartFormatter | matplotlib Figure → e2b/chart 图表字典 | template/startup_scripts/0002_data.py:33 |
E2BJSONFormatter | list/dict → JSON(处理 numpy / 非字符串键) | template/startup_scripts/0002_data.py:58 |
| 图片补丁 | Image.save 到路径时顺手 display;禁掉无显示报错 | template/startup_scripts/0003_images.py |
服务端 Result | MIME bundle → 结构化字段 | template/server/api/models/result.py:43 |
客户端 Result | 反序列化字段,chart 还原成类型对象 | python/e2b_code_interpreter/models.py:133 |
| 图表模型层级 | Chart/BarChart/… 带类型的数据结构 | python/e2b_code_interpreter/charts.py |
核心分工一句话: 内核侧格式器负责「抽」,服务端 Result 负责「铺进字段」,客户端负责「还原成类型」。
3. 核心原理(逐个机制,由浅入深)
3.1 IPython 的 _repr_*_ / display_formatter 机制
它要解决的小问题: 同一个对象,在不同前端要显示成不同东西(终端要纯文本、浏览器要 HTML、 记事本要图片)。IPython 用「一个对象 + 多个格式器」解决:每个格式器盯一种 MIME 类型。
思路: 每个格式器有两个约定:
format_type:它产出的 MIME 字符串(如"text/html")。print_method:它去对象上找的那个方法名(如_repr_html_)——对象有这方法就调它。
E2B 自定义格式器就是照这个套路,只是不依赖对象自带方法,而是在格式器的 __call__ 里
自己判断对象类型、自己抽数据。看 E2BDataFormatter 的声明:
# template/startup_scripts/0002_data.py:8 E2BDataFormatter
class E2BDataFormatter(BaseFormatter):
format_type = Unicode("e2b/data") # 产出这个自定义 MIME
print_method = ObjectName("_repr_e2b_data_")
_return_type = (dict, str)
注册这一步是把格式器塞进内核那张全局的「翻译官名册」:
# template/startup_scripts/0002_data.py:76 注册进 display_formatter
ip = IPython.get_ipython()
ip.display_formatter.formatters["e2b/data"] = E2BDataFormatter(parent=ip.display_formatter)
ip.display_formatter.formatters["e2b/chart"] = E2BChartFormatter(parent=ip.display_formatter)
ip.display_formatter.formatters["application/json"] = E2BJSONFormatter(parent=ip.display_formatter)
关键细节: 这段代码在内核启动时由 startup script 执行(文件名 0002_ 保证顺序),
所以之后用户跑的每一行代码,只要显示对象,都会自动过这几个格式器。用户完全无感。
0002_data.py/0003_images.py属于内核启动脚本,构建沙箱模板时被放进内核的 startup 目录。 「这台机器怎么装出来的」见「沙箱模板」。
3.2 E2BDataFormatter:把 DataFrame 拆成列字典
它要解决的小问题: 让 agent 拿到 DataFrame 的结构化内容,而不是一坨对齐的文本表。
思路 + 精妙点(sys.modules 门控): IPython 对每一个被显示的对象,都会调用每一个
格式器。也就是说,你跑 1 + 1,那个 2 也会被送进 E2BDataFormatter。如果格式器上来就
import pandas,那哪怕你从没用过 pandas,每次显示都要付一次 pandas 导入代价。E2B 用一个巧妙的
门控绕开:不 import,而是查 sys.modules——
# template/startup_scripts/0002_data.py:14 E2BDataFormatter.__call__
def __call__(self, obj):
pandas = sys.modules.get("pandas") # 没导入过就是 None,不触发导入
if pandas is None or not isinstance(obj, pandas.DataFrame):
return super().__call__(obj) # 不是我的菜,走默认(通常返回 None)
result = obj.to_dict(orient="list") # {列名: [值...]}
for key, value in result.items():
result[key] = [
v.isoformat() if isinstance(v, pandas.Timestamp) else v for v in value
] # Timestamp → ISO 字符串(可 JSON 化)
return result
逻辑分三层,一层一件事:
- 门控:
sys.modules.get("pandas")为None说明用户根本没 import pandas,那这对象不可能 是 DataFrame——直接放行。一个 DataFrame 能存在的前提就是 pandas 已被导入,所以这个判断 零漏报,又零导入成本。这是本章第一个精妙点。 - 抽取:
to_dict(orient="list")把表转成「列名 → 该列所有值」的字典。 - JSON 兼容: pandas 的
Timestamp不能直接 JSON 序列化,逐个转成isoformat()字符串。
结果去向: 这个 dict 成为 MIME bundle 里 e2b/data 键的值,最后落到 result.data。