Compare commits

..

2 Commits

Author SHA1 Message Date
leehuwuj 670e2631f3 bump @llamaindex/server 2025-04-02 17:20:32 +07:00
leehuwuj afb519d231 fix llamacloud api and markdown issue 2025-04-02 16:51:21 +07:00
60 changed files with 297 additions and 3771 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
fix: add trycatch for generating error
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
bump: chat-ui and tailwind v4
+1 -3
View File
@@ -67,8 +67,6 @@ jobs:
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
PYTHONIOENCODING: utf-8
PYTHONLEGACYWINDOWSSTDIO: utf-8
working-directory: .
- uses: actions/upload-artifact@v4
@@ -88,7 +86,7 @@ jobs:
node-version: [18, 20]
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs"]
frameworks: ["nextjs", "express"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
-23
View File
@@ -1,28 +1,5 @@
# create-llama
## 0.5.2
### Patch Changes
- c9f8f8d: Use custom component for deep research use case
## 0.5.1
### Patch Changes
- 08b3e07: Simplify the local index code.
## 0.5.0
### Minor Changes
- 54c9e2f: Simplified generated code using LlamaIndexServer
### Patch Changes
- 0e4ecfa: fix: add trycatch for generating error
- ee69ce7: bump: chat-ui and tailwind v4
## 0.4.0
### Minor Changes
+2 -2
View File
@@ -90,7 +90,7 @@ export async function createApp({
// Install backend
await installTemplate({ ...args, backend: true });
if (frontend && framework === "fastapi" && template !== "llamaindexserver") {
if (frontend && framework === "fastapi") {
// install frontend
const frontendRoot = path.join(root, ".frontend");
await makeDir(frontendRoot);
@@ -110,7 +110,7 @@ export async function createApp({
console.log();
}
if (toolsRequireConfig(tools) && template !== "llamaindexserver") {
if (toolsRequireConfig(tools)) {
const configFile =
framework === "fastapi" ? "config/tools.yaml" : "config/tools.json";
console.log(
@@ -16,17 +16,15 @@ const templateFramework: TemplateFramework = process.env.FRAMEWORK
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = "--frontend";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage = "Write a blog post about physical standards for letters";
const templateUseCases = ["financial_report", "agentic_rag", "deep_research"];
const templateUseCases = ["financial_report", "blog", "form_filling"];
for (const useCase of templateUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.describe(`Test multiagent template ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" ||
process.env.DATASOURCE === "--no-files" ||
templateFramework === "express",
"The llamaindexserver template currently only works with nextjs, fastapi. We also only run on Linux to speed up tests.",
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
);
let port: number;
let cwd: string;
@@ -40,7 +38,7 @@ for (const useCase of templateUseCases) {
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "llamaindexserver",
templateType: "multiagent",
templateFramework,
dataSource,
vectorDb,
@@ -74,9 +72,9 @@ for (const useCase of templateUseCases) {
test.skip(
templatePostInstallAction !== "runApp" ||
useCase === "financial_report" ||
useCase === "deep_research" ||
useCase === "form_filling" ||
templateFramework === "express",
"Skip chat tests for financial report and deep research.",
"Skip chat tests for financial report and form filling.",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
@@ -88,12 +86,6 @@ for (const useCase of templateUseCases) {
await page.click("form button[type=submit]");
const response = await responsePromise;
console.log(`Response status: ${response.status()}`);
const responseBody = await response
.text()
.catch((e) => `Error reading body: ${e}`);
console.log(`Response body: ${responseBody}`);
expect(response.ok()).toBeTruthy();
});
+1 -6
View File
@@ -113,12 +113,7 @@ export async function runCreateLlama({
if (observability) {
commandArgs.push("--observability", observability);
}
if (
(templateType === "multiagent" ||
templateType === "reflex" ||
templateType === "llamaindexserver") &&
useCase
) {
if ((templateType === "multiagent" || templateType === "reflex") && useCase) {
commandArgs.push("--use-case", useCase);
}
+36 -59
View File
@@ -44,7 +44,6 @@ const renderEnvVar = (envVars: EnvVar[]): string => {
const getVectorDBEnvs = (
vectorDb?: TemplateVectorDB,
framework?: TemplateFramework,
template?: TemplateType,
): EnvVar[] => {
if (!vectorDb || !framework) {
return [];
@@ -169,7 +168,7 @@ const getVectorDBEnvs = (
description:
"The organization ID for the LlamaCloud project (uses default organization if not specified)",
},
...(framework === "nextjs" && template !== "llamaindexserver"
...(framework === "nextjs"
? // activate index selector per default (not needed for non-NextJS backends as it's handled by createFrontendEnvFile)
[
{
@@ -224,15 +223,13 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
},
];
default:
return template !== "llamaindexserver"
? [
{
name: "STORAGE_CACHE_DIR",
description: "The directory to store the local storage cache.",
value: ".cache",
},
]
: [];
return [
{
name: "STORAGE_CACHE_DIR",
description: "The directory to store the local storage cache.",
value: ".cache",
},
];
}
};
@@ -385,42 +382,38 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
const getFrameworkEnvs = (
framework: TemplateFramework,
template: TemplateType,
port?: number,
): EnvVar[] => {
const sPort = port?.toString() || "8000";
const result: EnvVar[] =
template !== "llamaindexserver"
? [
{
name: "FILESERVER_URL_PREFIX",
description:
"FILESERVER_URL_PREFIX is the URL prefix of the server storing the images generated by the interpreter.",
value:
framework === "nextjs"
? // FIXME: if we are using nextjs, port should be 3000
"http://localhost:3000/api/files"
: `http://localhost:${sPort}/api/files`,
},
]
: [];
const result: EnvVar[] = [
{
name: "FILESERVER_URL_PREFIX",
description:
"FILESERVER_URL_PREFIX is the URL prefix of the server storing the images generated by the interpreter.",
value:
framework === "nextjs"
? // FIXME: if we are using nextjs, port should be 3000
"http://localhost:3000/api/files"
: `http://localhost:${sPort}/api/files`,
},
];
if (framework === "fastapi") {
result.push(
...[
{
name: "APP_HOST",
description: "The address to start the FastAPI app.",
description: "The address to start the backend app.",
value: "0.0.0.0",
},
{
name: "APP_PORT",
description: "The port to start the FastAPI app.",
description: "The port to start the backend app.",
value: sPort,
},
],
);
}
if (framework === "nextjs" && template !== "llamaindexserver") {
if (framework === "nextjs") {
result.push({
name: "NEXT_PUBLIC_CHAT_API",
description:
@@ -576,41 +569,25 @@ export const createBackendEnvFile = async (
| "port"
| "tools"
| "observability"
| "useLlamaParse"
>,
) => {
// Init env values
const envFileName = ".env";
const envVars: EnvVar[] = [
...(opts.useLlamaParse
? [
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
]
: []),
...getVectorDBEnvs(opts.vectorDb, opts.framework, opts.template),
...getToolEnvs(opts.tools),
...getFrameworkEnvs(opts.framework, opts.template, opts.port),
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
// Add environment variables of each component
...(opts.template === "llamaindexserver"
? [
{
name: "OPENAI_API_KEY",
description: "The OpenAI API key to use.",
value: opts.modelConfig.apiKey,
},
]
: [
// don't use this stuff for llama-indexserver
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
]),
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getVectorDBEnvs(opts.vectorDb, opts.framework),
...getFrameworkEnvs(opts.framework, opts.port),
...getToolEnvs(opts.tools),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
];
// Render and write env file
const content = renderEnvVar(envVars);
+17 -19
View File
@@ -1,7 +1,7 @@
import { callPackageManager } from "./install";
import path from "path";
import picocolors, { cyan } from "picocolors";
import { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { writeLoadersConfig } from "./datasources";
@@ -41,11 +41,7 @@ const checkForGenerateScript = (
missingSettings.push("your LLAMA_CLOUD_API_KEY");
}
if (
vectorDb !== undefined &&
vectorDb !== "none" &&
vectorDb !== "llamacloud"
) {
if (vectorDb !== "none" && vectorDb !== "llamacloud") {
missingSettings.push("your Vector DB environment variables");
}
@@ -96,7 +92,7 @@ async function generateContextData(
}
const settingsMessage = `After setting ${missingSettings.join(" and ")}, run ${runGenerate} to generate the context data.`;
console.log(picocolors.yellow(`\n${settingsMessage}\n\n`));
console.log(`\n${settingsMessage}\n\n`);
}
}
@@ -170,17 +166,6 @@ export const installTemplate = async (
if (props.framework === "fastapi") {
await installPythonTemplate(props);
} else {
await installTSTemplate(props);
}
// write configurations
if (props.template !== "llamaindexserver") {
await writeToolsConfig(
props.root,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
);
if (props.vectorDb !== "llamacloud") {
// write loaders configuration (currently Python only)
// not needed for LlamaCloud as it has its own loaders
@@ -190,13 +175,26 @@ export const installTemplate = async (
props.useLlamaParse,
);
}
} else {
await installTSTemplate(props);
}
// write tools configuration
await writeToolsConfig(
props.root,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
);
if (props.backend) {
// This is a backend, so we need to copy the test data and create the env file.
// Copy the environment file to the target directory.
if (props.template !== "community" && props.template !== "llamapack") {
if (
props.template === "streaming" ||
props.template === "multiagent" ||
props.template === "reflex"
) {
await createBackendEnvFile(props.root, props);
}
+52 -146
View File
@@ -12,7 +12,6 @@ import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateObservability,
TemplateType,
TemplateVectorDB,
} from "./types";
@@ -30,7 +29,6 @@ const getAdditionalDependencies = (
dataSources?: TemplateDataSource[],
tools?: Tool[],
templateType?: TemplateType,
observability?: TemplateObservability,
) => {
const dependencies: Dependency[] = [];
@@ -105,7 +103,7 @@ const getAdditionalDependencies = (
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "0.6.3",
version: "^0.6.3",
});
break;
}
@@ -270,21 +268,6 @@ const getAdditionalDependencies = (
break;
}
if (observability && observability !== "none") {
if (observability === "traceloop") {
dependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
}
if (observability === "llamatrace") {
dependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.3.0",
});
}
}
return dependencies;
};
@@ -396,24 +379,47 @@ export const installPythonDependencies = (
}
};
const installLegacyPythonTemplate = async ({
export const installPythonTemplate = async ({
appName,
root,
template,
framework,
vectorDb,
postInstallAction,
modelConfig,
dataSources,
tools,
useLlamaParse,
useCase,
observability,
}: Pick<
InstallTemplateArgs,
| "appName"
| "root"
| "template"
| "framework"
| "vectorDb"
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "tools"
| "useLlamaParse"
| "useCase"
| "observability"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "reflex") {
templatePath = path.join(templatesDir, "types", "reflex");
} else {
templatePath = path.join(templatesDir, "types", "streaming", framework);
}
await copy("**", root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
const compPath = path.join(templatesDir, "components");
const enginePath = path.join(root, "app", "engine");
@@ -503,7 +509,34 @@ const installLegacyPythonTemplate = async ({
}
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
modelConfig,
vectorDb,
dataSources,
tools,
template,
);
if (observability && observability !== "none") {
if (observability === "traceloop") {
addOnDependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
}
if (observability === "llamatrace") {
addOnDependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.3.0",
constraints: {
python: ">=3.11,<3.13",
},
});
}
const templateObservabilityPath = path.join(
templatesDir,
"components",
@@ -515,133 +548,6 @@ const installLegacyPythonTemplate = async ({
cwd: templateObservabilityPath,
});
}
};
const installLlamaIndexServerTemplate = async ({
root,
useCase,
useLlamaParse,
}: Pick<InstallTemplateArgs, "root" | "useCase" | "useLlamaParse">) => {
if (!useCase) {
console.log(
red(
`There is no use case selected. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
await copy("workflow.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
});
// Copy custom UI component code
await copy(`*`, path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
});
if (useLlamaParse) {
await copy("index.py", path.join(root, "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"python",
),
});
// TODO: Consider moving generate.py to app folder.
await copy("generate.py", path.join(root), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"python",
),
});
}
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
rename: assetRelocator,
});
};
export const installPythonTemplate = async ({
appName,
root,
template,
framework,
vectorDb,
postInstallAction,
modelConfig,
dataSources,
tools,
useLlamaParse,
useCase,
observability,
}: Pick<
InstallTemplateArgs,
| "appName"
| "root"
| "template"
| "framework"
| "vectorDb"
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "tools"
| "useLlamaParse"
| "useCase"
| "observability"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "reflex") {
templatePath = path.join(templatesDir, "types", "reflex");
} else {
templatePath = path.join(templatesDir, "types", template, framework);
}
await copy("**", root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
if (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
useLlamaParse,
});
} else {
await installLegacyPythonTemplate({
root,
template,
vectorDb,
dataSources,
tools,
useCase,
observability,
});
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
modelConfig,
vectorDb,
dataSources,
tools,
template,
);
await addDependencies(root, addOnDependencies);
+2 -2
View File
@@ -124,7 +124,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "e2b_code_interpreter",
version: "1.1.1",
version: "1.0.3",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
@@ -155,7 +155,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "e2b_code_interpreter",
version: "1.1.1",
version: "1.0.3",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
+2 -4
View File
@@ -24,8 +24,7 @@ export type TemplateType =
| "community"
| "llamapack"
| "multiagent"
| "reflex"
| "llamaindexserver";
| "reflex";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
@@ -56,8 +55,7 @@ export type TemplateUseCase =
| "deep_research"
| "form_filling"
| "extractor"
| "contract_review"
| "agentic_rag";
| "contract_review";
// Config for both file and folder
export type FileSourceConfig =
| {
+39 -169
View File
@@ -8,104 +8,42 @@ import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { InstallTemplateArgs, ModelProvider, TemplateVectorDB } from "./types";
const installLlamaIndexServerTemplate = async ({
root,
useCase,
vectorDb,
}: Pick<InstallTemplateArgs, "root" | "useCase" | "vectorDb">) => {
if (!useCase) {
console.log(
red(
`There is no use case selected. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
if (!vectorDb) {
console.log(
red(
`There is no vector db selected. Please pick a vector db to use via --vector-db flag.`,
),
);
process.exit(1);
}
await copy("workflow.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"workflows",
"typescript",
useCase,
),
});
// copy workflow UI components to output/components folder
await copy("*", path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
});
if (vectorDb === "llamacloud") {
await copy("generate.ts", path.join(root, "src"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"typescript",
),
});
await copy("index.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"typescript",
),
rename: () => "data.ts",
});
}
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"workflows",
"typescript",
useCase,
),
rename: assetRelocator,
});
};
const installLegacyTSTemplate = async ({
/**
* Install a LlamaIndex internal template to a given `root` directory.
*/
export const installTSTemplate = async ({
appName,
root,
packageManager,
isOnline,
template,
backend,
framework,
ui,
vectorDb,
postInstallAction,
backend,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
relativeEngineDestPath,
}: InstallTemplateArgs & {
backend: boolean;
relativeEngineDestPath: string;
}) => {
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
/**
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", "streaming", framework);
const copySource = ["**"];
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
/**
* If next.js is used, update its configuration if necessary
*/
@@ -160,6 +98,10 @@ const installLegacyTSTemplate = async ({
}
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");
// copy llamaindex code for TS templates
@@ -294,75 +236,6 @@ const installLegacyTSTemplate = async ({
await fs.rm(path.join(root, "app", "api"), { recursive: true });
await fs.rm(path.join(root, "config"), { recursive: true, force: true });
}
};
/**
* Install a LlamaIndex internal template to a given `root` directory.
*/
export const installTSTemplate = async ({
appName,
root,
packageManager,
isOnline,
template,
framework,
ui,
vectorDb,
postInstallAction,
backend,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
/**
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", template, framework);
const copySource = ["**"];
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
const relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
if (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
vectorDb,
});
} else {
await installLegacyTSTemplate({
appName,
root,
packageManager,
isOnline,
template,
backend,
framework,
ui,
vectorDb,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
relativeEngineDestPath,
});
}
const packageJson = await updatePackageJson({
root,
@@ -375,7 +248,6 @@ export const installTSTemplate = async ({
vectorDb,
backend,
modelConfig,
template,
});
if (
@@ -390,27 +262,27 @@ const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/openai": "^0.1.52",
},
gemini: {
"@llamaindex/google": "^0.2.0",
"@llamaindex/google": "^0.0.7",
},
ollama: {
"@llamaindex/ollama": "^0.1.0",
"@llamaindex/ollama": "^0.0.40",
},
mistral: {
"@llamaindex/mistral": "^0.2.0",
"@llamaindex/mistral": "^0.0.5",
},
"azure-openai": {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/openai": "^0.1.52",
},
groq: {
"@llamaindex/groq": "^0.0.61",
"@llamaindex/huggingface": "^0.1.0", // groq uses huggingface as default embedding model
"@llamaindex/groq": "^0.0.51",
"@llamaindex/huggingface": "^0.0.36", // groq uses huggingface as default embedding model
},
anthropic: {
"@llamaindex/anthropic": "^0.3.0",
"@llamaindex/huggingface": "^0.1.0", // anthropic uses huggingface as default embedding model
"@llamaindex/anthropic": "^0.1.0",
"@llamaindex/huggingface": "^0.0.36", // anthropic uses huggingface as default embedding model
},
};
@@ -459,7 +331,6 @@ async function updatePackageJson({
vectorDb,
backend,
modelConfig,
template,
}: Pick<
InstallTemplateArgs,
| "root"
@@ -470,7 +341,6 @@ async function updatePackageJson({
| "observability"
| "vectorDb"
| "modelConfig"
| "template"
> & {
relativeEngineDestPath: string;
backend: boolean;
@@ -482,7 +352,7 @@ async function updatePackageJson({
packageJson.name = appName;
packageJson.version = "0.1.0";
if (relativeEngineDestPath && template !== "llamaindexserver") {
if (relativeEngineDestPath) {
// TODO: move script to {root}/scripts for all frameworks
// add generate script if using context engine
packageJson.scripts = {
+1 -7
View File
@@ -6,7 +6,7 @@ LlamaIndexServer is a FastAPI-based application that allows you to quickly launc
- Serving a workflow as a chatbot
- Built on FastAPI for high performance and easy API development
- Optional built-in chat UI with extendable UI components
- Optional built-in chat UI
- Prebuilt development code
## Installation
@@ -75,7 +75,6 @@ The LlamaIndexServer accepts the following configuration parameters:
- `use_default_routers`: Whether to include default routers (chat, static file serving)
- `env`: Environment setting ('dev' enables CORS and UI by default)
- `include_ui`: Whether to include the chat UI
- `component_dir`: The directory for custom UI components rendering events emitted by the workflow. The default is None, which does not render custom UI components.
- `starter_questions`: List of starter questions for the chat UI
- `verbose`: Enable verbose logging
- `api_prefix`: API route prefix (default: "/api")
@@ -102,11 +101,6 @@ When enabled, the server provides a chat interface at the root path (`/`) with:
- Real-time chat interface
- API endpoint integration
### Custom UI Components
You can add custom UI components for your workflow by providing `component_dir` config and adding custom .jsx or .tsx files to the directory.
See [Custom UI Components](docs/custom_ui_component.md) for more details.
## Development Mode
In development mode (`env="dev"`), the server:
@@ -1,67 +0,0 @@
# Custom UI Components
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
## Overview
Custom UI components are a powerful feature that enables you to:
- Add custom interface elements to the chat UI using React JSX or TSX files
- Extend the default chat interface functionality
- Create specialized visualizations or interactions
## Configuration
### Workflow events
Your workflow must emit events that fit this structure, allowing the LlamaIndex server to display the right UI components based on the event type.
```json
{
"type": "<event_name>",
"data": <data model>
}
```
In Pydantic, this is equivalent to:
```python
from pydantic import BaseModel
from typing import Literal, Any
class MyCustomEvent(BaseModel):
type: Literal["<my_custom_event_name>"]
data: dict | Any
def to_response(self):
return self.model_dump()
```
### Server Setup
1. Initialize the LlamaIndex server with a component directory:
```python
server = LlamaIndexServer(
workflow_factory=your_workflow,
component_dir="path/to/components",
include_ui=True
)
```
2. Add the custom component code to the directory following the naming pattern:
- File Extension: `.jsx` and `.tsx` for React components
- File Name: Should match the event type from your workflow (e.g., `deep_research_event.jsx` for handling `deep_research_event` type that you defined in your workflow). If there are TSX and JSX files with the same name, the TSX file will be used.
- Component Name: Export a default React component named `Component` that receives props from the event data
Example component structure:
```jsx
function Component({ events }) {
// Your component logic here
return (
// Your UI code here
);
}
```
@@ -134,9 +134,3 @@ class SourceNodes(BaseModel):
cls, source_nodes: List[NodeWithScore]
) -> List["SourceNodes"]:
return [cls.from_source_node(node) for node in source_nodes]
class ComponentDefinition(BaseModel):
type: str
code: str
filename: str
@@ -1,4 +0,0 @@
from llama_index.server.api.routers.chat import chat_router
from llama_index.server.api.routers.ui import custom_components_router
__all__ = ["chat_router", "custom_components_router"]
@@ -1,20 +0,0 @@
import logging
from typing import List
from fastapi import APIRouter
from llama_index.server.api.models import ComponentDefinition
from llama_index.server.services.custom_ui import CustomUI
def custom_components_router(
component_dir: str,
logger: logging.Logger,
) -> APIRouter:
router = APIRouter(prefix="/components")
@router.get("")
async def components() -> List[ComponentDefinition]:
custom_ui = CustomUI(component_dir=component_dir, logger=logger)
return custom_ui.get_components()
return router
@@ -5,7 +5,7 @@ from typing import Optional
import requests
CHAT_UI_VERSION = "0.0.9"
CHAT_UI_VERSION = "0.0.6"
def download_chat_ui(
@@ -7,7 +7,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from llama_index.core.workflow import Workflow
from llama_index.server.api.routers import chat_router, custom_components_router
from llama_index.server.api.routers.chat import chat_router
from llama_index.server.chat_ui import download_chat_ui
from llama_index.server.settings import server_settings
@@ -18,7 +18,6 @@ class LlamaIndexServer(FastAPI):
starter_questions: Optional[list[str]]
verbose: bool = False
ui_path: str = ".ui"
component_dir: Optional[str] = None
def __init__(
self,
@@ -27,7 +26,6 @@ class LlamaIndexServer(FastAPI):
use_default_routers: Optional[bool] = True,
env: Optional[str] = None,
include_ui: Optional[bool] = None,
component_dir: Optional[str] = None,
starter_questions: Optional[list[str]] = None,
server_url: Optional[str] = None,
api_prefix: Optional[str] = None,
@@ -44,7 +42,6 @@ class LlamaIndexServer(FastAPI):
use_default_routers: Whether to use the default routers (chat, mount `data` and `output` directories).
env: The environment to run the server in.
include_ui: Whether to show an chat UI in the root path.
component_dir: The directory to custom UI components code.
starter_questions: A list of starter questions to display in the chat UI.
server_url: The URL of the server.
api_prefix: The prefix for the API endpoints.
@@ -58,8 +55,6 @@ class LlamaIndexServer(FastAPI):
self.include_ui = include_ui # Store the explicitly passed value first
self.starter_questions = starter_questions
self.use_default_routers = use_default_routers or True
if component_dir:
self.component_dir = component_dir
# Update the settings
if server_url:
@@ -74,7 +69,6 @@ class LlamaIndexServer(FastAPI):
self.allow_cors("*")
if self.include_ui is None:
self.include_ui = True
if self.include_ui is None:
self.include_ui = False
@@ -92,8 +86,6 @@ class LlamaIndexServer(FastAPI):
config["LLAMA_CLOUD_API"] = (
f"{server_settings.api_url}/chat/config/llamacloud"
)
if self.component_dir:
config["COMPONENTS_API"] = f"{server_settings.api_url}/components"
return config
# Default routers
@@ -114,28 +106,12 @@ class LlamaIndexServer(FastAPI):
prefix=server_settings.api_prefix,
)
def add_components_router(self) -> None:
"""
Add the UI router.
"""
if self.component_dir is None:
raise ValueError("component_dir must be specified to add components router")
self.include_router(
custom_components_router(self.component_dir, self.logger),
prefix=server_settings.api_prefix,
)
def mount_ui(self) -> None:
"""
Mount the UI.
"""
# Check if the static folder exists
if self.include_ui:
if self.component_dir:
if not os.path.exists(self.component_dir):
os.makedirs(self.component_dir)
self.add_components_router()
# Check if the static folder exists
if not os.path.exists(self.ui_path):
self.logger.warning(
f"UI files not found, downloading UI to {self.ui_path}"
@@ -1,81 +0,0 @@
import logging
import os
from typing import List, Optional
from llama_index.server.api.models import ComponentDefinition
class CustomUI:
def __init__(
self, component_dir: str, logger: Optional[logging.Logger] = None
) -> None:
self.component_dir = component_dir
self.logger = logger or logging.getLogger(__name__)
def get_components(self) -> List[ComponentDefinition]:
"""
List all js files in the component directory and return a list of ComponentDefinition objects.
Ignores files that fail to load and logs the error.
TSX files take precedence over JSX files when duplicate component names are found.
"""
components_dict: dict[str, ComponentDefinition] = {}
if not os.path.exists(self.component_dir):
self.logger.warning(
f"Component directory {self.component_dir} does not exist"
)
return []
try:
for file in os.listdir(self.component_dir):
if not file.endswith((".jsx", ".tsx")):
continue
component_name = file.split(".")[0]
file_path = os.path.join(self.component_dir, file)
file_ext = os.path.splitext(file)[1]
try:
with open(file_path, "r") as f:
code = f.read()
new_component = ComponentDefinition(
type=component_name,
code=code,
filename=file,
)
if component_name in components_dict:
existing_ext = os.path.splitext(
components_dict[component_name].filename
)[1]
# If existing is TSX and new is JSX, skip and warn
if existing_ext == ".tsx" and file_ext == ".jsx":
self.logger.warning(
f"Skipping duplicate JSX component {file} as TSX version already exists"
)
continue
# If both are same extension, warn and skip
if existing_ext == file_ext:
self.logger.warning(
f"Skipping duplicate component {file} with same extension"
)
continue
# If existing is JSX and new is TSX, replace and warn
if existing_ext == ".jsx" and file_ext == ".tsx":
self.logger.warning(
f"Replacing JSX component {components_dict[component_name].filename} with TSX version {file}"
)
components_dict[component_name] = new_component
continue
components_dict[component_name] = new_component
except Exception as e:
self.logger.error(f"Failed to load component {file}: {str(e)}")
continue
except Exception as e:
self.logger.error(f"Error reading component directory: {str(e)}")
return list(components_dict.values())
@@ -149,8 +149,7 @@ def _create_index(
"type": "OPENAI_EMBEDDING",
"component": {
"api_key": os.getenv("OPENAI_API_KEY"), # editable
"model_name": Settings.embed_model.model_name
or "text-embedding-3-small",
"model_name": os.getenv("EMBEDDING_MODEL"),
},
},
"transform_config": {
+4 -41
View File
@@ -376,28 +376,6 @@ files = [
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
[[package]]
name = "banks"
version = "2.1.1"
description = "A prompt programming language"
optional = false
python-versions = ">=3.9"
files = [
{file = "banks-2.1.1-py3-none-any.whl", hash = "sha256:06e4ee46a0ff2fcdf5f64a5f028a7b7ceb719d5c7b9339f5aa90b24936fbb7f5"},
{file = "banks-2.1.1.tar.gz", hash = "sha256:95ec9c8f3c173c9f1c21eb2451ba0e21dda87f1ceb738854fabadb54bc387b86"},
]
[package.dependencies]
deprecated = "*"
eval-type-backport = {version = "*", markers = "python_version < \"3.10\""}
griffe = "*"
jinja2 = "*"
platformdirs = "*"
pydantic = "*"
[package.extras]
all = ["litellm", "redis"]
[[package]]
name = "beautifulsoup4"
version = "4.13.3"
@@ -1480,20 +1458,6 @@ files = [
docs = ["Sphinx", "furo"]
test = ["objgraph", "psutil"]
[[package]]
name = "griffe"
version = "1.7.2"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
optional = false
python-versions = ">=3.9"
files = [
{file = "griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423"},
{file = "griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c"},
]
[package.dependencies]
colorama = ">=0.4"
[[package]]
name = "h11"
version = "0.14.0"
@@ -2252,18 +2216,17 @@ pydantic = ">=1.10"
[[package]]
name = "llama-index-core"
version = "0.12.28"
version = "0.12.25"
description = "Interface between LLMs and your data"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "llama_index_core-0.12.28-py3-none-any.whl", hash = "sha256:9bd24224bd57dd5e97bb0a2550f31f6c08b7dc396305f35bbe9714d17b1409a6"},
{file = "llama_index_core-0.12.28.tar.gz", hash = "sha256:bf4a9697525e1294855d2df5c3a56b9720c185cde0eae2da713e7629c456c643"},
{file = "llama_index_core-0.12.25-py3-none-any.whl", hash = "sha256:affd0b240daf3c1200ad8ca86696ce339f411c3028981b0b5b71b59d78f281ae"},
{file = "llama_index_core-0.12.25.tar.gz", hash = "sha256:e0145617f0ab5358c30a7a716fa46dd3c40183bad1b87dd0cbfd3d196fbee8c4"},
]
[package.dependencies]
aiohttp = ">=3.8.6,<4.0.0"
banks = ">=2.0.0,<3.0.0"
dataclasses-json = "*"
deprecated = ">=1.2.9.3"
dirtyjson = ">=1.0.8,<2.0.0"
@@ -6134,4 +6097,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<4.0"
content-hash = "61f7e3d0861287b293d3c5839cde36bfc032262f38e6314875ffaf456b77d300"
content-hash = "27ab493aba5c297223b335c422184f902abd56eaf35e8be2271d22a76d6e8b3a"
+2 -2
View File
@@ -26,7 +26,7 @@ license = "MIT"
name = "llama-index-server"
packages = [{include = "llama_index/"}]
readme = "README.md"
version = "0.1.9"
version = "0.1.6"
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
@@ -34,7 +34,7 @@ fastapi = {extras = ["standard"], version = "^0.115.11"}
cachetools = "^5.5.2"
requests = "^2.32.3"
pydantic-settings = "^2.8.1"
llama-index-core = "0.12.28"
llama-index-core = "0.12.25"
llama-index-readers-file = "^0.4.6"
llama-index-indices-managed-llama-cloud = "0.6.3"
@@ -1,5 +1,6 @@
import pytest
from httpx import ASGITransport, AsyncClient
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.llms import MockLLM
from llama_index.server import LlamaIndexServer
@@ -103,99 +104,3 @@ async def test_ui_is_accessible(server: LlamaIndexServer) -> None:
response = await ac.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
@pytest.mark.asyncio()
async def test_component_dir_creation(server: LlamaIndexServer) -> None:
"""
Test if the component directory is created when specified and doesn't exist.
"""
import os
import shutil
test_component_dir = "./test_components"
# Clean up any existing directory
if os.path.exists(test_component_dir):
shutil.rmtree(test_component_dir)
# Create server with component directory
_ = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
component_dir=test_component_dir,
include_ui=True,
)
# Verify directory was created
assert os.path.exists(test_component_dir), "Component directory was not created"
assert os.path.isdir(test_component_dir), "Component path is not a directory"
# Clean up after test
shutil.rmtree(test_component_dir)
@pytest.mark.asyncio()
async def test_component_router_addition(server: LlamaIndexServer, tmp_path) -> None:
"""
Test if the component router is added when component directory is specified.
"""
test_component_dir = tmp_path / "test_components"
# Create server with component directory
component_server = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
component_dir=str(test_component_dir),
include_ui=True,
)
# Verify component route exists
component_route_exists = any(
route.path == "/api/components" for route in component_server.routes
)
assert component_route_exists, "Component API route not found in server routes"
@pytest.mark.asyncio()
async def test_ui_config_includes_components_api(
server: LlamaIndexServer, tmp_path
) -> None:
"""
Test if the UI config includes components API when component directory is set.
"""
test_component_dir = tmp_path / "test_components"
# Create server with component directory
component_server = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
component_dir=str(test_component_dir),
include_ui=True,
)
# Check if components API is in UI config
ui_config = component_server._ui_config
assert "COMPONENTS_API" in ui_config, "Components API not found in UI config"
assert ui_config["COMPONENTS_API"].endswith("/components"), (
"Incorrect components API path"
)
@pytest.mark.asyncio()
async def test_component_router_requires_component_dir(
server: LlamaIndexServer,
) -> None:
"""
Test that adding components router without component_dir raises an error.
"""
server_without_component_dir = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
include_ui=True,
)
with pytest.raises(
ValueError, match="component_dir must be specified to add components router"
):
server_without_component_dir.add_components_router()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.2",
"version": "0.4.0",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
+1 -2
View File
@@ -16,6 +16,5 @@ export const askQuestions = async (
await askProQuestions(args);
return args as unknown as QuestionResults;
}
const results = await askSimpleQuestions(args);
return results;
return await askSimpleQuestions(args);
};
+113 -22
View File
@@ -1,12 +1,25 @@
import prompts from "prompts";
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import {
AI_REPORTS,
EXAMPLE_10K_SEC_FILES,
EXAMPLE_FILE,
EXAMPLE_GDPR,
} from "../helpers/datasources";
import { askModelConfig } from "../helpers/providers";
import { getTools } from "../helpers/tools";
import { ModelConfig, TemplateFramework } from "../helpers/types";
import { PureQuestionArgs, QuestionResults } from "./types";
import { askPostInstallAction, questionHandlers } from "./utils";
type AppType = "agentic_rag" | "financial_report" | "deep_research";
type AppType =
| "rag"
| "code_artifact"
| "financial_report_agent"
| "form_filling"
| "extractor"
| "contract_review"
| "data_scientist"
| "deep_research";
type SimpleAnswers = {
appType: AppType;
@@ -22,22 +35,53 @@ export const askSimpleQuestions = async (
{
type: "select",
name: "appType",
message: "What use case do you want to build?",
message: "What app do you want to build?",
hint: "🤖: Agent, 🔀: Workflow",
choices: [
{
title: "Agentic RAG",
value: "agentic_rag",
title: "🤖 Agentic RAG",
value: "rag",
description:
"Chatbot that answers questions based on provided documents.",
},
{
title: "Financial Report",
value: "financial_report",
title: "🤖 Data Scientist",
value: "data_scientist",
description:
"Agent that analyzes data and generates visualizations by using a code interpreter.",
},
{
title: "Deep Research",
title: "🤖 Code Artifact Agent",
value: "code_artifact",
description:
"Agent that writes code, runs it in a sandbox, and shows the output in the chat UI.",
},
{
title: "🤖 Information Extractor",
value: "extractor",
description:
"Extracts information from documents and returns it as a structured JSON object.",
},
{
title: "🔀 Financial Report Generator",
value: "financial_report_agent",
description:
"Generates a financial report by analyzing the provided 10-K SEC data. Uses a code interpreter to create charts or to conduct further analysis.",
},
{
title: "🔀 Financial 10k SEC Form Filler",
value: "form_filling",
description:
"Extracts information from 10k SEC data and uses it to fill out a CSV form.",
},
{
title: "🔀 Contract Reviewer",
value: "contract_review",
description:
"Extracts and reviews contracts to ensure compliance with GDPR regulations",
},
{
title: "🔀 Deep Researcher",
value: "deep_research",
description:
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
@@ -49,10 +93,13 @@ export const askSimpleQuestions = async (
let language: TemplateFramework = "fastapi";
let llamaCloudKey = args.llamaCloudKey;
let useLlamaCloud = false;
if (appType !== "extractor" && appType !== "contract_review") {
if (
appType !== "extractor" &&
appType !== "contract_review" &&
appType !== "deep_research"
) {
const { language: newLanguage } = await prompts(
{
type: "select",
@@ -123,36 +170,80 @@ const convertAnswers = async (
};
const lookup: Record<
AppType,
Pick<QuestionResults, "template" | "tools" | "dataSources" | "useCase"> & {
Pick<
QuestionResults,
"template" | "tools" | "frontend" | "dataSources" | "useCase"
> & {
modelConfig?: ModelConfig;
}
> = {
agentic_rag: {
template: "llamaindexserver",
rag: {
template: "streaming",
tools: getTools(["weather"]),
frontend: true,
dataSources: [EXAMPLE_FILE],
},
financial_report: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
data_scientist: {
template: "streaming",
tools: getTools(["interpreter", "document_generator"]),
frontend: true,
dataSources: [],
modelConfig: MODEL_GPT4o,
},
code_artifact: {
template: "streaming",
tools: getTools(["artifact"]),
frontend: true,
dataSources: [],
modelConfig: MODEL_GPT4o,
},
financial_report_agent: {
template: "multiagent",
useCase: "financial_report",
tools: getTools(["document_generator", "interpreter"]),
dataSources: EXAMPLE_10K_SEC_FILES,
frontend: true,
modelConfig: MODEL_GPT4o,
},
form_filling: {
template: "multiagent",
useCase: "form_filling",
tools: getTools(["form_filling"]),
dataSources: EXAMPLE_10K_SEC_FILES,
frontend: true,
modelConfig: MODEL_GPT4o,
},
extractor: {
template: "reflex",
useCase: "extractor",
tools: [],
frontend: false,
dataSources: [EXAMPLE_FILE],
},
contract_review: {
template: "reflex",
useCase: "contract_review",
tools: [],
frontend: false,
dataSources: [EXAMPLE_GDPR],
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
template: "multiagent",
useCase: "deep_research",
tools: [],
modelConfig: MODEL_GPT4o,
frontend: true,
dataSources: [AI_REPORTS],
},
};
const results = lookup[answers.appType];
return {
framework: answers.language,
useCase: answers.appType,
ui: "shadcn",
llamaCloudKey: answers.llamaCloudKey,
useLlamaParse: answers.useLlamaCloud,
llamapack: "",
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
observability: "none",
...results,
modelConfig:
results.modelConfig ??
@@ -161,6 +252,6 @@ const convertAnswers = async (
askModels: args.askModels ?? false,
framework: answers.language,
})),
frontend: true,
frontend: answers.language === "nextjs" ? false : results.frontend,
};
};
@@ -1,409 +0,0 @@
function Component({ events }) {
// Aggregate events by type and track their state progression
const aggregateEvents = () => {
const retrieveEvents = events.filter((e) => e.event === "retrieve");
const analyzeEvents = events.filter((e) => e.event === "analyze");
const answerEvents = events.filter((e) => e.event === "answer");
// Get the latest state for retrieve and analyze events
const retrieveState =
retrieveEvents.length > 0
? retrieveEvents[retrieveEvents.length - 1].state
: null;
const analyzeState =
analyzeEvents.length > 0
? analyzeEvents[analyzeEvents.length - 1].state
: null;
// Group answer events by their ID
const answerGroups = {};
for (const event of answerEvents) {
if (!event.id) continue;
if (!answerGroups[event.id]) {
answerGroups[event.id] = {
id: event.id,
question: event.question,
answer: event.answer,
states: [],
};
}
const lastState =
answerGroups[event.id].states[answerGroups[event.id].states.length - 1];
if (lastState !== event.state) {
answerGroups[event.id].states.push(event.state);
}
if (event.answer) {
answerGroups[event.id].answer = event.answer;
}
}
return {
retrieveState,
analyzeState,
answerGroups: Object.values(answerGroups),
};
};
const { retrieveState, analyzeState, answerGroups } = aggregateEvents();
// Styles
const styles = {
container: {
maxWidth: "900px",
margin: "0 auto",
padding: "20px",
backgroundColor: "#FFFFFF",
borderRadius: "8px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
},
header: {
fontSize: "24px",
fontWeight: "bold",
textAlign: "center",
marginBottom: "20px",
color: "#333",
},
cardsContainer: {
display: "flex",
gap: "16px",
marginBottom: "30px",
flexWrap: "wrap",
},
card: {
flex: "1",
minWidth: "250px",
backgroundColor: "#FFFFFF",
borderRadius: "8px",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
padding: "16px",
border: "1px solid #E5E7EB",
transition: "all 0.3s ease",
},
cardHeader: {
display: "flex",
alignItems: "center",
marginBottom: "12px",
},
cardTitle: {
fontSize: "18px",
fontWeight: "bold",
marginLeft: "8px",
},
cardContent: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
badge: {
padding: "4px 8px",
borderRadius: "9999px",
fontSize: "12px",
fontWeight: "bold",
flexShrink: 0,
},
questionsList: {
marginTop: "30px",
},
questionItem: {
backgroundColor: "#F9FAFB",
border: "1px solid #E5E7EB",
borderRadius: "8px",
marginBottom: "12px",
overflow: "hidden",
},
questionHeader: {
padding: "12px 16px",
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
userSelect: "none",
},
questionTitle: {
fontWeight: "medium",
marginLeft: "12px",
},
answerContainer: {
padding: "0",
backgroundColor: "#F3F4F6",
overflow: "hidden",
maxHeight: "0",
transition: "max-height 0.3s ease-out",
whiteSpace: "pre-line",
fontSize: "13px",
},
answerContent: {
padding: "16px",
},
answerContainerExpanded: {
maxHeight: "1000px", // Adjust based on content
},
arrow: {
marginLeft: "8px",
transition: "transform 0.3s ease",
display: "inline-block",
},
loadingContainer: {
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "24px",
color: "#6B7280",
},
stateIconContainer: {
display: "flex",
alignItems: "center",
gap: "8px",
},
};
// Helper function to get color for a state
const getStateColor = (state) => {
switch (state) {
case "inprogress":
return "#FCD34D";
case "done":
return "#34D399";
case "pending":
return "#9CA3AF";
default:
return "#D1D5DB";
}
};
// Helper function to get badge styles based on state
const getBadgeStyles = (state) => {
const colors = {
inprogress: { background: "#FEF3C7", color: "#92400E" },
done: { background: "#D1FAE5", color: "#065F46" },
pending: { background: "#F3F4F6", color: "#1F2937" },
};
const stateColors = colors[state] || colors.pending;
return {
...styles.badge,
backgroundColor: stateColors.background,
color: stateColors.color,
};
};
// Helper function to render state icon
const renderStateIcon = (state) => {
const color = getStateColor(state);
if (state === "inprogress") {
return (
<div style={{ color, animation: "spin 1s linear infinite" }}></div>
);
} else if (state === "done") {
return <div style={{ color }}></div>;
} else if (state === "pending") {
return <div style={{ color }}></div>;
}
return <div style={{ color }}>?</div>;
};
// State for toggling question answers
const [expandedQuestions, setExpandedQuestions] = React.useState({});
const toggleQuestion = (questionId) => {
setExpandedQuestions((prev) => ({
...prev,
[questionId]: !prev[questionId],
}));
};
return (
<div style={styles.container}>
<h1 style={styles.header}>Research Progress</h1>
{/* Status Cards */}
<div style={styles.cardsContainer}>
{/* Retrieve Card */}
<div
style={{
...styles.card,
borderColor:
retrieveState === "done"
? "#A7F3D0"
: retrieveState === "inprogress"
? "#FDE68A"
: "#E5E7EB",
}}
>
<div style={styles.cardHeader}>
<span style={{ color: "#8B5CF6" }}>🔍</span>
<div style={styles.cardTitle}>Data Retrieval</div>
</div>
<div style={styles.cardContent}>
<span style={{ fontSize: "14px", color: "#6B7280" }}>Status:</span>
<div style={styles.stateIconContainer}>
{retrieveState && (
<span style={getBadgeStyles(retrieveState)}>
{retrieveState === "inprogress"
? "In Progress"
: retrieveState === "done"
? "Completed"
: "Pending"}
</span>
)}
{renderStateIcon(retrieveState)}
</div>
</div>
</div>
{/* Analyze Card */}
<div
style={{
...styles.card,
borderColor:
analyzeState === "done"
? "#A7F3D0"
: analyzeState === "inprogress"
? "#FDE68A"
: "#E5E7EB",
}}
>
<div style={styles.cardHeader}>
<span style={{ color: "#06B6D4" }}>📊</span>
<div style={styles.cardTitle}>Data Analysis</div>
</div>
<div style={styles.cardContent}>
<span style={{ fontSize: "14px", color: "#6B7280" }}>Status:</span>
<div style={styles.stateIconContainer}>
{analyzeState && (
<span style={getBadgeStyles(analyzeState)}>
{analyzeState === "inprogress"
? "In Progress"
: analyzeState === "done"
? "Completed"
: "Pending"}
</span>
)}
{renderStateIcon(analyzeState)}
</div>
</div>
</div>
{/* Questions Card */}
<div style={styles.card}>
<div style={styles.cardHeader}>
<span style={{ color: "#10B981" }}>💬</span>
<div style={styles.cardTitle}>Questions</div>
</div>
<div style={styles.cardContent}>
<span style={{ fontSize: "14px", color: "#6B7280" }}>Status:</span>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span
style={{
...styles.badge,
backgroundColor: "#F3F4F6",
color: "#1F2937",
}}
>
{answerGroups.length} Questions
</span>
<span
style={{
fontSize: "14px",
fontWeight: "500",
color: "#059669",
}}
>
{answerGroups.filter((g) => g.states.includes("done")).length}{" "}
Answered
</span>
</div>
</div>
</div>
</div>
{/* Questions List */}
{answerGroups.length > 0 && (
<div style={styles.questionsList}>
<h2
style={{
fontSize: "20px",
fontWeight: "600",
marginBottom: "16px",
}}
>
Research Questions
</h2>
{answerGroups.map((group, index) => {
const latestState = group.states[group.states.length - 1];
const questionId = group.id || `question-${index}`;
const isExpanded = expandedQuestions[questionId];
const answerWithoutCitation = group.answer?.replace(
/\[citation:[a-f0-9-]+\]/g,
"",
);
return (
<div key={questionId} style={styles.questionItem}>
<div
style={styles.questionHeader}
onClick={() => toggleQuestion(questionId)}
>
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: "12px",
fontSize: "14px",
}}
>
{renderStateIcon(latestState)}
<span style={styles.questionTitle}>{group.question}</span>
<span
style={{
...styles.arrow,
transform: isExpanded
? "rotate(180deg)"
: "rotate(0deg)",
}}
>
</span>
</div>
<span style={getBadgeStyles(latestState)}>
{latestState === "inprogress"
? "In Progress"
: latestState === "done"
? "Answered"
: "Pending"}
</span>
</div>
<div
style={{
...styles.answerContainer,
...(isExpanded ? styles.answerContainerExpanded : {}),
}}
>
{answerWithoutCitation ? (
<div style={styles.answerContent}>
{answerWithoutCitation}
</div>
) : (
<div
style={{
...styles.loadingContainer,
...styles.answerContent,
}}
>
<span style={{ marginLeft: "8px" }}>
Generating answer...
</span>
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
);
}
@@ -1,31 +0,0 @@
# flake8: noqa: E402
from dotenv import load_dotenv
load_dotenv()
import logging
from app.index import get_index
from app.settings import init_settings
from llama_index.server.services.llamacloud.generate import (
load_to_llamacloud,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_datasource():
init_settings()
logger.info("Generate index for the provided data")
index = get_index(create_if_missing=True)
if index is None:
raise ValueError("Index not found and could not be created")
load_to_llamacloud(index, logger=logger)
if __name__ == "__main__":
generate_datasource()
@@ -1,7 +0,0 @@
from llama_index.server.services.llamacloud import (
LlamaCloudIndex,
get_client,
get_index,
)
__all__ = ["LlamaCloudIndex", "get_client", "get_index"]
@@ -1,100 +0,0 @@
import * as dotenv from "dotenv";
import "dotenv/config";
import * as fs from "fs/promises";
import { LLamaCloudFileService } from "llamaindex";
import * as path from "path";
import { getIndex } from "./app/data";
import { initSettings } from "./app/settings";
dotenv.config();
const REQUIRED_ENV_VARS = [
"LLAMA_CLOUD_INDEX_NAME",
"LLAMA_CLOUD_PROJECT_NAME",
"LLAMA_CLOUD_API_KEY",
];
export function checkRequiredEnvVars() {
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
return !process.env[envVar];
});
if (missingEnvVars.length > 0) {
console.log(
`The following environment variables are required but missing: ${missingEnvVars.join(
", ",
)}`,
);
throw new Error(
`Missing environment variables: ${missingEnvVars.join(", ")}`,
);
}
}
async function* walk(dir: string): AsyncGenerator<string> {
const directory = await fs.opendir(dir);
for await (const dirent of directory) {
const entryPath = path.join(dir, dirent.name);
if (dirent.isDirectory()) {
yield* walk(entryPath); // Recursively walk through directories
} else if (dirent.isFile()) {
yield entryPath; // Yield file paths
}
}
}
async function loadAndIndex() {
const index = await getIndex();
// ensure the index is available or create a new one
await index.ensureIndex({
verbose: true,
embedding: {
type: "OPENAI_EMBEDDING",
component: {
api_key: process.env.OPENAI_API_KEY,
model_name: "text-embedding-3-small",
},
},
});
const projectId = await index.getProjectId();
const pipelineId = await index.getPipelineId();
// walk through the data directory and upload each file to LlamaCloud
for await (const filePath of walk("data")) {
const buffer = await fs.readFile(filePath);
const filename = path.basename(filePath);
try {
await LLamaCloudFileService.addFileToPipeline(
projectId,
pipelineId,
new File([buffer], filename),
);
} catch (error) {
if (
error instanceof ReferenceError &&
error.message.includes("File is not defined")
) {
throw new Error(
"File class is not supported in the current Node.js version. Please use Node.js 20 or higher.",
);
}
throw error;
}
}
console.log(`Successfully uploaded documents to LlamaCloud!`);
}
(async () => {
try {
checkRequiredEnvVars();
initSettings();
await loadAndIndex();
console.log("Finished generating storage.");
} catch (error) {
console.error("Error generating storage.", error);
}
})();
@@ -1,28 +0,0 @@
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
project: string;
pipeline: string;
};
};
export async function getIndex(params?: LlamaCloudDataSourceParams) {
const { project, pipeline } = params?.llamaCloudPipeline ?? {};
const projectName = project ?? process.env.LLAMA_CLOUD_PROJECT_NAME;
const pipelineName = pipeline ?? process.env.LLAMA_CLOUD_INDEX_NAME;
const apiKey = process.env.LLAMA_CLOUD_API_KEY;
if (!projectName || !pipelineName || !apiKey) {
throw new Error(
"LlamaCloud is not configured. Please set project, pipeline, and api key in the params or as environment variables.",
);
}
const index = new LlamaCloudIndex({
organizationId: process.env.LLAMA_CLOUD_ORGANIZATION_ID,
name: pipelineName,
projectName,
apiKey,
baseUrl: process.env.LLAMA_CLOUD_BASE_URL,
});
return index;
}
@@ -1,59 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) simple agentic RAG project using [Agent Workflows](https://docs.llamaindex.ai/en/stable/examples/agent/agent_workflow_basic/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM.
Second, generate the embeddings of the documents in the `./data` directory:
```shell
poetry run generate
```
Third, run the development server:
```shell
poetry run dev
```
Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI.
To start the app optimized for **production**, run:
```
poetry run prod
```
## Configure LLM and Embedding Model
You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py).
## Use Case
We have prepared an [example workflow](./app/workflow.py) for the agentic RAG use case, where you can ask questions about the example documents in the [./data](./data) directory.
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "What standards for a letter exist?" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,22 +0,0 @@
from typing import Optional
from app.index import get_index
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.settings import Settings
from llama_index.llms.openai import OpenAI
from llama_index.server.api.models import ChatRequest
from llama_index.server.tools.index import get_query_engine_tool
def create_workflow(chat_request: Optional[ChatRequest] = None) -> AgentWorkflow:
index = get_index(chat_request=chat_request)
if index is None:
raise RuntimeError(
"Index not found! Please run `poetry run generate` to index the data first."
)
query_tool = get_query_engine_tool(index=index)
return AgentWorkflow.from_tools_or_functions(
tools_or_functions=[query_tool],
llm=Settings.llm or OpenAI(model="gpt-4o-mini"),
system_prompt="You are a helpful assistant.",
)
@@ -1,59 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM.
Second, generate the embeddings of the documents in the `./data` directory:
```shell
poetry run generate
```
Third, run the development server:
```shell
poetry run dev
```
Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI.
To start the app optimized for **production**, run:
```
poetry run prod
```
## Configure LLM and Embedding Model
You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py).
## Use Case
We have prepared an [example workflow](./app/workflow.py) for the deep research use case, where you can ask questions about the example documents in the [./data](./data) directory.
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,534 +0,0 @@
import logging
import os
import uuid
from typing import List, Literal, Optional
from app.index import get_index
from llama_index.core.base.llms.types import (
CompletionResponse,
CompletionResponseAsyncGen,
)
from llama_index.core.indices.base import BaseIndex
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
from llama_index.core.prompts import PromptTemplate
from llama_index.core.schema import MetadataMode, Node, NodeWithScore
from llama_index.core.settings import Settings
from llama_index.core.types import ChatMessage, MessageRole
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.api.models import SourceNodesEvent
from llama_index.server.api.models import ChatRequest
from pydantic import BaseModel, Field
logger = logging.getLogger("uvicorn")
logger.setLevel(logging.INFO)
def create_workflow(chat_request: Optional[ChatRequest] = None) -> Workflow:
index = get_index(chat_request=chat_request)
if index is None:
raise ValueError(
"Index is not found. Try run generation script to create the index first."
)
return DeepResearchWorkflow(
index=index,
timeout=120.0,
)
# Workflow events
class PlanResearchEvent(Event):
pass
class ResearchEvent(Event):
question_id: str
question: str
context_nodes: List[NodeWithScore]
class CollectAnswersEvent(Event):
question_id: str
question: str
answer: str
class ReportEvent(Event):
pass
# Events that are streamed to the frontend and rendered there
class DeepResearchEventData(BaseModel):
event: Literal["retrieve", "analyze", "answer"]
state: Literal["pending", "inprogress", "done", "error"]
id: Optional[str] = None
question: Optional[str] = None
answer: Optional[str] = None
class DataEvent(Event):
type: Literal["deep_research_event"]
data: DeepResearchEventData
def to_response(self):
return self.model_dump()
class DeepResearchWorkflow(Workflow):
"""
A workflow to research and analyze documents from multiple perspectives and write a comprehensive report.
Requirements:
- An indexed documents containing the knowledge base related to the topic
Steps:
1. Retrieve information from the knowledge base
2. Analyze the retrieved information and provide questions for answering
3. Answer the questions
4. Write the report based on the research results
"""
memory: SimpleComposableMemory
context_nodes: List[Node]
index: BaseIndex
user_request: str
stream: bool = True
def __init__(
self,
index: BaseIndex,
**kwargs,
):
super().__init__(**kwargs)
self.index = index
self.context_nodes = []
self.memory = SimpleComposableMemory.from_defaults(
primary_memory=ChatMemoryBuffer.from_defaults(),
)
@step
async def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent:
"""
Initiate the workflow: memory, tools, agent
"""
self.stream = ev.get("stream", True)
self.user_request = ev.get("user_msg")
chat_history = ev.get("chat_history")
if chat_history is not None:
self.memory.put_messages(chat_history)
await ctx.set("total_questions", 0)
# Add user message to memory
self.memory.put_messages(
messages=[
ChatMessage(
role=MessageRole.USER,
content=self.user_request,
)
]
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "inprogress",
},
)
)
retriever = self.index.as_retriever(
similarity_top_k=int(os.getenv("TOP_K", 10)),
)
nodes = retriever.retrieve(self.user_request)
self.context_nodes.extend(nodes) # type: ignore
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "done",
},
)
)
# Send source nodes to the stream
# Use SourceNodesEvent to display source nodes in the UI.
ctx.write_event_to_stream(
SourceNodesEvent(
nodes=nodes,
)
)
return PlanResearchEvent()
@step
async def analyze(
self, ctx: Context, ev: PlanResearchEvent
) -> ResearchEvent | ReportEvent | StopEvent:
"""
Analyze the retrieved information
"""
logger.info("Analyzing the retrieved information")
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "inprogress",
},
)
)
total_questions = await ctx.get("total_questions")
res = await plan_research(
memory=self.memory,
context_nodes=self.context_nodes,
user_request=self.user_request,
total_questions=total_questions,
)
if res.decision == "cancel":
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return StopEvent(
result=res.cancel_reason,
)
elif res.decision == "write":
# Writing a report without any research context is not allowed.
# It's a LLM hallucination.
if total_questions == 0:
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return StopEvent(
result="Sorry, I have a problem when analyzing the retrieved information. Please try again.",
)
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="No more idea to analyze. We should report the answers.",
)
)
ctx.send_event(ReportEvent())
else:
total_questions += len(res.research_questions)
await ctx.set("total_questions", total_questions) # For tracking
await ctx.set(
"waiting_questions", len(res.research_questions)
) # For waiting questions to be answered
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="We need to find answers to the following questions:\n"
+ "\n".join(res.research_questions),
)
)
for question in res.research_questions:
question_id = str(uuid.uuid4())
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "pending",
"id": question_id,
"question": question,
"answer": None,
},
)
)
ctx.send_event(
ResearchEvent(
question_id=question_id,
question=question,
context_nodes=self.context_nodes,
)
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return None
@step(num_workers=2)
async def answer(self, ctx: Context, ev: ResearchEvent) -> CollectAnswersEvent:
"""
Answer the question
"""
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "inprogress",
"id": ev.question_id,
"question": ev.question,
},
)
)
try:
answer = await research(
context_nodes=ev.context_nodes,
question=ev.question,
)
except Exception as e:
logger.error(f"Error answering question {ev.question}: {e}")
answer = f"Got error when answering the question: {ev.question}"
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "done",
"id": ev.question_id,
"question": ev.question,
"answer": answer,
},
)
)
return CollectAnswersEvent(
question_id=ev.question_id,
question=ev.question,
answer=answer,
)
@step
async def collect_answers(
self, ctx: Context, ev: CollectAnswersEvent
) -> PlanResearchEvent:
"""
Collect answers to all questions
"""
num_questions = await ctx.get("waiting_questions")
results = ctx.collect_events(
ev,
expected=[CollectAnswersEvent] * num_questions,
)
if results is None:
return None
for result in results:
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content=f"<Question>{result.question}</Question>\n<Answer>{result.answer}</Answer>",
)
)
await ctx.set("waiting_questions", 0)
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="Researched all the questions. Now, i need to analyze if it's ready to write a report or need to research more.",
)
)
return PlanResearchEvent()
@step
async def report(self, ctx: Context, ev: ReportEvent) -> StopEvent:
"""
Report the answers
"""
res = await write_report(
memory=self.memory,
user_request=self.user_request,
stream=self.stream,
)
return StopEvent(
result=res,
)
class AnalysisDecision(BaseModel):
decision: Literal["research", "write", "cancel"] = Field(
description="Whether to continue research, write a report, or cancel the research after several retries"
)
research_questions: Optional[List[str]] = Field(
description="""
If the decision is to research, provide a list of questions to research that related to the user request.
Maximum 3 questions. Set to null or empty if writing a report or cancel the research.
""",
default_factory=list,
)
cancel_reason: Optional[str] = Field(
description="The reason for cancellation if the decision is to cancel research.",
default=None,
)
async def plan_research(
memory: SimpleComposableMemory,
context_nodes: List[Node],
user_request: str,
total_questions: int,
) -> AnalysisDecision:
analyze_prompt = """
You are a professor who is guiding a researcher to research a specific request/problem.
Your task is to decide on a research plan for the researcher.
The possible actions are:
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
The workflow should be:
+ Always begin by providing some initial questions for the researcher to investigate.
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
Here are the context:
<Collected information>
{context_str}
</Collected information>
<Conversation context>
{conversation_context}
</Conversation context>
{enhanced_prompt}
Now, provide your decision in the required format for this user request:
<User request>
{user_request}
</User request>
"""
# Manually craft the prompt to avoid LLM hallucination
enhanced_prompt = ""
if total_questions == 0:
# Avoid writing a report without any research context
enhanced_prompt = """
The student has no questions to research. Let start by asking some questions.
"""
elif total_questions > 6:
# Avoid asking too many questions (when the data is not ready for writing a report)
enhanced_prompt = f"""
The student has researched {total_questions} questions. Should cancel the research if the context is not enough to write a report.
"""
conversation_context = "\n".join(
[f"{message.role}: {message.content}" for message in memory.get_all()]
)
context_str = "\n".join(
[node.get_content(metadata_mode=MetadataMode.LLM) for node in context_nodes]
)
res = await Settings.llm.astructured_predict(
output_cls=AnalysisDecision,
prompt=PromptTemplate(template=analyze_prompt),
user_request=user_request,
context_str=context_str,
conversation_context=conversation_context,
enhanced_prompt=enhanced_prompt,
)
return res
async def research(
question: str,
context_nodes: List[NodeWithScore],
) -> str:
prompt = """
You are a researcher who is in the process of answering the question.
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
Always add citations to the sentence/point/paragraph using the id of the provided content.
The citation should follow this format: [citation:id] where id is the id of the content.
E.g:
If we have a context like this:
<Citation id='abc-xyz'>
Baby llama is called cria
</Citation id='abc-xyz'>
And your answer uses the content, then the citation should be:
- Baby llama is called cria [citation:abc-xyz]
Here is the provided context for the question:
<Collected information>
{context_str}
</Collected information>`
No prior knowledge, just use the provided context to answer the question: {question}
"""
context_str = "\n".join(
[_get_text_node_content_for_citation(node) for node in context_nodes]
)
res = await Settings.llm.acomplete(
prompt=prompt.format(question=question, context_str=context_str),
)
return res.text
async def write_report(
memory: SimpleComposableMemory,
user_request: str,
stream: bool = False,
) -> CompletionResponse | CompletionResponseAsyncGen:
report_prompt = """
You are a researcher writing a report based on a user request and the research context.
You have researched various perspectives related to the user request.
The report should provide a comprehensive outline covering all important points from the researched perspectives.
Create a well-structured outline for the research report that covers all the answers.
# IMPORTANT when writing in markdown format:
+ Use tables or figures where appropriate to enhance presentation.
+ Preserve all citation syntax (the `[citation:id]()` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
+ Do not add links, a table of contents, or a references section to the report.
<User request>
{user_request}
</User request>
<Research context>
{research_context}
</Research context>
Now, write a report addressing the user request based on the research provided following the format and guidelines above.
"""
research_context = "\n".join(
[f"{message.role}: {message.content}" for message in memory.get_all()]
)
llm_complete_func = (
Settings.llm.astream_complete if stream else Settings.llm.acomplete
)
res = await llm_complete_func(
prompt=report_prompt.format(
user_request=user_request,
research_context=research_context,
),
)
return res
def _get_text_node_content_for_citation(node: NodeWithScore) -> str:
"""
Construct node content for LLM with citation flag.
"""
node_id = node.node.node_id
content = f"<Citation id='{node_id}'>\n{node.get_content(metadata_mode=MetadataMode.LLM)}</Citation id='{node_id}'>"
return content
@@ -1,59 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM and the `E2B_API_KEY` for the code interpreter. You can get the E2B API key from [here](https://e2b.dev).
Second, generate the embeddings of the documents in the `./data` directory:
```shell
poetry run generate
```
Third, run the development server:
```shell
poetry run dev
```
Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI.
To start the app optimized for **production**, run:
```
poetry run prod
```
## Configure LLM and Embedding Model
You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py).
## Use Case
We have prepared an [example workflow](./app/workflow.py) for the financial report use case, where you can ask questions about the example documents in the [./data](./data) directory.
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,333 +0,0 @@
import os
from typing import List, Optional
from app.index import get_index
from llama_index.core import Settings
from llama_index.core.base.llms.types import ChatMessage, MessageRole
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.api.models import AgentRunEvent, ChatRequest
from llama_index.server.settings import server_settings
from llama_index.server.tools.document_generator import DocumentGenerator
from llama_index.server.tools.index import get_query_engine_tool
from llama_index.server.tools.interpreter import E2BCodeInterpreter
from llama_index.server.utils.agent_tool import (
call_tools,
chat_with_tools,
)
def create_workflow(chat_request: Optional[ChatRequest] = None) -> Workflow:
index = get_index(chat_request=chat_request)
if index is None:
raise ValueError(
"Index is not found. Try run generation script to create the index first."
)
query_engine_tool = get_query_engine_tool(index=index)
e2b_api_key = os.getenv("E2B_API_KEY")
if e2b_api_key is None:
raise ValueError(
"E2B_API_KEY is required to use the code interpreter tool. Please check README.md to know how to get the key."
)
code_interpreter_tool = E2BCodeInterpreter(api_key=e2b_api_key).to_tool()
document_generator_tool = DocumentGenerator(
file_server_url_prefix=server_settings.file_server_url_prefix,
).to_tool()
return FinancialReportWorkflow(
query_engine_tool=query_engine_tool,
code_interpreter_tool=code_interpreter_tool,
document_generator_tool=document_generator_tool,
timeout=180,
)
class InputEvent(Event):
input: List[ChatMessage]
response: bool = False
class ResearchEvent(Event):
input: list[ToolSelection]
class AnalyzeEvent(Event):
input: list[ToolSelection] | ChatMessage
class ReportEvent(Event):
input: list[ToolSelection]
class FinancialReportWorkflow(Workflow):
"""
A workflow to generate a financial report using indexed documents.
Requirements:
- Indexed documents containing financial data and a query engine tool to search them
- A code interpreter tool to analyze data and generate reports
- A document generator tool to create report files
Steps:
1. LLM Input: The LLM determines the next step based on function calling.
For example, if the model requests the query engine tool, it returns a ResearchEvent;
if it requests document generation, it returns a ReportEvent.
2. Research: Uses the query engine to find relevant chunks from indexed documents.
After gathering information, it requests analysis (step 3).
3. Analyze: Uses a custom prompt to analyze research results and can call the code
interpreter tool for visualization or calculation. Returns results to the LLM.
4. Report: Uses the document generator tool to create a report. Returns results to the LLM.
"""
_default_system_prompt = """
You are a financial analyst who are given a set of tools to help you.
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
"""
stream: bool = True
def __init__(
self,
query_engine_tool: QueryEngineTool,
code_interpreter_tool: FunctionTool,
document_generator_tool: FunctionTool,
llm: Optional[FunctionCallingLLM] = None,
system_prompt: Optional[str] = None,
**kwargs,
):
super().__init__(**kwargs)
self.system_prompt = system_prompt or self._default_system_prompt
self.query_engine_tool = query_engine_tool
self.code_interpreter_tool = code_interpreter_tool
self.document_generator_tool = document_generator_tool
assert query_engine_tool is not None, (
"Query engine tool is not found. Try run generation script or upload a document file first."
)
assert code_interpreter_tool is not None, "Code interpreter tool is required"
assert document_generator_tool is not None, (
"Document generator tool is required"
)
self.tools = [
self.query_engine_tool,
self.code_interpreter_tool,
self.document_generator_tool,
]
self.llm: FunctionCallingLLM = llm or Settings.llm # type: ignore
assert isinstance(self.llm, FunctionCallingLLM)
self.memory = ChatMemoryBuffer.from_defaults(llm=self.llm)
@step()
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
self.stream = ev.get("stream", True)
user_msg = ev.get("user_msg")
chat_history = ev.get("chat_history")
if chat_history is not None:
self.memory.put_messages(chat_history)
# Add user message to memory
self.memory.put(ChatMessage(role=MessageRole.USER, content=user_msg))
if self.system_prompt:
system_msg = ChatMessage(
role=MessageRole.SYSTEM, content=self.system_prompt
)
self.memory.put(system_msg)
return InputEvent(input=self.memory.get())
@step()
async def handle_llm_input( # type: ignore
self,
ctx: Context,
ev: InputEvent,
) -> ResearchEvent | AnalyzeEvent | ReportEvent | StopEvent:
"""
Handle an LLM input and decide the next step.
"""
# Always use the latest chat history from the input
chat_history: list[ChatMessage] = ev.input
# Get tool calls
response = await chat_with_tools(
self.llm,
self.tools, # type: ignore
chat_history,
)
if not response.has_tool_calls():
if self.stream:
return StopEvent(result=response.generator)
else:
return StopEvent(result=await response.full_response())
# calling different tools at the same time is not supported at the moment
# add an error message to tell the AI to process step by step
if response.is_calling_different_tools():
self.memory.put(
ChatMessage(
role=MessageRole.ASSISTANT,
content="Cannot call different tools at the same time. Try calling one tool at a time.",
)
)
return InputEvent(input=self.memory.get()) # type: ignore
self.memory.put(response.tool_call_message)
match response.tool_name():
case self.code_interpreter_tool.metadata.name:
return AnalyzeEvent(input=response.tool_calls)
case self.document_generator_tool.metadata.name:
return ReportEvent(input=response.tool_calls)
case self.query_engine_tool.metadata.name:
return ResearchEvent(input=response.tool_calls)
case _:
raise ValueError(f"Unknown tool: {response.tool_name()}")
@step()
async def research(self, ctx: Context, ev: ResearchEvent) -> AnalyzeEvent:
"""
Do a research to gather information for the user's request.
A researcher should have these tools: query engine, search engine, etc.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Researcher",
msg="Starting research",
)
)
tool_calls = ev.input
tool_call_outputs = await call_tools(
ctx=ctx,
agent_name="Researcher",
tools=[self.query_engine_tool],
tool_calls=tool_calls,
)
for tool_call_output in tool_call_outputs:
self.memory.put(
ChatMessage(
role=MessageRole.TOOL,
content=tool_call_output.tool_output.content,
additional_kwargs={
"name": tool_call_output.tool_output.tool_name,
"tool_call_id": tool_call_output.tool_call_id,
},
)
)
return AnalyzeEvent(
input=ChatMessage(
role=MessageRole.ASSISTANT,
content="Researcher: I've finished the research. Please analyze the result.",
),
)
@step()
async def analyze(self, ctx: Context, ev: AnalyzeEvent) -> InputEvent:
"""
Analyze the research result.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Analyst",
msg="Starting analysis",
)
)
event_requested_by_workflow_llm = isinstance(ev.input, list)
# Requested by the workflow LLM Input step, it's a tool call
if event_requested_by_workflow_llm:
# Set the tool calls
tool_calls = ev.input
else:
# Otherwise, it's triggered by the research step
# Use a custom prompt and independent memory for the analyst agent
analysis_prompt = """
You are a financial analyst, you are given a research result and a set of tools to help you.
Always use the given information, don't make up anything yourself. If there is not enough information, you can asking for more information.
If you have enough numerical information, it's good to include some charts/visualizations to the report so you can use the code interpreter tool to generate a report.
"""
# This is handled by analyst agent
# Clone the shared memory to avoid conflicting with the workflow.
chat_history = self.memory.get()
chat_history.append(
ChatMessage(role=MessageRole.SYSTEM, content=analysis_prompt)
)
chat_history.append(ev.input) # type: ignore
# Check if the analyst agent needs to call tools
response = await chat_with_tools(
self.llm,
[self.code_interpreter_tool],
chat_history,
)
if not response.has_tool_calls():
# If no tool call, fallback analyst message to the workflow
msg_content = await response.full_response()
analyst_msg = ChatMessage(
role=MessageRole.ASSISTANT,
content=f"Analyst: "
f"\nHere is the analysis result: {msg_content}"
"\nUse it for other steps or response the content to the user.",
)
self.memory.put(analyst_msg)
return InputEvent(input=self.memory.get())
else:
tool_calls = response.tool_calls
self.memory.put(response.tool_call_message)
# Call tools
tool_call_outputs = await call_tools(
ctx=ctx,
agent_name="Analyst",
tools=[self.code_interpreter_tool],
tool_calls=tool_calls, # type: ignore
)
for tool_call_output in tool_call_outputs:
self.memory.put(
ChatMessage(
role=MessageRole.TOOL,
content=tool_call_output.tool_output.content,
additional_kwargs={
"name": tool_call_output.tool_output.tool_name,
"tool_call_id": tool_call_output.tool_call_id,
},
)
)
# Fallback to the input with the latest chat history
return InputEvent(input=self.memory.get())
@step()
async def report(self, ctx: Context, ev: ReportEvent) -> InputEvent:
"""
Generate a report based on the analysis result.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Reporter",
msg="Starting report generation",
)
)
tool_calls = ev.input
tool_call_outputs = await call_tools(
ctx=ctx,
agent_name="Reporter",
tools=[self.document_generator_tool],
tool_calls=tool_calls,
)
for tool_call_output in tool_call_outputs:
self.memory.put(
ChatMessage(
role=MessageRole.TOOL,
content=tool_call_output.tool_output.content,
additional_kwargs={
"name": tool_call_output.tool_output.tool_name,
"tool_call_id": tool_call_output.tool_call_id,
},
)
)
# After the tool calls, fallback to the input with the latest chat history
return InputEvent(input=self.memory.get())
@@ -1,52 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM.
Second, generate the embeddings of the example documents in the `./data` directory:
```
npm run generate
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) and [embedding model](https://ts.llamaindex.ai/docs/llamaindex/modules/embeddings) in the [settings file](src/app/settings.ts).
## Use Case
We have prepared an [example workflow](./src/app/workflow.ts) for the agentic RAG use case, where you can ask questions about the example documents in the [./data](./data) directory.
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "What standards for a letter exist?" }] }'
```
## 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/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Agent Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/agent_workflow) - learn about LlamaIndexTS Agent Workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -1,16 +0,0 @@
import { agent } from "llamaindex";
import { getIndex } from "./data";
export const workflowFactory = async (reqBody: any) => {
const index = await getIndex(reqBody?.data);
const queryEngineTool = index.queryTool({
metadata: {
name: "query_document",
description: `This tool can retrieve information about Apple and Tesla financial data`,
},
includeSourceNodes: true,
});
return agent({ tools: [queryEngineTool] });
};
@@ -1,56 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM.
Second, generate the embeddings of the example documents in the `./data` directory:
```
npm run generate
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) and [embedding model](https://ts.llamaindex.ai/docs/llamaindex/modules/embeddings) in the [settings file](src/app/settings.ts).
## Custom UI Components
For Deep Research, we have a custom component located in `components/deep_research_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
## Use Case
We have prepared an [example workflow](./src/app/workflow.ts) for the Deep Research use case, where you can request a detailed answer about the example documents in the [./data](./data) directory.
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
```
## 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/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -1,425 +0,0 @@
import {
DeepResearchEvent,
toSourceEvent,
toStreamGenerator,
} from "@llamaindex/server";
import {
AgentInputData,
AgentWorkflowContext,
ChatMemoryBuffer,
ChatResponseChunk,
HandlerContext,
LlamaCloudIndex,
Metadata,
MetadataMode,
NodeWithScore,
PromptTemplate,
Settings,
StartEvent,
StopEvent as StopEventBase,
ToolCallLLM,
VectorStoreIndex,
Workflow,
WorkflowEvent,
} from "llamaindex";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { getIndex } from "./data";
// workflow factory
export const workflowFactory = async (reqBody: any) => {
const index = await getIndex(reqBody?.data);
return new DeepResearchWorkflow(index);
};
// workflow configs
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
const TIMEOUT = 360; // timeout in seconds
const TOP_K = 10; // number of nodes to retrieve from the vector store
const createPlanResearchPrompt = new PromptTemplate({
template: `
You are a professor who is guiding a researcher to research a specific request/problem.
Your task is to decide on a research plan for the researcher.
The possible actions are:
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
The workflow should be:
+ Always begin by providing some initial questions for the researcher to investigate.
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
Here are the context:
<Collected information>
{context_str}
</Collected information>
<Conversation context>
{conversation_context}
</Conversation context>
{enhanced_prompt}
Now, provide your decision in the required format for this user request:
<User request>
{user_request}
</User request>
`,
templateVars: [
"context_str",
"conversation_context",
"enhanced_prompt",
"user_request",
],
});
const researchPrompt = new PromptTemplate({
template: `
You are a researcher who is in the process of answering the question.
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
Always add citations to the sentence/point/paragraph using the id of the provided content.
The citation should follow this format: [citation:id] where id is the id of the content.
E.g:
If we have a context like this:
<Citation id='abc-xyz'>
Baby llama is called cria
</Citation id='abc-xyz'>
And your answer uses the content, then the citation should be:
- Baby llama is called cria [citation:abc-xyz]
Here is the provided context for the question:
<Collected information>
{context_str}
</Collected information>
No prior knowledge, just use the provided context to answer the question: {question}
`,
templateVars: ["context_str", "question"],
});
const WRITE_REPORT_PROMPT = `
You are a researcher writing a report based on a user request and the research context.
You have researched various perspectives related to the user request.
The report should provide a comprehensive outline covering all important points from the researched perspectives.
Create a well-structured outline for the research report that covers all the answers.
# IMPORTANT when writing in markdown format:
+ Use tables or figures where appropriate to enhance presentation.
+ Preserve all citation syntax (the \`[citation:id]()\` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
+ Do not add links, a table of contents, or a references section to the report.
`;
// workflow events
type ResearchQuestion = { questionId: string; question: string };
type ResearchResult = ResearchQuestion & { answer: string };
class PlanResearchEvent extends WorkflowEvent<{}> {}
class ResearchEvent extends WorkflowEvent<ResearchQuestion[]> {}
class ReportEvent extends WorkflowEvent<{}> {}
class StopEvent extends StopEventBase<AsyncGenerator<ChatResponseChunk>> {}
// workflow definition
class DeepResearchWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
#llm = Settings.llm as ToolCallLLM;
#index?: VectorStoreIndex | LlamaCloudIndex;
userRequest: string = "";
totalQuestions: number = 0;
contextNodes: NodeWithScore<Metadata>[] = [];
memory: ChatMemoryBuffer = new ChatMemoryBuffer({ llm: Settings.llm });
constructor(index: VectorStoreIndex | LlamaCloudIndex) {
super({ timeout: TIMEOUT });
this.#index = index;
this.addWorkflowSteps();
}
addWorkflowSteps() {
this.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [PlanResearchEvent],
},
this.handleStartWorkflow,
);
this.addStep(
{
inputs: [PlanResearchEvent],
outputs: [ResearchEvent, ReportEvent, StopEvent],
},
this.handlePlanResearch,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [PlanResearchEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [ReportEvent],
outputs: [StopEvent],
},
this.handleReport,
);
}
async initWorkflow(data: AgentInputData) {
const { userInput, chatHistory = [] } = data;
if (!userInput) throw new Error("Invalid input");
this.userRequest = userInput;
await this.memory.set(chatHistory);
await this.memory.put({ role: "user", content: userInput });
}
handleStartWorkflow = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<PlanResearchEvent> => {
await this.initWorkflow(ev.data);
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "retrieve", state: "inprogress" },
}),
);
const retrievedNodes = await this.retriever.retrieve(this.userRequest);
ctx.sendEvent(toSourceEvent(retrievedNodes));
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "retrieve", state: "done" },
}),
);
this.contextNodes = retrievedNodes;
return new PlanResearchEvent({});
};
handlePlanResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: PlanResearchEvent,
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "analyze", state: "inprogress" },
}),
);
const { decision, researchQuestions, cancelReason } =
await this.createResearchPlan();
// Stop workflow due to decision from LLM
if (decision === "cancel") {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "analyze", state: "done" },
}),
);
return new StopEvent(
toStreamGenerator(
cancelReason ?? "Research cancelled without any reason.",
),
);
}
// Trigger research from generated questions
if (decision === "research") {
this.memory.put({
role: "assistant",
content:
"We need to find answers to the following questions:\n" +
researchQuestions.join("\n"),
});
researchQuestions.forEach(({ questionId: id, question }) => {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "answer", state: "pending", id, question },
}),
);
});
return new ResearchEvent(researchQuestions);
}
// Resarch done, start writing report
this.memory.put({
role: "assistant",
content: "No more idea to analyze. We should report the answers.",
});
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "analyze", state: "done" },
}),
);
return new ReportEvent({});
};
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<PlanResearchEvent> => {
const researchQuestions = ev.data;
// Answer questions in parallel
const researchResults: ResearchResult[] = await Promise.all(
researchQuestions.map(async ({ questionId: id, question }) => {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "answer", state: "inprogress", id, question },
}),
);
const answer = await this.answerQuestion(question);
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
data: { event: "answer", state: "done", id, question, answer },
}),
);
return { questionId: id, question, answer };
}),
);
// Save answers to memory
researchResults.forEach(({ question, answer }) => {
this.memory.put({
role: "assistant",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
});
});
this.memory.put({
role: "assistant",
content:
"Researched all the questions. Now, I need to analyze if it's ready to write a report or need to research more.",
});
this.totalQuestions += researchResults.length;
return new PlanResearchEvent({});
};
handleReport = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportEvent,
): Promise<StopEvent> => {
const chatHistory = await this.memory.getAllMessages();
const messages = chatHistory.concat([
{
role: "system",
content: WRITE_REPORT_PROMPT,
},
{
role: "user",
content:
"Write a report addressing the user request based on the research provided the context",
},
]);
const stream = await this.llm.chat({ messages, stream: true });
return new StopEvent(toStreamGenerator(stream));
};
get llm() {
if (!this.#llm.supportToolCall) throw new Error("LLM is not a ToolCallLLM");
return this.#llm;
}
get retriever() {
if (!this.#index) throw new Error("Index is not initialized");
return this.#index.asRetriever({ similarityTopK: TOP_K });
}
get contextStr() {
return this.contextNodes
.map((node) => {
const nodeId = node.node.id_;
const nodeContent = node.node.getContent(MetadataMode.NONE);
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
})
.join("\n");
}
get enhancedPrompt() {
if (this.totalQuestions === 0) {
return "The student has no questions to research. Let start by asking some questions.";
}
if (this.totalQuestions > MAX_QUESTIONS) {
return `The student has researched ${this.totalQuestions} questions. Should cancel the research if the context is not enough to write a report.`;
}
return "";
}
async createResearchPlan() {
const chatHistory = await this.memory.getMessages();
const conversationContext = chatHistory
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const prompt = createPlanResearchPrompt.format({
context_str: this.contextStr,
conversation_context: conversationContext,
enhanced_prompt: this.enhancedPrompt,
user_request: this.userRequest,
});
const responseFormat = z.object({
decision: z.enum(["research", "write", "cancel"]),
researchQuestions: z.array(z.string()),
cancelReason: z.string().optional(),
});
const result = await this.llm.complete({ prompt, responseFormat });
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
return {
...plan,
researchQuestions: plan.researchQuestions.map((question) => ({
questionId: randomUUID(),
question,
})),
};
}
async answerQuestion(question: string) {
const prompt = researchPrompt.format({
context_str: this.contextStr,
question,
});
const result = await this.llm.complete({ prompt });
return result.text;
}
}
@@ -1,52 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM and the `E2B_API_KEY` for the code interpreter. You can get the E2B API key from [here](https://e2b.dev).
Second, generate the embeddings of the example documents in the `./data` directory:
```
npm run generate
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) and [embedding model](https://ts.llamaindex.ai/docs/llamaindex/modules/embeddings) in the [settings file](src/app/settings.ts).
## Use Case
We have prepared an [example workflow](./src/app/workflow.ts) for the financial report generation use case, where you can request for the app to generate a financial report using the example documents in the [./data](./data).
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Generate a financial report that compares the financial performance of Apple and Tesla" }] }'
```
## 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/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -1,396 +0,0 @@
import { toAgentRunEvent, toSourceEvent } from "@llamaindex/server";
import {
callTools,
chatWithTools,
documentGenerator,
interpreter,
} from "@llamaindex/tools";
import {
AgentInputData,
AgentWorkflowContext,
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponseChunk,
HandlerContext,
Metadata,
NodeWithScore,
Settings,
StartEvent,
StopEvent,
ToolCall,
ToolCallLLM,
Workflow,
WorkflowEvent,
} from "llamaindex";
import { getIndex } from "./data";
const TIMEOUT = 360 * 1000;
export async function workflowFactory(reqBody: any) {
const index = await getIndex(reqBody?.data);
const queryEngineTool = index.queryTool({
metadata: {
name: "query_document",
description: `This tool can retrieve information about Apple and Tesla financial data`,
},
includeSourceNodes: true,
});
if (!process.env.E2B_API_KEY) {
throw new Error("E2B_API_KEY is required to use the code interpreter tool");
}
const codeInterpreterTool = interpreter({
apiKey: process.env.E2B_API_KEY!,
});
const documentGeneratorTool = documentGenerator();
return new FinancialReportWorkflow({
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
timeout: TIMEOUT,
});
}
// Create a custom event type
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
class ResearchEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
class AnalyzeEvent extends WorkflowEvent<{
input: ChatMessage | ToolCall[];
}> {}
class ReportGenerationEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
const DEFAULT_SYSTEM_PROMPT = `
You are a financial analyst who are given a set of tools to help you.
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
`;
class FinancialReportWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
constructor(options: {
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.llm = Settings.llm as ToolCallLLM;
if (!this.llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.queryEngineTool = options.queryEngineTool;
this.codeInterpreterTool = options.codeInterpreterTool;
this.documentGeneratorTool = options.documentGeneratorTool;
this.memory = new ChatMemoryBuffer({ llm: this.llm, chatHistory: [] });
// Add steps
this.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [InputEvent],
},
this.prepareChatHistory,
);
this.addStep(
{
inputs: [InputEvent],
outputs: [
InputEvent,
ResearchEvent,
AnalyzeEvent,
ReportGenerationEvent,
StopEvent,
],
},
this.handleLLMInput,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [AnalyzeEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [AnalyzeEvent],
outputs: [InputEvent],
},
this.handleAnalyze,
);
this.addStep(
{
inputs: [ReportGenerationEvent],
outputs: [InputEvent],
},
this.handleReportGeneration,
);
}
prepareChatHistory = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<InputEvent> => {
const { userInput, chatHistory = [] } = ev.data;
if (!userInput) throw new Error("Invalid input");
this.memory.set(chatHistory);
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
this.memory.put({ role: "user", content: userInput });
const messages = await this.memory.getMessages();
return new InputEvent({ input: messages });
};
handleLLMInput = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: InputEvent,
): Promise<
| InputEvent
| ResearchEvent
| AnalyzeEvent
| ReportGenerationEvent
| StopEvent<AsyncGenerator<ChatResponseChunk, any, any> | undefined>
> => {
const chatHistory = ev.data.input;
const tools = [
this.codeInterpreterTool,
this.documentGeneratorTool,
this.queryEngineTool,
];
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
return new StopEvent(toolCallResponse.responseGenerator);
}
if (toolCallResponse.hasMultipleTools()) {
this.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
this.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case this.codeInterpreterTool.metadata.name:
return new AnalyzeEvent({
input: toolCallResponse.toolCalls,
});
case this.documentGeneratorTool.metadata.name:
return new ReportGenerationEvent({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (this.queryEngineTool.metadata.name === toolName) {
return new ResearchEvent({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
};
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<AnalyzeEvent> => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Researcher",
text: "Researching data",
type: "text",
}),
);
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.queryEngineTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Researcher",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
.map((msg) => (msg.options as any)?.toolResult?.result?.sourceNodes)
.flat()
.filter(Boolean);
if (sourcesNodes.length > 0) {
ctx.sendEvent(toSourceEvent(sourcesNodes));
}
return new AnalyzeEvent({
input: {
role: "assistant",
content:
"I have finished researching the data, please analyze the data.",
},
});
};
/**
* Analyze a research result or a tool call for code interpreter from the LLM
*/
handleAnalyze = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: AnalyzeEvent,
): Promise<InputEvent> => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Analyst",
text: "Analyzing data",
type: "text",
}),
);
// Request by workflow LLM, input is a list of tool calls
let toolCalls: ToolCall[] = [];
if (Array.isArray(ev.data.input)) {
toolCalls = ev.data.input;
} else {
// Requested by Researcher, input is a ChatMessage
// We start new LLM chat specifically for analyzing the data
const analysisPrompt = `
You are an expert in analyzing financial data.
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
Construct the analysis in textual format; including tables would be great!
Don't need to synthesize the data, just analyze and provide your findings.
`;
// Clone the current chat history
// Add the analysis system prompt and the message from the researcher
const chatHistory = await this.memory.getMessages();
const newChatHistory = [
...chatHistory,
{ role: "system", content: analysisPrompt },
ev.data.input,
];
const toolCallResponse = await chatWithTools(
this.llm,
[this.codeInterpreterTool],
newChatHistory as ChatMessage[],
);
if (!toolCallResponse.hasToolCall()) {
this.memory.put(await toolCallResponse.asFullResponse());
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
} else {
this.memory.put(toolCallResponse.toolCallMessage!);
toolCalls = toolCallResponse.toolCalls;
}
}
// Call the tools
const toolMsgs = await callTools({
tools: [this.codeInterpreterTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Analyst",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
handleReportGeneration = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportGenerationEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.documentGeneratorTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Reporter",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
}
@@ -1,23 +0,0 @@
import logging
import os
from typing import Optional
from llama_index.core.indices import load_index_from_storage
from llama_index.server.api.models import ChatRequest
from llama_index.server.tools.index.utils import get_storage_context
logger = logging.getLogger("uvicorn")
STORAGE_DIR = "storage"
def get_index(chat_request: Optional[ChatRequest] = None):
# check if storage already exists
if not os.path.exists(STORAGE_DIR):
return None
# load the existing index
logger.info(f"Loading index from {STORAGE_DIR}...")
storage_context = get_storage_context(STORAGE_DIR)
index = load_index_from_storage(storage_context)
logger.info(f"Finished loading index from {STORAGE_DIR}")
return index
@@ -1,8 +0,0 @@
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
def init_settings():
Settings.llm = OpenAI(model="gpt-4o-mini")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
@@ -1,33 +0,0 @@
import logging
import os
from app.index import STORAGE_DIR
from app.settings import init_settings
from dotenv import load_dotenv
from llama_index.core.indices import (
VectorStoreIndex,
)
from llama_index.core.readers import SimpleDirectoryReader
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_datasource():
load_dotenv()
init_settings()
logger.info("Creating new index")
# load the documents and create the index
reader = SimpleDirectoryReader(
os.environ.get("DATA_DIR", "data"),
recursive=True,
)
documents = reader.load_data()
index = VectorStoreIndex.from_documents(
documents,
show_progress=True,
)
# store it for later
index.storage_context.persist(STORAGE_DIR)
logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}")
@@ -1,5 +0,0 @@
__pycache__
storage
.env
output
.ui/
@@ -1,40 +0,0 @@
import logging
import os
import subprocess
from app.settings import init_settings
from app.workflow import create_workflow
from dotenv import load_dotenv
from llama_index.server import LlamaIndexServer
logger = logging.getLogger("uvicorn")
def create_app():
env = os.environ.get("APP_ENV")
app = LlamaIndexServer(
workflow_factory=create_workflow, # A factory function that creates a new workflow for each request
component_dir="components",
env=env,
logger=logger,
)
# You can also add custom routes to the app
app.add_api_route("/api/health", lambda: {"message": "OK"}, status_code=200)
return app
load_dotenv()
init_settings()
app = create_app()
def run(env: str):
os.environ["APP_ENV"] = env
app_host = os.getenv("APP_HOST", "0.0.0.0")
app_port = os.getenv("APP_PORT", "8000")
if env == "dev":
subprocess.run(["fastapi", "dev", "--host", app_host, "--port", app_port])
else:
subprocess.run(["fastapi", "run", "--host", app_host, "--port", app_port])
@@ -1,47 +0,0 @@
[tool]
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["Marcus Schiesser <mail@marcusschiesser.de>"]
readme = "README.md"
[tool.poetry.scripts]
generate = "generate:generate_datasource"
dev = "main:run('dev')"
prod = "main:run('prod')"
[tool.poetry.dependencies]
python = ">=3.11,<3.14"
python-dotenv = "^1.0.0"
pydantic = "<2.10"
aiostream = "^0.5.2"
llama-index-core = "^0.12.28"
llama-index-server = "^0.1.8"
[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"
pytest = "^8.3.5"
pytest-asyncio = "^0.25.3"
[tool.mypy]
python_version = "3.11"
plugins = "pydantic.mypy"
exclude = ["tests", "venv", ".venv", "output", "config"]
check_untyped_defs = true
warn_unused_ignores = false
show_error_codes = true
namespace_packages = true
ignore_missing_imports = true
follow_imports = "silent"
implicit_optional = true
strict_optional = false
disable_error_code = ["return-value", "assignment"]
[[tool.mypy.overrides]]
module = "app.*"
ignore_missing_imports = false
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
@@ -1,40 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
output/
storage/
.env
@@ -1,23 +0,0 @@
{
"name": "llamaindex-server",
"version": "0.1.0",
"scripts": {
"generate": "tsx src/generate.ts",
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts"
},
"dependencies": {
"@llamaindex/openai": "0.2.0",
"@llamaindex/readers": "^2.0.0",
"@llamaindex/server": "0.0.9",
"@llamaindex/tools": "0.0.4",
"dotenv": "^16.4.7",
"llamaindex": "0.9.17",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.10.3",
"tsx": "^4.7.2",
"typescript": "^5.3.2"
}
}
@@ -1,3 +0,0 @@
{
"trailingComma": "all"
}
@@ -1,22 +0,0 @@
import {
SimpleDocumentStore,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
export async function getIndex(params?: any) {
const storageContext = await storageContextFromDefaults({
persistDir: "storage",
});
const numberOfDocs = Object.keys(
(storageContext.docStore as SimpleDocumentStore).toDict(),
).length;
if (numberOfDocs === 0) {
throw new Error(
"Index not found. Please run `pnpm run generate` to generate the embeddings of the documents",
);
}
return await VectorStoreIndex.init({ storageContext });
}
@@ -1,11 +0,0 @@
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
});
}
@@ -1,25 +0,0 @@
import "dotenv/config";
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { initSettings } from "./app/settings";
async function generateDatasource() {
console.log(`Generating storage context...`);
// Split documents, create embeddings and store them in the storage context
const storageContext = await storageContextFromDefaults({
persistDir: "storage",
});
// load documents from current directoy into an index
const reader = new SimpleDirectoryReader();
const documents = await reader.loadData("data");
await VectorStoreIndex.fromDocuments(documents, {
storageContext,
});
console.log("Storage context successfully generated.");
}
(async () => {
initSettings();
await generateDatasource();
})();
@@ -1,12 +0,0 @@
import { LlamaIndexServer } from "@llamaindex/server";
import "dotenv/config";
import { initSettings } from "./app/settings";
import { workflowFactory } from "./app/workflow";
initSettings();
new LlamaIndexServer({
workflow: workflowFactory,
appTitle: "LlamaIndex App",
componentsDir: "components",
}).start();
@@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}