Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/runtimeuse-client-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Issues = "https://github.com/getlark/runtimeuse/issues"

[project.optional-dependencies]
dev = [
"boto3",
"daytona",
"e2b",
"e2b-code-interpreter",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,11 @@ class QueryOptions:
timeout: float | None = None
#: Logger instance; falls back to the module-level logger when ``None``.
logger: logging.Logger | None = None

def __post_init__(self) -> None:
has_dir = self.artifacts_dir is not None
has_cb = self.on_artifact_upload_request is not None
if has_dir != has_cb:
raise ValueError(
"artifacts_dir and on_artifact_upload_request must be specified together"
)
50 changes: 50 additions & 0 deletions packages/runtimeuse-client-python/test/e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import signal
import socket
import subprocess
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -87,3 +89,51 @@ def query_options() -> QueryOptions:
@pytest.fixture
def make_query_options():
return _make_query_options


@pytest.fixture
def http_server():
"""Local HTTP server for serving downloadable files and receiving artifact uploads.

Yields (base_url, files, uploads) where:
- files: dict[str, bytes] — seed with content before the test runs a query
- uploads: dict[str, bytes] — populated by PUT requests during the test
"""
uploads: dict[str, bytes] = {}
files: dict[str, bytes] = {}

class _Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path.startswith("/files/"):
name = self.path[len("/files/"):]
if name in files:
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.end_headers()
self.wfile.write(files[name])
return
self.send_response(404)
self.end_headers()

def do_PUT(self) -> None:
if self.path.startswith("/uploads/"):
name = self.path[len("/uploads/"):]
length = int(self.headers.get("Content-Length", 0))
uploads[name] = self.rfile.read(length)
self.send_response(200)
self.end_headers()
return
self.send_response(404)
self.end_headers()

def log_message(self, format: str, *args: Any) -> None:
pass

server = HTTPServer(("127.0.0.1", 0), _Handler)
port = server.server_address[1]
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()

yield f"http://127.0.0.1:{port}", files, uploads

server.shutdown()
29 changes: 29 additions & 0 deletions packages/runtimeuse-client-python/test/e2e/echo_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
* STRUCTURED:<json> — return structured_output result
* SLOW:<ms> — sleep then return text (timeout / cancel tests)
* STREAM:<n> — send n assistant messages before returning
* STREAM_TEXT:<text> — send text as assistant message, then return "done"
* ERROR:<msg> — send error via sender and throw
* WRITE_FILE:<path> <c> — write file, sleep 3s for chokidar, return text
* READ_FILE:<path> — read file and return its contents as text
* (anything else) — echo the prompt back as text
*/

Expand Down Expand Up @@ -40,6 +43,32 @@ export const handler = {
return { type: "text", text: `streamed ${count} messages` };
}

if (prompt.startsWith("STREAM_TEXT:")) {
const text = prompt.slice("STREAM_TEXT:".length);
sender.sendAssistantMessage([text]);
return { type: "text", text: "done" };
}

if (prompt.startsWith("WRITE_FILE:")) {
const rest = prompt.slice("WRITE_FILE:".length);
const spaceIdx = rest.indexOf(" ");
const filePath = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const content = spaceIdx === -1 ? "" : rest.slice(spaceIdx + 1);
const fs = await import("fs");
const path = await import("path");
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
await new Promise((r) => setTimeout(r, 3000));
return { type: "text", text: `wrote ${filePath}` };
}

if (prompt.startsWith("READ_FILE:")) {
const filePath = prompt.slice("READ_FILE:".length).trim();
const fs = await import("fs");
const content = fs.readFileSync(filePath, "utf-8");
return { type: "text", text: content };
}

if (prompt.startsWith("ERROR:")) {
const msg = prompt.slice("ERROR:".length);
sender.sendErrorMessage(msg, { source: "echo_handler" });
Expand Down
Loading