Compare commits

..

5 Commits

Author SHA1 Message Date
leehuwuj a21dc86eaa update typescript start 2024-03-27 16:26:02 +07:00
leehuwuj 83681febd4 update docs for mounting .env file 2024-03-27 16:25:03 +07:00
leehuwuj 8ebd1651b3 update readme template 2024-03-27 16:17:42 +07:00
leehuwuj 4cbe8c6641 simpler python dockerifle 2024-03-27 16:13:04 +07:00
leehuwuj 90e38358b3 add dockerfile template 2024-03-27 16:10:43 +07:00
68 changed files with 603 additions and 945 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Let user select multiple datasources (URLs, files and folders)
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Merge non-streaming and streaming template to one
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add support for agent generation for Typescript
-16
View File
@@ -1,21 +1,5 @@
# create-llama
## 0.0.32
### Patch Changes
- 625ed4d: Support Astra VectorDB
- 922e0ce: Remove UI question (use shadcn as default). Use `html` UI by calling create-llama with --ui html parameter
- ce2f24d: Update loaders and tools config to yaml format (for Python)
- e8db041: Let user select multiple datasources (URLs, files and folders)
- c06d4af: Add nodes to the response (Python)
- 29b17ee: Allow using agents without any data source
- 665c26c: Add redirect to documentation page when accessing the base URL (FastAPI)
- 78ded9e: Add Dockerfile templates for Typescript and Python
- 99e758f: Merge non-streaming and streaming template to one
- b3f2685: Add support for agent generation for Typescript
- 2739714: Use a database (MySQL or PostgreSQL) as a data source
## 0.0.31
### Patch Changes
+4 -4
View File
@@ -29,6 +29,7 @@ export async function createApp({
ui,
appPath,
packageManager,
eslint,
frontend,
openAiKey,
llamaCloudKey,
@@ -77,6 +78,7 @@ export async function createApp({
ui,
packageManager,
isOnline,
eslint,
openAiKey,
llamaCloudKey,
model,
@@ -125,13 +127,11 @@ export async function createApp({
}
if (toolsRequireConfig(tools)) {
const configFile =
framework === "fastapi" ? "config/tools.yaml" : "config/tools.json";
console.log(
yellow(
`You have selected tools that require configuration. Please configure them in the ${terminalLink(
configFile,
`file://${root}/${configFile}`,
"config/tools.json",
`file://${root}/config/tools.json`,
)} file.`,
),
);
+1
View File
@@ -103,6 +103,7 @@ export async function runCreateLlama(
"--open-ai-key",
process.env.OPENAI_API_KEY,
appType,
"--eslint",
"--use-pnpm",
"--port",
port,
-17
View File
@@ -48,20 +48,3 @@ export const copy = async (
}),
);
};
export const assetRelocator = (name: string) => {
switch (name) {
case "gitignore":
case "eslintrc.json": {
return `.${name}`;
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
case "README-template.md": {
return "README.md";
}
default: {
return name;
}
}
};
+1 -80
View File
@@ -1,8 +1,6 @@
import fs from "fs/promises";
import path from "path";
import yaml, { Document } from "yaml";
import { templatesDir } from "./dir";
import { DbSourceConfig, TemplateDataSource, WebSourceConfig } from "./types";
import { TemplateDataSource } from "./types";
export const EXAMPLE_FILE: TemplateDataSource = {
type: "file",
@@ -30,80 +28,3 @@ export function getDataSources(
}
return dataSources;
}
export async function writeLoadersConfig(
root: string,
dataSources: TemplateDataSource[],
useLlamaParse?: boolean,
) {
if (dataSources.length === 0) return; // no datasources, no config needed
const loaderConfig = new Document({});
// Web loader config
if (dataSources.some((ds) => ds.type === "web")) {
const webLoaderConfig = new Document({});
// Create config for browser driver arguments
const driverArgNodeValue = webLoaderConfig.createNode([
"--no-sandbox",
"--disable-dev-shm-usage",
]);
driverArgNodeValue.commentBefore =
" The arguments to pass to the webdriver. E.g.: add --headless to run in headless mode";
webLoaderConfig.set("driver_arguments", driverArgNodeValue);
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
const urlConfigNode = webLoaderConfig.createNode(urlConfigs);
urlConfigNode.commentBefore = ` base_url: The URL to start crawling with
prefix: Only crawl URLs matching the specified prefix
depth: The maximum depth for BFS traversal
You can add more websites by adding more entries (don't forget the - prefix from YAML)`;
webLoaderConfig.set("urls", urlConfigNode);
// Add web config to the loaders config
loaderConfig.set("web", webLoaderConfig);
}
// File loader config
if (dataSources.some((ds) => ds.type === "file")) {
// Add documentation to web loader config
const node = loaderConfig.createNode({
use_llama_parse: useLlamaParse,
});
node.commentBefore = ` use_llama_parse: Use LlamaParse if \`true\`. Needs a \`LLAMA_CLOUD_API_KEY\` from https://cloud.llamaindex.ai set as environment variable`;
loaderConfig.set("file", node);
}
// DB loader config
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
if (dbLoaders.length > 0) {
const dbLoaderConfig = new Document({});
const configEntries = dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
const node = dbLoaderConfig.createNode(configEntries);
node.commentBefore = ` The configuration for the database loader, only supports MySQL and PostgreSQL databases for now.
uri: The URI for the database. E.g.: mysql+pymysql://user:password@localhost:3306/db or postgresql+psycopg2://user:password@localhost:5432/db
query: The query to fetch data from the database. E.g.: SELECT * FROM table`;
loaderConfig.set("db", node);
}
// Write loaders config
const loaderConfigPath = path.join(root, "config", "loaders.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(loaderConfigPath, yaml.stringify(loaderConfig));
}
+2 -5
View File
@@ -46,16 +46,13 @@ export const writeDevcontainer = async (
framework: TemplateFramework,
frontend: boolean,
) => {
const devcontainerDir = path.join(root, ".devcontainer");
if (fs.existsSync(devcontainerDir)) {
console.log("Template already has a .devcontainer. Using it.");
return;
}
console.log("Adding .devcontainer");
const devcontainerContent = renderDevcontainerContent(
templatesDir,
framework,
frontend,
);
const devcontainerDir = path.join(root, ".devcontainer");
fs.mkdirSync(devcontainerDir);
await fs.promises.writeFile(
path.join(devcontainerDir, "devcontainer.json"),
-15
View File
@@ -93,21 +93,6 @@ const getVectorDBEnvs = (vectorDb: TemplateVectorDB) => {
description: "The password to access the Milvus server.",
},
];
case "astra":
return [
{
name: "ASTRA_DB_APPLICATION_TOKEN",
description: "The generated app token for your Astra database",
},
{
name: "ASTRA_DB_ENDPOINT",
description: "The API endpoint for your Astra database",
},
{
name: "ASTRA_DB_COLLECTION",
description: "The name of the collection in your Astra database",
},
];
default:
return [];
}
+1 -19
View File
@@ -4,14 +4,12 @@ import path from "path";
import { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { writeLoadersConfig } from "./datasources";
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
import { PackageManager } from "./get-pkg-manager";
import { installLlamapackProject } from "./llama-pack";
import { isHavingPoetryLockFile, tryPoetryRun } from "./poetry";
import { installPythonTemplate } from "./python";
import { downloadAndExtractRepo } from "./repo";
import { ConfigFileType, writeToolsConfig } from "./tools";
import {
FileSourceConfig,
InstallTemplateArgs,
@@ -119,23 +117,10 @@ export const installTemplate = async (
if (props.framework === "fastapi") {
await installPythonTemplate(props);
// write loaders configuration (currently Python only)
await writeLoadersConfig(
props.root,
props.dataSources,
props.useLlamaParse,
);
} else {
await installTSTemplate(props);
}
// write tools configuration
await writeToolsConfig(
props.root,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
);
if (props.backend) {
// This is a backend, so we need to copy the test data and create the env file.
@@ -153,10 +138,7 @@ export const installTemplate = async (
if (props.dataSources.length > 0) {
console.log("\nGenerating context data...\n");
await copyContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
await copyContextData(props.root, props.dataSources);
if (
props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies"
+104 -62
View File
@@ -3,8 +3,7 @@ import path from "path";
import { cyan, red } from "picocolors";
import { parse, stringify } from "smol-toml";
import terminalLink from "terminal-link";
import { assetRelocator, copy } from "./copy";
import { copy } from "./copy";
import { templatesDir } from "./dir";
import { isPoetryAvailable, tryPoetryInstall } from "./poetry";
import { Tool } from "./tools";
@@ -12,6 +11,7 @@ import {
InstallTemplateArgs,
TemplateDataSource,
TemplateVectorDB,
WebSourceConfig,
} from "./types";
interface Dependency {
@@ -60,45 +60,21 @@ const getAdditionalDependencies = (
});
break;
}
case "astra": {
dependencies.push({
name: "llama-index-vector-stores-astra-db",
version: "^0.1.5",
});
break;
}
}
// Add data source dependencies
const dataSourceType = dataSource?.type;
switch (dataSourceType) {
case "file":
dependencies.push({
name: "docx2txt",
version: "^0.8",
});
break;
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: "^0.1.6",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.1.3",
});
dependencies.push({
name: "pymysql",
version: "^1.1.0",
extras: ["rsa"],
});
dependencies.push({
name: "psycopg2",
version: "^2.9.9",
});
break;
if (dataSourceType === "file") {
// llama-index-readers-file (pdf, excel, csv) is already included in llama_index package
dependencies.push({
name: "docx2txt",
version: "^0.8",
});
} else if (dataSourceType === "web") {
dependencies.push({
name: "llama-index-readers-web",
version: "^0.1.6",
});
}
// Add tools dependencies
@@ -222,38 +198,104 @@ export const installPythonTemplate = async ({
await copy("**", root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
rename(name) {
switch (name) {
case "gitignore": {
return `.${name}`;
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
case "README-template.md": {
return "README.md";
}
default: {
return name;
}
}
},
});
const compPath = path.join(templatesDir, "components");
const enginePath = path.join(root, "app", "engine");
// Copy selected vector DB
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "vectordbs", "python", vectorDb ?? "none"),
});
if (dataSources.length > 0) {
const enginePath = path.join(root, "app", "engine");
// Copy all loaders to enginePath
const loaderPath = path.join(enginePath, "loaders");
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
const vectorDbDirName = vectorDb ?? "none";
const VectorDBPath = path.join(
compPath,
"vectordbs",
"python",
vectorDbDirName,
);
await copy("**", enginePath, {
parents: true,
cwd: VectorDBPath,
});
// Select and copy engine code based on data sources and tools
let engine;
tools = tools ?? [];
if (dataSources.length > 0 && tools.length === 0) {
console.log("\nNo tools selected - use optimized context chat engine\n");
engine = "chat";
} else {
engine = "agent";
// Copy engine code
if (tools !== undefined && tools.length > 0) {
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", "agent"),
});
// Write tool configs
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
configContent[tool.name] = tool.config ?? {};
});
const configFilePath = path.join(root, "config/tools.json");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(
configFilePath,
JSON.stringify(configContent, null, 2),
);
} else {
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", "chat"),
});
}
const loaderConfigs: Record<string, any> = {};
const loaderPath = path.join(enginePath, "loaders");
// Copy loaders to enginePath
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
// Generate loaders config
// Web loader config
if (dataSources.some((ds) => ds.type === "web")) {
const webLoaderConfig = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
loaderConfigs["web"] = webLoaderConfig;
}
// File loader config
if (dataSources.some((ds) => ds.type === "file")) {
loaderConfigs["file"] = {
use_llama_parse: useLlamaParse,
};
}
// Write loaders config
if (Object.keys(loaderConfigs).length > 0) {
const loaderConfigPath = path.join(root, "config/loaders.json");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(
loaderConfigPath,
JSON.stringify(loaderConfigs, null, 2),
);
}
}
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", engine),
});
const addOnDependencies = dataSources
.map((ds) => getAdditionalDependencies(vectorDb, ds, tools))
-35
View File
@@ -1,8 +1,4 @@
import fs from "fs/promises";
import path from "path";
import { red } from "picocolors";
import yaml from "yaml";
import { makeDir } from "./make-dir";
import { TemplateFramework } from "./types";
export type Tool = {
@@ -12,7 +8,6 @@ export type Tool = {
dependencies?: ToolDependencies[];
supportedFrameworks?: Array<TemplateFramework>;
};
export type ToolDependencies = {
name: string;
version?: string;
@@ -78,33 +73,3 @@ export const toolsRequireConfig = (tools?: Tool[]): boolean => {
}
return false;
};
export enum ConfigFileType {
YAML = "yaml",
JSON = "json",
}
export const writeToolsConfig = async (
root: string,
tools: Tool[] = [],
type: ConfigFileType = ConfigFileType.YAML,
) => {
if (tools.length === 0) return; // no tools selected, no config need
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
configContent[tool.name] = tool.config ?? {};
});
const configPath = path.join(root, "config");
await makeDir(configPath);
if (type === ConfigFileType.YAML) {
await fs.writeFile(
path.join(configPath, "tools.yaml"),
yaml.stringify(configContent),
);
} else {
await fs.writeFile(
path.join(configPath, "tools.json"),
JSON.stringify(configContent, null, 2),
);
}
};
+4 -16
View File
@@ -4,13 +4,7 @@ import { Tool } from "./tools";
export type TemplateType = "streaming" | "community" | "llamapack";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
| "none"
| "mongo"
| "pg"
| "pinecone"
| "milvus"
| "astra";
export type TemplateVectorDB = "none" | "mongo" | "pg" | "pinecone" | "milvus";
export type TemplatePostInstallAction =
| "none"
| "VSCode"
@@ -20,7 +14,7 @@ export type TemplateDataSource = {
type: TemplateDataSourceType;
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db";
export type TemplateDataSourceType = "file" | "web";
export type TemplateObservability = "none" | "opentelemetry";
// Config for both file and folder
export type FileSourceConfig = {
@@ -31,15 +25,8 @@ export type WebSourceConfig = {
prefix?: string;
depth?: number;
};
export type DbSourceConfig = {
uri?: string;
queries?: string;
};
export type TemplateDataSourceConfig =
| FileSourceConfig
| WebSourceConfig
| DbSourceConfig;
export type TemplateDataSourceConfig = FileSourceConfig | WebSourceConfig;
export type CommunityProjectConfig = {
owner: string;
@@ -57,6 +44,7 @@ export interface InstallTemplateArgs {
framework: TemplateFramework;
ui: TemplateUI;
dataSources: TemplateDataSource[];
eslint: boolean;
customApiPath?: string;
openAiKey?: string;
llamaCloudKey?: string;
+129 -96
View File
@@ -2,12 +2,50 @@ import fs from "fs/promises";
import os from "os";
import path from "path";
import { bold, cyan } from "picocolors";
import { assetRelocator, copy } from "../helpers/copy";
import { copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { InstallTemplateArgs } from "./types";
const rename = (name: string) => {
switch (name) {
case "gitignore":
case "eslintrc.json": {
return `.${name}`;
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
case "README-template.md": {
return "README.md";
}
default: {
return name;
}
}
};
export const installTSDependencies = async (
packageJson: any,
packageManager: PackageManager,
isOnline: boolean,
): Promise<void> => {
console.log("\nInstalling dependencies:");
for (const dependency in packageJson.dependencies)
console.log(`- ${cyan(dependency)}`);
console.log("\nInstalling devDependencies:");
for (const dependency in packageJson.devDependencies)
console.log(`- ${cyan(dependency)}`);
console.log();
await callPackageManager(packageManager, isOnline).catch((error) => {
console.error("Failed to install TS dependencies. Exiting...");
process.exit(1);
});
};
/**
* Install a LlamaIndex internal template to a given `root` directory.
*/
@@ -19,6 +57,8 @@ export const installTSTemplate = async ({
template,
framework,
ui,
eslint,
customApiPath,
vectorDb,
postInstallAction,
backend,
@@ -35,11 +75,12 @@ export const installTSTemplate = async ({
console.log("\nInitializing project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", template, framework);
const copySource = ["**"];
if (!eslint) copySource.push("!eslintrc.json");
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
rename,
});
/**
@@ -97,6 +138,9 @@ export const installTSTemplate = async ({
);
}
/**
* Copy the selected chat engine files to the target directory and reference it.
*/
const compPath = path.join(templatesDir, "components");
const relativeEngineDestPath =
framework === "nextjs"
@@ -104,33 +148,58 @@ export const installTSTemplate = async ({
: path.join("src", "controllers");
const enginePath = path.join(root, relativeEngineDestPath, "engine");
// copy vector db component
console.log("\nUsing vector DB:", vectorDb, "\n");
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
});
// copy loader component (TS only supports llama_parse and file for now)
const loaderFolder = useLlamaParse ? "llama_parse" : "file";
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
});
// Select and copy engine code based on data sources and tools
let engine;
tools = tools ?? [];
if (dataSources.length > 0 && tools.length === 0) {
console.log("\nNo tools selected - use optimized context chat engine\n");
engine = "chat";
if (dataSources.length === 0) {
// use simple hat engine if user neither select tools nor a data source
console.log("\nUsing simple chat engine\n");
} else {
engine = "agent";
if (vectorDb) {
// copy vector db component
console.log("\nUsing vector DB:", vectorDb, "\n");
const vectorDBPath = path.join(
compPath,
"vectordbs",
"typescript",
vectorDb,
);
await copy("**", enginePath, {
parents: true,
cwd: vectorDBPath,
});
}
// copy loader component (TS only supports llama_parse and file for now)
let loaderFolder: string;
loaderFolder = useLlamaParse ? "llama_parse" : "file";
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
});
if (tools?.length) {
// use agent chat engine if user selects tools
console.log("\nUsing agent chat engine\n");
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "typescript", "agent"),
});
// Write tools_config.json
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
configContent[tool.name] = tool.config ?? {};
});
const configFilePath = path.join(enginePath, "tools_config.json");
await fs.writeFile(
configFilePath,
JSON.stringify(configContent, null, 2),
);
} else {
// use context chat engine if user does not select tools
console.log("\nUsing context chat engine\n");
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "typescript", "chat"),
});
}
}
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "typescript", engine),
});
/**
* Copy the selected UI files to the target directory and reference it.
@@ -145,54 +214,13 @@ export const installTSTemplate = async ({
await copy("**", destUiPath, {
parents: true,
cwd: uiPath,
rename: assetRelocator,
rename,
});
}
/** Modify frontend code to use custom API path */
if (framework === "nextjs" && !backend) {
console.log(
"\nUsing external API for frontend, removing API code and configuration\n",
);
// remove the default api folder and config folder
await fs.rm(path.join(root, "app", "api"), { recursive: true });
await fs.rm(path.join(root, "config"), { recursive: true, force: true });
}
const packageJson = await updatePackageJson({
root,
appName,
dataSources,
relativeEngineDestPath,
framework,
ui,
observability,
});
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
await installTSDependencies(packageJson, packageManager, isOnline);
}
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
};
async function updatePackageJson({
root,
appName,
dataSources,
relativeEngineDestPath,
framework,
ui,
observability,
}: Pick<
InstallTemplateArgs,
"root" | "appName" | "dataSources" | "framework" | "ui" | "observability"
> & {
relativeEngineDestPath: string;
}): Promise<any> {
/**
* Update the package.json scripts.
*/
const packageJsonFile = path.join(root, "package.json");
const packageJson: any = JSON.parse(
await fs.readFile(packageJsonFile, "utf8"),
@@ -200,15 +228,26 @@ async function updatePackageJson({
packageJson.name = appName;
packageJson.version = "0.1.0";
if (framework === "nextjs" && customApiPath) {
console.log(
"\nUsing external API with custom API path:",
customApiPath,
"\n",
);
// remove the default api folder
const apiPath = path.join(root, "app", "api");
await fs.rm(apiPath, { recursive: true });
// modify the dev script to use the custom api path
}
if (dataSources.length > 0 && relativeEngineDestPath) {
// TODO: move script to {root}/scripts for all frameworks
// add generate script if using context engine
packageJson.scripts = {
...packageJson.scripts,
generate: `ts-node ${path.join(
generate: `node ${path.join(
relativeEngineDestPath,
"engine",
"generate.ts",
"generate.mjs",
)}`,
};
}
@@ -248,31 +287,25 @@ async function updatePackageJson({
};
}
if (!eslint) {
// Remove packages starting with "eslint" from devDependencies
packageJson.devDependencies = Object.fromEntries(
Object.entries(packageJson.devDependencies).filter(
([key]) => !key.startsWith("eslint"),
),
);
}
await fs.writeFile(
packageJsonFile,
JSON.stringify(packageJson, null, 2) + os.EOL,
);
return packageJson;
}
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
await installTSDependencies(packageJson, packageManager, isOnline);
}
async function installTSDependencies(
packageJson: any,
packageManager: PackageManager,
isOnline: boolean,
): Promise<void> {
console.log("\nInstalling dependencies:");
for (const dependency in packageJson.dependencies)
console.log(`- ${cyan(dependency)}`);
console.log("\nInstalling devDependencies:");
for (const dependency in packageJson.devDependencies)
console.log(`- ${cyan(dependency)}`);
console.log();
await callPackageManager(packageManager, isOnline).catch((error) => {
console.error("Failed to install TS dependencies. Exiting...");
process.exit(1);
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
}
};
+11
View File
@@ -32,6 +32,13 @@ const program = new Commander.Command(packageJson.name)
.action((name) => {
projectPath = name;
})
.option(
"--eslint",
`
Initialize with eslint config.
`,
)
.option(
"--use-npm",
`
@@ -182,6 +189,9 @@ const program = new Commander.Command(packageJson.name)
if (process.argv.includes("--no-frontend")) {
program.frontend = false;
}
if (process.argv.includes("--no-eslint")) {
program.eslint = false;
}
if (process.argv.includes("--tools")) {
if (program.tools === "none") {
program.tools = [];
@@ -286,6 +296,7 @@ async function run(): Promise<void> {
ui: program.ui,
appPath: resolvedProjectPath,
packageManager,
eslint: program.eslint,
frontend: program.frontend,
openAiKey: program.openAiKey,
llamaCloudKey: program.llamaCloudKey,
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.0.32",
"version": "0.0.31",
"keywords": [
"rag",
"llamaindex",
@@ -71,8 +71,7 @@
"typescript": "^5.3.3",
"eslint-config-prettier": "^8.10.0",
"ora": "^8.0.1",
"fs-extra": "11.2.0",
"yaml": "2.4.1"
"fs-extra": "11.2.0"
},
"engines": {
"node": ">=16.14.0"
-9
View File
@@ -113,9 +113,6 @@ devDependencies:
wait-port:
specifier: ^1.1.0
version: 1.1.0
yaml:
specifier: 2.4.1
version: 2.4.1
packages:
@@ -3369,12 +3366,6 @@ packages:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
/yaml@2.4.1:
resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==}
engines: {node: '>= 14'}
hasBin: true
dev: true
/yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
+89 -96
View File
@@ -70,7 +70,8 @@ if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
const defaults: QuestionArgs = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
ui: "html",
eslint: true,
frontend: false,
openAiKey: "",
llamaCloudKey: "",
@@ -101,7 +102,6 @@ const getVectorDbChoices = (framework: TemplateFramework) => {
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
@@ -132,7 +132,7 @@ export const getDataSourceChoices = (
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No data, just a simple chat or agent",
title: "No data, just a simple chat",
value: "none",
});
choices.push({
@@ -160,10 +160,6 @@ export const getDataSourceChoices = (
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
return choices;
};
@@ -506,7 +502,25 @@ export const askQuestions = async (
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = defaults.ui;
if (ciInfo.isCI) {
program.ui = getPrefOrDefault("ui");
} else {
const { ui } = await prompts(
{
type: "select",
name: "ui",
message: "Which UI would you like to use?",
choices: [
{ title: "Just HTML", value: "html" },
{ title: "Shadcn", value: "shadcn" },
],
initial: 0,
},
handlers,
);
program.ui = ui;
preferences.ui = ui;
}
}
}
@@ -613,19 +627,19 @@ export const askQuestions = async (
program.dataSources = [];
// continue asking user for data sources if none are initially provided
while (true) {
const firstQuestion = program.dataSources.length === 0;
const { selectedSource } = await prompts(
{
type: "select",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
message:
program.dataSources.length === 0
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices: getDataSourceChoices(
program.framework,
program.dataSources,
),
initial: firstQuestion ? 1 : 0,
initial: 0,
},
handlers,
);
@@ -634,93 +648,52 @@ export const askQuestions = async (
// user doesn't want another data source or any data source
break;
}
switch (selectedSource) {
case "exampleFile": {
program.dataSources.push(EXAMPLE_FILE);
break;
}
case "file":
case "folder": {
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "file",
config: {
path: p,
},
});
}
break;
}
case "web": {
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
},
handlers,
);
if (selectedSource === "exampleFile") {
program.dataSources.push(EXAMPLE_FILE);
} else if (selectedSource === "file" || selectedSource === "folder") {
// Select local data source
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "web",
type: "file",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
path: p,
},
});
break;
}
case "db": {
const dbPrompts: prompts.PromptObject<string>[] = [
{
type: "text",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
validate: (value: string) => {
if (!value) {
return "Please provide a valid connection string";
} else if (
!(
value.startsWith("mysql+pymysql://") ||
value.startsWith("postgresql+psycopg://")
)
) {
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
}
return true;
},
} else if (selectedSource === "web") {
// Selected web data source
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
// Only ask for a query, user can provide more complex queries in the config file later
{
type: (prev) => (prev ? "text" : null),
name: "queries",
message: "Please enter the SQL query to fetch data:",
initial: "SELECT * FROM mytable",
},
];
program.dataSources.push({
type: "db",
config: await prompts(dbPrompts, handlers),
});
}
},
handlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
}
}
}
@@ -741,7 +714,7 @@ export const askQuestions = async (
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
initial: true,
active: "yes",
inactive: "no",
},
@@ -784,7 +757,8 @@ export const askQuestions = async (
}
}
if (!program.tools) {
// TODO: allow tools also without datasources
if (!program.tools && program.dataSources.length > 0) {
if (ciInfo.isCI) {
program.tools = getPrefOrDefault("tools");
} else {
@@ -810,5 +784,24 @@ export const askQuestions = async (
}
}
if (program.framework !== "fastapi" && program.eslint === undefined) {
if (ciInfo.isCI) {
program.eslint = getPrefOrDefault("eslint");
} else {
const styledEslint = blue("ESLint");
const { eslint } = await prompts({
onState: onPromptState,
type: "toggle",
name: "eslint",
message: `Would you like to use ${styledEslint}?`,
initial: getPrefOrDefault("eslint"),
active: "Yes",
inactive: "No",
});
program.eslint = Boolean(eslint);
preferences.eslint = Boolean(eslint);
}
}
await askPostInstallAction();
};
@@ -10,10 +10,6 @@ RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python
ln -s /opt/poetry/bin/poetry && \
poetry config virtualenvs.create false
# Install Chromium for web loader
# Can disable this if you don't use the web loader to reduce the image size
RUN apt update && apt install -y chromium chromium-driver
# Install dependencies
COPY ./pyproject.toml ./poetry.lock* /app/
RUN poetry install --no-root --no-cache --only main
@@ -3,14 +3,17 @@ FROM node:20-alpine as build
WORKDIR /app
# Install dependencies
COPY package.json package-lock.* ./
RUN npm install
COPY package.json pnpm-lock.yaml* /app/
RUN npm install -g pnpm
RUN pnpm install
# Build the application
COPY . .
RUN npm run build
RUN pnpm run build
# ====================================
FROM build as release
CMD ["npm", "run", "start"]
RUN chmod +x start.sh
CMD ["/bin/sh", "start.sh"]
@@ -0,0 +1,12 @@
#!/bin/bash
set -e
if [ "$ENVIRONMENT" = "dev" ]; then
echo "Running Development mode"
pnpm dev
else
echo "Running Production mode"
pnpm build
pnpm start
fi
@@ -11,12 +11,11 @@ def get_chat_engine():
top_k = os.getenv("TOP_K", "3")
tools = []
# Add query tool if index exists
# Add query tool
index = get_index()
if index is not None:
query_engine = index.as_query_engine(similarity_top_k=int(top_k))
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
tools.append(query_engine_tool)
query_engine = index.as_query_engine(similarity_top_k=int(top_k))
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
tools.append(query_engine_tool)
# Add additional tools
tools += ToolFactory.from_env()
@@ -1,5 +1,4 @@
import os
import yaml
import json
import importlib
from llama_index.core.tools.tool_spec.base import BaseToolSpec
@@ -27,9 +26,8 @@ class ToolFactory:
@staticmethod
def from_env() -> list[FunctionTool]:
tools = []
if os.path.exists("config/tools.yaml"):
with open("config/tools.yaml", "r") as f:
tool_configs = yaml.safe_load(f)
for name, config in tool_configs.items():
tools += ToolFactory.create_tool(name, **config)
with open("config/tools.json", "r") as f:
tool_configs = json.load(f)
for name, config in tool_configs.items():
tools += ToolFactory.create_tool(name, **config)
return tools
@@ -6,13 +6,7 @@ def get_chat_engine():
system_prompt = os.getenv("SYSTEM_PROMPT")
top_k = os.getenv("TOP_K", 3)
index = get_index()
if index is None:
raise Exception(
"StorageContext is empty - call 'python app/engine/generate.py' to generate the storage first"
)
return index.as_chat_engine(
return get_index().as_chat_engine(
similarity_top_k=int(top_k),
system_prompt=system_prompt,
chat_mode="condense_plus_context",
@@ -1,44 +1,26 @@
import {
BaseTool,
OpenAI,
OpenAIAgent,
QueryEngineTool,
ToolFactory,
} from "llamaindex";
import fs from "node:fs/promises";
import path from "node:path";
import { OpenAI, OpenAIAgent, QueryEngineTool, ToolFactory } from "llamaindex";
import { STORAGE_CACHE_DIR } from "./constants.mjs";
import { getDataSource } from "./index";
import { STORAGE_CACHE_DIR } from "./shared";
import config from "./tools_config.json";
export async function createChatEngine(llm: OpenAI) {
let tools: BaseTool[] = [];
// Add a query engine tool if we have a data source
// Delete this code if you don't have a data source
const index = await getDataSource(llm);
if (index) {
tools.push(
new QueryEngineTool({
queryEngine: index.asQueryEngine(),
metadata: {
name: "data_query_engine",
description: `A query engine for documents in storage folder: ${STORAGE_CACHE_DIR}`,
},
}),
);
}
try {
// add tools from config file if it exists
const config = JSON.parse(
await fs.readFile(path.join("config", "tools.json"), "utf8"),
);
tools = tools.concat(await ToolFactory.createTools(config));
} catch {}
return new OpenAIAgent({
tools,
llm,
verbose: true,
const queryEngine = index.asQueryEngine();
const queryEngineTool = new QueryEngineTool({
queryEngine: queryEngine,
metadata: {
name: "data_query_engine",
description: `A query engine for documents in storage folder: ${STORAGE_CACHE_DIR}`,
},
});
const externalTools = await ToolFactory.createTools(config);
const agent = new OpenAIAgent({
tools: [queryEngineTool, ...externalTools],
verbose: true,
llm,
});
return agent;
}
@@ -3,11 +3,6 @@ import { getDataSource } from "./index";
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
if (!index) {
throw new Error(
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
);
}
const retriever = index.asRetriever();
retriever.similarityTopK = 3;
+10 -16
View File
@@ -1,18 +1,17 @@
import os
import yaml
import json
import importlib
import logging
from typing import Dict
from app.engine.loaders.file import FileLoaderConfig, get_file_documents
from app.engine.loaders.web import WebLoaderConfig, get_web_documents
from app.engine.loaders.db import DBLoaderConfig, get_db_documents
logger = logging.getLogger(__name__)
def load_configs():
with open("config/loaders.yaml") as f:
configs = yaml.safe_load(f)
with open("config/loaders.json") as f:
configs = json.load(f)
return configs
@@ -23,17 +22,12 @@ def get_documents():
logger.info(
f"Loading documents from loader: {loader_type}, config: {loader_config}"
)
match loader_type:
case "file":
document = get_file_documents(FileLoaderConfig(**loader_config))
case "web":
document = get_web_documents(WebLoaderConfig(**loader_config))
case "db":
document = get_db_documents(
configs=[DBLoaderConfig(**cfg) for cfg in loader_config]
)
case _:
raise ValueError(f"Invalid loader type: {loader_type}")
documents.extend(document)
if loader_type == "file":
document = get_file_documents(FileLoaderConfig(**loader_config))
documents.extend(document)
elif loader_type == "web":
for entry in loader_config:
document = get_web_documents(WebLoaderConfig(**entry))
documents.extend(document)
return documents
-26
View File
@@ -1,26 +0,0 @@
import os
import logging
from typing import List
from pydantic import BaseModel, validator
from llama_index.core.indices.vector_store import VectorStoreIndex
logger = logging.getLogger(__name__)
class DBLoaderConfig(BaseModel):
uri: str
queries: List[str]
def get_db_documents(configs: list[DBLoaderConfig]):
from llama_index.readers.database import DatabaseReader
docs = []
for entry in configs:
loader = DatabaseReader(uri=entry.uri)
for query in entry.queries:
logger.info(f"Loading data from database with query: {query}")
documents = loader.load_data(query=query)
docs.extend(documents)
return documents
+6 -23
View File
@@ -3,34 +3,17 @@ import json
from pydantic import BaseModel, Field
class CrawlUrl(BaseModel):
class WebLoaderConfig(BaseModel):
base_url: str
prefix: str
max_depth: int = Field(default=1, ge=0)
class WebLoaderConfig(BaseModel):
driver_arguments: list[str] = Field(default=None)
urls: list[CrawlUrl]
def get_web_documents(config: WebLoaderConfig):
from llama_index.readers.web import WholeSiteReader
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
driver_arguments = config.driver_arguments or []
for arg in driver_arguments:
options.add_argument(arg)
docs = []
for url in config.urls:
scraper = WholeSiteReader(
prefix=url.prefix,
max_depth=url.max_depth,
driver=webdriver.Chrome(options=options),
)
docs.extend(scraper.load_data(url.base_url))
return docs
scraper = WholeSiteReader(
prefix=config.prefix,
max_depth=config.max_depth,
)
return scraper.load_data(config.base_url)
@@ -1,37 +0,0 @@
from dotenv import load_dotenv
load_dotenv()
import os
import logging
from llama_index.core.storage import StorageContext
from llama_index.core.indices import VectorStoreIndex
from llama_index.vector_stores.astra_db import AstraDBVectorStore
from app.settings import init_settings
from app.engine.loaders import get_documents
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_datasource():
logger.info("Creating new index")
documents = get_documents()
store = AstraDBVectorStore(
token=os.environ["ASTRA_DB_APPLICATION_TOKEN"],
api_endpoint=os.environ["ASTRA_DB_ENDPOINT"],
collection_name=os.environ["ASTRA_DB_COLLECTION"],
embedding_dimension=1536,
)
storage_context = StorageContext.from_defaults(vector_store=store)
VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
show_progress=True, # this will show you a progress bar as the embeddings are created
)
logger.info(f"Successfully created embeddings in the AstraDB")
if __name__ == "__main__":
init_settings()
generate_datasource()
@@ -1,21 +0,0 @@
import logging
import os
from llama_index.core.indices import VectorStoreIndex
from llama_index.vector_stores.astra_db import AstraDBVectorStore
logger = logging.getLogger("uvicorn")
def get_index():
logger.info("Connecting to index from AstraDB...")
store = AstraDBVectorStore(
token=os.environ["ASTRA_DB_APPLICATION_TOKEN"],
api_endpoint=os.environ["ASTRA_DB_ENDPOINT"],
collection_name=os.environ["ASTRA_DB_COLLECTION"],
embedding_dimension=1536,
)
index = VectorStoreIndex.from_vector_store(store)
logger.info("Finished connecting to index from AstraDB.")
return index
@@ -11,7 +11,10 @@ logger = logging.getLogger("uvicorn")
def get_index():
# check if storage already exists
if not os.path.exists(STORAGE_DIR):
return None
raise Exception(
"StorageContext is empty - call 'python app/engine/generate.py' to generate the storage first"
)
# load the existing index
logger.info(f"Loading index from {STORAGE_DIR}...")
storage_context = StorageContext.from_defaults(persist_dir=STORAGE_DIR)
@@ -1,38 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import {
AstraDBVectorStore,
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader";
import { checkRequiredEnvVars } from "./shared";
dotenv.config();
async function loadAndIndex() {
// load objects from storage and convert them into LlamaIndex Document objects
const documents = await getDocuments();
// create vector store and a collection
const collectionName = process.env.ASTRA_DB_COLLECTION!;
const vectorStore = new AstraDBVectorStore();
await vectorStore.create(collectionName, {
vector: { dimension: 1536, metric: "cosine" },
});
await vectorStore.connect(collectionName);
// create index from documents and store them in Astra
console.log("Start creating embeddings...");
const storageContext = await storageContextFromDefaults({ vectorStore });
await VectorStoreIndex.fromDocuments(documents, { storageContext });
console.log(
"Successfully created embeddings and save to your Astra database.",
);
}
(async () => {
checkRequiredEnvVars();
await loadAndIndex();
console.log("Finished generating storage.");
})();
@@ -1,20 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import {
AstraDBVectorStore,
LLM,
VectorStoreIndex,
serviceContextFromDefaults,
} from "llamaindex";
import { CHUNK_OVERLAP, CHUNK_SIZE, checkRequiredEnvVars } from "./shared";
export async function getDataSource(llm: LLM) {
checkRequiredEnvVars();
const serviceContext = serviceContextFromDefaults({
llm,
chunkSize: CHUNK_SIZE,
chunkOverlap: CHUNK_OVERLAP,
});
const store = new AstraDBVectorStore();
await store.connect(process.env.ASTRA_DB_COLLECTION!);
return await VectorStoreIndex.fromVectorStore(store, serviceContext);
}
@@ -1,25 +0,0 @@
export const CHUNK_SIZE = 512;
export const CHUNK_OVERLAP = 20;
const REQUIRED_ENV_VARS = [
"ASTRA_DB_APPLICATION_TOKEN",
"ASTRA_DB_ENDPOINT",
"ASTRA_DB_COLLECTION",
];
export function checkRequiredEnvVars() {
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
return !process.env[envVar];
});
if (missingEnvVars.length > 0) {
console.log(
`The following environment variables are required but missing: ${missingEnvVars.join(
", ",
)}`,
);
throw new Error(
`Missing environment variables: ${missingEnvVars.join(", ")}`,
);
}
}
@@ -5,8 +5,8 @@ import {
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader";
import { checkRequiredEnvVars, getMilvusClient } from "./shared";
import { getDocuments } from "./loader.mjs";
import { checkRequiredEnvVars, getMilvusClient } from "./shared.mjs";
dotenv.config();
@@ -1,4 +1,5 @@
import {
ContextChatEngine,
LLM,
MilvusVectorStore,
serviceContextFromDefaults,
@@ -9,9 +10,9 @@ import {
CHUNK_OVERLAP,
CHUNK_SIZE,
getMilvusClient,
} from "./shared";
} from "./shared.mjs";
export async function getDataSource(llm: LLM) {
async function getDataSource(llm: LLM) {
checkRequiredEnvVars();
const serviceContext = serviceContextFromDefaults({
llm,
@@ -23,3 +24,12 @@ export async function getDataSource(llm: LLM) {
return await VectorStoreIndex.fromVectorStore(store, serviceContext);
}
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
const retriever = index.asRetriever({ similarityTopK: 3 });
return new ContextChatEngine({
chatModel: llm,
retriever,
});
}
@@ -16,7 +16,7 @@ export function getMilvusClient() {
throw new Error("MILVUS_ADDRESS environment variable is required");
}
return new MilvusClient({
address: process.env.MILVUS_ADDRESS!,
address: process.env.MILVUS_ADDRESS,
username: process.env.MILVUS_USERNAME,
password: process.env.MILVUS_PASSWORD,
});
@@ -6,14 +6,14 @@ import {
storageContextFromDefaults,
} from "llamaindex";
import { MongoClient } from "mongodb";
import { getDocuments } from "./loader";
import { checkRequiredEnvVars } from "./shared";
import { getDocuments } from "./loader.mjs";
import { checkRequiredEnvVars } from "./shared.mjs";
dotenv.config();
const mongoUri = process.env.MONGO_URI!;
const databaseName = process.env.MONGODB_DATABASE!;
const vectorCollectionName = process.env.MONGODB_VECTORS!;
const mongoUri = process.env.MONGO_URI;
const databaseName = process.env.MONGODB_DATABASE;
const vectorCollectionName = process.env.MONGODB_VECTORS;
const indexName = process.env.MONGODB_VECTOR_INDEX;
async function loadAndIndex() {
@@ -1,14 +1,15 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import {
ContextChatEngine,
LLM,
MongoDBAtlasVectorSearch,
serviceContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
import { MongoClient } from "mongodb";
import { checkRequiredEnvVars, CHUNK_OVERLAP, CHUNK_SIZE } from "./shared";
import { checkRequiredEnvVars, CHUNK_OVERLAP, CHUNK_SIZE } from "./shared.mjs";
export async function getDataSource(llm: LLM) {
async function getDataSource(llm: LLM) {
checkRequiredEnvVars();
const client = new MongoClient(process.env.MONGO_URI!);
const serviceContext = serviceContextFromDefaults({
@@ -18,10 +19,19 @@ export async function getDataSource(llm: LLM) {
});
const store = new MongoDBAtlasVectorSearch({
mongodbClient: client,
dbName: process.env.MONGODB_DATABASE!,
collectionName: process.env.MONGODB_VECTORS!,
dbName: process.env.MONGODB_DATABASE,
collectionName: process.env.MONGODB_VECTORS,
indexName: process.env.MONGODB_VECTOR_INDEX,
});
return await VectorStoreIndex.fromVectorStore(store, serviceContext);
}
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
const retriever = index.asRetriever({ similarityTopK: 3 });
return new ContextChatEngine({
chatModel: llm,
retriever,
});
}
@@ -1,5 +1,4 @@
import {
ServiceContext,
serviceContextFromDefaults,
storageContextFromDefaults,
VectorStoreIndex,
@@ -7,20 +6,20 @@ import {
import * as dotenv from "dotenv";
import { getDocuments } from "./loader";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./shared";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
import { getDocuments } from "./loader.mjs";
// Load environment variables from local .env file
dotenv.config();
async function getRuntime(func: any) {
async function getRuntime(func) {
const start = Date.now();
await func();
const end = Date.now();
return end - start;
}
async function generateDatasource(serviceContext: ServiceContext) {
async function generateDatasource(serviceContext) {
console.log(`Generating storage context...`);
// Split documents, create embeddings and store them in the storage context
const ms = await getRuntime(async () => {
@@ -5,7 +5,7 @@ import {
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./shared";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
export async function getDataSource(llm: LLM) {
const serviceContext = serviceContextFromDefaults({
@@ -21,7 +21,9 @@ export async function getDataSource(llm: LLM) {
(storageContext.docStore as SimpleDocumentStore).toDict(),
).length;
if (numberOfDocs === 0) {
return null;
throw new Error(
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
);
}
return await VectorStoreIndex.init({
storageContext,
@@ -5,13 +5,13 @@ import {
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader";
import { getDocuments } from "./loader.mjs";
import {
PGVECTOR_COLLECTION,
PGVECTOR_SCHEMA,
PGVECTOR_TABLE,
checkRequiredEnvVars,
} from "./shared";
} from "./shared.mjs";
dotenv.config();
@@ -1,5 +1,6 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import {
ContextChatEngine,
LLM,
PGVectorStore,
VectorStoreIndex,
@@ -11,9 +12,9 @@ import {
PGVECTOR_SCHEMA,
PGVECTOR_TABLE,
checkRequiredEnvVars,
} from "./shared";
} from "./shared.mjs";
export async function getDataSource(llm: LLM) {
async function getDataSource(llm: LLM) {
checkRequiredEnvVars();
const pgvs = new PGVectorStore({
connectionString: process.env.PG_CONNECTION_STRING,
@@ -27,3 +28,12 @@ export async function getDataSource(llm: LLM) {
});
return await VectorStoreIndex.fromVectorStore(pgvs, serviceContext);
}
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
const retriever = index.asRetriever({ similarityTopK: 3 });
return new ContextChatEngine({
chatModel: llm,
retriever,
});
}
@@ -5,8 +5,8 @@ import {
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader";
import { checkRequiredEnvVars } from "./shared";
import { getDocuments } from "./loader.mjs";
import { checkRequiredEnvVars } from "./shared.mjs";
dotenv.config();
@@ -1,13 +1,14 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import {
ContextChatEngine,
LLM,
PineconeVectorStore,
VectorStoreIndex,
serviceContextFromDefaults,
} from "llamaindex";
import { CHUNK_OVERLAP, CHUNK_SIZE, checkRequiredEnvVars } from "./shared";
import { CHUNK_OVERLAP, CHUNK_SIZE, checkRequiredEnvVars } from "./shared.mjs";
export async function getDataSource(llm: LLM) {
async function getDataSource(llm: LLM) {
checkRequiredEnvVars();
const serviceContext = serviceContextFromDefaults({
llm,
@@ -17,3 +18,12 @@ export async function getDataSource(llm: LLM) {
const store = new PineconeVectorStore();
return await VectorStoreIndex.fromVectorStore(store, serviceContext);
}
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
const retriever = index.asRetriever({ similarityTopK: 5 });
return new ContextChatEngine({
chatModel: llm,
retriever,
});
}
@@ -0,0 +1,74 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```
poetry install
poetry shell
```
By default, we use the OpenAI LLM (though you can customize, see app/api/routers/chat.py). As a result you need to specify an `OPENAI_API_KEY` in an .env file in this directory.
Example `backend/.env` file:
```
OPENAI_API_KEY=<openai_api_key>
```
If you are using any tools or data sources, you can update their config files in the `config` folder.
Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step):
```
python app/engine/generate.py
```
Third, run the development server:
```
python main.py
```
Then call the API endpoint `/api/chat` to see the result:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
```
You can start editing the API by modifying `app/api/routers/chat.py`. The endpoint auto-updates as you save the file.
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
```
ENVIRONMENT=prod python main.py
```
## Using docker
1. Build an image for FastAPI app:
```
docker build -t <your_backend_image_name> .
```
2. Run a container:
```
docker run --rm -v $(pwd)/.env:/app/.env -p 8000:8000 <your_backend_image_name>
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -60,37 +60,18 @@ NODE_ENV=production npm run start
> Note that the `NODE_ENV` environment variable is set to `production`. This disables CORS for all origins.
## Using Docker
## Using docker
1. Build an image for the Express API:
1. Build an image for Express app:
```
docker build -t <your_backend_image_name> .
```
2. Generate embeddings:
Parse the data and generate the vector embeddings if the `./data` folder exists - otherwise, skip this step:
2. Run a container:
```
docker run --rm \
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
-v $(pwd)/config:/app/config \
-v $(pwd)/data:/app/data \
-v $(pwd)/cache:/app/cache \ # Use your file system to store the vector database
<your_backend_image_name>
npm run generate
```
3. Start the API:
```
docker run \
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
-v $(pwd)/config:/app/config \
-v $(pwd)/cache:/app/cache \ # Use your file system to store the vector database
-p 8000:8000 \
<your_backend_image_name>
docker run --rm -v $(pwd)/.env:/app/.env -p 8000:8000 <your_backend_image_name>
```
## Learn More
@@ -1,7 +1,3 @@
{
"extends": ["eslint:recommended", "prettier"],
"rules": {
"max-params": ["error", 4],
"prefer-const": "error"
}
"extends": "eslint:recommended"
}
+5 -10
View File
@@ -1,13 +1,12 @@
{
"name": "llama-index-express-streaming",
"version": "1.0.0",
"main": "dist/index.mjs",
"main": "dist/index.js",
"type": "module",
"scripts": {
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"build": "tsup index.ts --format esm --dts",
"start": "node dist/index.mjs",
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.mjs\""
"start": "node dist/index.js",
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.js\""
},
"dependencies": {
"ai": "^2.2.25",
@@ -24,10 +23,6 @@
"eslint": "^8.54.0",
"nodemon": "^3.0.1",
"tsup": "^8.0.1",
"typescript": "^5.3.2",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"eslint-config-prettier": "^8.10.0",
"ts-node": "^10.9.2"
"typescript": "^5.3.2"
}
}
@@ -1,3 +0,0 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
@@ -5,14 +5,6 @@
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node",
"paths": {
"@/*": ["./*"]
}
},
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
"moduleResolution": "node"
}
}
@@ -64,38 +64,18 @@ The API allows CORS for all origins to simplify development. You can change this
ENVIRONMENT=prod python main.py
```
## Using Docker
## Using docker
1. Build an image for the FastAPI app:
1. Build an image for FastAPI app:
```
docker build -t <your_backend_image_name> .
```
2. Generate embeddings:
Parse the data and generate the vector embeddings if the `./data` folder exists - otherwise, skip this step:
2. Run a container:
```
docker run \
--rm \
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
-v $(pwd)/config:/app/config \
-v $(pwd)/data:/app/data \ # Use your local folder to read the data
-v $(pwd)/storage:/app/storage \ # Use your file system to store the vector database
<your_backend_image_name> \
python app/engine/generate.py
```
3. Start the API:
```
docker run \
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
-v $(pwd)/config:/app/config \
-v $(pwd)/storage:/app/storage \ # Use your file system to store gea vector database
-p 8000:8000 \
<your_backend_image_name>
docker run --rm -v $(pwd)/.env:/app/.env -p 8000:8000 <your_backend_image_name>
```
## Learn More
@@ -1,13 +1,11 @@
from typing import List
from pydantic import BaseModel
from typing import List, Any, Optional, Dict, Tuple
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from llama_index.core.chat_engine.types import (
BaseChatEngine,
)
from llama_index.core.schema import NodeWithScore
from fastapi import APIRouter, Depends, HTTPException, Request, status
from llama_index.core.chat_engine.types import BaseChatEngine
from llama_index.core.llms import ChatMessage, MessageRole
from app.engine import get_chat_engine
from typing import List, Tuple
chat_router = r = APIRouter()
@@ -20,40 +18,9 @@ class _Message(BaseModel):
class _ChatData(BaseModel):
messages: List[_Message]
class Config:
json_schema_extra = {
"example": {
"messages": [
{
"role": "user",
"content": "What standards for letters exist?",
}
]
}
}
class _SourceNodes(BaseModel):
id: str
metadata: Dict[str, Any]
score: Optional[float]
@classmethod
def from_source_node(cls, source_node: NodeWithScore):
return cls(
id=source_node.node.node_id,
metadata=source_node.node.metadata,
score=source_node.score,
)
@classmethod
def from_source_nodes(cls, source_nodes: List[NodeWithScore]):
return [cls.from_source_node(node) for node in source_nodes]
class _Result(BaseModel):
result: _Message
nodes: List[_SourceNodes]
async def parse_chat_data(data: _ChatData) -> Tuple[str, List[ChatMessage]]:
@@ -110,6 +77,5 @@ async def chat_request(
response = await chat_engine.achat(last_message_content, messages)
return _Result(
result=_Message(role=MessageRole.ASSISTANT, content=response.response),
nodes=_SourceNodes.from_source_nodes(response.source_nodes),
result=_Message(role=MessageRole.ASSISTANT, content=response.response)
)
@@ -7,7 +7,6 @@ import os
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from app.api.routers.chat import chat_router
from app.settings import init_settings
@@ -30,12 +29,6 @@ if environment == "dev":
allow_headers=["*"],
)
# Redirect to documentation page when accessing base URL
@app.get("/")
async def redirect_to_docs():
return RedirectResponse(url="/docs")
app.include_router(chat_router, prefix="/api/chat")
@@ -26,39 +26,18 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Using Docker
## Using docker
1. Build an image for the Next.js app:
1. Build an image for Next app:
```
docker build -t <your_app_image_name> .
```
2. Generate embeddings:
Parse the data and generate the vector embeddings if the `./data` folder exists - otherwise, skip this step:
2. Run a container:
```
docker run \
--rm \
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
-v $(pwd)/config:/app/config \
-v $(pwd)/data:/app/data \
-v $(pwd)/cache:/app/cache \ # Use your file system to store the vector database
<your_app_image_name> \
npm run generate
```
3. Start the app:
```
docker run \
--rm \
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
-v $(pwd)/config:/app/config \
-v $(pwd)/cache:/app/cache \ # Use your file system to store gea vector database
-p 3000:3000 \
<your_app_image_name>
docker run --rm -v $(pwd)/.env:/app/.env -p 3000:3000 <your_app_image_name>
```
## Learn More
@@ -1,7 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier"],
"rules": {
"max-params": ["error", 4],
"prefer-const": "error"
}
"extends": "next/core-web-vitals"
}
@@ -2,8 +2,6 @@
"name": "llama-index-nextjs-streaming",
"version": "1.0.0",
"scripts": {
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"dev": "next dev",
"build": "next build",
"start": "next start",
@@ -40,10 +38,6 @@
"tailwindcss": "^3.3.6",
"typescript": "^5.3.2",
"@types/react-syntax-highlighter": "^15.5.11",
"cross-env": "^7.0.3",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"eslint-config-prettier": "^8.10.0",
"ts-node": "^10.9.2"
"cross-env": "^7.0.3"
}
}
@@ -1,3 +0,0 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
@@ -24,10 +24,5 @@
"forceConsistentCasingInFileNames": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"],
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
}
"exclude": ["node_modules"]
}