Compare commits

..

27 Commits

Author SHA1 Message Date
github-actions[bot] a8162a9269 Release 0.3.1 (#377)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-16 15:23:09 +07:00
Huu Le f3577c50d6 add data scientist use case (directly using uploaded files) (#355)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2024-10-16 14:00:59 +07:00
Huu Le a5f5c9dc9c fix always ask post installation action (#376) 2024-10-16 09:52:25 +07:00
Huu Le 2be68d1c7f ci: activate llamacloud for TS (#372)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-15 13:40:47 +07:00
Thuc Pham 8c80cc05ce fix: enhance performance for codeblock (#347)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-15 12:21:08 +07:00
github-actions[bot] dfd4fd58ab Release 0.3.0 (#368)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-14 16:25:37 +07:00
Thuc Pham 0a69fe09fa fix: missing params when init Astra vectorstore (#373) 2024-10-14 16:03:41 +07:00
Marcus Schiesser de88b32208 fix: remove llamacloud for extractor 2024-10-14 15:35:59 +07:00
Marcus Schiesser ef88bff211 chore: upgrade reflex 2024-10-14 15:09:16 +07:00
Marcus Schiesser 7562cb48d6 docs: changeset 2024-10-14 13:41:22 +07:00
Marcus Schiesser 9dde6d0288 feat: simplify questions asked (#370) 2024-10-14 13:35:39 +07:00
Thuc Pham 98a82b0b25 docs: chroma env variables (#367) 2024-10-11 11:10:29 +07:00
github-actions[bot] 7db72b6f2e Release 0.2.19 (#365)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-10 18:41:25 +07:00
Thuc Pham 3d41488301 feat: use selected llamacloud for multiagent (#359) 2024-10-10 18:37:55 +07:00
github-actions[bot] 1ee05eaf4b Release 0.2.18 (#364)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-10 18:03:43 +07:00
Huu Le 75e1f6104c fix: TypeScript templates do not create a new LlamaCloud index or upload a file to an existing index. (#356) 2024-10-10 17:58:12 +07:00
Huu Le 88220f1dd2 feat: add canceling workflow for multiagent (#361) 2024-10-10 15:24:43 +07:00
github-actions[bot] 6304114ef5 Release 0.2.17 (#357)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-09 16:31:50 +07:00
Marcus Schiesser 6335de1174 docs: changeset 2024-10-09 16:18:11 +07:00
Huu Le b9184ff59a fix: (FastAPI) Using LlamaCloud parameters does not use the configured value in the environment. (#358) 2024-10-09 16:13:35 +07:00
Thuc Pham cd3fcd0512 bump: use latest LITS (#343) 2024-10-09 13:40:04 +07:00
github-actions[bot] a47d778602 Release 0.2.16 (#349)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-08 17:28:40 +07:00
Marcus Schiesser 7f4ac228ee Don't need to run generate script for LlamaCloud (#352) 2024-10-08 16:56:12 +07:00
Marcus Schiesser 5263bde8e7 feat: Use selected LlamaCloud index in multi-agent template (#350) 2024-10-08 16:54:14 +07:00
Huu Le 4dee65b93d add astral's uv tool to github action (#351) 2024-10-08 16:19:20 +07:00
Huu Le c60182a925 Add mypy checker (#346) 2024-10-08 15:17:38 +07:00
Marcus Schiesser 0e78ba4603 fix: .env not loaded on poetry run generate (#348)
--------
Co-authored-by: leehuwuj <leehuwuj@gmail.com>
2024-10-08 13:41:37 +07:00
107 changed files with 2583 additions and 1799 deletions
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["fastapi"]
datasources: ["--no-files", "--example-file"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
shell: bash
@@ -86,7 +86,7 @@ jobs:
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs", "express"]
datasources: ["--no-files", "--example-file"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
shell: bash
+3
View File
@@ -17,6 +17,9 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
+1
View File
@@ -1,2 +1,3 @@
pnpm format
pnpm lint
uvx ruff format --check templates/
+49
View File
@@ -1,5 +1,54 @@
# create-llama
## 0.3.1
### Patch Changes
- f3577c5: Fix event streaming is blocked
- f3577c5: Add upload file to sandbox (artifact and code interpreter)
## 0.3.0
### Minor Changes
- 7562cb4: Simplified default questions and added pro mode
### Patch Changes
- 0a69fe0: fix: missing params when init Astra vectorstore
- 98a82b0: docs: chroma env variables
## 0.2.19
### Patch Changes
- 3d41488: feat: use selected llamacloud for multiagent
## 0.2.18
### Patch Changes
- 75e1f61: Fix cannot query public document from llamacloud
- 88220f1: fix workflow doesn't stop when user presses stop generation button
- 75e1f61: Fix typescript templates cannot upload file to llamacloud
- 88220f1: Bump llama_index@0.11.17
## 0.2.17
### Patch Changes
- cd3fcd0: bump: use LlamaIndexTS 0.6.18
- 6335de1: Fix using LlamaCloud selector does not use the configured values in the environment (Python)
## 0.2.16
### Patch Changes
- 0e78ba4: Fix: programmatically ensure index for LlamaCloud
- 0e78ba4: Fix .env not loaded on poetry run generate
- 7f4ac22: Don't need to run generate script for LlamaCloud
- 5263bde: Use selected LlamaCloud index in multi-agent template
## 0.2.15
### Patch Changes
+149 -93
View File
@@ -15,6 +15,8 @@ const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// TODO: add support for other templates
if (
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
) {
@@ -45,14 +47,86 @@ if (
const observabilityOptions = ["llamatrace", "traceloop"];
// Run separate tests for each observability option to reduce CI runtime
test.describe("Test resolve python dependencies with observability", () => {
// Testing with streaming template, vectorDb: none, tools: none, and dataSource: --example-file
for (const observability of observabilityOptions) {
test(`observability: ${observability}`, async () => {
const cwd = await createTestDir();
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
await createAndCheckLlamaProject({
// Test vector databases
for (const vectorDb of vectorDbs) {
test(`Mypy check for vectorDB: ${vectorDb}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb,
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (vectorDb !== "none") {
if (vectorDb === "pg") {
expect(pyprojectContent).toContain(
"llama-index-vector-stores-postgres",
);
} else {
expect(pyprojectContent).toContain(
`llama-index-vector-stores-${vectorDb}`,
);
}
}
});
}
// Test tools
for (const tool of toolOptions) {
test(`Mypy check for tool: ${tool}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: tool,
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (tool === "wikipedia.WikipediaToolSpec") {
expect(pyprojectContent).toContain("wikipedia");
}
if (tool === "google.GoogleSearchToolSpec") {
expect(pyprojectContent).toContain("google");
}
});
}
// Test data sources
for (const dataSource of dataSources) {
const dataSourceType = dataSource.split(" ")[0];
test(`Mypy check for data source: ${dataSourceType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
@@ -60,89 +134,53 @@ if (
dataSource,
vectorDb: "none",
tools: "none",
port: 3000, // port, not used
externalPort: 8000, // externalPort, not used
postInstallAction: "none", // postInstallAction
templateUI: undefined, // ui
appType: "--no-frontend", // appType
llamaCloudProjectName: undefined, // llamaCloudProjectName
llamaCloudIndexName: undefined, // llamaCloudIndexName
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (dataSource.includes("--web-source")) {
expect(pyprojectContent).toContain("llama-index-readers-web");
}
if (dataSource.includes("--db-source")) {
expect(pyprojectContent).toContain("llama-index-readers-database");
}
});
}
// Test observability options
for (const observability of observabilityOptions) {
test(`Mypy check for observability: ${observability}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability,
},
});
});
}
});
test.describe("Test resolve python dependencies", () => {
for (const vectorDb of vectorDbs) {
for (const tool of toolOptions) {
for (const dataSource of dataSources) {
const dataSourceType = dataSource.split(" ")[0];
const toolDescription = tool === "none" ? "no tools" : tool;
const optionDescription = `vectorDb: ${vectorDb}, ${toolDescription}, dataSource: ${dataSourceType}`;
test(`options: ${optionDescription}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath, projectPath } =
await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource,
vectorDb,
tools: tool,
port: 3000, // port, not used
externalPort: 8000, // externalPort, not used
postInstallAction: "none", // postInstallAction
templateUI: undefined, // ui
appType: "--no-frontend", // appType
llamaCloudProjectName: undefined, // llamaCloudProjectName
llamaCloudIndexName: undefined, // llamaCloudIndexName
observability: undefined, // observability
},
});
// Additional checks for specific dependencies
// Verify that specific dependencies are in pyproject.toml
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (vectorDb !== "none") {
if (vectorDb === "pg") {
expect(pyprojectContent).toContain(
"llama-index-vector-stores-postgres",
);
} else {
expect(pyprojectContent).toContain(
`llama-index-vector-stores-${vectorDb}`,
);
}
}
if (tool !== "none") {
if (tool === "wikipedia.WikipediaToolSpec") {
expect(pyprojectContent).toContain("wikipedia");
}
if (tool === "google.GoogleSearchToolSpec") {
expect(pyprojectContent).toContain("google");
}
}
// Check for data source specific dependencies
if (dataSource.includes("--web-source")) {
expect(pyprojectContent).toContain("llama-index-readers-web");
}
if (dataSource.includes("--db-source")) {
expect(pyprojectContent).toContain(
"llama-index-readers-database ",
);
}
});
}
}
}
});
}
async function createAndCheckLlamaProject({
@@ -161,21 +199,39 @@ async function createAndCheckLlamaProject({
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
// Run poetry lock
const env = {
...process.env,
POETRY_VIRTUALENVS_IN_PROJECT: "true",
};
// Run poetry install
try {
const { stdout, stderr } = await execAsync(
"poetry config virtualenvs.in-project true && poetry lock --no-update",
{ cwd: projectPath },
const { stdout: installStdout, stderr: installStderr } = await execAsync(
"poetry install",
{ cwd: projectPath, env },
);
console.log("poetry lock stdout:", stdout);
console.error("poetry lock stderr:", stderr);
console.log("poetry install stdout:", installStdout);
console.error("poetry install stderr:", installStderr);
} catch (error) {
console.error("Error running poetry lock:", error);
console.error("Error running poetry install:", error);
throw error;
}
// Check if poetry.lock file was created
expect(fs.existsSync(path.join(projectPath, "poetry.lock"))).toBeTruthy();
// Run poetry run mypy
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"poetry run mypy .",
{ cwd: projectPath, env },
);
console.log("poetry run mypy stdout:", mypyStdout);
console.error("poetry run mypy stderr:", mypyStderr);
} catch (error) {
console.error("Error running mypy:", error);
throw error;
}
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
}
+7
View File
@@ -27,6 +27,13 @@ const userMessage =
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
const isNode18 = process.version.startsWith("v18");
const isLlamaCloud = dataSource === "--llamacloud";
// llamacloud is using File API which is not supported on node 18
if (isNode18 && isLlamaCloud) {
test.skip(true, "Skipping tests for Node 18 and LlamaCloud data source");
}
let port: number;
let externalPort: number;
let cwd: string;
+3 -3
View File
@@ -65,7 +65,7 @@ const getVectorDBEnvs = (
{
name: "PG_CONNECTION_STRING",
description:
"For generating a connection URI, see https://docs.timescale.com/use-timescale/latest/services/create-a-service\nThe PostgreSQL connection string.",
"For generating a connection URI, see https://supabase.com/vector\nThe PostgreSQL connection string.",
},
];
@@ -182,11 +182,11 @@ const getVectorDBEnvs = (
},
{
name: "CHROMA_HOST",
description: "The API endpoint for your Chroma database",
description: "The hostname for your Chroma database. Eg: localhost",
},
{
name: "CHROMA_PORT",
description: "The port for your Chroma database",
description: "The port for your Chroma database. Eg: 8000",
},
];
// TS Version doesn't support config local storage path
+1 -1
View File
@@ -1,7 +1,7 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = [
"claude-3-opus",
+1 -1
View File
@@ -1,7 +1,7 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { questionHandlers } from "../../questions";
import { questionHandlers } from "../../questions/utils";
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
"gpt-35-turbo": { openAIModel: "gpt-3.5-turbo" },
+1 -1
View File
@@ -1,7 +1,7 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
type ModelData = {
+1 -1
View File
@@ -1,7 +1,7 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { questionHandlers, toChoice } from "../../questions/utils";
import got from "got";
import ora from "ora";
+2 -3
View File
@@ -1,6 +1,5 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { questionHandlers } from "../../questions";
import { questionHandlers } from "../../questions/utils";
import { ModelConfig, ModelProvider, TemplateFramework } from "../types";
import { askAnthropicQuestions } from "./anthropic";
import { askAzureQuestions } from "./azure";
@@ -27,7 +26,7 @@ export async function askModelConfig({
framework,
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
if (askModels && !ciInfo.isCI) {
if (askModels) {
let choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
+1 -1
View File
@@ -4,7 +4,7 @@ import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers } from "../../questions";
import { questionHandlers } from "../../questions/utils";
export const TSYSTEMS_LLMHUB_API_URL =
"https://llm-server.llmhub.t-systems.net/v2";
+1 -1
View File
@@ -1,7 +1,7 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = ["mistral-tiny", "mistral-small", "mistral-medium"];
type ModelData = {
+1 -1
View File
@@ -3,7 +3,7 @@ import ollama, { type ModelResponse } from "ollama";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { questionHandlers, toChoice } from "../../questions/utils";
type ModelData = {
dimensions: number;
+1 -1
View File
@@ -4,7 +4,7 @@ import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { questionHandlers } from "../../questions";
import { questionHandlers } from "../../questions/utils";
const OPENAI_API_URL = "https://api.openai.com/v1";
+7 -7
View File
@@ -93,6 +93,12 @@ const getAdditionalDependencies = (
});
break;
}
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.3.1",
});
break;
}
// Add data source dependencies
@@ -123,16 +129,10 @@ const getAdditionalDependencies = (
extras: ["rsa"],
});
dependencies.push({
name: "psycopg2",
name: "psycopg2-binary",
version: "^2.9.9",
});
break;
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.3.1",
});
break;
}
}
}
+1 -1
View File
@@ -139,7 +139,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "e2b_code_interpreter",
version: "0.0.10",
version: "0.0.11b38",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
+1 -1
View File
@@ -46,7 +46,7 @@ export type TemplateDataSource = {
type: TemplateDataSourceType;
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db" | "llamacloud";
export type TemplateDataSourceType = "file" | "web" | "db";
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
// Config for both file and folder
export type FileSourceConfig = {
+1 -6
View File
@@ -279,12 +279,7 @@ async function updatePackageJson({
"remark-gfm": undefined,
"remark-math": undefined,
"react-markdown": undefined,
"react-syntax-highlighter": undefined,
};
packageJson.devDependencies = {
...packageJson.devDependencies,
"@types/react-syntax-highlighter": undefined,
"highlight.js": undefined,
};
}
+57 -87
View File
@@ -1,7 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import Commander from "commander";
import Conf from "conf";
import { Command } from "commander";
import fs from "fs";
import path from "path";
import { bold, cyan, green, red, yellow } from "picocolors";
@@ -17,8 +16,9 @@ import { runApp } from "./helpers/run-app";
import { getTools } from "./helpers/tools";
import { validateNpmName } from "./helpers/validate-pkg";
import packageJson from "./package.json";
import { QuestionArgs, askQuestions, onPromptState } from "./questions";
import { askQuestions } from "./questions/index";
import { QuestionArgs } from "./questions/types";
import { onPromptState } from "./questions/utils";
// Run the initialization function
initializeGlobalAgent();
@@ -29,12 +29,14 @@ const handleSigTerm = () => process.exit(0);
process.on("SIGINT", handleSigTerm);
process.on("SIGTERM", handleSigTerm);
const program = new Commander.Command(packageJson.name)
const program = new Command(packageJson.name)
.version(packageJson.version)
.arguments("<project-directory>")
.usage(`${green("<project-directory>")} [options]`)
.arguments("[project-directory]")
.usage(`${green("[project-directory]")} [options]`)
.action((name) => {
projectPath = name;
if (name) {
projectPath = name;
}
})
.option(
"--use-npm",
@@ -55,13 +57,6 @@ const program = new Commander.Command(packageJson.name)
`
Explicitly tell the CLI to bootstrap the application using Yarn
`,
)
.option(
"--reset-preferences",
`
Explicitly tell the CLI to reset any stored preferences
`,
)
.option(
@@ -124,7 +119,14 @@ const program = new Commander.Command(packageJson.name)
"--frontend",
`
Whether to generate a frontend for your backend.
Generate a frontend for your backend.
`,
)
.option(
"--no-frontend",
`
Do not generate a frontend for your backend.
`,
)
.option(
@@ -161,6 +163,13 @@ const program = new Commander.Command(packageJson.name)
Specify the tools you want to use by providing a comma-separated list. For example, 'wikipedia.WikipediaToolSpec,google.GoogleSearchToolSpec'. Use 'none' to not using any tools.
`,
(tools, _) => {
if (tools === "none") {
return [];
} else {
return getTools(tools.split(","));
}
},
)
.option(
"--use-llama-parse",
@@ -189,86 +198,66 @@ const program = new Commander.Command(packageJson.name)
Allow interactive selection of LLM and embedding models of different model providers.
`,
false,
)
.option(
"--ask-examples",
"--pro",
`
Allow interactive selection of community templates and LlamaPacks.
Allow interactive selection of all features.
`,
false,
)
.allowUnknownOption()
.parse(process.argv);
if (process.argv.includes("--no-frontend")) {
program.frontend = false;
}
if (process.argv.includes("--tools")) {
if (program.tools === "none") {
program.tools = [];
} else {
program.tools = getTools(program.tools.split(","));
}
}
const options = program.opts();
if (
process.argv.includes("--no-llama-parse") ||
program.template === "extractor"
options.template === "extractor"
) {
program.useLlamaParse = false;
options.useLlamaParse = false;
}
program.askModels = process.argv.includes("--ask-models");
program.askExamples = process.argv.includes("--ask-examples");
if (process.argv.includes("--no-files")) {
program.dataSources = [];
options.dataSources = [];
} else if (process.argv.includes("--example-file")) {
program.dataSources = getDataSources(program.files, program.exampleFile);
options.dataSources = getDataSources(options.files, options.exampleFile);
} else if (process.argv.includes("--llamacloud")) {
program.dataSources = [
{
type: "llamacloud",
config: {},
},
EXAMPLE_FILE,
];
options.dataSources = [EXAMPLE_FILE];
options.vectorDb = "llamacloud";
} else if (process.argv.includes("--web-source")) {
program.dataSources = [
options.dataSources = [
{
type: "web",
config: {
baseUrl: program.webSource,
prefix: program.webSource,
baseUrl: options.webSource,
prefix: options.webSource,
depth: 1,
},
},
];
} else if (process.argv.includes("--db-source")) {
program.dataSources = [
options.dataSources = [
{
type: "db",
config: {
uri: program.dbSource,
queries: program.dbQuery || "SELECT * FROM mytable",
uri: options.dbSource,
queries: options.dbQuery || "SELECT * FROM mytable",
},
},
];
}
const packageManager = !!program.useNpm
const packageManager = !!options.useNpm
? "npm"
: !!program.usePnpm
: !!options.usePnpm
? "pnpm"
: !!program.useYarn
: !!options.useYarn
? "yarn"
: getPkgManager();
async function run(): Promise<void> {
const conf = new Conf({ projectName: "create-llama" });
if (program.resetPreferences) {
conf.clear();
console.log(`Preferences reset successfully`);
return;
}
if (typeof projectPath === "string") {
projectPath = projectPath.trim();
}
@@ -331,35 +320,16 @@ async function run(): Promise<void> {
process.exit(1);
}
const preferences = (conf.get("preferences") || {}) as QuestionArgs;
await askQuestions(
program as unknown as QuestionArgs,
preferences,
program.openAiKey,
);
const answers = await askQuestions(options as unknown as QuestionArgs);
await createApp({
template: program.template,
framework: program.framework,
ui: program.ui,
...answers,
appPath: resolvedProjectPath,
packageManager,
frontend: program.frontend,
modelConfig: program.modelConfig,
llamaCloudKey: program.llamaCloudKey,
communityProjectConfig: program.communityProjectConfig,
llamapack: program.llamapack,
vectorDb: program.vectorDb,
externalPort: program.externalPort,
postInstallAction: program.postInstallAction,
dataSources: program.dataSources,
tools: program.tools,
useLlamaParse: program.useLlamaParse,
observability: program.observability,
externalPort: options.externalPort,
});
conf.set("preferences", preferences);
if (program.postInstallAction === "VSCode") {
if (answers.postInstallAction === "VSCode") {
console.log(`Starting VSCode in ${root}...`);
try {
execSync(`code . --new-window --goto README.md`, {
@@ -383,15 +353,15 @@ Please check ${cyan(
)} for more information.`,
);
}
} else if (program.postInstallAction === "runApp") {
} else if (answers.postInstallAction === "runApp") {
console.log(`Running app in ${root}...`);
await runApp(
root,
program.template,
program.frontend,
program.framework,
program.port,
program.externalPort,
answers.template,
answers.frontend,
answers.framework,
options.port,
options.externalPort,
);
}
}
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.2.15",
"version": "0.3.1",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -49,8 +49,7 @@
"async-retry": "1.3.1",
"async-sema": "3.0.1",
"ci-info": "github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
"commander": "2.20.0",
"conf": "10.2.0",
"commander": "12.1.0",
"cross-spawn": "7.0.3",
"fast-glob": "3.3.1",
"fs-extra": "11.2.0",
@@ -59,7 +58,7 @@
"ollama": "^0.5.0",
"ora": "^8.0.1",
"picocolors": "1.0.0",
"prompts": "2.1.0",
"prompts": "2.4.2",
"smol-toml": "^1.1.4",
"tar": "6.1.15",
"terminal-link": "^3.0.0",
+11 -147
View File
@@ -42,11 +42,8 @@ importers:
specifier: github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540
version: https://codeload.github.com/watson/ci-info/tar.gz/f43f6a1cefff47fb361c88cf4b943fdbcaafe540
commander:
specifier: 2.20.0
version: 2.20.0
conf:
specifier: 10.2.0
version: 10.2.0
specifier: 12.1.0
version: 12.1.0
cross-spawn:
specifier: 7.0.3
version: 7.0.3
@@ -72,8 +69,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
prompts:
specifier: 2.1.0
version: 2.1.0
specifier: 2.4.2
version: 2.4.2
smol-toml:
specifier: ^1.1.4
version: 1.1.4
@@ -336,20 +333,9 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@8.13.0:
resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -410,10 +396,6 @@ packages:
async-sema@3.0.1:
resolution: {integrity: sha512-fKT2riE8EHAvJEfLJXZiATQWqZttjx1+tfgnVshCDrH8vlw4YC8aECe0B8MU184g+aVRFVgmfxFlKZKaozSrNw==}
atomically@1.7.0:
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
engines: {node: '>=10.12.0'}
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -530,8 +512,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@2.20.0:
resolution: {integrity: sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
@@ -540,10 +523,6 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
conf@10.2.0:
resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==}
engines: {node: '>=12'}
cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
@@ -576,10 +555,6 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
debounce-fn@4.0.0:
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
engines: {node: '>=10'}
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -638,10 +613,6 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
dot-prop@6.0.1:
resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
engines: {node: '>=10'}
duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
@@ -664,10 +635,6 @@ packages:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -788,10 +755,6 @@ packages:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@@ -1057,10 +1020,6 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-obj@2.0.0:
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
engines: {node: '>=8'}
is-path-inside@3.0.3:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
@@ -1138,12 +1097,6 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema-typed@7.0.3:
resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -1182,10 +1135,6 @@ packages:
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
engines: {node: '>=6'}
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -1243,10 +1192,6 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mimic-fn@3.1.0:
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==}
engines: {node: '>=8'}
mimic-response@1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
@@ -1375,10 +1320,6 @@ packages:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
@@ -1407,10 +1348,6 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1449,10 +1386,6 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
pkg-up@3.1.0:
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
engines: {node: '>=8'}
playwright-core@1.44.0:
resolution: {integrity: sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==}
engines: {node: '>=16'}
@@ -1498,8 +1431,8 @@ packages:
engines: {node: '>=14'}
hasBin: true
prompts@2.1.0:
resolution: {integrity: sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
pseudomap@1.0.2:
@@ -1557,10 +1490,6 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
@@ -2306,10 +2235,6 @@ snapshots:
acorn@8.11.3: {}
ajv-formats@2.1.1(ajv@8.13.0):
optionalDependencies:
ajv: 8.13.0
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -2317,13 +2242,6 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.13.0:
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
ansi-colors@4.1.3: {}
ansi-escapes@5.0.0:
@@ -2383,8 +2301,6 @@ snapshots:
async-sema@3.0.1: {}
atomically@1.7.0: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.0.0
@@ -2506,25 +2422,12 @@ snapshots:
color-name@1.1.4: {}
commander@2.20.0: {}
commander@12.1.0: {}
commander@9.5.0: {}
concat-map@0.0.1: {}
conf@10.2.0:
dependencies:
ajv: 8.13.0
ajv-formats: 2.1.1(ajv@8.13.0)
atomically: 1.7.0
debounce-fn: 4.0.0
dot-prop: 6.0.1
env-paths: 2.2.1
json-schema-typed: 7.0.3
onetime: 5.1.2
pkg-up: 3.1.0
semver: 7.6.1
cross-spawn@5.1.0:
dependencies:
lru-cache: 4.1.5
@@ -2568,10 +2471,6 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
debounce-fn@4.0.0:
dependencies:
mimic-fn: 3.1.0
debug@4.3.4:
dependencies:
ms: 2.1.2
@@ -2621,10 +2520,6 @@ snapshots:
dependencies:
esutils: 2.0.3
dot-prop@6.0.1:
dependencies:
is-obj: 2.0.0
duplexer3@0.1.5: {}
eastasianwidth@0.2.0: {}
@@ -2644,8 +2539,6 @@ snapshots:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
env-paths@2.2.1: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@@ -2841,10 +2734,6 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
find-up@3.0.0:
dependencies:
locate-path: 3.0.0
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
@@ -3129,8 +3018,6 @@ snapshots:
is-number@7.0.0: {}
is-obj@2.0.0: {}
is-path-inside@3.0.3: {}
is-plain-obj@1.1.0: {}
@@ -3197,10 +3084,6 @@ snapshots:
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-schema-typed@7.0.3: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json-stringify-safe@5.0.1: {}
@@ -3239,11 +3122,6 @@ snapshots:
pify: 4.0.1
strip-bom: 3.0.0
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
path-exists: 3.0.0
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -3301,8 +3179,6 @@ snapshots:
mimic-fn@2.1.0: {}
mimic-fn@3.1.0: {}
mimic-response@1.0.1: {}
mimic-response@2.1.0: {}
@@ -3425,10 +3301,6 @@ snapshots:
dependencies:
yocto-queue: 0.1.0
p-locate@3.0.0:
dependencies:
p-limit: 2.3.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
@@ -3456,8 +3328,6 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
path-exists@3.0.0: {}
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -3483,10 +3353,6 @@ snapshots:
dependencies:
find-up: 4.1.0
pkg-up@3.1.0:
dependencies:
find-up: 3.0.0
playwright-core@1.44.0: {}
playwright@1.44.0:
@@ -3515,7 +3381,7 @@ snapshots:
prettier@3.2.5: {}
prompts@2.1.0:
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
@@ -3585,8 +3451,6 @@ snapshots:
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
require-main-filename@2.0.0: {}
resolve-from@4.0.0: {}
-769
View File
@@ -1,769 +0,0 @@
import { execSync } from "child_process";
import ciInfo from "ci-info";
import fs from "fs";
import path from "path";
import { blue, green, red } from "picocolors";
import prompts from "prompts";
import { InstallAppArgs } from "./create-app";
import {
TemplateDataSource,
TemplateDataSourceType,
TemplateFramework,
TemplateType,
} from "./helpers";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
import { EXAMPLE_FILE } from "./helpers/datasources";
import { templatesDir } from "./helpers/dir";
import { getAvailableLlamapackOptions } from "./helpers/llama-pack";
import { askModelConfig } from "./helpers/providers";
import { getProjectOptions } from "./helpers/repo";
import {
supportedTools,
toolRequiresConfig,
toolsRequireConfig,
} from "./helpers/tools";
export type QuestionArgs = Omit<
InstallAppArgs,
"appPath" | "packageManager"
> & {
askModels?: boolean;
askExamples?: boolean;
};
const supportedContextFileTypes = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
];
const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.windows.forms
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$dialogResult = $folderBrowser.ShowDialog()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
{
$folderBrowser.SelectedPath
}
`;
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: "",
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
tools: [],
};
export const questionHandlers = {
onCancel: () => {
console.error("Exiting.");
process.exit(1);
},
};
const getVectorDbChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No, just store the data in the file system",
value: "none",
},
{ title: "MongoDB", value: "mongo" },
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
{ title: "ChromaDB", value: "chroma" },
{ title: "Weaviate", value: "weaviate" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
const compPath = path.join(templatesDir, "components");
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
const availableChoices = fs
.readdirSync(vectordbPath)
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
const displayedChoices = choices.filter((choice) =>
availableChoices.includes(choice.value),
);
return displayedChoices;
};
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
template?: TemplateType,
) => {
// If LlamaCloud is already selected, don't show any other options
if (selectedDataSource.find((s) => s.type === "llamacloud")) {
return [];
}
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No datasource",
value: "none",
});
choices.push({
title:
process.platform !== "linux"
? "Use an example PDF"
: "Use an example PDF (you can add your own data files later)",
value: "exampleFile",
});
}
// Linux has many distros so we won't support file/folder picker for now
if (process.platform !== "linux") {
choices.push(
{
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
value: "file",
},
{
title:
process.platform === "win32"
? "Use a local folder"
: "Use local folders",
value: "folder",
},
);
}
if (framework === "fastapi" && template !== "extractor") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
if (!selectedDataSource.length && template !== "extractor") {
choices.push({
title: "Use managed index from LlamaCloud",
value: "llamacloud",
});
}
return choices;
};
const selectLocalContextData = async (type: TemplateDataSourceType) => {
try {
let selectedPath: string = "";
let execScript: string;
let execOpts: any = {};
switch (process.platform) {
case "win32": // Windows
execScript =
type === "file"
? WINDOWS_FILE_SELECTION_SCRIPT
: WINDOWS_FOLDER_SELECTION_SCRIPT;
execOpts = { shell: "powershell.exe" };
break;
case "darwin": // MacOS
execScript =
type === "file"
? MACOS_FILE_SELECTION_SCRIPT
: MACOS_FOLDER_SELECTION_SCRIPT;
break;
default: // Unsupported OS
console.log(red("Unsupported OS error!"));
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
),
);
process.exit(1);
}
}
return paths;
} catch (error) {
console.log(
red(
"Got an error when trying to select local context data! Please try again or select another data source option.",
),
);
process.exit(1);
}
};
export const onPromptState = (state: any) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write("\x1B[?25h");
process.stdout.write("\n");
process.exit(1);
}
};
export const askQuestions = async (
program: QuestionArgs,
preferences: QuestionArgs,
openAiKey?: string,
) => {
const getPrefOrDefault = <K extends keyof Omit<QuestionArgs, "modelConfig">>(
field: K,
): Omit<QuestionArgs, "modelConfig">[K] =>
preferences[field] ?? defaults[field];
// Ask for next action after installation
async function askPostInstallAction() {
if (program.postInstallAction === undefined) {
if (ciInfo.isCI) {
program.postInstallAction = getPrefOrDefault("postInstallAction");
} else {
const actionChoices = [
{
title: "Just generate code (~1 sec)",
value: "none",
},
{
title: "Start in VSCode (~1 sec)",
value: "VSCode",
},
{
title: "Generate code and install dependencies (~2 min)",
value: "dependencies",
},
];
const modelConfigured =
!program.llamapack && program.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = program.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(program.tools)
) {
actionChoices.push({
title:
"Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await prompts(
{
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: actionChoices,
initial: 1,
},
questionHandlers,
);
program.postInstallAction = action;
}
}
}
if (!program.template) {
if (ciInfo.isCI) {
program.template = getPrefOrDefault("template");
} else {
const styledRepo = blue(
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
);
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
{
title: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ title: "Structured Extractor", value: "extractor" },
...(program.askExamples
? [
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
]
: []),
],
initial: 0,
},
questionHandlers,
);
program.template = template;
preferences.template = template;
}
}
if (program.template === "community") {
const projectOptions = await getProjectOptions(
COMMUNITY_OWNER,
COMMUNITY_REPO,
);
const { communityProjectConfig } = await prompts(
{
type: "select",
name: "communityProjectConfig",
message: "Select community template",
choices: projectOptions.map(({ title, value }) => ({
title,
value: JSON.stringify(value), // serialize value to string in terminal
})),
initial: 0,
},
questionHandlers,
);
const projectConfig = JSON.parse(communityProjectConfig);
program.communityProjectConfig = projectConfig;
preferences.communityProjectConfig = projectConfig;
return; // early return - no further questions needed for community projects
}
if (program.template === "llamapack") {
const availableLlamaPacks = await getAvailableLlamapackOptions();
const { llamapack } = await prompts(
{
type: "select",
name: "llamapack",
message: "Select LlamaPack",
choices: availableLlamaPacks.map((pack) => ({
title: pack.name,
value: pack.folderPath,
})),
initial: 0,
},
questionHandlers,
);
program.llamapack = llamapack;
preferences.llamapack = llamapack;
await askPostInstallAction();
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "extractor") {
// Extractor template only supports FastAPI, empty data sources, and llamacloud
// So we just use example file for extractor template, this allows user to choose vector database later
program.dataSources = [EXAMPLE_FILE];
program.framework = preferences.framework = "fastapi";
}
if (!program.framework) {
if (ciInfo.isCI) {
program.framework = getPrefOrDefault("framework");
} else {
const choices = [
{ title: "NextJS", value: "nextjs" },
{ title: "Express", value: "express" },
{ title: "FastAPI (Python)", value: "fastapi" },
];
const { framework } = await prompts(
{
type: "select",
name: "framework",
message: "Which framework would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.framework = framework;
preferences.framework = framework;
}
}
if (
(program.framework === "express" || program.framework === "fastapi") &&
(program.template === "streaming" || program.template === "multiagent")
) {
// if a backend-only framework is selected, ask whether we should create a frontend
if (program.frontend === undefined) {
if (ciInfo.isCI) {
program.frontend = getPrefOrDefault("frontend");
} else {
const styledNextJS = blue("NextJS");
const styledBackend = green(
program.framework === "express"
? "Express "
: program.framework === "fastapi"
? "FastAPI (Python) "
: "",
);
const { frontend } = await prompts({
onState: onPromptState,
type: "toggle",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
initial: getPrefOrDefault("frontend"),
active: "Yes",
inactive: "No",
});
program.frontend = Boolean(frontend);
preferences.frontend = Boolean(frontend);
}
}
} else {
program.frontend = false;
}
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = defaults.ui;
}
}
if (!program.observability && program.template === "streaming") {
if (ciInfo.isCI) {
program.observability = getPrefOrDefault("observability");
} else {
const { observability } = await prompts(
{
type: "select",
name: "observability",
message: "Would you like to set up observability?",
choices: [
{ title: "No", value: "none" },
...(program.framework === "fastapi"
? [{ title: "LlamaTrace", value: "llamatrace" }]
: []),
{ title: "Traceloop", value: "traceloop" },
],
initial: 0,
},
questionHandlers,
);
program.observability = observability;
preferences.observability = observability;
}
}
if (!program.modelConfig) {
const modelConfig = await askModelConfig({
openAiKey,
askModels: program.askModels ?? false,
framework: program.framework,
});
program.modelConfig = modelConfig;
preferences.modelConfig = modelConfig;
}
if (!program.dataSources) {
if (ciInfo.isCI) {
program.dataSources = getPrefOrDefault("dataSources");
} else {
program.dataSources = [];
// continue asking user for data sources if none are initially provided
while (true) {
const firstQuestion = program.dataSources.length === 0;
const choices = getDataSourceChoices(
program.framework,
program.dataSources,
program.template,
);
if (choices.length === 0) break;
const { selectedSource } = await prompts(
{
type: "select",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices,
initial: firstQuestion ? 1 : 0,
},
questionHandlers,
);
if (selectedSource === "no" || selectedSource === "none") {
// user doesn't want another data source or any data source
break;
}
switch (selectedSource) {
case "exampleFile": {
program.dataSources.push(EXAMPLE_FILE);
break;
}
case "file":
case "folder": {
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "file",
config: {
path: p,
},
});
}
break;
}
case "web": {
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
},
questionHandlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
break;
}
case "db": {
const dbPrompts: prompts.PromptObject<string>[] = [
{
type: "text",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
validate: (value: string) => {
if (!value) {
return "Please provide a valid connection string";
} else if (
!(
value.startsWith("mysql+pymysql://") ||
value.startsWith("postgresql+psycopg://")
)
) {
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
}
return true;
},
},
// Only ask for a query, user can provide more complex queries in the config file later
{
type: (prev) => (prev ? "text" : null),
name: "queries",
message: "Please enter the SQL query to fetch data:",
initial: "SELECT * FROM mytable",
},
];
program.dataSources.push({
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
break;
}
case "llamacloud": {
program.dataSources.push({
type: "llamacloud",
config: {},
});
program.dataSources.push(EXAMPLE_FILE);
break;
}
}
}
}
}
const isUsingLlamaCloud = program.dataSources.some(
(ds) => ds.type === "llamacloud",
);
// Asking for LlamaParse if user selected file data source
if (isUsingLlamaCloud) {
// default to use LlamaParse if using LlamaCloud
program.useLlamaParse = preferences.useLlamaParse = true;
} else {
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
if (
program.useLlamaParse === undefined &&
program.template !== "extractor"
) {
// if already set useLlamaParse, don't ask again
if (program.dataSources.some((ds) => ds.type === "file")) {
if (ciInfo.isCI) {
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
} else {
const { useLlamaParse } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
active: "yes",
inactive: "no",
},
questionHandlers,
);
program.useLlamaParse = useLlamaParse;
preferences.useLlamaParse = useLlamaParse;
}
}
}
}
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
if (isUsingLlamaCloud || program.useLlamaParse) {
if (!program.llamaCloudKey) {
// if already set, don't ask again
if (ciInfo.isCI) {
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
} else {
// Ask for LlamaCloud API key
const { llamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
program.llamaCloudKey = preferences.llamaCloudKey =
llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
}
}
if (isUsingLlamaCloud) {
// When using a LlamaCloud index, don't ask for vector database and use code in `llamacloud` folder for vector database
const vectorDb = "llamacloud";
program.vectorDb = vectorDb;
preferences.vectorDb = vectorDb;
} else if (program.dataSources.length > 0 && !program.vectorDb) {
if (ciInfo.isCI) {
program.vectorDb = getPrefOrDefault("vectorDb");
} else {
const { vectorDb } = await prompts(
{
type: "select",
name: "vectorDb",
message: "Would you like to use a vector database?",
choices: getVectorDbChoices(program.framework),
initial: 0,
},
questionHandlers,
);
program.vectorDb = vectorDb;
preferences.vectorDb = vectorDb;
}
}
if (
!program.tools &&
(program.template === "streaming" || program.template === "multiagent")
) {
if (ciInfo.isCI) {
program.tools = getPrefOrDefault("tools");
} else {
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
value: tool.name,
}));
const { toolsName } = await prompts({
type: "multiselect",
name: "toolsName",
message:
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
choices: toolChoices,
});
const tools = toolsName?.map((tool: string) =>
supportedTools.find((t) => t.name === tool),
);
program.tools = tools;
preferences.tools = tools;
}
}
await askPostInstallAction();
};
export const toChoice = (value: string) => {
return { title: value, value };
};
+30
View File
@@ -0,0 +1,30 @@
import { askModelConfig } from "../helpers/providers";
import { QuestionArgs, QuestionResults } from "./types";
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: "",
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
tools: [],
};
export async function getCIQuestionResults(
program: QuestionArgs,
): Promise<QuestionResults> {
return {
...defaults,
...program,
modelConfig: await askModelConfig({
openAiKey: program.openAiKey,
askModels: false,
framework: program.framework,
}),
};
}
+64
View File
@@ -0,0 +1,64 @@
import {
TemplateDataSource,
TemplateFramework,
TemplateType,
} from "../helpers";
import { supportedContextFileTypes } from "./utils";
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
template?: TemplateType,
) => {
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No datasource",
value: "none",
});
choices.push({
title:
process.platform !== "linux"
? "Use an example PDF"
: "Use an example PDF (you can add your own data files later)",
value: "exampleFile",
});
}
// Linux has many distros so we won't support file/folder picker for now
if (process.platform !== "linux") {
choices.push(
{
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
value: "file",
},
{
title:
process.platform === "win32"
? "Use a local folder"
: "Use local folders",
value: "folder",
},
);
}
if (framework === "fastapi" && template !== "extractor") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
return choices;
};
+18
View File
@@ -0,0 +1,18 @@
import ciInfo from "ci-info";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
import { QuestionArgs, QuestionResults } from "./types";
export const askQuestions = async (
args: QuestionArgs,
): Promise<QuestionResults> => {
if (ciInfo.isCI) {
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
await askProQuestions(args);
return args as unknown as QuestionResults;
}
return await askSimpleQuestions(args);
};
+404
View File
@@ -0,0 +1,404 @@
import { blue, green } from "picocolors";
import prompts from "prompts";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
import { EXAMPLE_FILE } from "../helpers/datasources";
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
import { askModelConfig } from "../helpers/providers";
import { getProjectOptions } from "../helpers/repo";
import { supportedTools, toolRequiresConfig } from "../helpers/tools";
import { getDataSourceChoices } from "./datasources";
import { getVectorDbChoices } from "./stores";
import { QuestionArgs } from "./types";
import {
askPostInstallAction,
onPromptState,
questionHandlers,
selectLocalContextData,
} from "./utils";
export const askProQuestions = async (program: QuestionArgs) => {
if (!program.template) {
const styledRepo = blue(
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
);
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
{
title: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ title: "Structured Extractor", value: "extractor" },
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
],
initial: 0,
},
questionHandlers,
);
program.template = template;
}
if (program.template === "community") {
const projectOptions = await getProjectOptions(
COMMUNITY_OWNER,
COMMUNITY_REPO,
);
const { communityProjectConfig } = await prompts(
{
type: "select",
name: "communityProjectConfig",
message: "Select community template",
choices: projectOptions.map(({ title, value }) => ({
title,
value: JSON.stringify(value), // serialize value to string in terminal
})),
initial: 0,
},
questionHandlers,
);
const projectConfig = JSON.parse(communityProjectConfig);
program.communityProjectConfig = projectConfig;
return; // early return - no further questions needed for community projects
}
if (program.template === "llamapack") {
const availableLlamaPacks = await getAvailableLlamapackOptions();
const { llamapack } = await prompts(
{
type: "select",
name: "llamapack",
message: "Select LlamaPack",
choices: availableLlamaPacks.map((pack) => ({
title: pack.name,
value: pack.folderPath,
})),
initial: 0,
},
questionHandlers,
);
program.llamapack = llamapack;
if (!program.postInstallAction) {
program.postInstallAction = await askPostInstallAction(program);
}
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "extractor") {
// Extractor template only supports FastAPI, empty data sources, and llamacloud
// So we just use example file for extractor template, this allows user to choose vector database later
program.dataSources = [EXAMPLE_FILE];
program.framework = "fastapi";
}
if (!program.framework) {
const choices = [
{ title: "NextJS", value: "nextjs" },
{ title: "Express", value: "express" },
{ title: "FastAPI (Python)", value: "fastapi" },
];
const { framework } = await prompts(
{
type: "select",
name: "framework",
message: "Which framework would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.framework = framework;
}
if (
(program.framework === "express" || program.framework === "fastapi") &&
(program.template === "streaming" || program.template === "multiagent")
) {
// if a backend-only framework is selected, ask whether we should create a frontend
if (program.frontend === undefined) {
const styledNextJS = blue("NextJS");
const styledBackend = green(
program.framework === "express"
? "Express "
: program.framework === "fastapi"
? "FastAPI (Python) "
: "",
);
const { frontend } = await prompts({
onState: onPromptState,
type: "toggle",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
initial: false,
active: "Yes",
inactive: "No",
});
program.frontend = Boolean(frontend);
}
} else {
program.frontend = false;
}
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = "shadcn";
}
}
if (!program.observability && program.template === "streaming") {
const { observability } = await prompts(
{
type: "select",
name: "observability",
message: "Would you like to set up observability?",
choices: [
{ title: "No", value: "none" },
...(program.framework === "fastapi"
? [{ title: "LlamaTrace", value: "llamatrace" }]
: []),
{ title: "Traceloop", value: "traceloop" },
],
initial: 0,
},
questionHandlers,
);
program.observability = observability;
}
if (!program.modelConfig) {
const modelConfig = await askModelConfig({
openAiKey: program.openAiKey,
askModels: program.askModels ?? false,
framework: program.framework,
});
program.modelConfig = modelConfig;
}
if (!program.vectorDb) {
const { vectorDb } = await prompts(
{
type: "select",
name: "vectorDb",
message: "Would you like to use a vector database?",
choices: getVectorDbChoices(program.framework),
initial: 0,
},
questionHandlers,
);
program.vectorDb = vectorDb;
}
if (program.vectorDb === "llamacloud") {
// When using a LlamaCloud index, don't ask for data sources just copy an example file
program.dataSources = [EXAMPLE_FILE];
}
if (!program.dataSources) {
program.dataSources = [];
// continue asking user for data sources if none are initially provided
while (true) {
const firstQuestion = program.dataSources.length === 0;
const choices = getDataSourceChoices(
program.framework,
program.dataSources,
program.template,
);
if (choices.length === 0) break;
const { selectedSource } = await prompts(
{
type: "select",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices,
initial: firstQuestion ? 1 : 0,
},
questionHandlers,
);
if (selectedSource === "no" || selectedSource === "none") {
// user doesn't want another data source or any data source
break;
}
switch (selectedSource) {
case "exampleFile": {
program.dataSources.push(EXAMPLE_FILE);
break;
}
case "file":
case "folder": {
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "file",
config: {
path: p,
},
});
}
break;
}
case "web": {
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
},
questionHandlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
break;
}
case "db": {
const dbPrompts: prompts.PromptObject<string>[] = [
{
type: "text",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
validate: (value: string) => {
if (!value) {
return "Please provide a valid connection string";
} else if (
!(
value.startsWith("mysql+pymysql://") ||
value.startsWith("postgresql+psycopg://")
)
) {
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
}
return true;
},
},
// Only ask for a query, user can provide more complex queries in the config file later
{
type: (prev) => (prev ? "text" : null),
name: "queries",
message: "Please enter the SQL query to fetch data:",
initial: "SELECT * FROM mytable",
},
];
program.dataSources.push({
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
break;
}
}
}
}
const isUsingLlamaCloud = program.vectorDb === "llamacloud";
// Asking for LlamaParse if user selected file data source
if (isUsingLlamaCloud) {
// default to use LlamaParse if using LlamaCloud
program.useLlamaParse = true;
} else {
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
if (
program.useLlamaParse === undefined &&
program.template !== "extractor"
) {
// if already set useLlamaParse, don't ask again
if (program.dataSources.some((ds) => ds.type === "file")) {
const { useLlamaParse } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
active: "Yes",
inactive: "No",
},
questionHandlers,
);
program.useLlamaParse = useLlamaParse;
}
}
}
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
if (isUsingLlamaCloud || program.useLlamaParse) {
if (!program.llamaCloudKey) {
// if already set, don't ask again
// Ask for LlamaCloud API key
const { llamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
program.llamaCloudKey = llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
}
if (
!program.tools &&
(program.template === "streaming" || program.template === "multiagent")
) {
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
value: tool.name,
}));
const { toolsName } = await prompts({
type: "multiselect",
name: "toolsName",
message:
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
choices: toolChoices,
});
const tools = toolsName?.map((tool: string) =>
supportedTools.find((t) => t.name === tool),
);
program.tools = tools;
}
if (!program.postInstallAction) {
program.postInstallAction = await askPostInstallAction(program);
}
};
+177
View File
@@ -0,0 +1,177 @@
import prompts from "prompts";
import { EXAMPLE_FILE } 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 =
| "rag"
| "code_artifact"
| "multiagent"
| "extractor"
| "data_scientist";
type SimpleAnswers = {
appType: AppType;
language: TemplateFramework;
useLlamaCloud: boolean;
llamaCloudKey?: string;
};
export const askSimpleQuestions = async (
args: PureQuestionArgs,
): Promise<QuestionResults> => {
const { appType } = await prompts(
{
type: "select",
name: "appType",
message: "What app do you want to build?",
choices: [
{ title: "Agentic RAG", value: "rag" },
{ title: "Data Scientist", value: "data_scientist" },
{ title: "Code Artifact Agent", value: "code_artifact" },
{ title: "Multi-Agent Report Gen", value: "multiagent" },
{ title: "Structured extraction", value: "extractor" },
],
},
questionHandlers,
);
let language: TemplateFramework = "fastapi";
let llamaCloudKey = args.llamaCloudKey;
let useLlamaCloud = false;
if (appType !== "extractor") {
const { language: newLanguage } = await prompts(
{
type: "select",
name: "language",
message: "What language do you want to use?",
choices: [
{ title: "Python (FastAPI)", value: "fastapi" },
{ title: "Typescript (NextJS)", value: "nextjs" },
],
},
questionHandlers,
);
language = newLanguage;
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
{
type: "toggle",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud services?",
initial: false,
active: "Yes",
inactive: "No",
hint: "see https://www.llamaindex.ai/enterprise for more info",
},
questionHandlers,
);
useLlamaCloud = newUseLlamaCloud;
if (useLlamaCloud && !llamaCloudKey) {
// Ask for LlamaCloud API key, if not set
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
}
const results = await convertAnswers(args, {
appType,
language,
useLlamaCloud,
llamaCloudKey,
});
results.postInstallAction = await askPostInstallAction(results);
return results;
};
const convertAnswers = async (
args: PureQuestionArgs,
answers: SimpleAnswers,
): Promise<QuestionResults> => {
const MODEL_GPT4o: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4o",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
return !!args.openAiKey;
},
};
const lookup: Record<
AppType,
Pick<QuestionResults, "template" | "tools" | "frontend" | "dataSources"> & {
modelConfig?: ModelConfig;
}
> = {
rag: {
template: "streaming",
tools: getTools(["duckduckgo"]),
frontend: true,
dataSources: [EXAMPLE_FILE],
},
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,
},
multiagent: {
template: "multiagent",
tools: getTools([
"document_generator",
"wikipedia.WikipediaToolSpec",
"duckduckgo",
"img_gen",
]),
frontend: true,
dataSources: [EXAMPLE_FILE],
},
extractor: {
template: "extractor",
tools: [],
frontend: false,
dataSources: [EXAMPLE_FILE],
},
};
const results = lookup[answers.appType];
return {
framework: answers.language,
ui: "shadcn",
llamaCloudKey: answers.llamaCloudKey,
useLlamaParse: answers.useLlamaCloud,
llamapack: "",
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
observability: "none",
...results,
modelConfig:
results.modelConfig ??
(await askModelConfig({
openAiKey: args.openAiKey,
askModels: args.askModels ?? false,
framework: answers.language,
})),
frontend: answers.language === "nextjs" ? false : results.frontend,
};
};
+36
View File
@@ -0,0 +1,36 @@
import fs from "fs";
import path from "path";
import { TemplateFramework } from "../helpers";
import { templatesDir } from "../helpers/dir";
export const getVectorDbChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No, just store the data in the file system",
value: "none",
},
{ title: "MongoDB", value: "mongo" },
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
{ title: "ChromaDB", value: "chroma" },
{ title: "Weaviate", value: "weaviate" },
{ title: "LlamaCloud (use Managed Index)", value: "llamacloud" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
const compPath = path.join(templatesDir, "components");
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
const availableChoices = fs
.readdirSync(vectordbPath)
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
const displayedChoices = choices.filter((choice) =>
availableChoices.includes(choice.value),
);
return displayedChoices;
};
+15
View File
@@ -0,0 +1,15 @@
import { InstallAppArgs } from "../create-app";
export type QuestionResults = Omit<
InstallAppArgs,
"appPath" | "packageManager" | "externalPort"
>;
export type PureQuestionArgs = {
askModels?: boolean;
pro?: boolean;
openAiKey?: string;
llamaCloudKey?: string;
};
export type QuestionArgs = QuestionResults & PureQuestionArgs;
+178
View File
@@ -0,0 +1,178 @@
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import { red } from "picocolors";
import prompts from "prompts";
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
import { toolsRequireConfig } from "../helpers/tools";
import { QuestionResults } from "./types";
export const supportedContextFileTypes = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
];
const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.windows.forms
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$dialogResult = $folderBrowser.ShowDialog()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
{
$folderBrowser.SelectedPath
}
`;
export const selectLocalContextData = async (type: TemplateDataSourceType) => {
try {
let selectedPath: string = "";
let execScript: string;
let execOpts: any = {};
switch (process.platform) {
case "win32": // Windows
execScript =
type === "file"
? WINDOWS_FILE_SELECTION_SCRIPT
: WINDOWS_FOLDER_SELECTION_SCRIPT;
execOpts = { shell: "powershell.exe" };
break;
case "darwin": // MacOS
execScript =
type === "file"
? MACOS_FILE_SELECTION_SCRIPT
: MACOS_FOLDER_SELECTION_SCRIPT;
break;
default: // Unsupported OS
console.log(red("Unsupported OS error!"));
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
),
);
process.exit(1);
}
}
return paths;
} catch (error) {
console.log(
red(
"Got an error when trying to select local context data! Please try again or select another data source option.",
),
);
process.exit(1);
}
};
export const onPromptState = (state: any) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write("\x1B[?25h");
process.stdout.write("\n");
process.exit(1);
}
};
export const toChoice = (value: string) => {
return { title: value, value };
};
export const questionHandlers = {
onCancel: () => {
console.error("Exiting.");
process.exit(1);
},
};
// Ask for next action after installation
export async function askPostInstallAction(
args: QuestionResults,
): Promise<TemplatePostInstallAction> {
const actionChoices = [
{
title: "Just generate code (~1 sec)",
value: "none",
},
{
title: "Start in VSCode (~1 sec)",
value: "VSCode",
},
{
title: "Generate code and install dependencies (~2 min)",
value: "dependencies",
},
];
const modelConfigured = !args.llamapack && args.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = args.useLlamaParse
? args.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = args.vectorDb && args.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(args.tools)
) {
actionChoices.push({
title: "Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await prompts(
{
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: actionChoices,
initial: 1,
},
questionHandlers,
);
return action;
}
@@ -1,17 +1,19 @@
import os
from typing import List
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from llama_index.core.agent import AgentRunner
from llama_index.core.callbacks import CallbackManager
from llama_index.core.settings import Settings
from llama_index.core.tools import BaseTool
from llama_index.core.tools.query_engine import QueryEngineTool
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
top_k = int(os.getenv("TOP_K", 0))
tools = []
tools: List[BaseTool] = []
callback_manager = CallbackManager(handlers=event_handlers or [])
# Add query tool if index exists
@@ -25,7 +27,8 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
tools.append(query_engine_tool)
# Add additional tools
tools += ToolFactory.from_env()
configured_tools: List[BaseTool] = ToolFactory.from_env()
tools.extend(configured_tools)
return AgentRunner.from_llm(
llm=Settings.llm,
@@ -1,7 +1,8 @@
import importlib
import os
from typing import Dict, List, Union
import yaml
import yaml # type: ignore
from llama_index.core.tools.function_tool import FunctionTool
from llama_index.core.tools.tool_spec.base import BaseToolSpec
@@ -17,7 +18,8 @@ class ToolFactory:
ToolType.LOCAL: "app.engine.tools",
}
def load_tools(tool_type: str, tool_name: str, config: dict) -> list[FunctionTool]:
@staticmethod
def load_tools(tool_type: str, tool_name: str, config: dict) -> List[FunctionTool]:
source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type]
try:
if "ToolSpec" in tool_name:
@@ -43,24 +45,32 @@ class ToolFactory:
@staticmethod
def from_env(
map_result: bool = False,
) -> list[FunctionTool] | dict[str, FunctionTool]:
) -> Union[Dict[str, List[FunctionTool]], List[FunctionTool]]:
"""
Load tools from the configured file.
Params:
- use_map: if True, return map of tool name and the tool itself
Args:
map_result: If True, return a map of tool names to their corresponding tools.
Returns:
A dictionary of tool names to lists of FunctionTools if map_result is True,
otherwise a list of FunctionTools.
"""
if map_result:
tools = {}
else:
tools = []
tools: Union[Dict[str, List[FunctionTool]], List[FunctionTool]] = (
{} if map_result else []
)
if os.path.exists("config/tools.yaml"):
with open("config/tools.yaml", "r") as f:
tool_configs = yaml.safe_load(f)
for tool_type, config_entries in tool_configs.items():
for tool_name, config in config_entries.items():
tool = ToolFactory.load_tools(tool_type, tool_name, config)
loaded_tools = ToolFactory.load_tools(
tool_type, tool_name, config
)
if map_result:
tools[tool_name] = tool
tools[tool_name] = loaded_tools # type: ignore
else:
tools.extend(tool)
tools.extend(loaded_tools) # type: ignore
return tools
@@ -66,31 +66,42 @@ class CodeGeneratorTool:
def __init__(self):
pass
def artifact(self, query: str, old_code: Optional[str] = None) -> Dict:
"""Generate a code artifact based on the input.
def artifact(
self,
query: str,
sandbox_files: Optional[List[str]] = None,
old_code: Optional[str] = None,
) -> Dict:
"""Generate a code artifact based on the provided input.
Args:
query (str): The description of the application you want to build.
query (str): A description of the application you want to build.
sandbox_files (Optional[List[str]], optional): A list of sandbox file paths. Defaults to None. Include these files if the code requires them.
old_code (Optional[str], optional): The existing code to be modified. Defaults to None.
Returns:
Dict: A dictionary containing the generated artifact information.
Dict: A dictionary containing information about the generated artifact.
"""
if old_code:
user_message = f"{query}\n\nThe existing code is: \n```\n{old_code}\n```"
else:
user_message = query
if sandbox_files:
user_message += f"\n\nThe provided files are: \n{str(sandbox_files)}"
messages: List[ChatMessage] = [
ChatMessage(role="system", content=CODE_GENERATION_PROMPT),
ChatMessage(role="user", content=user_message),
]
try:
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact)
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact) # type: ignore
response = sllm.chat(messages)
data: CodeArtifact = response.raw
return data.model_dump()
data_dict = data.model_dump()
if sandbox_files:
data_dict["files"] = sandbox_files
return data_dict
except Exception as e:
logger.error(f"Failed to generate artifact: {str(e)}")
raise e
@@ -21,14 +21,15 @@ def duckduckgo_search(
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
)
params = {
"keywords": query,
"region": region,
"max_results": max_results,
}
results = []
with DDGS() as ddg:
results = list(ddg.text(**params))
results = list(
ddg.text(
keywords=query,
region=region,
max_results=max_results,
)
)
return results
@@ -51,13 +52,14 @@ def duckduckgo_image_search(
"duckduckgo_search package is required to use this function."
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
)
params = {
"keywords": query,
"region": region,
"max_results": max_results,
}
with DDGS() as ddg:
results = list(ddg.images(**params))
results = list(
ddg.images(
keywords=query,
region=region,
max_results=max_results,
)
)
return results
@@ -2,14 +2,15 @@ import base64
import logging
import os
import uuid
from typing import Dict, List, Optional
from typing import List, Optional
from app.engine.utils.file_helper import FileMetadata, save_file
from e2b_code_interpreter import CodeInterpreter
from e2b_code_interpreter.models import Logs
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel
logger = logging.getLogger(__name__)
logger = logging.getLogger("uvicorn")
class InterpreterExtraResult(BaseModel):
@@ -22,11 +23,14 @@ class InterpreterExtraResult(BaseModel):
class E2BToolOutput(BaseModel):
is_error: bool
logs: Logs
error_message: Optional[str] = None
results: List[InterpreterExtraResult] = []
retry_count: int = 0
class E2BCodeInterpreter:
output_dir = "output/tools"
uploaded_files_dir = "output/uploaded"
def __init__(self, api_key: str = None):
if api_key is None:
@@ -42,40 +46,43 @@ class E2BCodeInterpreter:
)
self.filesever_url_prefix = filesever_url_prefix
self.interpreter = CodeInterpreter(api_key=api_key)
self.interpreter = None
self.api_key = api_key
def __del__(self):
self.interpreter.close()
"""
Kill the interpreter when the tool is no longer in use
"""
if self.interpreter is not None:
self.interpreter.kill()
def get_output_path(self, filename: str) -> str:
# if output directory doesn't exist, create it
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir, exist_ok=True)
return os.path.join(self.output_dir, filename)
def _init_interpreter(self, sandbox_files: List[str] = []):
"""
Lazily initialize the interpreter.
"""
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
self.interpreter = CodeInterpreter(api_key=self.api_key)
if len(sandbox_files) > 0:
for file_path in sandbox_files:
file_name = os.path.basename(file_path)
local_file_path = os.path.join(self.uploaded_files_dir, file_name)
with open(local_file_path, "rb") as f:
content = f.read()
if self.interpreter and self.interpreter.files:
self.interpreter.files.write(file_path, content)
logger.info(f"Uploaded {len(sandbox_files)} files to sandbox")
def save_to_disk(self, base64_data: str, ext: str) -> Dict:
filename = f"{uuid.uuid4()}.{ext}" # generate a unique filename
def _save_to_disk(self, base64_data: str, ext: str) -> FileMetadata:
buffer = base64.b64decode(base64_data)
output_path = self.get_output_path(filename)
try:
with open(output_path, "wb") as file:
file.write(buffer)
except IOError as e:
logger.error(f"Failed to write to file {output_path}: {str(e)}")
raise e
filename = f"{uuid.uuid4()}.{ext}" # generate a unique filename
output_path = os.path.join(self.output_dir, filename)
logger.info(f"Saved file to {output_path}")
file_metadata = save_file(buffer, file_path=output_path)
return {
"outputPath": output_path,
"filename": filename,
}
return file_metadata
def get_file_url(self, filename: str) -> str:
return f"{self.filesever_url_prefix}/{self.output_dir}/{filename}"
def parse_result(self, result) -> List[InterpreterExtraResult]:
def _parse_result(self, result) -> List[InterpreterExtraResult]:
"""
The result could include multiple formats (e.g. png, svg, etc.) but encoded in base64
We save each result to disk and return saved file metadata (extension, filename, url)
@@ -92,16 +99,20 @@ class E2BCodeInterpreter:
for ext, data in zip(formats, results):
match ext:
case "png" | "svg" | "jpeg" | "pdf":
result = self.save_to_disk(data, ext)
filename = result["filename"]
file_metadata = self._save_to_disk(data, ext)
output.append(
InterpreterExtraResult(
type=ext,
filename=filename,
url=self.get_file_url(filename),
filename=file_metadata.name,
url=file_metadata.url,
)
)
case _:
# Try serialize data to string
try:
data = str(data)
except Exception as e:
data = f"Error when serializing data: {e}"
output.append(
InterpreterExtraResult(
type=ext,
@@ -114,28 +125,75 @@ class E2BCodeInterpreter:
return output
def interpret(self, code: str) -> E2BToolOutput:
def interpret(
self,
code: str,
sandbox_files: List[str] = [],
retry_count: int = 0,
) -> E2BToolOutput:
"""
Execute python code in a Jupyter notebook cell, the toll will return result, stdout, stderr, display_data, and error.
Execute Python code in a Jupyter notebook cell. The tool will return the result, stdout, stderr, display_data, and error.
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
You have a maximum of 3 retries to get the code to run successfully.
Parameters:
code (str): The python code to be executed in a single cell.
code (str): The Python code to be executed in a single cell.
sandbox_files (List[str]): List of local file paths to be used by the code. The tool will throw an error if a file is not found.
retry_count (int): Number of times the tool has been retried.
"""
logger.info(
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
)
exec = self.interpreter.notebook.exec_cell(code)
if retry_count > 2:
return E2BToolOutput(
is_error=True,
logs=Logs(
stdout="",
stderr="",
display_data="",
error="",
),
error_message="Failed to execute the code after 3 retries. Explain the error to the user and suggest a fix.",
retry_count=retry_count,
)
if exec.error:
logger.error("Error when executing code", exec.error)
output = E2BToolOutput(is_error=True, logs=exec.logs, results=[])
else:
if len(exec.results) == 0:
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
if self.interpreter is None:
self._init_interpreter(sandbox_files)
if self.interpreter and self.interpreter.notebook:
logger.info(
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
)
exec = self.interpreter.notebook.exec_cell(code)
if exec.error:
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
logger.error(error_message)
# Calling the generated code caused an error. Kill the interpreter and return the error to the LLM so it can try to fix the error
try:
self.interpreter.kill() # type: ignore
except Exception:
pass
finally:
self.interpreter = None
output = E2BToolOutput(
is_error=True,
logs=exec.logs,
results=[],
error_message=error_message,
retry_count=retry_count + 1,
)
else:
results = self.parse_result(exec.results[0])
output = E2BToolOutput(is_error=False, logs=exec.logs, results=results)
return output
if len(exec.results) == 0:
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
else:
results = self._parse_result(exec.results[0])
output = E2BToolOutput(
is_error=False,
logs=exec.logs,
results=results,
retry_count=retry_count + 1,
)
return output
else:
raise ValueError("Interpreter is not initialized.")
def get_tools(**kwargs):
@@ -43,6 +43,6 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
memory=memory,
system_prompt=system_prompt,
retriever=retriever,
node_postprocessors=node_postprocessors,
node_postprocessors=node_postprocessors, # type: ignore
callback_manager=callback_manager,
)
@@ -1,6 +1,6 @@
import {
BaseChatEngine,
BaseToolWithCall,
ChatEngine,
OpenAIAgent,
QueryEngineTool,
} from "llamaindex";
@@ -45,7 +45,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
const agent = new OpenAIAgent({
tools,
systemPrompt: process.env.SYSTEM_PROMPT,
}) as unknown as ChatEngine;
}) as unknown as BaseChatEngine;
return agent;
}
@@ -48,11 +48,13 @@ export type CodeArtifact = {
port: number | null;
file_path: string;
code: string;
files?: string[];
};
export type CodeGeneratorParameter = {
requirement: string;
oldCode?: string;
sandboxFiles?: string[];
};
export type CodeGeneratorToolParams = {
@@ -75,6 +77,15 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
description: "The existing code to be modified",
nullable: true,
},
sandboxFiles: {
type: "array",
description:
"A list of sandbox file paths. Include these files if the code requires them.",
items: {
type: "string",
},
nullable: true,
},
},
required: ["requirement"],
},
@@ -93,6 +104,9 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
input.requirement,
input.oldCode,
);
if (input.sandboxFiles) {
artifact.files = input.sandboxFiles;
}
return artifact as JSONValue;
} catch (error) {
return { isError: true };
@@ -7,6 +7,8 @@ import path from "node:path";
export type InterpreterParameter = {
code: string;
sandboxFiles?: string[];
retryCount?: number;
};
export type InterpreterToolParams = {
@@ -18,7 +20,9 @@ export type InterpreterToolParams = {
export type InterpreterToolOutput = {
isError: boolean;
logs: Logs;
text?: string;
extraResult: InterpreterExtraResult[];
retryCount?: number;
};
type InterpreterExtraType =
@@ -41,8 +45,10 @@ export type InterpreterExtraResult = {
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
name: "interpreter",
description:
"Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.",
description: `Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
You have a maximum of 3 retries to get the code to run successfully.
`,
parameters: {
type: "object",
properties: {
@@ -50,6 +56,21 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
type: "string",
description: "The python code to execute in a single cell.",
},
sandboxFiles: {
type: "array",
description:
"List of local file paths to be used by the code. The tool will throw an error if a file is not found.",
items: {
type: "string",
},
nullable: true,
},
retryCount: {
type: "number",
description: "The number of times the tool has been retried",
default: 0,
nullable: true,
},
},
required: ["code"],
},
@@ -57,6 +78,7 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
export class InterpreterTool implements BaseTool<InterpreterParameter> {
private readonly outputDir = "output/tools";
private readonly uploadedFilesDir = "output/uploaded";
private apiKey?: string;
private fileServerURLPrefix?: string;
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
@@ -80,33 +102,64 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
}
}
public async initInterpreter() {
public async initInterpreter(input: InterpreterParameter) {
if (!this.codeInterpreter) {
this.codeInterpreter = await CodeInterpreter.create({
apiKey: this.apiKey,
});
}
// upload files to sandbox
if (input.sandboxFiles) {
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
for (const filePath of input.sandboxFiles) {
const fileName = path.basename(filePath);
const localFilePath = path.join(this.uploadedFilesDir, fileName);
const content = fs.readFileSync(localFilePath);
await this.codeInterpreter?.files.write(filePath, content);
}
console.log(`Uploaded ${input.sandboxFiles.length} files to sandbox`);
}
return this.codeInterpreter;
}
public async codeInterpret(code: string): Promise<InterpreterToolOutput> {
public async codeInterpret(
input: InterpreterParameter,
): Promise<InterpreterToolOutput> {
console.log(
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
`Sandbox files: ${input.sandboxFiles}. Retry count: ${input.retryCount}`,
);
const interpreter = await this.initInterpreter();
const exec = await interpreter.notebook.execCell(code);
if (input.retryCount && input.retryCount >= 3) {
return {
isError: true,
logs: {
stdout: [],
stderr: [],
},
text: "Max retries reached",
extraResult: [],
};
}
console.log(
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${input.code}\n${"=".repeat(50)}`,
);
const interpreter = await this.initInterpreter(input);
const exec = await interpreter.notebook.execCell(input.code);
if (exec.error) console.error("[Code Interpreter error]", exec.error);
const extraResult = await this.getExtraResult(exec.results[0]);
const result: InterpreterToolOutput = {
isError: !!exec.error,
logs: exec.logs,
text: exec.text,
extraResult,
retryCount: input.retryCount ? input.retryCount + 1 : 1,
};
return result;
}
async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
const result = await this.codeInterpret(input.code);
const result = await this.codeInterpret(input);
return result;
}
@@ -1,3 +1,5 @@
import { Document } from "llamaindex";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { getExtractors } from "../../engine/loader";
@@ -5,23 +7,58 @@ import { getExtractors } from "../../engine/loader";
const MIME_TYPE_TO_EXT: Record<string, string> = {
"application/pdf": "pdf",
"text/plain": "txt",
"text/csv": "csv",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"docx",
};
const UPLOADED_FOLDER = "output/uploaded";
export type FileMetadata = {
id: string;
name: string;
url: string;
refs: string[];
};
export async function storeAndParseFile(
filename: string,
fileBuffer: Buffer,
mimeType: string,
): Promise<FileMetadata> {
const fileMetadata = await storeFile(filename, fileBuffer, mimeType);
const documents: Document[] = await parseFile(fileBuffer, filename, mimeType);
// Update document IDs in the file metadata
fileMetadata.refs = documents.map((document) => document.id_ as string);
return fileMetadata;
}
export async function storeFile(
filename: string,
fileBuffer: Buffer,
mimeType: string,
) {
const fileExt = MIME_TYPE_TO_EXT[mimeType];
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
const fileId = crypto.randomUUID();
const newFilename = `${fileId}_${sanitizeFileName(filename)}`;
const filepath = path.join(UPLOADED_FOLDER, newFilename);
const fileUrl = await saveDocument(filepath, fileBuffer);
return {
id: fileId,
name: newFilename,
url: fileUrl,
refs: [] as string[],
} as FileMetadata;
}
export async function parseFile(
fileBuffer: Buffer,
filename: string,
mimeType: string,
) {
const documents = await loadDocuments(fileBuffer, mimeType);
const filepath = path.join(UPLOADED_FOLDER, filename);
await saveDocument(filepath, fileBuffer);
for (const document of documents) {
document.metadata = {
...document.metadata,
@@ -48,12 +85,6 @@ export async function saveDocument(filepath: string, content: string | Buffer) {
if (path.isAbsolute(filepath)) {
throw new Error("Absolute file paths are not allowed.");
}
const fileName = path.basename(filepath);
if (!/^[a-zA-Z0-9_.-]+$/.test(fileName)) {
throw new Error(
"File name is not allowed to contain any special characters.",
);
}
if (!process.env.FILESERVER_URL_PREFIX) {
throw new Error("FILESERVER_URL_PREFIX environment variable is not set.");
}
@@ -71,3 +102,7 @@ export async function saveDocument(filepath: string, content: string | Buffer) {
console.log(`Saved document to ${filepath}. Reachable at URL: ${fileurl}`);
return fileurl;
}
function sanitizeFileName(fileName: string) {
return fileName.replace(/[^a-zA-Z0-9_.-]/g, "_");
}
@@ -7,7 +7,7 @@ import {
} from "llamaindex";
export async function runPipeline(
currentIndex: VectorStoreIndex,
currentIndex: VectorStoreIndex | null,
documents: Document[],
) {
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
@@ -21,8 +21,18 @@ export async function runPipeline(
],
});
const nodes = await pipeline.run({ documents });
await currentIndex.insertNodes(nodes);
currentIndex.storageContext.docStore.persist();
console.log("Added nodes to the vector store.");
return documents.map((document) => document.id_);
if (currentIndex) {
await currentIndex.insertNodes(nodes);
currentIndex.storageContext.docStore.persist();
console.log("Added nodes to the vector store.");
return documents.map((document) => document.id_);
} else {
// Initialize a new index with the documents
const newIndex = await VectorStoreIndex.fromDocuments(documents);
newIndex.storageContext.docStore.persist();
console.log(
"Got empty index, created new index with the uploaded documents",
);
return documents.map((document) => document.id_);
}
}
@@ -1,32 +1,70 @@
import { LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { storeAndParseFile } from "./helper";
import fs from "node:fs/promises";
import path from "node:path";
import { FileMetadata, parseFile, storeFile } from "./helper";
import { runPipeline } from "./pipeline";
export async function uploadDocument(
index: VectorStoreIndex | LlamaCloudIndex,
index: VectorStoreIndex | LlamaCloudIndex | null,
filename: string,
raw: string,
): Promise<string[]> {
): Promise<FileMetadata> {
const [header, content] = raw.split(",");
const mimeType = header.replace("data:", "").replace(";base64", "");
const fileBuffer = Buffer.from(content, "base64");
// Store file
const fileMetadata = await storeFile(filename, fileBuffer, mimeType);
// If the file is csv and has codeExecutorTool, we don't need to index the file.
if (mimeType === "text/csv" && (await hasCodeExecutorTool())) {
return fileMetadata;
}
if (index instanceof LlamaCloudIndex) {
// trigger LlamaCloudIndex API to upload the file and run the pipeline
const projectId = await index.getProjectId();
const pipelineId = await index.getPipelineId();
return [
await LLamaCloudFileService.addFileToPipeline(
try {
const documentId = await LLamaCloudFileService.addFileToPipeline(
projectId,
pipelineId,
new File([fileBuffer], filename, { type: mimeType }),
{ private: "true" },
),
];
);
// Update file metadata with document IDs
fileMetadata.refs = [documentId];
return fileMetadata;
} 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;
}
}
// run the pipeline for other vector store indexes
const documents = await storeAndParseFile(filename, fileBuffer, mimeType);
return runPipeline(index, documents);
const documents: Document[] = await parseFile(fileBuffer, filename, mimeType);
// Update file metadata with document IDs
fileMetadata.refs = documents.map((document) => document.id_ as string);
// Run the pipeline
await runPipeline(index, documents);
return fileMetadata;
}
const hasCodeExecutorTool = async () => {
const codeExecutorTools = ["interpreter", "artifact"];
const configFile = path.join("config", "tools.json");
const toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
const localTools = toolConfig.local || {};
// Check if local tools contains codeExecutorTools
return codeExecutorTools.some((tool) => localTools[tool] !== undefined);
};
@@ -3,17 +3,17 @@ import { MessageContent, MessageContentDetail } from "llamaindex";
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
export type DocumentFileContent = {
type: "ref" | "text";
value: string[] | string;
export type UploadedFileMeta = {
id: string;
name: string;
url?: string;
refs?: string[];
};
export type DocumentFile = {
id: string;
filename: string;
filesize: number;
filetype: DocumentFileType;
content: DocumentFileContent;
type: DocumentFileType;
url: string;
metadata: UploadedFileMeta;
};
type Annotation = {
@@ -29,28 +29,25 @@ export function isValidMessages(messages: Message[]): boolean {
export function retrieveDocumentIds(messages: Message[]): string[] {
// retrieve document Ids from the annotations of all messages (if any)
const documentFiles = retrieveDocumentFiles(messages);
return documentFiles.map((file) => file.metadata?.refs || []).flat();
}
export function retrieveDocumentFiles(messages: Message[]): DocumentFile[] {
const annotations = getAllAnnotations(messages);
if (annotations.length === 0) return [];
const ids: string[] = [];
const files: DocumentFile[] = [];
for (const { type, data } of annotations) {
if (
type === "document_file" &&
"files" in data &&
Array.isArray(data.files)
) {
const files = data.files as DocumentFile[];
for (const file of files) {
if (Array.isArray(file.content.value)) {
// it's an array, so it's an array of doc IDs
ids.push(...file.content.value);
}
}
files.push(...data.files);
}
}
return ids;
return files;
}
export function retrieveMessageContent(messages: Message[]): MessageContent {
@@ -65,6 +62,36 @@ export function retrieveMessageContent(messages: Message[]): MessageContent {
];
}
function getFileContent(file: DocumentFile): string {
const fileMetadata = file.metadata;
let defaultContent = `=====File: ${fileMetadata.name}=====\n`;
// Include file URL if it's available
const urlPrefix = process.env.FILESERVER_URL_PREFIX;
let urlContent = "";
if (urlPrefix) {
if (fileMetadata.url) {
urlContent = `File URL: ${fileMetadata.url}\n`;
} else {
urlContent = `File URL (instruction: do not update this file URL yourself): ${urlPrefix}/output/uploaded/${fileMetadata.name}\n`;
}
} else {
console.warn(
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server",
);
}
defaultContent += urlContent;
// Include document IDs if it's available
if (fileMetadata.refs) {
defaultContent += `Document IDs: ${fileMetadata.refs}\n`;
}
// Include sandbox file paths
const sandboxFilePath = `/tmp/${fileMetadata.name}`;
defaultContent += `Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): ${sandboxFilePath}\n`;
return defaultContent;
}
function getAllAnnotations(messages: Message[]): Annotation[] {
return messages.flatMap((message) =>
(message.annotations ?? []).map((annotation) =>
@@ -131,25 +158,11 @@ function convertAnnotations(messages: Message[]): MessageContentDetail[] {
"files" in data &&
Array.isArray(data.files)
) {
// get all CSV files and convert their whole content to one text message
// currently CSV files are the only files where we send the whole content - we don't use an index
const csvFiles: DocumentFile[] = data.files.filter(
(file: DocumentFile) => file.filetype === "csv",
);
if (csvFiles && csvFiles.length > 0) {
const csvContents = csvFiles.map((file: DocumentFile) => {
const fileContent = Array.isArray(file.content.value)
? file.content.value.join("\n")
: file.content.value;
return "```csv\n" + fileContent + "\n```";
});
const text =
"Use the following CSV content:\n" + csvContents.join("\n\n");
content.push({
type: "text",
text,
});
}
const fileContent = data.files.map(getFileContent).join("\n");
content.push({
type: "text",
text: fileContent,
});
}
});
@@ -172,3 +185,26 @@ function getValidAnnotation(annotation: JSONValue): Annotation {
}
return { type: annotation.type, data: annotation.data };
}
// validate and get all annotations of a specific type or role from the frontend messages
export function getAnnotations<
T extends Annotation["data"] = Annotation["data"],
>(
messages: Message[],
options?: {
role?: Message["role"]; // message role
type?: Annotation["type"]; // annotation type
},
): {
type: string;
data: T;
}[] {
const messagesByRole = options?.role
? messages.filter((msg) => msg.role === options?.role)
: messages;
const annotations = getAllAnnotations(messagesByRole);
const annotationsByType = options?.type
? annotations.filter((a) => a.type === options.type)
: annotations;
return annotationsByType as { type: string; data: T }[];
}
@@ -75,7 +75,7 @@ export function createCallbackManager(stream: StreamData) {
callbackManager.on("retrieve-end", (data) => {
const { nodes, query } = data.detail;
appendSourceData(stream, nodes);
appendEventData(stream, `Retrieving context for query: '${query}'`);
appendEventData(stream, `Retrieving context for query: '${query.query}'`);
appendEventData(
stream,
`Retrieved ${nodes.length} sources to use as context for the query`,
@@ -1,20 +1,22 @@
import logging
from typing import Any, Dict, List
import yaml
import yaml # type: ignore
from app.engine.loaders.db import DBLoaderConfig, get_db_documents
from app.engine.loaders.file import FileLoaderConfig, get_file_documents
from app.engine.loaders.web import WebLoaderConfig, get_web_documents
from llama_index.core import Document
logger = logging.getLogger(__name__)
def load_configs():
def load_configs() -> Dict[str, Any]:
with open("config/loaders.yaml") as f:
configs = yaml.safe_load(f)
return configs
def get_documents():
def get_documents() -> List[Document]:
documents = []
config = load_configs()
for loader_type, loader_config in config.items():
+8 -1
View File
@@ -1,5 +1,6 @@
import logging
from typing import List
from pydantic import BaseModel
logger = logging.getLogger(__name__)
@@ -11,7 +12,13 @@ class DBLoaderConfig(BaseModel):
def get_db_documents(configs: list[DBLoaderConfig]):
from llama_index.readers.database import DatabaseReader
try:
from llama_index.readers.database import DatabaseReader
except ImportError:
logger.error(
"Failed to import DatabaseReader. Make sure llama_index is installed."
)
raise
docs = []
for entry in configs:
+4 -2
View File
@@ -1,3 +1,5 @@
from typing import List, Optional
from pydantic import BaseModel, Field
@@ -8,8 +10,8 @@ class CrawlUrl(BaseModel):
class WebLoaderConfig(BaseModel):
driver_arguments: list[str] = Field(default=None)
urls: list[CrawlUrl]
driver_arguments: Optional[List[str]] = Field(default_factory=list)
urls: List[CrawlUrl]
def get_web_documents(config: WebLoaderConfig):
@@ -1,11 +1,10 @@
import logging
from app.api.routers.events import EventCallbackHandler
from app.api.routers.models import (
ChatData,
)
from app.api.routers.vercel_response import VercelStreamResponse
from app.engine import get_chat_engine
from app.engine.engine import get_chat_engine
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
chat_router = r = APIRouter()
@@ -23,13 +22,12 @@ async def chat(
last_message_content = data.get_last_message_content()
messages = data.get_history_messages(include_agent_messages=True)
event_handler = EventCallbackHandler()
# The chat API supports passing private document filters and chat params
# but agent workflow does not support them yet
# ignore chat params and use all documents for now
# TODO: generate filters based on doc_ids
# TODO: use chat params
engine = get_chat_engine(chat_history=messages)
params = data.data or {}
engine = get_chat_engine(chat_history=messages, params=params)
event_handler = engine.run(input=last_message_content, streaming=True)
return VercelStreamResponse(
@@ -1,6 +1,6 @@
import asyncio
import json
import logging
from abc import ABC
from typing import AsyncGenerator, List
from aiostream import stream
@@ -13,7 +13,7 @@ from fastapi.responses import StreamingResponse
logger = logging.getLogger("uvicorn")
class VercelStreamResponse(StreamingResponse, ABC):
class VercelStreamResponse(StreamingResponse):
"""
Base class to convert the response from the chat engine to the streaming format expected by Vercel
"""
@@ -23,26 +23,34 @@ class VercelStreamResponse(StreamingResponse, ABC):
def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs):
self.request = request
stream = self._create_stream(request, chat_data, *args, **kwargs)
content = self.content_generator(stream)
self.chat_data = chat_data
content = self.content_generator(*args, **kwargs)
super().__init__(content=content)
async def content_generator(self, stream):
async def content_generator(self, event_handler, events):
logger.info("Starting content_generator")
stream = self._create_stream(
self.request, self.chat_data, event_handler, events
)
is_stream_started = False
try:
async with stream.stream() as streamer:
async for output in streamer:
if not is_stream_started:
is_stream_started = True
# Stream a blank message to start the stream
yield self.convert_text("")
async with stream.stream() as streamer:
async for output in streamer:
if not is_stream_started:
is_stream_started = True
# Stream a blank message to start the stream
yield self.convert_text("")
yield output
if await self.request.is_disconnected():
break
yield output
except asyncio.CancelledError:
logger.info("Stopping workflow")
await event_handler.cancel_run()
except Exception as e:
logger.error(
f"Unexpected error in content_generator: {str(e)}", exc_info=True
)
finally:
logger.info("The stream has been stopped!")
def _create_stream(
self,
@@ -18,11 +18,11 @@ def get_chat_engine(
agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
match agent_type:
case "choreography":
agent = create_choreography(chat_history)
agent = create_choreography(chat_history, **kwargs)
case "orchestrator":
agent = create_orchestrator(chat_history)
agent = create_orchestrator(chat_history, **kwargs)
case _:
agent = create_workflow(chat_history)
agent = create_workflow(chat_history, **kwargs)
logger.info(f"Using agent pattern: {agent_type}")
@@ -8,8 +8,8 @@ from app.examples.researcher import create_researcher
from llama_index.core.chat_engine.types import ChatMessage
def create_choreography(chat_history: Optional[List[ChatMessage]] = None):
researcher = create_researcher(chat_history)
def create_choreography(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
researcher = create_researcher(chat_history, **kwargs)
publisher = create_publisher(chat_history)
reviewer = FunctionCallingAgent(
name="reviewer",
@@ -21,12 +21,14 @@ def create_choreography(chat_history: Optional[List[ChatMessage]] = None):
name="writer",
agents=[researcher, reviewer, publisher],
description="expert in writing blog posts, needs researched information and images to write a blog post",
system_prompt=dedent("""
system_prompt=dedent(
"""
You are an expert in writing blog posts. You are given a task to write a blog post. Before starting to write the post, consult the researcher agent to get the information you need. Don't make up any information yourself.
After creating a draft for the post, send it to the reviewer agent to receive feedback and make sure to incorporate the feedback from the reviewer.
You can consult the reviewer and researcher a maximum of two times. Your output should contain only the blog post.
Finally, always request the publisher to create a document (PDF, HTML) and publish the blog post.
"""),
"""
),
# TODO: add chat_history support to AgentCallingAgent
# chat_history=chat_history,
)
@@ -8,28 +8,32 @@ from app.examples.researcher import create_researcher
from llama_index.core.chat_engine.types import ChatMessage
def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None):
researcher = create_researcher(chat_history)
def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
researcher = create_researcher(chat_history, **kwargs)
writer = FunctionCallingAgent(
name="writer",
description="expert in writing blog posts, need information and images to write a post",
system_prompt=dedent("""
system_prompt=dedent(
"""
You are an expert in writing blog posts.
You are given a task to write a blog post. Do not make up any information yourself.
If you don't have the necessary information to write a blog post, reply "I need information about the topic to write the blog post".
If you need to use images, reply "I need images about the topic to write the blog post". Do not use any dummy images made up by you.
If you have all the information needed, write the blog post.
"""),
"""
),
chat_history=chat_history,
)
reviewer = FunctionCallingAgent(
name="reviewer",
description="expert in reviewing blog posts, needs a written blog post to review",
system_prompt=dedent("""
system_prompt=dedent(
"""
You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post and fix any issues found yourself. You must output a final blog post.
A post must include at least one valid image. If not, reply "I need images about the topic to write the blog post". An image URL starting with "example" or "your website" is not valid.
Especially check for logical inconsistencies and proofread the post for grammar and spelling errors.
"""),
"""
),
chat_history=chat_history,
)
publisher = create_publisher(chat_history)
@@ -3,17 +3,19 @@ from textwrap import dedent
from typing import List
from app.agents.single import FunctionCallingAgent
from app.engine.index import get_index
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.tools import QueryEngineTool, ToolMetadata
def _create_query_engine_tool() -> QueryEngineTool:
def _create_query_engine_tool(params=None) -> QueryEngineTool:
"""
Provide an agent worker that can be used to query the index.
"""
index = get_index()
# Add query tool if index exists
index_config = IndexConfig(**(params or {}))
index = get_index(index_config)
if index is None:
return None
top_k = int(os.getenv("TOP_K", 0))
@@ -31,13 +33,13 @@ def _create_query_engine_tool() -> QueryEngineTool:
)
def _get_research_tools() -> QueryEngineTool:
def _get_research_tools(**kwargs) -> QueryEngineTool:
"""
Researcher take responsibility for retrieving information.
Try init wikipedia or duckduckgo tool if available.
"""
tools = []
query_engine_tool = _create_query_engine_tool()
query_engine_tool = _create_query_engine_tool(**kwargs)
if query_engine_tool is not None:
tools.append(query_engine_tool)
researcher_tool_names = ["duckduckgo", "wikipedia.WikipediaToolSpec"]
@@ -48,16 +50,17 @@ def _get_research_tools() -> QueryEngineTool:
return tools
def create_researcher(chat_history: List[ChatMessage]):
def create_researcher(chat_history: List[ChatMessage], **kwargs):
"""
Researcher is an agent that take responsibility for using tools to complete a given task.
"""
tools = _get_research_tools()
tools = _get_research_tools(**kwargs)
return FunctionCallingAgent(
name="researcher",
tools=tools,
description="expert in retrieving any unknown content or searching for images from the internet",
system_prompt=dedent("""
system_prompt=dedent(
"""
You are a researcher agent. You are given a research task.
If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
@@ -77,6 +80,7 @@ def create_researcher(chat_history: List[ChatMessage]):
If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history."
"""),
"""
),
chat_history=chat_history,
)
@@ -17,9 +17,10 @@ from llama_index.core.workflow import (
)
def create_workflow(chat_history: Optional[List[ChatMessage]] = None):
def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
researcher = create_researcher(
chat_history=chat_history,
**kwargs,
)
publisher = create_publisher(
chat_history=chat_history,
@@ -127,7 +128,8 @@ class BlogPostWorkflow(Workflow):
self, input: str, chat_history: List[ChatMessage]
) -> str:
prompt_template = PromptTemplate(
dedent("""
dedent(
"""
You are an expert in decision-making, helping people write and publish blog posts.
If the user is asking for a file or to publish content, respond with 'publish'.
If the user requests to write or update a blog post, respond with 'not_publish'.
@@ -140,7 +142,8 @@ class BlogPostWorkflow(Workflow):
Given the chat history and the new user request, decide whether to publish based on existing information.
Decision (respond with either 'not_publish' or 'publish'):
""")
"""
)
)
chat_history_str = "\n".join(
@@ -1,13 +1,13 @@
import { StopEvent } from "@llamaindex/core/workflow";
import { Message, streamToResponse } from "ai";
import { Request, Response } from "express";
import { ChatMessage, ChatResponseChunk } from "llamaindex";
import { ChatResponseChunk } from "llamaindex";
import { createWorkflow } from "./workflow/factory";
import { toDataStream, workflowEventsToStreamData } from "./workflow/stream";
export const chat = async (req: Request, res: Response) => {
try {
const { messages }: { messages: Message[] } = req.body;
const { messages, data }: { messages: Message[]; data?: any } = req.body;
const userMessage = messages.pop();
if (!messages || !userMessage || userMessage.role !== "user") {
return res.status(400).json({
@@ -16,8 +16,7 @@ export const chat = async (req: Request, res: Response) => {
});
}
const chatHistory = messages as ChatMessage[];
const agent = createWorkflow(chatHistory);
const agent = createWorkflow(messages, data);
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
userMessage.content,
) as unknown as Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>;
@@ -1,7 +1,7 @@
import { initObservability } from "@/app/observability";
import { StopEvent } from "@llamaindex/core/workflow";
import { Message, StreamingTextResponse } from "ai";
import { ChatMessage, ChatResponseChunk } from "llamaindex";
import { ChatResponseChunk } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { initSettings } from "./engine/settings";
import { createWorkflow } from "./workflow/factory";
@@ -16,7 +16,7 @@ export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { messages }: { messages: Message[] } = body;
const { messages, data }: { messages: Message[]; data?: any } = body;
const userMessage = messages.pop();
if (!messages || !userMessage || userMessage.role !== "user") {
return NextResponse.json(
@@ -28,8 +28,7 @@ export async function POST(request: NextRequest) {
);
}
const chatHistory = messages as ChatMessage[];
const agent = createWorkflow(chatHistory);
const agent = createWorkflow(messages, data);
// TODO: fix type in agent.run in LITS
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
userMessage.content,
@@ -1,14 +1,19 @@
import { ChatMessage } from "llamaindex";
import { FunctionCallingAgent } from "./single-agent";
import { lookupTools } from "./tools";
import { getQueryEngineTool, lookupTools } from "./tools";
export const createResearcher = async (chatHistory: ChatMessage[]) => {
const tools = await lookupTools([
"query_index",
"wikipedia_tool",
"duckduckgo_search",
"image_generator",
]);
export const createResearcher = async (
chatHistory: ChatMessage[],
params?: any,
) => {
const queryEngineTool = await getQueryEngineTool(params);
const tools = (
await lookupTools([
"wikipedia_tool",
"duckduckgo_search",
"image_generator",
])
).concat(queryEngineTool ? [queryEngineTool] : []);
return new FunctionCallingAgent({
name: "researcher",
@@ -5,7 +5,9 @@ import {
Workflow,
WorkflowEvent,
} from "@llamaindex/core/workflow";
import { Message } from "ai";
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
import { getAnnotations } from "../llamaindex/streaming/annotations";
import {
createPublisher,
createResearcher,
@@ -25,19 +27,15 @@ class WriteEvent extends WorkflowEvent<{
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
class PublishEvent extends WorkflowEvent<{ input: string }> {}
const prepareChatHistory = (chatHistory: ChatMessage[]) => {
const prepareChatHistory = (chatHistory: Message[]): ChatMessage[] => {
// By default, the chat history only contains the assistant and user messages
// all the agents messages are stored in annotation data which is not visible to the LLM
const MAX_AGENT_MESSAGES = 10;
// Construct a new agent message from agent messages
// Get annotations from assistant messages
const agentAnnotations = chatHistory
.filter((msg) => msg.role === "assistant")
.flatMap((msg) => msg.annotations || [])
.filter((annotation) => annotation.type === "agent")
.slice(-MAX_AGENT_MESSAGES);
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
chatHistory,
{ role: "assistant", type: "agent" },
).slice(-MAX_AGENT_MESSAGES);
const agentMessages = agentAnnotations
.map(
@@ -59,13 +57,13 @@ const prepareChatHistory = (chatHistory: ChatMessage[]) => {
...chatHistory.slice(0, -1),
agentMessage,
chatHistory.slice(-1)[0],
];
] as ChatMessage[];
}
return chatHistory;
return chatHistory as ChatMessage[];
};
export const createWorkflow = (chatHistory: ChatMessage[]) => {
const chatHistoryWithAgentMessages = prepareChatHistory(chatHistory);
export const createWorkflow = (messages: Message[], params?: any) => {
const chatHistoryWithAgentMessages = prepareChatHistory(messages);
const runAgent = async (
context: Context,
agent: Workflow,
@@ -123,7 +121,10 @@ Decision (respond with either 'not_publish' or 'publish'):`;
};
const research = async (context: Context, ev: ResearchEvent) => {
const researcher = await createResearcher(chatHistoryWithAgentMessages);
const researcher = await createResearcher(
chatHistoryWithAgentMessages,
params,
);
const researchRes = await runAgent(context, researcher, {
message: ev.data.input,
});
@@ -143,7 +143,7 @@ export class FunctionCallingAgent extends Workflow {
fullResponse = chunk;
}
if (fullResponse) {
if (fullResponse?.options && Object.keys(fullResponse.options).length) {
memory.put({
role: "assistant",
content: "",
@@ -4,8 +4,10 @@ import path from "path";
import { getDataSource } from "../engine";
import { createTools } from "../engine/tools/index";
const getQueryEngineTool = async (): Promise<QueryEngineTool | null> => {
const index = await getDataSource();
export const getQueryEngineTool = async (
params?: any,
): Promise<QueryEngineTool | null> => {
const index = await getDataSource(params);
if (!index) {
return null;
}
+38 -7
View File
@@ -16,7 +16,8 @@ import base64
import logging
import os
import uuid
from typing import Dict, List, Optional, Union
from dataclasses import asdict
from typing import Any, Dict, List, Optional, Union
from app.engine.tools.artifact import CodeArtifact
from app.engine.utils.file_helper import save_file
@@ -36,7 +37,7 @@ class ExecutionResult(BaseModel):
template: str
stdout: List[str]
stderr: List[str]
runtime_error: Optional[Dict[str, Union[str, List[str]]]] = None
runtime_error: Optional[Dict[str, Any]] = None
output_urls: List[Dict[str, str]]
url: Optional[str]
@@ -54,15 +55,27 @@ class ExecutionResult(BaseModel):
}
class FileUpload(BaseModel):
id: str
name: str
@sandbox_router.post("")
async def create_sandbox(request: Request):
request_data = await request.json()
artifact_data = request_data.get("artifact", None)
sandbox_files = artifact_data.get("files", [])
if not artifact_data:
raise HTTPException(
status_code=400, detail="Could not create artifact from the request data"
)
try:
artifact = CodeArtifact(**request_data["artifact"])
artifact = CodeArtifact(**artifact_data)
except Exception:
logger.error(f"Could not create artifact from request data: {request_data}")
return HTTPException(
raise HTTPException(
status_code=400, detail="Could not create artifact from the request data"
)
@@ -94,6 +107,10 @@ async def create_sandbox(request: Request):
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
)
# Copy files
if len(sandbox_files) > 0:
_upload_files(sbx, sandbox_files)
# Copy code to disk
if isinstance(artifact.code, list):
for file in artifact.code:
@@ -107,11 +124,12 @@ async def create_sandbox(request: Request):
if artifact.template == "code-interpreter-multilang":
result = sbx.notebook.exec_cell(artifact.code or "")
output_urls = _download_cell_results(result.results)
runtime_error = asdict(result.error) if result.error else None
return ExecutionResult(
template=artifact.template,
stdout=result.logs.stdout,
stderr=result.logs.stderr,
runtime_error=result.error,
runtime_error=runtime_error,
output_urls=output_urls,
url=None,
).to_response()
@@ -126,6 +144,19 @@ async def create_sandbox(request: Request):
).to_response()
def _upload_files(
sandbox: Union[CodeInterpreter, Sandbox],
sandbox_files: List[str] = [],
) -> None:
for file_path in sandbox_files:
file_name = os.path.basename(file_path)
local_file_path = os.path.join("output", "uploaded", file_name)
with open(local_file_path, "rb") as f:
content = f.read()
sandbox.files.write(file_path, content)
return None
def _download_cell_results(cell_results: Optional[List]) -> List[Dict[str, str]]:
"""
To pull results from code interpreter cell and save them to disk for serving
@@ -141,14 +172,14 @@ def _download_cell_results(cell_results: Optional[List]) -> List[Dict[str, str]]
data = result[ext]
if ext in ["png", "svg", "jpeg", "pdf"]:
file_path = f"output/tools/{uuid.uuid4()}.{ext}"
file_path = os.path.join("output", "tools", f"{uuid.uuid4()}.{ext}")
base64_data = data
buffer = base64.b64decode(base64_data)
file_meta = save_file(content=buffer, file_path=file_path)
output.append(
{
"type": ext,
"filename": file_meta.filename,
"filename": file_meta.name,
"url": file_meta.url,
}
)
+122 -48
View File
@@ -1,17 +1,21 @@
import base64
import mimetypes
import os
import re
import uuid
from io import BytesIO
from pathlib import Path
from typing import List, Optional, Tuple
from typing import Dict, List, Optional, Tuple
from app.engine.index import IndexConfig, get_index
from app.engine.utils.file_helper import FileMetadata, save_file
from llama_index.core import VectorStoreIndex
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.readers.file.base import (
_try_loading_included_file_formats as get_file_loaders_map,
)
from llama_index.core.schema import Document
from llama_index.core.tools.function_tool import FunctionTool
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
from llama_index.readers.file import FlatReader
@@ -31,14 +35,19 @@ def get_llamaparse_parser():
def default_file_loaders_map():
default_loaders = get_file_loaders_map()
default_loaders[".txt"] = FlatReader
default_loaders[".csv"] = FlatReader
return default_loaders
class PrivateFileService:
"""
To store the files uploaded by the user and add them to the index.
"""
PRIVATE_STORE_PATH = "output/uploaded"
@staticmethod
def preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
def _preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
header, data = base64_content.split(",", 1)
mime_type = header.split(";")[0].split(":", 1)[1]
extension = mimetypes.guess_extension(mime_type)
@@ -46,79 +55,144 @@ class PrivateFileService:
return base64.b64decode(data), extension
@staticmethod
def store_and_parse_file(file_name, file_data, extension) -> List[Document]:
def _store_file(file_name, file_data) -> FileMetadata:
"""
Store the file to the private directory and return the file metadata
"""
# Store file to the private directory
os.makedirs(PrivateFileService.PRIVATE_STORE_PATH, exist_ok=True)
file_path = Path(os.path.join(PrivateFileService.PRIVATE_STORE_PATH, file_name))
# write file
with open(file_path, "wb") as f:
f.write(file_data)
return save_file(file_data, file_path=str(file_path))
@staticmethod
def _load_file_to_documents(file_metadata: FileMetadata) -> List[Document]:
"""
Load the file from the private directory and return the documents
"""
_, extension = os.path.splitext(file_metadata.name)
extension = extension.lstrip(".")
# Load file to documents
# If LlamaParse is enabled, use it to parse the file
# Otherwise, use the default file loaders
reader = get_llamaparse_parser()
if reader is None:
reader_cls = default_file_loaders_map().get(extension)
reader_cls = default_file_loaders_map().get(f".{extension}")
if reader_cls is None:
raise ValueError(f"File extension {extension} is not supported")
reader = reader_cls()
documents = reader.load_data(file_path)
documents = reader.load_data(Path(file_metadata.path))
# Add custom metadata
for doc in documents:
doc.metadata["file_name"] = file_name
doc.metadata["file_name"] = file_metadata.name
doc.metadata["private"] = "true"
return documents
@staticmethod
def _add_documents_to_vector_store_index(
documents: List[Document], index: VectorStoreIndex
) -> None:
"""
Add the documents to the vector store index
"""
pipeline = IngestionPipeline()
nodes = pipeline.run(documents=documents)
# Add the nodes to the index and persist it
if index is None:
index = VectorStoreIndex(nodes=nodes)
else:
index.insert_nodes(nodes=nodes)
index.storage_context.persist(
persist_dir=os.environ.get("STORAGE_DIR", "storage")
)
@staticmethod
def _add_file_to_llama_cloud_index(
index: LlamaCloudIndex,
file_name: str,
file_data: bytes,
) -> str:
"""
Add the file to the LlamaCloud index.
LlamaCloudIndex is a managed index so we can directly use the files.
"""
try:
from app.engine.service import LLamaCloudFileService
except ImportError:
raise ValueError("LlamaCloudFileService is not found")
project_id = index._get_project_id()
pipeline_id = index._get_pipeline_id()
# LlamaCloudIndex is a managed index so we can directly use the files
upload_file = (file_name, BytesIO(file_data))
doc_id = LLamaCloudFileService.add_file_to_pipeline(
project_id,
pipeline_id,
upload_file,
custom_metadata={},
)
return doc_id
@staticmethod
def _sanitize_file_name(file_name: str) -> str:
file_name, extension = os.path.splitext(file_name)
return re.sub(r"[^a-zA-Z0-9]", "_", file_name) + extension
@classmethod
def process_file(
file_name: str, base64_content: str, params: Optional[dict] = None
) -> List[str]:
cls,
file_name: str,
base64_content: str,
params: Optional[dict] = None,
) -> FileMetadata:
if params is None:
params = {}
file_data, extension = PrivateFileService.preprocess_base64_file(base64_content)
# Add the nodes to the index and persist it
index_config = IndexConfig(**params)
current_index = get_index(index_config)
index = get_index(index_config)
# Insert the documents into the index
if isinstance(current_index, LlamaCloudIndex):
from app.engine.service import LLamaCloudFileService
# Generate a new file name if the same file is uploaded multiple times
file_id = str(uuid.uuid4())
new_file_name = f"{file_id}_{cls._sanitize_file_name(file_name)}"
project_id = current_index._get_project_id()
pipeline_id = current_index._get_pipeline_id()
# LlamaCloudIndex is a managed index so we can directly use the files
upload_file = (file_name, BytesIO(file_data))
return [
LLamaCloudFileService.add_file_to_pipeline(
project_id,
pipeline_id,
upload_file,
custom_metadata={
# Set private=true to mark the document as private user docs (required for filtering)
"private": "true",
},
)
]
# Preprocess and store the file
file_data, extension = cls._preprocess_base64_file(base64_content)
file_metadata = cls._store_file(new_file_name, file_data)
tools = cls._get_available_tools()
code_executor_tools = ["interpreter", "artifact"]
# If the file is CSV and there is a code executor tool, we don't need to index.
if extension == ".csv" and any(tool in tools for tool in code_executor_tools):
return file_metadata
else:
# First process documents into nodes
documents = PrivateFileService.store_and_parse_file(
file_name, file_data, extension
)
pipeline = IngestionPipeline()
nodes = pipeline.run(documents=documents)
# Add the nodes to the index and persist it
if current_index is None:
current_index = VectorStoreIndex(nodes=nodes)
# Insert the file into the index and update document ids to the file metadata
if isinstance(index, LlamaCloudIndex):
doc_id = cls._add_file_to_llama_cloud_index(
index, new_file_name, file_data
)
# Add document ids to the file metadata
file_metadata.refs = [doc_id]
else:
current_index.insert_nodes(nodes=nodes)
current_index.storage_context.persist(
persist_dir=os.environ.get("STORAGE_DIR", "storage")
)
documents = cls._load_file_to_documents(file_metadata)
cls._add_documents_to_vector_store_index(documents, index)
# Add document ids to the file metadata
file_metadata.refs = [doc.doc_id for doc in documents]
# Return the document ids
return [doc.doc_id for doc in documents]
# Return the file metadata
return file_metadata
@staticmethod
def _get_available_tools() -> Dict[str, List[FunctionTool]]:
try:
from app.engine.tools import ToolFactory
tools = ToolFactory.from_env(map_result=True)
return tools
except ImportError:
# There is no tool code
return {}
except Exception as e:
raise ValueError(f"Failed to get available tools: {e}") from e
+12 -4
View File
@@ -1,7 +1,11 @@
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core.settings import Settings
from typing import Dict
import logging
import os
from typing import Dict
from llama_index.core.settings import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
logger = logging.getLogger(__name__)
DEFAULT_MODEL = "gpt-3.5-turbo"
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
@@ -50,7 +54,11 @@ def embedding_config_from_env() -> Dict:
def init_llmhub():
from llama_index.llms.openai_like import OpenAILike
try:
from llama_index.llms.openai_like import OpenAILike
except ImportError:
logger.error("Failed to import OpenAILike. Make sure llama_index is installed.")
raise
llm_configs = llm_config_from_env()
embedding_configs = embedding_config_from_env()
@@ -33,8 +33,13 @@ def init_settings():
def init_ollama():
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama.base import DEFAULT_REQUEST_TIMEOUT, Ollama
try:
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama.base import DEFAULT_REQUEST_TIMEOUT, Ollama
except ImportError:
raise ImportError(
"Ollama support is not installed. Please install it with `poetry add llama-index-llms-ollama` and `poetry add llama-index-embeddings-ollama`"
)
base_url = os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434"
request_timeout = float(
@@ -55,25 +60,29 @@ def init_openai():
from llama_index.llms.openai import OpenAI
max_tokens = os.getenv("LLM_MAX_TOKENS")
config = {
"model": os.getenv("MODEL"),
"temperature": float(os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)),
"max_tokens": int(max_tokens) if max_tokens is not None else None,
}
Settings.llm = OpenAI(**config)
Settings.llm = OpenAI(
model=os.getenv("MODEL", "gpt-4o-mini"),
temperature=float(os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)),
max_tokens=int(max_tokens) if max_tokens is not None else None,
)
dimensions = os.getenv("EMBEDDING_DIM")
config = {
"model": os.getenv("EMBEDDING_MODEL"),
"dimensions": int(dimensions) if dimensions is not None else None,
}
Settings.embed_model = OpenAIEmbedding(**config)
Settings.embed_model = OpenAIEmbedding(
model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"),
dimensions=int(dimensions) if dimensions is not None else None,
)
def init_azure_openai():
from llama_index.core.constants import DEFAULT_TEMPERATURE
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
from llama_index.llms.azure_openai import AzureOpenAI
try:
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
from llama_index.llms.azure_openai import AzureOpenAI
except ImportError:
raise ImportError(
"Azure OpenAI support is not installed. Please install it with `poetry add llama-index-llms-azure-openai` and `poetry add llama-index-embeddings-azure-openai`"
)
llm_deployment = os.environ["AZURE_OPENAI_LLM_DEPLOYMENT"]
embedding_deployment = os.environ["AZURE_OPENAI_EMBEDDING_DEPLOYMENT"]
@@ -105,26 +114,37 @@ def init_azure_openai():
def init_fastembed():
"""
Use Qdrant Fastembed as the local embedding provider.
"""
from llama_index.embeddings.fastembed import FastEmbedEmbedding
try:
from llama_index.embeddings.fastembed import FastEmbedEmbedding
except ImportError:
raise ImportError(
"FastEmbed support is not installed. Please install it with `poetry add llama-index-embeddings-fastembed`"
)
embed_model_map: Dict[str, str] = {
# Small and multilingual
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
# Large and multilingual
"paraphrase-multilingual-mpnet-base-v2": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", # noqa: E501
"paraphrase-multilingual-mpnet-base-v2": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
}
embedding_model = os.getenv("EMBEDDING_MODEL")
if embedding_model is None:
raise ValueError("EMBEDDING_MODEL environment variable is not set")
# This will download the model automatically if it is not already downloaded
Settings.embed_model = FastEmbedEmbedding(
model_name=embed_model_map[os.getenv("EMBEDDING_MODEL")]
model_name=embed_model_map[embedding_model]
)
def init_groq():
from llama_index.llms.groq import Groq
try:
from llama_index.llms.groq import Groq
except ImportError:
raise ImportError(
"Groq support is not installed. Please install it with `poetry add llama-index-llms-groq`"
)
Settings.llm = Groq(model=os.getenv("MODEL"))
# Groq does not provide embeddings, so we use FastEmbed instead
@@ -132,7 +152,12 @@ def init_groq():
def init_anthropic():
from llama_index.llms.anthropic import Anthropic
try:
from llama_index.llms.anthropic import Anthropic
except ImportError:
raise ImportError(
"Anthropic support is not installed. Please install it with `poetry add llama-index-llms-anthropic`"
)
model_map: Dict[str, str] = {
"claude-3-opus": "claude-3-opus-20240229",
@@ -148,8 +173,13 @@ def init_anthropic():
def init_gemini():
from llama_index.embeddings.gemini import GeminiEmbedding
from llama_index.llms.gemini import Gemini
try:
from llama_index.embeddings.gemini import GeminiEmbedding
from llama_index.llms.gemini import Gemini
except ImportError:
raise ImportError(
"Gemini support is not installed. Please install it with `poetry add llama-index-llms-gemini` and `poetry add llama-index-embeddings-gemini`"
)
model_name = f"models/{os.getenv('MODEL')}"
embed_model_name = f"models/{os.getenv('EMBEDDING_MODEL')}"
@@ -15,6 +15,6 @@ def get_vector_store():
token=token,
api_endpoint=endpoint,
collection_name=collection,
embedding_dimension=int(os.getenv("EMBEDDING_DIM")),
embedding_dimension=int(os.getenv("EMBEDDING_DIM", 768)),
)
return store
@@ -1,4 +1,5 @@
import os
from llama_index.vector_stores.chroma import ChromaVectorStore
@@ -18,7 +19,7 @@ def get_vector_store():
)
store = ChromaVectorStore.from_params(
host=os.getenv("CHROMA_HOST"),
port=int(os.getenv("CHROMA_PORT")),
port=os.getenv("CHROMA_PORT", "8001"),
collection_name=collection_name,
)
return store
@@ -1,22 +1,66 @@
# flake8: noqa: E402
from dotenv import load_dotenv
import os
from app.engine.index import get_index
from dotenv import load_dotenv
load_dotenv()
import logging
from llama_index.core.readers import SimpleDirectoryReader
from app.engine.index import get_client, get_index
from app.engine.service import LLamaCloudFileService
from app.settings import init_settings
from llama_cloud import PipelineType
from llama_index.core.readers import SimpleDirectoryReader
from llama_index.core.settings import Settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def ensure_index(index):
project_id = index._get_project_id()
client = get_client()
pipelines = client.pipelines.search_pipelines(
project_id=project_id,
pipeline_name=index.name,
pipeline_type=PipelineType.MANAGED.value,
)
if len(pipelines) == 0:
from llama_index.embeddings.openai import OpenAIEmbedding
if not isinstance(Settings.embed_model, OpenAIEmbedding):
raise ValueError(
"Creating a new pipeline with a non-OpenAI embedding model is not supported."
)
client.pipelines.upsert_pipeline(
project_id=project_id,
request={
"name": index.name,
"embedding_config": {
"type": "OPENAI_EMBEDDING",
"component": {
"api_key": os.getenv("OPENAI_API_KEY"), # editable
"model_name": os.getenv("EMBEDDING_MODEL"),
},
},
"transform_config": {
"mode": "auto",
"config": {
"chunk_size": Settings.chunk_size, # editable
"chunk_overlap": Settings.chunk_overlap, # editable
},
},
},
)
def generate_datasource():
init_settings()
logger.info("Generate index for the provided data")
index = get_index()
ensure_index(index)
project_id = index._get_project_id()
pipeline_id = index._get_pipeline_id()
@@ -34,13 +78,7 @@ def generate_datasource():
f"Adding file {input_file} to pipeline {index.name} in project {index.project_name}"
)
LLamaCloudFileService.add_file_to_pipeline(
project_id,
pipeline_id,
f,
custom_metadata={
# Set private=false to mark the document as public (required for filtering)
"private": "false",
},
project_id, pipeline_id, f, custom_metadata={}
)
logger.info("Finished generating the index")
@@ -7,7 +7,7 @@ from llama_index.core.ingestion.api_utils import (
get_client as llama_cloud_get_client,
)
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field, field_validator
logger = logging.getLogger("uvicorn")
@@ -15,31 +15,39 @@ logger = logging.getLogger("uvicorn")
class LlamaCloudConfig(BaseModel):
# Private attributes
api_key: str = Field(
default=os.getenv("LLAMA_CLOUD_API_KEY"),
exclude=True, # Exclude from the model representation
)
base_url: Optional[str] = Field(
default=os.getenv("LLAMA_CLOUD_BASE_URL"),
exclude=True,
)
organization_id: Optional[str] = Field(
default=os.getenv("LLAMA_CLOUD_ORGANIZATION_ID"),
exclude=True,
)
# Configuration attributes, can be set by the user
pipeline: str = Field(
description="The name of the pipeline to use",
default=os.getenv("LLAMA_CLOUD_INDEX_NAME"),
)
project: str = Field(
description="The name of the LlamaCloud project",
default=os.getenv("LLAMA_CLOUD_PROJECT_NAME"),
)
def __init__(self, **kwargs):
if "api_key" not in kwargs:
kwargs["api_key"] = os.getenv("LLAMA_CLOUD_API_KEY")
if "base_url" not in kwargs:
kwargs["base_url"] = os.getenv("LLAMA_CLOUD_BASE_URL")
if "organization_id" not in kwargs:
kwargs["organization_id"] = os.getenv("LLAMA_CLOUD_ORGANIZATION_ID")
if "pipeline" not in kwargs:
kwargs["pipeline"] = os.getenv("LLAMA_CLOUD_INDEX_NAME")
if "project" not in kwargs:
kwargs["project"] = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
super().__init__(**kwargs)
# Validate and throw error if the env variables are not set before starting the app
@validator("pipeline", "project", "api_key", pre=True, always=True)
@field_validator("pipeline", "project", "api_key", mode="before")
@classmethod
def validate_env_vars(cls, value):
def validate_fields(cls, value):
if value is None:
raise ValueError(
"Please set LLAMA_CLOUD_INDEX_NAME, LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY"
@@ -56,7 +64,7 @@ class LlamaCloudConfig(BaseModel):
class IndexConfig(BaseModel):
llama_cloud_pipeline_config: LlamaCloudConfig = Field(
default=LlamaCloudConfig(),
default_factory=lambda: LlamaCloudConfig(),
alias="llamaCloudPipeline",
)
callback_manager: Optional[CallbackManager] = Field(
@@ -5,7 +5,7 @@ def generate_filters(doc_ids):
"""
Generate public/private document filters based on the doc_ids and the vector store.
"""
# Using "is_empty" filter to include the documents don't have the "private" key because they're uploaded in LlamaCloud UI
# public documents (ingested by "poetry run generate" or in the LlamaCloud UI) don't have the "private" field
public_doc_filter = MetadataFilter(
key="private",
value=None,
@@ -1,4 +1,5 @@
import os
from llama_index.vector_stores.milvus import MilvusVectorStore
@@ -15,6 +16,6 @@ def get_vector_store():
user=os.getenv("MILVUS_USERNAME"),
password=os.getenv("MILVUS_PASSWORD"),
collection_name=collection,
dim=int(os.getenv("EMBEDDING_DIM")),
dim=int(os.getenv("EMBEDDING_DIM", 768)),
)
return store
@@ -3,7 +3,7 @@ import os
from datetime import timedelta
from typing import Optional
from cachetools import TTLCache, cached
from cachetools import TTLCache, cached # type: ignore
from llama_index.core.callbacks import CallbackManager
from llama_index.core.indices import load_index_from_storage
from llama_index.core.storage import StorageContext
@@ -14,7 +14,12 @@ async function loadAndIndex() {
// create vector store and a collection
const collectionName = process.env.ASTRA_DB_COLLECTION!;
const vectorStore = new AstraDBVectorStore();
const vectorStore = new AstraDBVectorStore({
params: {
endpoint: process.env.ASTRA_DB_ENDPOINT!,
token: process.env.ASTRA_DB_APPLICATION_TOKEN!,
},
});
await vectorStore.createAndConnect(collectionName, {
vector: {
dimension: parseInt(process.env.EMBEDDING_DIM!),
@@ -5,7 +5,12 @@ import { checkRequiredEnvVars } from "./shared";
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const store = new AstraDBVectorStore();
const store = new AstraDBVectorStore({
params: {
endpoint: process.env.ASTRA_DB_ENDPOINT!,
token: process.env.ASTRA_DB_APPLICATION_TOKEN!,
},
});
await store.connect(process.env.ASTRA_DB_COLLECTION!);
return await VectorStoreIndex.fromVectorStore(store);
}
@@ -25,6 +25,8 @@ async function* walk(dir: string): AsyncGenerator<string> {
async function loadAndIndex() {
const index = await getDataSource();
// ensure the index is available or create a new one
await index.ensureIndex({ verbose: true });
const projectId = await index.getProjectId();
const pipelineId = await index.getPipelineId();
@@ -32,10 +34,23 @@ async function loadAndIndex() {
for await (const filePath of walk(DATA_DIR)) {
const buffer = await fs.readFile(filePath);
const filename = path.basename(filePath);
const file = new File([buffer], filename);
await LLamaCloudFileService.addFileToPipeline(projectId, pipelineId, file, {
private: "false",
});
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!`);
@@ -1,14 +1,17 @@
import { MetadataFilter, MetadataFilters } from "llamaindex";
import { CloudRetrieveParams, MetadataFilter } from "llamaindex";
export function generateFilters(documentIds: string[]): MetadataFilters {
// public documents don't have the "private" field or it's set to "false"
export function generateFilters(documentIds: string[]) {
// public documents (ingested by "npm run generate" or in the LlamaCloud UI) don't have the "private" field
const publicDocumentsFilter: MetadataFilter = {
key: "private",
operator: "is_empty",
};
// if no documentIds are provided, only retrieve information from public documents
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
if (!documentIds.length)
return {
filters: [publicDocumentsFilter],
} as CloudRetrieveParams["filters"];
const privateDocumentsFilter: MetadataFilter = {
key: "file_id", // Note: LLamaCloud uses "file_id" to reference private document ids as "doc_id" is a restricted field in LlamaCloud
@@ -20,5 +23,5 @@ export function generateFilters(documentIds: string[]): MetadataFilters {
return {
filters: [publicDocumentsFilter, privateDocumentsFilter],
condition: "or",
};
} as CloudRetrieveParams["filters"];
}
@@ -18,7 +18,9 @@ async function loadAndIndex() {
// create postgres vector store
const vectorStore = new PGVectorStore({
connectionString: process.env.PG_CONNECTION_STRING,
clientConfig: {
connectionString: process.env.PG_CONNECTION_STRING,
},
schemaName: PGVECTOR_SCHEMA,
tableName: PGVECTOR_TABLE,
});
@@ -9,7 +9,9 @@ import {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const pgvs = new PGVectorStore({
connectionString: process.env.PG_CONNECTION_STRING,
clientConfig: {
connectionString: process.env.PG_CONNECTION_STRING,
},
schemaName: PGVECTOR_SCHEMA,
tableName: PGVECTOR_TABLE,
});
@@ -6,7 +6,7 @@ load_dotenv()
import logging
import os
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.ingestion import DocstoreStrategy, IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.settings import Settings
from llama_index.core.storage import StorageContext
@@ -41,7 +41,7 @@ def run_pipeline(docstore, vector_store, documents):
Settings.embed_model,
],
docstore=docstore,
docstore_strategy="upserts_and_delete",
docstore_strategy=DocstoreStrategy.UPSERTS_AND_DELETE, # type: ignore
vector_store=vector_store,
)
@@ -16,7 +16,7 @@ class IndexConfig(BaseModel):
)
def get_index(config: IndexConfig = None):
def get_index(config: Optional[IndexConfig] = None) -> VectorStoreIndex:
if config is None:
config = IndexConfig()
logger.info("Connecting vector store...")
@@ -15,7 +15,7 @@ uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
llama-index = "^0.11.1"
cachetools = "^5.3.3"
reflex = "^0.5.9"
reflex = "^0.6.2.post1"
[build-system]
requires = ["poetry-core"]
@@ -21,7 +21,7 @@
"dotenv": "^16.3.1",
"duck-duck-scrape": "^2.2.5",
"express": "^4.18.2",
"llamaindex": "0.6.2",
"llamaindex": "0.6.22",
"pdf2json": "3.0.5",
"ajv": "^8.12.0",
"@e2b/code-interpreter": "0.0.9-beta.3",
@@ -1,8 +1,6 @@
import logging
from typing import List
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from llama_index.core.chat_engine.types import NodeWithScore
from llama_index.core.llms import MessageRole
from app.api.routers.events import EventCallbackHandler
@@ -13,7 +11,7 @@ from app.api.routers.models import (
SourceNodes,
)
from app.api.routers.vercel_response import VercelStreamResponse
from app.engine import get_chat_engine
from app.engine.engine import get_chat_engine
from app.engine.query_filter import generate_filters
chat_router = r = APIRouter()
@@ -42,10 +40,11 @@ async def chat(
chat_engine = get_chat_engine(
filters=filters, params=params, event_handlers=[event_handler]
)
response = await chat_engine.astream_chat(last_message_content, messages)
process_response_nodes(response.source_nodes, background_tasks)
response = chat_engine.astream_chat(last_message_content, messages)
return VercelStreamResponse(request, event_handler, response, data)
return VercelStreamResponse(
request, event_handler, response, data, background_tasks
)
except Exception as e:
logger.exception("Error in chat engine", exc_info=True)
raise HTTPException(
@@ -76,17 +75,3 @@ async def chat_request(
result=Message(role=MessageRole.ASSISTANT, content=response.response),
nodes=SourceNodes.from_source_nodes(response.source_nodes),
)
def process_response_nodes(
nodes: List[NodeWithScore],
background_tasks: BackgroundTasks,
):
try:
# Start background tasks to download documents from LlamaCloud if needed
from app.engine.service import LLamaCloudFileService
LLamaCloudFileService.download_files_from_nodes(nodes, background_tasks)
except ImportError:
logger.debug("LlamaCloud is not configured. Skipping post processing of nodes")
pass
@@ -1,11 +1,10 @@
import logging
import os
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from app.api.routers.models import ChatConfig
config_router = r = APIRouter()
logger = logging.getLogger("uvicorn")
@@ -23,10 +22,14 @@ async def chat_config() -> ChatConfig:
try:
from app.engine.service import LLamaCloudFileService
logger.info("LlamaCloud is configured. Adding /config/llamacloud route.")
print("LlamaCloud is configured. Adding /config/llamacloud route.")
@r.get("/llamacloud")
async def chat_llama_cloud_config():
if not os.getenv("LLAMA_CLOUD_API_KEY"):
raise HTTPException(
status_code=500, detail="LlamaCloud API KEY is not configured"
)
projects = LLamaCloudFileService.get_all_projects_with_pipelines()
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
@@ -42,7 +45,5 @@ try:
}
except ImportError:
logger.debug(
"LlamaCloud is not configured. Skipping adding /config/llamacloud route."
)
print("LlamaCloud is not configured. Skipping adding /config/llamacloud route.")
pass
@@ -1,13 +1,13 @@
import json
import asyncio
import json
import logging
from typing import AsyncGenerator, Dict, Any, List, Optional
from typing import Any, AsyncGenerator, Dict, List, Optional
from llama_index.core.callbacks.base import BaseCallbackHandler
from llama_index.core.callbacks.schema import CBEventType
from llama_index.core.tools.types import ToolOutput
from pydantic import BaseModel
logger = logging.getLogger(__name__)
@@ -31,15 +31,20 @@ class CallbackEvent(BaseModel):
return None
def get_tool_message(self) -> dict | None:
if self.payload is None:
return None
func_call_args = self.payload.get("function_call")
if func_call_args is not None and "tool" in self.payload:
tool = self.payload.get("tool")
if tool is None:
return None
return {
"type": "events",
"data": {
"title": f"Calling tool: {tool.name} with inputs: {func_call_args}",
},
}
return None
def _is_output_serializable(self, output: Any) -> bool:
try:
@@ -49,6 +54,8 @@ class CallbackEvent(BaseModel):
return False
def get_agent_tool_response(self) -> dict | None:
if self.payload is None:
return None
response = self.payload.get("response")
if response is not None:
sources = response.sources
@@ -74,6 +81,7 @@ class CallbackEvent(BaseModel):
},
},
}
return None
def to_response(self):
try:
@@ -114,11 +122,13 @@ class EventCallbackHandler(BaseCallbackHandler):
event_type: CBEventType,
payload: Optional[Dict[str, Any]] = None,
event_id: str = "",
parent_id: str = "",
**kwargs: Any,
) -> str:
event = CallbackEvent(event_id=event_id, event_type=event_type, payload=payload)
if event.to_response() is not None:
self._aqueue.put_nowait(event)
return event_id
def on_event_end(
self,
@@ -1,6 +1,6 @@
import logging
import os
from typing import Any, Dict, List, Literal, Optional
from typing import Any, Dict, List, Optional
from llama_index.core.llms import ChatMessage, MessageRole
from llama_index.core.schema import NodeWithScore
@@ -12,19 +12,52 @@ from app.config import DATA_DIR
logger = logging.getLogger("uvicorn")
class FileContent(BaseModel):
type: Literal["text", "ref"]
# If the file is pure text then the value is be a string
# otherwise, it's a list of document IDs
value: str | List[str]
class FileMetadata(BaseModel):
id: str
name: str
url: Optional[str] = None
refs: Optional[List[str]] = None
def _get_url_llm_content(self) -> Optional[str]:
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
if url_prefix:
if self.url is not None:
return f"File URL: {self.url}\n"
else:
# Construct url from file name
return f"File URL (instruction: do not update this file URL yourself): {url_prefix}/output/uploaded/{self.name}\n"
else:
logger.warning(
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
)
return None
def to_llm_content(self) -> str:
"""
Construct content for LLM from the file metadata
"""
default_content = f"=====File: {self.name}=====\n"
# Include file URL if it's available
url_content = self._get_url_llm_content()
if url_content:
default_content += url_content
# Include document IDs if it's available
if self.refs is not None:
default_content += f"Document IDs: {self.refs}\n"
# Include sandbox file path
sandbox_file_path = f"/tmp/{self.name}"
default_content += f"Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): {sandbox_file_path}\n"
return default_content
class File(BaseModel):
id: str
content: FileContent
filename: str
filesize: int
filetype: str
metadata: FileMetadata
def _load_file_content(self) -> str:
file_path = f"output/uploaded/{self.metadata.name}"
with open(file_path, "r") as file:
return file.read()
class AnnotationFileData(BaseModel):
@@ -64,14 +97,16 @@ class Annotation(BaseModel):
type: str
data: AnnotationFileData | List[str] | AgentAnnotation | ArtifactAnnotation
def to_content(self) -> str | None:
if self.type == "document_file":
# We only support generating context content for CSV files for now
csv_files = [file for file in self.data.files if file.filetype == "csv"]
if len(csv_files) > 0:
return "Use data from following CSV raw content\n" + "\n".join(
[f"```csv\n{csv_file.content.value}\n```" for csv_file in csv_files]
def to_content(self) -> Optional[str]:
if self.type == "document_file" and isinstance(self.data, AnnotationFileData):
# iterate through all files and construct content for LLM
file_contents = [file.metadata.to_llm_content() for file in self.data.files]
if len(file_contents) > 0:
return "Use data from following files content\n" + "\n".join(
file_contents
)
elif self.type == "image":
raise NotImplementedError("Use image file is not supported yet!")
else:
logger.warning(
f"The annotation {self.type} is not supported for generating context content"
@@ -167,7 +202,11 @@ class ChatData(BaseModel):
):
tool_output = annotation.data.toolOutput
if tool_output and not tool_output.get("isError", False):
return tool_output.get("output", {}).get("code", None)
output = tool_output.get("output", {})
if isinstance(output, dict) and output.get("code"):
return output.get("code")
else:
return None
return None
def get_history_messages(
@@ -208,17 +247,26 @@ class ChatData(BaseModel):
Get the document IDs from the chat messages
"""
document_ids: List[str] = []
uploaded_files = self.get_uploaded_files()
for _file in uploaded_files:
refs = _file.metadata.refs
if refs is not None:
document_ids.extend(refs)
return list(set(document_ids))
def get_uploaded_files(self) -> List[File]:
"""
Get the uploaded files from the chat data
"""
uploaded_files = []
for message in self.messages:
if message.role == MessageRole.USER and message.annotations is not None:
for annotation in message.annotations:
if (
annotation.type == "document_file"
and annotation.data.files is not None
if annotation.type == "document_file" and isinstance(
annotation.data, AnnotationFileData
):
for fi in annotation.data.files:
if fi.content.type == "ref":
document_ids += fi.content.value
return list(set(document_ids))
uploaded_files.extend(annotation.data.files)
return uploaded_files
class SourceNodes(BaseModel):
@@ -242,7 +290,7 @@ class SourceNodes(BaseModel):
)
@classmethod
def get_url_from_metadata(cls, metadata: Dict[str, Any]) -> str:
def get_url_from_metadata(cls, metadata: Dict[str, Any]) -> Optional[str]:
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
if not url_prefix:
logger.warning(
@@ -1,5 +1,5 @@
import logging
from typing import List, Any
from typing import Any, Dict
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
@@ -18,12 +18,18 @@ class FileUploadRequest(BaseModel):
@r.post("")
def upload_file(request: FileUploadRequest) -> List[str]:
def upload_file(request: FileUploadRequest) -> Dict[str, Any]:
"""
To upload a private file from the chat UI.
Returns:
The metadata of the uploaded file.
"""
try:
logger.info("Processing file")
return PrivateFileService.process_file(
logger.info(f"Processing file: {request.filename}")
file_meta = PrivateFileService.process_file(
request.filename, request.base64, request.params
)
return file_meta.to_upload_response()
except Exception as e:
logger.error(f"Error processing file: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Error processing file")
@@ -1,15 +1,19 @@
import json
from typing import List
import logging
from typing import Awaitable, List
from aiostream import stream
from fastapi import Request
from fastapi import BackgroundTasks, Request
from fastapi.responses import StreamingResponse
from llama_index.core.chat_engine.types import StreamingAgentChatResponse
from llama_index.core.schema import NodeWithScore
from app.api.routers.events import EventCallbackHandler
from app.api.routers.models import ChatData, Message, SourceNodes
from app.api.services.suggestion import NextQuestionSuggestion
logger = logging.getLogger("uvicorn")
class VercelStreamResponse(StreamingResponse):
"""
@@ -19,6 +23,103 @@ class VercelStreamResponse(StreamingResponse):
TEXT_PREFIX = "0:"
DATA_PREFIX = "8:"
def __init__(
self,
request: Request,
event_handler: EventCallbackHandler,
response: Awaitable[StreamingAgentChatResponse],
chat_data: ChatData,
background_tasks: BackgroundTasks,
):
content = VercelStreamResponse.content_generator(
request, event_handler, response, chat_data, background_tasks
)
super().__init__(content=content)
@classmethod
async def content_generator(
cls,
request: Request,
event_handler: EventCallbackHandler,
response: Awaitable[StreamingAgentChatResponse],
chat_data: ChatData,
background_tasks: BackgroundTasks,
):
chat_response_generator = cls._chat_response_generator(
response, background_tasks, event_handler, chat_data
)
event_generator = cls._event_generator(event_handler)
# Merge the chat response generator and the event generator
combine = stream.merge(chat_response_generator, event_generator)
is_stream_started = False
async with combine.stream() as streamer:
async for output in streamer:
if not is_stream_started:
is_stream_started = True
# Stream a blank message to start displaying the response in the UI
yield cls.convert_text("")
yield output
if await request.is_disconnected():
break
@classmethod
async def _event_generator(cls, event_handler: EventCallbackHandler):
"""
Yield the events from the event handler
"""
async for event in event_handler.async_event_gen():
event_response = event.to_response()
if event_response is not None:
yield cls.convert_data(event_response)
@classmethod
async def _chat_response_generator(
cls,
response: Awaitable[StreamingAgentChatResponse],
background_tasks: BackgroundTasks,
event_handler: EventCallbackHandler,
chat_data: ChatData,
):
"""
Yield the text response and source nodes from the chat engine
"""
# Wait for the response from the chat engine
result = await response
# Once we got a source node, start a background task to download the files (if needed)
cls._process_response_nodes(result.source_nodes, background_tasks)
# Yield the source nodes
yield cls.convert_data(
{
"type": "sources",
"data": {
"nodes": [
SourceNodes.from_source_node(node).model_dump()
for node in result.source_nodes
]
},
}
)
final_response = ""
async for token in result.async_response_gen():
final_response += token
yield cls.convert_text(token)
# Generate next questions if next question prompt is configured
question_data = await cls._generate_next_questions(
chat_data.messages, final_response
)
if question_data:
yield cls.convert_data(question_data)
# the text_generator is the leading stream, once it's finished, also finish the event stream
event_handler.is_done = True
@classmethod
def convert_text(cls, token: str):
# Escape newlines and double quotes to avoid breaking the stream
@@ -30,76 +131,23 @@ class VercelStreamResponse(StreamingResponse):
data_str = json.dumps(data)
return f"{cls.DATA_PREFIX}[{data_str}]\n"
def __init__(
self,
request: Request,
event_handler: EventCallbackHandler,
response: StreamingAgentChatResponse,
chat_data: ChatData,
@staticmethod
def _process_response_nodes(
source_nodes: List[NodeWithScore],
background_tasks: BackgroundTasks,
):
content = VercelStreamResponse.content_generator(
request, event_handler, response, chat_data
)
super().__init__(content=content)
try:
# Start background tasks to download documents from LlamaCloud if needed
from app.engine.service import LLamaCloudFileService
@classmethod
async def content_generator(
cls,
request: Request,
event_handler: EventCallbackHandler,
response: StreamingAgentChatResponse,
chat_data: ChatData,
):
# Yield the text response
async def _chat_response_generator():
final_response = ""
async for token in response.async_response_gen():
final_response += token
yield cls.convert_text(token)
# Generate next questions if next question prompt is configured
question_data = await cls._generate_next_questions(
chat_data.messages, final_response
LLamaCloudFileService.download_files_from_nodes(
source_nodes, background_tasks
)
if question_data:
yield cls.convert_data(question_data)
# the text_generator is the leading stream, once it's finished, also finish the event stream
event_handler.is_done = True
# Yield the source nodes
yield cls.convert_data(
{
"type": "sources",
"data": {
"nodes": [
SourceNodes.from_source_node(node).model_dump()
for node in response.source_nodes
]
},
}
except ImportError:
logger.debug(
"LlamaCloud is not configured. Skipping post processing of nodes"
)
# Yield the events from the event handler
async def _event_generator():
async for event in event_handler.async_event_gen():
event_response = event.to_response()
if event_response is not None:
yield cls.convert_data(event_response)
combine = stream.merge(_chat_response_generator(), _event_generator())
is_stream_started = False
async with combine.stream() as streamer:
async for output in streamer:
if not is_stream_started:
is_stream_started = True
# Stream a blank message to start the stream
yield cls.convert_text("")
yield output
if await request.is_disconnected():
break
pass
@staticmethod
async def _generate_next_questions(chat_history: List[Message], response: str):
@@ -1 +0,0 @@
from .engine import get_chat_engine as get_chat_engine
@@ -6,15 +6,16 @@ load_dotenv()
import logging
import os
from app.engine.loaders import get_documents
from app.engine.vectordb import get_vector_store
from app.settings import init_settings
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.ingestion import DocstoreStrategy, IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.settings import Settings
from llama_index.core.storage import StorageContext
from llama_index.core.storage.docstore import SimpleDocumentStore
from app.engine.loaders import get_documents
from app.engine.vectordb import get_vector_store
from app.settings import init_settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
@@ -40,7 +41,7 @@ def run_pipeline(docstore, vector_store, documents):
Settings.embed_model,
],
docstore=docstore,
docstore_strategy="upserts_and_delete",
docstore_strategy=DocstoreStrategy.UPSERTS_AND_DELETE, # type: ignore
vector_store=vector_store,
)
@@ -1,17 +1,36 @@
import logging
import os
import uuid
from typing import Optional
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field, computed_field
logger = logging.getLogger(__name__)
class FileMetadata(BaseModel):
outputPath: str
filename: str
url: str
path: str = Field(..., description="The stored path of the file")
name: str = Field(..., description="The name of the file")
url: str = Field(..., description="The URL of the file")
refs: Optional[List[str]] = Field(
None, description="The indexed document IDs that the file is referenced to"
)
@computed_field
def file_id(self) -> Optional[str]:
file_els = self.name.split("_", maxsplit=1)
if len(file_els) == 2:
return file_els[0]
return None
def to_upload_response(self) -> Dict[str, Any]:
response = {
"id": self.file_id,
"name": self.name,
"url": self.url,
"refs": self.refs,
}
return response
def save_file(
@@ -58,7 +77,7 @@ def save_file(
logger.info(f"Saved file to {file_path}")
return FileMetadata(
outputPath=file_path,
filename=file_name,
path=file_path if isinstance(file_path, str) else str(file_path),
name=file_name,
url=f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}",
)
@@ -15,8 +15,25 @@ uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
aiostream = "^0.5.2"
cachetools = "^5.3.3"
llama-index = "0.11.6"
llama-index = "^0.11.17"
[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[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", "import-untyped", "assignment"]
@@ -23,11 +23,6 @@ export async function POST(request: NextRequest) {
);
}
const index = await getDataSource(params);
if (!index) {
throw new Error(
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
);
}
return NextResponse.json(await uploadDocument(index, filename, base64));
} catch (error) {
console.error("[Upload API]", error);
@@ -19,6 +19,8 @@ import {
Result,
Sandbox,
} from "@e2b/code-interpreter";
import fs from "node:fs/promises";
import path from "node:path";
import { saveDocument } from "../chat/llamaindex/documents/helper";
type CodeArtifact = {
@@ -32,6 +34,7 @@ type CodeArtifact = {
port: number | null;
file_path: string;
code: string;
files?: string[];
};
const sandboxTimeout = 10 * 60 * 1000; // 10 minute in ms
@@ -82,6 +85,18 @@ export async function POST(req: Request) {
}
}
// Copy files
if (artifact.files) {
artifact.files.forEach(async (sandboxFilePath) => {
const fileName = path.basename(sandboxFilePath);
const localFilePath = path.join("output", "uploaded", fileName);
const fileContent = await fs.readFile(localFilePath);
await sbx.files.write(sandboxFilePath, fileContent);
console.log(`Copied file to ${sandboxFilePath} in ${sbx.sandboxID}`);
});
}
// Copy code to fs
if (artifact.code && Array.isArray(artifact.code)) {
artifact.code.forEach(async (file) => {
@@ -95,9 +95,9 @@ export default function ChatInput(
)}
{files.length > 0 && (
<div className="flex gap-4 w-full overflow-auto py-2">
{files.map((file) => (
{files.map((file, index) => (
<DocumentPreview
key={file.id}
key={file.metadata?.id ?? `${file.filename}-${index}`}
file={file}
onRemove={() => removeDoc(file)}
/>
@@ -5,8 +5,11 @@ export function ChatFiles({ data }: { data: DocumentFileData }) {
if (!data.files.length) return null;
return (
<div className="flex gap-2 items-center">
{data.files.map((file) => (
<DocumentPreview key={file.id} file={file} />
{data.files.map((file, index) => (
<DocumentPreview
key={file.metadata?.id ?? `${file.filename}-${index}`}
file={file}
/>
))}
</div>
);
@@ -1,8 +1,7 @@
import { Check, Copy, FileText } from "lucide-react";
import Image from "next/image";
import { Check, Copy } from "lucide-react";
import { useMemo } from "react";
import { Button } from "../../button";
import { FileIcon } from "../../document-preview";
import { PreviewCard } from "../../document-preview";
import {
HoverCard,
HoverCardContent,
@@ -49,13 +48,7 @@ export function ChatSources({ data }: { data: SourceData }) {
);
}
export function SourceInfo({
node,
index,
}: {
node?: SourceNode;
index: number;
}) {
function SourceInfo({ node, index }: { node?: SourceNode; index: number }) {
if (!node) return <SourceNumberButton index={index} />;
return (
<HoverCard>
@@ -97,49 +90,33 @@ export function SourceNumberButton({
);
}
function DocumentInfo({ document }: { document: Document }) {
if (!document.sources.length) return null;
export function DocumentInfo({
document,
className,
}: {
document: Document;
className?: string;
}) {
const { url, sources } = document;
const fileName = sources[0].metadata.file_name as string | undefined;
const fileExt = fileName?.split(".").pop();
const fileImage = fileExt ? FileIcon[fileExt as DocumentFileType] : null;
// Extract filename from URL
const urlParts = url.split("/");
const fileName = urlParts.length > 0 ? urlParts[urlParts.length - 1] : url;
const fileExt = fileName?.split(".").pop() as DocumentFileType | undefined;
const previewFile = {
filename: fileName,
filetype: fileExt,
};
const DocumentDetail = (
<div
key={url}
className="h-28 w-48 flex flex-col justify-between p-4 border rounded-md shadow-md cursor-pointer"
>
<p
title={fileName}
className={cn(
fileName ? "truncate" : "text-blue-900 break-words",
"text-left",
)}
>
{fileName ?? url}
</p>
<div className="flex justify-between items-center">
<div className="space-x-2 flex">
{sources.map((node: SourceNode, index: number) => {
return (
<div key={node.id}>
<SourceInfo node={node} index={index} />
</div>
);
})}
</div>
{fileImage ? (
<div className="relative h-8 w-8 shrink-0 overflow-hidden rounded-md">
<Image
className="h-full w-auto"
priority
src={fileImage}
alt="Icon"
/>
<div className={`relative ${className}`}>
<PreviewCard className={"cursor-pointer"} file={previewFile} />
<div className="absolute bottom-2 right-2 space-x-2 flex">
{sources.map((node: SourceNode, index: number) => (
<div key={node.id}>
<SourceInfo node={node} index={index} />
</div>
) : (
<FileText className="text-gray-500" />
)}
))}
</div>
</div>
);
@@ -1,19 +1,17 @@
"use client";
import hljs from "highlight.js";
// instead of atom-one-dark theme, there are a lot of others: https://highlightjs.org/demo
import "highlight.js/styles/atom-one-dark-reasonable.css";
import { Check, Copy, Download } from "lucide-react";
import { FC, memo } from "react";
import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter";
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { FC, memo, useEffect, useRef } from "react";
import { Button } from "../../button";
import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
// TODO: Remove this when @type/react-syntax-highlighter is updated
const SyntaxHighlighter = Prism as unknown as FC<SyntaxHighlighterProps>;
interface Props {
language: string;
value: string;
className?: string;
}
interface languageMap {
@@ -56,8 +54,15 @@ export const generateRandomString = (length: number, lowercase = false) => {
return lowercase ? result.toLowerCase() : result;
};
const CodeBlock: FC<Props> = memo(({ language, value }) => {
const CodeBlock: FC<Props> = memo(({ language, value, className }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
const codeRef = useRef<HTMLElement>(null);
useEffect(() => {
if (codeRef.current && codeRef.current.dataset.highlighted !== "yes") {
hljs.highlightElement(codeRef.current);
}
}, [language, value]);
const downloadAsFile = () => {
if (typeof window === "undefined") {
@@ -93,7 +98,9 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
};
return (
<div className="codeblock relative w-full bg-zinc-950 font-sans">
<div
className={`codeblock relative w-full bg-zinc-950 font-sans ${className}`}
>
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
@@ -111,26 +118,11 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
</Button>
</div>
</div>
<SyntaxHighlighter
language={language}
style={coldarkDark}
PreTag="div"
showLineNumbers
customStyle={{
width: "100%",
background: "transparent",
padding: "1.5rem 1rem",
borderRadius: "0.5rem",
}}
codeTagProps={{
style: {
fontSize: "0.9rem",
fontFamily: "var(--font-mono)",
},
}}
>
{value}
</SyntaxHighlighter>
<pre className="border border-zinc-700">
<code ref={codeRef} className={`language-${language} font-mono`}>
{value}
</code>
</pre>
</div>
);
});
@@ -5,8 +5,9 @@ import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { SourceData } from "..";
import { SourceNumberButton } from "./chat-sources";
import { DOCUMENT_FILE_TYPES, DocumentFileType, SourceData } from "..";
import { useClientConfig } from "../hooks/use-config";
import { DocumentInfo, SourceNumberButton } from "./chat-sources";
import { CodeBlock } from "./codeblock";
const MemoizedReactMarkdown: FC<Options> = memo(
@@ -78,6 +79,7 @@ export default function Markdown({
sources?: SourceData;
}) {
const processedContent = preprocessContent(content, sources);
const { backend } = useClientConfig();
return (
<MemoizedReactMarkdown
@@ -86,7 +88,7 @@ export default function Markdown({
rehypePlugins={[rehypeKatex as any]}
components={{
p({ children }) {
return <p className="mb-2 last:mb-0">{children}</p>;
return <div className="mb-2 last:mb-0">{children}</div>;
},
code({ node, inline, className, children, ...props }) {
if (children.length) {
@@ -114,11 +116,32 @@ export default function Markdown({
key={Math.random()}
language={(match && match[1]) || ""}
value={String(children).replace(/\n$/, "")}
className="mb-2"
{...props}
/>
);
},
a({ href, children }) {
// If href starts with `{backend}/api/files`, then it's a local document and we use DocumenInfo for rendering
if (href?.startsWith(backend + "/api/files")) {
// Check if the file is document file type
const fileExtension = href.split(".").pop()?.toLowerCase();
if (
fileExtension &&
DOCUMENT_FILE_TYPES.includes(fileExtension as DocumentFileType)
) {
return (
<DocumentInfo
document={{
url: new URL(decodeURIComponent(href)).href,
sources: [],
}}
className="mb-2 mt-2"
/>
);
}
}
// If a text link starts with 'citation:', then render it as a citation reference
if (
Array.isArray(children) &&

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