Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/code-interpreter-python-extra-mime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/code-interpreter-python': patch
---

fix(code-interpreter): collect non-standard MIME types into `Result.extra` in the Python SDK — `Result.__init__` declared an `extra` field for data "not part of the standard types", but unknown keys from the kernel message were dropped via `**kwargs` instead of collected, so non-standard representations (e.g. `application/vnd.plotly.v1+json`, bokeh, vega-lite) were silently lost. They are now swept into `extra`, listed by `formats()`, and preserved by `Execution.to_json()`, matching the JS SDK's `Result` constructor. `serialize_results` reads extra keys from `Result.extra` rather than treating them as attributes.
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,14 @@ def __init__(
f"Error deserializing chart, check if you are using the latest version of the library: {e}"
)
self.is_main_result = is_main_result
self.extra = extra
# Collect non-standard MIME types (e.g. application/vnd.plotly.v1+json)
# into `extra`, matching the JS SDK's Result constructor, which sweeps
# every key outside its known set into `extra`. Without this the
# kernel's non-standard representations are silently dropped.
if kwargs:
self.extra = {**(extra or {}), **kwargs}
Comment on lines +174 to +175

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Plain metadata appears as a MIME format

When a result message includes plain, Result.extra retains it as a custom format. JS Result excludes plain, so Python now advertises a non-MIME key in formats().

Learn more

_parse_output passes every result message field into Result. The JS result constructor treats plain as a known field and skips it when collecting extra representations, but the Python sweep accepts it. formats() then reports plain and Execution.to_json() includes it among the result representations.

Example: For a result message {'type': 'result', 'text': 'hello', 'plain': 'hello'}, Python reports ['text', 'plain']; the JS result omits plain from extra and formats.

Recommended fix: Exclude protocol-only keys such as plain from kwargs before merging MIME representations into extra, matching the JS constructor's known-key list.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

else:
self.extra = extra

def formats(self) -> Iterable[str]:
"""
Expand Down Expand Up @@ -337,6 +344,8 @@ def serialize_results(results: List[Result]) -> List[Dict[str, str]]:
chart = result.chart
if chart is not None:
serialized_dict[key] = chart.to_dict()
elif result.extra is not None and key in result.extra:
serialized_dict[key] = result.extra[key]
Comment on lines +347 to +348

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-1 — JS, sync Python, and async Python must mirror each other 1:1 in semantics, not just names.

This flattens each non-standard MIME type into the top level of the serialized result ({"text": ..., "application/vnd.plotly.v1+json": ...}). JS Result.toJSON() (packages/code-interpreter-js/src/messaging.ts) nests them instead: ...(Object.keys(this.extra).length > 0 ? { extra: this.extra } : {}). So the same execution would serialize to two different shapes depending on the SDK, which goes against the PR's own goal of "matching the JS SDK". It also lets an arbitrary kernel MIME key share a namespace with the standard keys (text, html, …).

Use JS's nested shape:

Suggested change
elif result.extra is not None and key in result.extra:
serialized_dict[key] = result.extra[key]
elif result.extra is not None and key in result.extra:
serialized_dict.setdefault("extra", {})[key] = result.extra[key]

Comment on lines +347 to +348

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Extra HTML replaces the actual result

When extra contains html, serialize_results writes that value instead of result.html. The serialized execution contains different HTML from the result users received.

Learn more

Result.formats() lists standard fields and then keys in extra, including duplicate names. Previously, both passes through serialize_results read the standard attribute, so an explicit extra key could not replace the result's HTML. The new branch selects extra for both passes and overwrites the standard value. The same collision affects other standard fields besides text, which is reset later.

Example: Execution(results=[Result(html='<b>new</b>', extra={'html': '<i>old</i>'})]).to_json() now contains "html": "<i>old</i>", while result.html remains <b>new</b>.

Recommended fix: Keep standard fields authoritative in serialize_results, and only read result.extra[key] for keys not represented by standard fields; account for duplicate keys from formats().

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

else:
serialized_dict[key] = result[key]

Expand Down
74 changes: 74 additions & 0 deletions packages/code-interpreter-python/tests/test_result_extra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json

from e2b_code_interpreter.models import Execution, Result, parse_output


def _result_from(message: dict) -> Result:
execution = Execution()
parse_output(
execution, json.dumps({"type": "result", "is_main_result": True, **message})
)
return execution.results[0]


def test_collects_non_standard_mime_types_into_extra():
# The IPython kernel sends non-standard MIME types (e.g. plotly/bokeh) that
# aren't among the known representations. They should land in `extra`, like
# the JS SDK's Result constructor does — not be silently dropped.
result = _result_from(
{"text": "hello", "application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]}}
)

assert result.extra == {"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]}}


def test_formats_lists_non_standard_mime_types():
result = _result_from(
{"text": "hello", "application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]}}
)

assert "application/vnd.plotly.v1+json" in list(result.formats())


def test_to_json_preserves_non_standard_mime_types():
execution = Execution()
parse_output(
execution,
json.dumps(
{
"type": "result",
"is_main_result": True,
"text": "hello",
"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]},
}
),
)

assert json.loads(execution.to_json())["results"] == [
{
"text": "hello",
"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-1 (follow-up to the serialize_results comment) — this assertion locks in the flattened shape, which differs from JS Result.toJSON(). That method emits non-standard MIME types under an extra key. To match JS:

Suggested change
"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]},
"extra": {"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]}},

}
]


def test_result_without_extra_fields_has_no_extra():
result = _result_from({"text": "hello"})

assert result.extra is None
assert list(result.formats()) == ["text"]


def test_explicit_extra_and_unknown_kwargs_are_merged():
# A non-standard MIME type arriving via the parsed message and an explicit
# `extra` passed to the constructor should both be retained.
result = Result(
text="hello",
extra={"application/vnd.custom+json": {"y": 2}},
**{"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]}},
)

assert result.extra == {
"application/vnd.custom+json": {"y": 2},
"application/vnd.plotly.v1+json": {"data": [{"x": [1, 2]}]},
}