Compare commits

..

1 Commits

Author SHA1 Message Date
thucpn 7626ca7787 refactor: import from llamaindex edge for typescript templates and vectordbs 2024-03-18 18:03:11 +07:00
106 changed files with 1045 additions and 1185 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Remove UI question (use shadcn as default). Use `html` UI by calling create-llama with --ui html parameter
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Update loaders and tools config to yaml format
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Let user select multiple datasources (URLs, files and folders)
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Merge non-streaming and streaming template to one
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Add support for agent generation for Typescript
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add fetching llm and embedding models from server
@@ -2,4 +2,4 @@
"create-llama": patch
---
Add Dockerfile template
Add Milvus vector database
+6 -3
View File
@@ -46,11 +46,14 @@ jobs:
- name: Build create-llama
run: pnpm run build
working-directory: .
- name: Install
run: pnpm run pack-install
- name: Pack
run: pnpm pack --pack-destination ./output
working-directory: .
- name: Extract Pack
run: tar -xvzf ./output/*.tgz -C ./output
working-directory: .
- name: Run Playwright tests
run: pnpm run e2e
run: pnpm exec playwright test
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: .
-3
View File
@@ -45,6 +45,3 @@ e2e/cache
# intellij
**/.idea
# build artifacts
create-llama-*.tgz
-16
View File
@@ -1,21 +1,5 @@
# create-llama
## 0.0.31
### Patch Changes
- 56faee0: Added windows e2e tests
- 60ed8fe: Added missing environment variable config for URL data source
- 60ed8fe: Fixed tool usage by freezing llama-index package versions
## 0.0.30
### Patch Changes
- 3af6328: Add support for llamaparse using Typescript
- dd92b91: Add fetching llm and embedding models from server
- bac1b43: Add Milvus vector database
## 0.0.29
### Patch Changes
-73
View File
@@ -1,73 +0,0 @@
# Contributing
## Getting Started
Install NodeJS. Preferably v18 using nvm or n.
Inside the `create-llama` directory:
```
npm i -g pnpm ts-node
pnpm install
```
Note: we use pnpm in this repo, which has a lot of the same functionality and CLI options as npm but it does do some things better, like caching.
### Building
When we publish to NPM we will have a [ncc](https://github.com/vercel/ncc) compiled version of the tool. To run the build command, run
```
pnpm run build
```
### Test cases
We are using a set of e2e tests to ensure that the tool works as expected.
We're using [playwright](https://playwright.dev/) to run the tests.
To install it, call:
```
pnpm exec playwright install --with-deps
```
Then you can create a global `create-llama` command (used by the e2e tests) that is linked to your local dev environment (if you update the build, you don't need to re-link):
```
pnpm link --global
```
And then finally run the tests:
```
pnpm run e2e
```
To write new test cases write them in [e2e](/e2e)
## Changeset
We use [changesets](https://github.com/changesets/changesets) for managing versions and changelogs. To create a new changeset, run:
```
pnpm changeset
```
Please send a descriptive changeset for each PR.
## Publishing (maintainers only)
To publish a new version of the library, first create a new version:
```shell
pnpm new-version
```
If everything looks good, commit the generated files and release the new version:
```shell
pnpm release
git push # push to the main branch
git push --tags
```
+8 -8
View File
@@ -26,9 +26,11 @@ export type InstallAppArgs = Omit<
export async function createApp({
template,
framework,
engine,
ui,
appPath,
packageManager,
eslint,
frontend,
openAiKey,
llamaCloudKey,
@@ -39,9 +41,8 @@ export async function createApp({
vectorDb,
externalPort,
postInstallAction,
dataSources,
dataSource,
tools,
useLlamaParse,
observability,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
@@ -74,9 +75,11 @@ export async function createApp({
root,
template,
framework,
engine,
ui,
packageManager,
isOnline,
eslint,
openAiKey,
llamaCloudKey,
model,
@@ -86,9 +89,8 @@ export async function createApp({
vectorDb,
externalPort,
postInstallAction,
dataSources,
dataSource,
tools,
useLlamaParse,
observability,
};
@@ -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}`,
"tools_config.json",
`file://${root}/tools_config.json`,
)} file.`,
),
);
+25 -9
View File
@@ -4,6 +4,7 @@ import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateEngine,
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
@@ -11,13 +12,13 @@ import type {
} from "../helpers";
import { createTestDir, runCreateLlama, type AppType } from "./utils";
const templateTypes: TemplateType[] = ["streaming"];
const templateTypes: TemplateType[] = ["streaming", "simple"];
const templateFrameworks: TemplateFramework[] = [
"nextjs",
"express",
"fastapi",
];
const dataSources: string[] = ["--no-files", "--example-file"];
const templateEngines: TemplateEngine[] = ["simple", "context"];
const templateUIs: TemplateUI[] = ["shadcn", "html"];
const templatePostInstallActions: TemplatePostInstallAction[] = [
"none",
@@ -26,12 +27,24 @@ const templatePostInstallActions: TemplatePostInstallAction[] = [
for (const templateType of templateTypes) {
for (const templateFramework of templateFrameworks) {
for (const dataSource of dataSources) {
for (const templateEngine of templateEngines) {
for (const templateUI of templateUIs) {
for (const templatePostInstallAction of templatePostInstallActions) {
if (templateFramework === "nextjs" && templateType === "simple") {
// nextjs doesn't support simple templates - skip tests
continue;
}
const appType: AppType =
templateFramework === "nextjs" ? "" : "--frontend";
test.describe(`try create-llama ${templateType} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
templateFramework === "express" || templateFramework === "fastapi"
? templateType === "simple"
? "--no-frontend" // simple templates don't have frontends
: "--frontend"
: "";
if (appType === "--no-frontend" && templateUI !== "html") {
// if there's no frontend, don't iterate over UIs
continue;
}
test.describe(`try create-llama ${templateType} ${templateFramework} ${templateEngine} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
let port: number;
let externalPort: number;
let cwd: string;
@@ -48,7 +61,7 @@ for (const templateType of templateTypes) {
cwd,
templateType,
templateFramework,
dataSource,
templateEngine,
templateUI,
vectorDb,
appType,
@@ -66,6 +79,7 @@ for (const templateType of templateTypes) {
});
test("Frontend should have a title", async ({ page }) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(appType === "--no-frontend");
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
@@ -74,6 +88,7 @@ for (const templateType of templateTypes) {
page,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(appType === "--no-frontend");
await page.goto(`http://localhost:${port}`);
await page.fill("form input", "hello");
const [response] = await Promise.all([
@@ -94,13 +109,14 @@ for (const templateType of templateTypes) {
expect(response.ok()).toBeTruthy();
});
test("Backend frameworks should response when calling non-streaming chat API", async ({
test("Backend should response when calling API", async ({
request,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(templateFramework === "nextjs");
test.skip(appType !== "--no-frontend");
const backendPort = appType === "" ? port : externalPort;
const response = await request.post(
`http://localhost:${externalPort}/api/chat/request`,
`http://localhost:${backendPort}/api/chat`,
{
data: {
messages: [
+19 -9
View File
@@ -4,6 +4,7 @@ import { mkdir } from "node:fs/promises";
import * as path from "path";
import waitPort from "wait-port";
import {
TemplateEngine,
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
@@ -66,7 +67,7 @@ export async function runCreateLlama(
cwd: string,
templateType: TemplateType,
templateFramework: TemplateFramework,
dataSource: string,
templateEngine: TemplateEngine,
templateUI: TemplateUI,
vectorDb: TemplateVectorDB,
appType: AppType,
@@ -74,24 +75,32 @@ export async function runCreateLlama(
externalPort: number,
postInstallAction: TemplatePostInstallAction,
): Promise<CreateLlamaResult> {
if (!process.env.OPENAI_API_KEY) {
throw new Error("Setting OPENAI_API_KEY is mandatory to run tests");
}
const createLlama = path.join(
__dirname,
"..",
"output",
"package",
"dist",
"index.js",
);
const name = [
templateType,
templateFramework,
dataSource,
templateEngine,
templateUI,
appType,
].join("-");
const command = [
"create-llama",
"node",
createLlama,
name,
"--template",
templateType,
"--framework",
templateFramework,
dataSource,
"--engine",
templateEngine,
"--ui",
templateUI,
"--vector-db",
@@ -101,9 +110,10 @@ export async function runCreateLlama(
"--embedding-model",
EMBEDDING_MODEL,
"--open-ai-key",
process.env.OPENAI_API_KEY,
process.env.OPENAI_API_KEY || "testKey",
appType,
"--use-pnpm",
"--eslint",
"--use-npm",
"--port",
port,
"--external-port",
-30
View File
@@ -1,30 +0,0 @@
import path from "path";
import { templatesDir } from "./dir";
import { TemplateDataSource } from "./types";
export const EXAMPLE_FILE: TemplateDataSource = {
type: "file",
config: {
path: path.join(templatesDir, "components", "data", "101.pdf"),
},
};
export function getDataSources(
files?: string,
exampleFile?: boolean,
): TemplateDataSource[] | undefined {
let dataSources: TemplateDataSource[] | undefined = undefined;
if (files) {
// If user specified files option, then the program should use context engine
dataSources = files.split(",").map((filePath) => ({
type: "file",
config: {
path: filePath,
},
}));
}
if (exampleFile) {
dataSources = [...(dataSources ? dataSources : []), EXAMPLE_FILE];
}
return dataSources;
}
+33 -6
View File
@@ -1,6 +1,7 @@
import fs from "fs/promises";
import path from "path";
import {
FileSourceConfig,
TemplateDataSource,
TemplateFramework,
TemplateVectorDB,
@@ -98,6 +99,28 @@ const getVectorDBEnvs = (vectorDb: TemplateVectorDB) => {
}
};
const getDataSourceEnvs = (dataSource: TemplateDataSource) => {
switch (dataSource.type) {
case "web":
return [
{
name: "BASE_URL",
description: "The base URL to start web scraping.",
},
{
name: "URL_PREFIX",
description: "The prefix of the URL to start web scraping.",
},
{
name: "MAX_DEPTH",
description: "The maximum depth to scrape.",
},
];
default:
return [];
}
};
export const createBackendEnvFile = async (
root: string,
opts: {
@@ -107,7 +130,7 @@ export const createBackendEnvFile = async (
model?: string;
embeddingModel?: string;
framework?: TemplateFramework;
dataSources?: TemplateDataSource[];
dataSource?: TemplateDataSource;
port?: number;
},
) => {
@@ -126,13 +149,10 @@ export const createBackendEnvFile = async (
description: "The OpenAI API key to use.",
value: opts.openAiKey,
},
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
// Add vector database environment variables
...(opts.vectorDb ? getVectorDBEnvs(opts.vectorDb) : []),
// Add data source environment variables
...(opts.dataSource ? getDataSourceEnvs(opts.dataSource) : []),
];
let envVars: EnvVar[] = [];
if (opts.framework === "fastapi") {
@@ -184,6 +204,13 @@ We have provided context information below.
Given this information, please answer the question: {query_str}
"`,
},
(opts?.dataSource?.config as FileSourceConfig).useLlamaParse
? {
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
}
: {},
],
];
} else {
+58 -34
View File
@@ -1,9 +1,11 @@
import { copy } from "./copy";
import { callPackageManager } from "./install";
import fs from "fs/promises";
import path from "path";
import { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { templatesDir } from "./dir";
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
import { PackageManager } from "./get-pkg-manager";
import { installLlamapackProject } from "./llama-pack";
@@ -25,8 +27,8 @@ async function generateContextData(
packageManager?: PackageManager,
openAiKey?: string,
vectorDb?: TemplateVectorDB,
dataSource?: TemplateDataSource,
llamaCloudKey?: string,
useLlamaParse?: boolean,
) {
if (packageManager) {
const runGenerate = `${cyan(
@@ -35,31 +37,35 @@ async function generateContextData(
: `${packageManager} run generate`,
)}`;
const openAiKeyConfigured = openAiKey || process.env["OPENAI_API_KEY"];
const llamaCloudKeyConfigured = useLlamaParse
const llamaCloudKeyConfigured = (dataSource?.config as FileSourceConfig)
?.useLlamaParse
? llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = vectorDb && vectorDb !== "none";
if (openAiKeyConfigured && llamaCloudKeyConfigured && !hasVectorDb) {
// If all the required environment variables are set, run the generate script
if (framework === "fastapi") {
if (isHavingPoetryLockFile()) {
console.log(`Running ${runGenerate} to generate the context data.`);
const result = tryPoetryRun("python app/engine/generate.py");
if (!result) {
console.log(`Failed to run ${runGenerate}.`);
process.exit(1);
}
console.log(`Generated context data`);
return;
if (framework === "fastapi") {
if (
openAiKeyConfigured &&
llamaCloudKeyConfigured &&
!hasVectorDb &&
isHavingPoetryLockFile()
) {
console.log(`Running ${runGenerate} to generate the context data.`);
const result = tryPoetryRun("python app/engine/generate.py");
if (!result) {
console.log(`Failed to run ${runGenerate}.`);
process.exit(1);
}
} else {
console.log(`Generated context data`);
return;
}
} else {
if (openAiKeyConfigured && vectorDb === "none") {
console.log(`Running ${runGenerate} to generate the context data.`);
await callPackageManager(packageManager, true, ["run", "generate"]);
return;
}
}
// generate the message of what to do to run the generate script manually
const settings = [];
if (!openAiKeyConfigured) settings.push("your OpenAI key");
if (!llamaCloudKeyConfigured) settings.push("your Llama Cloud key");
@@ -73,16 +79,38 @@ async function generateContextData(
const copyContextData = async (
root: string,
dataSources: TemplateDataSource[],
dataSource?: TemplateDataSource,
) => {
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// Copy local data
const dataPath = dataSourceConfig.path;
const destPath = path.join(root, "data");
const destPath = path.join(root, "data", path.basename(dataPath));
console.log("Copying data from path:", dataPath);
await fsExtra.copy(dataPath, destPath);
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// Copy file
if (dataSource?.type === "file") {
if (dataSourceConfig.path) {
console.log(`\nCopying file to ${cyan(destPath)}\n`);
await fs.mkdir(destPath, { recursive: true });
await fs.copyFile(
dataSourceConfig.path,
path.join(destPath, path.basename(dataSourceConfig.path)),
);
} else {
console.log("Missing file path in config");
process.exit(1);
}
return;
}
// Copy folder
if (dataSource?.type === "folder") {
const srcPath =
dataSourceConfig.path ?? path.join(templatesDir, "components", "data");
console.log(`\nCopying data to ${cyan(destPath)}\n`);
await copy("**", destPath, {
parents: true,
cwd: srcPath,
});
return;
}
};
@@ -132,16 +160,12 @@ export const installTemplate = async (
model: props.model,
embeddingModel: props.embeddingModel,
framework: props.framework,
dataSources: props.dataSources,
dataSource: props.dataSource,
port: props.externalPort,
});
if (props.dataSources.length > 0) {
console.log("\nGenerating context data...\n");
await copyContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (props.engine === "context") {
await copyContextData(props.root, props.dataSource);
if (
props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies"
@@ -151,14 +175,14 @@ export const installTemplate = async (
props.packageManager,
props.openAiKey,
props.vectorDb,
props.dataSource,
props.llamaCloudKey,
props.useLlamaParse,
);
}
}
} else {
// this is a frontend for a full-stack app, create .env file with model information
await createFrontendEnvFile(props.root, {
createFrontendEnvFile(props.root, {
model: props.model,
customApiPath: props.customApiPath,
});
+31 -83
View File
@@ -3,16 +3,15 @@ import path from "path";
import { cyan, red } from "picocolors";
import { parse, stringify } from "smol-toml";
import terminalLink from "terminal-link";
import yaml, { Document } from "yaml";
import { copy } from "./copy";
import { templatesDir } from "./dir";
import { isPoetryAvailable, tryPoetryInstall } from "./poetry";
import { Tool } from "./tools";
import {
FileSourceConfig,
InstallTemplateArgs,
TemplateDataSource,
TemplateVectorDB,
WebSourceConfig,
} from "./types";
interface Dependency {
@@ -55,17 +54,13 @@ const getAdditionalDependencies = (
name: "llama-index-vector-stores-milvus",
version: "^0.1.6",
});
dependencies.push({
name: "pymilvus",
version: "2.3.7",
});
break;
}
}
// Add data source dependencies
const dataSourceType = dataSource?.type;
if (dataSourceType === "file") {
if (dataSourceType === "file" || dataSourceType === "folder") {
// llama-index-readers-file (pdf, excel, csv) is already included in llama_index package
dependencies.push({
name: "docx2txt",
@@ -178,20 +173,20 @@ export const installPythonTemplate = async ({
root,
template,
framework,
engine,
vectorDb,
dataSources,
dataSource,
tools,
postInstallAction,
useLlamaParse,
}: Pick<
InstallTemplateArgs,
| "root"
| "framework"
| "template"
| "engine"
| "vectorDb"
| "dataSources"
| "dataSource"
| "tools"
| "useLlamaParse"
| "postInstallAction"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
@@ -216,10 +211,9 @@ export const installPythonTemplate = async ({
},
});
const compPath = path.join(templatesDir, "components");
if (dataSources.length > 0) {
if (engine === "context") {
const enginePath = path.join(root, "app", "engine");
const compPath = path.join(templatesDir, "components");
const vectorDbDirName = vectorDb ?? "none";
const VectorDBPath = path.join(
@@ -239,14 +233,16 @@ export const installPythonTemplate = async ({
parents: true,
cwd: path.join(compPath, "engines", "python", "agent"),
});
// Write tool configs
// Write tools_config.json
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
configContent[tool.name] = tool.config ?? {};
});
const configFilePath = path.join(root, "config/tools.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(configFilePath, yaml.stringify(configContent));
const configFilePath = path.join(root, "tools_config.json");
await fs.writeFile(
configFilePath,
JSON.stringify(configContent, null, 2),
);
} else {
await copy("**", enginePath, {
parents: true,
@@ -254,78 +250,30 @@ export const installPythonTemplate = async ({
});
}
const loaderConfig = new Document({});
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 = 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,
const dataSourceType = dataSource?.type;
if (dataSourceType !== undefined && dataSourceType !== "none") {
let loaderFolder: string;
if (dataSourceType === "file" || dataSourceType === "folder") {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
loaderFolder = dataSourceConfig.useLlamaParse ? "llama_parse" : "file";
} else {
loaderFolder = dataSourceType;
}
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "loaders", "python", loaderFolder),
});
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);
}
// Write loaders config
if (Object.keys(loaderConfig).length > 0) {
const loaderConfigPath = path.join(root, "config/loaders.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(loaderConfigPath, yaml.stringify(loaderConfig));
}
}
const addOnDependencies = dataSources
.map((ds) => getAdditionalDependencies(vectorDb, ds, tools))
.flat();
const addOnDependencies = getAdditionalDependencies(
vectorDb,
dataSource,
tools,
);
await addDependencies(root, addOnDependencies);
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
// Copy deployment files for python
await copy("**", root, {
cwd: path.join(compPath, "deployments", "python"),
});
};
-4
View File
@@ -1,12 +1,10 @@
import { red } from "picocolors";
import { TemplateFramework } from "./types";
export type Tool = {
display: string;
name: string;
config?: Record<string, any>;
dependencies?: ToolDependencies[];
supportedFrameworks?: Array<TemplateFramework>;
};
export type ToolDependencies = {
name: string;
@@ -29,7 +27,6 @@ export const supportedTools: Tool[] = [
version: "0.1.2",
},
],
supportedFrameworks: ["fastapi"],
},
{
display: "Wikipedia",
@@ -40,7 +37,6 @@ export const supportedTools: Tool[] = [
version: "0.1.2",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
},
];
+8 -7
View File
@@ -1,8 +1,9 @@
import { PackageManager } from "../helpers/get-pkg-manager";
import { Tool } from "./tools";
export type TemplateType = "streaming" | "community" | "llamapack";
export type TemplateType = "simple" | "streaming" | "community" | "llamapack";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateEngine = "simple" | "context";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB = "none" | "mongo" | "pg" | "pinecone" | "milvus";
export type TemplatePostInstallAction =
@@ -14,18 +15,17 @@ export type TemplateDataSource = {
type: TemplateDataSourceType;
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web";
export type TemplateDataSourceType = "none" | "file" | "folder" | "web";
export type TemplateObservability = "none" | "opentelemetry";
// Config for both file and folder
export type FileSourceConfig = {
path: string;
path?: string;
useLlamaParse?: boolean;
};
export type WebSourceConfig = {
baseUrl?: string;
prefix?: string;
depth?: number;
};
export type TemplateDataSourceConfig = FileSourceConfig | WebSourceConfig;
export type CommunityProjectConfig = {
@@ -42,12 +42,13 @@ export interface InstallTemplateArgs {
isOnline: boolean;
template: TemplateType;
framework: TemplateFramework;
engine: TemplateEngine;
ui: TemplateUI;
dataSources: TemplateDataSource[];
dataSource?: TemplateDataSource;
eslint: boolean;
customApiPath?: string;
openAiKey?: string;
llamaCloudKey?: string;
useLlamaParse?: boolean;
model: string;
embeddingModel: string;
communityProjectConfig?: CommunityProjectConfig;
+33 -65
View File
@@ -6,7 +6,6 @@ import { copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { makeDir } from "./make-dir";
import { InstallTemplateArgs } from "./types";
const rename = (name: string) => {
@@ -57,15 +56,14 @@ export const installTSTemplate = async ({
isOnline,
template,
framework,
engine,
ui,
eslint,
customApiPath,
vectorDb,
postInstallAction,
backend,
observability,
tools,
dataSources,
useLlamaParse,
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
@@ -75,6 +73,7 @@ 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,
@@ -119,7 +118,6 @@ export const installTSTemplate = async ({
}
}
// copy observability component
if (observability && observability !== "none") {
const chosenObservabilityPath = path.join(
templatesDir,
@@ -140,65 +138,32 @@ export const installTSTemplate = async ({
/**
* Copy the selected chat engine files to the target directory and reference it.
*/
let relativeEngineDestPath;
const compPath = path.join(templatesDir, "components");
const relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
const enginePath = path.join(root, relativeEngineDestPath, "engine");
if (engine && (framework === "express" || framework === "nextjs")) {
console.log("\nUsing chat engine:", engine, "\n");
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 {
if (vectorDb) {
// copy vector db component
let vectorDBFolder: string = engine;
if (engine !== "simple" && vectorDb) {
console.log("\nUsing vector DB:", vectorDb, "\n");
const vectorDBPath = path.join(
compPath,
"vectordbs",
"typescript",
vectorDb,
);
await copy("**", enginePath, {
parents: true,
cwd: vectorDBPath,
});
vectorDBFolder = vectorDb;
}
// 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 config/tools.json
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
configContent[tool.name] = tool.config ?? {};
});
const configPath = path.join(root, "config");
await makeDir(configPath);
await fs.writeFile(
path.join(configPath, "tools.json"),
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"),
});
}
const VectorDBPath = path.join(
compPath,
"vectordbs",
"typescript",
vectorDBFolder,
);
relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
await copy("**", path.join(root, relativeEngineDestPath, "engine"), {
parents: true,
cwd: VectorDBPath,
});
}
/**
@@ -240,7 +205,7 @@ export const installTSTemplate = async ({
// modify the dev script to use the custom api path
}
if (dataSources.length > 0 && relativeEngineDestPath) {
if (engine === "context" && relativeEngineDestPath) {
// add generate script if using context engine
packageJson.scripts = {
...packageJson.scripts,
@@ -287,6 +252,14 @@ export const installTSTemplate = async ({
};
}
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,
@@ -295,9 +268,4 @@ export const installTSTemplate = async ({
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
await installTSDependencies(packageJson, packageManager, isOnline);
}
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
};
+23 -17
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env node
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import Commander from "commander";
@@ -9,7 +10,6 @@ import prompts from "prompts";
import terminalLink from "terminal-link";
import checkForUpdate from "update-check";
import { createApp } from "./create-app";
import { getDataSources } from "./helpers/datasources";
import { getPkgManager } from "./helpers/get-pkg-manager";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { runApp } from "./helpers/run-app";
@@ -32,6 +32,13 @@ const program = new Commander.Command(packageJson.name)
.action((name) => {
projectPath = name;
})
.option(
"--eslint",
`
Initialize with eslint config.
`,
)
.option(
"--use-npm",
`
@@ -65,6 +72,13 @@ const program = new Commander.Command(packageJson.name)
`
Select a template to bootstrap the application with.
`,
)
.option(
"--engine <engine>",
`
Select a chat engine to bootstrap the application with.
`,
)
.option(
@@ -79,13 +93,6 @@ const program = new Commander.Command(packageJson.name)
`
Specify the path to a local file or folder for chatting.
`,
)
.option(
"--example-file",
`
Select to use an example PDF as data source.
`,
)
.option(
@@ -158,7 +165,7 @@ const program = new Commander.Command(packageJson.name)
`,
)
.option(
"--use-llama-parse",
"--llama-parse",
`
Enable LlamaParse.
`,
@@ -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 = [];
@@ -190,12 +200,7 @@ if (process.argv.includes("--tools")) {
}
}
if (process.argv.includes("--no-llama-parse")) {
program.useLlamaParse = false;
}
if (process.argv.includes("--no-files")) {
program.dataSources = [];
} else {
program.dataSources = getDataSources(program.files, program.exampleFile);
program.llamaParse = false;
}
const packageManager = !!program.useNpm
@@ -283,9 +288,11 @@ async function run(): Promise<void> {
await createApp({
template: program.template,
framework: program.framework,
engine: program.engine,
ui: program.ui,
appPath: resolvedProjectPath,
packageManager,
eslint: program.eslint,
frontend: program.frontend,
openAiKey: program.openAiKey,
llamaCloudKey: program.llamaCloudKey,
@@ -296,9 +303,8 @@ async function run(): Promise<void> {
vectorDb: program.vectorDb,
externalPort: program.externalPort,
postInstallAction: program.postInstallAction,
dataSources: program.dataSources,
dataSource: program.dataSource,
tools: program.tools,
useLlamaParse: program.useLlamaParse,
observability: program.observability,
});
conf.set("preferences", preferences);
+4 -11
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.0.31",
"version": "0.0.29",
"keywords": [
"rag",
"llamaindex",
@@ -24,16 +24,12 @@
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"dev": "ncc build ./index.ts -w -o dist/",
"build": "bash ./scripts/build.sh",
"build:ncc": "pnpm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"build": "npm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
"e2e": "playwright test",
"prepare": "husky",
"release": "pnpm run build && changeset publish",
"new-version": "pnpm run build && changeset version",
"release-snapshot": "pnpm run build && changeset publish --tag snapshot",
"new-snapshot": "pnpm run build && changeset version --snapshot",
"pack-install": "bash ./scripts/pack.sh"
"new-version": "pnpm run build && changeset version"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
@@ -44,7 +40,6 @@
"@types/prompts": "2.0.1",
"@types/tar": "6.1.5",
"@types/validate-npm-package-name": "3.0.0",
"@types/fs-extra": "11.0.4",
"@vercel/ncc": "0.38.1",
"async-retry": "1.3.1",
"async-sema": "3.0.1",
@@ -70,9 +65,7 @@
"prettier-plugin-organize-imports": "^3.2.4",
"typescript": "^5.3.3",
"eslint-config-prettier": "^8.10.0",
"ora": "^8.0.1",
"fs-extra": "11.2.0",
"yaml": "2.4.1"
"ora": "^8.0.1"
},
"engines": {
"node": ">=16.14.0"
-50
View File
@@ -20,9 +20,6 @@ devDependencies:
'@types/cross-spawn':
specifier: 6.0.0
version: 6.0.0
'@types/fs-extra':
specifier: 11.0.4
version: 11.0.4
'@types/node':
specifier: ^20.11.7
version: 20.11.26
@@ -65,9 +62,6 @@ devDependencies:
fast-glob:
specifier: 3.3.1
version: 3.3.1
fs-extra:
specifier: 11.2.0
version: 11.2.0
got:
specifier: 10.7.0
version: 10.7.0
@@ -113,9 +107,6 @@ devDependencies:
wait-port:
specifier: ^1.1.0
version: 1.1.0
yaml:
specifier: 2.4.1
version: 2.4.1
packages:
@@ -498,23 +489,10 @@ packages:
'@types/node': 20.11.26
dev: true
/@types/fs-extra@11.0.4:
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 20.11.26
dev: true
/@types/http-cache-semantics@4.0.4:
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
dev: true
/@types/jsonfile@6.1.4:
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
dependencies:
'@types/node': 20.11.26
dev: true
/@types/keyv@3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
@@ -1459,15 +1437,6 @@ packages:
signal-exit: 4.1.0
dev: true
/fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'}
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
dev: true
/fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@@ -2013,14 +1982,6 @@ packages:
graceful-fs: 4.2.11
dev: true
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
dev: true
/keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies:
@@ -3224,11 +3185,6 @@ packages:
engines: {node: '>= 4.0.0'}
dev: true
/universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
dev: true
/update-check@1.5.4:
resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==}
dependencies:
@@ -3369,12 +3325,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'}
+240 -156
View File
@@ -8,12 +8,11 @@ import { blue, green, red } from "picocolors";
import prompts from "prompts";
import { InstallAppArgs } from "./create-app";
import {
TemplateDataSource,
FileSourceConfig,
TemplateDataSourceType,
TemplateFramework,
} from "./helpers";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
import { EXAMPLE_FILE } from "./helpers/datasources";
import { templatesDir } from "./helpers/dir";
import { getAvailableLlamapackOptions } from "./helpers/llama-pack";
import { getProjectOptions } from "./helpers/repo";
@@ -25,6 +24,8 @@ export type QuestionArgs = Omit<
InstallAppArgs,
"appPath" | "packageManager"
> & {
files?: string;
llamaParse?: boolean;
listServerModels?: boolean;
};
const supportedContextFileTypes = [
@@ -39,22 +40,21 @@ const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
a.chooseFile({ withPrompt: "Please select a file to process:" }).toString()
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
a.chooseFolder({ withPrompt: "Please select a folder to process:" }).toString()
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
$openFileDialog.FileName
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
@@ -70,17 +70,21 @@ if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
const defaults: QuestionArgs = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
engine: "simple",
ui: "html",
eslint: true,
frontend: false,
openAiKey: "",
llamaCloudKey: "",
useLlamaParse: false,
model: "gpt-3.5-turbo",
embeddingModel: "text-embedding-ada-002",
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
dataSource: {
type: "none",
config: {},
},
tools: [],
};
@@ -118,42 +122,24 @@ const getVectorDbChoices = (framework: TemplateFramework) => {
return displayedChoices;
};
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
) => {
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
const getDataSourceChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No data, just a simple chat",
value: "none",
value: "simple",
},
{ title: "Use an example PDF", value: "exampleFile" },
];
if (process.platform === "win32" || process.platform === "darwin") {
choices.push({
title: `Use a local file (${supportedContextFileTypes.join(", ")})`,
value: "localFile",
});
choices.push({
title: "Use an example PDF",
value: "exampleFile",
title: `Use a local folder`,
value: "localFolder",
});
}
choices.push(
{
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
value: "file",
},
{
title:
process.platform === "win32"
? "Use a local folder"
: "Use local folders",
value: "folder",
},
);
if (framework === "fastapi") {
choices.push({
title: "Use website content (requires Chrome)",
@@ -187,16 +173,9 @@ const selectLocalContextData = async (type: TemplateDataSourceType) => {
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
if (type === "file") {
const fileType = path.extname(selectedPath);
if (!supportedContextFileTypes.includes(fileType)) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
@@ -205,7 +184,7 @@ const selectLocalContextData = async (type: TemplateDataSourceType) => {
process.exit(1);
}
}
return paths;
return selectedPath;
} catch (error) {
console.log(
red(
@@ -330,7 +309,9 @@ export const askQuestions = async (
const openAiKeyConfigured =
program.openAiKey || process.env["OPENAI_API_KEY"];
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = program.useLlamaParse
const llamaCloudKeyConfigured = (
program.dataSource?.config as FileSourceConfig
)?.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
@@ -378,7 +359,8 @@ export const askQuestions = async (
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Chat", value: "streaming" },
{ title: "Chat without streaming", value: "simple" },
{ title: "Chat with streaming", value: "streaming" },
{
title: `Community template from ${styledRepo}`,
value: "community",
@@ -388,7 +370,7 @@ export const askQuestions = async (
value: "llamapack",
},
],
initial: 0,
initial: 1,
},
handlers,
);
@@ -447,10 +429,13 @@ export const askQuestions = async (
program.framework = getPrefOrDefault("framework");
} else {
const choices = [
{ title: "NextJS", value: "nextjs" },
{ title: "Express", value: "express" },
{ title: "FastAPI (Python)", value: "fastapi" },
];
if (program.template === "streaming") {
// allow NextJS only for streaming template
choices.unshift({ title: "NextJS", value: "nextjs" });
}
const { framework } = await prompts(
{
@@ -467,7 +452,10 @@ export const askQuestions = async (
}
}
if (program.framework === "express" || program.framework === "fastapi") {
if (
program.template === "streaming" &&
(program.framework === "express" || program.framework === "fastapi")
) {
// if a backend-only framework is selected, ask whether we should create a frontend
// (only for streaming backends)
if (program.frontend === undefined) {
@@ -501,7 +489,25 @@ export const askQuestions = async (
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = getPrefOrDefault("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;
}
}
}
@@ -601,110 +607,131 @@ export const askQuestions = async (
}
}
if (!program.dataSources) {
if (ciInfo.isCI) {
program.dataSources = getPrefOrDefault("dataSources");
if (program.files) {
// If user specified files option, then the program should use context engine
program.engine == "context";
if (!fs.existsSync(program.files)) {
console.log("File or folder not found");
process.exit(1);
} else {
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?",
choices: getDataSourceChoices(
program.framework,
program.dataSources,
),
initial: firstQuestion ? 1 : 0,
},
handlers,
);
if (selectedSource === "no" || selectedSource === "none") {
// user doesn't want another data source or any data source
break;
}
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: "file",
config: {
path: p,
},
});
}
} 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;
},
},
handlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
}
}
program.dataSource = {
type: fs.lstatSync(program.files).isDirectory() ? "folder" : "file",
config: {
path: program.files,
},
};
}
}
// Asking for LlamaParse if user selected file or folder data source
if (
program.dataSources.some((ds) => ds.type === "file") &&
program.useLlamaParse === undefined
) {
if (!program.engine) {
if (ciInfo.isCI) {
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
program.engine = getPrefOrDefault("engine");
} else {
const { useLlamaParse } = await prompts(
const { dataSource } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
active: "yes",
inactive: "no",
type: "select",
name: "dataSource",
message: "Which data source would you like to use?",
choices: getDataSourceChoices(program.framework),
initial: 1,
},
handlers,
);
program.useLlamaParse = useLlamaParse;
// Initialize with default config
program.dataSource = getPrefOrDefault("dataSource");
if (program.dataSource) {
switch (dataSource) {
case "simple":
program.engine = "simple";
program.dataSource = { type: "none", config: {} };
break;
case "exampleFile":
program.engine = "context";
// Treat example as a folder data source with no config
program.dataSource = { type: "folder", config: {} };
break;
case "localFile":
program.engine = "context";
program.dataSource = {
type: "file",
config: {
path: await selectLocalContextData("file"),
},
};
break;
case "localFolder":
program.engine = "context";
program.dataSource = {
type: "folder",
config: {
path: await selectLocalContextData("folder"),
},
};
break;
case "web":
program.engine = "context";
program.dataSource.type = "web";
break;
}
}
}
} else if (!program.dataSource) {
// Handle a case when engine is specified but dataSource is not
if (program.engine === "context") {
program.dataSource = {
type: "folder",
config: {},
};
} else if (program.engine === "simple") {
program.dataSource = {
type: "none",
config: {},
};
}
}
if (
(program.dataSource?.type === "file" ||
program.dataSource?.type === "folder") &&
program.framework === "fastapi"
) {
if (ciInfo.isCI) {
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
} else {
const dataSourceConfig = program.dataSource.config as FileSourceConfig;
dataSourceConfig.useLlamaParse = program.llamaParse;
// Is pdf file selected as data source or is it a folder data source
const askingLlamaParse =
dataSourceConfig.useLlamaParse === undefined &&
(program.dataSource.type === "folder"
? true
: dataSourceConfig.path &&
path.extname(dataSourceConfig.path) === ".pdf");
// Ask if user wants to use LlamaParse
if (askingLlamaParse) {
const { useLlamaParse } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: true,
active: "yes",
inactive: "no",
},
handlers,
);
dataSourceConfig.useLlamaParse = useLlamaParse;
program.dataSource.config = dataSourceConfig;
}
// Ask for LlamaCloud API key
if (useLlamaParse && program.llamaCloudKey === undefined) {
if (
dataSourceConfig.useLlamaParse &&
program.llamaCloudKey === undefined
) {
const { llamaCloudKey } = await prompts(
{
type: "text",
@@ -719,7 +746,39 @@ export const askQuestions = async (
}
}
if (program.dataSources.length > 0 && !program.vectorDb) {
if (program.dataSource?.type === "web" && program.framework === "fastapi") {
let { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website:",
initial: "https://www.llamaindex.ai",
},
handlers,
);
try {
if (!baseUrl.includes("://")) {
baseUrl = `https://${baseUrl}`;
}
const checkUrl = new URL(baseUrl);
if (checkUrl.protocol !== "https:" && checkUrl.protocol !== "http:") {
throw new Error("Invalid protocol");
}
} catch (error) {
console.log(
red(
"Invalid URL provided! Please provide a valid URL (e.g. https://www.llamaindex.ai)",
),
);
process.exit(1);
}
program.dataSource.config = {
baseUrl: baseUrl,
depth: 1,
};
}
if (program.engine !== "simple" && !program.vectorDb) {
if (ciInfo.isCI) {
program.vectorDb = getPrefOrDefault("vectorDb");
} else {
@@ -738,15 +797,15 @@ export const askQuestions = async (
}
}
// TODO: allow tools also without datasources
if (!program.tools && program.dataSources.length > 0) {
if (
!program.tools &&
program.framework === "fastapi" &&
program.engine === "context"
) {
if (ciInfo.isCI) {
program.tools = getPrefOrDefault("tools");
} else {
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
const toolChoices = supportedTools.map((tool) => ({
title: tool.display,
value: tool.name,
}));
@@ -765,5 +824,30 @@ 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();
// TODO: consider using zod to validate the input (doesn't work like this as not every option is required)
// templateUISchema.parse(program.ui);
// templateEngineSchema.parse(program.engine);
// templateFrameworkSchema.parse(program.framework);
// templateTypeSchema.parse(program.template);``
};
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
# build dist/index.js file
pnpm run build:ncc
# add shebang to the top of dist/index.js
# XXX: Windows needs a space after `node` to work correctly
# Note: ncc can handle shebang but it didn't work with Windows in our tests
echo '#!/usr/bin/env node ' | cat - dist/index.js >temp && mv temp dist/index.js
# make dist/index.js executable
chmod +x dist/index.js
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
pnpm pack && npm install -g $(pwd)/$(ls ./*.tgz | head -1)
@@ -1,26 +0,0 @@
FROM python:3.11 as build
WORKDIR /app
ENV PYTHONPATH=/app
# Install Poetry
RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \
cd /usr/local/bin && \
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
# ====================================
FROM build as release
COPY . .
CMD ["python", "main.py"]
@@ -1,16 +0,0 @@
FROM node:20-alpine as build
WORKDIR /app
# Install dependencies
COPY package.json package-lock.* ./
RUN npm install
# Build the application
COPY . .
RUN npm run build
# ====================================
FROM build as release
CMD ["npm", "run", "start"]
@@ -1,4 +1,4 @@
import yaml
import json
import importlib
from llama_index.core.tools.tool_spec.base import BaseToolSpec
@@ -26,8 +26,8 @@ class ToolFactory:
@staticmethod
def from_env() -> list[FunctionTool]:
tools = []
with open("config/tools.yaml", "r") as f:
tool_configs = yaml.safe_load(f)
with open("tools_config.json", "r") as f:
tool_configs = json.load(f)
for name, config in tool_configs.items():
tools += ToolFactory.create_tool(name, **config)
return tools
@@ -1,26 +0,0 @@
import config from "@/config/tools.json";
import { OpenAI, OpenAIAgent, QueryEngineTool, ToolFactory } from "llamaindex";
import { STORAGE_CACHE_DIR } from "./constants.mjs";
import { getDataSource } from "./index";
export async function createChatEngine(llm: OpenAI) {
const index = await getDataSource(llm);
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;
}
@@ -1,13 +0,0 @@
import { ContextChatEngine, LLM } from "llamaindex";
import { getDataSource } from "./index";
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
const retriever = index.asRetriever();
retriever.similarityTopK = 3;
return new ContextChatEngine({
chatModel: llm,
retriever,
});
}
@@ -1,32 +0,0 @@
import os
import yaml
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
logger = logging.getLogger(__name__)
def load_configs():
with open("config/loaders.yaml") as f:
configs = yaml.safe_load(f)
return configs
def get_documents():
documents = []
config = load_configs()
for loader_type, loader_config in config.items():
logger.info(
f"Loading documents from loader: {loader_type}, config: {loader_config}"
)
if loader_type == "file":
document = get_file_documents(FileLoaderConfig(**loader_config))
documents.extend(document)
elif loader_type == "web":
document = get_web_documents(WebLoaderConfig(**loader_config))
documents.extend(document)
return documents
@@ -1,37 +0,0 @@
import os
from llama_parse import LlamaParse
from pydantic import BaseModel, validator
class FileLoaderConfig(BaseModel):
data_dir: str = "data"
use_llama_parse: bool = False
@validator("data_dir")
def data_dir_must_exist(cls, v):
if not os.path.isdir(v):
raise ValueError(f"Directory '{v}' does not exist")
return v
def llama_parse_parser():
if os.getenv("LLAMA_CLOUD_API_KEY") is None:
raise ValueError(
"LLAMA_CLOUD_API_KEY environment variable is not set. "
"Please set it in .env file or in your shell environment then run again!"
)
parser = LlamaParse(result_type="markdown", verbose=True, language="en")
return parser
def get_file_documents(config: FileLoaderConfig):
from llama_index.core.readers import SimpleDirectoryReader
reader = SimpleDirectoryReader(
config.data_dir,
recursive=True,
)
if config.use_llama_parse:
parser = llama_parse_parser()
reader.file_extractor = {".pdf": parser}
return reader.load_data()
@@ -0,0 +1,7 @@
from llama_index.core.readers import SimpleDirectoryReader
DATA_DIR = "data" # directory containing the documents
def get_documents():
return SimpleDirectoryReader(DATA_DIR).load_data()
@@ -0,0 +1,17 @@
import os
from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
DATA_DIR = "data" # directory containing the documents
def get_documents():
if os.getenv("LLAMA_CLOUD_API_KEY") is None:
raise ValueError(
"LLAMA_CLOUD_API_KEY environment variable is not set. "
"Please set it in .env file or in your shell environment then run again!"
)
parser = LlamaParse(result_type="markdown", verbose=True, language="en")
reader = SimpleDirectoryReader(DATA_DIR, file_extractor={".pdf": parser})
return reader.load_data()
@@ -1,36 +0,0 @@
import os
import json
from pydantic import BaseModel, Field
class CrawlUrl(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
@@ -0,0 +1,13 @@
import os
from llama_index.readers.web import WholeSiteReader
def get_documents():
# Initialize the scraper with a prefix URL and maximum depth
scraper = WholeSiteReader(
prefix=os.environ.get("URL_PREFIX"), max_depth=int(os.environ.get("MAX_DEPTH"))
)
# Start scraping from a base URL
documents = scraper.load_data(base_url=os.environ.get("BASE_URL"))
return documents
@@ -1,9 +0,0 @@
import { SimpleDirectoryReader } from "llamaindex";
export const DATA_DIR = "./data";
export async function getDocuments() {
return await new SimpleDirectoryReader().loadData({
directoryPath: DATA_DIR,
});
}
@@ -1,19 +0,0 @@
import {
FILE_EXT_TO_READER,
LlamaParseReader,
SimpleDirectoryReader,
} from "llamaindex";
export const DATA_DIR = "./data";
export async function getDocuments() {
const reader = new SimpleDirectoryReader();
// Load PDFs using LlamaParseReader
return await reader.loadData({
directoryPath: DATA_DIR,
fileExtToReader: {
...FILE_EXT_TO_READER,
pdf: new LlamaParseReader({ resultType: "markdown" }),
},
});
}
@@ -1,5 +1,5 @@
import * as LlamaIndex from "@llamaindex/edge";
import * as traceloop from "@traceloop/node-server-sdk";
import * as LlamaIndex from "llamaindex";
export const initObservability = () => {
traceloop.initialize({
@@ -7,7 +7,7 @@ export default function ChatItem(message: Message) {
return (
<div className="flex items-start gap-4 pt-5">
<ChatAvatar {...message} />
<p className="break-words whitespace-pre-wrap">{message.content}</p>
<p className="break-words">{message.content}</p>
</div>
);
}
@@ -8,7 +8,7 @@ from llama_index.core.storage import StorageContext
from llama_index.core.indices import VectorStoreIndex
from llama_index.vector_stores.milvus import MilvusVectorStore
from app.settings import init_settings
from app.engine.loaders import get_documents
from app.engine.loader import get_documents
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
@@ -20,7 +20,7 @@ def generate_datasource():
documents = get_documents()
store = MilvusVectorStore(
uri=os.environ["MILVUS_ADDRESS"],
user=os.getenv("MILVUS_USERNAME"),
user=os.getenv("MILVUS_USER"),
password=os.getenv("MILVUS_PASSWORD"),
collection_name=os.getenv("MILVUS_COLLECTION"),
dim=int(os.getenv("MILVUS_DIMENSION", "1536")),
@@ -12,7 +12,7 @@ def get_index():
logger.info("Connecting to index from Milvus...")
store = MilvusVectorStore(
uri=os.getenv("MILVUS_ADDRESS"),
user=os.getenv("MILVUS_USERNAME"),
user=os.getenv("MILVUS_USER"),
password=os.getenv("MILVUS_PASSWORD"),
collection_name=os.getenv("MILVUS_COLLECTION"),
dim=int(os.getenv("EMBEDDING_DIM", "1536")),
@@ -8,7 +8,7 @@ from llama_index.core.storage import StorageContext
from llama_index.core.indices import VectorStoreIndex
from llama_index.vector_stores.mongodb import MongoDBAtlasVectorSearch
from app.settings import init_settings
from app.engine.loaders import get_documents
from app.engine.loader import get_documents
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
@@ -7,7 +7,7 @@ from llama_index.core.indices import (
VectorStoreIndex,
)
from app.engine.constants import STORAGE_DIR
from app.engine.loaders import get_documents
from app.engine.loader import get_documents
from app.settings import init_settings
@@ -6,7 +6,7 @@ import logging
from llama_index.core.indices import VectorStoreIndex
from llama_index.core.storage import StorageContext
from app.engine.loaders import get_documents
from app.engine.loader import get_documents
from app.settings import init_settings
from app.engine.utils import init_pg_vector_store_from_env
@@ -8,7 +8,7 @@ from llama_index.core.storage import StorageContext
from llama_index.core.indices import VectorStoreIndex
from llama_index.vector_stores.pinecone import PineconeVectorStore
from app.settings import init_settings
from app.engine.loaders import get_documents
from app.engine.loader import get_documents
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
@@ -1,12 +1,15 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { VectorStoreIndex } from "@llamaindex/edge";
import { SimpleDirectoryReader } from "@llamaindex/edge/readers/SimpleDirectoryReader";
import { storageContextFromDefaults } from "@llamaindex/edge/storage/StorageContext";
import { MilvusVectorStore } from "@llamaindex/edge/storage/vectorStore/MilvusVectorStore";
import * as dotenv from "dotenv";
import {
MilvusVectorStore,
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader.mjs";
import { checkRequiredEnvVars, getMilvusClient } from "./shared.mjs";
STORAGE_DIR,
checkRequiredEnvVars,
getMilvusClient,
} from "./shared.mjs";
dotenv.config();
@@ -14,7 +17,9 @@ const collectionName = process.env.MILVUS_COLLECTION;
async function loadAndIndex() {
// load objects from storage and convert them into LlamaIndex Document objects
const documents = await getDocuments();
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: STORAGE_DIR,
});
// Connect to Milvus
const milvusClient = getMilvusClient();
@@ -1,14 +1,14 @@
import {
ContextChatEngine,
LLM,
MilvusVectorStore,
serviceContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
serviceContextFromDefaults,
} from "@llamaindex/edge";
import { MilvusVectorStore } from "@llamaindex/edge/storage/vectorStore/MilvusVectorStore";
import {
checkRequiredEnvVars,
CHUNK_OVERLAP,
CHUNK_SIZE,
checkRequiredEnvVars,
getMilvusClient,
} from "./shared.mjs";
@@ -1,5 +1,6 @@
import { MilvusClient } from "@zilliz/milvus2-sdk-node";
export const STORAGE_DIR = "./data";
export const CHUNK_SIZE = 512;
export const CHUNK_OVERLAP = 20;
@@ -1,13 +1,11 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { VectorStoreIndex } from "@llamaindex/edge";
import { SimpleDirectoryReader } from "@llamaindex/edge/readers/SimpleDirectoryReader";
import { storageContextFromDefaults } from "@llamaindex/edge/storage/StorageContext";
import { MongoDBAtlasVectorSearch } from "@llamaindex/edge/storage/vectorStore/MongoDBAtlasVectorSearch";
import * as dotenv from "dotenv";
import {
MongoDBAtlasVectorSearch,
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { MongoClient } from "mongodb";
import { getDocuments } from "./loader.mjs";
import { checkRequiredEnvVars } from "./shared.mjs";
import { STORAGE_DIR, checkRequiredEnvVars } from "./shared.mjs";
dotenv.config();
@@ -21,7 +19,9 @@ async function loadAndIndex() {
const client = new MongoClient(mongoUri);
// load objects from storage and convert them into LlamaIndex Document objects
const documents = await getDocuments();
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: STORAGE_DIR,
});
// create Atlas as a vector store
const vectorStore = new MongoDBAtlasVectorSearch({
@@ -2,12 +2,12 @@
import {
ContextChatEngine,
LLM,
MongoDBAtlasVectorSearch,
serviceContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
serviceContextFromDefaults,
} from "@llamaindex/edge";
import { MongoDBAtlasVectorSearch } from "@llamaindex/edge/storage/vectorStore/MongoDBAtlasVectorSearch";
import { MongoClient } from "mongodb";
import { checkRequiredEnvVars, CHUNK_OVERLAP, CHUNK_SIZE } from "./shared.mjs";
import { CHUNK_OVERLAP, CHUNK_SIZE, checkRequiredEnvVars } from "./shared.mjs";
async function getDataSource(llm: LLM) {
checkRequiredEnvVars();
@@ -1,3 +1,4 @@
export const STORAGE_DIR = "./data";
export const CHUNK_SIZE = 512;
export const CHUNK_OVERLAP = 20;
@@ -1,3 +1,4 @@
export const STORAGE_DIR = "./data";
export const STORAGE_CACHE_DIR = "./cache";
export const CHUNK_SIZE = 512;
export const CHUNK_OVERLAP = 20;
@@ -1,13 +1,15 @@
import {
serviceContextFromDefaults,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
import { VectorStoreIndex, serviceContextFromDefaults } from "@llamaindex/edge";
import { SimpleDirectoryReader } from "@llamaindex/edge/readers/SimpleDirectoryReader";
import { storageContextFromDefaults } from "@llamaindex/edge/storage/StorageContext";
import * as dotenv from "dotenv";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
import { getDocuments } from "./loader.mjs";
import {
CHUNK_OVERLAP,
CHUNK_SIZE,
STORAGE_CACHE_DIR,
STORAGE_DIR,
} from "./constants.mjs";
// Load environment variables from local .env file
dotenv.config();
@@ -26,7 +28,9 @@ async function generateDatasource(serviceContext) {
const storageContext = await storageContextFromDefaults({
persistDir: STORAGE_CACHE_DIR,
});
const documents = await getDocuments();
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: STORAGE_DIR,
});
await VectorStoreIndex.fromDocuments(documents, {
storageContext,
serviceContext,
@@ -1,13 +1,14 @@
import {
ContextChatEngine,
LLM,
serviceContextFromDefaults,
SimpleDocumentStore,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
serviceContextFromDefaults,
} from "@llamaindex/edge";
import { storageContextFromDefaults } from "@llamaindex/edge/storage/StorageContext";
import { SimpleDocumentStore } from "@llamaindex/edge/storage/docStore/SimpleDocumentStore";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
export async function getDataSource(llm: LLM) {
async function getDataSource(llm: LLM) {
const serviceContext = serviceContextFromDefaults({
llm,
chunkSize: CHUNK_SIZE,
@@ -30,3 +31,14 @@ export async function getDataSource(llm: LLM) {
serviceContext,
});
}
export async function createChatEngine(llm: LLM) {
const index = await getDataSource(llm);
const retriever = index.asRetriever();
retriever.similarityTopK = 3;
return new ContextChatEngine({
chatModel: llm,
retriever,
});
}
@@ -1,15 +1,13 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { VectorStoreIndex } from "@llamaindex/edge";
import { SimpleDirectoryReader } from "@llamaindex/edge/readers/SimpleDirectoryReader";
import { storageContextFromDefaults } from "@llamaindex/edge/storage/StorageContext";
import { PGVectorStore } from "@llamaindex/edge/storage/vectorStore/PGVectorStore";
import * as dotenv from "dotenv";
import {
PGVectorStore,
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader.mjs";
import {
PGVECTOR_COLLECTION,
PGVECTOR_SCHEMA,
PGVECTOR_TABLE,
STORAGE_DIR,
checkRequiredEnvVars,
} from "./shared.mjs";
@@ -17,7 +15,9 @@ dotenv.config();
async function loadAndIndex() {
// load objects from storage and convert them into LlamaIndex Document objects
const documents = await getDocuments();
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: STORAGE_DIR,
});
// create postgres vector store
const vectorStore = new PGVectorStore({
@@ -25,7 +25,7 @@ async function loadAndIndex() {
schemaName: PGVECTOR_SCHEMA,
tableName: PGVECTOR_TABLE,
});
vectorStore.setCollection(PGVECTOR_COLLECTION);
vectorStore.setCollection(STORAGE_DIR);
vectorStore.clearCollection();
// create index from all the Documents
@@ -2,10 +2,10 @@
import {
ContextChatEngine,
LLM,
PGVectorStore,
VectorStoreIndex,
serviceContextFromDefaults,
} from "llamaindex";
} from "@llamaindex/edge";
import { PGVectorStore } from "@llamaindex/edge/storage/vectorStore/PGVectorStore";
import {
CHUNK_OVERLAP,
CHUNK_SIZE,
@@ -1,4 +1,4 @@
export const PGVECTOR_COLLECTION = "data";
export const STORAGE_DIR = "./data";
export const CHUNK_SIZE = 512;
export const CHUNK_OVERLAP = 20;
export const PGVECTOR_SCHEMA = "public";
@@ -1,18 +1,18 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { VectorStoreIndex } from "@llamaindex/edge";
import { SimpleDirectoryReader } from "@llamaindex/edge/readers/SimpleDirectoryReader";
import { storageContextFromDefaults } from "@llamaindex/edge/storage/StorageContext";
import { PineconeVectorStore } from "@llamaindex/edge/storage/vectorStore/PineconeVectorStore";
import * as dotenv from "dotenv";
import {
PineconeVectorStore,
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader.mjs";
import { checkRequiredEnvVars } from "./shared.mjs";
import { STORAGE_DIR, checkRequiredEnvVars } from "./shared.mjs";
dotenv.config();
async function loadAndIndex() {
// load objects from storage and convert them into LlamaIndex Document objects
const documents = await getDocuments();
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: STORAGE_DIR,
});
// create vector store
const vectorStore = new PineconeVectorStore();
@@ -2,10 +2,10 @@
import {
ContextChatEngine,
LLM,
PineconeVectorStore,
VectorStoreIndex,
serviceContextFromDefaults,
} from "llamaindex";
} from "@llamaindex/edge";
import { PineconeVectorStore } from "@llamaindex/edge/storage/vectorStore/PineconeVectorStore";
import { CHUNK_OVERLAP, CHUNK_SIZE, checkRequiredEnvVars } from "./shared.mjs";
async function getDataSource(llm: LLM) {
@@ -1,3 +1,4 @@
export const STORAGE_DIR = "./data";
export const CHUNK_SIZE = 512;
export const CHUNK_OVERLAP = 20;
@@ -0,0 +1,50 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Express](https://expressjs.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Second, run the development server:
```
npm run dev
```
Then call the express 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 `src/controllers/chat.controller.ts`. The endpoint auto-updates as you save the file.
## Production
First, build the project:
```
npm run build
```
You can then run the production server:
```
NODE_ENV=production npm run start
```
> Note that the `NODE_ENV` environment variable is set to `production`. This disables CORS for all origins.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -0,0 +1,3 @@
{
"extends": "eslint:recommended"
}
+3
View File
@@ -0,0 +1,3 @@
# local env files
.env
node_modules/
+44
View File
@@ -0,0 +1,44 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import cors from "cors";
import "dotenv/config";
import express, { Express, Request, Response } from "express";
import { initObservability } from "./src/observability";
import chatRouter from "./src/routes/chat.route";
const app: Express = express();
const port = parseInt(process.env.PORT || "8000");
const env = process.env["NODE_ENV"];
const isDevelopment = !env || env === "development";
const prodCorsOrigin = process.env["PROD_CORS_ORIGIN"];
initObservability();
app.use(express.json());
if (isDevelopment) {
console.warn("Running in development mode - allowing CORS for all origins");
app.use(cors());
} else if (prodCorsOrigin) {
console.log(
`Running in production mode - allowing CORS for domain: ${prodCorsOrigin}`,
);
const corsOptions = {
origin: prodCorsOrigin, // Restrict to production domain
};
app.use(cors(corsOptions));
} else {
console.warn("Production CORS origin not set, defaulting to no CORS.");
}
app.use(express.text());
app.get("/", (req: Request, res: Response) => {
res.send("LlamaIndex Express Server");
});
app.use("/api/chat", chatRouter);
app.listen(port, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
});
@@ -0,0 +1,27 @@
{
"name": "llama-index-express",
"version": "1.0.0",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsup index.ts --format esm --dts",
"start": "node dist/index.js",
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.js\""
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"@llamaindex/edge": "^0.2.1"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.9.5",
"concurrently": "^8.2.2",
"eslint": "^8.54.0",
"nodemon": "^3.0.1",
"tsup": "^7.3.0",
"typescript": "^5.3.2"
}
}
@@ -1,6 +1,6 @@
import { ChatMessage, MessageContent, OpenAI } from "@llamaindex/edge";
import { Request, Response } from "express";
import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
import { createChatEngine } from "./engine/chat";
import { createChatEngine } from "./engine";
const convertMessageContent = (
textMessage: string,
@@ -21,7 +21,7 @@ const convertMessageContent = (
];
};
export const chatRequest = async (req: Request, res: Response) => {
export const chat = async (req: Request, res: Response) => {
try {
const { messages, data }: { messages: ChatMessage[]; data: any } = req.body;
const userMessage = messages.pop();
@@ -48,7 +48,7 @@ export const chatRequest = async (req: Request, res: Response) => {
// Calling LlamaIndex's ChatEngine to get a response
const response = await chatEngine.chat({
message: userMessageContent,
chatHistory: messages,
messages,
});
const result: ChatMessage = {
role: "assistant",
@@ -1,4 +1,4 @@
import { LLM, SimpleChatEngine } from "llamaindex";
import { LLM, SimpleChatEngine } from "@llamaindex/edge";
export async function createChatEngine(llm: LLM) {
return new SimpleChatEngine({
@@ -0,0 +1 @@
export const initObservability = () => {};
@@ -0,0 +1,8 @@
import express from "express";
import { chat } from "../controllers/chat.controller";
const llmRouter = express.Router();
llmRouter.route("/").post(chat);
export default llmRouter;
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2016",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node"
}
}
@@ -0,0 +1,58 @@
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>
```
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
```
## 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!
@@ -0,0 +1,54 @@
from typing import List
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, 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
chat_router = r = APIRouter()
class _Message(BaseModel):
role: MessageRole
content: str
class _ChatData(BaseModel):
messages: List[_Message]
class _Result(BaseModel):
result: _Message
@r.post("")
async def chat(
data: _ChatData,
chat_engine: BaseChatEngine = Depends(get_chat_engine),
) -> _Result:
# check preconditions and get last message
if len(data.messages) == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No messages provided",
)
lastMessage = data.messages.pop()
if lastMessage.role != MessageRole.USER:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Last message must be from user",
)
# convert messages coming from the request to type ChatMessage
messages = [
ChatMessage(
role=m.role,
content=m.content,
)
for m in data.messages
]
# query chat engine
response = await chat_engine.achat(lastMessage.content, messages)
return _Result(
result=_Message(role=MessageRole.ASSISTANT, content=response.response)
)
@@ -0,0 +1,5 @@
from llama_index.core.chat_engine import SimpleChatEngine
def get_chat_engine():
return SimpleChatEngine.from_defaults()
@@ -0,0 +1,41 @@
import os
from typing import Dict
from llama_index.core.settings import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
def llm_config_from_env() -> Dict:
from llama_index.core.constants import DEFAULT_TEMPERATURE
model = os.getenv("MODEL")
temperature = os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)
max_tokens = os.getenv("LLM_MAX_TOKENS")
config = {
"model": model,
"temperature": float(temperature),
"max_tokens": int(max_tokens) if max_tokens is not None else None,
}
return config
def embedding_config_from_env() -> Dict:
model = os.getenv("EMBEDDING_MODEL")
dimension = os.getenv("EMBEDDING_DIM")
config = {
"model": model,
"dimension": int(dimension) if dimension is not None else None,
}
return config
def init_settings():
llm_configs = llm_config_from_env()
embedding_configs = embedding_config_from_env()
Settings.llm = OpenAI(**llm_configs)
Settings.embed_model = OpenAIEmbedding(**embedding_configs)
Settings.chunk_size = int(os.getenv("CHUNK_SIZE", "1024"))
Settings.chunk_overlap = int(os.getenv("CHUNK_OVERLAP", "20"))
+3
View File
@@ -0,0 +1,3 @@
__pycache__
storage
.env
+39
View File
@@ -0,0 +1,39 @@
from dotenv import load_dotenv
load_dotenv()
import logging
import os
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routers.chat import chat_router
from app.settings import init_settings
app = FastAPI()
init_settings()
environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
if environment == "dev":
logger = logging.getLogger("uvicorn")
logger.warning("Running in development mode - allowing CORS for all origins")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(chat_router, prefix="/api/chat")
if __name__ == "__main__":
app_host = os.getenv("APP_HOST", "0.0.0.0")
app_port = int(os.getenv("APP_PORT", "8000"))
reload = True if environment == "dev" else False
uvicorn.run(app="main:app", host=app_host, port=app_port, reload=reload)
@@ -0,0 +1,17 @@
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["Marcus Schiesser <mail@marcusschiesser.de>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11,<3.12"
fastapi = "^0.109.1"
uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
llama-index = "^0.10.7"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
@@ -8,41 +8,21 @@ First, install the dependencies:
npm install
```
Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step):
```
npm run generate
```
Third, run the development server:
Second, run the development server:
```
npm run dev
```
The example provides two different API endpoints:
1. `/api/chat` - a streaming chat endpoint (found in `src/controllers/chat.controller.ts`)
2. `/api/chat/request` - a non-streaming chat endpoint (found in `src/controllers/chat-request.controller.ts`)
You can test the streaming endpoint with the following curl request:
Then call the express API endpoint `/api/chat` to see the result:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--header 'Content-Type: text/plain' \
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
```
And for the non-streaming endpoint run:
```
curl --location 'localhost:8000/api/chat/request' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
```
You can start editing the API by modifying `src/controllers/chat.controller.ts` or `src/controllers/chat-request.controller.ts`. The endpoint auto-updates as you save the file.
You can delete the endpoint that you're not using.
You can start editing the API by modifying `src/controllers/chat.controller.ts`. The endpoint auto-updates as you save the file.
## Production
@@ -60,39 +40,6 @@ 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
1. Build an image for the Express API:
```
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:
```
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>
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -1,7 +1,3 @@
{
"extends": ["eslint:recommended", "prettier"],
"rules": {
"max-params": ["error", 4],
"prefer-const": "error"
}
"extends": "eslint:recommended"
}
@@ -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,9 +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"
"typescript": "^5.3.2"
}
}
@@ -1,3 +0,0 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
@@ -1,7 +1,7 @@
import { ChatMessage, MessageContent, OpenAI } from "@llamaindex/edge";
import { streamToResponse } from "ai";
import { Request, Response } from "express";
import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
import { createChatEngine } from "./engine/chat";
import { createChatEngine } from "./engine";
import { LlamaIndexStream } from "./llamaindex-stream";
const convertMessageContent = (
@@ -1,4 +1,4 @@
import { LLM, SimpleChatEngine } from "llamaindex";
import { LLM, SimpleChatEngine } from "@llamaindex/edge";
export async function createChatEngine(llm: LLM) {
return new SimpleChatEngine({
@@ -1,3 +1,4 @@
import { Response } from "@llamaindex/edge";
import {
JSONValue,
createCallbacksTransformer,
@@ -6,7 +7,6 @@ import {
trimStartOfStreamHelper,
type AIStreamCallbacksAndOptions,
} from "ai";
import { Response, StreamingAgentChatResponse } from "llamaindex";
type ParserOptions = {
image_url?: string;
@@ -52,17 +52,13 @@ function createParser(
}
export function LlamaIndexStream(
response: StreamingAgentChatResponse | AsyncIterable<Response>,
res: AsyncIterable<Response>,
opts?: {
callbacks?: AIStreamCallbacksAndOptions;
parserOptions?: ParserOptions;
},
): { stream: ReadableStream; data: experimental_StreamData } {
const data = new experimental_StreamData();
const res =
response instanceof StreamingAgentChatResponse
? response.response
: response;
return {
stream: createParser(res, data, opts?.parserOptions)
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
@@ -1,10 +1,8 @@
import express, { Router } from "express";
import { chatRequest } from "../controllers/chat-request.controller";
import express from "express";
import { chat } from "../controllers/chat.controller";
const llmRouter: Router = express.Router();
const llmRouter = express.Router();
llmRouter.route("/").post(chat);
llmRouter.route("/request").post(chatRequest);
export default llmRouter;
@@ -5,9 +5,6 @@
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node",
"paths": {
"@/*": ["./*"]
}
"moduleResolution": "node"
}
}
@@ -11,7 +11,7 @@ poetry install
poetry shell
```
By default, we use the OpenAI LLM (though you can customize, see `app/settings.py`). As a result, you need to specify an `OPENAI_API_KEY` in an .env file in this directory.
By default, we use the OpenAI LLM (though you can customize, see `app/settings.py`). As a result you need to specify an `OPENAI_API_KEY` in an .env file in this directory.
Example `.env` file:
@@ -19,8 +19,6 @@ Example `.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):
```
@@ -33,12 +31,7 @@ Third, run the development server:
python main.py
```
The example provides two different API endpoints:
1. `/api/chat` - a streaming chat endpoint
2. `/api/chat/request` - a non-streaming chat endpoint
You can test the streaming endpoint with the following curl request:
Then call the API endpoint `/api/chat` to see the result:
```
curl --location 'localhost:8000/api/chat' \
@@ -46,15 +39,7 @@ curl --location 'localhost:8000/api/chat' \
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
```
And for the non-streaming endpoint run:
```
curl --location 'localhost:8000/api/chat/request' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
```
You can start editing the API endpoints by modifying `app/api/routers/chat.py`. The endpoints auto-update as you save the file. You can delete the endpoint you're not using.
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.
@@ -64,40 +49,6 @@ The API allows CORS for all origins to simplify development. You can change this
ENVIRONMENT=prod python main.py
```
## Using Docker
1. Build an image for the 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:
```
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>
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -5,7 +5,6 @@ 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()
@@ -19,19 +18,20 @@ class _ChatData(BaseModel):
messages: List[_Message]
class _Result(BaseModel):
result: _Message
async def parse_chat_data(data: _ChatData) -> Tuple[str, List[ChatMessage]]:
@r.post("")
async def chat(
request: Request,
data: _ChatData,
chat_engine: BaseChatEngine = Depends(get_chat_engine),
):
# check preconditions and get last message
if len(data.messages) == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No messages provided",
)
last_message = data.messages.pop()
if last_message.role != MessageRole.USER:
lastMessage = data.messages.pop()
if lastMessage.role != MessageRole.USER:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Last message must be from user",
@@ -44,38 +44,16 @@ async def parse_chat_data(data: _ChatData) -> Tuple[str, List[ChatMessage]]:
)
for m in data.messages
]
return last_message.content, messages
# query chat engine
response = await chat_engine.astream_chat(lastMessage.content, messages)
# streaming endpoint - delete if not needed
@r.post("")
async def chat(
request: Request,
data: _ChatData,
chat_engine: BaseChatEngine = Depends(get_chat_engine),
):
last_message_content, messages = await parse_chat_data(data)
response = await chat_engine.astream_chat(last_message_content, messages)
# stream response
async def event_generator():
async for token in response.async_response_gen():
# If client closes connection, stop sending events
if await request.is_disconnected():
break
yield token
return StreamingResponse(event_generator(), media_type="text/plain")
# non-streaming endpoint - delete if not needed
@r.post("/request")
async def chat_request(
data: _ChatData,
chat_engine: BaseChatEngine = Depends(get_chat_engine),
) -> _Result:
last_message_content, messages = await parse_chat_data(data)
response = await chat_engine.achat(last_message_content, messages)
return _Result(
result=_Message(role=MessageRole.ASSISTANT, content=response.response)
)
@@ -10,9 +10,7 @@ python = "^3.11,<3.12"
fastapi = "^0.109.1"
uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
llama-index = "0.10.15"
llama-index-core = "0.10.15"
llama-index-agent-openai = "0.1.5"
llama-index = "^0.10.7"
[build-system]
requires = ["poetry-core"]
@@ -8,13 +8,7 @@ First, install the dependencies:
npm install
```
Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step):
```
npm run generate
```
Third, run the development server:
Second, run the development server:
```
npm run dev
@@ -26,41 +20,6 @@ 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
1. Build an image for the Next.js 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:
```
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>
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -0,0 +1,7 @@
import { LLM, SimpleChatEngine } from "@llamaindex/edge";
export async function createChatEngine(llm: LLM) {
return new SimpleChatEngine({
llm,
});
}
@@ -1,3 +1,4 @@
import { Response } from "@llamaindex/edge";
import {
JSONValue,
createCallbacksTransformer,
@@ -6,7 +7,6 @@ import {
trimStartOfStreamHelper,
type AIStreamCallbacksAndOptions,
} from "ai";
import { Response, StreamingAgentChatResponse } from "llamaindex";
type ParserOptions = {
image_url?: string;
@@ -52,17 +52,13 @@ function createParser(
}
export function LlamaIndexStream(
response: StreamingAgentChatResponse | AsyncIterable<Response>,
res: AsyncIterable<Response>,
opts?: {
callbacks?: AIStreamCallbacksAndOptions;
parserOptions?: ParserOptions;
},
): { stream: ReadableStream; data: experimental_StreamData } {
const data = new experimental_StreamData();
const res =
response instanceof StreamingAgentChatResponse
? response.response
: response;
return {
stream: createParser(res, data, opts?.parserOptions)
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
@@ -1,13 +1,13 @@
import { initObservability } from "@/app/observability";
import { ChatMessage, MessageContent, OpenAI } from "@llamaindex/edge";
import { StreamingTextResponse } from "ai";
import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { createChatEngine } from "./engine/chat";
import { createChatEngine } from "./engine";
import { LlamaIndexStream } from "./llamaindex-stream";
initObservability();
export const runtime = "nodejs";
export const runtime = "edge";
export const dynamic = "force-dynamic";
const convertMessageContent = (

Some files were not shown because too many files have changed in this diff Show More