Skip to content

Commit cde0bae

Browse files
committed
feat: implement interpreter tool
1 parent 260d37a commit cde0bae

File tree

9 files changed

+243
-3
lines changed

9 files changed

+243
-3
lines changed

helpers/tools.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export const supportedTools: Tool[] = [
6262
supportedFrameworks: ["fastapi", "express", "nextjs"],
6363
type: ToolType.LOCAL,
6464
},
65+
{
66+
display: "Interpreter",
67+
name: "interpreter",
68+
dependencies: [],
69+
supportedFrameworks: ["fastapi", "express", "nextjs"],
70+
type: ToolType.LOCAL,
71+
},
6572
];
6673

6774
export const getTool = (toolName: string): Tool | undefined => {

templates/components/engines/typescript/agent/tools/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BaseToolWithCall } from "llamaindex";
2+
import { InterpreterTool, InterpreterToolParams } from "./interpreter";
23
import { WeatherTool, WeatherToolParams } from "./weather";
34

45
type ToolCreator = (config: unknown) => BaseToolWithCall;
@@ -7,6 +8,9 @@ const toolFactory: Record<string, ToolCreator> = {
78
weather: (config: unknown) => {
89
return new WeatherTool(config as WeatherToolParams);
910
},
11+
interpreter: (config: unknown) => {
12+
return new InterpreterTool(config as InterpreterToolParams);
13+
},
1014
};
1115

