@chat-js/cli 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1173 -964
- package/package.json +1 -1
- package/templates/chat-app/app/(auth)/device-login/page.tsx +37 -0
- package/templates/chat-app/app/(auth)/login/page.tsx +26 -2
- package/templates/chat-app/app/(auth)/register/page.tsx +0 -12
- package/templates/chat-app/app/(chat)/api/chat/filter-reasoning-parts.ts +1 -1
- package/templates/chat-app/app/(chat)/api/chat/prepare/route.ts +94 -0
- package/templates/chat-app/app/(chat)/api/chat/route.ts +107 -16
- package/templates/chat-app/app/(chat)/layout.tsx +4 -1
- package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
- package/templates/chat-app/app/globals.css +9 -9
- package/templates/chat-app/app/layout.tsx +4 -2
- package/templates/chat-app/biome.jsonc +3 -3
- package/templates/chat-app/chat.config.ts +32 -12
- package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
- package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
- package/templates/chat-app/components/artifact-actions.tsx +81 -18
- package/templates/chat-app/components/artifact-panel.tsx +142 -41
- package/templates/chat-app/components/attachment-list.tsx +1 -1
- package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
- package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
- package/templates/chat-app/components/chat-menu-items.tsx +1 -1
- package/templates/chat-app/components/chat-sync.tsx +9 -11
- package/templates/chat-app/components/console.tsx +9 -9
- package/templates/chat-app/components/context-usage.tsx +2 -2
- package/templates/chat-app/components/create-artifact.tsx +15 -5
- package/templates/chat-app/components/data-stream-handler.tsx +57 -16
- package/templates/chat-app/components/device-login-page.tsx +191 -0
- package/templates/chat-app/components/diffview.tsx +8 -2
- package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
- package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
- package/templates/chat-app/components/favicon-group.tsx +1 -1
- package/templates/chat-app/components/feedback-actions.tsx +7 -3
- package/templates/chat-app/components/greeting.tsx +1 -1
- package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
- package/templates/chat-app/components/interactive-charts.tsx +1 -1
- package/templates/chat-app/components/login-form.tsx +52 -10
- package/templates/chat-app/components/message-editor.tsx +7 -3
- package/templates/chat-app/components/message-siblings.tsx +14 -1
- package/templates/chat-app/components/model-selector.tsx +295 -27
- package/templates/chat-app/components/multimodal-input.tsx +259 -22
- package/templates/chat-app/components/parallel-response-cards.tsx +175 -0
- package/templates/chat-app/components/part/code-execution.tsx +8 -2
- package/templates/chat-app/components/part/document-common.tsx +1 -1
- package/templates/chat-app/components/part/document-preview.tsx +5 -5
- package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
- package/templates/chat-app/components/part/text-message-part.tsx +9 -1
- package/templates/chat-app/components/project-chat-item.tsx +1 -1
- package/templates/chat-app/components/project-menu-items.tsx +1 -1
- package/templates/chat-app/components/research-task.tsx +1 -1
- package/templates/chat-app/components/research-tasks.tsx +1 -1
- package/templates/chat-app/components/retry-button.tsx +25 -8
- package/templates/chat-app/components/sandbox.tsx +1 -1
- package/templates/chat-app/components/sheet-editor.tsx +7 -7
- package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
- package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
- package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
- package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
- package/templates/chat-app/components/signup-form.tsx +49 -10
- package/templates/chat-app/components/sources.tsx +4 -4
- package/templates/chat-app/components/text-editor.tsx +5 -2
- package/templates/chat-app/components/toolbar.tsx +3 -3
- package/templates/chat-app/components/ui/sidebar.tsx +0 -1
- package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
- package/templates/chat-app/components/user-message.tsx +14 -2
- package/templates/chat-app/electron.d.ts +41 -0
- package/templates/chat-app/evals/my-eval.eval.ts +3 -1
- package/templates/chat-app/hooks/chat-sync-hooks.ts +11 -0
- package/templates/chat-app/hooks/use-artifact.tsx +13 -13
- package/templates/chat-app/hooks/use-navigate-to-message.ts +39 -0
- package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
- package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
- package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
- package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
- package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
- package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
- package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
- package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
- package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
- package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
- package/templates/chat-app/lib/ai/types.ts +74 -3
- package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
- package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
- package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
- package/templates/chat-app/lib/auth-client.ts +23 -1
- package/templates/chat-app/lib/auth.ts +18 -1
- package/templates/chat-app/lib/blob.ts +1 -1
- package/templates/chat-app/lib/clone-messages.ts +1 -1
- package/templates/chat-app/lib/config-schema.ts +18 -1
- package/templates/chat-app/lib/constants.ts +3 -4
- package/templates/chat-app/lib/db/migrations/0044_gray_red_shift.sql +5 -0
- package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +1480 -0
- package/templates/chat-app/lib/db/migrations/meta/_journal.json +7 -0
- package/templates/chat-app/lib/db/queries.ts +84 -4
- package/templates/chat-app/lib/db/schema.ts +4 -1
- package/templates/chat-app/lib/editor/config.ts +4 -4
- package/templates/chat-app/lib/electron-auth.ts +96 -0
- package/templates/chat-app/lib/env-schema.ts +33 -4
- package/templates/chat-app/lib/message-conversion.ts +14 -2
- package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
- package/templates/chat-app/lib/social-auth.ts +5 -0
- package/templates/chat-app/lib/stores/hooks-threads.ts +38 -1
- package/templates/chat-app/lib/stores/with-threads.test.ts +137 -0
- package/templates/chat-app/lib/stores/with-threads.ts +159 -7
- package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
- package/templates/chat-app/lib/thread-utils.ts +22 -3
- package/templates/chat-app/lib/utils/download-assets.ts +6 -7
- package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
- package/templates/chat-app/package.json +20 -18
- package/templates/chat-app/playwright.config.ts +0 -19
- package/templates/chat-app/providers/chat-input-provider.tsx +40 -2
- package/templates/chat-app/proxy.ts +28 -3
- package/templates/chat-app/scripts/check-env.ts +10 -0
- package/templates/chat-app/scripts/db-branch-delete.sh +7 -1
- package/templates/chat-app/scripts/db-branch-use.sh +7 -1
- package/templates/chat-app/scripts/with-db.sh +7 -1
- package/templates/chat-app/trpc/server.tsx +7 -2
- package/templates/chat-app/tsconfig.json +2 -1
- package/templates/chat-app/vercel.json +0 -10
- package/templates/chat-app/vitest.config.ts +2 -0
- package/templates/electron/CHANGELOG.md +7 -0
- package/templates/electron/README.md +54 -0
- package/templates/electron/entitlements.mac.plist +10 -0
- package/templates/electron/forge.config.ts +157 -0
- package/templates/electron/icon.png +0 -0
- package/templates/electron/package.json +53 -0
- package/templates/electron/scripts/generate-icons.test.js +37 -0
- package/templates/electron/scripts/generate-icons.ts +29 -0
- package/templates/electron/scripts/run-forge.cjs +28 -0
- package/templates/electron/scripts/write-branding.ts +18 -0
- package/templates/electron/src/config.ts +16 -0
- package/templates/electron/src/lib/auth-client.ts +64 -0
- package/templates/electron/src/main.ts +670 -0
- package/templates/electron/src/preload.d.ts +27 -0
- package/templates/electron/src/preload.ts +25 -0
- package/templates/electron/tsconfig.json +18 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { Sandbox } from "@vercel/sandbox";
|
|
2
|
+
import type {
|
|
3
|
+
CodeExecutionContext,
|
|
4
|
+
CodeExecutionResult,
|
|
5
|
+
} from "./code-execution.types";
|
|
6
|
+
|
|
7
|
+
const WHITESPACE_REGEX = /\s+/;
|
|
8
|
+
const PACKAGE_SPEC_SPLIT_RE = /[=<>![\s]/;
|
|
9
|
+
const CHART_JSON_PREFIX = "__CHART_JSON__:";
|
|
10
|
+
|
|
11
|
+
function packageName(spec: string): string {
|
|
12
|
+
return spec.split(PACKAGE_SPEC_SPLIT_RE)[0].toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function installBasePackages(
|
|
16
|
+
sandbox: Sandbox,
|
|
17
|
+
basePackages: readonly string[],
|
|
18
|
+
requestId: string,
|
|
19
|
+
log: CodeExecutionContext["log"]
|
|
20
|
+
): Promise<{
|
|
21
|
+
success: boolean;
|
|
22
|
+
result?: CodeExecutionResult;
|
|
23
|
+
}> {
|
|
24
|
+
const installStep = await sandbox.runCommand({
|
|
25
|
+
cmd: "pip",
|
|
26
|
+
args: ["install", ...basePackages],
|
|
27
|
+
});
|
|
28
|
+
if (installStep.exitCode !== 0) {
|
|
29
|
+
const installStderr = await installStep.stderr();
|
|
30
|
+
log.error(
|
|
31
|
+
{ requestId, stderr: installStderr },
|
|
32
|
+
"base package installation failed"
|
|
33
|
+
);
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
result: {
|
|
37
|
+
message: `Failed to install base packages: ${installStderr}`,
|
|
38
|
+
chart: "",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
log.info({ requestId }, "base packages installed");
|
|
43
|
+
return { success: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function processExtraPackages(
|
|
47
|
+
code: string,
|
|
48
|
+
basePackages: readonly string[],
|
|
49
|
+
sandbox: Sandbox,
|
|
50
|
+
requestId: string,
|
|
51
|
+
log: CodeExecutionContext["log"]
|
|
52
|
+
): Promise<{
|
|
53
|
+
codeToRun: string;
|
|
54
|
+
installResult: {
|
|
55
|
+
success: boolean;
|
|
56
|
+
result?: CodeExecutionResult;
|
|
57
|
+
};
|
|
58
|
+
}> {
|
|
59
|
+
const basePackageNames = new Set(basePackages.map((p) => p.toLowerCase()));
|
|
60
|
+
const lines = code.split("\n");
|
|
61
|
+
const pipLines = lines.filter((line) =>
|
|
62
|
+
line.trim().startsWith("!pip install ")
|
|
63
|
+
);
|
|
64
|
+
const extraPackages = pipLines
|
|
65
|
+
.flatMap((line) =>
|
|
66
|
+
line
|
|
67
|
+
.trim()
|
|
68
|
+
.slice("!pip install ".length)
|
|
69
|
+
.split(WHITESPACE_REGEX)
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
)
|
|
72
|
+
.filter((spec) => !basePackageNames.has(packageName(spec)));
|
|
73
|
+
|
|
74
|
+
const codeWithoutPipLines = lines
|
|
75
|
+
.filter((line) => !line.trim().startsWith("!pip install "))
|
|
76
|
+
.join("\n");
|
|
77
|
+
|
|
78
|
+
if (extraPackages.length === 0) {
|
|
79
|
+
return { codeToRun: codeWithoutPipLines, installResult: { success: true } };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
log.info({ requestId, extraPackages }, "installing extra packages");
|
|
83
|
+
const dynamicInstall = await sandbox.runCommand({
|
|
84
|
+
cmd: "pip",
|
|
85
|
+
args: ["install", ...extraPackages],
|
|
86
|
+
});
|
|
87
|
+
if (dynamicInstall.exitCode !== 0) {
|
|
88
|
+
const stderr = await dynamicInstall.stderr();
|
|
89
|
+
log.error({ requestId, stderr }, "dynamic package installation failed");
|
|
90
|
+
return {
|
|
91
|
+
codeToRun: code,
|
|
92
|
+
installResult: {
|
|
93
|
+
success: false,
|
|
94
|
+
result: {
|
|
95
|
+
message: `Failed to install packages: ${stderr}`,
|
|
96
|
+
chart: "",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
codeToRun: codeWithoutPipLines,
|
|
104
|
+
installResult: { success: true },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createWrappedCode(codeToRun: string, chartPath: string): string {
|
|
109
|
+
return `
|
|
110
|
+
import sys
|
|
111
|
+
import json
|
|
112
|
+
import traceback
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
import matplotlib.pyplot as _plt_module
|
|
116
|
+
_orig_savefig = _plt_module.savefig
|
|
117
|
+
def _intercepted_savefig(*args, **kwargs):
|
|
118
|
+
_orig_savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
|
|
119
|
+
_user_target = args[0] if args else kwargs.get('fname')
|
|
120
|
+
if _user_target not in (None, '${chartPath}'):
|
|
121
|
+
return _orig_savefig(*args, **kwargs)
|
|
122
|
+
_plt_module.savefig = _intercepted_savefig
|
|
123
|
+
except ImportError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
exec(${JSON.stringify(codeToRun)})
|
|
128
|
+
try:
|
|
129
|
+
_locals = locals()
|
|
130
|
+
_globals = globals()
|
|
131
|
+
_chart_var = _locals.get("chart") or _globals.get("chart")
|
|
132
|
+
if (isinstance(_chart_var, dict)
|
|
133
|
+
and isinstance(_chart_var.get("type"), str)
|
|
134
|
+
and isinstance(_chart_var.get("elements"), list)):
|
|
135
|
+
print("__CHART_JSON__:" + json.dumps(_chart_var))
|
|
136
|
+
else:
|
|
137
|
+
if "result" in _locals:
|
|
138
|
+
print(_locals["result"])
|
|
139
|
+
elif "result" in _globals:
|
|
140
|
+
print(_globals["result"])
|
|
141
|
+
elif "results" in _locals:
|
|
142
|
+
print(_locals["results"])
|
|
143
|
+
elif "results" in _globals:
|
|
144
|
+
print(_globals["results"])
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
try:
|
|
148
|
+
import matplotlib.pyplot as plt
|
|
149
|
+
if plt.get_fignums():
|
|
150
|
+
plt.savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
|
|
151
|
+
plt.close('all')
|
|
152
|
+
except ImportError:
|
|
153
|
+
pass
|
|
154
|
+
print(json.dumps({"success": True}))
|
|
155
|
+
except Exception as e:
|
|
156
|
+
error_info = {"success": False, "error": {"name": type(e).__name__, "value": str(e), "traceback": traceback.format_exc()}}
|
|
157
|
+
print(json.dumps(error_info))
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function parseExecutionOutput(execResult: {
|
|
163
|
+
stdout: () => Promise<string>;
|
|
164
|
+
exitCode: number;
|
|
165
|
+
}): Promise<{
|
|
166
|
+
outputText: string;
|
|
167
|
+
chartData: Record<string, unknown> | null;
|
|
168
|
+
execInfo: {
|
|
169
|
+
success: boolean;
|
|
170
|
+
error?: { name: string; value: string; traceback: string };
|
|
171
|
+
};
|
|
172
|
+
}> {
|
|
173
|
+
const stdout = await execResult.stdout();
|
|
174
|
+
let execInfo: {
|
|
175
|
+
success: boolean;
|
|
176
|
+
error?: { name: string; value: string; traceback: string };
|
|
177
|
+
} = { success: true };
|
|
178
|
+
let outputText = "";
|
|
179
|
+
let chartData: Record<string, unknown> | null = null;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const outLines = (stdout ?? "").trim().split("\n");
|
|
183
|
+
const lastLine = outLines.at(-1);
|
|
184
|
+
execInfo = JSON.parse(lastLine ?? "{}");
|
|
185
|
+
outLines.pop();
|
|
186
|
+
|
|
187
|
+
const chartLineIdx = outLines.findIndex((line) =>
|
|
188
|
+
line.startsWith(CHART_JSON_PREFIX)
|
|
189
|
+
);
|
|
190
|
+
if (chartLineIdx !== -1) {
|
|
191
|
+
const raw = outLines[chartLineIdx].slice(CHART_JSON_PREFIX.length);
|
|
192
|
+
try {
|
|
193
|
+
chartData = JSON.parse(raw) as Record<string, unknown>;
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore malformed chart JSON from the sandboxed snippet.
|
|
196
|
+
}
|
|
197
|
+
outLines.splice(chartLineIdx, 1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
outputText = outLines.join("\n");
|
|
201
|
+
} catch {
|
|
202
|
+
outputText = stdout ?? "";
|
|
203
|
+
if (execResult.exitCode !== 0) {
|
|
204
|
+
execInfo = {
|
|
205
|
+
success: false,
|
|
206
|
+
error: {
|
|
207
|
+
name: "SandboxExecutionError",
|
|
208
|
+
value: "Execution completed without a parsable status trailer",
|
|
209
|
+
traceback: "",
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { outputText, chartData, execInfo };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function checkForChart(
|
|
219
|
+
sandbox: Sandbox,
|
|
220
|
+
chartPath: string,
|
|
221
|
+
requestId: string,
|
|
222
|
+
log: CodeExecutionContext["log"]
|
|
223
|
+
): Promise<{ base64: string; format: string } | undefined> {
|
|
224
|
+
const chartCheck = await sandbox.runCommand({
|
|
225
|
+
cmd: "test",
|
|
226
|
+
args: ["-f", chartPath],
|
|
227
|
+
});
|
|
228
|
+
if (chartCheck.exitCode === 0) {
|
|
229
|
+
const b64 = await (
|
|
230
|
+
await sandbox.runCommand({
|
|
231
|
+
cmd: "base64",
|
|
232
|
+
args: ["-w", "0", chartPath],
|
|
233
|
+
})
|
|
234
|
+
).stdout();
|
|
235
|
+
log.info({ requestId }, "chart generated");
|
|
236
|
+
return { base64: (b64 ?? "").trim(), format: "png" };
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildResponseMessage({
|
|
242
|
+
outputText,
|
|
243
|
+
stderr,
|
|
244
|
+
execInfo,
|
|
245
|
+
log,
|
|
246
|
+
requestId,
|
|
247
|
+
}: {
|
|
248
|
+
outputText: string;
|
|
249
|
+
stderr: string;
|
|
250
|
+
execInfo: {
|
|
251
|
+
success: boolean;
|
|
252
|
+
error?: { name: string; value: string; traceback: string };
|
|
253
|
+
};
|
|
254
|
+
log: CodeExecutionContext["log"];
|
|
255
|
+
requestId: string;
|
|
256
|
+
}): string {
|
|
257
|
+
let message = "";
|
|
258
|
+
|
|
259
|
+
if (outputText) {
|
|
260
|
+
message += `${outputText}\n`;
|
|
261
|
+
}
|
|
262
|
+
if (stderr && stderr.trim().length > 0) {
|
|
263
|
+
message += `${stderr}\n`;
|
|
264
|
+
}
|
|
265
|
+
if (execInfo.error) {
|
|
266
|
+
message += `Error: ${execInfo.error.name}: ${execInfo.error.value}\n`;
|
|
267
|
+
log.error({ requestId, error: execInfo.error }, "python execution error");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return message;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function executePythonInSandbox({
|
|
274
|
+
sandbox,
|
|
275
|
+
code,
|
|
276
|
+
log,
|
|
277
|
+
requestId,
|
|
278
|
+
}: CodeExecutionContext): Promise<CodeExecutionResult> {
|
|
279
|
+
const basePackages = [
|
|
280
|
+
"matplotlib",
|
|
281
|
+
"pandas",
|
|
282
|
+
"numpy",
|
|
283
|
+
"sympy",
|
|
284
|
+
"yfinance",
|
|
285
|
+
] as const;
|
|
286
|
+
const chartPath = "/tmp/chart.png";
|
|
287
|
+
|
|
288
|
+
const baseInstallResult = await installBasePackages(
|
|
289
|
+
sandbox,
|
|
290
|
+
basePackages,
|
|
291
|
+
requestId,
|
|
292
|
+
log
|
|
293
|
+
);
|
|
294
|
+
if (!baseInstallResult.success) {
|
|
295
|
+
return baseInstallResult.result ?? { message: "Unknown error", chart: "" };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { codeToRun, installResult } = await processExtraPackages(
|
|
299
|
+
code,
|
|
300
|
+
basePackages,
|
|
301
|
+
sandbox,
|
|
302
|
+
requestId,
|
|
303
|
+
log
|
|
304
|
+
);
|
|
305
|
+
if (!installResult.success) {
|
|
306
|
+
return installResult.result ?? { message: "Unknown error", chart: "" };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const wrappedCode = createWrappedCode(codeToRun, chartPath);
|
|
310
|
+
const execResult = await sandbox.runCommand({
|
|
311
|
+
cmd: "python3",
|
|
312
|
+
args: ["-c", wrappedCode],
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const { outputText, chartData, execInfo } =
|
|
316
|
+
await parseExecutionOutput(execResult);
|
|
317
|
+
|
|
318
|
+
const message = buildResponseMessage({
|
|
319
|
+
outputText,
|
|
320
|
+
stderr: await execResult.stderr(),
|
|
321
|
+
execInfo,
|
|
322
|
+
log,
|
|
323
|
+
requestId,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (chartData) {
|
|
327
|
+
log.info({ requestId }, "interactive chart data returned");
|
|
328
|
+
return { message: message.trim(), chart: chartData };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const chartOut = await checkForChart(sandbox, chartPath, requestId, log);
|
|
332
|
+
return {
|
|
333
|
+
message: message.trim(),
|
|
334
|
+
chart: chartOut ?? "",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const envMock: {
|
|
4
|
+
VERCEL_SANDBOX_RUNTIME: string | undefined;
|
|
5
|
+
VERCEL_SANDBOX_RUNTIME_PYTHON: string | undefined;
|
|
6
|
+
VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: string | undefined;
|
|
7
|
+
} = {
|
|
8
|
+
VERCEL_SANDBOX_RUNTIME: undefined,
|
|
9
|
+
VERCEL_SANDBOX_RUNTIME_PYTHON: undefined,
|
|
10
|
+
VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: undefined,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
vi.mock("@/lib/env", () => ({
|
|
14
|
+
env: envMock,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe("getSandboxRuntime", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
envMock.VERCEL_SANDBOX_RUNTIME = undefined;
|
|
20
|
+
envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = undefined;
|
|
21
|
+
envMock.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT = undefined;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("uses Python defaults when no override is set", async () => {
|
|
25
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
26
|
+
|
|
27
|
+
expect(getSandboxRuntime("python")).toBe("python3.13");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("uses JavaScript defaults when no override is set", async () => {
|
|
31
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
32
|
+
|
|
33
|
+
expect(getSandboxRuntime("javascript")).toBe("node22");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("honors VERCEL_SANDBOX_RUNTIME_PYTHON override for python", async () => {
|
|
37
|
+
envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = "python3.12";
|
|
38
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
39
|
+
|
|
40
|
+
expect(getSandboxRuntime("python")).toBe("python3.12");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("honors VERCEL_SANDBOX_RUNTIME_JAVASCRIPT override for javascript", async () => {
|
|
44
|
+
envMock.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT = "node20";
|
|
45
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
46
|
+
|
|
47
|
+
expect(getSandboxRuntime("javascript")).toBe("node20");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("falls back to legacy VERCEL_SANDBOX_RUNTIME for python", async () => {
|
|
51
|
+
envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
|
|
52
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
53
|
+
|
|
54
|
+
expect(getSandboxRuntime("python")).toBe("python3.11");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("prefers VERCEL_SANDBOX_RUNTIME_PYTHON over legacy VERCEL_SANDBOX_RUNTIME", async () => {
|
|
58
|
+
envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = "python3.12";
|
|
59
|
+
envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
|
|
60
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
61
|
+
|
|
62
|
+
expect(getSandboxRuntime("python")).toBe("python3.12");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not use legacy VERCEL_SANDBOX_RUNTIME for javascript", async () => {
|
|
66
|
+
envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
|
|
67
|
+
const { getSandboxRuntime } = await import("./code-execution.shared");
|
|
68
|
+
|
|
69
|
+
expect(getSandboxRuntime("javascript")).toBe("node22");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
2
|
+
import { env } from "@/lib/env";
|
|
3
|
+
import type { createModuleLogger } from "@/lib/logger";
|
|
4
|
+
import type { SupportedExecutionLanguage } from "./code-execution.types";
|
|
5
|
+
|
|
6
|
+
export function getTokenAuth(): Record<string, string> {
|
|
7
|
+
const { VERCEL_TEAM_ID, VERCEL_PROJECT_ID, VERCEL_TOKEN } = env;
|
|
8
|
+
if (VERCEL_TEAM_ID && VERCEL_PROJECT_ID && VERCEL_TOKEN) {
|
|
9
|
+
return {
|
|
10
|
+
teamId: VERCEL_TEAM_ID,
|
|
11
|
+
projectId: VERCEL_PROJECT_ID,
|
|
12
|
+
token: VERCEL_TOKEN,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSandboxRuntime(
|
|
19
|
+
language: SupportedExecutionLanguage
|
|
20
|
+
): string {
|
|
21
|
+
if (language === "javascript") {
|
|
22
|
+
return env.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT ?? "node22";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
env.VERCEL_SANDBOX_RUNTIME_PYTHON ??
|
|
27
|
+
env.VERCEL_SANDBOX_RUNTIME ??
|
|
28
|
+
"python3.13"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createSandbox(runtime: string): Promise<Sandbox> {
|
|
33
|
+
return Sandbox.create({
|
|
34
|
+
runtime,
|
|
35
|
+
timeout: 5 * 60 * 1000,
|
|
36
|
+
resources: { vcpus: 2 },
|
|
37
|
+
...getTokenAuth(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function cleanupSandbox(
|
|
42
|
+
sandbox: Sandbox | undefined,
|
|
43
|
+
log: ReturnType<typeof createModuleLogger>,
|
|
44
|
+
requestId: string
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
if (!sandbox) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await sandbox.stop();
|
|
51
|
+
log.info({ requestId }, "sandbox closed");
|
|
52
|
+
} catch (closeErr) {
|
|
53
|
+
log.warn({ requestId, closeErr }, "failed to close sandbox");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getErrorMessage(err: unknown): string {
|
|
58
|
+
return err instanceof Error ? err.message : "Unknown error";
|
|
59
|
+
}
|