mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 15:03:35 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 163492f189 | |||
| a84743c576 | |||
| fc5e56efa5 | |||
| a7a6592441 |
@@ -1,5 +1,14 @@
|
||||
# create-llama
|
||||
|
||||
## 0.3.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a84743c: Change --agents paramameter to --use-case
|
||||
- a84743c: Add LlamaCloud support for Reflex templates
|
||||
- a7a6592: Fix the npm issue on the full-stack Python template
|
||||
- fc5e56e: bump: code interpreter v1
|
||||
|
||||
## 0.3.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ export async function createApp({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function createApp({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
};
|
||||
|
||||
// Install backend
|
||||
|
||||
@@ -18,10 +18,10 @@ const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateAgents = ["financial_report", "blog", "form_filling"];
|
||||
const templateUseCases = ["financial_report", "blog", "form_filling"];
|
||||
|
||||
for (const agents of templateAgents) {
|
||||
test.describe(`Test multiagent template ${agents} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
for (const useCase of templateUseCases) {
|
||||
test.describe(`Test multiagent template ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
test.skip(
|
||||
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
@@ -46,7 +46,7 @@ for (const agents of templateAgents) {
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
agents,
|
||||
useCase,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
@@ -71,8 +71,8 @@ for (const agents of templateAgents) {
|
||||
}) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
agents === "financial_report" ||
|
||||
agents === "form_filling" ||
|
||||
useCase === "financial_report" ||
|
||||
useCase === "form_filling" ||
|
||||
templateFramework === "express",
|
||||
"Skip chat tests for financial report and form filling.",
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { TemplateAgents, TemplateFramework } from "../../helpers";
|
||||
import { TemplateFramework, TemplateUseCase } from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
@@ -12,7 +12,7 @@ const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateAgents: TemplateAgents[] = ["extractor", "contract_review"];
|
||||
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
|
||||
|
||||
// The reflex template currently only works with FastAPI and files (and not on Windows)
|
||||
if (
|
||||
@@ -20,8 +20,8 @@ if (
|
||||
templateFramework === "fastapi" &&
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
for (const agents of templateAgents) {
|
||||
test.describe(`Test reflex template ${agents} ${templateFramework} ${dataSource}`, async () => {
|
||||
for (const useCase of templateUseCases) {
|
||||
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
|
||||
let appPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
@@ -39,7 +39,7 @@ if (
|
||||
vectorDb: "none",
|
||||
port: appPort,
|
||||
postInstallAction: "runApp",
|
||||
agents,
|
||||
useCase,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
|
||||
+4
-4
@@ -33,7 +33,7 @@ export type RunCreateLlamaOptions = {
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
agents?: string;
|
||||
useCase?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
@@ -51,7 +51,7 @@ export async function runCreateLlama({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
|
||||
throw new Error(
|
||||
@@ -113,8 +113,8 @@ export async function runCreateLlama({
|
||||
if (observability) {
|
||||
commandArgs.push("--observability", observability);
|
||||
}
|
||||
if ((templateType === "multiagent" || templateType === "reflex") && agents) {
|
||||
commandArgs.push("--agents", agents);
|
||||
if ((templateType === "multiagent" || templateType === "reflex") && useCase) {
|
||||
commandArgs.push("--use-case", useCase);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
|
||||
@@ -470,11 +470,11 @@ const getSystemPromptEnv = (
|
||||
});
|
||||
|
||||
const systemPrompt =
|
||||
"'" +
|
||||
'"' +
|
||||
DEFAULT_SYSTEM_PROMPT +
|
||||
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
|
||||
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
|
||||
"'";
|
||||
'"';
|
||||
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_PROMPT",
|
||||
|
||||
+15
-20
@@ -380,28 +380,32 @@ export const installPythonDependencies = (
|
||||
};
|
||||
|
||||
export const installPythonTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
modelConfig,
|
||||
dataSources,
|
||||
tools,
|
||||
postInstallAction,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
observability,
|
||||
modelConfig,
|
||||
agents,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "appName"
|
||||
| "root"
|
||||
| "framework"
|
||||
| "template"
|
||||
| "framework"
|
||||
| "vectorDb"
|
||||
| "postInstallAction"
|
||||
| "modelConfig"
|
||||
| "dataSources"
|
||||
| "tools"
|
||||
| "postInstallAction"
|
||||
| "useLlamaParse"
|
||||
| "useCase"
|
||||
| "observability"
|
||||
| "modelConfig"
|
||||
| "agents"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
let templatePath;
|
||||
@@ -476,21 +480,12 @@ export const installPythonTemplate = async ({
|
||||
await copyRouterCode(root, tools ?? []);
|
||||
}
|
||||
|
||||
if (template === "multiagent") {
|
||||
// Copy multi-agent code
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "multiagent", "python"),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
}
|
||||
|
||||
if (template === "multiagent" || template === "reflex") {
|
||||
if (agents) {
|
||||
if (useCase) {
|
||||
const sourcePath =
|
||||
template === "multiagent"
|
||||
? path.join(compPath, "agents", "python", agents)
|
||||
: path.join(compPath, "reflex", agents);
|
||||
? path.join(compPath, "agents", "python", useCase)
|
||||
: path.join(compPath, "reflex", useCase);
|
||||
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
@@ -500,7 +495,7 @@ export const installPythonTemplate = async ({
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no agent selected for ${template} template. Please pick an agent to use via --agents flag.`,
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
|
||||
+2
-2
@@ -124,7 +124,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.11b38",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -155,7 +155,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.11b38",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ export type TemplateDataSource = {
|
||||
};
|
||||
export type TemplateDataSourceType = "file" | "web" | "db";
|
||||
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
|
||||
export type TemplateAgents =
|
||||
export type TemplateUseCase =
|
||||
| "financial_report"
|
||||
| "blog"
|
||||
| "form_filling"
|
||||
@@ -106,5 +106,5 @@ export interface InstallTemplateArgs {
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
agents?: TemplateAgents;
|
||||
useCase?: TemplateUseCase;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const installTSTemplate = async ({
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
agents,
|
||||
useCase,
|
||||
}: InstallTemplateArgs & { backend: boolean }) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
@@ -131,16 +131,16 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(multiagentPath, "workflow"),
|
||||
});
|
||||
|
||||
// Copy agents use case code for multiagent template
|
||||
if (agents) {
|
||||
console.log("\nCopying agent:", agents, "\n");
|
||||
const useCasePath = path.join(compPath, "agents", "typescript", agents);
|
||||
const agentsCodePath = path.join(useCasePath, "workflow");
|
||||
// Copy use case code for multiagent template
|
||||
if (useCase) {
|
||||
console.log("\nCopying use case:", useCase, "\n");
|
||||
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
|
||||
const useCaseCodePath = path.join(useCasePath, "workflow");
|
||||
|
||||
// Copy agent codes
|
||||
// Copy use case codes
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: agentsCodePath,
|
||||
cwd: useCaseCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
@@ -153,7 +153,7 @@ export const installTSTemplate = async ({
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no agent selected for ${template} template. Please pick an agent to use via --agents flag.`,
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
|
||||
@@ -202,10 +202,10 @@ const program = new Command(packageJson.name)
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--agents <agents>",
|
||||
"--use-case <useCase>",
|
||||
`
|
||||
|
||||
Select which agents to use for the multi-agent template (e.g: financial_report, blog).
|
||||
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
|
||||
`,
|
||||
)
|
||||
.allowUnknownOption()
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.3.23",
|
||||
"version": "0.3.24",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
+61
-25
@@ -2,7 +2,7 @@ import { blue } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { isCI } from ".";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
|
||||
import { EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
|
||||
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getProjectOptions } from "../helpers/repo";
|
||||
@@ -33,7 +33,7 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
title: "Multi-agent app (using workflows)",
|
||||
value: "multiagent",
|
||||
},
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{ title: "Fullstack python template with Reflex", value: "reflex" },
|
||||
{
|
||||
title: `Community template from ${styledRepo}`,
|
||||
value: "community",
|
||||
@@ -100,6 +100,24 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
// So we just use example file for extractor template, this allows user to choose vector database later
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
program.framework = "fastapi";
|
||||
// Ask for which Reflex use case to use
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to build?",
|
||||
choices: [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
if (!program.framework) {
|
||||
@@ -171,32 +189,50 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
program.observability = observability;
|
||||
}
|
||||
|
||||
// Ask agents
|
||||
if (program.template === "multiagent" && !program.agents) {
|
||||
const { agents } = await prompts(
|
||||
if (
|
||||
(program.template === "reflex" || program.template === "multiagent") &&
|
||||
!program.useCase
|
||||
) {
|
||||
const choices =
|
||||
program.template === "reflex"
|
||||
? [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{ title: "Blog writer (Write a blog post)", value: "blog" },
|
||||
];
|
||||
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "agents",
|
||||
message: "Which agents would you like to use?",
|
||||
choices: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{
|
||||
title: "Blog writer (Write a blog post)",
|
||||
value: "blog_writer",
|
||||
},
|
||||
],
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.agents = agents;
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
// Configure framework and data sources for Reflex template
|
||||
if (program.template === "reflex") {
|
||||
program.framework = "fastapi";
|
||||
|
||||
program.dataSources =
|
||||
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
@@ -222,8 +258,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
program.vectorDb = vectorDb;
|
||||
}
|
||||
|
||||
if (program.vectorDb === "llamacloud") {
|
||||
// When using a LlamaCloud index, don't ask for data sources just copy an example file
|
||||
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
|
||||
// When using a LlamaCloud index and no data sources are provided, just copy an example file
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
}
|
||||
|
||||
@@ -354,7 +390,7 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = true;
|
||||
} else {
|
||||
// Reflex template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
|
||||
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
|
||||
if (program.useLlamaParse === undefined && program.template !== "reflex") {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
|
||||
+28
-28
@@ -74,34 +74,34 @@ export const askSimpleQuestions = async (
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
|
||||
const results = await convertAnswers(args, {
|
||||
@@ -133,7 +133,7 @@ const convertAnswers = async (
|
||||
AppType,
|
||||
Pick<
|
||||
QuestionResults,
|
||||
"template" | "tools" | "frontend" | "dataSources" | "agents"
|
||||
"template" | "tools" | "frontend" | "dataSources" | "useCase"
|
||||
> & {
|
||||
modelConfig?: ModelConfig;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ const convertAnswers = async (
|
||||
},
|
||||
financial_report_agent: {
|
||||
template: "multiagent",
|
||||
agents: "financial_report",
|
||||
useCase: "financial_report",
|
||||
tools: getTools(["document_generator", "interpreter"]),
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
frontend: true,
|
||||
@@ -168,7 +168,7 @@ const convertAnswers = async (
|
||||
},
|
||||
form_filling: {
|
||||
template: "multiagent",
|
||||
agents: "form_filling",
|
||||
useCase: "form_filling",
|
||||
tools: getTools(["form_filling"]),
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
frontend: true,
|
||||
@@ -176,14 +176,14 @@ const convertAnswers = async (
|
||||
},
|
||||
extractor: {
|
||||
template: "reflex",
|
||||
agents: "extractor",
|
||||
useCase: "extractor",
|
||||
tools: [],
|
||||
frontend: false,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
contract_review: {
|
||||
template: "reflex",
|
||||
agents: "contract_review",
|
||||
useCase: "contract_review",
|
||||
tools: [],
|
||||
frontend: false,
|
||||
dataSources: [EXAMPLE_GDPR],
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuid
|
||||
from typing import List, Optional
|
||||
|
||||
from app.services.file import DocumentFile, FileService
|
||||
from e2b_code_interpreter import CodeInterpreter
|
||||
from e2b_code_interpreter import Sandbox
|
||||
from e2b_code_interpreter.models import Logs
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel
|
||||
@@ -61,7 +61,7 @@ class E2BCodeInterpreter:
|
||||
Lazily initialize the interpreter.
|
||||
"""
|
||||
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
|
||||
self.interpreter = CodeInterpreter(api_key=self.api_key)
|
||||
self.interpreter = Sandbox(api_key=self.api_key)
|
||||
if len(sandbox_files) > 0:
|
||||
for file_path in sandbox_files:
|
||||
file_name = os.path.basename(file_path)
|
||||
@@ -159,11 +159,11 @@ class E2BCodeInterpreter:
|
||||
if self.interpreter is None:
|
||||
self._init_interpreter(sandbox_files)
|
||||
|
||||
if self.interpreter and self.interpreter.notebook:
|
||||
if self.interpreter:
|
||||
logger.info(
|
||||
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
|
||||
)
|
||||
exec = self.interpreter.notebook.exec_cell(code)
|
||||
exec = self.interpreter.run_code(code)
|
||||
|
||||
if exec.error:
|
||||
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
|
||||
|
||||
@@ -103,6 +103,7 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
const artifact = await this.generateArtifact(
|
||||
input.requirement,
|
||||
input.oldCode,
|
||||
input.sandboxFiles, // help the generated code use exact files
|
||||
);
|
||||
if (input.sandboxFiles) {
|
||||
artifact.files = input.sandboxFiles;
|
||||
@@ -117,10 +118,12 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
async generateArtifact(
|
||||
query: string,
|
||||
oldCode?: string,
|
||||
attachments?: string[],
|
||||
): Promise<CodeArtifact> {
|
||||
const userMessage = `
|
||||
${query}
|
||||
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
|
||||
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
|
||||
`;
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: CODE_GENERATION_PROMPT },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CodeInterpreter, Logs, Result } from "@e2b/code-interpreter";
|
||||
import { Logs, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import fs from "fs";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
@@ -82,7 +82,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
private apiKey?: string;
|
||||
private fileServerURLPrefix?: string;
|
||||
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
|
||||
codeInterpreter?: CodeInterpreter;
|
||||
codeInterpreter?: Sandbox;
|
||||
|
||||
constructor(params?: InterpreterToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
@@ -104,26 +104,27 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
|
||||
public async initInterpreter(input: InterpreterParameter) {
|
||||
if (!this.codeInterpreter) {
|
||||
this.codeInterpreter = await CodeInterpreter.create({
|
||||
this.codeInterpreter = await Sandbox.create({
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
}
|
||||
// upload files to sandbox
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
// upload files to sandbox when it's initialized
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
|
||||
const arrayBuffer = new Uint8Array(content).buffer;
|
||||
await this.codeInterpreter?.files.write(filePath, arrayBuffer);
|
||||
const arrayBuffer = new Uint8Array(content).buffer;
|
||||
await this.codeInterpreter?.files.write(filePath, arrayBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
}
|
||||
|
||||
return this.codeInterpreter;
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${input.code}\n${"=".repeat(50)}`,
|
||||
);
|
||||
const interpreter = await this.initInterpreter(input);
|
||||
const exec = await interpreter.notebook.execCell(input.code);
|
||||
const exec = await interpreter.runCode(input.code);
|
||||
if (exec.error) console.error("[Code Interpreter error]", exec.error);
|
||||
const extraResult = await this.getExtraResult(exec.results[0]);
|
||||
const result: InterpreterToolOutput = {
|
||||
@@ -169,7 +170,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.codeInterpreter?.close();
|
||||
await this.codeInterpreter?.kill();
|
||||
}
|
||||
|
||||
private async getExtraResult(
|
||||
|
||||
@@ -17,11 +17,11 @@ import logging
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import asdict
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.engine.tools.artifact import CodeArtifact
|
||||
from app.services.file import FileService
|
||||
from e2b_code_interpreter import CodeInterpreter, Sandbox # type: ignore
|
||||
from e2b_code_interpreter import Sandbox # type: ignore
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -60,6 +60,14 @@ class FileUpload(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
SUPPORTED_TEMPLATES = [
|
||||
"nextjs-developer",
|
||||
"vue-developer",
|
||||
"streamlit-developer",
|
||||
"gradio-developer",
|
||||
]
|
||||
|
||||
|
||||
@sandbox_router.post("")
|
||||
async def create_sandbox(request: Request):
|
||||
request_data = await request.json()
|
||||
@@ -79,33 +87,22 @@ async def create_sandbox(request: Request):
|
||||
status_code=400, detail="Could not create artifact from the request data"
|
||||
)
|
||||
|
||||
sbx = None
|
||||
|
||||
# Create an interpreter or a sandbox
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
sbx = CodeInterpreter(api_key=os.getenv("E2B_API_KEY"), timeout=SANDBOX_TIMEOUT)
|
||||
logger.debug(f"Created code interpreter {sbx}")
|
||||
else:
|
||||
sbx = Sandbox(
|
||||
api_key=os.getenv("E2B_API_KEY"),
|
||||
template=artifact.template,
|
||||
metadata={"template": artifact.template, "user_id": "default"},
|
||||
timeout=SANDBOX_TIMEOUT,
|
||||
)
|
||||
logger.debug(f"Created sandbox {sbx}")
|
||||
sbx = Sandbox(
|
||||
api_key=os.getenv("E2B_API_KEY"),
|
||||
template=(
|
||||
None if artifact.template not in SUPPORTED_TEMPLATES else artifact.template
|
||||
),
|
||||
metadata={"template": artifact.template, "user_id": "default"},
|
||||
timeout=SANDBOX_TIMEOUT,
|
||||
)
|
||||
logger.debug(f"Created sandbox {sbx}")
|
||||
|
||||
# Install packages
|
||||
if artifact.has_additional_dependencies:
|
||||
if isinstance(sbx, CodeInterpreter):
|
||||
sbx.notebook.exec_cell(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in code interpreter {sbx}"
|
||||
)
|
||||
elif isinstance(sbx, Sandbox):
|
||||
sbx.commands.run(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
|
||||
)
|
||||
sbx.commands.run(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
|
||||
)
|
||||
|
||||
# Copy files
|
||||
if len(sandbox_files) > 0:
|
||||
@@ -122,7 +119,7 @@ async def create_sandbox(request: Request):
|
||||
|
||||
# Execute code or return a URL to the running sandbox
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
result = sbx.notebook.exec_cell(artifact.code or "")
|
||||
result = sbx.run_code(artifact.code or "")
|
||||
output_urls = _download_cell_results(result.results)
|
||||
runtime_error = asdict(result.error) if result.error else None
|
||||
return ExecutionResult(
|
||||
@@ -145,7 +142,7 @@ async def create_sandbox(request: Request):
|
||||
|
||||
|
||||
def _upload_files(
|
||||
sandbox: Union[CodeInterpreter, Sandbox],
|
||||
sandbox: Sandbox,
|
||||
sandbox_files: List[str] = [],
|
||||
) -> None:
|
||||
for file_path in sandbox_files:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import DATA_DIR
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class SourceNodes(BaseModel):
|
||||
id: str
|
||||
metadata: Dict[str, Any]
|
||||
score: Optional[float]
|
||||
text: str
|
||||
url: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def from_source_node(cls, source_node: NodeWithScore):
|
||||
metadata = source_node.node.metadata
|
||||
url = cls.get_url_from_metadata(metadata)
|
||||
|
||||
return cls(
|
||||
id=source_node.node.node_id,
|
||||
metadata=metadata,
|
||||
score=source_node.score,
|
||||
text=source_node.node.text, # type: ignore
|
||||
url=url,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_url_from_metadata(cls, metadata: Dict[str, Any]) -> Optional[str]:
|
||||
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if not url_prefix:
|
||||
logger.warning(
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
|
||||
)
|
||||
file_name = metadata.get("file_name")
|
||||
|
||||
if file_name and url_prefix:
|
||||
# file_name exists and file server is configured
|
||||
pipeline_id = metadata.get("pipeline_id")
|
||||
if pipeline_id:
|
||||
# file is from LlamaCloud
|
||||
file_name = f"{pipeline_id}${file_name}"
|
||||
return f"{url_prefix}/output/llamacloud/{file_name}"
|
||||
is_private = metadata.get("private", "false") == "true"
|
||||
if is_private:
|
||||
# file is a private upload
|
||||
return f"{url_prefix}/output/uploaded/{file_name}"
|
||||
# file is from calling the 'generate' script
|
||||
# Get the relative path of file_path to data_dir
|
||||
file_path = metadata.get("file_path")
|
||||
data_dir = os.path.abspath(DATA_DIR)
|
||||
if file_path and data_dir:
|
||||
relative_path = os.path.relpath(file_path, data_dir)
|
||||
return f"{url_prefix}/data/{relative_path}"
|
||||
# fallback to URL in metadata (e.g. for websites)
|
||||
return metadata.get("URL")
|
||||
|
||||
@classmethod
|
||||
def from_source_nodes(cls, source_nodes: List[NodeWithScore]):
|
||||
return [cls.from_source_node(node) for node in source_nodes]
|
||||
@@ -0,0 +1,281 @@
|
||||
import base64
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from llama_index.core import VectorStoreIndex
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.readers.file.base import (
|
||||
_try_loading_included_file_formats as get_file_loaders_map,
|
||||
)
|
||||
from llama_index.core.schema import Document
|
||||
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
|
||||
from llama_index.readers.file import FlatReader
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PRIVATE_STORE_PATH = str(Path("output", "uploaded"))
|
||||
TOOL_STORE_PATH = str(Path("output", "tools"))
|
||||
LLAMA_CLOUD_STORE_PATH = str(Path("output", "llamacloud"))
|
||||
|
||||
|
||||
class DocumentFile(BaseModel):
|
||||
id: str
|
||||
name: str # Stored file name
|
||||
type: str = None
|
||||
size: int = None
|
||||
url: str = None
|
||||
path: Optional[str] = Field(
|
||||
None,
|
||||
description="The stored file path. Used internally in the server.",
|
||||
exclude=True,
|
||||
)
|
||||
refs: Optional[List[str]] = Field(
|
||||
None, description="The document ids in the index."
|
||||
)
|
||||
|
||||
|
||||
class FileService:
|
||||
"""
|
||||
To store the files uploaded by the user and add them to the index.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def process_private_file(
|
||||
cls,
|
||||
file_name: str,
|
||||
base64_content: str,
|
||||
params: Optional[dict] = None,
|
||||
) -> DocumentFile:
|
||||
"""
|
||||
Store the uploaded file and index it if necessary.
|
||||
"""
|
||||
try:
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
except ImportError as e:
|
||||
raise ValueError("IndexConfig or get_index is not found") from e
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
|
||||
# Preprocess and store the file
|
||||
file_data, extension = cls._preprocess_base64_file(base64_content)
|
||||
|
||||
document_file = cls.save_file(
|
||||
file_data,
|
||||
file_name=file_name,
|
||||
save_dir=PRIVATE_STORE_PATH,
|
||||
)
|
||||
|
||||
# Don't index csv files (they are handled by tools)
|
||||
if extension == "csv":
|
||||
return document_file
|
||||
else:
|
||||
# Insert the file into the index and update document ids to the file metadata
|
||||
if isinstance(index, LlamaCloudIndex):
|
||||
doc_id = cls._add_file_to_llama_cloud_index(
|
||||
index, document_file.name, file_data
|
||||
)
|
||||
# Add document ids to the file metadata
|
||||
document_file.refs = [doc_id]
|
||||
else:
|
||||
documents = cls._load_file_to_documents(document_file)
|
||||
cls._add_documents_to_vector_store_index(documents, index)
|
||||
# Add document ids to the file metadata
|
||||
document_file.refs = [doc.doc_id for doc in documents]
|
||||
|
||||
# Return the file metadata
|
||||
return document_file
|
||||
|
||||
@classmethod
|
||||
def save_file(
|
||||
cls,
|
||||
content: bytes | str,
|
||||
file_name: str,
|
||||
save_dir: Optional[str] = None,
|
||||
) -> DocumentFile:
|
||||
"""
|
||||
Save the content to a file in the local file server (accessible via URL)
|
||||
|
||||
Args:
|
||||
content (bytes | str): The content to save, either bytes or string.
|
||||
file_name (str): The original name of the file.
|
||||
save_dir (Optional[str]): The relative path from the current working directory. Defaults to the `output/uploaded` directory.
|
||||
Returns:
|
||||
The metadata of the saved file.
|
||||
"""
|
||||
if save_dir is None:
|
||||
save_dir = os.path.join("output", "uploaded")
|
||||
|
||||
file_id = str(uuid.uuid4())
|
||||
name, extension = os.path.splitext(file_name)
|
||||
extension = extension.lstrip(".")
|
||||
sanitized_name = _sanitize_file_name(name)
|
||||
if extension == "":
|
||||
raise ValueError("File is not supported!")
|
||||
new_file_name = f"{sanitized_name}_{file_id}.{extension}"
|
||||
|
||||
file_path = os.path.join(save_dir, new_file_name)
|
||||
|
||||
if isinstance(content, str):
|
||||
content = content.encode()
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(content)
|
||||
except PermissionError as e:
|
||||
logger.error(
|
||||
f"Permission denied when writing to file {file_path}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
except IOError as e:
|
||||
logger.error(
|
||||
f"IO error occurred when writing to file {file_path}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error when writing to file {file_path}: {str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"Saved file to {file_path}")
|
||||
|
||||
file_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if file_url_prefix is None:
|
||||
logger.warning(
|
||||
"FILESERVER_URL_PREFIX is not set, fallback to http://localhost:8000/api/files"
|
||||
)
|
||||
file_url_prefix = "http://localhost:8000/api/files"
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
file_url = os.path.join(
|
||||
file_url_prefix,
|
||||
save_dir,
|
||||
new_file_name,
|
||||
)
|
||||
|
||||
return DocumentFile(
|
||||
id=file_id,
|
||||
name=new_file_name,
|
||||
type=extension,
|
||||
size=file_size,
|
||||
path=file_path,
|
||||
url=file_url,
|
||||
refs=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
|
||||
header, data = base64_content.split(",", 1)
|
||||
mime_type = header.split(";")[0].split(":", 1)[1]
|
||||
extension = mimetypes.guess_extension(mime_type).lstrip(".")
|
||||
# File data as bytes
|
||||
return base64.b64decode(data), extension
|
||||
|
||||
@staticmethod
|
||||
def _load_file_to_documents(file: DocumentFile) -> List[Document]:
|
||||
"""
|
||||
Load the file from the private directory and return the documents
|
||||
"""
|
||||
_, extension = os.path.splitext(file.name)
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
# Load file to documents
|
||||
# If LlamaParse is enabled, use it to parse the file
|
||||
# Otherwise, use the default file loaders
|
||||
reader = _get_llamaparse_parser()
|
||||
if reader is None:
|
||||
reader_cls = _default_file_loaders_map().get(f".{extension}")
|
||||
if reader_cls is None:
|
||||
raise ValueError(f"File extension {extension} is not supported")
|
||||
reader = reader_cls()
|
||||
if file.path is None:
|
||||
raise ValueError("Document file path is not set")
|
||||
documents = reader.load_data(Path(file.path))
|
||||
# Add custom metadata
|
||||
for doc in documents:
|
||||
doc.metadata["file_name"] = file.name
|
||||
doc.metadata["private"] = "true"
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def _add_documents_to_vector_store_index(
|
||||
documents: List[Document], index: VectorStoreIndex
|
||||
) -> None:
|
||||
"""
|
||||
Add the documents to the vector store index
|
||||
"""
|
||||
pipeline = IngestionPipeline()
|
||||
nodes = pipeline.run(documents=documents)
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
if index is None:
|
||||
index = VectorStoreIndex(nodes=nodes)
|
||||
else:
|
||||
index.insert_nodes(nodes=nodes)
|
||||
index.storage_context.persist(
|
||||
persist_dir=os.environ.get("STORAGE_DIR", "storage")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_file_to_llama_cloud_index(
|
||||
index: LlamaCloudIndex,
|
||||
file_name: str,
|
||||
file_data: bytes,
|
||||
) -> str:
|
||||
"""
|
||||
Add the file to the LlamaCloud index.
|
||||
LlamaCloudIndex is a managed index so we can directly use the files.
|
||||
"""
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService # type: ignore
|
||||
except ImportError as e:
|
||||
raise ValueError("LlamaCloudFileService is not found") from e
|
||||
|
||||
# LlamaCloudIndex is a managed index so we can directly use the files
|
||||
upload_file = (file_name, BytesIO(file_data))
|
||||
doc_id = LLamaCloudFileService.add_file_to_pipeline(
|
||||
index.project.id,
|
||||
index.pipeline.id,
|
||||
upload_file,
|
||||
custom_metadata={},
|
||||
wait_for_processing=True,
|
||||
)
|
||||
return doc_id
|
||||
|
||||
|
||||
def _sanitize_file_name(file_name: str) -> str:
|
||||
"""
|
||||
Sanitize the file name by replacing all non-alphanumeric characters with underscores
|
||||
"""
|
||||
sanitized_name = re.sub(r"[^a-zA-Z0-9.]", "_", file_name)
|
||||
return sanitized_name
|
||||
|
||||
|
||||
def _get_llamaparse_parser():
|
||||
from app.engine.loaders import load_configs
|
||||
from app.engine.loaders.file import FileLoaderConfig, llama_parse_parser
|
||||
|
||||
config = load_configs()
|
||||
file_loader_config = FileLoaderConfig(**config["file"])
|
||||
if file_loader_config.use_llama_parse:
|
||||
return llama_parse_parser()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _default_file_loaders_map():
|
||||
default_loaders = get_file_loaders_map()
|
||||
default_loaders[".txt"] = FlatReader
|
||||
default_loaders[".csv"] = FlatReader
|
||||
return default_loaders
|
||||
@@ -24,7 +24,7 @@
|
||||
"llamaindex": "0.8.2",
|
||||
"pdf2json": "3.0.5",
|
||||
"ajv": "^8.12.0",
|
||||
"@e2b/code-interpreter": "0.0.9-beta.3",
|
||||
"@e2b/code-interpreter": "1.0.4",
|
||||
"got": "^14.4.1",
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"formdata-node": "^6.0.3",
|
||||
|
||||
@@ -13,13 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
CodeInterpreter,
|
||||
ExecutionError,
|
||||
Result,
|
||||
Sandbox,
|
||||
} from "@e2b/code-interpreter";
|
||||
import { ExecutionError, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import { Request, Response } from "express";
|
||||
import path from "node:path";
|
||||
import { saveDocument } from "./llamaindex/documents/helper";
|
||||
|
||||
type CodeArtifact = {
|
||||
@@ -39,6 +35,8 @@ const sandboxTimeout = 10 * 60 * 1000; // 10 minute in ms
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const OUTPUT_DIR = path.join("output", "tools");
|
||||
|
||||
export type ExecutionResult = {
|
||||
template: string;
|
||||
stdout: string[];
|
||||
@@ -48,60 +46,53 @@ export type ExecutionResult = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// see https://github.com/e2b-dev/fragments/tree/main/sandbox-templates
|
||||
const SUPPORTED_TEMPLATES = [
|
||||
"nextjs-developer",
|
||||
"vue-developer",
|
||||
"streamlit-developer",
|
||||
"gradio-developer",
|
||||
];
|
||||
|
||||
export const sandbox = async (req: Request, res: Response) => {
|
||||
const { artifact }: { artifact: CodeArtifact } = req.body;
|
||||
|
||||
let sbx: Sandbox | CodeInterpreter | undefined = undefined;
|
||||
|
||||
// Create a interpreter or a sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
sbx = await CodeInterpreter.create({
|
||||
metadata: { template: artifact.template },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created code interpreter", sbx.sandboxID);
|
||||
let sbx: Sandbox;
|
||||
const sandboxOpts = {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
};
|
||||
if (SUPPORTED_TEMPLATES.includes(artifact.template)) {
|
||||
sbx = await Sandbox.create(artifact.template, sandboxOpts);
|
||||
} else {
|
||||
sbx = await Sandbox.create(artifact.template, {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created sandbox", sbx.sandboxID);
|
||||
sbx = await Sandbox.create(sandboxOpts);
|
||||
}
|
||||
console.log("Created sandbox", sbx.sandboxId);
|
||||
|
||||
// Install packages
|
||||
if (artifact.has_additional_dependencies) {
|
||||
if (sbx instanceof CodeInterpreter) {
|
||||
await sbx.notebook.execCell(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in code interpreter ${sbx.sandboxID}`,
|
||||
);
|
||||
} else if (sbx instanceof Sandbox) {
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxID}`,
|
||||
);
|
||||
}
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Copy code to fs
|
||||
if (artifact.code && Array.isArray(artifact.code)) {
|
||||
artifact.code.forEach(async (file) => {
|
||||
await sbx.files.write(file.file_path, file.file_content);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxId}`);
|
||||
});
|
||||
} else {
|
||||
await sbx.files.write(artifact.file_path, artifact.code);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxId}`);
|
||||
}
|
||||
|
||||
// Execute code or return a URL to the running sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
const result = await (sbx as CodeInterpreter).notebook.execCell(
|
||||
artifact.code || "",
|
||||
);
|
||||
await (sbx as CodeInterpreter).close();
|
||||
const result = await sbx.runCode(artifact.code || "");
|
||||
await sbx.kill();
|
||||
const outputUrls = await downloadCellResults(result.results);
|
||||
|
||||
return res.status(200).json({
|
||||
template: artifact.template,
|
||||
stdout: result.logs.stdout,
|
||||
@@ -121,17 +112,23 @@ async function downloadCellResults(
|
||||
cellResults?: Result[],
|
||||
): Promise<Array<{ url: string; filename: string }>> {
|
||||
if (!cellResults) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
cellResults.map(async (res) => {
|
||||
const formats = res.formats(); // available formats in the result
|
||||
const formatResults = await Promise.all(
|
||||
formats.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(filename, buffer);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
formats
|
||||
.filter((ext) => ["png", "svg", "jpeg", "pdf"].includes(ext))
|
||||
.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(
|
||||
path.join(OUTPUT_DIR, filename),
|
||||
buffer,
|
||||
);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
);
|
||||
return formatResults;
|
||||
}),
|
||||
|
||||
@@ -14,10 +14,33 @@ dotenv.load_dotenv()
|
||||
|
||||
|
||||
FRONTEND_DIR = Path(os.getenv("FRONTEND_DIR", ".frontend"))
|
||||
DEFAULT_FRONTEND_PORT = 3000
|
||||
APP_HOST = os.getenv("APP_HOST", "localhost")
|
||||
APP_PORT = int(
|
||||
os.getenv("APP_PORT", 8000)
|
||||
) # Allocated to backend but also for access to the app, please change it in .env
|
||||
DEFAULT_FRONTEND_PORT = (
|
||||
3000 # Not for access directly, but for proxying to the backend in development
|
||||
)
|
||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", "static"))
|
||||
|
||||
|
||||
class NodePackageManager(str):
|
||||
def __new__(cls, value: str) -> "NodePackageManager":
|
||||
return super().__new__(cls, value)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return Path(self).stem
|
||||
|
||||
@property
|
||||
def is_pnpm(self) -> bool:
|
||||
return self.name == "pnpm"
|
||||
|
||||
@property
|
||||
def is_npm(self) -> bool:
|
||||
return self.name == "npm"
|
||||
|
||||
|
||||
def build():
|
||||
"""
|
||||
Build the frontend and copy the static files to the backend.
|
||||
@@ -146,22 +169,20 @@ async def _run_frontend(
|
||||
package_manager,
|
||||
"run",
|
||||
"dev",
|
||||
"--" if package_manager.is_npm else "",
|
||||
"-p",
|
||||
str(port),
|
||||
cwd=FRONTEND_DIR,
|
||||
)
|
||||
rich.print(
|
||||
f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]"
|
||||
)
|
||||
rich.print("\n[bold]Waiting for frontend to start...")
|
||||
# Block until the frontend is accessible
|
||||
for _ in range(timeout):
|
||||
await asyncio.sleep(1)
|
||||
# Check if the frontend is accessible (port is open) or frontend_process is running
|
||||
if frontend_process.returncode is not None:
|
||||
raise RuntimeError("Could not start frontend dev server")
|
||||
if not _is_bindable_port(port):
|
||||
if _is_server_running(port):
|
||||
rich.print(
|
||||
f"\n[bold green]Frontend dev server is running on port {port}[/bold green]"
|
||||
"\n[bold]Frontend dev server is running. Please wait a while for the app to be ready...[/bold]"
|
||||
)
|
||||
return frontend_process, port
|
||||
raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds")
|
||||
@@ -173,33 +194,50 @@ async def _run_backend(
|
||||
"""
|
||||
Start the backend development server.
|
||||
|
||||
Args:
|
||||
frontend_port: The port number the frontend is running on
|
||||
Returns:
|
||||
Process: The backend process
|
||||
"""
|
||||
# Merge environment variables
|
||||
envs = {**os.environ, **(envs or {})}
|
||||
rich.print("\n[bold]Starting backend FastAPI server...[/bold]")
|
||||
# Check if the port is free
|
||||
if not _is_port_available(APP_PORT):
|
||||
raise SystemError(
|
||||
f"Port {APP_PORT} is not available! Please change the port in .env file or kill the process running on this port."
|
||||
)
|
||||
rich.print(f"\n[bold]Starting app on port {APP_PORT}...[/bold]")
|
||||
poetry_executable = _get_poetry_executable()
|
||||
return await asyncio.create_subprocess_exec(
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
poetry_executable,
|
||||
"run",
|
||||
"python",
|
||||
"main.py",
|
||||
env=envs,
|
||||
)
|
||||
# Wait for port is started
|
||||
timeout = 30
|
||||
for _ in range(timeout):
|
||||
await asyncio.sleep(1)
|
||||
if process.returncode is not None:
|
||||
raise RuntimeError("Could not start backend dev server")
|
||||
if _is_server_running(APP_PORT):
|
||||
rich.print(
|
||||
f"\n[bold green]App is running. You now can access it at http://{APP_HOST}:{APP_PORT}[/bold green]"
|
||||
)
|
||||
return process
|
||||
# Timeout, kill the process
|
||||
process.terminate()
|
||||
raise TimeoutError(f"Backend dev server failed to start within {timeout} seconds")
|
||||
|
||||
|
||||
def _install_frontend_dependencies():
|
||||
package_manager = _get_node_package_manager()
|
||||
rich.print(
|
||||
f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]"
|
||||
f"\n[bold]Installing frontend dependencies using {package_manager.name}. It might take a while...[/bold]"
|
||||
)
|
||||
run([package_manager, "install"], cwd=".frontend", check=True)
|
||||
|
||||
|
||||
def _get_node_package_manager() -> str:
|
||||
def _get_node_package_manager() -> NodePackageManager:
|
||||
"""
|
||||
Check for available package managers and return the preferred one.
|
||||
Returns 'pnpm' if installed, falls back to 'npm'.
|
||||
@@ -215,12 +253,12 @@ def _get_node_package_manager() -> str:
|
||||
for cmd in pnpm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
return NodePackageManager(cmd_path)
|
||||
|
||||
for cmd in npm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
return NodePackageManager(cmd_path)
|
||||
|
||||
raise SystemError(
|
||||
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
|
||||
@@ -244,28 +282,27 @@ def _get_poetry_executable() -> str:
|
||||
raise SystemError("Poetry is not installed. Please install Poetry first.")
|
||||
|
||||
|
||||
def _is_bindable_port(port: int) -> bool:
|
||||
"""Check if a port is available by attempting to connect to it."""
|
||||
def _is_port_available(port: int) -> bool:
|
||||
"""Check if a port is available for binding."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
# Try to connect to the port
|
||||
s.connect(("localhost", port))
|
||||
# If we can connect, port is in use
|
||||
return False
|
||||
return False # Port is in use, so not available
|
||||
except ConnectionRefusedError:
|
||||
# Connection refused means port is available
|
||||
return True
|
||||
return True # Port is available
|
||||
except socket.error:
|
||||
# Other socket errors also likely mean port is available
|
||||
return True
|
||||
return True # Other socket errors likely mean port is available
|
||||
|
||||
|
||||
def _is_server_running(port: int) -> bool:
|
||||
"""Check if a server is running on the specified port."""
|
||||
return not _is_port_available(port)
|
||||
|
||||
|
||||
def _find_free_port(start_port: int) -> int:
|
||||
"""
|
||||
Find a free port starting from the given port number.
|
||||
"""
|
||||
"""Find a free port starting from the given port number."""
|
||||
for port in range(start_port, 65535):
|
||||
if _is_bindable_port(port):
|
||||
if _is_port_available(port):
|
||||
return port
|
||||
raise SystemError("No free port found")
|
||||
|
||||
|
||||
@@ -13,12 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
CodeInterpreter,
|
||||
ExecutionError,
|
||||
Result,
|
||||
Sandbox,
|
||||
} from "@e2b/code-interpreter";
|
||||
import { ExecutionError, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { saveDocument } from "../chat/llamaindex/documents/helper";
|
||||
@@ -41,6 +36,8 @@ const sandboxTimeout = 10 * 60 * 1000; // 10 minute in ms
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const OUTPUT_DIR = path.join("output", "tools");
|
||||
|
||||
export type ExecutionResult = {
|
||||
template: string;
|
||||
stdout: string[];
|
||||
@@ -50,39 +47,35 @@ export type ExecutionResult = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// see https://github.com/e2b-dev/fragments/tree/main/sandbox-templates
|
||||
const SUPPORTED_TEMPLATES = [
|
||||
"nextjs-developer",
|
||||
"vue-developer",
|
||||
"streamlit-developer",
|
||||
"gradio-developer",
|
||||
];
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { artifact }: { artifact: CodeArtifact } = await req.json();
|
||||
|
||||
let sbx: Sandbox | CodeInterpreter | undefined = undefined;
|
||||
|
||||
// Create a interpreter or a sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
sbx = await CodeInterpreter.create({
|
||||
metadata: { template: artifact.template },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created code interpreter", sbx.sandboxID);
|
||||
let sbx: Sandbox;
|
||||
const sandboxOpts = {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
};
|
||||
if (SUPPORTED_TEMPLATES.includes(artifact.template)) {
|
||||
sbx = await Sandbox.create(artifact.template, sandboxOpts);
|
||||
} else {
|
||||
sbx = await Sandbox.create(artifact.template, {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created sandbox", sbx.sandboxID);
|
||||
sbx = await Sandbox.create(sandboxOpts);
|
||||
}
|
||||
console.log("Created sandbox", sbx.sandboxId);
|
||||
|
||||
// Install packages
|
||||
if (artifact.has_additional_dependencies) {
|
||||
if (sbx instanceof CodeInterpreter) {
|
||||
await sbx.notebook.execCell(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in code interpreter ${sbx.sandboxID}`,
|
||||
);
|
||||
} else if (sbx instanceof Sandbox) {
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxID}`,
|
||||
);
|
||||
}
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Copy files
|
||||
@@ -94,7 +87,7 @@ export async function POST(req: Request) {
|
||||
|
||||
const arrayBuffer = new Uint8Array(fileContent).buffer;
|
||||
await sbx.files.write(sandboxFilePath, arrayBuffer);
|
||||
console.log(`Copied file to ${sandboxFilePath} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${sandboxFilePath} in ${sbx.sandboxId}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,19 +95,17 @@ export async function POST(req: Request) {
|
||||
if (artifact.code && Array.isArray(artifact.code)) {
|
||||
artifact.code.forEach(async (file) => {
|
||||
await sbx.files.write(file.file_path, file.file_content);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxId}`);
|
||||
});
|
||||
} else {
|
||||
await sbx.files.write(artifact.file_path, artifact.code);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxId}`);
|
||||
}
|
||||
|
||||
// Execute code or return a URL to the running sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
const result = await (sbx as CodeInterpreter).notebook.execCell(
|
||||
artifact.code || "",
|
||||
);
|
||||
await (sbx as CodeInterpreter).close();
|
||||
const result = await sbx.runCode(artifact.code || "");
|
||||
await sbx.kill();
|
||||
const outputUrls = await downloadCellResults(result.results);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
@@ -139,17 +130,23 @@ async function downloadCellResults(
|
||||
cellResults?: Result[],
|
||||
): Promise<Array<{ url: string; filename: string }>> {
|
||||
if (!cellResults) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
cellResults.map(async (res) => {
|
||||
const formats = res.formats(); // available formats in the result
|
||||
const formatResults = await Promise.all(
|
||||
formats.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(filename, buffer);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
formats
|
||||
.filter((ext) => ["png", "svg", "jpeg", "pdf"].includes(ext))
|
||||
.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(
|
||||
path.join(OUTPUT_DIR, filename),
|
||||
buffer,
|
||||
);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
);
|
||||
return formatResults;
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@e2b/code-interpreter": "0.0.9-beta.3",
|
||||
"@e2b/code-interpreter": "1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
|
||||
Reference in New Issue
Block a user