1216
export function createLocalTools(
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { CodeInterpreter, Logs, Result } from "@e2b/code-interpreter";
2+
import type { JSONSchemaType } from "ajv";
3+
import fs from "fs";
4+
import { BaseTool, ToolMetadata } from "llamaindex";
5+
import crypto from "node:crypto";
6+
import path from "node:path";
7+
8+
export type InterpreterParameter = {
9+
code: string;
10+
};
11+
12+
export type InterpreterToolParams = {
13+
metadata?: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
14+
apiKey?: string;
15+
};
16+
17+
export type InterpreterToolOuput = {
18+
isError: boolean;
19+
logs: Logs;
20+
extraResult: InterpreterExtraResult[];
21+
};
22+
23+
type InterpreterExtraType =
24+
| "html"
25+
| "markdown"
26+
| "svg"
27+
| "png"
28+
| "jpeg"
29+
| "pdf"
30+
| "latex"
31+
| "json"
32+
| "javascript";
33+
34+
export type InterpreterExtraResult = {
35+
type: InterpreterExtraType;
36+
// url: string;
37+
filename: string;
38+
};
39+
40+
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
41+
name: "interpreter",
42+
description: `
43+
- You are a Python interpreter.
44+
- You are given tasks to complete and you run python code to solve them.
45+
- The python code runs in a Jupyter notebook. Every time you call \`interpreter\` tool, the python code is executed in a separate cell. It's okay to make multiple calls to \`interpreter\`.
46+
- Display visualizations using matplotlib or any other visualization library directly in the notebook. Shouldn't save the visualizations to a file, just return the base64 encoded data.
47+
- You can install any pip package (if it exists) if you need to but the usual packages for data analysis are already preinstalled.
48+
- You can run any python code you want in a secure environment.
49+
`, // TODO: Add more guide to help AI use data to generate code (eg. wiki tool, google sheet tool data)
50+
parameters: {
51+
type: "object",
52+
properties: {
53+
code: {
54+
type: "string",
55+
description: "The python code to execute in a single cell.",
56+
},
57+
},
58+
required: ["code"],
59+
},
60+
};
61+
62+
export class InterpreterTool implements BaseTool<InterpreterParameter> {
63+
private readonly outputDir = "data";
64+
private readonly dataAPI = "/api/data/";
65+
private apiKey?: string;
66+
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
67+
codeInterpreter?: CodeInterpreter;
68+
69+
constructor(params?: InterpreterToolParams) {
70+
this.metadata = params?.metadata || DEFAULT_META_DATA;
71+
this.apiKey = params?.apiKey || process.env.E2B_API_KEY;
72+
}
73+
74+
public async initInterpreter() {
75+
if (!this.apiKey) {
76+
throw new Error(
77+
"E2B_API_KEY key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key",
78+
);
79+
}
80+
if (!this.codeInterpreter) {
81+
this.codeInterpreter = await CodeInterpreter.create({
82+
apiKey: this.apiKey,
83+
});
84+
}
85+
return this.codeInterpreter;
86+
}
87+
88+
public async codeInterpret(code: string): Promise<InterpreterToolOuput> {
89+
console.log(
90+
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
91+
);
92+
const interpreter = await this.initInterpreter();
93+
const exec = await interpreter.notebook.execCell(code);
94+
if (exec.error) console.error("[Code Interpreter error]", exec.error);
95+
const extraResult = await this.getExtraResult(exec.results[0]);
96+
const result: InterpreterToolOuput = {
97+
isError: !!exec.error,
98+
logs: exec.logs,
99+
extraResult,
100+
};
101+
return result;
102+
}
103+
104+
async call(input: InterpreterParameter): Promise<InterpreterToolOuput> {
105+
const result = await this.codeInterpret(input.code);
106+
await this.codeInterpreter?.close();
107+
return result;
108+
}
109+
110+
private async getExtraResult(
111+
res?: Result,
112+
): Promise<InterpreterExtraResult[]> {
113+
if (!res) return [];
114+
const output: InterpreterExtraResult[] = [];
115+
116+
try {
117+
const formats = res.formats(); // formats available for the result. Eg: ['png', ...]
118+
const base64DataArr = formats.map((f) => res[f as keyof Result]); // get base64 data for each format
119+
120+
// save base64 data to file and return the url
121+
for (let i = 0; i < formats.length; i++) {
122+
const ext = formats[i];
123+
const base64Data = base64DataArr[i];
124+
if (ext && base64Data) {
125+
const { filename } = this.saveToDisk(base64Data, ext);
126+
output.push({
127+
type: ext as InterpreterExtraType,
128+
filename,
129+
// url: this.getDataUrl(filename),
130+
});
131+
}
132+
}
133+
} catch (error) {
134+
console.error("Error when saving data to disk", error);
135+
}
136+
137+
return output;
138+
}
139+
140+
// Consider saving to cloud storage instead but it may cost more for you
141+
// See: https://e2b.dev/docs/sandbox/api/filesystem#write-to-file
142+
private saveToDisk(
143+
base64Data: string,
144+
ext: string,
145+
): {
146+
outputPath: string;
147+
filename: string;
148+
} {
149+
const filename = `${crypto.randomUUID()}.${ext}`; // generate a unique filename
150+
const buffer = Buffer.from(base64Data, "base64");
151+
const outputPath = this.getOutputPath(filename);
152+
fs.writeFileSync(outputPath, buffer);
153+
console.log(`Saved file to ${outputPath}`);
154+
return {
155+
outputPath,
156+
filename,
157+
};
158+
}
159+
160+
private getOutputPath(filename: string): string {
161+
// if outputDir doesn't exist, create it
162+
if (!fs.existsSync(this.outputDir)) {
163+
fs.mkdirSync(this.outputDir, { recursive: true });
164+
}
165+
return path.join(this.outputDir, filename);
166+
}
167+
168+
private getDataUrl(filename: string): string {
169+
return `${this.dataAPI}${filename}`;
170+
}
171+
}

