diff --git a/README.md b/README.md index 9898fba..7565b19 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@ The multi-agents are implemented using [Workflows](https://ts.llamaindex.ai/modu ## Getting Started 1. Clone the repository: + ``` git clone https://github.com/run-llama/app-creator.git cd app-creator ``` 2. Install dependencies: + ``` pnpm install ``` diff --git a/agent.ts b/agent.ts index e9cca1c..82e5acc 100644 --- a/agent.ts +++ b/agent.ts @@ -6,13 +6,14 @@ import { WorkflowEvent, } from "@llamaindex/core/workflow"; import { OpenAI, Settings } from "llamaindex"; +import { PackageEvent, packager } from "./packager"; const MAX_REVIEWS = 3; // Create custom event types export class MessageEvent extends WorkflowEvent<{ msg: string }> {} -export class CodeEvent extends WorkflowEvent<{ code: string }> {} -export class ReviewEvent extends WorkflowEvent<{ +class CodeEvent extends WorkflowEvent<{ code: string }> {} +class ReviewEvent extends WorkflowEvent<{ review: string; code: string; }> {} @@ -88,7 +89,7 @@ const reviewer = async (context: Context, ev: CodeEvent) => { msg: `Reviewer says: ${review}`, }), ); - return new StopEvent({ result: code }); + return new PackageEvent({ code }); } return new ReviewEvent({ review, code }); @@ -98,7 +99,10 @@ export function createAgent(model: string): Workflow { const codeAgent = new Workflow({ validate: true }); codeAgent.addStep(StartEvent, architect, { outputs: CodeEvent }); codeAgent.addStep(ReviewEvent, coder, { outputs: CodeEvent }); - codeAgent.addStep(CodeEvent, reviewer, { outputs: ReviewEvent }); + codeAgent.addStep(CodeEvent, reviewer, { + outputs: [ReviewEvent, PackageEvent], + }); + codeAgent.addStep(PackageEvent, packager, { outputs: StopEvent }); // Update the llm model with the provided model Settings.llm = new OpenAI({ model, temperature: 1 }); diff --git a/main.ts b/main.ts index f715da3..93a8597 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,7 @@ import { createAgent, MessageEvent } from "./agent"; -import * as fs from 'fs/promises'; -import * as path from 'path'; +import * as fs from "fs/promises"; +import * as path from "path"; +import { PackageResult } from "./packager"; const apps = [ { @@ -9,22 +10,45 @@ const apps = [ database where they are mapped to answers. If there is a close match, it retrieves the matched answer. If there isn't, it asks the user to provide an answer and stores the question/answer pair in the database.`, - models: ["gpt-4o-mini", "gpt-4o", "o1-mini", "o1-preview"] + models: ["gpt-4o-mini", "gpt-4o", "o1-mini", "o1-preview"], }, { name: "JavaScript Todo App", spec: `Create a full-stack NextJS todo app with a TailwindCSS frontend, that allows users to add, remove, and mark tasks as complete. Use a Postgres database to persist the tasks.`, - models: ["gpt-4o-mini", "gpt-4o", "o1-mini"] // didn't complete with o1-preview + models: ["gpt-4o-mini", "gpt-4o", "o1-mini"], // didn't complete with o1-preview }, { name: "Iphone calculator", spec: `Iphone style scientific calculator in one html file, using tailwind css and javascript.`, - models: ["gpt-4o-mini", "gpt-4o", "o1-mini", "o1-preview"] - } + models: ["gpt-4o-mini", "gpt-4o", "o1-mini", "o1-preview"], + }, // Add more specifications as needed ]; +async function outputResult( + name: string, + model: string, + packageResult: PackageResult, +) { + // Create output directory if it doesn't exist + const outputDir = path.join("output", name, model); + await fs.mkdir(outputDir, { recursive: true }); + + // Iterate over all files in the packageResult and store each file with their correct path + for (const file of packageResult.files) { + const filePath = path.join(outputDir, file.path); + const fileDir = path.dirname(filePath); + + // Create directory if it doesn't exist + await fs.mkdir(fileDir, { recursive: true }); + + // Write file content + await fs.writeFile(filePath, file.content); + console.log(`File written to: ${filePath}`); + } +} + async function runGeneration(name: string, spec: string, model: string) { console.log(`Running generation with model: ${model}`); const codeAgent = createAgent(model); @@ -34,15 +58,9 @@ async function runGeneration(name: string, spec: string, model: string) { console.log(`${msg}\n`); } const result = await run; - - // Create output directory if it doesn't exist - const outputDir = path.join('output', name); - await fs.mkdir(outputDir, { recursive: true }); - - // Write the generated code to a file - const outputFile = path.join(outputDir, `${model}.code`); - await fs.writeFile(outputFile, result.data.result); - console.log(`Generated code written to: ${outputFile}\n`); + const packageResult = result.data.result as unknown as PackageResult; + + await outputResult(name, model, packageResult); } async function main() { diff --git a/package.json b/package.json index 34a06f6..5c0eba5 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "license": "ISC", "dependencies": { "@llamaindex/core": "^0.2.0", - "llamaindex": "^0.6.0" + "llamaindex": "^0.6.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.3" }, "devDependencies": { "prettier": "^3.3.3", "tsx": "^4.19.1" } -} \ No newline at end of file +} diff --git a/packager.ts b/packager.ts new file mode 100644 index 0000000..2ecca23 --- /dev/null +++ b/packager.ts @@ -0,0 +1,50 @@ +import { zodToJsonSchema } from "zod-to-json-schema"; +import { Context, StopEvent, WorkflowEvent } from "@llamaindex/core/workflow"; +import { OpenAI } from "llamaindex"; +import { z } from "zod"; + +export class PackageEvent extends WorkflowEvent<{ + code: string; +}> {} + +const FileSchema = z.object({ + path: z.string().describe("Path to the filename, e.g., 'app/main.py'"), + content: z.string().describe("Complete content of the file"), +}); + +const PackageResultSchema = z.object({ + files: z.array(FileSchema), +}); + +export type PackageResult = z.infer; + +export const packager = async (context: Context, ev: PackageEvent) => { + const { code } = ev.data; + + // use own llm for extracting the files + const llm = new OpenAI({ + model: "gpt-4o-mini", + additionalChatOptions: { response_format: { type: "json_object" } }, + }); + + const schema = JSON.stringify(zodToJsonSchema(PackageResultSchema)); + + const response = await llm.chat({ + messages: [ + { + role: "system", + content: `You are an expert in extracting single files (path and content) from one large string.\n\nGenerate a valid JSON following the given schema below:\n\n${schema}`, + }, + { + role: "user", + content: `Here is the large string: \n------\n${code}\n------`, + }, + ], + }); + + const json = response.message.content as string; + const result = PackageResultSchema.parse(JSON.parse(json)); + + // TODO: allow different types of outputs in LlamaIndexTS + return new StopEvent({ result: result as unknown as string }); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ef20f2..bf59c8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,12 @@ importers: llamaindex: specifier: ^0.6.0 version: 0.6.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/credential-providers@3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0)))(@notionhq/client@2.2.15(encoding@0.1.13))(encoding@0.1.13)(typescript@5.6.2) + zod: + specifier: ^3.23.8 + version: 3.23.8 + zod-to-json-schema: + specifier: ^3.23.3 + version: 3.23.3(zod@3.23.8) devDependencies: prettier: specifier: ^3.3.3 @@ -3738,6 +3744,14 @@ packages: } engines: { node: ">=12" } + zod-to-json-schema@3.23.3: + resolution: + { + integrity: sha512-TYWChTxKQbRJp5ST22o/Irt9KC5nj7CdBKYB/AosCRdj/wxEMvv4NNaj9XVUHDOIp53ZxArGhnw5HMZziPFjog==, + } + peerDependencies: + zod: ^3.23.3 + zod@3.23.8: resolution: { @@ -6532,4 +6546,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + zod-to-json-schema@3.23.3(zod@3.23.8): + dependencies: + zod: 3.23.8 + zod@3.23.8: {}