mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 23:13:36 -04:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e70e327d3 | |||
| 8b371d8347 | |||
| 30fe269575 | |||
| 49c35b834b | |||
| 82c2580ee5 | |||
| fc5b266a40 | |||
| f8f97d2c00 | |||
| 9c2e094883 | |||
| 00f0b3ae03 | |||
| 4663dec81d | |||
| 7f14e47f56 | |||
| 6925676013 | |||
| 44b34fb464 | |||
| a108911fc1 | |||
| 282eaa07fc | |||
| 80db5f7c46 | |||
| 7a22c9f56d | |||
| 8431b788ad | |||
| 2b712cebec | |||
| 6edea6af5c | |||
| d79d1652d1 | |||
| 8ebd8d7039 | |||
| 2b8aaa835d | |||
| 1fe21f85bd | |||
| b9570b2eb9 | |||
| 00009ae53e | |||
| 63558c11fa |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
feat: bump LITS 0.8.2
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Optimize generated workflow code for Python
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
feat: use llamaindex chat-ui for nextjs frontend
|
||||
@@ -1,5 +1,59 @@
|
||||
# create-llama
|
||||
|
||||
## 0.3.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8b371d8: Set pydantic version to <2.10 to avoid incompatibility with llama-index.
|
||||
- 30fe269: Deactive duckduckgo tool for TS
|
||||
- 30fe269: Replace DuckDuckGo by Wikipedia tool for agentic template
|
||||
|
||||
## 0.3.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fc5b266: Improve DX for Python template (use one deployment instead of two)
|
||||
- f8f97d2: Add support for python 3.13
|
||||
|
||||
## 0.3.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 00f0b3a: fix: dont include user message in chat history
|
||||
- 4663dec: chore: bump react19 rc
|
||||
- 44b34fb: chore: update eslint 9, nextjs 15, react 19
|
||||
- 6925676: feat: use latest chat UI
|
||||
|
||||
## 0.3.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 282eaa0: Ensure that the index and document store are created when uploading a file with no available index.
|
||||
|
||||
## 0.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6edea6a: Optimize generated workflow code for Python
|
||||
- 8431b78: Optimize Typescript multi-agent code
|
||||
- 8431b78: Add form filling use case (Typescript)
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2b8aaa8: Add support for local models via Hugging Face
|
||||
- b9570b2: Fix: use generic LLMAgent instead of OpenAIAgent (adds support for Gemini and Anthropic for Agentic RAG)
|
||||
- 1fe21f8: Fix the highlight.js issue with the Next.js static build
|
||||
- 00009ae: feat: import pdf css
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9172fed: feat: bump LITS 0.8.2
|
||||
- 78ccde7: feat: use llamaindex chat-ui for nextjs frontend
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+10
-19
@@ -7,13 +7,12 @@ import { getOnline } from "./helpers/is-online";
|
||||
import { isWriteable } from "./helpers/is-writeable";
|
||||
import { makeDir } from "./helpers/make-dir";
|
||||
|
||||
import fs from "fs";
|
||||
import terminalLink from "terminal-link";
|
||||
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
|
||||
import { installTemplate } from "./helpers";
|
||||
import { writeDevcontainer } from "./helpers/devcontainer";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { toolsRequireConfig } from "./helpers/tools";
|
||||
import { configVSCode } from "./helpers/vscode";
|
||||
|
||||
export type InstallAppArgs = Omit<
|
||||
InstallTemplateArgs,
|
||||
@@ -35,7 +34,7 @@ export async function createApp({
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
port,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
@@ -81,7 +80,7 @@ export async function createApp({
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
port,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
@@ -90,31 +89,23 @@ export async function createApp({
|
||||
agents,
|
||||
};
|
||||
|
||||
if (frontend) {
|
||||
// install backend
|
||||
const backendRoot = path.join(root, "backend");
|
||||
await makeDir(backendRoot);
|
||||
await installTemplate({ ...args, root: backendRoot, backend: true });
|
||||
// Install backend
|
||||
await installTemplate({ ...args, backend: true });
|
||||
|
||||
if (frontend && framework === "fastapi") {
|
||||
// install frontend
|
||||
const frontendRoot = path.join(root, "frontend");
|
||||
const frontendRoot = path.join(root, ".frontend");
|
||||
await makeDir(frontendRoot);
|
||||
await installTemplate({
|
||||
...args,
|
||||
root: frontendRoot,
|
||||
framework: "nextjs",
|
||||
customApiPath: `http://localhost:${externalPort ?? 8000}/api/chat`,
|
||||
customApiPath: `http://localhost:${port ?? 8000}/api/chat`,
|
||||
backend: false,
|
||||
});
|
||||
// copy readme for fullstack
|
||||
await fs.promises.copyFile(
|
||||
path.join(templatesDir, "README-fullstack.md"),
|
||||
path.join(root, "README.md"),
|
||||
);
|
||||
} else {
|
||||
await installTemplate({ ...args, backend: true });
|
||||
}
|
||||
|
||||
await writeDevcontainer(root, templatesDir, framework, frontend);
|
||||
await configVSCode(root, templatesDir, framework);
|
||||
|
||||
process.chdir(root);
|
||||
if (tryGitInit(root)) {
|
||||
|
||||
@@ -63,7 +63,6 @@ if (
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
@@ -101,7 +100,6 @@ if (
|
||||
vectorDb: "none",
|
||||
tools: tool,
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
@@ -135,7 +133,6 @@ if (
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
@@ -169,7 +166,6 @@ if (
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
|
||||
@@ -20,8 +20,7 @@ if (
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
test.describe("Test extractor template", async () => {
|
||||
let frontendPort: number;
|
||||
let backendPort: number;
|
||||
let appPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
let cwd: string;
|
||||
@@ -29,16 +28,14 @@ if (
|
||||
// Create extractor app
|
||||
test.beforeAll(async () => {
|
||||
cwd = await createTestDir();
|
||||
frontendPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
backendPort = frontendPort + 1;
|
||||
appPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "extractor",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: frontendPort,
|
||||
externalPort: backendPort,
|
||||
port: appPort,
|
||||
postInstallAction: "runApp",
|
||||
});
|
||||
name = result.projectName;
|
||||
@@ -54,7 +51,7 @@ if (
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${frontendPort}`);
|
||||
await page.goto(`http://localhost:${appPort}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 2000 * 60,
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
const dataSource: string = "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateAgents = ["financial_report", "blog", "form_filling"];
|
||||
|
||||
@@ -26,12 +26,7 @@ for (const agents of templateAgents) {
|
||||
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
test.skip(
|
||||
agents === "form_filling" && templateFramework !== "fastapi",
|
||||
"Form filling is currently only supported with FastAPI.",
|
||||
);
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
@@ -40,7 +35,6 @@ for (const agents of templateAgents) {
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
@@ -49,7 +43,6 @@ for (const agents of templateAgents) {
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -65,6 +58,10 @@ for (const agents of templateAgents) {
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
@@ -73,7 +70,10 @@ for (const agents of templateAgents) {
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
agents === "financial_report" || agents === "form_filling",
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
agents === "financial_report" ||
|
||||
agents === "form_filling" ||
|
||||
templateFramework === "express",
|
||||
"Skip chat tests for financial report and form filling.",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
|
||||
@@ -22,7 +22,7 @@ const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
|
||||
@@ -35,7 +35,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
}
|
||||
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
@@ -44,7 +43,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
@@ -53,7 +51,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -68,8 +65,11 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
@@ -77,7 +77,9 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
@@ -102,7 +104,7 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${externalPort}/api/chat/request`,
|
||||
`http://localhost:${port}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
|
||||
@@ -56,7 +56,6 @@ test.describe("Test resolve TS dependencies", () => {
|
||||
dataSource: dataSource,
|
||||
vectorDb: vectorDb,
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
|
||||
+1
-27
@@ -25,7 +25,6 @@ export type RunCreateLlamaOptions = {
|
||||
dataSource: string;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
externalPort: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
templateUI?: TemplateUI;
|
||||
appType?: AppType;
|
||||
@@ -44,7 +43,6 @@ export async function runCreateLlama({
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -90,21 +88,15 @@ export async function runCreateLlama({
|
||||
...dataSourceArgs,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--open-ai-key",
|
||||
process.env.OPENAI_API_KEY,
|
||||
"--use-pnpm",
|
||||
"--port",
|
||||
port,
|
||||
"--external-port",
|
||||
externalPort,
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--tools",
|
||||
tools ?? "none",
|
||||
"--observability",
|
||||
"none",
|
||||
"--llama-cloud-key",
|
||||
process.env.LLAMA_CLOUD_API_KEY,
|
||||
];
|
||||
|
||||
if (templateUI) {
|
||||
@@ -146,12 +138,7 @@ export async function runCreateLlama({
|
||||
|
||||
// Wait for app to start
|
||||
if (postInstallAction === "runApp") {
|
||||
await checkAppHasStarted(
|
||||
appType === "--frontend",
|
||||
templateFramework,
|
||||
port,
|
||||
externalPort,
|
||||
);
|
||||
await waitPorts([port]);
|
||||
} else if (postInstallAction === "dependencies") {
|
||||
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
|
||||
} else {
|
||||
@@ -171,19 +158,6 @@ export async function createTestDir() {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
async function checkAppHasStarted(
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
) {
|
||||
const portsToWait = frontend
|
||||
? [port, externalPort]
|
||||
: [framework === "nextjs" ? port : externalPort];
|
||||
await waitPorts(portsToWait);
|
||||
}
|
||||
|
||||
async function waitPorts(ports: number[]): Promise<void> {
|
||||
const waitForPort = async (port: number): Promise<void> => {
|
||||
await waitPort({
|
||||
|
||||
@@ -61,6 +61,9 @@ export const assetRelocator = (name: string) => {
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
case "vscode_settings.json": {
|
||||
return "settings.json";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,13 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
name: "STORAGE_CACHE_DIR",
|
||||
description: "The directory to store the local storage cache.",
|
||||
value: ".cache",
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -336,6 +342,20 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "huggingface"
|
||||
? [
|
||||
{
|
||||
name: "EMBEDDING_BACKEND",
|
||||
description:
|
||||
"The backend to use for the Sentence Transformers embedding model, either 'torch', 'onnx', or 'openvino'. Defaults to 'onnx'.",
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_TRUST_REMOTE_CODE",
|
||||
description:
|
||||
"Whether to trust remote code for the embedding model, required for some models with custom code.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "t-systems"
|
||||
? [
|
||||
{
|
||||
@@ -533,7 +553,7 @@ export const createBackendEnvFile = async (
|
||||
| "framework"
|
||||
| "dataSources"
|
||||
| "template"
|
||||
| "externalPort"
|
||||
| "port"
|
||||
| "tools"
|
||||
| "observability"
|
||||
>,
|
||||
@@ -550,7 +570,7 @@ export const createBackendEnvFile = async (
|
||||
...getModelEnvs(opts.modelConfig),
|
||||
...getEngineEnvs(),
|
||||
...getVectorDBEnvs(opts.vectorDb, opts.framework),
|
||||
...getFrameworkEnvs(opts.framework, opts.externalPort),
|
||||
...getFrameworkEnvs(opts.framework, opts.port),
|
||||
...getToolEnvs(opts.tools),
|
||||
...getTemplateEnvs(opts.template),
|
||||
...getObservabilityEnvs(opts.observability),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = ["HuggingFaceH4/zephyr-7b-alpha"];
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
"BAAI/bge-small-en-v1.5": { dimensions: 384 },
|
||||
"BAAI/bge-base-en-v1.5": { dimensions: 768 },
|
||||
"BAAI/bge-large-en-v1.5": { dimensions: 1024 },
|
||||
"sentence-transformers/all-MiniLM-L6-v2": { dimensions: 384 },
|
||||
"sentence-transformers/all-mpnet-base-v2": { dimensions: 768 },
|
||||
"intfloat/multilingual-e5-large": { dimensions: 1024 },
|
||||
"mixedbread-ai/mxbai-embed-large-v1": { dimensions: 1024 },
|
||||
"nomic-ai/nomic-embed-text-v1.5": { dimensions: 768 },
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type HuggingfaceQuestionsParams = {
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askHuggingfaceQuestions({
|
||||
askModels,
|
||||
}: HuggingfaceQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which Hugging Face model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { askAnthropicQuestions } from "./anthropic";
|
||||
import { askAzureQuestions } from "./azure";
|
||||
import { askGeminiQuestions } from "./gemini";
|
||||
import { askGroqQuestions } from "./groq";
|
||||
import { askHuggingfaceQuestions } from "./huggingface";
|
||||
import { askLLMHubQuestions } from "./llmhub";
|
||||
import { askMistralQuestions } from "./mistral";
|
||||
import { askOllamaQuestions } from "./ollama";
|
||||
@@ -39,6 +40,7 @@ export async function askModelConfig({
|
||||
|
||||
if (framework === "fastapi") {
|
||||
choices.push({ title: "T-Systems", value: "t-systems" });
|
||||
choices.push({ title: "Huggingface", value: "huggingface" });
|
||||
}
|
||||
const { provider } = await prompts(
|
||||
{
|
||||
@@ -76,6 +78,9 @@ export async function askModelConfig({
|
||||
case "t-systems":
|
||||
modelConfig = await askLLMHubQuestions({ askModels });
|
||||
break;
|
||||
case "huggingface":
|
||||
modelConfig = await askHuggingfaceQuestions({ askModels });
|
||||
break;
|
||||
default:
|
||||
modelConfig = await askOpenAIQuestions({
|
||||
openAiKey,
|
||||
|
||||
@@ -3,6 +3,7 @@ import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { isCI } from "../../questions";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
@@ -30,7 +31,7 @@ export async function askOpenAIQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
if (!config.apiKey && !isCI) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
|
||||
+32
-2
@@ -20,6 +20,7 @@ interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
constraints?: Record<string, string>;
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
@@ -51,6 +52,9 @@ const getAdditionalDependencies = (
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: "^0.2.1",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -76,6 +80,9 @@ const getAdditionalDependencies = (
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-qdrant",
|
||||
version: "^0.3.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -234,6 +241,21 @@ const getAdditionalDependencies = (
|
||||
version: "0.2.4",
|
||||
});
|
||||
break;
|
||||
case "huggingface":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-huggingface",
|
||||
version: "^0.3.5",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-huggingface",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "optimum",
|
||||
version: "^1.23.3",
|
||||
extras: ["onnxruntime"],
|
||||
});
|
||||
break;
|
||||
case "t-systems":
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
@@ -264,14 +286,19 @@ const mergePoetryDependencies = (
|
||||
value.version = dependency.version ?? value.version;
|
||||
value.extras = dependency.extras ?? value.extras;
|
||||
|
||||
// Merge constraints if they exist
|
||||
if (dependency.constraints) {
|
||||
value = { ...value, ...dependency.constraints };
|
||||
}
|
||||
|
||||
if (value.version === undefined) {
|
||||
throw new Error(
|
||||
`Dependency "${dependency.name}" is missing attribute "version"!`,
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize separately only if extras are provided
|
||||
if (value.extras && value.extras.length > 0) {
|
||||
// Serialize as object if there are any additional properties
|
||||
if (Object.keys(value).length > 1) {
|
||||
existingDependencies[dependency.name] = value;
|
||||
} else {
|
||||
// Otherwise, serialize just the version string
|
||||
@@ -498,6 +525,9 @@ export const installPythonTemplate = async ({
|
||||
addOnDependencies.push({
|
||||
name: "llama-index-callbacks-arize-phoenix",
|
||||
version: "^0.2.1",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+44
-62
@@ -1,40 +1,39 @@
|
||||
import { ChildProcess, SpawnOptions, spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { SpawnOptions, spawn } from "child_process";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
const createProcess = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: SpawnOptions,
|
||||
) => {
|
||||
return spawn(command, args, {
|
||||
...options,
|
||||
shell: true,
|
||||
})
|
||||
.on("exit", function (code) {
|
||||
if (code !== 0) {
|
||||
console.log(`Child process exited with code=${code}`);
|
||||
process.exit(1);
|
||||
}
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
spawn(command, args, {
|
||||
...options,
|
||||
shell: true,
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.log("Error when running chill process: ", err);
|
||||
process.exit(1);
|
||||
});
|
||||
.on("exit", function (code) {
|
||||
if (code !== 0) {
|
||||
console.log(`Child process exited with code=${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.log("Error when running child process: ", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function runReflexApp(
|
||||
appPath: string,
|
||||
frontendPort?: number,
|
||||
backendPort?: number,
|
||||
) {
|
||||
const commandArgs = ["run", "reflex", "run"];
|
||||
if (frontendPort) {
|
||||
commandArgs.push("--frontend-port", frontendPort.toString());
|
||||
}
|
||||
if (backendPort) {
|
||||
commandArgs.push("--backend-port", backendPort.toString());
|
||||
}
|
||||
export function runReflexApp(appPath: string, port: number) {
|
||||
const commandArgs = [
|
||||
"run",
|
||||
"reflex",
|
||||
"run",
|
||||
"--frontend-port",
|
||||
port.toString(),
|
||||
];
|
||||
return createProcess("poetry", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
@@ -42,11 +41,10 @@ export function runReflexApp(
|
||||
}
|
||||
|
||||
export function runFastAPIApp(appPath: string, port: number) {
|
||||
const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port];
|
||||
|
||||
return createProcess("poetry", commandArgs, {
|
||||
return createProcess("poetry", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
env: { ...process.env, APP_PORT: `${port}` },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,39 +59,23 @@ export function runTSApp(appPath: string, port: number) {
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
template: string,
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
externalPort?: number,
|
||||
): Promise<any> {
|
||||
const processes: ChildProcess[] = [];
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort =
|
||||
framework === "nextjs" || template === "extractor" ? 3000 : 8000;
|
||||
|
||||
// Callback to kill all sub processes if the main process is killed
|
||||
process.on("exit", () => {
|
||||
console.log("Killing app processes...");
|
||||
processes.forEach((p) => p.kill());
|
||||
});
|
||||
|
||||
// Default sub app paths
|
||||
const backendPath = path.join(appPath, "backend");
|
||||
const frontendPath = path.join(appPath, "frontend");
|
||||
|
||||
if (template === "extractor") {
|
||||
processes.push(runReflexApp(appPath, port, externalPort));
|
||||
const appRunner =
|
||||
template === "extractor"
|
||||
? runReflexApp
|
||||
: framework === "fastapi"
|
||||
? runFastAPIApp
|
||||
: runTSApp;
|
||||
await appRunner(appPath, port || defaultPort);
|
||||
} catch (error) {
|
||||
console.error("Failed to run app:", error);
|
||||
throw error;
|
||||
}
|
||||
if (template === "streaming" || template === "multiagent") {
|
||||
if (framework === "fastapi" || framework === "express") {
|
||||
const backendRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
|
||||
if (frontend) {
|
||||
processes.push(backendRunner(backendPath, externalPort || 8000));
|
||||
processes.push(runTSApp(frontendPath, port || 3000));
|
||||
} else {
|
||||
processes.push(backendRunner(appPath, externalPort || 8000));
|
||||
}
|
||||
} else if (framework === "nextjs") {
|
||||
processes.push(runTSApp(appPath, port || 3000));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(processes);
|
||||
}
|
||||
|
||||
+2
-2
@@ -62,10 +62,10 @@ export const supportedTools: Tool[] = [
|
||||
dependencies: [
|
||||
{
|
||||
name: "duckduckgo-search",
|
||||
version: "6.1.7",
|
||||
version: "^6.3.5",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ export type ModelProvider =
|
||||
| "gemini"
|
||||
| "mistral"
|
||||
| "azure-openai"
|
||||
| "huggingface"
|
||||
| "t-systems";
|
||||
export type ModelConfig = {
|
||||
provider: ModelProvider;
|
||||
@@ -95,7 +96,7 @@ export interface InstallTemplateArgs {
|
||||
communityProjectConfig?: CommunityProjectConfig;
|
||||
llamapack?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
externalPort?: number;
|
||||
port?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
|
||||
+17
-13
@@ -58,11 +58,9 @@ export const installTSTemplate = async ({
|
||||
console.log("\nUsing static site generation\n");
|
||||
} else {
|
||||
if (vectorDb === "milvus") {
|
||||
nextConfigJson.experimental.serverComponentsExternalPackages =
|
||||
nextConfigJson.experimental.serverComponentsExternalPackages ?? [];
|
||||
nextConfigJson.experimental.serverComponentsExternalPackages.push(
|
||||
"@zilliz/milvus2-sdk-node",
|
||||
);
|
||||
nextConfigJson.serverExternalPackages =
|
||||
nextConfigJson.serverExternalPackages ?? [];
|
||||
nextConfigJson.serverExternalPackages.push("@zilliz/milvus2-sdk-node");
|
||||
}
|
||||
}
|
||||
await fs.writeFile(
|
||||
@@ -136,19 +134,22 @@ export const installTSTemplate = async ({
|
||||
// Copy agents use case code for multiagent template
|
||||
if (agents) {
|
||||
console.log("\nCopying agent:", agents, "\n");
|
||||
const useCasePath = path.join(compPath, "agents", "typescript", agents);
|
||||
const agentsCodePath = path.join(useCasePath, "workflow");
|
||||
|
||||
const agentsCodePath = path.join(
|
||||
compPath,
|
||||
"agents",
|
||||
"typescript",
|
||||
agents,
|
||||
);
|
||||
|
||||
// Copy agent codes
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: agentsCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// Copy use case files to project root
|
||||
await copy("*.*", path.join(root), {
|
||||
parents: true,
|
||||
cwd: useCasePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
@@ -240,7 +241,10 @@ export const installTSTemplate = async ({
|
||||
vectorDb,
|
||||
});
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
if (
|
||||
backend &&
|
||||
(postInstallAction === "runApp" || postInstallAction === "dependencies")
|
||||
) {
|
||||
await installTSDependencies(packageJson, packageManager, isOnline);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,26 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { assetRelocator, copy } from "./copy";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
function renderDevcontainerContent(
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
frontend: boolean,
|
||||
) {
|
||||
const devcontainerJson: any = JSON.parse(
|
||||
fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
|
||||
);
|
||||
|
||||
// Modify postCreateCommand
|
||||
if (frontend) {
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi"
|
||||
? "cd backend && poetry install && cd ../frontend && npm install"
|
||||
: "cd backend && npm install && cd ../frontend && npm install";
|
||||
} else {
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi" ? "poetry install" : "npm install";
|
||||
}
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi" ? "poetry install" : "npm install";
|
||||
|
||||
// Modify containerEnv
|
||||
if (framework === "fastapi") {
|
||||
if (frontend) {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
|
||||
};
|
||||
} else {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
|
||||
};
|
||||
}
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(devcontainerJson, null, 2);
|
||||
@@ -44,7 +30,6 @@ export const writeDevcontainer = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
frontend: boolean,
|
||||
) => {
|
||||
const devcontainerDir = path.join(root, ".devcontainer");
|
||||
if (fs.existsSync(devcontainerDir)) {
|
||||
@@ -54,7 +39,6 @@ export const writeDevcontainer = async (
|
||||
const devcontainerContent = renderDevcontainerContent(
|
||||
templatesDir,
|
||||
framework,
|
||||
frontend,
|
||||
);
|
||||
fs.mkdirSync(devcontainerDir);
|
||||
await fs.promises.writeFile(
|
||||
@@ -62,3 +46,25 @@ export const writeDevcontainer = async (
|
||||
devcontainerContent,
|
||||
);
|
||||
};
|
||||
|
||||
export const copyVSCodeSettings = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
) => {
|
||||
const vscodeDir = path.join(root, ".vscode");
|
||||
await copy("vscode_settings.json", vscodeDir, {
|
||||
cwd: templatesDir,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
};
|
||||
|
||||
export const configVSCode = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
) => {
|
||||
await writeDevcontainer(root, templatesDir, framework);
|
||||
if (framework === "fastapi") {
|
||||
await copyVSCodeSettings(root, templatesDir);
|
||||
}
|
||||
};
|
||||
@@ -134,13 +134,6 @@ const program = new Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select UI port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--external-port <external>",
|
||||
`
|
||||
|
||||
Select external port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -333,7 +326,7 @@ async function run(): Promise<void> {
|
||||
...answers,
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
externalPort: options.externalPort,
|
||||
port: options.port,
|
||||
});
|
||||
|
||||
if (answers.postInstallAction === "VSCode") {
|
||||
@@ -362,14 +355,7 @@ Please check ${cyan(
|
||||
}
|
||||
} else if (answers.postInstallAction === "runApp") {
|
||||
console.log(`Running app in ${root}...`);
|
||||
await runApp(
|
||||
root,
|
||||
answers.template,
|
||||
answers.frontend,
|
||||
answers.framework,
|
||||
options.port,
|
||||
options.externalPort,
|
||||
);
|
||||
await runApp(root, answers.template, answers.framework, options.port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.3.9",
|
||||
"version": "0.3.16",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
+3
-1
@@ -4,10 +4,12 @@ import { askProQuestions } from "./questions";
|
||||
import { askSimpleQuestions } from "./simple";
|
||||
import { QuestionArgs, QuestionResults } from "./types";
|
||||
|
||||
export const isCI = ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1";
|
||||
|
||||
export const askQuestions = async (
|
||||
args: QuestionArgs,
|
||||
): Promise<QuestionResults> => {
|
||||
if (ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1") {
|
||||
if (isCI) {
|
||||
return await getCIQuestionResults(args);
|
||||
} else if (args.pro) {
|
||||
// TODO: refactor pro questions to return a result object
|
||||
|
||||
+5
-11
@@ -1,5 +1,6 @@
|
||||
import { blue, green } from "picocolors";
|
||||
import { blue } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { isCI } from ".";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
|
||||
import { EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
|
||||
@@ -122,24 +123,17 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
}
|
||||
|
||||
if (
|
||||
(program.framework === "express" || program.framework === "fastapi") &&
|
||||
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?`,
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
@@ -386,7 +380,7 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
|
||||
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
|
||||
if (isUsingLlamaCloud || program.useLlamaParse) {
|
||||
if (!program.llamaCloudKey) {
|
||||
if (!program.llamaCloudKey && !isCI) {
|
||||
// if already set, don't ask again
|
||||
// Ask for LlamaCloud API key
|
||||
const { llamaCloudKey } = await prompts(
|
||||
|
||||
+14
-17
@@ -52,22 +52,19 @@ export const askSimpleQuestions = async (
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (appType !== "extractor") {
|
||||
// TODO: Add TS support for form filling use case
|
||||
if (appType !== "form_filling") {
|
||||
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 { 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(
|
||||
{
|
||||
@@ -134,7 +131,7 @@ const convertAnswers = async (
|
||||
> = {
|
||||
rag: {
|
||||
template: "streaming",
|
||||
tools: getTools(["duckduckgo"]),
|
||||
tools: getTools(["wikipedia.WikipediaToolSpec"]),
|
||||
frontend: true,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { InstallAppArgs } from "../create-app";
|
||||
|
||||
export type QuestionResults = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager" | "externalPort"
|
||||
"appPath" | "packageManager"
|
||||
>;
|
||||
|
||||
export type PureQuestionArgs = {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
__pycache__
|
||||
poetry.lock
|
||||
storage
|
||||
@@ -1,18 +0,0 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, startup the backend as described in the [backend README](./backend/README.md).
|
||||
|
||||
Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md).
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -32,7 +32,7 @@ poetry run generate
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
Per default, the example is using the explicit workflow. You can change the example by setting the `EXAMPLE_TYPE` environment variable to `choreography` or `orchestrator`.
|
||||
@@ -47,12 +47,12 @@ curl --location 'localhost:8000/api/chat' \
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/examples/workflow.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -21,7 +21,7 @@ poetry run generate
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
The example provides one streaming API endpoint `/api/chat`.
|
||||
@@ -35,12 +35,12 @@ curl --location 'localhost:8000/api/chat' \
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/financial_report.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -16,7 +16,7 @@ Make sure you have the `OPENAI_API_KEY` set.
|
||||
Second, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
## Use Case: Filling Financial CSV Template
|
||||
@@ -41,12 +41,12 @@ curl --location 'localhost:8000/api/chat' \
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/form_filling.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
import {
|
||||
Context,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
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,
|
||||
createReviewer,
|
||||
createWriter,
|
||||
} from "./agents";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
const MAX_ATTEMPTS = 2;
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class WriteEvent extends WorkflowEvent<{
|
||||
input: string;
|
||||
isGood: boolean;
|
||||
}> {}
|
||||
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class PublishEvent extends WorkflowEvent<{ input: string }> {}
|
||||
|
||||
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;
|
||||
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
|
||||
chatHistory,
|
||||
{ role: "assistant", type: "agent" },
|
||||
).slice(-MAX_AGENT_MESSAGES);
|
||||
|
||||
const agentMessages = agentAnnotations
|
||||
.map(
|
||||
(annotation) =>
|
||||
`\n<${annotation.data.agent}>\n${annotation.data.text}\n</${annotation.data.agent}>`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const agentContent = agentMessages
|
||||
? "Here is the previous conversation of agents:\n" + agentMessages
|
||||
: "";
|
||||
|
||||
if (agentContent) {
|
||||
const agentMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: agentContent,
|
||||
};
|
||||
return [
|
||||
...chatHistory.slice(0, -1),
|
||||
agentMessage,
|
||||
chatHistory.slice(-1)[0],
|
||||
] as ChatMessage[];
|
||||
}
|
||||
return chatHistory as ChatMessage[];
|
||||
};
|
||||
|
||||
export const createWorkflow = (messages: Message[], params?: any) => {
|
||||
const chatHistoryWithAgentMessages = prepareChatHistory(messages);
|
||||
const runAgent = async (
|
||||
context: Context,
|
||||
agent: Workflow,
|
||||
input: AgentInput,
|
||||
) => {
|
||||
const run = agent.run(new StartEvent({ input }));
|
||||
for await (const event of agent.streamEvents()) {
|
||||
if (event.data instanceof AgentRunEvent) {
|
||||
context.writeEventToStream(event.data);
|
||||
}
|
||||
}
|
||||
return await run;
|
||||
};
|
||||
|
||||
const start = async (context: Context, ev: StartEvent) => {
|
||||
context.set("task", ev.data.input);
|
||||
|
||||
const chatHistoryStr = chatHistoryWithAgentMessages
|
||||
.map((msg) => `${msg.role}: ${msg.content}`)
|
||||
.join("\n");
|
||||
|
||||
// Decision-making process
|
||||
const decision = await decideWorkflow(ev.data.input, chatHistoryStr);
|
||||
|
||||
if (decision !== "publish") {
|
||||
return new ResearchEvent({
|
||||
input: `Research for this task: ${ev.data.input}`,
|
||||
});
|
||||
} else {
|
||||
return new PublishEvent({
|
||||
input: `Publish content based on the chat history\n${chatHistoryStr}\n\n and task: ${ev.data.input}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const decideWorkflow = async (task: string, chatHistoryStr: string) => {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const prompt = `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'.
|
||||
|
||||
Here is the chat history:
|
||||
${chatHistoryStr}
|
||||
|
||||
The current user request is:
|
||||
${task}
|
||||
|
||||
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'):`;
|
||||
|
||||
const output = await llm.complete({ prompt: prompt });
|
||||
const decision = output.text.trim().toLowerCase();
|
||||
return decision === "publish" ? "publish" : "research";
|
||||
};
|
||||
|
||||
const research = async (context: Context, ev: ResearchEvent) => {
|
||||
const researcher = await createResearcher(
|
||||
chatHistoryWithAgentMessages,
|
||||
params,
|
||||
);
|
||||
const researchRes = await runAgent(context, researcher, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
const researchResult = researchRes.data.result;
|
||||
return new WriteEvent({
|
||||
input: `Write a blog post given this task: ${context.get("task")} using this research content: ${researchResult}`,
|
||||
isGood: false,
|
||||
});
|
||||
};
|
||||
|
||||
const write = async (context: Context, ev: WriteEvent) => {
|
||||
const writer = createWriter(chatHistoryWithAgentMessages);
|
||||
|
||||
context.set("attempts", context.get("attempts", 0) + 1);
|
||||
const tooManyAttempts = context.get("attempts") > MAX_ATTEMPTS;
|
||||
if (tooManyAttempts) {
|
||||
context.writeEventToStream(
|
||||
new AgentRunEvent({
|
||||
name: "writer",
|
||||
msg: `Too many attempts (${MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (ev.data.isGood || tooManyAttempts) {
|
||||
// the blog post is good or too many attempts
|
||||
// stream the final content
|
||||
const result = await runAgent(context, writer, {
|
||||
message: `Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: ${ev.data.input}`,
|
||||
streaming: true,
|
||||
});
|
||||
return result as unknown as StopEvent<AsyncGenerator<ChatResponseChunk>>;
|
||||
}
|
||||
|
||||
const writeRes = await runAgent(context, writer, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
const writeResult = writeRes.data.result;
|
||||
context.set("result", writeResult); // store the last result
|
||||
return new ReviewEvent({ input: writeResult });
|
||||
};
|
||||
|
||||
const review = async (context: Context, ev: ReviewEvent) => {
|
||||
const reviewer = createReviewer(chatHistoryWithAgentMessages);
|
||||
const reviewRes = await reviewer.run(
|
||||
new StartEvent<AgentInput>({ input: { message: ev.data.input } }),
|
||||
);
|
||||
const reviewResult = reviewRes.data.result;
|
||||
const oldContent = context.get("result");
|
||||
const postIsGood = reviewResult.toLowerCase().includes("post is good");
|
||||
context.writeEventToStream(
|
||||
new AgentRunEvent({
|
||||
name: "reviewer",
|
||||
msg: `The post is ${postIsGood ? "" : "not "}good enough for publishing. Sending back to the writer${
|
||||
postIsGood ? " for publication." : "."
|
||||
}`,
|
||||
}),
|
||||
);
|
||||
if (postIsGood) {
|
||||
return new WriteEvent({
|
||||
input: "",
|
||||
isGood: true,
|
||||
});
|
||||
}
|
||||
|
||||
return new WriteEvent({
|
||||
input: `Improve the writing of a given blog post by using a given review.
|
||||
Blog post:
|
||||
\`\`\`
|
||||
${oldContent}
|
||||
\`\`\`
|
||||
|
||||
Review:
|
||||
\`\`\`
|
||||
${reviewResult}
|
||||
\`\`\``,
|
||||
isGood: false,
|
||||
});
|
||||
};
|
||||
|
||||
const publish = async (context: Context, ev: PublishEvent) => {
|
||||
const publisher = await createPublisher(chatHistoryWithAgentMessages);
|
||||
|
||||
const publishResult = await runAgent(context, publisher, {
|
||||
message: `${ev.data.input}`,
|
||||
streaming: true,
|
||||
});
|
||||
return publishResult as unknown as StopEvent<
|
||||
AsyncGenerator<ChatResponseChunk>
|
||||
>;
|
||||
};
|
||||
|
||||
const workflow = new Workflow({ timeout: TIMEOUT, validate: true });
|
||||
workflow.addStep(StartEvent, start, {
|
||||
outputs: [ResearchEvent, PublishEvent],
|
||||
});
|
||||
workflow.addStep(ResearchEvent, research, { outputs: WriteEvent });
|
||||
workflow.addStep(WriteEvent, write, { outputs: [ReviewEvent, StopEvent] });
|
||||
workflow.addStep(ReviewEvent, review, { outputs: WriteEvent });
|
||||
workflow.addStep(PublishEvent, publish, { outputs: StopEvent });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import { BaseToolWithCall, QueryEngineTool } from "llamaindex";
|
||||
import path from "path";
|
||||
import { getDataSource } from "../engine";
|
||||
import { createTools } from "../engine/tools/index";
|
||||
|
||||
export const getQueryEngineTool = async (
|
||||
params?: any,
|
||||
): Promise<QueryEngineTool | null> => {
|
||||
const index = await getDataSource(params);
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
|
||||
return new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
}),
|
||||
metadata: {
|
||||
name: "query_index",
|
||||
description: `Use this tool to retrieve information about the text corpus from the index.`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAvailableTools = async () => {
|
||||
const configFile = path.join("config", "tools.json");
|
||||
let toolConfig: any;
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
try {
|
||||
toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
|
||||
} catch (e) {
|
||||
console.info(`Could not read ${configFile} file. Using no tools.`);
|
||||
}
|
||||
if (toolConfig) {
|
||||
tools.push(...(await createTools(toolConfig)));
|
||||
}
|
||||
const queryEngineTool = await getQueryEngineTool();
|
||||
if (queryEngineTool) {
|
||||
tools.push(queryEngineTool);
|
||||
}
|
||||
|
||||
return tools;
|
||||
};
|
||||
|
||||
export const lookupTools = async (
|
||||
toolNames: string[],
|
||||
): Promise<BaseToolWithCall[]> => {
|
||||
const availableTools = await getAvailableTools();
|
||||
return availableTools.filter((tool) =>
|
||||
toolNames.includes(tool.metadata.name),
|
||||
);
|
||||
};
|
||||
+13
-16
@@ -1,19 +1,16 @@
|
||||
import { ChatMessage } from "llamaindex";
|
||||
import { getTool } from "../engine/tools";
|
||||
import { FunctionCallingAgent } from "./single-agent";
|
||||
import { getQueryEngineTool, lookupTools } from "./tools";
|
||||
import { getQueryEngineTools } from "./tools";
|
||||
|
||||
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] : []);
|
||||
export const createResearcher = async (chatHistory: ChatMessage[]) => {
|
||||
const queryEngineTools = await getQueryEngineTools();
|
||||
const tools = [
|
||||
await getTool("wikipedia_tool"),
|
||||
await getTool("duckduckgo_search"),
|
||||
await getTool("image_generator"),
|
||||
...(queryEngineTools ? queryEngineTools : []),
|
||||
].filter((tool) => tool !== undefined);
|
||||
|
||||
return new FunctionCallingAgent({
|
||||
name: "researcher",
|
||||
@@ -81,17 +78,17 @@ Example:
|
||||
};
|
||||
|
||||
export const createPublisher = async (chatHistory: ChatMessage[]) => {
|
||||
const tools = await lookupTools(["document_generator"]);
|
||||
const tool = await getTool("document_generator");
|
||||
let systemPrompt = `You are an expert in publishing blog posts. You are given a task to publish a blog post.
|
||||
If the writer says that there was an error, you should reply with the error and not publish the post.`;
|
||||
if (tools.length > 0) {
|
||||
if (tool) {
|
||||
systemPrompt = `${systemPrompt}.
|
||||
If the user requests to generate a file, use the document_generator tool to generate the file and reply with the link to the file.
|
||||
Otherwise, simply return the content of the post.`;
|
||||
}
|
||||
return new FunctionCallingAgent({
|
||||
name: "publisher",
|
||||
tools: tools,
|
||||
tools: tool ? [tool] : [],
|
||||
systemPrompt: systemPrompt,
|
||||
chatHistory,
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
|
||||
import {
|
||||
createPublisher,
|
||||
createResearcher,
|
||||
createReviewer,
|
||||
createWriter,
|
||||
} from "./agents";
|
||||
import {
|
||||
FunctionCallingAgent,
|
||||
FunctionCallingAgentInput,
|
||||
} from "./single-agent";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
const MAX_ATTEMPTS = 2;
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class WriteEvent extends WorkflowEvent<{
|
||||
input: string;
|
||||
isGood: boolean;
|
||||
}> {}
|
||||
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class PublishEvent extends WorkflowEvent<{ input: string }> {}
|
||||
|
||||
type BlogContext = {
|
||||
task: string;
|
||||
attempts: number;
|
||||
result: string;
|
||||
};
|
||||
|
||||
export const createWorkflow = ({
|
||||
chatHistory,
|
||||
params,
|
||||
}: {
|
||||
chatHistory: ChatMessage[];
|
||||
params?: any;
|
||||
}) => {
|
||||
const runAgent = async (
|
||||
context: HandlerContext<BlogContext>,
|
||||
agent: FunctionCallingAgent,
|
||||
input: FunctionCallingAgentInput,
|
||||
) => {
|
||||
const agentContext = agent.run(input, {
|
||||
streaming: input.streaming ?? false,
|
||||
});
|
||||
for await (const event of agentContext) {
|
||||
if (event instanceof AgentRunEvent) {
|
||||
context.sendEvent(event);
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const start = async (
|
||||
context: HandlerContext<BlogContext>,
|
||||
ev: StartEvent<AgentInput>,
|
||||
) => {
|
||||
const chatHistoryStr = chatHistory
|
||||
.map((msg) => `${msg.role}: ${msg.content}`)
|
||||
.join("\n");
|
||||
|
||||
// Decision-making process
|
||||
const decision = await decideWorkflow(
|
||||
ev.data.message.toString(),
|
||||
chatHistoryStr,
|
||||
);
|
||||
|
||||
if (decision !== "publish") {
|
||||
return new ResearchEvent({
|
||||
input: `Research for this task: ${JSON.stringify(context.data.task)}`,
|
||||
});
|
||||
} else {
|
||||
return new PublishEvent({
|
||||
input: `Publish content based on the chat history\n${chatHistoryStr}\n\n and task: ${context.data.task}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const decideWorkflow = async (task: string, chatHistoryStr: string) => {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const prompt = `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'.
|
||||
|
||||
Here is the chat history:
|
||||
${chatHistoryStr}
|
||||
|
||||
The current user request is:
|
||||
${task}
|
||||
|
||||
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'):`;
|
||||
|
||||
const output = await llm.complete({ prompt: prompt });
|
||||
const decision = output.text.trim().toLowerCase();
|
||||
return decision === "publish" ? "publish" : "research";
|
||||
};
|
||||
|
||||
const research = async (
|
||||
context: HandlerContext<BlogContext>,
|
||||
ev: ResearchEvent,
|
||||
) => {
|
||||
const researcher = await createResearcher(chatHistory);
|
||||
const researchRes = await runAgent(context, researcher, {
|
||||
displayName: "Researcher",
|
||||
message: ev.data.input,
|
||||
});
|
||||
const researchResult = researchRes?.data;
|
||||
|
||||
return new WriteEvent({
|
||||
input: `Write a blog post given this task: ${JSON.stringify(
|
||||
context.data.task,
|
||||
)} using this research content: ${researchResult}`,
|
||||
isGood: false,
|
||||
});
|
||||
};
|
||||
|
||||
const write = async (
|
||||
context: HandlerContext<BlogContext>,
|
||||
ev: WriteEvent,
|
||||
) => {
|
||||
const writer = createWriter(chatHistory);
|
||||
context.data.attempts = context.data.attempts + 1;
|
||||
const tooManyAttempts = context.data.attempts > MAX_ATTEMPTS;
|
||||
if (tooManyAttempts) {
|
||||
context.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: "writer",
|
||||
text: `Too many attempts (${MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.`,
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (ev.data.isGood || tooManyAttempts) {
|
||||
// the blog post is good or too many attempts
|
||||
// stream the final content
|
||||
const result = await runAgent(context, writer, {
|
||||
message: `Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: ${ev.data.input}`,
|
||||
displayName: "Writer",
|
||||
streaming: true,
|
||||
});
|
||||
return result as unknown as StopEvent<AsyncGenerator<ChatResponseChunk>>;
|
||||
}
|
||||
|
||||
const writeRes = await runAgent(context, writer, {
|
||||
message: ev.data.input,
|
||||
displayName: "Writer",
|
||||
streaming: false,
|
||||
});
|
||||
const writeResult = writeRes?.data;
|
||||
context.data.result = writeResult; // store the last result
|
||||
|
||||
return new ReviewEvent({ input: writeResult });
|
||||
};
|
||||
|
||||
const review = async (
|
||||
context: HandlerContext<BlogContext>,
|
||||
ev: ReviewEvent,
|
||||
) => {
|
||||
const reviewer = createReviewer(chatHistory);
|
||||
const reviewResult = (await runAgent(context, reviewer, {
|
||||
message: ev.data.input,
|
||||
displayName: "Reviewer",
|
||||
streaming: false,
|
||||
})) as unknown as StopEvent<string>;
|
||||
const reviewResultStr = reviewResult.data;
|
||||
const oldContent = context.data.result;
|
||||
const postIsGood = reviewResultStr.toLowerCase().includes("post is good");
|
||||
context.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: "reviewer",
|
||||
text: `The post is ${postIsGood ? "" : "not "}good enough for publishing. Sending back to the writer${
|
||||
postIsGood ? " for publication." : "."
|
||||
}`,
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
if (postIsGood) {
|
||||
return new WriteEvent({
|
||||
input: "",
|
||||
isGood: true,
|
||||
});
|
||||
}
|
||||
|
||||
return new WriteEvent({
|
||||
input: `Improve the writing of a given blog post by using a given review.
|
||||
Blog post:
|
||||
\`\`\`
|
||||
${oldContent}
|
||||
\`\`\`
|
||||
|
||||
Review:
|
||||
\`\`\`
|
||||
${reviewResult}
|
||||
\`\`\``,
|
||||
isGood: false,
|
||||
});
|
||||
};
|
||||
|
||||
const publish = async (
|
||||
context: HandlerContext<BlogContext>,
|
||||
ev: PublishEvent,
|
||||
) => {
|
||||
const publisher = await createPublisher(chatHistory);
|
||||
|
||||
const publishResult = await runAgent(context, publisher, {
|
||||
message: `${ev.data.input}`,
|
||||
displayName: "Publisher",
|
||||
streaming: true,
|
||||
});
|
||||
return publishResult as unknown as StopEvent<
|
||||
AsyncGenerator<ChatResponseChunk>
|
||||
>;
|
||||
};
|
||||
|
||||
const workflow: Workflow<
|
||||
BlogContext,
|
||||
AgentInput,
|
||||
string | AsyncGenerator<boolean | ChatResponseChunk>
|
||||
> = new Workflow();
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInput>],
|
||||
outputs: [ResearchEvent, PublishEvent],
|
||||
},
|
||||
start,
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [ResearchEvent],
|
||||
outputs: [WriteEvent],
|
||||
},
|
||||
research,
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [WriteEvent],
|
||||
outputs: [ReviewEvent, StopEvent<AsyncGenerator<ChatResponseChunk>>],
|
||||
},
|
||||
write,
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [ReviewEvent],
|
||||
outputs: [WriteEvent],
|
||||
},
|
||||
review,
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [PublishEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
publish,
|
||||
);
|
||||
|
||||
// Overload run method to initialize the context
|
||||
workflow.run = function (
|
||||
input: AgentInput,
|
||||
): WorkflowContext<
|
||||
AgentInput,
|
||||
string | AsyncGenerator<boolean | ChatResponseChunk>,
|
||||
BlogContext
|
||||
> {
|
||||
return Workflow.prototype.run.call(workflow, new StartEvent(input), {
|
||||
task: input.message.toString(),
|
||||
attempts: 0,
|
||||
result: "",
|
||||
});
|
||||
};
|
||||
|
||||
return workflow;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory.
|
||||
Make sure you have the `OPENAI_API_KEY` set.
|
||||
|
||||
Second, generate the embeddings of the documents in the `./data` directory:
|
||||
|
||||
```
|
||||
npm run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
|
||||
|
||||
## Use Case: Filling Financial CSV Template
|
||||
|
||||
You can start by sending an request on the chat UI to create a report comparing the finances of Apple and Tesla.
|
||||
Or you can test the `/api/chat` endpoint with the following curl request:
|
||||
|
||||
```
|
||||
curl --location 'localhost:3000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
|
||||
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/guide/workflow) - learn about LlamaIndexTS workflows.
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ChatMessage } from "llamaindex";
|
||||
import { FunctionCallingAgent } from "./single-agent";
|
||||
import { getQueryEngineTools, lookupTools } from "./tools";
|
||||
|
||||
export const createResearcher = async (
|
||||
chatHistory: ChatMessage[],
|
||||
params?: any,
|
||||
) => {
|
||||
const queryEngineTools = await getQueryEngineTools(params);
|
||||
|
||||
if (!queryEngineTools) {
|
||||
throw new Error("Query engine tool not found");
|
||||
}
|
||||
|
||||
return new FunctionCallingAgent({
|
||||
name: "researcher",
|
||||
tools: queryEngineTools,
|
||||
systemPrompt: `You are a researcher agent. You are responsible for retrieving information from the corpus.
|
||||
## Instructions:
|
||||
+ Don't synthesize the information, just return the whole retrieved information.
|
||||
+ Don't need to retrieve the information that is already provided in the chat history and respond with: "There is no new information, please reuse the information from the conversation."
|
||||
`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createAnalyst = async (chatHistory: ChatMessage[]) => {
|
||||
let systemPrompt = `You are an expert in analyzing financial data.
|
||||
You are given a task and a set of financial data to analyze. Your task is to analyze the financial data and return a report.
|
||||
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
|
||||
Construct the analysis in textual format; including tables would be great!
|
||||
Don't need to synthesize the data, just analyze and provide your findings.
|
||||
Always use the provided information, don't make up any information yourself.`;
|
||||
const tools = await lookupTools(["interpreter"]);
|
||||
if (tools.length > 0) {
|
||||
systemPrompt = `${systemPrompt}
|
||||
You are able to visualize the financial data using code interpreter tool.
|
||||
It's very useful to create and include visualizations in the report. Never include any code in the report, just the visualization.`;
|
||||
}
|
||||
return new FunctionCallingAgent({
|
||||
name: "analyst",
|
||||
tools: tools,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createReporter = async (chatHistory: ChatMessage[]) => {
|
||||
const tools = await lookupTools(["document_generator"]);
|
||||
let systemPrompt = `You are a report generation assistant tasked with producing a well-formatted report given parsed context.
|
||||
Given a comprehensive analysis of the user request, your task is to synthesize the information and return a well-formatted report.
|
||||
|
||||
## Instructions
|
||||
You are responsible for representing the analysis in a well-formatted report. If tables or visualizations are provided, add them to the most relevant sections.
|
||||
Finally, the report should be presented in markdown format.`;
|
||||
if (tools.length > 0) {
|
||||
systemPrompt = `${systemPrompt}.
|
||||
You are also able to generate an HTML file of the report.`;
|
||||
}
|
||||
return new FunctionCallingAgent({
|
||||
name: "reporter",
|
||||
tools: tools,
|
||||
systemPrompt: systemPrompt,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
import {
|
||||
Context,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/core/workflow";
|
||||
import { Message } from "ai";
|
||||
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
|
||||
import { getAnnotations } from "../llamaindex/streaming/annotations";
|
||||
import { createAnalyst, createReporter, createResearcher } from "./agents";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
const MAX_ATTEMPTS = 2;
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class AnalyzeEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class ReportEvent extends WorkflowEvent<{ input: string }> {}
|
||||
|
||||
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;
|
||||
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
|
||||
chatHistory,
|
||||
{ role: "assistant", type: "agent" },
|
||||
).slice(-MAX_AGENT_MESSAGES);
|
||||
|
||||
const agentMessages = agentAnnotations
|
||||
.map(
|
||||
(annotation) =>
|
||||
`\n<${annotation.data.agent}>\n${annotation.data.text}\n</${annotation.data.agent}>`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const agentContent = agentMessages
|
||||
? "Here is the previous conversation of agents:\n" + agentMessages
|
||||
: "";
|
||||
|
||||
if (agentContent) {
|
||||
const agentMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: agentContent,
|
||||
};
|
||||
return [
|
||||
...chatHistory.slice(0, -1),
|
||||
agentMessage,
|
||||
chatHistory.slice(-1)[0],
|
||||
] as ChatMessage[];
|
||||
}
|
||||
return chatHistory as ChatMessage[];
|
||||
};
|
||||
|
||||
export const createWorkflow = (messages: Message[], params?: any) => {
|
||||
const chatHistoryWithAgentMessages = prepareChatHistory(messages);
|
||||
const runAgent = async (
|
||||
context: Context,
|
||||
agent: Workflow,
|
||||
input: AgentInput,
|
||||
) => {
|
||||
const run = agent.run(new StartEvent({ input }));
|
||||
for await (const event of agent.streamEvents()) {
|
||||
if (event.data instanceof AgentRunEvent) {
|
||||
context.writeEventToStream(event.data);
|
||||
}
|
||||
}
|
||||
return await run;
|
||||
};
|
||||
|
||||
const start = async (context: Context, ev: StartEvent) => {
|
||||
context.set("task", ev.data.input);
|
||||
|
||||
const chatHistoryStr = chatHistoryWithAgentMessages
|
||||
.map((msg) => `${msg.role}: ${msg.content}`)
|
||||
.join("\n");
|
||||
|
||||
// Decision-making process
|
||||
const decision = await decideWorkflow(ev.data.input, chatHistoryStr);
|
||||
|
||||
if (decision !== "publish") {
|
||||
return new ResearchEvent({
|
||||
input: `Research for this task: ${ev.data.input}`,
|
||||
});
|
||||
} else {
|
||||
return new ReportEvent({
|
||||
input: `Publish content based on the chat history\n${chatHistoryStr}\n\n and task: ${ev.data.input}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const decideWorkflow = async (task: string, chatHistoryStr: string) => {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const prompt = `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'.
|
||||
|
||||
Here is the chat history:
|
||||
${chatHistoryStr}
|
||||
|
||||
The current user request is:
|
||||
${task}
|
||||
|
||||
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'):`;
|
||||
|
||||
const output = await llm.complete({ prompt: prompt });
|
||||
const decision = output.text.trim().toLowerCase();
|
||||
return decision === "publish" ? "publish" : "research";
|
||||
};
|
||||
|
||||
const research = async (context: Context, ev: ResearchEvent) => {
|
||||
const researcher = await createResearcher(
|
||||
chatHistoryWithAgentMessages,
|
||||
params,
|
||||
);
|
||||
const researchRes = await runAgent(context, researcher, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
const researchResult = researchRes.data.result;
|
||||
return new AnalyzeEvent({
|
||||
input: `Write a blog post given this task: ${context.get("task")} using this research content: ${researchResult}`,
|
||||
});
|
||||
};
|
||||
|
||||
const analyze = async (context: Context, ev: AnalyzeEvent) => {
|
||||
const analyst = await createAnalyst(chatHistoryWithAgentMessages);
|
||||
const analyzeRes = await runAgent(context, analyst, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
return new ReportEvent({
|
||||
input: `Publish content based on the chat history\n${analyzeRes.data.result}\n\n and task: ${ev.data.input}`,
|
||||
});
|
||||
};
|
||||
|
||||
const report = async (context: Context, ev: ReportEvent) => {
|
||||
const reporter = await createReporter(chatHistoryWithAgentMessages);
|
||||
|
||||
const reportResult = await runAgent(context, reporter, {
|
||||
message: `${ev.data.input}`,
|
||||
streaming: true,
|
||||
});
|
||||
return reportResult as unknown as StopEvent<
|
||||
AsyncGenerator<ChatResponseChunk>
|
||||
>;
|
||||
};
|
||||
|
||||
const workflow = new Workflow({ timeout: TIMEOUT, validate: true });
|
||||
workflow.addStep(StartEvent, start, {
|
||||
outputs: [ResearchEvent, ReportEvent],
|
||||
});
|
||||
workflow.addStep(ResearchEvent, research, { outputs: AnalyzeEvent });
|
||||
workflow.addStep(AnalyzeEvent, analyze, { outputs: ReportEvent });
|
||||
workflow.addStep(ReportEvent, report, { outputs: StopEvent });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import { BaseToolWithCall, LlamaCloudIndex, QueryEngineTool } from "llamaindex";
|
||||
import path from "path";
|
||||
import { getDataSource } from "../engine";
|
||||
import { createTools } from "../engine/tools/index";
|
||||
|
||||
export const getQueryEngineTools = async (
|
||||
params?: any,
|
||||
): Promise<QueryEngineTool[] | null> => {
|
||||
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
|
||||
|
||||
const index = await getDataSource(params);
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
// index is LlamaCloudIndex use two query engine tools
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
return [
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
retrieval_mode: "files_via_content",
|
||||
}),
|
||||
metadata: {
|
||||
name: "document_retriever",
|
||||
description: `Document retriever that retrieves entire documents from the corpus.
|
||||
ONLY use for research questions that may require searching over entire research reports.
|
||||
Will be slower and more expensive than chunk-level retrieval but may be necessary.`,
|
||||
},
|
||||
}),
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
retrieval_mode: "chunks",
|
||||
}),
|
||||
metadata: {
|
||||
name: "chunk_retriever",
|
||||
description: `Retrieves a small set of relevant document chunks from the corpus.
|
||||
Use for research questions that want to look up specific facts from the knowledge corpus,
|
||||
and need entire documents.`,
|
||||
},
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
new QueryEngineTool({
|
||||
queryEngine: (index as any).asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
}),
|
||||
metadata: {
|
||||
name: "retriever",
|
||||
description: `Use this tool to retrieve information about the text corpus from the index.`,
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const getAvailableTools = async () => {
|
||||
const configFile = path.join("config", "tools.json");
|
||||
let toolConfig: any;
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
try {
|
||||
toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
|
||||
} catch (e) {
|
||||
console.info(`Could not read ${configFile} file. Using no tools.`);
|
||||
}
|
||||
if (toolConfig) {
|
||||
tools.push(...(await createTools(toolConfig)));
|
||||
}
|
||||
const queryEngineTools = await getQueryEngineTools();
|
||||
if (queryEngineTools) {
|
||||
tools.push(...queryEngineTools);
|
||||
}
|
||||
|
||||
return tools;
|
||||
};
|
||||
|
||||
export const lookupTools = async (
|
||||
toolNames: string[],
|
||||
): Promise<BaseToolWithCall[]> => {
|
||||
const availableTools = await getAvailableTools();
|
||||
return availableTools.filter((tool) =>
|
||||
toolNames.includes(tool.metadata.name),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ChatMessage, ToolCallLLM } from "llamaindex";
|
||||
import { getTool } from "../engine/tools";
|
||||
import { FinancialReportWorkflow } from "./fin-report";
|
||||
import { getQueryEngineTools } from "./tools";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
|
||||
export async function createWorkflow(options: {
|
||||
chatHistory: ChatMessage[];
|
||||
llm?: ToolCallLLM;
|
||||
}) {
|
||||
return new FinancialReportWorkflow({
|
||||
chatHistory: options.chatHistory,
|
||||
queryEngineTools: (await getQueryEngineTools()) || [],
|
||||
codeInterpreterTool: (await getTool("interpreter"))!,
|
||||
documentGeneratorTool: (await getTool("document_generator"))!,
|
||||
llm: options.llm,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import {
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
ChatResponseChunk,
|
||||
Settings,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
} from "llamaindex";
|
||||
import { callTools, chatWithTools } from "./tools";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
// Create a custom event type
|
||||
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
class AnalyzeEvent extends WorkflowEvent<{
|
||||
input: ChatMessage | ToolCall[];
|
||||
}> {}
|
||||
|
||||
class ReportGenerationEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT = `
|
||||
You are a financial analyst who are given a set of tools to help you.
|
||||
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
|
||||
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
|
||||
`;
|
||||
|
||||
export class FinancialReportWorkflow extends Workflow<
|
||||
null,
|
||||
AgentInput,
|
||||
ChatResponseChunk
|
||||
> {
|
||||
llm: ToolCallLLM;
|
||||
memory: ChatMemoryBuffer;
|
||||
queryEngineTools: BaseToolWithCall[];
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
|
||||
constructor(options: {
|
||||
llm?: ToolCallLLM;
|
||||
chatHistory: ChatMessage[];
|
||||
queryEngineTools: BaseToolWithCall[];
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({
|
||||
verbose: options?.verbose ?? false,
|
||||
timeout: options?.timeout ?? 360,
|
||||
});
|
||||
|
||||
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
|
||||
if (!(this.llm instanceof ToolCallLLM)) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
this.queryEngineTools = options.queryEngineTools;
|
||||
this.codeInterpreterTool = options.codeInterpreterTool;
|
||||
|
||||
this.documentGeneratorTool = options.documentGeneratorTool;
|
||||
this.memory = new ChatMemoryBuffer({
|
||||
llm: this.llm,
|
||||
chatHistory: options.chatHistory,
|
||||
});
|
||||
|
||||
// Add steps
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInput>],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.prepareChatHistory,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [InputEvent],
|
||||
outputs: [
|
||||
InputEvent,
|
||||
ResearchEvent,
|
||||
AnalyzeEvent,
|
||||
ReportGenerationEvent,
|
||||
StopEvent,
|
||||
],
|
||||
},
|
||||
this.handleLLMInput,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ResearchEvent],
|
||||
outputs: [AnalyzeEvent],
|
||||
},
|
||||
this.handleResearch,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [AnalyzeEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleAnalyze,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ReportGenerationEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleReportGeneration,
|
||||
);
|
||||
}
|
||||
|
||||
prepareChatHistory = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: StartEvent<AgentInput>,
|
||||
): Promise<InputEvent> => {
|
||||
const { message } = ev.data;
|
||||
|
||||
if (this.systemPrompt) {
|
||||
this.memory.put({ role: "system", content: this.systemPrompt });
|
||||
}
|
||||
this.memory.put({ role: "user", content: message });
|
||||
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
};
|
||||
|
||||
handleLLMInput = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: InputEvent,
|
||||
): Promise<
|
||||
| InputEvent
|
||||
| ResearchEvent
|
||||
| AnalyzeEvent
|
||||
| ReportGenerationEvent
|
||||
| StopEvent
|
||||
> => {
|
||||
const chatHistory = ev.data.input;
|
||||
|
||||
const tools = [this.codeInterpreterTool, this.documentGeneratorTool];
|
||||
if (this.queryEngineTools) {
|
||||
tools.push(...this.queryEngineTools);
|
||||
}
|
||||
|
||||
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
}
|
||||
|
||||
// Put the LLM tool call message into the memory
|
||||
// And trigger the next step according to the tool call
|
||||
if (toolCallResponse.toolCallMessage) {
|
||||
this.memory.put(toolCallResponse.toolCallMessage);
|
||||
}
|
||||
const toolName = toolCallResponse.getToolNames()[0];
|
||||
switch (toolName) {
|
||||
case this.codeInterpreterTool.metadata.name:
|
||||
return new AnalyzeEvent({
|
||||
input: toolCallResponse.toolCalls,
|
||||
});
|
||||
case this.documentGeneratorTool.metadata.name:
|
||||
return new ReportGenerationEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
default:
|
||||
if (
|
||||
this.queryEngineTools &&
|
||||
this.queryEngineTools.some((tool) => tool.metadata.name === toolName)
|
||||
) {
|
||||
return new ResearchEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleResearch = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: ResearchEvent,
|
||||
): Promise<AnalyzeEvent> => {
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text: "Researching data",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: this.queryEngineTools,
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: "Researcher",
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
return new AnalyzeEvent({
|
||||
input: {
|
||||
role: "assistant",
|
||||
content:
|
||||
"I have finished researching the data, please analyze the data.",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyze a research result or a tool call for code interpreter from the LLM
|
||||
*/
|
||||
handleAnalyze = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: AnalyzeEvent,
|
||||
): Promise<InputEvent> => {
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text: `Starting analysis`,
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
// Request by workflow LLM, input is a list of tool calls
|
||||
let toolCalls: ToolCall[] = [];
|
||||
if (Array.isArray(ev.data.input)) {
|
||||
toolCalls = ev.data.input;
|
||||
} else {
|
||||
// Requested by Researcher, input is a ChatMessage
|
||||
// We start new LLM chat specifically for analyzing the data
|
||||
const analysisPrompt = `
|
||||
You are an expert in analyzing financial data.
|
||||
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
|
||||
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
|
||||
Construct the analysis in textual format; including tables would be great!
|
||||
Don't need to synthesize the data, just analyze and provide your findings.
|
||||
`;
|
||||
|
||||
// Clone the current chat history
|
||||
// Add the analysis system prompt and the message from the researcher
|
||||
const newChatHistory = [
|
||||
...this.memory.getMessages(),
|
||||
{ role: "system", content: analysisPrompt },
|
||||
ev.data.input,
|
||||
];
|
||||
const toolCallResponse = await chatWithTools(
|
||||
this.llm,
|
||||
[this.codeInterpreterTool],
|
||||
newChatHistory as ChatMessage[],
|
||||
);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
this.memory.put(await toolCallResponse.asFullResponse());
|
||||
return new InputEvent({
|
||||
input: this.memory.getMessages(),
|
||||
});
|
||||
} else {
|
||||
this.memory.put(toolCallResponse.toolCallMessage);
|
||||
toolCalls = toolCallResponse.toolCalls;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the tools
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.codeInterpreterTool],
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: "Analyst",
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
|
||||
return new InputEvent({
|
||||
input: this.memory.getMessages(),
|
||||
});
|
||||
};
|
||||
|
||||
handleReportGeneration = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: ReportGenerationEvent,
|
||||
): Promise<InputEvent> => {
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.documentGeneratorTool],
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: "Reporter",
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory.
|
||||
Make sure you have the `OPENAI_API_KEY` set.
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
|
||||
|
||||
## Use Case: Filling Financial CSV Template
|
||||
|
||||
1. Upload the Apple and Tesla financial reports from the [data](./data) directory. Just send an empty message.
|
||||
2. Upload the CSV file [sec_10k_template.csv](./sec_10k_template.csv) and send the message "Fill the missing cells in the CSV file".
|
||||
|
||||
The agent will fill the missing cells by retrieving the information from the uploaded financial reports and return a new CSV file with the filled cells.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
|
||||
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/guide/workflow) - learn about LlamaIndexTS workflows.
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,17 @@
|
||||
Parameter,2023 Apple (AAPL),2023 Tesla (TSLA)
|
||||
Revenue,,
|
||||
Net Income,,
|
||||
Earnings Per Share (EPS),,
|
||||
Debt-to-Equity Ratio,,
|
||||
Current Ratio,,
|
||||
Gross Margin,,
|
||||
Operating Margin,,
|
||||
Net Profit Margin,,
|
||||
Inventory Turnover,,
|
||||
Accounts Receivable Turnover,,
|
||||
Capital Expenditure,,
|
||||
Research and Development Expense,,
|
||||
Market Cap,,
|
||||
Price to Earnings Ratio,,
|
||||
Dividend Yield,,
|
||||
Year-over-Year Growth Rate,,
|
||||
|
@@ -0,0 +1,20 @@
|
||||
import { ChatMessage, ToolCallLLM } from "llamaindex";
|
||||
import { getTool } from "../engine/tools";
|
||||
import { FormFillingWorkflow } from "./form-filling";
|
||||
import { getQueryEngineTools } from "./tools";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
|
||||
export async function createWorkflow(options: {
|
||||
chatHistory: ChatMessage[];
|
||||
llm?: ToolCallLLM;
|
||||
}) {
|
||||
return new FormFillingWorkflow({
|
||||
chatHistory: options.chatHistory,
|
||||
queryEngineTools: (await getQueryEngineTools()) || [],
|
||||
extractorTool: (await getTool("extract_missing_cells"))!,
|
||||
fillMissingCellsTool: (await getTool("fill_missing_cells"))!,
|
||||
llm: options.llm,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
ChatResponseChunk,
|
||||
Settings,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
} from "llamaindex";
|
||||
import { callTools, chatWithTools } from "./tools";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
// Create a custom event type
|
||||
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
|
||||
|
||||
class ExtractMissingCellsEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
class FindAnswersEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
class FillMissingCellsEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT = `
|
||||
You are a helpful assistant who helps fill missing cells in a CSV file.
|
||||
Only use the information from the retriever tool - don't make up any information yourself. Fill N/A if an answer is not found.
|
||||
If there is no retriever tool or the gathered information has many N/A values indicating the questions don't match the data, respond with a warning and ask the user to upload a different file or connect to a knowledge base.
|
||||
You can make multiple tool calls at once but only call with the same tool.
|
||||
Only use the local file path for the tools.
|
||||
`;
|
||||
|
||||
export class FormFillingWorkflow extends Workflow<
|
||||
null,
|
||||
AgentInput,
|
||||
ChatResponseChunk
|
||||
> {
|
||||
llm: ToolCallLLM;
|
||||
memory: ChatMemoryBuffer;
|
||||
extractorTool: BaseToolWithCall;
|
||||
queryEngineTools?: BaseToolWithCall[];
|
||||
fillMissingCellsTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
|
||||
constructor(options: {
|
||||
llm?: ToolCallLLM;
|
||||
chatHistory: ChatMessage[];
|
||||
extractorTool: BaseToolWithCall;
|
||||
queryEngineTools?: BaseToolWithCall[];
|
||||
fillMissingCellsTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({
|
||||
verbose: options?.verbose ?? false,
|
||||
timeout: options?.timeout ?? 360,
|
||||
});
|
||||
|
||||
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
|
||||
if (!(this.llm instanceof ToolCallLLM)) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
this.extractorTool = options.extractorTool;
|
||||
this.queryEngineTools = options.queryEngineTools;
|
||||
this.fillMissingCellsTool = options.fillMissingCellsTool;
|
||||
|
||||
this.memory = new ChatMemoryBuffer({
|
||||
llm: this.llm,
|
||||
chatHistory: options.chatHistory,
|
||||
});
|
||||
|
||||
// Add steps
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInput>],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.prepareChatHistory,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [InputEvent],
|
||||
outputs: [
|
||||
InputEvent,
|
||||
ExtractMissingCellsEvent,
|
||||
FindAnswersEvent,
|
||||
FillMissingCellsEvent,
|
||||
StopEvent,
|
||||
],
|
||||
},
|
||||
this.handleLLMInput,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ExtractMissingCellsEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleExtractMissingCells,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [FindAnswersEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleFindAnswers,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [FillMissingCellsEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleFillMissingCells,
|
||||
);
|
||||
}
|
||||
|
||||
prepareChatHistory = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: StartEvent<AgentInput>,
|
||||
): Promise<InputEvent> => {
|
||||
const { message } = ev.data;
|
||||
|
||||
if (this.systemPrompt) {
|
||||
this.memory.put({ role: "system", content: this.systemPrompt });
|
||||
}
|
||||
this.memory.put({ role: "user", content: message });
|
||||
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
};
|
||||
|
||||
handleLLMInput = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: InputEvent,
|
||||
): Promise<
|
||||
| InputEvent
|
||||
| ExtractMissingCellsEvent
|
||||
| FindAnswersEvent
|
||||
| FillMissingCellsEvent
|
||||
| StopEvent
|
||||
> => {
|
||||
const chatHistory = ev.data.input;
|
||||
|
||||
const tools = [this.extractorTool, this.fillMissingCellsTool];
|
||||
if (this.queryEngineTools) {
|
||||
tools.push(...this.queryEngineTools);
|
||||
}
|
||||
|
||||
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
}
|
||||
|
||||
// Put the LLM tool call message into the memory
|
||||
// And trigger the next step according to the tool call
|
||||
if (toolCallResponse.toolCallMessage) {
|
||||
this.memory.put(toolCallResponse.toolCallMessage);
|
||||
}
|
||||
const toolName = toolCallResponse.getToolNames()[0];
|
||||
switch (toolName) {
|
||||
case this.extractorTool.metadata.name:
|
||||
return new ExtractMissingCellsEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
case this.fillMissingCellsTool.metadata.name:
|
||||
return new FillMissingCellsEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
default:
|
||||
if (
|
||||
this.queryEngineTools &&
|
||||
this.queryEngineTools.some((tool) => tool.metadata.name === toolName)
|
||||
) {
|
||||
return new FindAnswersEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleExtractMissingCells = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: ExtractMissingCellsEvent,
|
||||
): Promise<InputEvent> => {
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: "CSVExtractor",
|
||||
text: "Extracting missing cells",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
const { toolCalls } = ev.data;
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.extractorTool],
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: "CSVExtractor",
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
};
|
||||
|
||||
handleFindAnswers = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: FindAnswersEvent,
|
||||
): Promise<InputEvent> => {
|
||||
const { toolCalls } = ev.data;
|
||||
if (!this.queryEngineTools) {
|
||||
throw new Error("Query engine tool is not available");
|
||||
}
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text: "Finding answers",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
const toolMsgs = await callTools({
|
||||
tools: this.queryEngineTools,
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: "Researcher",
|
||||
});
|
||||
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
};
|
||||
|
||||
handleFillMissingCells = async (
|
||||
ctx: HandlerContext<null>,
|
||||
ev: FillMissingCellsEvent,
|
||||
): Promise<InputEvent> => {
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.fillMissingCellsTool],
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: "Processor",
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
BaseChatEngine,
|
||||
BaseToolWithCall,
|
||||
OpenAIAgent,
|
||||
LLMAgent,
|
||||
QueryEngineTool,
|
||||
} from "llamaindex";
|
||||
import fs from "node:fs/promises";
|
||||
@@ -42,7 +42,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
tools.push(...(await createTools(toolConfig)));
|
||||
}
|
||||
|
||||
const agent = new OpenAIAgent({
|
||||
const agent = new LLMAgent({
|
||||
tools,
|
||||
systemPrompt: process.env.SYSTEM_PROMPT,
|
||||
}) as unknown as BaseChatEngine;
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import fs from "fs";
|
||||
import { BaseTool, Settings, ToolMetadata } from "llamaindex";
|
||||
import Papa from "papaparse";
|
||||
import path from "path";
|
||||
import { saveDocument } from "../../llamaindex/documents/helper";
|
||||
|
||||
type ExtractMissingCellsParameter = {
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
export type MissingCell = {
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
question: string;
|
||||
};
|
||||
|
||||
const CSV_EXTRACTION_PROMPT = `You are a data analyst. You are given a table with missing cells.
|
||||
Your task is to identify the missing cells and the questions needed to fill them.
|
||||
IMPORTANT: Column indices should be 0-based
|
||||
|
||||
# Instructions:
|
||||
- Understand the entire content of the table and the topics of the table.
|
||||
- Identify the missing cells and the meaning of the data in the cells.
|
||||
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
|
||||
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
|
||||
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
|
||||
- Finally, only return the answer in JSON format with the following schema:
|
||||
{
|
||||
"missing_cells": [
|
||||
{
|
||||
"rowIndex": number,
|
||||
"columnIndex": number,
|
||||
"question": string
|
||||
}
|
||||
]
|
||||
}
|
||||
- If there are no missing cells, return an empty array.
|
||||
- The answer is only the JSON object, nothing else and don't wrap it inside markdown code block.
|
||||
|
||||
# Example:
|
||||
# | | Name | Age | City |
|
||||
# |----|------|-----|------|
|
||||
# | 0 | John | | Paris|
|
||||
# | 1 | Mary | | |
|
||||
# | 2 | | 30 | |
|
||||
#
|
||||
# Your thoughts:
|
||||
# - The table is about people's names, ages, and cities.
|
||||
# - Row: 1, Column: 2 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
|
||||
# - Row: 1, Column: 3 (City column), Question: "In which city does Mary live? Please provide only the city name."
|
||||
# Your answer:
|
||||
# {
|
||||
# "missing_cells": [
|
||||
# {
|
||||
# "rowIndex": 1,
|
||||
# "columnIndex": 2,
|
||||
# "question": "How old is Mary? Please provide only the numerical answer."
|
||||
# },
|
||||
# {
|
||||
# "rowIndex": 1,
|
||||
# "columnIndex": 3,
|
||||
# "question": "In which city does Mary live? Please provide only the city name."
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
|
||||
# Here is your task:
|
||||
|
||||
- Table content:
|
||||
{table_content}
|
||||
|
||||
- Your answer:
|
||||
`;
|
||||
|
||||
const DEFAULT_METADATA: ToolMetadata<
|
||||
JSONSchemaType<ExtractMissingCellsParameter>
|
||||
> = {
|
||||
name: "extract_missing_cells",
|
||||
description: `Use this tool to extract missing cells in a CSV file and generate questions to fill them. This tool only works with local file path.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filePath: {
|
||||
type: "string",
|
||||
description: "The local file path to the CSV file.",
|
||||
},
|
||||
},
|
||||
required: ["filePath"],
|
||||
},
|
||||
};
|
||||
|
||||
export interface ExtractMissingCellsParams {
|
||||
metadata?: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
|
||||
}
|
||||
|
||||
export class ExtractMissingCellsTool
|
||||
implements BaseTool<ExtractMissingCellsParameter>
|
||||
{
|
||||
metadata: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
|
||||
defaultExtractionPrompt: string;
|
||||
|
||||
constructor(params: ExtractMissingCellsParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_METADATA;
|
||||
this.defaultExtractionPrompt = CSV_EXTRACTION_PROMPT;
|
||||
}
|
||||
|
||||
private readCsvFile(filePath: string): Promise<string[][]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedData = Papa.parse<string[]>(data, {
|
||||
skipEmptyLines: false,
|
||||
});
|
||||
|
||||
if (parsedData.errors.length) {
|
||||
reject(parsedData.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all rows have the same number of columns as the header
|
||||
const maxColumns = parsedData.data[0].length;
|
||||
const paddedRows = parsedData.data.map((row) => {
|
||||
return [...row, ...Array(maxColumns - row.length).fill("")];
|
||||
});
|
||||
|
||||
resolve(paddedRows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private formatToMarkdownTable(data: string[][]): string {
|
||||
if (data.length === 0) return "";
|
||||
|
||||
const maxColumns = data[0].length;
|
||||
|
||||
const headerRow = `| ${data[0].join(" | ")} |`;
|
||||
const separatorRow = `| ${Array(maxColumns).fill("---").join(" | ")} |`;
|
||||
|
||||
const dataRows = data.slice(1).map((row) => {
|
||||
return `| ${row.join(" | ")} |`;
|
||||
});
|
||||
|
||||
return [headerRow, separatorRow, ...dataRows].join("\n");
|
||||
}
|
||||
|
||||
async call(input: ExtractMissingCellsParameter): Promise<MissingCell[]> {
|
||||
const { filePath } = input;
|
||||
let tableContent: string[][];
|
||||
try {
|
||||
tableContent = await this.readCsvFile(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to read CSV file. Make sure that you are reading a local file path (not a sandbox path).`,
|
||||
);
|
||||
}
|
||||
|
||||
const prompt = this.defaultExtractionPrompt.replace(
|
||||
"{table_content}",
|
||||
this.formatToMarkdownTable(tableContent),
|
||||
);
|
||||
|
||||
const llm = Settings.llm;
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
const rawAnswer = response.text;
|
||||
const parsedResponse = JSON.parse(rawAnswer) as {
|
||||
missing_cells: MissingCell[];
|
||||
};
|
||||
if (!parsedResponse.missing_cells) {
|
||||
throw new Error(
|
||||
"The answer is not in the correct format. There should be a missing_cells array.",
|
||||
);
|
||||
}
|
||||
const answer = parsedResponse.missing_cells;
|
||||
|
||||
return answer;
|
||||
}
|
||||
}
|
||||
|
||||
type FillMissingCellsParameter = {
|
||||
filePath: string;
|
||||
cells: {
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
answer: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const FILL_CELLS_METADATA: ToolMetadata<
|
||||
JSONSchemaType<FillMissingCellsParameter>
|
||||
> = {
|
||||
name: "fill_missing_cells",
|
||||
description: `Use this tool to fill missing cells in a CSV file with provided answers. This tool only works with local file path.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filePath: {
|
||||
type: "string",
|
||||
description: "The local file path to the CSV file.",
|
||||
},
|
||||
cells: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
rowIndex: { type: "number" },
|
||||
columnIndex: { type: "number" },
|
||||
answer: { type: "string" },
|
||||
},
|
||||
required: ["rowIndex", "columnIndex", "answer"],
|
||||
},
|
||||
description: "Array of cells to fill with their answers",
|
||||
},
|
||||
},
|
||||
required: ["filePath", "cells"],
|
||||
},
|
||||
};
|
||||
|
||||
export interface FillMissingCellsParams {
|
||||
metadata?: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
|
||||
}
|
||||
|
||||
export class FillMissingCellsTool
|
||||
implements BaseTool<FillMissingCellsParameter>
|
||||
{
|
||||
metadata: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
|
||||
|
||||
constructor(params: FillMissingCellsParams = {}) {
|
||||
this.metadata = params.metadata ?? FILL_CELLS_METADATA;
|
||||
}
|
||||
|
||||
async call(input: FillMissingCellsParameter): Promise<string> {
|
||||
const { filePath, cells } = input;
|
||||
|
||||
// Read the CSV file
|
||||
const fileContent = await new Promise<string>((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parse CSV with PapaParse
|
||||
const parseResult = Papa.parse<string[]>(fileContent, {
|
||||
header: false, // Ensure the header is not treated as a separate object
|
||||
skipEmptyLines: false, // Ensure empty lines are not skipped
|
||||
});
|
||||
|
||||
if (parseResult.errors.length) {
|
||||
throw new Error(
|
||||
"Failed to parse CSV file: " + parseResult.errors[0].message,
|
||||
);
|
||||
}
|
||||
|
||||
const rows = parseResult.data;
|
||||
|
||||
// Fill the cells with answers
|
||||
for (const cell of cells) {
|
||||
// Adjust rowIndex to start from 1 for data rows
|
||||
const adjustedRowIndex = cell.rowIndex + 1;
|
||||
if (
|
||||
adjustedRowIndex < rows.length &&
|
||||
cell.columnIndex < rows[adjustedRowIndex].length
|
||||
) {
|
||||
rows[adjustedRowIndex][cell.columnIndex] = cell.answer;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to CSV format
|
||||
const updatedContent = Papa.unparse(rows, {
|
||||
delimiter: parseResult.meta.delimiter,
|
||||
});
|
||||
|
||||
// Use the helper function to write the file
|
||||
const parsedPath = path.parse(filePath);
|
||||
const newFileName = `${parsedPath.name}-filled${parsedPath.ext}`;
|
||||
const newFilePath = path.join("output/tools", newFileName);
|
||||
|
||||
const newFileUrl = await saveDocument(newFilePath, updatedContent);
|
||||
|
||||
return (
|
||||
"Successfully filled missing cells in the CSV file. File URL to show to the user: " +
|
||||
newFileUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import { BaseToolWithCall } from "llamaindex";
|
||||
import { ToolsFactory } from "llamaindex/tools/ToolsFactory";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { CodeGeneratorTool, CodeGeneratorToolParams } from "./code-generator";
|
||||
import {
|
||||
DocumentGenerator,
|
||||
DocumentGeneratorParams,
|
||||
} from "./document-generator";
|
||||
import { DuckDuckGoSearchTool, DuckDuckGoToolParams } from "./duckduckgo";
|
||||
import {
|
||||
ExtractMissingCellsParams,
|
||||
ExtractMissingCellsTool,
|
||||
FillMissingCellsParams,
|
||||
FillMissingCellsTool,
|
||||
} from "./form-filling";
|
||||
import { ImgGeneratorTool, ImgGeneratorToolParams } from "./img-gen";
|
||||
import { InterpreterTool, InterpreterToolParams } from "./interpreter";
|
||||
import { OpenAPIActionTool } from "./openapi-action";
|
||||
@@ -54,6 +62,12 @@ const toolFactory: Record<string, ToolCreator> = {
|
||||
document_generator: async (config: unknown) => {
|
||||
return [new DocumentGenerator(config as DocumentGeneratorParams)];
|
||||
},
|
||||
form_filling: async (config: unknown) => {
|
||||
return [
|
||||
new ExtractMissingCellsTool(config as ExtractMissingCellsParams),
|
||||
new FillMissingCellsTool(config as FillMissingCellsParams),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
async function createLocalTools(
|
||||
@@ -70,3 +84,19 @@ async function createLocalTools(
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
export async function getConfiguredTools(
|
||||
configPath?: string,
|
||||
): Promise<BaseToolWithCall[]> {
|
||||
const configFile = path.join(configPath ?? "config", "tools.json");
|
||||
const toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
|
||||
const tools = await createTools(toolConfig);
|
||||
return tools;
|
||||
}
|
||||
|
||||
export async function getTool(
|
||||
toolName: string,
|
||||
): Promise<BaseToolWithCall | undefined> {
|
||||
const tools = await getConfiguredTools();
|
||||
return tools.find((tool) => tool.metadata.name === toolName);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
"docx",
|
||||
};
|
||||
|
||||
const UPLOADED_FOLDER = "output/uploaded";
|
||||
export const UPLOADED_FOLDER = "output/uploaded";
|
||||
|
||||
export async function storeAndParseFile(
|
||||
name: string,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
IngestionPipeline,
|
||||
Settings,
|
||||
SimpleNodeParser,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
|
||||
@@ -28,11 +29,20 @@ export async function runPipeline(
|
||||
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",
|
||||
);
|
||||
const persistDir = process.env.STORAGE_CACHE_DIR;
|
||||
if (!persistDir) {
|
||||
throw new Error("STORAGE_CACHE_DIR environment variable is required!");
|
||||
}
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir,
|
||||
});
|
||||
const newIndex = await VectorStoreIndex.fromDocuments(documents, {
|
||||
storageContext,
|
||||
});
|
||||
await newIndex.storageContext.docStore.persist();
|
||||
return documents.map((document) => document.id_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
import { parseFile, storeFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
@@ -18,8 +16,8 @@ export async function uploadDocument(
|
||||
// Store file
|
||||
const fileMetadata = await storeFile(name, fileBuffer, mimeType);
|
||||
|
||||
// If the file is csv and has codeExecutorTool, we don't need to index the file.
|
||||
if (mimeType === "text/csv" && (await hasCodeExecutorTool())) {
|
||||
// Do not index csv files
|
||||
if (mimeType === "text/csv") {
|
||||
return fileMetadata;
|
||||
}
|
||||
let documentIds: string[] = [];
|
||||
@@ -61,14 +59,3 @@ export async function uploadDocument(
|
||||
fileMetadata.refs = documentIds;
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { JSONValue, Message } from "ai";
|
||||
import { MessageContent, MessageContentDetail } from "llamaindex";
|
||||
import {
|
||||
ChatMessage,
|
||||
MessageContent,
|
||||
MessageContentDetail,
|
||||
MessageType,
|
||||
} from "llamaindex";
|
||||
import { UPLOADED_FOLDER } from "../documents/helper";
|
||||
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
@@ -58,6 +64,45 @@ export function retrieveMessageContent(messages: Message[]): MessageContent {
|
||||
];
|
||||
}
|
||||
|
||||
export function convertToChatHistory(messages: Message[]): ChatMessage[] {
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
return [];
|
||||
}
|
||||
const agentHistory = retrieveAgentHistoryMessage(messages);
|
||||
if (agentHistory) {
|
||||
const previousMessages = messages.slice(0, -1);
|
||||
return [...previousMessages, agentHistory].map((msg) => ({
|
||||
role: msg.role as MessageType,
|
||||
content: msg.content,
|
||||
}));
|
||||
}
|
||||
return messages.map((msg) => ({
|
||||
role: msg.role as MessageType,
|
||||
content: msg.content,
|
||||
}));
|
||||
}
|
||||
|
||||
function retrieveAgentHistoryMessage(
|
||||
messages: Message[],
|
||||
maxAgentMessages = 10,
|
||||
): ChatMessage | null {
|
||||
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
|
||||
messages,
|
||||
{ role: "assistant", type: "agent" },
|
||||
).slice(-maxAgentMessages);
|
||||
|
||||
if (agentAnnotations.length > 0) {
|
||||
const messageContent =
|
||||
"Here is the previous conversation of agents:\n" +
|
||||
agentAnnotations.map((annotation) => annotation.data.text).join("\n");
|
||||
return {
|
||||
role: "assistant",
|
||||
content: messageContent,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFileContent(file: DocumentFile): string {
|
||||
let defaultContent = `=====File: ${file.name}=====\n`;
|
||||
// Include file URL if it's available
|
||||
@@ -84,6 +129,10 @@ function getFileContent(file: DocumentFile): string {
|
||||
const sandboxFilePath = `/tmp/${file.name}`;
|
||||
defaultContent += `Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): ${sandboxFilePath}\n`;
|
||||
|
||||
// Include local file path
|
||||
const localFilePath = `${UPLOADED_FOLDER}/${file.name}`;
|
||||
defaultContent += `Local file path (instruction: use for local tool that requires a local path): ${localFilePath}\n`;
|
||||
|
||||
return defaultContent;
|
||||
}
|
||||
|
||||
@@ -127,13 +176,10 @@ function retrieveLatestArtifact(messages: Message[]): MessageContentDetail[] {
|
||||
}
|
||||
|
||||
function convertAnnotations(messages: Message[]): MessageContentDetail[] {
|
||||
// annotations from the last user message that has annotations
|
||||
const annotations: Annotation[] =
|
||||
messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((message) => message.role === "user" && message.annotations)
|
||||
?.annotations?.map(getValidAnnotation) || [];
|
||||
// get all annotations from user messages
|
||||
const annotations: Annotation[] = messages
|
||||
.filter((message) => message.role === "user" && message.annotations)
|
||||
.flatMap((message) => message.annotations?.map(getValidAnnotation) || []);
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
const content: MessageContentDetail[] = [];
|
||||
|
||||
@@ -24,16 +24,6 @@ class ContextAwareTool(FunctionTool, ABC):
|
||||
pass
|
||||
|
||||
|
||||
class ResponseGenerator(BaseModel):
|
||||
"""
|
||||
A response generator from chat_with_tools.
|
||||
"""
|
||||
|
||||
generator: AsyncGenerator[ChatResponse | None, None]
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class ChatWithToolsResponse(BaseModel):
|
||||
"""
|
||||
A tool call response from chat_with_tools.
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, streamToResponse } from "ai";
|
||||
import { Request, Response } from "express";
|
||||
import { ChatResponseChunk } from "llamaindex";
|
||||
import {
|
||||
convertToChatHistory,
|
||||
retrieveMessageContent,
|
||||
} from "./llamaindex/streaming/annotations";
|
||||
import { createWorkflow } from "./workflow/factory";
|
||||
import { toDataStream, workflowEventsToStreamData } from "./workflow/stream";
|
||||
import { createStreamFromWorkflowContext } from "./workflow/stream";
|
||||
|
||||
export const chat = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { messages, data }: { messages: Message[]; data?: any } = req.body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
const { messages }: { messages: Message[] } = req.body;
|
||||
if (!messages || messages.length === 0) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
"messages are required in the request body and the last message must be from the user",
|
||||
error: "messages are required in the request body",
|
||||
});
|
||||
}
|
||||
const chatHistory = convertToChatHistory(messages);
|
||||
const userMessageContent = retrieveMessageContent(messages);
|
||||
|
||||
const agent = createWorkflow(messages, data);
|
||||
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
|
||||
userMessage.content,
|
||||
) as unknown as Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>;
|
||||
const workflow = await createWorkflow({ chatHistory });
|
||||
|
||||
// convert the workflow events to a vercel AI stream data object
|
||||
const agentStreamData = await workflowEventsToStreamData(
|
||||
agent.streamEvents(),
|
||||
);
|
||||
// convert the workflow result to a vercel AI content stream
|
||||
const stream = toDataStream(result, {
|
||||
onFinal: () => agentStreamData.close(),
|
||||
const context = workflow.run({
|
||||
message: userMessageContent,
|
||||
streaming: true,
|
||||
});
|
||||
|
||||
return streamToResponse(stream, res, {}, agentStreamData);
|
||||
const { stream, dataStream } =
|
||||
await createStreamFromWorkflowContext(context);
|
||||
|
||||
return streamToResponse(stream, res, {}, dataStream);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return res.status(500).json({
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { initObservability } from "@/app/observability";
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, StreamingTextResponse } from "ai";
|
||||
import { ChatResponseChunk } from "llamaindex";
|
||||
import { StreamingTextResponse, type Message } from "ai";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { initSettings } from "./engine/settings";
|
||||
import {
|
||||
convertToChatHistory,
|
||||
isValidMessages,
|
||||
retrieveMessageContent,
|
||||
} from "./llamaindex/streaming/annotations";
|
||||
import { createWorkflow } from "./workflow/factory";
|
||||
import { toDataStream, workflowEventsToStreamData } from "./workflow/stream";
|
||||
import { createStreamFromWorkflowContext } from "./workflow/stream";
|
||||
|
||||
initObservability();
|
||||
initSettings();
|
||||
@@ -16,9 +19,8 @@ export const dynamic = "force-dynamic";
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages, data }: { messages: Message[]; data?: any } = body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
const { messages }: { messages: Message[]; data?: any } = body;
|
||||
if (!isValidMessages(messages)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
@@ -28,20 +30,20 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const agent = createWorkflow(messages, data);
|
||||
// TODO: fix type in agent.run in LITS
|
||||
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
|
||||
userMessage.content,
|
||||
) as unknown as Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>;
|
||||
// convert the workflow events to a vercel AI stream data object
|
||||
const agentStreamData = await workflowEventsToStreamData(
|
||||
agent.streamEvents(),
|
||||
);
|
||||
// convert the workflow result to a vercel AI content stream
|
||||
const stream = toDataStream(result, {
|
||||
onFinal: () => agentStreamData.close(),
|
||||
const chatHistory = convertToChatHistory(messages);
|
||||
const userMessageContent = retrieveMessageContent(messages);
|
||||
|
||||
const workflow = await createWorkflow({ chatHistory });
|
||||
|
||||
const context = workflow.run({
|
||||
message: userMessageContent,
|
||||
streaming: true,
|
||||
});
|
||||
return new StreamingTextResponse(stream, {}, agentStreamData);
|
||||
const { stream, dataStream } =
|
||||
await createStreamFromWorkflowContext(context);
|
||||
|
||||
// Return the two streams in one response
|
||||
return new StreamingTextResponse(stream, {}, dataStream);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import {
|
||||
Context,
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/core/workflow";
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseChunk,
|
||||
QueryEngineTool,
|
||||
Settings,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
ToolCallLLMMessageOptions,
|
||||
callTool,
|
||||
} from "llamaindex";
|
||||
import { callTools, chatWithTools } from "./tools";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
class InputEvent extends WorkflowEvent<{
|
||||
@@ -27,11 +26,23 @@ class ToolCallEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
export class FunctionCallingAgent extends Workflow {
|
||||
type FunctionCallingAgentContextData = {
|
||||
streaming: boolean;
|
||||
};
|
||||
|
||||
export type FunctionCallingAgentInput = AgentInput & {
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export class FunctionCallingAgent extends Workflow<
|
||||
FunctionCallingAgentContextData,
|
||||
FunctionCallingAgentInput,
|
||||
string | AsyncGenerator<boolean | ChatResponseChunk<object>>
|
||||
> {
|
||||
name: string;
|
||||
llm: ToolCallLLM;
|
||||
memory: ChatMemoryBuffer;
|
||||
tools: BaseToolWithCall[];
|
||||
tools: BaseToolWithCall[] | QueryEngineTool[];
|
||||
systemPrompt?: string;
|
||||
writeEvents: boolean;
|
||||
role?: string;
|
||||
@@ -53,7 +64,9 @@ export class FunctionCallingAgent extends Workflow {
|
||||
});
|
||||
this.name = options?.name;
|
||||
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
|
||||
this.checkToolCallSupport();
|
||||
if (!(this.llm instanceof ToolCallLLM)) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
this.memory = new ChatMemoryBuffer({
|
||||
llm: this.llm,
|
||||
chatHistory: options.chatHistory,
|
||||
@@ -64,175 +77,103 @@ export class FunctionCallingAgent extends Workflow {
|
||||
this.role = options?.role;
|
||||
|
||||
// add steps
|
||||
this.addStep(StartEvent<AgentInput>, this.prepareChatHistory, {
|
||||
outputs: InputEvent,
|
||||
});
|
||||
this.addStep(InputEvent, this.handleLLMInput, {
|
||||
outputs: [ToolCallEvent, StopEvent],
|
||||
});
|
||||
this.addStep(ToolCallEvent, this.handleToolCalls, {
|
||||
outputs: InputEvent,
|
||||
});
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInput>],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.prepareChatHistory,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [InputEvent],
|
||||
outputs: [ToolCallEvent, StopEvent],
|
||||
},
|
||||
this.handleLLMInput,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ToolCallEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleToolCalls,
|
||||
);
|
||||
}
|
||||
|
||||
private get chatHistory() {
|
||||
return this.memory.getMessages();
|
||||
}
|
||||
|
||||
private async prepareChatHistory(
|
||||
ctx: Context,
|
||||
prepareChatHistory = async (
|
||||
ctx: HandlerContext<FunctionCallingAgentContextData>,
|
||||
ev: StartEvent<AgentInput>,
|
||||
): Promise<InputEvent> {
|
||||
const { message, streaming } = ev.data.input;
|
||||
ctx.set("streaming", streaming);
|
||||
): Promise<InputEvent> => {
|
||||
const { message, streaming } = ev.data;
|
||||
ctx.data.streaming = streaming ?? false;
|
||||
this.writeEvent(`Start to work on: ${message}`, ctx);
|
||||
if (this.systemPrompt) {
|
||||
this.memory.put({ role: "system", content: this.systemPrompt });
|
||||
}
|
||||
this.memory.put({ role: "user", content: message });
|
||||
return new InputEvent({ input: this.chatHistory });
|
||||
}
|
||||
};
|
||||
|
||||
private async handleLLMInput(
|
||||
ctx: Context,
|
||||
handleLLMInput = async (
|
||||
ctx: HandlerContext<FunctionCallingAgentContextData>,
|
||||
ev: InputEvent,
|
||||
): Promise<StopEvent<string | AsyncGenerator> | ToolCallEvent> {
|
||||
if (ctx.get("streaming")) {
|
||||
return await this.handleLLMInputStream(ctx, ev);
|
||||
): Promise<StopEvent<string | AsyncGenerator> | ToolCallEvent> => {
|
||||
const toolCallResponse = await chatWithTools(
|
||||
this.llm,
|
||||
this.tools,
|
||||
this.chatHistory,
|
||||
);
|
||||
if (toolCallResponse.toolCallMessage) {
|
||||
this.memory.put(toolCallResponse.toolCallMessage);
|
||||
}
|
||||
|
||||
const result = await this.llm.chat({
|
||||
messages: this.chatHistory,
|
||||
tools: this.tools,
|
||||
});
|
||||
this.memory.put(result.message);
|
||||
|
||||
const toolCalls = this.getToolCallsFromResponse(result);
|
||||
if (toolCalls.length) {
|
||||
return new ToolCallEvent({ toolCalls });
|
||||
if (toolCallResponse.hasToolCall()) {
|
||||
return new ToolCallEvent({ toolCalls: toolCallResponse.toolCalls });
|
||||
}
|
||||
this.writeEvent("Finished task", ctx);
|
||||
return new StopEvent({ result: result.message.content.toString() });
|
||||
}
|
||||
|
||||
private async handleLLMInputStream(
|
||||
context: Context,
|
||||
ev: InputEvent,
|
||||
): Promise<StopEvent<AsyncGenerator> | ToolCallEvent> {
|
||||
const { llm, tools, memory } = this;
|
||||
const llmArgs = { messages: this.chatHistory, tools };
|
||||
|
||||
const responseGenerator = async function* () {
|
||||
const responseStream = await llm.chat({ ...llmArgs, stream: true });
|
||||
|
||||
let fullResponse = null;
|
||||
let yieldedIndicator = false;
|
||||
for await (const chunk of responseStream) {
|
||||
const hasToolCalls = chunk.options && "toolCall" in chunk.options;
|
||||
if (!hasToolCalls) {
|
||||
if (!yieldedIndicator) {
|
||||
yield false;
|
||||
yieldedIndicator = true;
|
||||
}
|
||||
yield chunk;
|
||||
} else if (!yieldedIndicator) {
|
||||
yield true;
|
||||
yieldedIndicator = true;
|
||||
}
|
||||
|
||||
fullResponse = chunk;
|
||||
if (ctx.data.streaming) {
|
||||
if (!toolCallResponse.responseGenerator) {
|
||||
throw new Error("No streaming response");
|
||||
}
|
||||
|
||||
if (fullResponse?.options && Object.keys(fullResponse.options).length) {
|
||||
memory.put({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
options: fullResponse.options,
|
||||
});
|
||||
yield fullResponse;
|
||||
}
|
||||
};
|
||||
|
||||
const generator = responseGenerator();
|
||||
const isToolCall = await generator.next();
|
||||
if (isToolCall.value) {
|
||||
const fullResponse = await generator.next();
|
||||
const toolCalls = this.getToolCallsFromResponse(
|
||||
fullResponse.value as ChatResponseChunk<ToolCallLLMMessageOptions>,
|
||||
);
|
||||
return new ToolCallEvent({ toolCalls });
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
}
|
||||
|
||||
this.writeEvent("Finished task", context);
|
||||
return new StopEvent({ result: generator });
|
||||
}
|
||||
const fullResponse = await toolCallResponse.asFullResponse();
|
||||
this.memory.put(fullResponse);
|
||||
return new StopEvent(fullResponse.content.toString());
|
||||
};
|
||||
|
||||
private async handleToolCalls(
|
||||
ctx: Context,
|
||||
handleToolCalls = async (
|
||||
ctx: HandlerContext<FunctionCallingAgentContextData>,
|
||||
ev: ToolCallEvent,
|
||||
): Promise<InputEvent> {
|
||||
): Promise<InputEvent> => {
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs: ChatMessage[] = [];
|
||||
|
||||
for (const call of toolCalls) {
|
||||
const targetTool = this.tools.find(
|
||||
(tool) => tool.metadata.name === call.name,
|
||||
);
|
||||
// TODO: make logger optional in callTool in framework
|
||||
const toolOutput = await callTool(targetTool, call, {
|
||||
log: () => {},
|
||||
error: (...args: unknown[]) => {
|
||||
console.error(`[Tool ${call.name} Error]:`, ...args);
|
||||
},
|
||||
warn: () => {},
|
||||
});
|
||||
toolMsgs.push({
|
||||
content: JSON.stringify(toolOutput.output),
|
||||
role: "user",
|
||||
options: {
|
||||
toolResult: {
|
||||
result: toolOutput.output,
|
||||
isError: toolOutput.isError,
|
||||
id: call.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const toolMsgs = await callTools({
|
||||
tools: this.tools,
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName: this.name,
|
||||
});
|
||||
|
||||
for (const msg of toolMsgs) {
|
||||
this.memory.put(msg);
|
||||
}
|
||||
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
}
|
||||
};
|
||||
|
||||
private writeEvent(msg: string, context: Context) {
|
||||
writeEvent = (
|
||||
msg: string,
|
||||
ctx: HandlerContext<FunctionCallingAgentContextData>,
|
||||
) => {
|
||||
if (!this.writeEvents) return;
|
||||
context.writeEventToStream({
|
||||
data: new AgentRunEvent({ name: this.name, msg }),
|
||||
});
|
||||
}
|
||||
|
||||
private checkToolCallSupport() {
|
||||
const { supportToolCall } = this.llm as ToolCallLLM;
|
||||
if (!supportToolCall) throw new Error("LLM does not support tool calls");
|
||||
}
|
||||
|
||||
private getToolCallsFromResponse(
|
||||
response:
|
||||
| ChatResponse<ToolCallLLMMessageOptions>
|
||||
| ChatResponseChunk<ToolCallLLMMessageOptions>,
|
||||
): ToolCall[] {
|
||||
let options;
|
||||
if ("message" in response) {
|
||||
options = response.message.options;
|
||||
} else {
|
||||
options = response.options;
|
||||
}
|
||||
if (options && "toolCall" in options) {
|
||||
return options.toolCall as ToolCall[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({ agent: this.name, text: msg, type: "text" }),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,65 +1,77 @@
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import {
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
StopEvent,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
StreamData,
|
||||
createStreamDataTransformer,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import { ChatResponseChunk } from "llamaindex";
|
||||
import { AgentRunEvent } from "./type";
|
||||
|
||||
export function toDataStream(
|
||||
result: Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>,
|
||||
callbacks?: AIStreamCallbacksAndOptions,
|
||||
) {
|
||||
return toReadableStream(result)
|
||||
.pipeThrough(createCallbacksTransformer(callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
|
||||
function toReadableStream(
|
||||
result: Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>,
|
||||
) {
|
||||
export async function createStreamFromWorkflowContext<Input, Output, Context>(
|
||||
context: WorkflowContext<Input, Output, Context>,
|
||||
): Promise<{ stream: ReadableStream<string>; dataStream: StreamData }> {
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
return new ReadableStream<string>({
|
||||
start(controller) {
|
||||
controller.enqueue(""); // Kickstart the stream
|
||||
const dataStream = new StreamData();
|
||||
const encoder = new TextEncoder();
|
||||
let generator: AsyncGenerator<ChatResponseChunk> | undefined;
|
||||
|
||||
const closeStreams = (controller: ReadableStreamDefaultController) => {
|
||||
controller.close();
|
||||
dataStream.close();
|
||||
};
|
||||
|
||||
const mainStream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// Kickstart the stream by sending an empty string
|
||||
controller.enqueue(encoder.encode(""));
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const stopEvent = await result;
|
||||
const generator = stopEvent.data.result;
|
||||
const { value, done } = await generator.next();
|
||||
async pull(controller) {
|
||||
while (!generator) {
|
||||
// get next event from workflow context
|
||||
const { value: event, done } =
|
||||
await context[Symbol.asyncIterator]().next();
|
||||
if (done) {
|
||||
closeStreams(controller);
|
||||
return;
|
||||
}
|
||||
generator = handleEvent(event, dataStream);
|
||||
}
|
||||
|
||||
const { value: chunk, done } = await generator.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
closeStreams(controller);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = trimStartOfStream(value.delta ?? "");
|
||||
if (text) controller.enqueue(text);
|
||||
const text = trimStartOfStream(chunk.delta ?? "");
|
||||
if (text) {
|
||||
controller.enqueue(encoder.encode(text));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
stream: mainStream.pipeThrough(createStreamDataTransformer()),
|
||||
dataStream,
|
||||
};
|
||||
}
|
||||
|
||||
export async function workflowEventsToStreamData(
|
||||
events: AsyncIterable<AgentRunEvent>,
|
||||
): Promise<StreamData> {
|
||||
const streamData = new StreamData();
|
||||
|
||||
(async () => {
|
||||
for await (const event of events) {
|
||||
if (event instanceof AgentRunEvent) {
|
||||
const { name, msg } = event.data;
|
||||
if ((streamData as any).isClosed) {
|
||||
break;
|
||||
}
|
||||
streamData.appendMessageAnnotation({
|
||||
type: "agent",
|
||||
data: { agent: name, text: msg },
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return streamData;
|
||||
function handleEvent(
|
||||
event: WorkflowEvent<any>,
|
||||
dataStream: StreamData,
|
||||
): AsyncGenerator<ChatResponseChunk> | undefined {
|
||||
// Handle for StopEvent
|
||||
if (event instanceof StopEvent) {
|
||||
return event.data as AsyncGenerator<ChatResponseChunk>;
|
||||
}
|
||||
// Handle for AgentRunEvent
|
||||
if (event instanceof AgentRunEvent) {
|
||||
dataStream.appendMessageAnnotation({
|
||||
type: "agent",
|
||||
data: event.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { HandlerContext } from "@llamaindex/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
callTool,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseChunk,
|
||||
LlamaCloudIndex,
|
||||
PartialToolCall,
|
||||
QueryEngineTool,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
ToolCallLLMMessageOptions,
|
||||
} from "llamaindex";
|
||||
import crypto from "node:crypto";
|
||||
import { getDataSource } from "../engine";
|
||||
import { AgentRunEvent } from "./type";
|
||||
|
||||
export const getQueryEngineTools = async (): Promise<
|
||||
QueryEngineTool[] | null
|
||||
> => {
|
||||
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
|
||||
|
||||
const index = await getDataSource();
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
// index is LlamaCloudIndex use two query engine tools
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
return [
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
retrieval_mode: "files_via_content",
|
||||
}),
|
||||
metadata: {
|
||||
name: "document_retriever",
|
||||
description: `Document retriever that retrieves entire documents from the corpus.
|
||||
ONLY use for research questions that may require searching over entire research reports.
|
||||
Will be slower and more expensive than chunk-level retrieval but may be necessary.`,
|
||||
},
|
||||
}),
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
retrieval_mode: "chunks",
|
||||
}),
|
||||
metadata: {
|
||||
name: "chunk_retriever",
|
||||
description: `Retrieves a small set of relevant document chunks from the corpus.
|
||||
Use for research questions that want to look up specific facts from the knowledge corpus,
|
||||
and need entire documents.`,
|
||||
},
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
}),
|
||||
metadata: {
|
||||
name: "retriever",
|
||||
description: `Use this tool to retrieve information about the text corpus from the index.`,
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Call multiple tools and return the tool messages
|
||||
*/
|
||||
export const callTools = async <T>({
|
||||
tools,
|
||||
toolCalls,
|
||||
ctx,
|
||||
agentName,
|
||||
writeEvent = true,
|
||||
}: {
|
||||
toolCalls: ToolCall[];
|
||||
tools: BaseToolWithCall[];
|
||||
ctx: HandlerContext<T>;
|
||||
agentName: string;
|
||||
writeEvent?: boolean;
|
||||
}): Promise<ChatMessage[]> => {
|
||||
const toolMsgs: ChatMessage[] = [];
|
||||
if (toolCalls.length === 0) {
|
||||
return toolMsgs;
|
||||
}
|
||||
if (toolCalls.length === 1) {
|
||||
const tool = tools.find((tool) => tool.metadata.name === toolCalls[0].name);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool ${toolCalls[0].name} not found`);
|
||||
}
|
||||
return [
|
||||
await callSingleTool(
|
||||
tool,
|
||||
toolCalls[0],
|
||||
writeEvent
|
||||
? (msg: string) => {
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: agentName,
|
||||
text: msg,
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
];
|
||||
}
|
||||
// Multiple tool calls, show events in progress
|
||||
const progressId = crypto.randomUUID();
|
||||
const totalSteps = toolCalls.length;
|
||||
let currentStep = 0;
|
||||
for (const toolCall of toolCalls) {
|
||||
const tool = tools.find((tool) => tool.metadata.name === toolCall.name);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool ${toolCall.name} not found`);
|
||||
}
|
||||
const toolMsg = await callSingleTool(tool, toolCall, (msg: string) => {
|
||||
ctx.sendEvent(
|
||||
new AgentRunEvent({
|
||||
agent: agentName,
|
||||
text: msg,
|
||||
type: "progress",
|
||||
data: {
|
||||
id: progressId,
|
||||
total: totalSteps,
|
||||
current: currentStep,
|
||||
},
|
||||
}),
|
||||
);
|
||||
currentStep++;
|
||||
});
|
||||
toolMsgs.push(toolMsg);
|
||||
}
|
||||
return toolMsgs;
|
||||
};
|
||||
|
||||
export const callSingleTool = async (
|
||||
tool: BaseToolWithCall,
|
||||
toolCall: ToolCall,
|
||||
eventEmitter?: (msg: string) => void,
|
||||
): Promise<ChatMessage> => {
|
||||
if (eventEmitter) {
|
||||
eventEmitter(
|
||||
`Calling tool ${toolCall.name} with input: ${JSON.stringify(toolCall.input)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const toolOutput = await callTool(tool, toolCall, {
|
||||
log: () => {},
|
||||
error: (...args: unknown[]) => {
|
||||
console.error(`Tool ${toolCall.name} got error:`, ...args);
|
||||
if (eventEmitter) {
|
||||
eventEmitter(`Tool ${toolCall.name} got error: ${args.join(" ")}`);
|
||||
}
|
||||
return {
|
||||
content: JSON.stringify({
|
||||
error: args.join(" "),
|
||||
}),
|
||||
role: "user",
|
||||
options: {
|
||||
toolResult: {
|
||||
id: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
error: args.join(" "),
|
||||
}),
|
||||
isError: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
return {
|
||||
content: JSON.stringify(toolOutput.output),
|
||||
role: "user",
|
||||
options: {
|
||||
toolResult: {
|
||||
result: toolOutput.output,
|
||||
isError: toolOutput.isError,
|
||||
id: toolCall.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
class ChatWithToolsResponse {
|
||||
toolCalls: ToolCall[];
|
||||
toolCallMessage?: ChatMessage;
|
||||
responseGenerator?: AsyncGenerator<ChatResponseChunk>;
|
||||
|
||||
constructor(options: {
|
||||
toolCalls: ToolCall[];
|
||||
toolCallMessage?: ChatMessage;
|
||||
responseGenerator?: AsyncGenerator<ChatResponseChunk>;
|
||||
}) {
|
||||
this.toolCalls = options.toolCalls;
|
||||
this.toolCallMessage = options.toolCallMessage;
|
||||
this.responseGenerator = options.responseGenerator;
|
||||
}
|
||||
|
||||
hasMultipleTools() {
|
||||
const uniqueToolNames = new Set(this.getToolNames());
|
||||
return uniqueToolNames.size > 1;
|
||||
}
|
||||
|
||||
hasToolCall() {
|
||||
return this.toolCalls.length > 0;
|
||||
}
|
||||
|
||||
getToolNames() {
|
||||
return this.toolCalls.map((toolCall) => toolCall.name);
|
||||
}
|
||||
|
||||
async asFullResponse(): Promise<ChatMessage> {
|
||||
if (!this.responseGenerator) {
|
||||
throw new Error("No response generator");
|
||||
}
|
||||
let fullResponse = "";
|
||||
for await (const chunk of this.responseGenerator) {
|
||||
fullResponse += chunk.delta;
|
||||
}
|
||||
return {
|
||||
role: "assistant",
|
||||
content: fullResponse,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const chatWithTools = async (
|
||||
llm: ToolCallLLM,
|
||||
tools: BaseToolWithCall[],
|
||||
messages: ChatMessage[],
|
||||
): Promise<ChatWithToolsResponse> => {
|
||||
const responseGenerator = async function* (): AsyncGenerator<
|
||||
boolean | ChatResponseChunk,
|
||||
void,
|
||||
unknown
|
||||
> {
|
||||
const responseStream = await llm.chat({ messages, tools, stream: true });
|
||||
|
||||
let fullResponse = null;
|
||||
let yieldedIndicator = false;
|
||||
const toolCallMap = new Map();
|
||||
for await (const chunk of responseStream) {
|
||||
const hasToolCalls = chunk.options && "toolCall" in chunk.options;
|
||||
if (!hasToolCalls) {
|
||||
if (!yieldedIndicator) {
|
||||
yield false;
|
||||
yieldedIndicator = true;
|
||||
}
|
||||
yield chunk;
|
||||
} else if (!yieldedIndicator) {
|
||||
yield true;
|
||||
yieldedIndicator = true;
|
||||
}
|
||||
|
||||
if (chunk.options && "toolCall" in chunk.options) {
|
||||
for (const toolCall of chunk.options.toolCall as PartialToolCall[]) {
|
||||
if (toolCall.id) {
|
||||
toolCallMap.set(toolCall.id, toolCall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
hasToolCalls &&
|
||||
(chunk.raw as any)?.choices?.[0]?.finish_reason !== null
|
||||
) {
|
||||
// Update the fullResponse with the tool calls
|
||||
const toolCalls = Array.from(toolCallMap.values());
|
||||
fullResponse = {
|
||||
...chunk,
|
||||
options: {
|
||||
...chunk.options,
|
||||
toolCall: toolCalls,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (fullResponse) {
|
||||
yield fullResponse;
|
||||
}
|
||||
};
|
||||
|
||||
const generator = responseGenerator();
|
||||
const isToolCall = await generator.next();
|
||||
|
||||
if (isToolCall.value) {
|
||||
// If it's a tool call, we need to wait for the full response
|
||||
let fullResponse = null;
|
||||
for await (const chunk of generator) {
|
||||
fullResponse = chunk;
|
||||
}
|
||||
|
||||
if (fullResponse) {
|
||||
const responseChunk = fullResponse as ChatResponseChunk;
|
||||
const toolCalls = getToolCallsFromResponse(responseChunk);
|
||||
return new ChatWithToolsResponse({
|
||||
toolCalls,
|
||||
toolCallMessage: {
|
||||
options: responseChunk.options,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Cannot get tool calls from response");
|
||||
}
|
||||
}
|
||||
|
||||
return new ChatWithToolsResponse({
|
||||
toolCalls: [],
|
||||
responseGenerator: generator as AsyncGenerator<ChatResponseChunk>,
|
||||
});
|
||||
};
|
||||
|
||||
export const getToolCallsFromResponse = (
|
||||
response:
|
||||
| ChatResponse<ToolCallLLMMessageOptions>
|
||||
| ChatResponseChunk<ToolCallLLMMessageOptions>,
|
||||
): ToolCall[] => {
|
||||
let options;
|
||||
|
||||
if ("message" in response) {
|
||||
options = response.message.options;
|
||||
} else {
|
||||
options = response.options;
|
||||
}
|
||||
|
||||
if (options && "toolCall" in options) {
|
||||
return options.toolCall as ToolCall[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
@@ -1,11 +1,24 @@
|
||||
import { WorkflowEvent } from "@llamaindex/core/workflow";
|
||||
import { WorkflowEvent } from "@llamaindex/workflow";
|
||||
import { MessageContent } from "llamaindex";
|
||||
|
||||
export type AgentInput = {
|
||||
message: string;
|
||||
message: MessageContent;
|
||||
streaming?: boolean;
|
||||
};
|
||||
|
||||
export type AgentRunEventType = "text" | "progress";
|
||||
|
||||
export type ProgressEventData = {
|
||||
id: string;
|
||||
total: number;
|
||||
current: number;
|
||||
};
|
||||
|
||||
export type AgentRunEventData = ProgressEventData;
|
||||
|
||||
export class AgentRunEvent extends WorkflowEvent<{
|
||||
name: string;
|
||||
msg: string;
|
||||
agent: string;
|
||||
text: string;
|
||||
type: AgentRunEventType;
|
||||
data?: AgentRunEventData;
|
||||
}> {}
|
||||
|
||||
@@ -60,10 +60,12 @@ class NextQuestionSuggestion:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _extract_questions(cls, text: str) -> List[str]:
|
||||
def _extract_questions(cls, text: str) -> List[str] | None:
|
||||
content_match = re.search(r"```(.*?)```", text, re.DOTALL)
|
||||
content = content_match.group(1) if content_match else ""
|
||||
return content.strip().split("\n")
|
||||
content = content_match.group(1) if content_match else None
|
||||
if not content:
|
||||
return None
|
||||
return [q.strip() for q in content.split("\n") if q.strip()]
|
||||
|
||||
@classmethod
|
||||
async def suggest_next_questions(
|
||||
|
||||
@@ -21,6 +21,8 @@ def init_settings():
|
||||
init_mistral()
|
||||
case "azure-openai":
|
||||
init_azure_openai()
|
||||
case "huggingface":
|
||||
init_huggingface()
|
||||
case "t-systems":
|
||||
from .llmhub import init_llmhub
|
||||
|
||||
@@ -138,6 +140,42 @@ def init_fastembed():
|
||||
)
|
||||
|
||||
|
||||
def init_huggingface_embedding():
|
||||
try:
|
||||
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Hugging Face support is not installed. Please install it with `poetry add llama-index-embeddings-huggingface`"
|
||||
)
|
||||
|
||||
embedding_model = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
||||
backend = os.getenv("EMBEDDING_BACKEND", "onnx") # "torch", "onnx", or "openvino"
|
||||
trust_remote_code = (
|
||||
os.getenv("EMBEDDING_TRUST_REMOTE_CODE", "false").lower() == "true"
|
||||
)
|
||||
|
||||
Settings.embed_model = HuggingFaceEmbedding(
|
||||
model_name=embedding_model,
|
||||
trust_remote_code=trust_remote_code,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
|
||||
def init_huggingface():
|
||||
try:
|
||||
from llama_index.llms.huggingface import HuggingFaceLLM
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Hugging Face support is not installed. Please install it with `poetry add llama-index-llms-huggingface` and `poetry add llama-index-embeddings-huggingface`"
|
||||
)
|
||||
|
||||
Settings.llm = HuggingFaceLLM(
|
||||
model_name=os.getenv("MODEL"),
|
||||
tokenizer_name=os.getenv("MODEL"),
|
||||
)
|
||||
init_huggingface_embedding()
|
||||
|
||||
|
||||
def init_groq():
|
||||
try:
|
||||
from llama_index.llms.groq import Groq
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as dotenv from "dotenv";
|
||||
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { STORAGE_CACHE_DIR } from "./shared";
|
||||
|
||||
// Load environment variables from local .env file
|
||||
dotenv.config();
|
||||
@@ -20,9 +19,13 @@ async function getRuntime(func: any) {
|
||||
async function generateDatasource() {
|
||||
console.log(`Generating storage context...`);
|
||||
// Split documents, create embeddings and store them in the storage context
|
||||
const persistDir = process.env.STORAGE_CACHE_DIR;
|
||||
if (!persistDir) {
|
||||
throw new Error("STORAGE_CACHE_DIR environment variable is required!");
|
||||
}
|
||||
const ms = await getRuntime(async () => {
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: STORAGE_CACHE_DIR,
|
||||
persistDir,
|
||||
});
|
||||
const documents = await getDocuments();
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { SimpleDocumentStore, VectorStoreIndex } from "llamaindex";
|
||||
import { storageContextFromDefaults } from "llamaindex/storage/StorageContext";
|
||||
import { STORAGE_CACHE_DIR } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
const persistDir = process.env.STORAGE_CACHE_DIR;
|
||||
if (!persistDir) {
|
||||
throw new Error("STORAGE_CACHE_DIR environment variable is required!");
|
||||
}
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: `${STORAGE_CACHE_DIR}`,
|
||||
persistDir,
|
||||
});
|
||||
|
||||
const numberOfDocs = Object.keys(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const STORAGE_CACHE_DIR = "./cache";
|
||||
@@ -13,6 +13,7 @@ python = "^3.11,<4.0"
|
||||
fastapi = "^0.109.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
python-dotenv = "^1.0.0"
|
||||
pydantic = "<2.10"
|
||||
llama-index = "^0.11.1"
|
||||
cachetools = "^5.3.3"
|
||||
reflex = "^0.6.2.post1"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import js from "@eslint/js";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
...compat.extends("eslint:recommended", "prettier"),
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
ignores: ["prettier.config.cjs"],
|
||||
},
|
||||
{ files: ["**/*.{ts}"] },
|
||||
{
|
||||
rules: {
|
||||
"max-params": ["error", 4],
|
||||
"prefer-const": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "prettier"],
|
||||
"rules": {
|
||||
"max-params": ["error", 4],
|
||||
"prefer-const": "error"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import cors from "cors";
|
||||
import "dotenv/config";
|
||||
import express, { Express, Request, Response } from "express";
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"build": "tsup index.ts --format esm --dts",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon --watch dist/index.js\""
|
||||
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon --watch dist/index.js\"",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "3.3.42",
|
||||
@@ -27,14 +28,18 @@
|
||||
"got": "^14.4.1",
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"formdata-node": "^6.0.3",
|
||||
"marked": "^14.1.2"
|
||||
"marked": "^14.1.2",
|
||||
"papaparse": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.16",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.9.5",
|
||||
"typescript-eslint": "^8.14.0",
|
||||
"@llamaindex/workflow": "^0.0.3",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
@@ -32,7 +32,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
|
||||
// Setup callbacks
|
||||
const callbackManager = createCallbackManager(vercelStreamData);
|
||||
const chatHistory: ChatMessage[] = messages as ChatMessage[];
|
||||
const chatHistory: ChatMessage[] = messages.slice(0, -1) as ChatMessage[];
|
||||
|
||||
// Calling LlamaIndex's ChatEngine to get a streamed response
|
||||
const response = await Settings.withCallbackManager(callbackManager, () => {
|
||||
|
||||
@@ -21,12 +21,14 @@ Second, generate the embeddings of the documents in the `./data` directory (if t
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
Third, run the app:
|
||||
|
||||
```
|
||||
python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The example provides two different API endpoints:
|
||||
|
||||
1. `/api/chat` - a streaming chat endpoint
|
||||
@@ -50,12 +52,10 @@ curl --location 'localhost:8000/api/chat/request' \
|
||||
|
||||
You can start editing the API endpoints by modifying `app/api/routers/chat.py`. The endpoints auto-update as you save the file. You can delete the endpoint you're not using.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## Using Docker
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import os
|
||||
|
||||
DATA_DIR = "data"
|
||||
STATIC_DIR = os.getenv("STATIC_DIR", "static")
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import logging
|
||||
from typing import Set
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class FrontendProxyMiddleware:
|
||||
"""
|
||||
Proxy requests to the frontend development server
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
frontend_endpoint: str,
|
||||
excluded_paths: Set[str],
|
||||
):
|
||||
self.app = app
|
||||
self.excluded_paths = excluded_paths
|
||||
self.frontend_endpoint = frontend_endpoint
|
||||
|
||||
async def _request_frontend(
|
||||
self,
|
||||
request: Request,
|
||||
path: str,
|
||||
timeout: float = 60.0,
|
||||
):
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
url = f"{self.frontend_endpoint}/{path}"
|
||||
if request.query_params:
|
||||
url = f"{url}?{request.query_params}"
|
||||
|
||||
headers = dict(request.headers)
|
||||
try:
|
||||
body = await request.body() if request.method != "GET" else None
|
||||
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
response_headers = dict(response.headers)
|
||||
response_headers.pop("content-encoding", None)
|
||||
response_headers.pop("content-length", None)
|
||||
|
||||
return StreamingResponse(
|
||||
response.iter_bytes(),
|
||||
status_code=response.status_code,
|
||||
headers=response_headers,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Proxy error: {str(e)}")
|
||||
raise
|
||||
|
||||
def _is_excluded_path(self, path: str) -> bool:
|
||||
return any(
|
||||
path.startswith(excluded_path) for excluded_path in self.excluded_paths
|
||||
)
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
request = Request(scope, receive)
|
||||
path = request.url.path
|
||||
|
||||
if self._is_excluded_path(path):
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
response = await self._request_frontend(request, path.lstrip("/"))
|
||||
return await response(scope, receive, send)
|
||||
@@ -1,2 +1,3 @@
|
||||
# TODO: You can add observability here. For templates re-start `create-llama` with `--pro` flag to generate a new project with observability.
|
||||
def init_observability():
|
||||
pass
|
||||
|
||||
@@ -242,13 +242,11 @@ class FileService:
|
||||
except ImportError as e:
|
||||
raise ValueError("LlamaCloudFileService is not found") from e
|
||||
|
||||
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,
|
||||
index.project.id,
|
||||
index.pipeline.id,
|
||||
upload_file,
|
||||
custom_metadata={},
|
||||
)
|
||||
|
||||
@@ -2,3 +2,4 @@ __pycache__
|
||||
storage
|
||||
.env
|
||||
output
|
||||
static/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# flake8: noqa: E402
|
||||
from app.config import DATA_DIR
|
||||
from app.config import DATA_DIR, STATIC_DIR
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
@@ -9,10 +9,10 @@ import os
|
||||
|
||||
import uvicorn
|
||||
from app.api.routers import api_router
|
||||
from app.middlewares.frontend import FrontendProxyMiddleware
|
||||
from app.observability import init_observability
|
||||
from app.settings import init_settings
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
@@ -24,38 +24,43 @@ init_observability()
|
||||
environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
if environment == "dev":
|
||||
logger.warning("Running in development mode - allowing CORS for all origins")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Redirect to documentation page when accessing base URL
|
||||
@app.get("/")
|
||||
async def redirect_to_docs():
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
|
||||
def mount_static_files(directory, path):
|
||||
def mount_static_files(directory, path, html=False):
|
||||
if os.path.exists(directory):
|
||||
logger.info(f"Mounting static files '{directory}' at '{path}'")
|
||||
app.mount(
|
||||
path,
|
||||
StaticFiles(directory=directory, check_dir=False),
|
||||
StaticFiles(directory=directory, check_dir=False, html=html),
|
||||
name=f"{directory}-static",
|
||||
)
|
||||
|
||||
|
||||
app.include_router(api_router, prefix="/api")
|
||||
|
||||
# Mount the data files to serve the file viewer
|
||||
mount_static_files(DATA_DIR, "/api/files/data")
|
||||
# Mount the output files from tools
|
||||
mount_static_files("output", "/api/files/output")
|
||||
|
||||
app.include_router(api_router, prefix="/api")
|
||||
if environment == "dev":
|
||||
frontend_endpoint = os.getenv("FRONTEND_ENDPOINT")
|
||||
if frontend_endpoint:
|
||||
app.add_middleware(
|
||||
FrontendProxyMiddleware,
|
||||
frontend_endpoint=frontend_endpoint,
|
||||
excluded_paths=set(
|
||||
route.path for route in app.routes if hasattr(route, "path")
|
||||
),
|
||||
)
|
||||
else:
|
||||
logger.warning("No frontend endpoint - starting API server only")
|
||||
|
||||
@app.get("/")
|
||||
async def redirect_to_docs():
|
||||
return RedirectResponse(url="/docs")
|
||||
else:
|
||||
# Mount the frontend static files (production)
|
||||
mount_static_files(STATIC_DIR, "/", html=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app_host = os.getenv("APP_HOST", "0.0.0.0")
|
||||
|
||||
@@ -7,15 +7,20 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
generate = "app.engine.generate:generate_datasource"
|
||||
dev = "run:dev" # Starts the app in dev mode
|
||||
prod = "run:prod" # Starts the app in prod mode
|
||||
build = "run:build" # Builds the frontend assets and copies them to the static directory
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.11,<3.13"
|
||||
python = ">=3.11,<3.14"
|
||||
fastapi = "^0.109.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
python-dotenv = "^1.0.0"
|
||||
pydantic = "<2.10"
|
||||
aiostream = "^0.5.2"
|
||||
cachetools = "^5.3.3"
|
||||
llama-index = "^0.11.17"
|
||||
rich = "^13.9.4"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = "^1.8.0"
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
from asyncio.subprocess import Process
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
import dotenv
|
||||
import rich
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
FRONTEND_DIR = Path(os.getenv("FRONTEND_DIR", ".frontend"))
|
||||
DEFAULT_FRONTEND_PORT = 3000
|
||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", "static"))
|
||||
|
||||
|
||||
def build():
|
||||
"""
|
||||
Build the frontend and copy the static files to the backend.
|
||||
|
||||
Raises:
|
||||
SystemError: If any build step fails
|
||||
"""
|
||||
static_dir = Path("static")
|
||||
|
||||
try:
|
||||
package_manager = _get_node_package_manager()
|
||||
_install_frontend_dependencies()
|
||||
|
||||
rich.print("\n[bold]Building the frontend[/bold]")
|
||||
run([package_manager, "run", "build"], cwd=FRONTEND_DIR, check=True)
|
||||
|
||||
if static_dir.exists():
|
||||
shutil.rmtree(static_dir)
|
||||
static_dir.mkdir(exist_ok=True)
|
||||
|
||||
shutil.copytree(FRONTEND_DIR / "out", static_dir, dirs_exist_ok=True)
|
||||
|
||||
rich.print(
|
||||
"\n[bold]Built frontend successfully![/bold]"
|
||||
"\n[bold]Run: 'poetry run prod' to start the app[/bold]"
|
||||
"\n[bold]Don't forget to update the .env file![/bold]"
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
raise SystemError(f"Build failed during {e.cmd}") from e
|
||||
except Exception as e:
|
||||
raise SystemError(f"Build failed: {str(e)}") from e
|
||||
|
||||
|
||||
def dev():
|
||||
asyncio.run(start_development_servers())
|
||||
|
||||
|
||||
def prod():
|
||||
asyncio.run(start_production_server())
|
||||
|
||||
|
||||
async def start_development_servers():
|
||||
"""
|
||||
Start both frontend and backend development servers.
|
||||
Frontend runs with hot reloading, backend runs FastAPI server.
|
||||
|
||||
Raises:
|
||||
SystemError: If either server fails to start
|
||||
"""
|
||||
rich.print("\n[bold]Starting development servers[/bold]")
|
||||
|
||||
try:
|
||||
processes = []
|
||||
if _is_frontend_included():
|
||||
frontend_process, frontend_port = await _run_frontend()
|
||||
processes.append(frontend_process)
|
||||
backend_process = await _run_backend(
|
||||
envs={
|
||||
"ENVIRONMENT": "dev",
|
||||
"FRONTEND_ENDPOINT": f"http://localhost:{frontend_port}",
|
||||
},
|
||||
)
|
||||
processes.append(backend_process)
|
||||
else:
|
||||
backend_process = await _run_backend(
|
||||
envs={"ENVIRONMENT": "dev"},
|
||||
)
|
||||
processes.append(backend_process)
|
||||
|
||||
try:
|
||||
# Wait for processes to complete
|
||||
await asyncio.gather(*[process.wait() for process in processes])
|
||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||
rich.print("\n[bold yellow]Shutting down...[/bold yellow]")
|
||||
finally:
|
||||
# Terminate both processes
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
|
||||
except Exception as e:
|
||||
raise SystemError(f"Failed to start development servers: {str(e)}") from e
|
||||
|
||||
|
||||
async def start_production_server():
|
||||
if _is_frontend_included():
|
||||
is_frontend_built = (FRONTEND_DIR / "out" / "index.html").exists()
|
||||
is_frontend_static_dir_exists = STATIC_DIR.exists()
|
||||
if not is_frontend_built or not is_frontend_static_dir_exists:
|
||||
build()
|
||||
|
||||
try:
|
||||
process = await _run_backend(
|
||||
envs={"ENVIRONMENT": "prod"},
|
||||
)
|
||||
await process.wait()
|
||||
except Exception as e:
|
||||
raise SystemError(f"Failed to start production server: {str(e)}") from e
|
||||
finally:
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
|
||||
|
||||
async def _run_frontend(
|
||||
port: int = DEFAULT_FRONTEND_PORT,
|
||||
timeout: int = 5,
|
||||
) -> tuple[Process, int]:
|
||||
"""
|
||||
Start the frontend development server and return its process and port.
|
||||
|
||||
Returns:
|
||||
tuple[Process, int]: The frontend process and the port it's running on
|
||||
"""
|
||||
# Install dependencies
|
||||
_install_frontend_dependencies()
|
||||
|
||||
port = _find_free_port(start_port=DEFAULT_FRONTEND_PORT)
|
||||
package_manager = _get_node_package_manager()
|
||||
frontend_process = await asyncio.create_subprocess_exec(
|
||||
package_manager,
|
||||
"run",
|
||||
"dev",
|
||||
"-p",
|
||||
str(port),
|
||||
cwd=FRONTEND_DIR,
|
||||
)
|
||||
rich.print(
|
||||
f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]"
|
||||
)
|
||||
# Block until the frontend is accessible
|
||||
for _ in range(timeout):
|
||||
await asyncio.sleep(1)
|
||||
# Check if the frontend is accessible (port is open) or frontend_process is running
|
||||
if frontend_process.returncode is not None:
|
||||
raise RuntimeError("Could not start frontend dev server")
|
||||
if not _is_bindable_port(port):
|
||||
rich.print(
|
||||
f"\n[bold green]Frontend dev server is running on port {port}[/bold green]"
|
||||
)
|
||||
return frontend_process, port
|
||||
raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds")
|
||||
|
||||
|
||||
async def _run_backend(
|
||||
envs: dict[str, str | None] = {},
|
||||
) -> Process:
|
||||
"""
|
||||
Start the backend development server.
|
||||
|
||||
Args:
|
||||
frontend_port: The port number the frontend is running on
|
||||
Returns:
|
||||
Process: The backend process
|
||||
"""
|
||||
# Merge environment variables
|
||||
envs = {**os.environ, **(envs or {})}
|
||||
rich.print("\n[bold]Starting backend FastAPI server...[/bold]")
|
||||
poetry_executable = _get_poetry_executable()
|
||||
return await asyncio.create_subprocess_exec(
|
||||
poetry_executable,
|
||||
"run",
|
||||
"python",
|
||||
"main.py",
|
||||
env=envs,
|
||||
)
|
||||
|
||||
|
||||
def _install_frontend_dependencies():
|
||||
package_manager = _get_node_package_manager()
|
||||
rich.print(
|
||||
f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]"
|
||||
)
|
||||
run([package_manager, "install"], cwd=".frontend", check=True)
|
||||
|
||||
|
||||
def _get_node_package_manager() -> str:
|
||||
"""
|
||||
Check for available package managers and return the preferred one.
|
||||
Returns 'pnpm' if installed, falls back to 'npm'.
|
||||
Raises SystemError if neither is installed.
|
||||
|
||||
Returns:
|
||||
str: The full path to the available package manager executable
|
||||
"""
|
||||
# On Windows, we need to check for .cmd extensions
|
||||
pnpm_cmds = ["pnpm", "pnpm.cmd"]
|
||||
npm_cmds = ["npm", "npm.cmd"]
|
||||
|
||||
for cmd in pnpm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
|
||||
for cmd in npm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
|
||||
raise SystemError(
|
||||
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
|
||||
)
|
||||
|
||||
|
||||
def _get_poetry_executable() -> str:
|
||||
"""
|
||||
Check for available Poetry executables and return the preferred one.
|
||||
Returns 'poetry' if installed, falls back to 'poetry.cmd'.
|
||||
Raises SystemError if neither is installed.
|
||||
|
||||
Returns:
|
||||
str: The full path to the available Poetry executable
|
||||
"""
|
||||
poetry_cmds = ["poetry", "poetry.cmd"]
|
||||
for cmd in poetry_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
raise SystemError("Poetry is not installed. Please install Poetry first.")
|
||||
|
||||
|
||||
def _is_bindable_port(port: int) -> bool:
|
||||
"""Check if a port is available by attempting to connect to it."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
# Try to connect to the port
|
||||
s.connect(("localhost", port))
|
||||
# If we can connect, port is in use
|
||||
return False
|
||||
except ConnectionRefusedError:
|
||||
# Connection refused means port is available
|
||||
return True
|
||||
except socket.error:
|
||||
# Other socket errors also likely mean port is available
|
||||
return True
|
||||
|
||||
|
||||
def _find_free_port(start_port: int) -> int:
|
||||
"""
|
||||
Find a free port starting from the given port number.
|
||||
"""
|
||||
for port in range(start_port, 65535):
|
||||
if _is_bindable_port(port):
|
||||
return port
|
||||
raise SystemError("No free port found")
|
||||
|
||||
|
||||
def _is_frontend_included() -> bool:
|
||||
"""Check if the app has frontend"""
|
||||
return FRONTEND_DIR.exists()
|
||||
@@ -45,7 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Setup callbacks
|
||||
const callbackManager = createCallbackManager(vercelStreamData);
|
||||
const chatHistory: ChatMessage[] = messages as ChatMessage[];
|
||||
const chatHistory: ChatMessage[] = messages.slice(0, -1) as ChatMessage[];
|
||||
|
||||
// Calling LlamaIndex's ChatEngine to get a streamed response
|
||||
const response = await Settings.withCallbackManager(callbackManager, () => {
|
||||
|
||||
@@ -9,9 +9,9 @@ import { DATA_DIR } from "../../chat/engine/loader";
|
||||
*/
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { slug: string[] } },
|
||||
{ params }: { params: Promise<{ slug: string[] }> },
|
||||
) {
|
||||
const slug = params.slug;
|
||||
const slug = (await params).slug;
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json({ detail: "Missing file slug" }, { status: 400 });
|
||||
@@ -21,7 +21,7 @@ export async function GET(
|
||||
return NextResponse.json({ detail: "Invalid file path" }, { status: 400 });
|
||||
}
|
||||
|
||||
const [folder, ...pathTofile] = params.slug; // data, file.pdf
|
||||
const [folder, ...pathTofile] = slug; // data, file.pdf
|
||||
const allowedFolders = ["data", "output"];
|
||||
|
||||
if (!allowedFolders.includes(folder)) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui";
|
||||
import "@llamaindex/chat-ui/styles/code.css";
|
||||
import "@llamaindex/chat-ui/styles/katex.css";
|
||||
import "@llamaindex/chat-ui/styles/pdf.css";
|
||||
import { useChat } from "ai/react";
|
||||
import CustomChatInput from "./ui/chat/chat-input";
|
||||
import CustomChatMessages from "./ui/chat/chat-messages";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ChatInput, useChatUI, useFile } from "@llamaindex/chat-ui";
|
||||
import { DocumentPreview, ImagePreview } from "@llamaindex/chat-ui/widgets";
|
||||
import { DocumentInfo, ImagePreview } from "@llamaindex/chat-ui/widgets";
|
||||
import { LlamaCloudSelector } from "./custom/llama-cloud-selector";
|
||||
import { useClientConfig } from "./hooks/use-config";
|
||||
|
||||
@@ -56,9 +56,10 @@ export default function CustomChatInput() {
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-4 w-full overflow-auto py-2">
|
||||
{files.map((file) => (
|
||||
<DocumentPreview
|
||||
<DocumentInfo
|
||||
key={file.id}
|
||||
file={file}
|
||||
document={{ url: file.url, sources: [] }}
|
||||
className="mb-2 mt-2"
|
||||
onRemove={() => removeDoc(file)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
ContentPosition,
|
||||
getSourceAnnotationData,
|
||||
useChatMessage,
|
||||
useChatUI,
|
||||
} from "@llamaindex/chat-ui";
|
||||
import { Markdown } from "./custom/markdown";
|
||||
import { ToolAnnotations } from "./tools/chat-tools";
|
||||
|
||||
export function ChatMessageContent() {
|
||||
const { isLoading, append } = useChatUI();
|
||||
const { message } = useChatMessage();
|
||||
const customContent = [
|
||||
{
|
||||
@@ -26,5 +28,11 @@ export function ChatMessageContent() {
|
||||
component: <ToolAnnotations message={message} />,
|
||||
},
|
||||
];
|
||||
return <ChatMessage.Content content={customContent} />;
|
||||
return (
|
||||
<ChatMessage.Content
|
||||
content={customContent}
|
||||
isLoading={isLoading}
|
||||
append={append}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function CustomChatMessages() {
|
||||
<ChatMessages.List>
|
||||
{messages.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
key={index}
|
||||
message={message}
|
||||
isLast={index === messages.length - 1}
|
||||
>
|
||||
|
||||
@@ -1,50 +1,40 @@
|
||||
import {
|
||||
getAnnotationData,
|
||||
Message,
|
||||
MessageAnnotation,
|
||||
useChatMessage,
|
||||
getAnnotationData,
|
||||
useChatUI,
|
||||
} from "@llamaindex/chat-ui";
|
||||
import { JSONValue, Message } from "ai";
|
||||
import { JSONValue } from "ai";
|
||||
import { useMemo } from "react";
|
||||
import { Artifact, CodeArtifact } from "./artifact";
|
||||
import { WeatherCard, WeatherData } from "./weather-card";
|
||||
|
||||
export function ToolAnnotations({ message }: { message: Message }) {
|
||||
// TODO: This is a bit of a hack to get the artifact version. better to generate the version in the tool call and
|
||||
// store it in CodeArtifact
|
||||
const { messages } = useChatUI();
|
||||
const artifactVersion = useMemo(
|
||||
() => getArtifactVersion(messages, message),
|
||||
[messages, message],
|
||||
);
|
||||
// Get the tool data from the message annotations
|
||||
const annotations = message.annotations as MessageAnnotation[] | undefined;
|
||||
const toolData = annotations
|
||||
? (getAnnotationData(annotations, "tools") as unknown as ToolData[])
|
||||
: null;
|
||||
return toolData?.[0] ? <ChatTools data={toolData[0]} /> : null;
|
||||
return toolData?.[0] ? (
|
||||
<ChatTools data={toolData[0]} artifactVersion={artifactVersion} />
|
||||
) : null;
|
||||
}
|
||||
|
||||
// TODO: Used to render outputs of tools. If needed, add more renderers here.
|
||||
function ChatTools({ data }: { data: ToolData }) {
|
||||
const { messages } = useChatUI();
|
||||
const { message } = useChatMessage();
|
||||
|
||||
// build a map of message id to artifact version
|
||||
const artifactVersionMap = useMemo(() => {
|
||||
const map = new Map<string, number | undefined>();
|
||||
let versionIndex = 1;
|
||||
messages.forEach((m) => {
|
||||
m.annotations?.forEach((annotation: any) => {
|
||||
if (
|
||||
typeof annotation === "object" &&
|
||||
annotation != null &&
|
||||
"type" in annotation &&
|
||||
annotation.type === "tools"
|
||||
) {
|
||||
const data = annotation.data as ToolData;
|
||||
if (data?.toolCall?.name === "artifact") {
|
||||
map.set(m.id, versionIndex);
|
||||
versionIndex++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return map;
|
||||
}, [messages]);
|
||||
|
||||
function ChatTools({
|
||||
data,
|
||||
artifactVersion,
|
||||
}: {
|
||||
data: ToolData;
|
||||
artifactVersion: number | undefined;
|
||||
}) {
|
||||
if (!data) return null;
|
||||
const { toolCall, toolOutput } = data;
|
||||
|
||||
@@ -66,7 +56,7 @@ function ChatTools({ data }: { data: ToolData }) {
|
||||
return (
|
||||
<Artifact
|
||||
artifact={toolOutput.output as CodeArtifact}
|
||||
version={artifactVersionMap.get(message.id)}
|
||||
version={artifactVersion}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -87,3 +77,25 @@ type ToolData = {
|
||||
isError: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
function getArtifactVersion(
|
||||
messages: Message[],
|
||||
message: Message,
|
||||
): number | undefined {
|
||||
const messageId = "id" in message ? message.id : undefined;
|
||||
if (!messageId) return undefined;
|
||||
let versionIndex = 1;
|
||||
for (const m of messages) {
|
||||
const toolData = m.annotations
|
||||
? (getAnnotationData(m.annotations, "tools") as unknown as ToolData[])
|
||||
: null;
|
||||
|
||||
if (toolData?.some((t) => t.toolCall.name === "artifact")) {
|
||||
if ("id" in m && m.id === messageId) {
|
||||
return versionIndex;
|
||||
}
|
||||
versionIndex++;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Drawer.displayName = "Drawer";
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
));
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
DrawerPortal,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
import * as React from "react";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "./markdown.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/* Custom CSS for chat message markdown */
|
||||
.custom-markdown ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-markdown ol {
|
||||
list-style-type: decimal;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-markdown li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.custom-markdown ol ol {
|
||||
list-style: lower-alpha;
|
||||
}
|
||||
|
||||
.custom-markdown ul ul,
|
||||
.custom-markdown ol ol {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.custom-markdown img {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.custom-markdown a {
|
||||
text-decoration: underline;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.custom-markdown h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.custom-markdown h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.custom-markdown h5 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.custom-markdown h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.custom-markdown h3 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.custom-markdown h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.custom-markdown h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.custom-markdown hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #e1e4e8;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"experimental": {
|
||||
"outputFileTracingIncludes": {
|
||||
"/*": ["./cache/**/*"]
|
||||
},
|
||||
"outputFileTracingExcludes": {
|
||||
"/api/files/*": [
|
||||
".next/**/*",
|
||||
"node_modules/**/*",
|
||||
"public/**/*",
|
||||
"app/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
"outputFileTracingIncludes": {
|
||||
"/*": ["./cache/**/*"]
|
||||
},
|
||||
"outputFileTracingExcludes": {
|
||||
"/api/files/*": [
|
||||
".next/**/*",
|
||||
"node_modules/**/*",
|
||||
"public/**/*",
|
||||
"app/**/*"
|
||||
]
|
||||
},
|
||||
"transpilePackages": ["highlight.js"]
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@e2b/code-interpreter": "0.0.9-beta.3",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"ai": "3.3.42",
|
||||
"@llamaindex/chat-ui": "0.0.9",
|
||||
"ai": "3.4.33",
|
||||
"ajv": "^8.12.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -27,28 +26,29 @@
|
||||
"formdata-node": "^6.0.3",
|
||||
"got": "^14.4.1",
|
||||
"llamaindex": "0.8.2",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"next": "^15.0.3",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"papaparse": "^5.4.1",
|
||||
"supports-color": "^8.1.1",
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tiktoken": "^1.0.15",
|
||||
"uuid": "^9.0.1",
|
||||
"vaul": "^0.9.1",
|
||||
"marked": "^14.1.2",
|
||||
"@llamaindex/chat-ui": "0.0.4"
|
||||
"marked": "^14.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@llamaindex/workflow": "^0.0.3",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-next": "^14.2.4",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.envFile": ""
|
||||
}
|
||||
Reference in New Issue
Block a user