templates/types/streaming/nextjs/app/components/ui/chat/chat-events.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function ChatEvents({
3838
<CollapsibleContent asChild>
3939
<div className="mt-4 text-sm space-y-2">
4040
{data.map((eventItem, index) => (
41-
<div key={index}>{eventItem.title}</div>
41+
<div className="whitespace-break-spaces" key={index}>{eventItem.title}</div>
4242
))}
4343
</div>
4444
</CollapsibleContent>

templates/types/streaming/nextjs/app/components/ui/chat/chat-tools.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ToolData } from "./index";
2+
import { InterpreterCard, InterpreterData } from "./widgets/InterpreterCard";
23
import { WeatherCard, WeatherData } from "./widgets/WeatherCard";
34

45
// TODO: If needed, add displaying more tool outputs here
@@ -20,6 +21,9 @@ export default function ChatTools({ data }: { data: ToolData }) {
2021
case "get_weather_information":
2122
const weatherData = toolOutput.output as unknown as WeatherData;
2223
return <WeatherCard data={weatherData} />;
24+
case "interpreter":
25+
const interpreterData = toolOutput.output as unknown as InterpreterData;
26+
return <InterpreterCard data={interpreterData} />;
2327
default:
2428
return null;
2529
}

templates/types/streaming/nextjs/app/components/ui/chat/markdown.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import rehypeKatex from "rehype-katex";
55
import remarkGfm from "remark-gfm";
66
import remarkMath from "remark-math";
77

8+
import { replaceAttachmentUrl } from "../lib/url";
89
import { CodeBlock } from "./codeblock";
910

1011
const MemoizedReactMarkdown: FC<Options> = memo(
@@ -28,8 +29,13 @@ const preprocessLaTeX = (content: string) => {
2829
return inlineProcessedContent;
2930
};
3031

32+
const preprocessUrl = (content: string) => {
33+
return replaceAttachmentUrl(content);
34+
};
35+
3136
export default function Markdown({ content }: { content: string }) {
3237
const processedContent = preprocessLaTeX(content);
38+
const processedContentWithUrl = preprocessUrl(processedContent);
3339
return (
3440
<MemoizedReactMarkdown
3541
className="prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words custom-markdown"
@@ -71,7 +77,7 @@ export default function Markdown({ content }: { content: string }) {
7177
},
7278
}}
7379
>
74-
{processedContent}
80+
{processedContentWithUrl}
7581
</MemoizedReactMarkdown>
7682
);
7783
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getStaticFileDataUrl } from "../../lib/url";
2+
3+
export interface InterpreterData {
4+
isError: boolean;
5+
extraResult: Array<{
6+
type: string;
7+
url: string;
8+
filename: string;
9+
}>;
10+
logs: {
11+
stderr: string[];
12+
stdout: string[];
13+
};
14+
}
15+
16+
export function InterpreterCard({ data }: { data: InterpreterData }) {
17+
const { isError, extraResult } = data;
18+
if (isError || !extraResult.length) return null;
19+
return (
20+
<div className="space-x-2">
21+
<span className="font-semibold">Output Files:</span>
22+
{extraResult.map((result, i) => (
23+
<a
24+
className="hover:underline text-blue-500 uppercase"
25+
href={getStaticFileDataUrl(result.filename)}
26+
key={i}
27+
target="_blank"
28+
>
29+
{result.type} file
30+
</a>
31+
))}
32+
</div>
33+
);
34+
}

templates/types/streaming/nextjs/app/components/ui/lib/url.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,16 @@ export const getStaticFileDataUrl = (filename: string) => {
99
}
1010
return fileUrl;
1111
};
12+
13+
// replace all attachment:// url with /api/data/
14+
export const replaceAttachmentUrl = (content: string) => {
15+
const isUsingBackend = !!process.env.NEXT_PUBLIC_CHAT_API;
16+
if (isUsingBackend) {
17+
const backendOrigin = new URL(process.env.NEXT_PUBLIC_CHAT_API!).origin;
18+
return content.replace(
19+
/attachment:\/\//g,
20+
`${backendOrigin}/api/${STORAGE_FOLDER}/`,
21+
);
22+
}
23+
return content.replace(/attachment:\/\//g, `/api/${STORAGE_FOLDER}/`);
24+
};

templates/types/streaming/nextjs/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"supports-color": "^8.1.1",
3535
"tailwind-merge": "^2.1.0",
3636
"vaul": "^0.9.1",
37-
"@llamaindex/pdf-viewer": "^1.1.1"
37+
"@llamaindex/pdf-viewer": "^1.1.1",
38+
"@e2b/code-interpreter": "^0.0.5"
3839
},
3940
"devDependencies": {
4041
"@types/node": "^20.10.3",

0 commit comments

Comments
 (0)