Skip to content

Commit 5a7216e

Browse files
feat: implement artifact tool in TS (#328)
--------- Co-authored-by: Marcus Schiesser <[email protected]>
1 parent 27a1b9f commit 5a7216e

File tree

25 files changed

+1070
-122
lines changed

25 files changed

+1070
-122
lines changed

.changeset/modern-cars-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-llama": patch
3+
---
4+
5+
feat: implement artifact tool in TS

e2e/shared/multiagent_template.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${tem
6666
page,
6767
}) => {
6868
await page.goto(`http://localhost:${port}`);
69-
await page.fill("form input", userMessage);
69+
await page.fill("form textarea", userMessage);
7070

7171
const responsePromise = page.waitForResponse((res) =>
7272
res.url().includes("/api/chat"),

e2e/shared/streaming_template.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
7272
}) => {
7373
test.skip(templatePostInstallAction !== "runApp");
7474
await page.goto(`http://localhost:${port}`);
75-
await page.fill("form input", userMessage);
75+
await page.fill("form textarea", userMessage);
7676
const [response] = await Promise.all([
7777
page.waitForResponse(
7878
(res) => {

helpers/env-variables.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,12 +397,6 @@ const getEngineEnvs = (): EnvVar[] => {
397397
description:
398398
"The number of similar embeddings to return when retrieving documents.",
399399
},
400-
{
401-
name: "STREAM_TIMEOUT",
402-
description:
403-
"The time in milliseconds to wait for the stream to return a response.",
404-
value: "60000",
405-
},
406400
];
407401
};
408402

helpers/tools.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,26 @@ For better results, you can specify the region parameter to get results from a s
162162
},
163163
],
164164
},
165+
{
166+
display: "Artifact Code Generator",
167+
name: "artifact",
168+
dependencies: [],
169+
supportedFrameworks: ["express", "nextjs"],
170+
type: ToolType.LOCAL,
171+
envVars: [
172+
{
173+
name: "E2B_API_KEY",
174+
description:
175+
"E2B_API_KEY key is required to run artifact code generator tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
176+
},
177+
{
178+
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
179+
description: "System prompt for artifact code generator tool.",
180+
value:
181+
"You are a code assistant that can generate and execute code using its tools. Don't generate code yourself, use the provided tools instead. Do not show the code or sandbox url in chat, just describe the steps to build the application based on the code that is generated by your tools. Do not describe how to run the code, just the steps to build the application.",
182+
},
183+
],
184+
},
165185
{
166186
display: "OpenAPI action",
167187
name: "openapi_action.OpenAPIActionToolSpec",
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { JSONSchemaType } from "ajv";
2+
import {
3+
BaseTool,
4+
ChatMessage,
5+
JSONValue,
6+
Settings,
7+
ToolMetadata,
8+
} from "llamaindex";
9+
10+
// prompt based on https://github.com/e2b-dev/ai-artifacts
11+
const CODE_GENERATION_PROMPT = `You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:\n
12+
13+
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
14+
15+
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: [email protected], typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
16+
17+
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, [email protected], tailwindcss. Port: 3000.
18+
19+
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
20+
21+
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
22+
23+
Provide detail information about the artifact you're about to generate in the following JSON format with the following keys:
24+
25+
commentary: Describe what you're about to do and the steps you want to take for generating the artifact in great detail.
26+
template: Name of the template used to generate the artifact.
27+
title: Short title of the artifact. Max 3 words.
28+
description: Short description of the artifact. Max 1 sentence.
29+
additional_dependencies: Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.
30+
has_additional_dependencies: Detect if additional dependencies that are not included in the template are required by the artifact.
31+
install_dependencies_command: Command to install additional dependencies required by the artifact.
32+
port: Port number used by the resulted artifact. Null when no ports are exposed.
33+
file_path: Relative path to the file, including the file name.
34+
code: Code generated by the artifact. Only runnable code is allowed.
35+
36+
Make sure to use the correct syntax for the programming language you're using. Make sure to generate only one code file. If you need to use CSS, make sure to include the CSS in the code file using Tailwind CSS syntax.
37+
`;
38+
39+
// detail information to execute code
40+
export type CodeArtifact = {
41+
commentary: string;
42+
template: string;
43+
title: string;
44+
description: string;
45+
additional_dependencies: string[];
46+
has_additional_dependencies: boolean;
47+
install_dependencies_command: string;
48+
port: number | null;
49+
file_path: string;
50+
code: string;
51+
};
52+
53+
export type CodeGeneratorParameter = {
54+
requirement: string;
55+
oldCode?: string;
56+
};
57+
58+
export type CodeGeneratorToolParams = {
59+
metadata?: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
60+
};
61+
62+
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
63+
{
64+
name: "artifact",
65+
description: `Generate a code artifact based on the input. Don't call this tool if the user has not asked for code generation. E.g. if the user asks to write a description or specification, don't call this tool.`,
66+
parameters: {
67+
type: "object",
68+
properties: {
69+
requirement: {
70+
type: "string",
71+
description: "The description of the application you want to build.",
72+
},
73+
oldCode: {
74+
type: "string",
75+
description: "The existing code to be modified",
76+
nullable: true,
77+
},
78+
},
79+
required: ["requirement"],
80+
},
81+
};
82+
83+
export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
84+
metadata: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
85+
86+
constructor(params?: CodeGeneratorToolParams) {
87+
this.metadata = params?.metadata || DEFAULT_META_DATA;
88+
}
89+
90+
async call(input: CodeGeneratorParameter) {
91+
try {
92+
const artifact = await this.generateArtifact(
93+
input.requirement,
94+
input.oldCode,
95+
);
96+
return artifact as JSONValue;
97+
} catch (error) {
98+
return { isError: true };
99+
}
100+
}
101+
102+
// Generate artifact (code, environment, dependencies, etc.)
103+
async generateArtifact(
104+
query: string,
105+
oldCode?: string,
106+
): Promise<CodeArtifact> {
107+
const userMessage = `
108+
${query}
109+
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
110+
`;
111+
const messages: ChatMessage[] = [
112+
{ role: "system", content: CODE_GENERATION_PROMPT },
113+
{ role: "user", content: userMessage },
114+
];
115+
try {
116+
const response = await Settings.llm.chat({ messages });
117+
const content = response.message.content.toString();
118+
const jsonContent = content
119+
.replace(/^```json\s*|\s*```$/g, "")
120+
.replace(/^`+|`+$/g, "")
121+
.trim();
122+
const artifact = JSON.parse(jsonContent) as CodeArtifact;
123+
return artifact;
124+
} catch (error) {
125+
console.log("Failed to generate artifact", error);
126+
throw error;
127+
}
128+
}
129+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BaseToolWithCall } from "llamaindex";
22
import { ToolsFactory } from "llamaindex/tools/ToolsFactory";
3+
import { CodeGeneratorTool, CodeGeneratorToolParams } from "./code-generator";
34
import {
45
DocumentGenerator,
56
DocumentGeneratorParams,
@@ -47,6 +48,9 @@ const toolFactory: Record<string, ToolCreator> = {
4748
img_gen: async (config: unknown) => {
4849
return [new ImgGeneratorTool(config as ImgGeneratorToolParams)];
4950
},
51+
artifact: async (config: unknown) => {
52+
return [new CodeGeneratorTool(config as CodeGeneratorToolParams)];
53+
},
5054
document_generator: async (config: unknown) => {
5155
return [new DocumentGenerator(config as DocumentGeneratorParams)];
5256
},

templates/components/llamaindex/typescript/streaming/annotations.ts

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JSONValue } from "ai";
1+
import { JSONValue, Message } from "ai";
22
import { MessageContent, MessageContentDetail } from "llamaindex";
33

44
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
@@ -21,13 +21,20 @@ type Annotation = {
2121
data: object;
2222
};
2323

24-
export function retrieveDocumentIds(annotations?: JSONValue[]): string[] {
25-
if (!annotations) return [];
24+
export function isValidMessages(messages: Message[]): boolean {
25+
const lastMessage =
26+
messages && messages.length > 0 ? messages[messages.length - 1] : null;
27+
return lastMessage !== null && lastMessage.role === "user";
28+
}
29+
30+
export function retrieveDocumentIds(messages: Message[]): string[] {
31+
// retrieve document Ids from the annotations of all messages (if any)
32+
const annotations = getAllAnnotations(messages);
33+
if (annotations.length === 0) return [];
2634

2735
const ids: string[] = [];
2836

29-
for (const annotation of annotations) {
30-
const { type, data } = getValidAnnotation(annotation);
37+
for (const { type, data } of annotations) {
3138
if (
3239
type === "document_file" &&
3340
"files" in data &&
@@ -37,9 +44,7 @@ export function retrieveDocumentIds(annotations?: JSONValue[]): string[] {
3744
for (const file of files) {
3845
if (Array.isArray(file.content.value)) {
3946
// it's an array, so it's an array of doc IDs
40-
for (const id of file.content.value) {
41-
ids.push(id);
42-
}
47+
ids.push(...file.content.value);
4348
}
4449
}
4550
}
@@ -48,24 +53,69 @@ export function retrieveDocumentIds(annotations?: JSONValue[]): string[] {
4853
return ids;
4954
}
5055

51-
export function convertMessageContent(
52-
content: string,
53-
annotations?: JSONValue[],
54-
): MessageContent {
55-
if (!annotations) return content;
56+
export function retrieveMessageContent(messages: Message[]): MessageContent {
57+
const userMessage = messages[messages.length - 1];
5658
return [
5759
{
5860
type: "text",
59-
text: content,
61+
text: userMessage.content,
6062
},
61-
...convertAnnotations(annotations),
63+
...retrieveLatestArtifact(messages),
64+
...convertAnnotations(messages),
6265
];
6366
}
6467

65-
function convertAnnotations(annotations: JSONValue[]): MessageContentDetail[] {
68+
function getAllAnnotations(messages: Message[]): Annotation[] {
69+
return messages.flatMap((message) =>
70+
(message.annotations ?? []).map((annotation) =>
71+
getValidAnnotation(annotation),
72+
),
73+
);
74+
}
75+
76+
// get latest artifact from annotations to append to the user message
77+
function retrieveLatestArtifact(messages: Message[]): MessageContentDetail[] {
78+
const annotations = getAllAnnotations(messages);
79+
if (annotations.length === 0) return [];
80+
81+
for (const { type, data } of annotations.reverse()) {
82+
if (
83+
type === "tools" &&
84+
"toolCall" in data &&
85+
"toolOutput" in data &&
86+
typeof data.toolCall === "object" &&
87+
typeof data.toolOutput === "object" &&
88+
data.toolCall !== null &&
89+
data.toolOutput !== null &&
90+
"name" in data.toolCall &&
91+
data.toolCall.name === "artifact"
92+
) {
93+
const toolOutput = data.toolOutput as { output?: { code?: string } };
94+
if (toolOutput.output?.code) {
95+
return [
96+
{
97+
type: "text",
98+
text: `The existing code is:\n\`\`\`\n${toolOutput.output.code}\n\`\`\``,
99+
},
100+
];
101+
}
102+
}
103+
}
104+
return [];
105+
}
106+
107+
function convertAnnotations(messages: Message[]): MessageContentDetail[] {
108+
// annotations from the last user message that has annotations
109+
const annotations: Annotation[] =
110+
messages
111+
.slice()
112+
.reverse()
113+
.find((message) => message.role === "user" && message.annotations)
114+
?.annotations?.map(getValidAnnotation) || [];
115+
if (annotations.length === 0) return [];
116+
66117
const content: MessageContentDetail[] = [];
67-
annotations.forEach((annotation: JSONValue) => {
68-
const { type, data } = getValidAnnotation(annotation);
118+
annotations.forEach(({ type, data }) => {
69119
// convert image
70120
if (type === "image" && "url" in data && typeof data.url === "string") {
71121
content.push({

templates/components/llamaindex/typescript/streaming/events.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,6 @@ export function appendToolData(
6969
});
7070
}
7171

72-
export function createStreamTimeout(stream: StreamData) {
73-
const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
74-
const t = setTimeout(() => {
75-
appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
76-
stream.close();
77-
}, timeout);
78-
return t;
79-
}
80-
8172
export function createCallbackManager(stream: StreamData) {
8273
const callbackManager = new CallbackManager();
8374

templates/types/streaming/express/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import cors from "cors";
33
import "dotenv/config";
44
import express, { Express, Request, Response } from "express";
5+
import { sandbox } from "./src/controllers/sandbox.controller";
56
import { initObservability } from "./src/observability";
67
import chatRouter from "./src/routes/chat.route";
78

@@ -40,6 +41,7 @@ app.get("/", (req: Request, res: Response) => {
4041
});
4142

4243
app.use("/api/chat", chatRouter);
44+
app.use("/api/sandbox", sandbox);
4345

4446
app.listen(port, () => {
4547
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);

templates/types/streaming/express/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"llamaindex": "0.6.2",
2525
"pdf2json": "3.0.5",
2626
"ajv": "^8.12.0",
27-
"@e2b/code-interpreter": "^0.0.5",
27+
"@e2b/code-interpreter": "0.0.9-beta.3",
2828
"got": "^14.4.1",
2929
"@apidevtools/swagger-parser": "^10.1.0",
3030
"formdata-node": "^6.0.3",

0 commit comments

Comments
 (0)