UNPKG

create-agent-chat-app

Version:
682 lines (672 loc) 28.3 kB
#!/usr/bin/env node import path from "path"; import fs from "fs-extra"; import chalk from "chalk"; import { fileURLToPath } from "url"; import { execSync } from "child_process"; import { Command } from "commander"; import { BASE_GITIGNORE, NEXTJS_GITIGNORE, VITE_GITIGNORE, } from "./gitignore.js"; import { intro, confirm, select, multiselect, isCancel, cancel, text, } from "@clack/prompts"; // Get the directory name of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const VERSION = "0.1.4"; /** * Creates a .yarnrc.yml file in the base directory of the project. * * @param {string} baseDir - The base directory of the project * @param {ChalkInstance} chalk - The chalk instance for logging */ async function createYarnRcYml(baseDir, chalk) { const yarnRcYmlContents = `nodeLinker: node-modules enableImmutableInstalls: false `; const fileName = `.yarnrc.yml`; try { await fs.promises.writeFile(path.join(baseDir, fileName), yarnRcYmlContents); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to create ${fileName}`); } } /** * Sets the `packageManager` field in package.json, and the `overrides` field * to ensure the same version of @langchain/core is set across all workspaces. * * @param {PackageManager} packageManager - The package manager to use * @param {string} baseDir - The base directory of the project * @param {ChalkInstance} chalk - The chalk instance for logging */ async function setPackageJsonFields(packageManager, baseDir, chalk) { // Add the `packageManager` field to package.json const pkgManagerMap = { yarn: "yarn@3.5.1", pnpm: "pnpm@10.6.3", npm: "npm@11.2.1", }; // Overrides to ensure the same version of @langchain/core is set across all workspaces. const overridesPkgManagerMap = { yarn: "resolutions", pnpm: "resolutions", npm: "overrides", }; try { const pkgJsonPath = path.join(baseDir, "package.json"); const pkgJson = JSON.parse(await fs.promises.readFile(pkgJsonPath, "utf8")); pkgJson.packageManager = `${pkgManagerMap[packageManager]}`; pkgJson[overridesPkgManagerMap[packageManager]] = { "@langchain/core": "^0.3.42", }; if (packageManager === "npm") { delete pkgJson["resolutions"]; } await fs.promises.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2)); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to set package manager in package.json`); } } /** * Writes the .gitignore file for the project. This creates one in the root of the project, * along with one inside the web directory. * * @param {string} baseDir - The base directory of the project * @param {Framework} framework - The framework to use * @param {ChalkInstance} chalk - The chalk instance for logging */ async function writeGitignore(baseDir, framework, chalk) { try { const gitignorePath = path.join(baseDir, ".gitignore"); // Write the base .gitignore file in the root await fs.promises.writeFile(gitignorePath, BASE_GITIGNORE); // write the framework-specific .gitignore file inside baseDir/apps/web const frameworkGitignorePath = path.join(baseDir, "apps", "web", ".gitignore"); if (framework === "nextjs") { await fs.promises.writeFile(frameworkGitignorePath, NEXTJS_GITIGNORE); } else { await fs.promises.writeFile(frameworkGitignorePath, VITE_GITIGNORE); } } catch (_) { console.log(`${chalk.red("Error: ")} Failed to write .gitignore`); } } /** * Updates the 'graph' field in the 'langgraph.json' configuration * file with the selected prebuilt agents. * * @param {string} baseDir - The base directory of the project * @param {ChalkInstance} chalk - The chalk instance for logging * @param {IncludeAgents} args The prebuilt agents which are included in the project */ async function updateLangGraphConfig(baseDir, chalk, args) { try { const langGraphConfigPath = path.join(baseDir, "langgraph.json"); const config = JSON.parse(await fs.promises.readFile(langGraphConfigPath, "utf8")); if (args.includeReactAgent) { config.graphs["agent"] = "./apps/agents/src/react-agent/graph.ts:graph"; } if (args.includeMemoryAgent) { config.graphs["memory_agent"] = "./apps/agents/src/memory-agent/graph.ts:graph"; } if (args.includeResearchAgent) { config.graphs["research_agent"] = "./apps/agents/src/research-agent/retrieval-graph/graph.ts:graph"; config.graphs["research_index_graph"] = "./apps/agents/src/research-agent/index-graph/graph.ts:graph"; } if (args.includeRetrievalAgent) { config.graphs["retrieval_agent"] = "./apps/agents/src/retrieval-agent/graph.ts:graph"; } await fs.promises.writeFile(langGraphConfigPath, JSON.stringify(config, null, 2) + "\n"); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to update LangGraph config`); } } /** * Creates a message to display to the user after the project has been created. * * @param {ChalkInstance} chalk - The chalk instance for logging * @param {PackageManager} packageManager - The package manager to use * @param {Framework} framework - The framework to use * @returns {string} The message to display */ const createStartServersMessage = (chalk, packageManager, framework) => { return `Then, start both the web, and LangGraph development servers with one command: ${chalk.cyan(`${packageManager} run dev`)} This will start the web server at: ${chalk.cyan(framework === "nextjs" ? "http://localhost:3000" : "http://localhost:5173")} And the LangGraph server at: ${chalk.cyan("http://localhost:2024")}`; }; const AGENT_DEPENDENCIES_MAP = { "react-agent": { "@langchain/community": "^0.3.35", "@langchain/anthropic": "^0.3.15", }, "memory-agent": { "@langchain/anthropic": "^0.3.15", }, "research-agent": { "@langchain/anthropic": "^0.3.15", "@elastic/elasticsearch": "^8.17.1", "@langchain/community": "^0.3.35", "@langchain/pinecone": "^0.2.0", "@langchain/mongodb": "^0.1.0", mongodb: "^6.14.2", "@pinecone-database/pinecone": "^5.1.1", "@langchain/cohere": "^0.3.2", "@langchain/openai": "^0.4.4", }, "retrieval-agent": { "@langchain/anthropic": "^0.3.15", "@elastic/elasticsearch": "^8.17.1", "@langchain/community": "^0.3.35", "@langchain/pinecone": "^0.2.0", "@langchain/mongodb": "^0.1.0", mongodb: "^6.14.2", "@pinecone-database/pinecone": "^5.1.1", "@langchain/cohere": "^0.3.2", "@langchain/openai": "^0.4.4", }, }; /** * Updates the 'package.json' file inside the agents workspace to include * all the necessary dependencies for the selected agents. * * @param baseDir - The base directory of the project * @param inputs - Object containing the following properties: * @param inputs.agentArgs - The prebuilt agents which are included in the project * @param inputs.packageManager - The package manager being used (npm, yarn, etc.) * @param inputs.chalk - Chalk instance for terminal styling */ async function setAgentPackageJsonFields(baseDir, inputs) { const { agentArgs, packageManager, chalk } = inputs; try { const agentsPkgJsonPath = path.join(baseDir, "apps", "agents", "package.json"); const pkgJson = JSON.parse(await fs.promises.readFile(agentsPkgJsonPath, "utf8")); const requiredPackages = {}; if (agentArgs.includeReactAgent) { Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["react-agent"]); } if (agentArgs.includeMemoryAgent) { Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["memory-agent"]); } if (agentArgs.includeResearchAgent) { Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["research-agent"]); } if (agentArgs.includeRetrievalAgent) { Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["retrieval-agent"]); } pkgJson.dependencies = { ...pkgJson.dependencies, ...requiredPackages, }; // Update the scripts to call the correct package manager pkgJson.scripts["build:internal"] = pkgJson.scripts["build:internal"].replace("{PACKAGE_MANAGER}", packageManager); await fs.promises.writeFile(agentsPkgJsonPath, JSON.stringify(pkgJson, null, 2)); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to set agent package.json fields`); } } const AGENT_ENV_VARS_MAP = { "react-agent": ["TAVILY_API_KEY", "ANTHROPIC_API_KEY"], "memory-agent": ["ANTHROPIC_API_KEY"], "research-agent": [ "ANTHROPIC_API_KEY", "ELASTICSEARCH_URL", "ELASTICSEARCH_USER", "ELASTICSEARCH_PASSWORD", "ELASTICSEARCH_API_KEY", "MONGODB_URI", "PINECONE_API_KEY", "PINECONE_ENVIRONMENT", "PINECONE_INDEX_NAME", "COHERE_API_KEY", "OPENAI_API_KEY", ], "retrieval-agent": [ "ANTHROPIC_API_KEY", "ELASTICSEARCH_URL", "ELASTICSEARCH_USER", "ELASTICSEARCH_PASSWORD", "ELASTICSEARCH_API_KEY", "MONGODB_URI", "PINECONE_API_KEY", "PINECONE_ENVIRONMENT", "PINECONE_INDEX_NAME", "COHERE_API_KEY", "OPENAI_API_KEY", ], }; /** * Creates a '.env.example' file containing all the required environment variables, * depending on the selected agents. * * @param {string} baseDir - The base directory of the project * @param {IncludeAgents} args The prebuilt agents which are included in the project * @param {ChalkInstance} chalk - The chalk instance for logging */ async function setEnvExampleFile(baseDir, args, chalk) { try { const envExamplePath = path.join(baseDir, ".env.example"); const requiredEnvVarsSet = new Set(); if (args.includeReactAgent) { AGENT_ENV_VARS_MAP["react-agent"].forEach((v) => requiredEnvVarsSet.add(v)); } if (args.includeMemoryAgent) { AGENT_ENV_VARS_MAP["memory-agent"].forEach((v) => requiredEnvVarsSet.add(v)); } if (args.includeResearchAgent) { AGENT_ENV_VARS_MAP["research-agent"].forEach((v) => requiredEnvVarsSet.add(v)); } if (args.includeRetrievalAgent) { AGENT_ENV_VARS_MAP["retrieval-agent"].forEach((v) => requiredEnvVarsSet.add(v)); } const requiredEnvVars = Array.from(requiredEnvVarsSet); const baseEnvVars = `# LANGSMITH_API_KEY="" # LANGSMITH_TRACING_V2="true" # LANGSMITH_PROJECT="default"`; const envExampleContent = `${baseEnvVars}\n\n${requiredEnvVars .map((envVar) => `${envVar}=""`) .join("\n")}`; await fs.promises.writeFile(envExamplePath, envExampleContent); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to set env example file`); } } /** * PNPM manages workspaces differently than NPM/Yarn, so if the user * selects PNPM as their package manager, we need to create a pnpm-workspace.yaml * file, and remove the workspaces field from the root package.json file. * * @param {string} baseDir - The base directory of the project * @param {ChalkInstance} chalk - The chalk instance for logging */ async function createPnpmWorkspacesFile(baseDir, chalk) { try { // first read package.json file const packageJsonPath = path.join(baseDir, "package.json"); const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf8")); // Remove the workspaces field delete packageJson.workspaces; // Write the updated package.json file await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); const pnpmWorkspacesPath = path.join(baseDir, "pnpm-workspace.yaml"); const pnpmWorkspacesContents = `packages: - 'apps/*' `; await fs.promises.writeFile(pnpmWorkspacesPath, pnpmWorkspacesContents); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to create pnpm workspaces file`); } } /** * PNPM does not resolve the LangGraph Checkpoint package direct dependency * the same way NPM/Yarn do. For this reason, if the user selects PNPM as their * package manager, we need to explicitly install it in the 'agents' workspace. * * @param {string} baseDir - The base directory of the project * @param {ChalkInstance} chalk - The chalk instance for logging */ async function addPnpmDirectDependencyWorkaround(baseDir, chalk) { try { const agentsPkgJsonPath = path.join(baseDir, "apps", "agents", "package.json"); const pkgJson = JSON.parse(await fs.promises.readFile(agentsPkgJsonPath, "utf8")); const additionalPackages = { "@langchain/langgraph-checkpoint": "^0.0.16", }; pkgJson.dependencies = { ...pkgJson.dependencies, ...additionalPackages, }; await fs.promises.writeFile(agentsPkgJsonPath, JSON.stringify(pkgJson, null, 2)); } catch (_) { console.log(`${chalk.red("Error: ")} Failed to update agents package.json dependencies`); } } /** * Parse command-line arguments and return project configuration. * If all required arguments are provided, this will bypass the interactive prompts. * If only some arguments are provided, the user will be prompted for the remaining ones. */ async function parseCommandLineArgs() { const program = new Command(); program .name("create-agent-chat-app") .description("Create an agent chat app with one command") .version(VERSION) .option("-Y, --yes", "Skip all prompts and use default values") .option("--project-name <name>", "Name of the project (default: agent-chat-app)") .option("--package-manager <manager>", "Package manager to use (npm, pnpm, yarn) (default: yarn)") .option("--install-deps <boolean>", "Automatically install dependencies (default: true)") .option("--framework <framework>", "Framework to use (nextjs, vite) (default: nextjs)") .option("--include-agent <agent...>", "Pre-built agents to include (react, memory, research, retrieval) (default: all)") .allowUnknownOption(); program.parse(); const options = program.opts(); const result = {}; // Only include options that were explicitly provided by the user if ("projectName" in options) { result.projectName = options.projectName; } if ("packageManager" in options) { if (["npm", "pnpm", "yarn"].includes(options.packageManager)) { result.packageManager = options.packageManager; } } if ("installDeps" in options) { result.autoInstallDeps = options.installDeps.toLowerCase() === "true"; } if ("framework" in options) { if (["nextjs", "vite"].includes(options.framework)) { result.framework = options.framework; } } if ("includeAgent" in options) { const selectedAgents = Array.isArray(options.includeAgent) ? options.includeAgent : [options.includeAgent]; result.includeReactAgent = selectedAgents.includes("react"); result.includeMemoryAgent = selectedAgents.includes("memory"); result.includeResearchAgent = selectedAgents.includes("research"); result.includeRetrievalAgent = selectedAgents.includes("retrieval"); } // If -Y or --yes flag is provided, use all defaults if (options.yes) { return { projectName: result.projectName ?? "agent-chat-app", packageManager: result.packageManager ?? "yarn", autoInstallDeps: result.autoInstallDeps ?? true, framework: result.framework ?? "nextjs", includeReactAgent: result.includeReactAgent ?? true, includeMemoryAgent: result.includeMemoryAgent ?? true, includeResearchAgent: result.includeResearchAgent ?? true, includeRetrievalAgent: result.includeRetrievalAgent ?? true, }; } return result; } /** * Prompt the user for any missing configuration options. * If a value is already provided in partialAnswers, the user won't be prompted for it. */ async function promptUser(partialAnswers = {}) { intro(chalk.green(" create-agent-chat-app ")); // Project name prompt let projectName = partialAnswers.projectName; if (!projectName) { const projectNameResponse = await text({ message: "What is the name of your project?", placeholder: "agent-chat-app", defaultValue: "agent-chat-app", }); if (isCancel(projectNameResponse)) { cancel("Operation cancelled"); process.exit(0); } projectName = projectNameResponse; } // Package manager prompt let packageManager = partialAnswers.packageManager; if (!packageManager) { const packageManagerResponse = await select({ message: "Which package manager would you like to use?", options: [ { value: "npm", label: "npm" }, { value: "pnpm", label: "pnpm" }, { value: "yarn", label: "yarn" }, ], }); if (isCancel(packageManagerResponse)) { cancel("Operation cancelled"); process.exit(0); } packageManager = packageManagerResponse; } // Auto install dependencies prompt let autoInstallDeps = partialAnswers.autoInstallDeps; if (autoInstallDeps === undefined) { const autoInstallDepsResponse = await confirm({ message: "Would you like to automatically install dependencies?", initialValue: true, }); if (isCancel(autoInstallDepsResponse)) { cancel("Operation cancelled"); process.exit(0); } autoInstallDeps = autoInstallDepsResponse; } // Framework prompt let framework = partialAnswers.framework; if (!framework) { const frameworkResponse = await select({ message: "Which framework would you like to use?", options: [ { value: "nextjs", label: "Next.js" }, { value: "vite", label: "Vite" }, ], }); if (isCancel(frameworkResponse)) { cancel("Operation cancelled"); process.exit(0); } framework = frameworkResponse; } // Check if all agent selections are already provided const allAgentSelectionsProvided = partialAnswers.includeReactAgent !== undefined && partialAnswers.includeMemoryAgent !== undefined && partialAnswers.includeResearchAgent !== undefined && partialAnswers.includeRetrievalAgent !== undefined; // Agent selection prompt if not all provided let includeReactAgent = partialAnswers.includeReactAgent ?? false; let includeMemoryAgent = partialAnswers.includeMemoryAgent ?? false; let includeResearchAgent = partialAnswers.includeResearchAgent ?? false; let includeRetrievalAgent = partialAnswers.includeRetrievalAgent ?? false; if (!allAgentSelectionsProvided) { const selectedAgentsResponse = await multiselect({ message: 'Which pre-built agents would you like to include? (Press "space" to select/unselect)', options: [ { value: "react", label: "ReAct Agent" }, { value: "memory", label: "Memory Agent" }, { value: "research", label: "Research Agent" }, { value: "retrieval", label: "Retrieval Agent" }, ], initialValues: ["react", "memory", "research", "retrieval"], required: false, }); if (isCancel(selectedAgentsResponse)) { cancel("Operation cancelled"); process.exit(0); } const selectedAgents = selectedAgentsResponse; // Determine which agents are included includeReactAgent = selectedAgents.includes("react"); includeMemoryAgent = selectedAgents.includes("memory"); includeResearchAgent = selectedAgents.includes("research"); includeRetrievalAgent = selectedAgents.includes("retrieval"); } // Combine all answers return { packageManager, autoInstallDeps, projectName, framework, includeReactAgent, includeMemoryAgent, includeResearchAgent, includeRetrievalAgent, }; } async function init() { // Parse command-line arguments first const cliOptions = await parseCommandLineArgs(); const allRequiredOptionsProvided = cliOptions.autoInstallDeps !== undefined && cliOptions.projectName !== undefined && cliOptions.packageManager !== undefined && cliOptions.framework !== undefined && cliOptions.includeReactAgent !== undefined && cliOptions.includeMemoryAgent !== undefined && cliOptions.includeResearchAgent !== undefined && cliOptions.includeRetrievalAgent !== undefined; // If all options are provided via CLI, use them directly // Otherwise, prompt for the missing options const answers = allRequiredOptionsProvided ? cliOptions : await promptUser(cliOptions); const { projectName, packageManager, autoInstallDeps, framework } = answers; // Create project directory const targetDir = path.join(process.cwd(), projectName); if (await fs.exists(targetDir)) { console.error(chalk.red(`Error: Directory ${projectName} already exists.`)); process.exit(1); } // Log the collected values console.log(`Project will be created at: ${chalk.green(targetDir)}\n`); console.log(`Framework: ${chalk.green(framework)}`); const includesAgentSelectionsMap = { includeReactAgent: answers.includeReactAgent, includeMemoryAgent: answers.includeMemoryAgent, includeResearchAgent: answers.includeResearchAgent, includeRetrievalAgent: answers.includeRetrievalAgent, }; if (Object.values(includesAgentSelectionsMap).every(Boolean)) { console.log(`Including: ${chalk.green("All pre-built agents")}`); } else { const selectedAgents = []; if (includesAgentSelectionsMap.includeReactAgent) selectedAgents.push("ReAct"); if (includesAgentSelectionsMap.includeMemoryAgent) selectedAgents.push("Memory"); if (includesAgentSelectionsMap.includeResearchAgent) selectedAgents.push("Research"); if (includesAgentSelectionsMap.includeRetrievalAgent) selectedAgents.push("Retrieval"); if (selectedAgents.length > 0) { console.log(`Including agents: ${chalk.green(selectedAgents.join(", "))}`); } else { console.log(`No additional agents selected.`); } } console.log(chalk.yellow("Creating project files...")); // Create the project directory await fs.mkdir(targetDir, { recursive: true }); // Copy the monorepo template to the target directory const monorepoTemplateDir = path.join(__dirname, "templates", "monorepo"); await fs.copy(monorepoTemplateDir, targetDir); await Promise.all([ updateLangGraphConfig(targetDir, chalk, includesAgentSelectionsMap), setAgentPackageJsonFields(targetDir, { agentArgs: includesAgentSelectionsMap, packageManager, chalk, }), setEnvExampleFile(targetDir, includesAgentSelectionsMap, chalk), ]); // Create web directory inside apps and copy the framework template const appsDir = path.join(targetDir, "apps"); const webDir = path.join(appsDir, "web"); await fs.mkdir(webDir, { recursive: true }); // Copy the framework template to the web directory const frameworkTemplateDir = path.join(__dirname, "templates", framework); await fs.copy(frameworkTemplateDir, webDir); await writeGitignore(targetDir, framework, chalk); // Get the path to the agents src directory which already exists in the monorepo template const agentsDir = path.join(appsDir, "agents", "src"); // Copy agent templates if selected - run in parallel for better performance const copyOperations = []; if (answers.includeReactAgent) { copyOperations.push(copyAgentTemplate("react-agent", agentsDir)); } if (answers.includeMemoryAgent) { copyOperations.push(copyAgentTemplate("memory-agent", agentsDir)); } if (answers.includeResearchAgent) { copyOperations.push(copyAgentTemplate("research-agent", agentsDir)); } if (answers.includeRetrievalAgent) { copyOperations.push(copyAgentTemplate("retrieval-agent", agentsDir)); } // Execute all copy operations concurrently await Promise.all(copyOperations); // Update root package.json with project name const rootPkgJsonPath = path.join(targetDir, "package.json"); if (await fs.exists(rootPkgJsonPath)) { const rootPkgJson = JSON.parse(await fs.readFile(rootPkgJsonPath, "utf8")); rootPkgJson.name = projectName; await fs.writeFile(rootPkgJsonPath, JSON.stringify(rootPkgJson, null, 2)); } if (packageManager === "yarn") { await createYarnRcYml(targetDir, chalk); } if (packageManager === "pnpm") { await Promise.all([ createPnpmWorkspacesFile(targetDir, chalk), addPnpmDirectDependencyWorkaround(targetDir, chalk), ]); } await setPackageJsonFields(packageManager, targetDir, chalk); // Install dependencies if autoInstallDeps is true if (autoInstallDeps) { console.log(chalk.yellow("\nInstalling dependencies...")); try { // Navigate to the project directory and run the install command process.chdir(targetDir); execSync(`${packageManager} install`, { stdio: "inherit" }); console.log(chalk.green("\nDependencies installed successfully!")); console.log(chalk.green("\nSuccess!")); console.log(` Your agent chat app has been created at ${chalk.green(targetDir)} To get started: ${chalk.cyan(`cd ${projectName}`)} ${createStartServersMessage(chalk, packageManager, framework)} `); } catch (error) { console.error(chalk.red("\nFailed to install dependencies:"), error); console.log(` Your agent chat app has been created, but dependencies could not be installed automatically. To get started: ${chalk.cyan(`cd ${projectName}`)} ${chalk.cyan(`${packageManager} install`)} ${createStartServersMessage(chalk, packageManager, framework)} `); } // Return early to not output the logs for when no auto install is requested. return; } // No auto install console.log(chalk.green("\nSuccess!")); console.log(` Your agent chat app has been created at ${chalk.green(targetDir)} To get started: ${chalk.cyan(`cd ${projectName}`)} ${chalk.cyan(`${packageManager} install`)} ${createStartServersMessage(chalk, packageManager, framework)} `); } // Helper function to copy agent templates async function copyAgentTemplate(agentName, agentsDir) { const agentTemplateDir = path.join(__dirname, "templates", agentName); // Determine the destination directory for the agent const agentDestDir = path.join(agentsDir, agentName); // Create the destination directory if it doesn't exist await fs.mkdir(agentDestDir, { recursive: true }); // Copy the agent template files await fs.copy(agentTemplateDir, agentDestDir); console.log(`${chalk.green("✓")} Added ${chalk.cyan(agentName)}`); } init().catch((err) => { console.error(chalk.red("Error:"), err); process.exit(1); });