Skip to content

Commit 12bd18e

Browse files
committed
Update code runner with mime type in stdout
1 parent 2466c8a commit 12bd18e

File tree

6 files changed

+212
-140
lines changed

6 files changed

+212
-140
lines changed

llmstack/common/runner/proto/runner.proto

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
syntax = "proto3";
22
import "google/protobuf/struct.proto";
33

4+
enum ContentMimeType {
5+
TEXT = 0; // text
6+
JSON = 1; // json
7+
HTML = 2; // html
8+
PNG = 3; // png
9+
JPEG = 4; // jpeg
10+
SVG = 5; // svg
11+
PDF = 6; // pdf
12+
LATEX = 7; // latex
13+
}
14+
15+
message Content {
16+
ContentMimeType mime_type = 1; // mime type of content
17+
bytes data = 2; // data of content
18+
}
19+
420
enum BrowserCommandType {
521
GOTO = 0; // go to url
622
TERMINATE = 1; // terminate session
@@ -123,7 +139,7 @@ message RestrictedPythonCodeRunnerRequest {
123139
message RestrictedPythonCodeRunnerResponse {
124140
RemoteBrowserState state = 1; // state of runner
125141
google.protobuf.Struct local_variables = 2; // local variable after running code
126-
repeated string stdout = 3; // stdout of code
142+
repeated Content stdout = 3; // stdout of code
127143
string stderr = 4; // stderr of code
128144
int32 exit_code = 5; // exit code of code
129145
}

llmstack/common/runner/proto/runner_pb2.py

Lines changed: 45 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

llmstack/common/runner/proto/runner_pb2.pyi

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ COPY: BrowserCommandType
1010
DESCRIPTOR: _descriptor.FileDescriptor
1111
ENTER: BrowserCommandType
1212
GOTO: BrowserCommandType
13+
HTML: ContentMimeType
14+
JPEG: ContentMimeType
15+
JSON: ContentMimeType
16+
LATEX: ContentMimeType
17+
PDF: ContentMimeType
18+
PNG: ContentMimeType
1319
RUNNING: RemoteBrowserState
1420
SCROLL_X: BrowserCommandType
1521
SCROLL_Y: BrowserCommandType
22+
SVG: ContentMimeType
1623
TERMINATE: BrowserCommandType
1724
TERMINATED: RemoteBrowserState
25+
TEXT: ContentMimeType
1826
TIMEOUT: RemoteBrowserState
1927
TYPE: BrowserCommandType
2028
WAIT: BrowserCommandType
@@ -119,6 +127,14 @@ class BrowserTextAreaField(_message.Message):
119127
text: str
120128
def __init__(self, selector: _Optional[str] = ..., text: _Optional[str] = ...) -> None: ...
121129

130+
class Content(_message.Message):
131+
__slots__ = ["data", "mime_type"]
132+
DATA_FIELD_NUMBER: _ClassVar[int]
133+
MIME_TYPE_FIELD_NUMBER: _ClassVar[int]
134+
data: bytes
135+
mime_type: ContentMimeType
136+
def __init__(self, mime_type: _Optional[_Union[ContentMimeType, str]] = ..., data: _Optional[bytes] = ...) -> None: ...
137+
122138
class PlaywrightBrowserRequest(_message.Message):
123139
__slots__ = ["session_data", "steps", "stream_video", "timeout", "url"]
124140
SESSION_DATA_FIELD_NUMBER: _ClassVar[int]
@@ -200,8 +216,11 @@ class RestrictedPythonCodeRunnerResponse(_message.Message):
200216
local_variables: _struct_pb2.Struct
201217
state: RemoteBrowserState
202218
stderr: str
203-
stdout: _containers.RepeatedScalarFieldContainer[str]
204-
def __init__(self, state: _Optional[_Union[RemoteBrowserState, str]] = ..., local_variables: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., stdout: _Optional[_Iterable[str]] = ..., stderr: _Optional[str] = ..., exit_code: _Optional[int] = ...) -> None: ...
219+
stdout: _containers.RepeatedCompositeFieldContainer[Content]
220+
def __init__(self, state: _Optional[_Union[RemoteBrowserState, str]] = ..., local_variables: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., stdout: _Optional[_Iterable[_Union[Content, _Mapping]]] = ..., stderr: _Optional[str] = ..., exit_code: _Optional[int] = ...) -> None: ...
221+
222+
class ContentMimeType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
223+
__slots__ = []
205224

206225
class BrowserCommandType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
207226
__slots__ = []

llmstack/common/runner/server.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import re
77
import subprocess
8+
import time
89
from concurrent import futures
910
from typing import Iterator
1011

@@ -20,6 +21,8 @@
2021
from llmstack.common.runner.playwright.browser import Playwright
2122
from llmstack.common.runner.proto.runner_pb2 import (
2223
TERMINATE,
24+
Content,
25+
ContentMimeType,
2326
PlaywrightBrowserRequest,
2427
PlaywrightBrowserResponse,
2528
RemoteBrowserRequest,
@@ -194,13 +197,13 @@ def GetRestrictedPythonCodeRunner(
194197
) -> Iterator[RestrictedPythonCodeRunnerResponse]:
195198
from google.protobuf.json_format import MessageToDict, ParseDict
196199
from google.protobuf.struct_pb2 import Struct, Value
197-
from RestrictedPython import compile_restricted, safe_builtins
200+
from RestrictedPython import compile_restricted
198201
from RestrictedPython.Guards import (
199202
guarded_iter_unpack_sequence,
200203
guarded_unpack_sequence,
201204
safe_builtins,
202205
)
203-
from RestrictedPython.PrintCollector import PrintCollector
206+
from RestrictedPython.transformer import IOPERATOR_TO_STR
204207

205208
class CustomPrint(object):
206209
def __init__(self):
@@ -211,7 +214,12 @@ def write(self, text):
211214
if self.enabled:
212215
if text and text.strip():
213216
log_line = "{0}".format(text)
214-
self.lines.append(log_line)
217+
self.lines.append(
218+
(
219+
time.time(),
220+
Content(data=bytes(log_line.encode("utf-8")), mime_type=ContentMimeType.TEXT),
221+
)
222+
)
215223

216224
def enable(self):
217225
self.enabled = True
@@ -256,7 +264,6 @@ async def execute_restricted_code(source_code, input_data={}):
256264
mathplot_lib_display = []
257265

258266
def custom_pyplot_show():
259-
import base64
260267
import io
261268

262269
from matplotlib.backends.backend_agg import (
@@ -284,17 +291,10 @@ def custom_pyplot_show():
284291
# Rewind the buffer to start
285292
buf.seek(0)
286293

287-
# Encode the buffer contents to base64
288-
encoded = base64.b64encode(buf.read())
289-
290-
# Convert to string for a cleaner return format
291-
encoded_string = encoded.decode("utf-8")
294+
mathplot_lib_display.append((time.time(), Content(data=buf.read(), mime_type=ContentMimeType.PNG)))
292295

293296
# Cleanup by closing the buffer
294297
buf.close()
295-
mathplot_lib_display.append(f"data:image/png;base64,{encoded_string}")
296-
# Return the base64 string
297-
return encoded_string
298298

299299
def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
300300
module = __import__(name, globals, locals, fromlist, level)
@@ -341,7 +341,11 @@ def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
341341
if isinstance(v, (int, float, str, bool, list, dict, tuple, type(None), Value, Struct))
342342
}
343343
# Return the result and any printed output
344-
return {**local_variables, **{"display": mathplot_lib_display}}, custom_print.lines, errors
344+
return (
345+
local_variables,
346+
[x[1] for x in sorted(custom_print.lines + mathplot_lib_display, key=lambda x: x[0])],
347+
errors,
348+
)
345349

346350
yield RestrictedPythonCodeRunnerResponse(state=RemoteBrowserState.RUNNING)
347351

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from enum import Enum
2+
3+
from pydantic import BaseModel, Field
4+
5+
6+
class ContentMimeType(str, Enum):
7+
TEXT = "text/plain"
8+
JSON = "application/json"
9+
HTML = "text/html"
10+
PNG = "image/png"
11+
JPEG = "image/jpeg"
12+
SVG = "image/svg+xml"
13+
PDF = "application/pdf"
14+
LATEX = "application/x-latex"
15+
16+
def __str__(self):
17+
return self.value
18+
19+
20+
class Content(BaseModel):
21+
data: str = Field(description="The content data")
22+
mime_type: ContentMimeType = Field(description="The content mime type", default=ContentMimeType.TEXT)

0 commit comments

Comments
 (0)