create-nodex
Version:
CLI tool to create modern Node.js projects with TypeScript, AI capabilities, and more
677 lines (651 loc) • 24.8 kB
JavaScript
import inquirer from "inquirer";
import fsExtra from "fs-extra";
import { join, dirname } from "path";
import { execSync } from "child_process";
import { readFileSync, existsSync } from "fs";
import { fileURLToPath } from "url";
import chalk from "chalk";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const { copySync, ensureDirSync, writeFileSync } = fsExtra;
const availableModels = [
{
name: "OpenAI (e.g., GPT-4, GPT-3.5 Turbo)",
value: "openai",
packageName: "@langchain/openai",
apiKeyEnv: "OPENAI_API_KEY",
},
{
name: "Groq (e.g., Llama, Mixtral)",
value: "groq",
packageName: "@langchain/groq",
apiKeyEnv: "GROQ_API_KEY",
},
{
name: "Anthropic (e.g., Claude)",
value: "anthropic",
packageName: "@langchain/anthropic",
apiKeyEnv: "ANTHROPIC_API_KEY",
},
{
name: "Google Gemini",
value: "google-gemini",
packageName: "@langchain/google-genai",
apiKeyEnv: "GOOGLE_API_KEY",
},
{
name: "FireworksAI",
value: "fireworks",
packageName: "@langchain/community",
apiKeyEnv: "FIREWORKS_API_KEY",
},
{
name: "MistralAI",
value: "mistral",
packageName: "@langchain/mistralai",
apiKeyEnv: "MISTRAL_API_KEY",
},
{
name: "Google VertexAI",
value: "google-vertexai",
packageName: "@langchain/google-vertexai",
apiKeyEnv: "GOOGLE_APPLICATION_CREDENTIALS",
},
];
const availableEmbeddings = [
{
name: "OpenAI",
value: "openai",
packageName: "@langchain/openai",
apiKeyEnv: "OPENAI_API_KEY",
className: "OpenAIEmbeddings",
importPath: "@langchain/openai",
modelName: "text-embedding-3-large",
},
{
name: "Azure OpenAI",
value: "azure",
packageName: "@langchain/openai",
apiKeyEnv: "AZURE_OPENAI_API_KEY",
className: "AzureOpenAIEmbeddings",
importPath: "@langchain/openai",
modelName: "text-embedding-ada-002",
additionalEnvVars: [
"AZURE_OPENAI_API_INSTANCE_NAME",
"AZURE_OPENAI_API_VERSION",
],
},
{
name: "AWS Bedrock",
value: "aws",
packageName: "@langchain/aws",
apiKeyEnv: "BEDROCK_AWS_REGION",
className: "BedrockEmbeddings",
importPath: "@langchain/aws",
modelName: "amazon.titan-embed-text-v1",
},
{
name: "Google VertexAI",
value: "vertexai",
packageName: "@langchain/google-vertexai",
apiKeyEnv: "GOOGLE_APPLICATION_CREDENTIALS",
className: "VertexAIEmbeddings",
importPath: "@langchain/google-vertexai",
modelName: "text-embedding-004",
},
{
name: "Cohere",
value: "cohere",
packageName: "@langchain/cohere",
apiKeyEnv: "COHERE_API_KEY",
className: "CohereEmbeddings",
importPath: "@langchain/cohere",
modelName: "embed-english-v3.0",
},
{
name: "MistralAI",
value: "mistral",
packageName: "@langchain/mistralai",
apiKeyEnv: "MISTRAL_API_KEY",
className: "MistralAIEmbeddings",
importPath: "@langchain/mistralai",
modelName: "mistral-embed",
},
];
const availableVectorStores = [
{
name: "Memory (In-memory, no persistence)",
value: "memory",
packageName: "langchain",
className: "MemoryVectorStore",
importPath: "langchain/vectorstores/memory",
requiresApiKey: false,
},
{
name: "Chroma",
value: "chroma",
packageName: "chromadb",
className: "Chroma",
importPath: "@langchain/community/vectorstores/chroma",
requiresApiKey: false,
},
{
name: "FAISS",
value: "faiss",
packageName: "@langchain/community",
className: "FaissStore",
importPath: "@langchain/community/vectorstores/faiss",
requiresApiKey: false,
},
{
name: "MongoDB Atlas",
value: "mongodb",
packageName: "@langchain/mongodb",
className: "MongoDBAtlasVectorSearch",
importPath: "@langchain/mongodb",
requiresApiKey: true,
apiKeyEnv: "MONGODB_ATLAS_URI",
additionalEnvVars: [
"MONGODB_ATLAS_DB_NAME",
"MONGODB_ATLAS_COLLECTION_NAME",
],
},
{
name: "PGVector (PostgreSQL)",
value: "pgvector",
packageName: "@langchain/community",
className: "PGVectorStore",
importPath: "@langchain/community/vectorstores/pgvector",
requiresApiKey: true,
apiKeyEnv: "PG_CONNECTION_STRING",
},
{
name: "Pinecone",
value: "pinecone",
packageName: "@pinecone-database/pinecone",
className: "PineconeStore",
importPath: "@langchain/pinecone",
apiKeyEnv: "PINECONE_API_KEY",
requiresApiKey: true,
additionalEnvVars: ["PINECONE_INDEX", "PINECONE_ENVIRONMENT"],
},
{
name: "Qdrant",
value: "qdrant",
packageName: "@langchain/qdrant",
className: "QdrantVectorStore",
importPath: "@langchain/qdrant",
requiresApiKey: false,
additionalEnvVars: ["QDRANT_URL"],
},
];
async function addAiRag() {
console.log(`${chalk.cyan.bold("Adding Langchain RAG features to your project...")}`);
const currentDir = process.cwd();
const pkgPath = join(currentDir, "package.json");
if (!existsSync(pkgPath)) {
console.error(`${chalk.bgRed.white(" ERROR ")} ${chalk.red("package.json not found. Make sure you're in a project directory.")}`);
process.exit(1);
}
const pkgData = JSON.parse(readFileSync(pkgPath, "utf8"));
const isTypeScript = (pkgData.devDependencies && pkgData.devDependencies.typescript) ||
(pkgData.dependencies && pkgData.dependencies.typescript);
console.log(`${chalk.blue("Detected")} ${chalk.blue.bold(isTypeScript ? "TypeScript" : "JavaScript")} ${chalk.blue("project.")}`);
// Model selection
const { selectedModel } = await inquirer.prompt([
{
type: "list",
name: "selectedModel",
message: "Which chat model would you like to use with RAG?",
choices: availableModels.map((m) => ({ name: m.name, value: m.value })),
},
]);
const modelChoice = availableModels.find((m) => m.value === selectedModel);
if (!modelChoice) {
console.error(`${chalk.bgRed.white(" ERROR ")} ${chalk.red("Invalid model selection.")}`);
process.exit(1);
}
// Embeddings selection
const { selectedEmbedding } = await inquirer.prompt([
{
type: "list",
name: "selectedEmbedding",
message: "Which embedding model would you like to use?",
choices: availableEmbeddings.map((e) => ({
name: e.name,
value: e.value,
})),
},
]);
const embeddingChoice = availableEmbeddings.find((e) => e.value === selectedEmbedding);
if (!embeddingChoice) {
console.error(`${chalk.bgRed.white(" ERROR ")} ${chalk.red("Invalid embedding model selection.")}`);
process.exit(1);
}
// Vector store selection
const { selectedVectorStore } = await inquirer.prompt([
{
type: "list",
name: "selectedVectorStore",
message: "Which vector store would you like to use?",
choices: availableVectorStores.map((vs) => ({
name: vs.name,
value: vs.value,
})),
},
]);
const vectorStoreChoice = availableVectorStores.find((vs) => vs.value === selectedVectorStore);
if (!vectorStoreChoice) {
console.error(`${chalk.bgRed.white(" ERROR ")} ${chalk.red("Invalid vector store selection.")}`);
process.exit(1);
}
console.log(`${chalk.cyan(`Setting up RAG with ${modelChoice.name}, ${embeddingChoice.name} embeddings, and ${vectorStoreChoice.name}...`)}`);
// Create directory structure in user's project
const srcDir = join(currentDir, "src");
const aiDir = join(srcDir, "ai");
const ragDir = join(aiDir, "rag");
ensureDirSync(srcDir);
ensureDirSync(aiDir);
ensureDirSync(ragDir);
// Generate RAG example file
const ragExampleFile = `rag-example.${isTypeScript ? "ts" : "js"}`;
const ragExamplePath = join(ragDir, ragExampleFile);
// Create RAG example content
let ragExampleContent = createRagExample(isTypeScript, modelChoice, embeddingChoice, vectorStoreChoice);
if (existsSync(ragExamplePath)) {
const { overwriteRag } = await inquirer.prompt([
{
type: "confirm",
name: "overwriteRag",
message: `${chalk.yellow("⚠️")} ${ragExampleFile} already exists in src/ai/rag/. Overwrite?`,
default: false,
},
]);
if (!overwriteRag) {
console.log(`${chalk.yellow(`Skipping ${ragExampleFile}...`)}`);
}
else {
writeFileSync(ragExamplePath, ragExampleContent);
console.log(`${chalk.green(`✓ Created ${ragExampleFile}`)}`);
}
}
else {
writeFileSync(ragExamplePath, ragExampleContent);
console.log(`${chalk.green(`✓ Created ${ragExampleFile}`)}`);
}
// Collect all required dependencies
const depsToInstall = [
"langchain",
"@langchain/core",
modelChoice.packageName,
embeddingChoice.packageName,
];
// Add vector store package
if (vectorStoreChoice.packageName !== "langchain") {
depsToInstall.push(vectorStoreChoice.packageName);
}
// Add text splitter and document loader
depsToInstall.push("@langchain/textsplitters", "@langchain/community");
// Add dotenv for API keys
depsToInstall.push("dotenv");
// Deduplicate dependencies
const uniqueDeps = [...new Set(depsToInstall)];
console.log(`\n${chalk.cyan("Installing Langchain RAG dependencies:")} ${uniqueDeps.join(", ")}`);
execSync(`npm install ${uniqueDeps.join(" ")}`, { stdio: "inherit" });
if (isTypeScript) {
const devDepsToInstall = ["@types/node"];
// Add types for dotenv
devDepsToInstall.push("@types/dotenv");
console.log(`\n${chalk.cyan("Installing TypeScript development dependencies:")} ${devDepsToInstall.join(", ")}`);
execSync(`npm install --save-dev ${devDepsToInstall.join(" ")}`, {
stdio: "inherit",
});
}
console.log(`\n${chalk.bgGreen.black(" SUCCESS ")} ${chalk.green.bold(`Langchain RAG with ${modelChoice.name}, ${embeddingChoice.name} embeddings, and ${vectorStoreChoice.name} added successfully!`)} ${chalk.green("🚀")}`);
console.log(`\n${chalk.cyan.bold("Next steps:")}`);
if (existsSync(ragExamplePath)) {
console.log(` - Explore the RAG example in ${chalk.yellow(`src/ai/rag/${ragExampleFile}`)}.`);
console.log(` - You can run it with: ${chalk.yellow(isTypeScript
? `npx tsx src/ai/rag/${ragExampleFile}`
: `node src/ai/rag/${ragExampleFile}`)}`);
}
// Collect all API keys needed
const requiredEnvVars = new Set();
if (modelChoice.apiKeyEnv) {
requiredEnvVars.add(modelChoice.apiKeyEnv);
}
if (embeddingChoice.apiKeyEnv) {
requiredEnvVars.add(embeddingChoice.apiKeyEnv);
}
if (vectorStoreChoice.requiresApiKey && vectorStoreChoice.apiKeyEnv) {
requiredEnvVars.add(vectorStoreChoice.apiKeyEnv);
// Add additional env vars if any
if (vectorStoreChoice.additionalEnvVars) {
vectorStoreChoice.additionalEnvVars.forEach((env) => requiredEnvVars.add(env));
}
}
if (requiredEnvVars.size > 0) {
console.log(`\n${chalk.yellow.bold("IMPORTANT:")} Set these environment variables:`);
requiredEnvVars.forEach((envVar) => {
console.log(` - ${chalk.yellow(envVar)}=${envVar.includes("KEY") ? "your-api-key" : "your-value"}`);
});
console.log(` Create a ${chalk.yellow(".env")} file with these variables in your project root.`);
}
}
function createRagExample(isTypeScript, modelChoice, embeddingChoice, vectorStoreChoice) {
const extension = isTypeScript ? "ts" : "js";
const typeAnnotations = isTypeScript ? ": string" : "";
return `${isTypeScript
? "// RAG Example with Langchain\n"
: "// RAG Example with Langchain\n"}
import dotenv from "dotenv";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { Document } from "@langchain/core/documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
${createImports(modelChoice, embeddingChoice, vectorStoreChoice)}
dotenv.config();
async function main() {
console.log("Initializing RAG system...");
// Check for required API keys
${createApiKeyChecks(modelChoice, embeddingChoice, vectorStoreChoice)}
// Initialize models
const llm = ${createLlmInitialization(modelChoice)};
const embeddings = ${createEmbeddingsInitialization(embeddingChoice)};
// Initialize vector store
${createVectorStoreInitialization(vectorStoreChoice)}
// Sample document data for testing
const sampleDocs = [
new Document({
pageContent: "LangChain is a framework for developing applications powered by language models.",
metadata: { source: "langchain-docs" }
}),
new Document({
pageContent: "RAG stands for Retrieval Augmented Generation. It's a pattern that combines search with LLM generation.",
metadata: { source: "rag-explanation" }
}),
new Document({
pageContent: "Vector databases store embeddings and enable semantic search based on similarity.",
metadata: { source: "vector-db-docs" }
}),
new Document({
pageContent: "Embedding models convert text into vector representations that capture semantic meaning.",
metadata: { source: "embedding-explanation" }
}),
];
// Split documents into chunks for better embedding
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200
});
const splitDocs = await textSplitter.splitDocuments(sampleDocs);
console.log(\`Adding \${splitDocs.length} document chunks to vector store...\`);
await vectorStore.addDocuments(splitDocs);
// Create RAG prompt template
const promptTemplate = ChatPromptTemplate.fromTemplate(\`
Answer the question based ONLY on the following context:
{context}
Question: {question}
Answer:\`
);
// Define the RAG pipeline
const retrievalChain = async (query${typeAnnotations}) => {
// Retrieve relevant documents
console.log(\`Searching for documents relevant to: "\${query}"\`);
const retrievedDocs = await vectorStore.similaritySearch(query);
const context = retrievedDocs.map(doc => doc.pageContent).join("\\n\\n");
// Format the prompt with context and query
console.log("Generating answer based on retrieved context...");
const messages = await promptTemplate.invoke({
context,
question: query
});
// Generate an answer
const answer = await llm.invoke(messages);
return answer.content;
};
// Test the RAG pipeline with a sample question
const testQuestion = "What is RAG and how does it work?";
const answer = await retrievalChain(testQuestion);
console.log("\\n====== RAG RESULT ======");
console.log(\`Question: \${testQuestion}\`);
console.log(\`Answer: \${answer}\`);
}
main().catch(console.error);
`;
}
// Helper functions for generating different parts of the example
function createImports(modelChoice, embeddingChoice, vectorStoreChoice) {
const imports = [
`import { ${getModelClassName(modelChoice)} } from "${modelChoice.packageName}";`,
`import { ${embeddingChoice.className} } from "${embeddingChoice.importPath}";`,
];
switch (vectorStoreChoice.value) {
case "memory":
imports.push(`import { ${vectorStoreChoice.className} } from "${vectorStoreChoice.importPath}";`);
break;
case "pinecone":
imports.push(`import { Pinecone } from "@pinecone-database/pinecone";`);
imports.push(`import { ${vectorStoreChoice.className} } from "${vectorStoreChoice.importPath}";`);
break;
case "mongodb":
imports.push(`import { MongoClient } from "mongodb";`);
imports.push(`import { ${vectorStoreChoice.className} } from "${vectorStoreChoice.importPath}";`);
break;
default:
imports.push(`import { ${vectorStoreChoice.className} } from "${vectorStoreChoice.importPath}";`);
break;
}
return imports.join("\n");
}
function getModelClassName(modelChoice) {
switch (modelChoice.value) {
case "openai":
return "ChatOpenAI";
case "anthropic":
return "ChatAnthropic";
case "groq":
return "ChatGroq";
case "google-gemini":
return "ChatGoogleGenerativeAI";
case "fireworks":
return "ChatFireworks";
case "mistral":
return "ChatMistralAI";
case "google-vertexai":
return "ChatVertexAI";
default:
return "ChatOpenAI";
}
}
function createApiKeyChecks(modelChoice, embeddingChoice, vectorStoreChoice) {
const checks = [];
// Add model API key check
if (modelChoice.apiKeyEnv) {
checks.push(`
const ${modelChoice.apiKeyEnv.toLowerCase()} = process.env.${modelChoice.apiKeyEnv};
if (!${modelChoice.apiKeyEnv.toLowerCase()}) {
console.error("${modelChoice.apiKeyEnv} not found. Please set it in your .env file.");
process.exit(1);
}`);
}
// Add embedding API key check if different from model
if (embeddingChoice.apiKeyEnv &&
embeddingChoice.apiKeyEnv !== modelChoice.apiKeyEnv) {
checks.push(`
const ${embeddingChoice.apiKeyEnv.toLowerCase()} = process.env.${embeddingChoice.apiKeyEnv};
if (!${embeddingChoice.apiKeyEnv.toLowerCase()}) {
console.error("${embeddingChoice.apiKeyEnv} not found. Please set it in your .env file.");
process.exit(1);
}`);
// Add checks for additional embedding environment variables
if (embeddingChoice.additionalEnvVars) {
embeddingChoice.additionalEnvVars.forEach((envVar) => {
checks.push(`
const ${envVar.toLowerCase()} = process.env.${envVar};
if (!${envVar.toLowerCase()}) {
console.error("${envVar} not found. Please set it in your .env file.");
process.exit(1);
}`);
});
}
}
// Add vector store API key check if needed
if (vectorStoreChoice.requiresApiKey && vectorStoreChoice.apiKeyEnv) {
checks.push(`
const ${vectorStoreChoice.apiKeyEnv.toLowerCase()} = process.env.${vectorStoreChoice.apiKeyEnv};
if (!${vectorStoreChoice.apiKeyEnv.toLowerCase()}) {
console.error("${vectorStoreChoice.apiKeyEnv} not found. Please set it in your .env file.");
process.exit(1);
}`);
// Add additional checks for vector store requirements
if (vectorStoreChoice.additionalEnvVars) {
vectorStoreChoice.additionalEnvVars.forEach((envVar) => {
checks.push(`
const ${envVar.toLowerCase()} = process.env.${envVar};
if (!${envVar.toLowerCase()}) {
console.error("${envVar} not found. Please set it in your .env file.");
process.exit(1);
}`);
});
}
}
return checks.join("\n");
}
function createLlmInitialization(modelChoice) {
const className = getModelClassName(modelChoice);
switch (modelChoice.value) {
case "openai":
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
model: "gpt-3.5-turbo",
temperature: 0
})`;
case "anthropic":
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
model: "claude-3-opus-20240229",
temperature: 0
})`;
case "groq":
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
model: "llama3-8b-8192",
temperature: 0
})`;
case "mistral":
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
model: "mistral-large-latest",
temperature: 0
})`;
case "google-gemini":
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
model: "gemini-pro",
temperature: 0
})`;
case "fireworks":
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
model: "accounts/fireworks/models/mixtral-8x7b-instruct",
temperature: 0
})`;
case "google-vertexai":
return `new ${className}({
model: "gemini-pro",
temperature: 0
})`;
default:
return `new ${className}({
apiKey: process.env.${modelChoice.apiKeyEnv},
temperature: 0
})`;
}
}
function createEmbeddingsInitialization(embeddingChoice) {
// Special handling for specific embedding models
switch (embeddingChoice.value) {
case "azure":
return `new ${embeddingChoice.className}({
azureOpenAIApiKey: process.env.${embeddingChoice.apiKeyEnv},
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
azureOpenAIApiEmbeddingsDeploymentName: "${embeddingChoice.modelName}"
})`;
case "aws":
return `new ${embeddingChoice.className}({
region: process.env.${embeddingChoice.apiKeyEnv},
model: "${embeddingChoice.modelName}"
})`;
case "vertexai":
return `new ${embeddingChoice.className}({
model: "${embeddingChoice.modelName}"
})`;
default:
return `new ${embeddingChoice.className}({
apiKey: process.env.${embeddingChoice.apiKeyEnv},
model: "${embeddingChoice.modelName}"
})`;
}
}
function createVectorStoreInitialization(vectorStoreChoice) {
switch (vectorStoreChoice.value) {
case "memory":
return `const vectorStore = await ${vectorStoreChoice.className}.fromDocuments([], embeddings);`;
case "chroma":
return `const vectorStore = new ${vectorStoreChoice.className}(embeddings, {
collectionName: "langchain-rag-collection"
});`;
case "faiss":
return `const vectorStore = new ${vectorStoreChoice.className}(embeddings, {});`;
case "mongodb":
return `import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.${vectorStoreChoice.apiKeyEnv} || "");
const collection = client
.db(process.env.MONGODB_ATLAS_DB_NAME)
.collection(process.env.MONGODB_ATLAS_COLLECTION_NAME);
const vectorStore = new ${vectorStoreChoice.className}(embeddings, {
collection: collection,
indexName: "vector_index",
textKey: "text",
embeddingKey: "embedding"
});`;
case "pgvector":
return `const vectorStore = await ${vectorStoreChoice.className}.initialize(embeddings, {
connectionString: process.env.PG_CONNECTION_STRING
});`;
case "pinecone":
return `const pinecone = new Pinecone({
apiKey: process.env.${vectorStoreChoice.apiKeyEnv}
});
const indexName = process.env.PINECONE_INDEX || "langchain-rag-index";
// Check if index exists, create if not
const indexList = await pinecone.listIndexes();
let pineconeIndex;
if (!indexList.indexes?.some(idx => idx.name === indexName)) {
console.log(\`Creating new Pinecone index: \${indexName}\`);
await pinecone.createIndex({
name: indexName,
dimension: 1536, // OpenAI embedding dimension (adjust if using different embeddings)
metric: "cosine",
});
}
pineconeIndex = pinecone.index(indexName);
const vectorStore = new ${vectorStoreChoice.className}(embeddings, {
pineconeIndex,
maxConcurrency: 5
});`;
case "qdrant":
return `const vectorStore = await ${vectorStoreChoice.className}.fromExistingCollection(
embeddings,
{
url: process.env.QDRANT_URL || "http://localhost:6333",
collectionName: "langchain-rag-collection",
}
);`;
default:
return `const vectorStore = await ${vectorStoreChoice.className}.fromDocuments([], embeddings);`;
}
}
export { addAiRag };