Compare commits

...

28 Commits

Author SHA1 Message Date
Marcus Schiesser 922e0ceb9b fix: simplify: remove UI question and use shadcn as default 2024-03-28 16:31:58 +08:00
Marcus Schiesser ae1536768a fix: Dockerfile for express and improve READMEs 2024-03-28 16:06:22 +08:00
Huu Le (Lee) 78ded9e242 feat: Add Dockerfile template (#27) 2024-03-28 12:35:27 +07:00
Marcus Schiesser 4f10840f04 fix: word-smith YAML comments 2024-03-28 12:28:52 +08:00
Marcus Schiesser 9ca343c34e fix: use config/tools.json for TS 2024-03-28 11:42:36 +08:00
Huu Le (Lee) ce2f24d73f feat: Use yaml format instead of json for loaders and tools config (#26) 2024-03-28 10:00:03 +07:00
Thuc Pham 65bc28d1f2 fix: remove eslint (#24) 2024-03-27 21:32:52 +08:00
Huu Le (Lee) 45e2eccb11 add filter for copy data files (#25) 2024-03-27 17:47:20 +08:00
Thuc Pham b38adf894d fix: type interred error for express router (#23) 2024-03-27 15:59:11 +07:00
Marcus Schiesser dd6f84fdd2 fix: don't copy data folder for example file 2024-03-27 15:45:53 +08:00
Marcus Schiesser 99e758fcbb docs(changeset): fix changesets 2024-03-27 15:10:34 +08:00
Marcus Schiesser b0720ccd8c fix: make chat template the default option 2024-03-27 15:07:44 +08:00
Marcus Schiesser 459824dbec fix: remove redundant template engine variable and fixed llamaparse flag (#22) 2024-03-27 14:06:14 +07:00
Thuc Pham b08cad0123 refactor: make non-streaming chat as /chat/request route (#20) 2024-03-27 12:59:18 +07:00
Huu Le (Lee) c7a978e0aa feat: Add multiple data sources (#19) 2024-03-27 09:33:12 +07:00
Thuc Pham 76aa33612c style: add list style for chat message markdown (#21) 2024-03-26 10:14:50 +07:00
Huu Le (Lee) e8db041d58 feat: Add multiple URLs reader (#18) 2024-03-25 12:43:50 +07:00
Marcus Schiesser b3f26856c4 docs(changeset): Add support for agent generation for Typescript 2024-03-25 10:01:28 +08:00
Thuc Pham 26ab74bd9a feat: support agent in typescript templates (#5) 2024-03-25 09:00:25 +07:00
Huu Le (Lee) 17afc91850 feat: Add multi files/folders selection (#15) 2024-03-22 15:41:44 +07:00
Marcus Schiesser cbc996fb30 RELEASING: Releasing 1 package(s)
Releases:
  create-llama@0.0.31

[skip ci]
2024-03-22 10:21:52 +07:00
Marcus Schiesser bf1430d5b6 fix: add chmod +x to dist/index.js (#16) 2024-03-22 09:47:49 +07:00
Huu Le (Lee) e9a6cd049a fixed pymilvus at 2.3.7 (#17) 2024-03-22 09:13:40 +07:00
Huu Le (Lee) 60ed8fe080 fixed llama-index package versions and add missing env for web data source (#14) 2024-03-22 08:43:23 +07:00
Thuc Pham 56faee0b44 fix: fix windows e2e (#13) 2024-03-21 13:25:32 +07:00
Marcus Schiesser abb7488895 ci: Add release-snapshot and new-snapshot scripts 2024-03-20 13:50:23 +07:00
Marcus Schiesser e851c0c834 docs: Update README templates to include instructions for generating embeddings 2024-03-20 13:29:59 +07:00
Marcus Schiesser 58d9c0c400 docs: Added changeset and publishing instructions 2024-03-20 11:32:12 +07:00
87 changed files with 1027 additions and 965 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Remove UI question (use shadcn as default). Use `html` UI by calling create-llama with --ui html parameter
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Update loaders and tools config to yaml format
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Let user select multiple datasources (URLs, files and folders)
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add Dockerfile template
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Merge non-streaming and streaming template to one
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add support for agent generation for Typescript
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
matrix:
node-version: [18, 20]
python-version: ["3.11"]
os: [macos-latest] #, windows-latest]
os: [macos-latest, windows-latest]
defaults:
run:
shell: bash
@@ -47,7 +47,7 @@ jobs:
run: pnpm run build
working-directory: .
- name: Install
run: pnpm run install-local
run: pnpm run pack-install
working-directory: .
- name: Run Playwright tests
run: pnpm run e2e
+3
View File
@@ -45,3 +45,6 @@ e2e/cache
# intellij
**/.idea
# build artifacts
create-llama-*.tgz
+8
View File
@@ -1,5 +1,13 @@
# 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
+28 -2
View File
@@ -32,10 +32,10 @@ To install it, call:
pnpm exec playwright install --with-deps
```
Then you can first install the `create-llama` command locally:
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 run install-local
pnpm link --global
```
And then finally run the tests:
@@ -45,3 +45,29 @@ 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,11 +26,9 @@ export type InstallAppArgs = Omit<
export async function createApp({
template,
framework,
engine,
ui,
appPath,
packageManager,
eslint,
frontend,
openAiKey,
llamaCloudKey,
@@ -41,8 +39,9 @@ export async function createApp({
vectorDb,
externalPort,
postInstallAction,
dataSource,
dataSources,
tools,
useLlamaParse,
observability,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
@@ -75,11 +74,9 @@ export async function createApp({
root,
template,
framework,
engine,
ui,
packageManager,
isOnline,
eslint,
openAiKey,
llamaCloudKey,
model,
@@ -89,8 +86,9 @@ export async function createApp({
vectorDb,
externalPort,
postInstallAction,
dataSource,
dataSources,
tools,
useLlamaParse,
observability,
};
@@ -127,11 +125,13 @@ 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(
"tools_config.json",
`file://${root}/tools_config.json`,
configFile,
`file://${root}/${configFile}`,
)} file.`,
),
);
+9 -25
View File
@@ -4,7 +4,6 @@ import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateEngine,
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
@@ -12,13 +11,13 @@ import type {
} from "../helpers";
import { createTestDir, runCreateLlama, type AppType } from "./utils";
const templateTypes: TemplateType[] = ["streaming", "simple"];
const templateTypes: TemplateType[] = ["streaming"];
const templateFrameworks: TemplateFramework[] = [
"nextjs",
"express",
"fastapi",
];
const templateEngines: TemplateEngine[] = ["simple", "context"];
const dataSources: string[] = ["--no-files", "--example-file"];
const templateUIs: TemplateUI[] = ["shadcn", "html"];
const templatePostInstallActions: TemplatePostInstallAction[] = [
"none",
@@ -27,24 +26,12 @@ const templatePostInstallActions: TemplatePostInstallAction[] = [
for (const templateType of templateTypes) {
for (const templateFramework of templateFrameworks) {
for (const templateEngine of templateEngines) {
for (const dataSource of dataSources) {
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 === "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 () => {
templateFramework === "nextjs" ? "" : "--frontend";
test.describe(`try create-llama ${templateType} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
let port: number;
let externalPort: number;
let cwd: string;
@@ -61,7 +48,7 @@ for (const templateType of templateTypes) {
cwd,
templateType,
templateFramework,
templateEngine,
dataSource,
templateUI,
vectorDb,
appType,
@@ -79,7 +66,6 @@ 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();
});
@@ -88,7 +74,6 @@ 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([
@@ -109,14 +94,13 @@ for (const templateType of templateTypes) {
expect(response.ok()).toBeTruthy();
});
test("Backend should response when calling API", async ({
test("Backend frameworks should response when calling non-streaming chat API", async ({
request,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(appType !== "--no-frontend");
const backendPort = appType === "" ? port : externalPort;
test.skip(templateFramework === "nextjs");
const response = await request.post(
`http://localhost:${backendPort}/api/chat`,
`http://localhost:${externalPort}/api/chat/request`,
{
data: {
messages: [
+7 -7
View File
@@ -4,7 +4,6 @@ import { mkdir } from "node:fs/promises";
import * as path from "path";
import waitPort from "wait-port";
import {
TemplateEngine,
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
@@ -67,7 +66,7 @@ export async function runCreateLlama(
cwd: string,
templateType: TemplateType,
templateFramework: TemplateFramework,
templateEngine: TemplateEngine,
dataSource: string,
templateUI: TemplateUI,
vectorDb: TemplateVectorDB,
appType: AppType,
@@ -75,10 +74,13 @@ 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 name = [
templateType,
templateFramework,
templateEngine,
dataSource,
templateUI,
appType,
].join("-");
@@ -89,8 +91,7 @@ export async function runCreateLlama(
templateType,
"--framework",
templateFramework,
"--engine",
templateEngine,
dataSource,
"--ui",
templateUI,
"--vector-db",
@@ -100,9 +101,8 @@ export async function runCreateLlama(
"--embedding-model",
EMBEDDING_MODEL,
"--open-ai-key",
process.env.OPENAI_API_KEY || "testKey",
process.env.OPENAI_API_KEY,
appType,
"--eslint",
"--use-pnpm",
"--port",
port,
+30
View File
@@ -0,0 +1,30 @@
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;
}
+6 -45
View File
@@ -1,7 +1,6 @@
import fs from "fs/promises";
import path from "path";
import {
FileSourceConfig,
TemplateDataSource,
TemplateFramework,
TemplateVectorDB,
@@ -99,44 +98,6 @@ const getVectorDBEnvs = (vectorDb: TemplateVectorDB) => {
}
};
const getDataSourceEnvs = (
dataSource: TemplateDataSource,
llamaCloudKey?: string,
) => {
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.",
},
];
case "file":
case "folder":
return [
...((dataSource?.config as FileSourceConfig).useLlamaParse
? [
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: llamaCloudKey,
},
]
: []),
];
default:
return [];
}
};
export const createBackendEnvFile = async (
root: string,
opts: {
@@ -146,7 +107,7 @@ export const createBackendEnvFile = async (
model?: string;
embeddingModel?: string;
framework?: TemplateFramework;
dataSource?: TemplateDataSource;
dataSources?: TemplateDataSource[];
port?: number;
},
) => {
@@ -165,13 +126,13 @@ 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, opts.llamaCloudKey)
: []),
];
let envVars: EnvVar[] = [];
if (opts.framework === "fastapi") {
+20 -41
View File
@@ -1,11 +1,9 @@
import { copy } from "./copy";
import { callPackageManager } from "./install";
import fs from "fs/promises";
import path from "path";
import { cyan } from "picocolors";
import { templatesDir } from "./dir";
import fsExtra from "fs-extra";
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
import { PackageManager } from "./get-pkg-manager";
import { installLlamapackProject } from "./llama-pack";
@@ -27,8 +25,8 @@ async function generateContextData(
packageManager?: PackageManager,
openAiKey?: string,
vectorDb?: TemplateVectorDB,
dataSource?: TemplateDataSource,
llamaCloudKey?: string,
useLlamaParse?: boolean,
) {
if (packageManager) {
const runGenerate = `${cyan(
@@ -37,8 +35,7 @@ async function generateContextData(
: `${packageManager} run generate`,
)}`;
const openAiKeyConfigured = openAiKey || process.env["OPENAI_API_KEY"];
const llamaCloudKeyConfigured = (dataSource?.config as FileSourceConfig)
?.useLlamaParse
const llamaCloudKeyConfigured = useLlamaParse
? llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = vectorDb && vectorDb !== "none";
@@ -76,38 +73,16 @@ async function generateContextData(
const copyContextData = async (
root: string,
dataSource?: TemplateDataSource,
dataSources: TemplateDataSource[],
) => {
const destPath = path.join(root, "data");
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// Copy local data
const dataPath = dataSourceConfig.path;
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;
const destPath = path.join(root, "data", path.basename(dataPath));
console.log("Copying data from path:", dataPath);
await fsExtra.copy(dataPath, destPath);
}
};
@@ -157,12 +132,16 @@ export const installTemplate = async (
model: props.model,
embeddingModel: props.embeddingModel,
framework: props.framework,
dataSource: props.dataSource,
dataSources: props.dataSources,
port: props.externalPort,
});
if (props.engine === "context") {
await copyContextData(props.root, props.dataSource);
if (props.dataSources.length > 0) {
console.log("\nGenerating context data...\n");
await copyContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies"
@@ -172,14 +151,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
createFrontendEnvFile(props.root, {
await createFrontendEnvFile(props.root, {
model: props.model,
customApiPath: props.customApiPath,
});
+83 -31
View File
@@ -3,15 +3,16 @@ 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 {
@@ -54,13 +55,17 @@ 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" || dataSourceType === "folder") {
if (dataSourceType === "file") {
// llama-index-readers-file (pdf, excel, csv) is already included in llama_index package
dependencies.push({
name: "docx2txt",
@@ -173,20 +178,20 @@ export const installPythonTemplate = async ({
root,
template,
framework,
engine,
vectorDb,
dataSource,
dataSources,
tools,
postInstallAction,
useLlamaParse,
}: Pick<
InstallTemplateArgs,
| "root"
| "framework"
| "template"
| "engine"
| "vectorDb"
| "dataSource"
| "dataSources"
| "tools"
| "useLlamaParse"
| "postInstallAction"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
@@ -211,9 +216,10 @@ export const installPythonTemplate = async ({
},
});
if (engine === "context") {
const compPath = path.join(templatesDir, "components");
if (dataSources.length > 0) {
const enginePath = path.join(root, "app", "engine");
const compPath = path.join(templatesDir, "components");
const vectorDbDirName = vectorDb ?? "none";
const VectorDBPath = path.join(
@@ -233,16 +239,14 @@ export const installPythonTemplate = async ({
parents: true,
cwd: path.join(compPath, "engines", "python", "agent"),
});
// Write tools_config.json
// Write tool configs
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
configContent[tool.name] = tool.config ?? {};
});
const configFilePath = path.join(root, "tools_config.json");
await fs.writeFile(
configFilePath,
JSON.stringify(configContent, null, 2),
);
const configFilePath = path.join(root, "config/tools.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(configFilePath, yaml.stringify(configContent));
} else {
await copy("**", enginePath, {
parents: true,
@@ -250,30 +254,78 @@ export const installPythonTemplate = async ({
});
}
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),
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,
});
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 = getAdditionalDependencies(
vectorDb,
dataSource,
tools,
);
const addOnDependencies = dataSources
.map((ds) => getAdditionalDependencies(vectorDb, ds, tools))
.flat();
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,10 +1,12 @@
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;
@@ -27,6 +29,7 @@ export const supportedTools: Tool[] = [
version: "0.1.2",
},
],
supportedFrameworks: ["fastapi"],
},
{
display: "Wikipedia",
@@ -37,6 +40,7 @@ export const supportedTools: Tool[] = [
version: "0.1.2",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
},
];
+7 -8
View File
@@ -1,9 +1,8 @@
import { PackageManager } from "../helpers/get-pkg-manager";
import { Tool } from "./tools";
export type TemplateType = "simple" | "streaming" | "community" | "llamapack";
export type TemplateType = "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 =
@@ -15,17 +14,18 @@ export type TemplateDataSource = {
type: TemplateDataSourceType;
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "none" | "file" | "folder" | "web";
export type TemplateDataSourceType = "file" | "web";
export type TemplateObservability = "none" | "opentelemetry";
// Config for both file and folder
export type FileSourceConfig = {
path?: string;
useLlamaParse?: boolean;
path: string;
};
export type WebSourceConfig = {
baseUrl?: string;
prefix?: string;
depth?: number;
};
export type TemplateDataSourceConfig = FileSourceConfig | WebSourceConfig;
export type CommunityProjectConfig = {
@@ -42,13 +42,12 @@ export interface InstallTemplateArgs {
isOnline: boolean;
template: TemplateType;
framework: TemplateFramework;
engine: TemplateEngine;
ui: TemplateUI;
dataSource?: TemplateDataSource;
eslint: boolean;
dataSources: TemplateDataSource[];
customApiPath?: string;
openAiKey?: string;
llamaCloudKey?: string;
useLlamaParse?: boolean;
model: string;
embeddingModel: string;
communityProjectConfig?: CommunityProjectConfig;
+63 -53
View File
@@ -6,7 +6,8 @@ import { copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { FileSourceConfig, InstallTemplateArgs } from "./types";
import { makeDir } from "./make-dir";
import { InstallTemplateArgs } from "./types";
const rename = (name: string) => {
switch (name) {
@@ -56,15 +57,15 @@ export const installTSTemplate = async ({
isOnline,
template,
framework,
engine,
ui,
eslint,
customApiPath,
vectorDb,
postInstallAction,
backend,
observability,
dataSource,
tools,
dataSources,
useLlamaParse,
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
@@ -74,7 +75,6 @@ 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,
@@ -140,50 +140,63 @@ 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");
if (engine && (framework === "express" || framework === "nextjs")) {
console.log("\nUsing chat engine:", engine, "\n");
const relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
const enginePath = path.join(root, relativeEngineDestPath, "engine");
let vectorDBFolder: string = engine;
if (engine !== "simple" && vectorDb) {
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
console.log("\nUsing vector DB:", vectorDb, "\n");
vectorDBFolder = vectorDb;
}
relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
const enginePath = path.join(root, relativeEngineDestPath, "engine");
// copy vector db component
const vectorDBPath = path.join(
compPath,
"vectordbs",
"typescript",
vectorDBFolder,
);
await copy("**", enginePath, {
parents: true,
cwd: vectorDBPath,
});
// copy loader component
const dataSourceType = dataSource?.type;
if (dataSourceType && 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;
}
const vectorDBPath = path.join(
compPath,
"vectordbs",
"typescript",
vectorDb,
);
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
cwd: vectorDBPath,
});
}
// copy loader component (TS only supports llama_parse and file for now)
let loaderFolder: string;
loaderFolder = useLlamaParse ? "llama_parse" : "file";
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
});
if (tools?.length) {
// use agent chat engine if user selects tools
console.log("\nUsing agent chat engine\n");
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "typescript", "agent"),
});
// Write 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"),
});
}
}
@@ -227,7 +240,7 @@ export const installTSTemplate = async ({
// modify the dev script to use the custom api path
}
if (engine === "context" && relativeEngineDestPath) {
if (dataSources.length > 0 && relativeEngineDestPath) {
// add generate script if using context engine
packageJson.scripts = {
...packageJson.scripts,
@@ -274,14 +287,6 @@ 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,
@@ -290,4 +295,9 @@ 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"),
});
};
+17 -23
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env node
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import Commander from "commander";
@@ -10,6 +9,7 @@ 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,13 +32,6 @@ const program = new Commander.Command(packageJson.name)
.action((name) => {
projectPath = name;
})
.option(
"--eslint",
`
Initialize with eslint config.
`,
)
.option(
"--use-npm",
`
@@ -72,13 +65,6 @@ 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(
@@ -93,6 +79,13 @@ 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(
@@ -165,7 +158,7 @@ const program = new Commander.Command(packageJson.name)
`,
)
.option(
"--llama-parse",
"--use-llama-parse",
`
Enable LlamaParse.
`,
@@ -189,9 +182,6 @@ 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 = [];
@@ -200,7 +190,12 @@ if (process.argv.includes("--tools")) {
}
}
if (process.argv.includes("--no-llama-parse")) {
program.llamaParse = false;
program.useLlamaParse = false;
}
if (process.argv.includes("--no-files")) {
program.dataSources = [];
} else {
program.dataSources = getDataSources(program.files, program.exampleFile);
}
const packageManager = !!program.useNpm
@@ -288,11 +283,9 @@ 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,
@@ -303,8 +296,9 @@ async function run(): Promise<void> {
vectorDb: program.vectorDb,
externalPort: program.externalPort,
postInstallAction: program.postInstallAction,
dataSource: program.dataSource,
dataSources: program.dataSources,
tools: program.tools,
useLlamaParse: program.useLlamaParse,
observability: program.observability,
});
conf.set("preferences", preferences);
+11 -5
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.0.30",
"version": "0.0.31",
"keywords": [
"rag",
"llamaindex",
@@ -24,13 +24,16 @@
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"dev": "ncc build ./index.ts -w -o dist/",
"build": "npm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"build": "bash ./scripts/build.sh",
"build:ncc": "pnpm 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",
"install-local": "pnpm link --global",
"release": "pnpm run build && changeset publish",
"new-version": "pnpm run build && changeset version"
"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"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
@@ -41,6 +44,7 @@
"@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",
@@ -66,7 +70,9 @@
"prettier-plugin-organize-imports": "^3.2.4",
"typescript": "^5.3.3",
"eslint-config-prettier": "^8.10.0",
"ora": "^8.0.1"
"ora": "^8.0.1",
"fs-extra": "11.2.0",
"yaml": "2.4.1"
},
"engines": {
"node": ">=16.14.0"
+50
View File
@@ -20,6 +20,9 @@ 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
@@ -62,6 +65,9 @@ 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
@@ -107,6 +113,9 @@ devDependencies:
wait-port:
specifier: ^1.1.0
version: 1.1.0
yaml:
specifier: 2.4.1
version: 2.4.1
packages:
@@ -489,10 +498,23 @@ 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:
@@ -1437,6 +1459,15 @@ 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'}
@@ -1982,6 +2013,14 @@ 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:
@@ -3185,6 +3224,11 @@ 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:
@@ -3325,6 +3369,12 @@ 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'}
+161 -244
View File
@@ -8,11 +8,12 @@ import { blue, green, red } from "picocolors";
import prompts from "prompts";
import { InstallAppArgs } from "./create-app";
import {
FileSourceConfig,
TemplateDataSource,
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";
@@ -24,8 +25,6 @@ export type QuestionArgs = Omit<
InstallAppArgs,
"appPath" | "packageManager"
> & {
files?: string;
llamaParse?: boolean;
listServerModels?: boolean;
};
const supportedContextFileTypes = [
@@ -40,21 +39,22 @@ const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select a file to process:" }).toString()
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select a folder to process:" }).toString()
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.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.FileName
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
@@ -70,21 +70,17 @@ if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
const defaults: QuestionArgs = {
template: "streaming",
framework: "nextjs",
engine: "simple",
ui: "html",
eslint: true,
ui: "shadcn",
frontend: false,
openAiKey: "",
llamaCloudKey: "",
useLlamaParse: false,
model: "gpt-3.5-turbo",
embeddingModel: "text-embedding-ada-002",
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSource: {
type: "none",
config: {},
},
dataSources: [],
tools: [],
};
@@ -122,24 +118,42 @@ const getVectorDbChoices = (framework: TemplateFramework) => {
return displayedChoices;
};
const getDataSourceChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No data, just a simple chat",
value: "simple",
},
{ title: "Use an example PDF", value: "exampleFile" },
];
if (process.platform === "win32" || process.platform === "darwin") {
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
) => {
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: `Use a local file (${supportedContextFileTypes.join(", ")})`,
value: "localFile",
});
choices.push({
title: `Use a local folder`,
value: "localFolder",
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No data, just a simple chat",
value: "none",
});
choices.push({
title: "Use an example PDF",
value: "exampleFile",
});
}
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)",
@@ -173,9 +187,16 @@ const selectLocalContextData = async (type: TemplateDataSourceType) => {
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
if (type === "file") {
const fileType = path.extname(selectedPath);
if (!supportedContextFileTypes.includes(fileType)) {
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))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
@@ -184,7 +205,7 @@ const selectLocalContextData = async (type: TemplateDataSourceType) => {
process.exit(1);
}
}
return selectedPath;
return paths;
} catch (error) {
console.log(
red(
@@ -309,9 +330,7 @@ export const askQuestions = async (
const openAiKeyConfigured =
program.openAiKey || process.env["OPENAI_API_KEY"];
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = (
program.dataSource?.config as FileSourceConfig
)?.useLlamaParse
const llamaCloudKeyConfigured = program.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
@@ -359,8 +378,7 @@ export const askQuestions = async (
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Chat without streaming", value: "simple" },
{ title: "Chat with streaming", value: "streaming" },
{ title: "Chat", value: "streaming" },
{
title: `Community template from ${styledRepo}`,
value: "community",
@@ -370,7 +388,7 @@ export const askQuestions = async (
value: "llamapack",
},
],
initial: 1,
initial: 0,
},
handlers,
);
@@ -429,13 +447,10 @@ 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(
{
@@ -452,10 +467,7 @@ export const askQuestions = async (
}
}
if (
program.template === "streaming" &&
(program.framework === "express" || program.framework === "fastapi")
) {
if (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) {
@@ -489,25 +501,7 @@ export const askQuestions = async (
if (program.framework === "nextjs" || program.frontend) {
if (!program.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;
}
program.ui = getPrefOrDefault("ui");
}
}
@@ -607,130 +601,110 @@ export const askQuestions = async (
}
}
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.dataSource = {
type: fs.lstatSync(program.files).isDirectory() ? "folder" : "file",
config: {
path: program.files,
},
};
}
}
if (!program.engine) {
if (!program.dataSources) {
if (ciInfo.isCI) {
program.engine = getPrefOrDefault("engine");
program.dataSources = getPrefOrDefault("dataSources");
} else {
const { dataSource } = await prompts(
{
type: "select",
name: "dataSource",
message: "Which data source would you like to use?",
choices: getDataSourceChoices(program.framework),
initial: 1,
},
handlers,
);
// 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"
) {
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(
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: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: true,
active: "yes",
inactive: "no",
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,
);
dataSourceConfig.useLlamaParse = useLlamaParse;
program.dataSource.config = dataSourceConfig;
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,
},
});
}
}
}
}
// Asking for LlamaParse if user selected file or folder data source
if (
program.dataSources.some((ds) => ds.type === "file") &&
program.useLlamaParse === undefined
) {
if (ciInfo.isCI) {
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
} else {
const { useLlamaParse } = 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",
},
handlers,
);
program.useLlamaParse = useLlamaParse;
// Ask for LlamaCloud API key
if (
dataSourceConfig.useLlamaParse &&
program.llamaCloudKey === undefined
) {
if (useLlamaParse && program.llamaCloudKey === undefined) {
const { llamaCloudKey } = await prompts(
{
type: "text",
@@ -745,39 +719,7 @@ export const askQuestions = async (
}
}
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 (program.dataSources.length > 0 && !program.vectorDb) {
if (ciInfo.isCI) {
program.vectorDb = getPrefOrDefault("vectorDb");
} else {
@@ -796,15 +738,15 @@ export const askQuestions = async (
}
}
if (
!program.tools &&
program.framework === "fastapi" &&
program.engine === "context"
) {
// TODO: allow tools also without datasources
if (!program.tools && program.dataSources.length > 0) {
if (ciInfo.isCI) {
program.tools = getPrefOrDefault("tools");
} else {
const toolChoices = supportedTools.map((tool) => ({
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
title: tool.display,
value: tool.name,
}));
@@ -823,30 +765,5 @@ 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
@@ -0,0 +1,12 @@
#!/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
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
pnpm pack && npm install -g $(pwd)/$(ls ./*.tgz | head -1)
@@ -0,0 +1,26 @@
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"]
@@ -0,0 +1,16 @@
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 json
import yaml
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("tools_config.json", "r") as f:
tool_configs = json.load(f)
with open("config/tools.yaml", "r") as f:
tool_configs = yaml.safe_load(f)
for name, config in tool_configs.items():
tools += ToolFactory.create_tool(name, **config)
return tools
@@ -0,0 +1,26 @@
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;
}
@@ -0,0 +1,13 @@
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,
});
}
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,37 @@
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()
@@ -1,7 +0,0 @@
from llama_index.core.readers import SimpleDirectoryReader
DATA_DIR = "data" # directory containing the documents
def get_documents():
return SimpleDirectoryReader(DATA_DIR).load_data()
@@ -1,17 +0,0 @@
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()
@@ -0,0 +1,36 @@
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
@@ -1,13 +0,0 @@
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
@@ -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">{message.content}</p>
<p className="break-words whitespace-pre-wrap">{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.loader import get_documents
from app.engine.loaders import get_documents
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
@@ -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.loader import get_documents
from app.engine.loaders 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.loader import get_documents
from app.engine.loaders 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.loader import get_documents
from app.engine.loaders 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.loader import get_documents
from app.engine.loaders import get_documents
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
@@ -1,5 +1,4 @@
import {
ContextChatEngine,
LLM,
serviceContextFromDefaults,
SimpleDocumentStore,
@@ -8,7 +7,7 @@ import {
} from "llamaindex";
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
async function getDataSource(llm: LLM) {
export async function getDataSource(llm: LLM) {
const serviceContext = serviceContextFromDefaults({
llm,
chunkSize: CHUNK_SIZE,
@@ -31,14 +30,3 @@ 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,50 +0,0 @@
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!
@@ -1,3 +0,0 @@
{
"extends": "eslint:recommended"
}
-3
View File
@@ -1,3 +0,0 @@
# local env files
.env
node_modules/
-44
View File
@@ -1,44 +0,0 @@
/* 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}`);
});
@@ -1,27 +0,0 @@
{
"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": "latest"
},
"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 +0,0 @@
export const initObservability = () => {};
@@ -1,8 +0,0 @@
import express from "express";
import { chat } from "../controllers/chat.controller";
const llmRouter = express.Router();
llmRouter.route("/").post(chat);
export default llmRouter;
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node"
}
}
@@ -1,58 +0,0 @@
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!
@@ -1,54 +0,0 @@
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)
)
@@ -1,5 +0,0 @@
from llama_index.core.chat_engine import SimpleChatEngine
def get_chat_engine():
return SimpleChatEngine.from_defaults()
@@ -1,41 +0,0 @@
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
@@ -1,3 +0,0 @@
__pycache__
storage
.env
-39
View File
@@ -1,39 +0,0 @@
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)
@@ -1,17 +0,0 @@
[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,21 +8,41 @@ First, install the dependencies:
npm install
```
Second, run the development server:
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:
```
npm run dev
```
Then call the express API endpoint `/api/chat` to see the result:
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:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: text/plain' \
--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.
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.
## Production
@@ -40,6 +60,39 @@ 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,3 +1,7 @@
{
"extends": "eslint:recommended"
"extends": ["eslint:recommended", "prettier"],
"rules": {
"max-params": ["error", 4],
"prefer-const": "error"
}
}
@@ -1,12 +1,13 @@
{
"name": "llama-index-express-streaming",
"version": "1.0.0",
"main": "dist/index.js",
"type": "module",
"main": "dist/index.mjs",
"scripts": {
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"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\""
"start": "node dist/index.mjs",
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.mjs\""
},
"dependencies": {
"ai": "^2.2.25",
@@ -23,6 +24,9 @@
"eslint": "^8.54.0",
"nodemon": "^3.0.1",
"tsup": "^8.0.1",
"typescript": "^5.3.2"
"typescript": "^5.3.2",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"eslint-config-prettier": "^8.10.0"
}
}
@@ -0,0 +1,3 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
@@ -1,6 +1,6 @@
import { Request, Response } from "express";
import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
import { createChatEngine } from "./engine";
import { createChatEngine } from "./engine/chat";
const convertMessageContent = (
textMessage: string,
@@ -21,7 +21,7 @@ const convertMessageContent = (
];
};
export const chat = async (req: Request, res: Response) => {
export const chatRequest = 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 chat = async (req: Request, res: Response) => {
// Calling LlamaIndex's ChatEngine to get a response
const response = await chatEngine.chat({
message: userMessageContent,
messages,
chatHistory: messages,
});
const result: ChatMessage = {
role: "assistant",
@@ -1,7 +1,7 @@
import { streamToResponse } from "ai";
import { Request, Response } from "express";
import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
import { createChatEngine } from "./engine";
import { createChatEngine } from "./engine/chat";
import { LlamaIndexStream } from "./llamaindex-stream";
const convertMessageContent = (
@@ -6,7 +6,7 @@ import {
trimStartOfStreamHelper,
type AIStreamCallbacksAndOptions,
} from "ai";
import { Response } from "llamaindex";
import { Response, StreamingAgentChatResponse } from "llamaindex";
type ParserOptions = {
image_url?: string;
@@ -52,13 +52,17 @@ function createParser(
}
export function LlamaIndexStream(
res: AsyncIterable<Response>,
response: StreamingAgentChatResponse | 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,8 +1,10 @@
import express from "express";
import express, { Router } from "express";
import { chatRequest } from "../controllers/chat-request.controller";
import { chat } from "../controllers/chat.controller";
const llmRouter = express.Router();
const llmRouter: Router = express.Router();
llmRouter.route("/").post(chat);
llmRouter.route("/request").post(chatRequest);
export default llmRouter;
@@ -5,6 +5,9 @@
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node"
"moduleResolution": "node",
"paths": {
"@/*": ["./*"]
}
}
}
@@ -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,6 +19,8 @@ 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):
```
@@ -31,7 +33,12 @@ Third, run the development server:
python main.py
```
Then call the API endpoint `/api/chat` to see the result:
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:
```
curl --location 'localhost:8000/api/chat' \
@@ -39,7 +46,15 @@ curl --location 'localhost:8000/api/chat' \
--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.
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.
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
@@ -49,6 +64,40 @@ 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,6 +5,7 @@ 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()
@@ -18,20 +19,19 @@ class _ChatData(BaseModel):
messages: List[_Message]
@r.post("")
async def chat(
request: Request,
data: _ChatData,
chat_engine: BaseChatEngine = Depends(get_chat_engine),
):
class _Result(BaseModel):
result: _Message
async def parse_chat_data(data: _ChatData) -> Tuple[str, List[ChatMessage]]:
# 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:
last_message = data.messages.pop()
if last_message.role != MessageRole.USER:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Last message must be from user",
@@ -44,16 +44,38 @@ async def chat(
)
for m in data.messages
]
return last_message.content, messages
# query chat engine
response = await chat_engine.astream_chat(lastMessage.content, messages)
# stream response
# 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)
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,7 +10,9 @@ 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"
llama-index = "0.10.15"
llama-index-core = "0.10.15"
llama-index-agent-openai = "0.1.5"
[build-system]
requires = ["poetry-core"]
@@ -8,7 +8,13 @@ First, install the dependencies:
npm install
```
Second, run the development server:
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:
```
npm run dev
@@ -20,6 +26,41 @@ 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:
@@ -1,7 +0,0 @@
import { LLM, SimpleChatEngine } from "llamaindex";
export async function createChatEngine(llm: LLM) {
return new SimpleChatEngine({
llm,
});
}
@@ -6,7 +6,7 @@ import {
trimStartOfStreamHelper,
type AIStreamCallbacksAndOptions,
} from "ai";
import { Response } from "llamaindex";
import { Response, StreamingAgentChatResponse } from "llamaindex";
type ParserOptions = {
image_url?: string;
@@ -52,13 +52,17 @@ function createParser(
}
export function LlamaIndexStream(
res: AsyncIterable<Response>,
response: StreamingAgentChatResponse | 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))
@@ -2,7 +2,7 @@ import { initObservability } from "@/app/observability";
import { StreamingTextResponse } from "ai";
import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { createChatEngine } from "./engine";
import { createChatEngine } from "./engine/chat";
import { LlamaIndexStream } from "./llamaindex-stream";
initObservability();
@@ -15,7 +15,7 @@ const MemoizedReactMarkdown: FC<Options> = memo(
export default function Markdown({ content }: { content: string }) {
return (
<MemoizedReactMarkdown
className="prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words"
className="prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words custom-markdown"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import "./markdown.css";
const inter = Inter({ subsets: ["latin"] });
@@ -0,0 +1,23 @@
/* Custom CSS for chat message markdown */
.custom-markdown ul {
list-style-type: disc;
margin-left: 20px;
}
.custom-markdown ol {
list-style-type: decimal;
margin-left: 20px;
}
.custom-markdown li {
margin-bottom: 5px;
}
.custom-markdown ol ol {
list-style: lower-alpha;
}
.custom-markdown ul ul,
.custom-markdown ol ol {
margin-left: 20px;
}
@@ -1,3 +1,7 @@
{
"extends": "next/core-web-vitals"
"extends": ["next/core-web-vitals", "prettier"],
"rules": {
"max-params": ["error", 4],
"prefer-const": "error"
}
}
@@ -2,6 +2,8 @@
"name": "llama-index-nextjs-streaming",
"version": "1.0.0",
"scripts": {
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"dev": "next dev",
"build": "next build",
"start": "next start",
@@ -38,6 +40,9 @@
"tailwindcss": "^3.3.6",
"typescript": "^5.3.2",
"@types/react-syntax-highlighter": "^15.5.11",
"cross-env": "^7.0.3"
"cross-env": "^7.0.3",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"eslint-config-prettier": "^8.10.0"
}
}
@@ -0,0 +1,3 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};