mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 15:03:35 -04:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3658fec684 | |||
| c3d275abe1 | |||
| 61204a1381 | |||
| 9e723c3a15 | |||
| d5da55b993 | |||
| c1552ebb00 | |||
| 131e63ae4a | |||
| 4e06714cdd | |||
| 18c8d2540c | |||
| d4b4338f54 | |||
| b4e41aa526 | |||
| 860b9d46d4 | |||
| f73d46bf10 | |||
| eec237c5fe | |||
| 5450096e96 | |||
| 163492f189 | |||
| a84743c576 | |||
| fc5e56efa5 | |||
| a7a6592441 | |||
| af21426952 | |||
| 9077cae2f5 | |||
| 765d2c4fff | |||
| 25667d45e9 | |||
| d31910a303 | |||
| 9852e7399c | |||
| 95227a7539 | |||
| 71f29ea85d | |||
| 27d2499aff | |||
| a07f320e6d | |||
| f9a057ddde | |||
| aedd73d8c0 | |||
| da4505aff7 | |||
| 63e961e635 | |||
| fe90a7e7ee | |||
| 02b2473103 | |||
| f17449b90a | |||
| 28c8808ce3 | |||
| 0a7dfcf84b | |||
| 6e70e327d3 | |||
| 8b371d8347 | |||
| 30fe269575 | |||
| 49c35b834b | |||
| 82c2580ee5 | |||
| fc5b266a40 | |||
| f8f97d2c00 | |||
| 9c2e094883 | |||
| 00f0b3ae03 | |||
| 4663dec81d | |||
| 7f14e47f56 | |||
| 6925676013 | |||
| 44b34fb464 | |||
| a108911fc1 | |||
| 282eaa07fc | |||
| 80db5f7c46 | |||
| 7a22c9f56d | |||
| 8431b788ad | |||
| 2b712cebec | |||
| 6edea6af5c | |||
| d79d1652d1 |
@@ -69,11 +69,12 @@ jobs:
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
working-directory: .
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-python
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
|
||||
path: ./playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
e2e-typescript:
|
||||
@@ -136,9 +137,10 @@ jobs:
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
working-directory: .
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-typescript
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}
|
||||
path: ./playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
+129
@@ -1,5 +1,134 @@
|
||||
# create-llama
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 61204a1: chore: bump LITS 0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9e723c3: Standardize the code of the workflow use case (Python)
|
||||
- d5da55b: feat: add components.json to use CLI
|
||||
- c1552eb: chore: move wikipedia tool to create-llama
|
||||
|
||||
## 0.3.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4e06714: Fix the error: Unable to view file sources due to CORS.
|
||||
|
||||
## 0.3.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b4e41aa: Add deep research over own documents use case (Python)
|
||||
|
||||
## 0.3.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f73d46b: Fix missing copy of the multiagent code
|
||||
|
||||
## 0.3.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5450096: bump: react 19 stable
|
||||
|
||||
## 0.3.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a84743c: Change --agents paramameter to --use-case
|
||||
- a84743c: Add LlamaCloud support for Reflex templates
|
||||
- a7a6592: Fix the npm issue on the full-stack Python template
|
||||
- fc5e56e: bump: code interpreter v1
|
||||
|
||||
## 0.3.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9077cae: Add contract review use case (Python)
|
||||
|
||||
## 0.3.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25667d4: Make OpenAPI spec usable by custom GPTs
|
||||
|
||||
## 0.3.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 95227a7: Add query endpoint
|
||||
|
||||
## 0.3.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 27d2499: Bump the LlamaCloud library and fix breaking changes (Python).
|
||||
|
||||
## 0.3.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f9a057d: Add support multimodal indexes (e.g. from LlamaCloud)
|
||||
- aedd73d: bump: chat-ui
|
||||
|
||||
## 0.3.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fe90a7e: chore: bump ai v4
|
||||
- 02b2473: Show streaming errors in Python, optimize system prompts for tool usage and set the weather tool as default for the Agentic RAG use case
|
||||
- 63e961e: Use auto_routed retriever mode for LlamaCloudIndex
|
||||
|
||||
## 0.3.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 28c8808: Add fly.io deployment
|
||||
- 0a7dfcf: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend
|
||||
|
||||
## 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
|
||||
|
||||
+10
-22
@@ -7,17 +7,16 @@ 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,
|
||||
"appName" | "root" | "isOnline" | "customApiPath"
|
||||
"appName" | "root" | "isOnline" | "port"
|
||||
> & {
|
||||
appPath: string;
|
||||
frontend: boolean;
|
||||
@@ -35,13 +34,12 @@ export async function createApp({
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
|
||||
@@ -81,40 +79,30 @@ export async function createApp({
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
};
|
||||
|
||||
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`,
|
||||
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",
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { TemplateFramework } from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
|
||||
// The extractor template currently only works with FastAPI and files (and not on Windows)
|
||||
if (
|
||||
process.platform !== "win32" &&
|
||||
templateFramework === "fastapi" &&
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
test.describe("Test extractor template", async () => {
|
||||
let frontendPort: number;
|
||||
let backendPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
let cwd: string;
|
||||
|
||||
// Create extractor app
|
||||
test.beforeAll(async () => {
|
||||
cwd = await createTestDir();
|
||||
frontendPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
backendPort = frontendPort + 1;
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "extractor",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: frontendPort,
|
||||
externalPort: backendPort,
|
||||
postInstallAction: "runApp",
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
appProcess.kill();
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${frontendPort}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 2000 * 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -16,18 +16,17 @@ 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"];
|
||||
const templateUseCases = ["financial_report", "blog", "form_filling"];
|
||||
|
||||
for (const agents of templateAgents) {
|
||||
test.describe(`Test multiagent template ${agents} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
for (const useCase of templateUseCases) {
|
||||
test.describe(`Test multiagent template ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
test.skip(
|
||||
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
@@ -36,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,
|
||||
@@ -45,11 +43,10 @@ for (const agents of templateAgents) {
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
agents,
|
||||
useCase,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
@@ -61,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();
|
||||
});
|
||||
@@ -68,6 +69,13 @@ for (const agents of templateAgents) {
|
||||
test("Frontend should be able to submit a message and receive the start of a streamed response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
useCase === "financial_report" ||
|
||||
useCase === "form_filling" ||
|
||||
templateFramework === "express",
|
||||
"Skip chat tests for financial report and form filling.",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { TemplateFramework, TemplateUseCase } from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
|
||||
|
||||
// The reflex template currently only works with FastAPI and files (and not on Windows)
|
||||
if (
|
||||
process.platform !== "win32" &&
|
||||
templateFramework === "fastapi" &&
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
for (const useCase of templateUseCases) {
|
||||
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
|
||||
let appPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
let cwd: string;
|
||||
|
||||
// Create reflex app
|
||||
test.beforeAll(async () => {
|
||||
cwd = await createTestDir();
|
||||
appPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "reflex",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: appPort,
|
||||
postInstallAction: "runApp",
|
||||
useCase,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
appProcess.kill();
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${appPort}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 2000 * 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+6
-32
@@ -25,7 +25,6 @@ export type RunCreateLlamaOptions = {
|
||||
dataSource: string;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
externalPort: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
templateUI?: TemplateUI;
|
||||
appType?: AppType;
|
||||
@@ -34,7 +33,7 @@ export type RunCreateLlamaOptions = {
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
agents?: string;
|
||||
useCase?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
@@ -44,7 +43,6 @@ export async function runCreateLlama({
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -53,7 +51,7 @@ export async function runCreateLlama({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
|
||||
throw new Error(
|
||||
@@ -90,21 +88,15 @@ export async function runCreateLlama({
|
||||
...dataSourceArgs,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--open-ai-key",
|
||||
process.env.OPENAI_API_KEY,
|
||||
"--use-pnpm",
|
||||
"--use-npm",
|
||||
"--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) {
|
||||
@@ -121,8 +113,8 @@ export async function runCreateLlama({
|
||||
if (observability) {
|
||||
commandArgs.push("--observability", observability);
|
||||
}
|
||||
if (templateType === "multiagent" && agents) {
|
||||
commandArgs.push("--agents", agents);
|
||||
if ((templateType === "multiagent" || templateType === "reflex") && useCase) {
|
||||
commandArgs.push("--use-case", useCase);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
|
||||
url: new URL(
|
||||
"https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf",
|
||||
),
|
||||
filename: "apple_10k_report.pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,10 +27,31 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
|
||||
url: new URL(
|
||||
"https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
|
||||
),
|
||||
filename: "tesla_10k_report.pdf",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const EXAMPLE_GDPR: TemplateDataSource = {
|
||||
type: "file",
|
||||
config: {
|
||||
url: new URL(
|
||||
"https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32016R0679",
|
||||
),
|
||||
filename: "gdpr.pdf",
|
||||
},
|
||||
};
|
||||
|
||||
export const AI_REPORTS: TemplateDataSource = {
|
||||
type: "file",
|
||||
config: {
|
||||
url: new URL(
|
||||
"https://www.europarl.europa.eu/RegData/etudes/ATAG/2024/760392/EPRS_ATA(2024)760392_EN.pdf",
|
||||
),
|
||||
filename: "EPRS_ATA_2024_760392_EN.pdf",
|
||||
},
|
||||
};
|
||||
|
||||
export function getDataSources(
|
||||
files?: string,
|
||||
exampleFile?: boolean,
|
||||
|
||||
+29
-18
@@ -13,6 +13,12 @@ import {
|
||||
|
||||
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT =
|
||||
"You are a helpful assistant who helps users with their questions.";
|
||||
|
||||
const DATA_SOURCES_PROMPT =
|
||||
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
|
||||
|
||||
export type EnvVar = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -217,7 +223,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",
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -401,6 +413,13 @@ const getFrameworkEnvs = (
|
||||
],
|
||||
);
|
||||
}
|
||||
if (framework === "nextjs") {
|
||||
result.push({
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description:
|
||||
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -436,9 +455,6 @@ const getSystemPromptEnv = (
|
||||
dataSources?: TemplateDataSource[],
|
||||
template?: TemplateType,
|
||||
): EnvVar[] => {
|
||||
const defaultSystemPrompt =
|
||||
"You are a helpful assistant who helps users with their questions.";
|
||||
|
||||
const systemPromptEnv: EnvVar[] = [];
|
||||
// build tool system prompt by merging all tool system prompts
|
||||
// multiagent template doesn't need system prompt
|
||||
@@ -453,9 +469,12 @@ const getSystemPromptEnv = (
|
||||
}
|
||||
});
|
||||
|
||||
const systemPrompt = toolSystemPrompt
|
||||
? `\"${toolSystemPrompt}\"`
|
||||
: defaultSystemPrompt;
|
||||
const systemPrompt =
|
||||
'"' +
|
||||
DEFAULT_SYSTEM_PROMPT +
|
||||
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
|
||||
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
|
||||
'"';
|
||||
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_PROMPT",
|
||||
@@ -506,7 +525,7 @@ Here is the conversation history
|
||||
---------------------
|
||||
{conversation}
|
||||
---------------------
|
||||
Given the conversation history, please give me 3 questions that you might ask next!
|
||||
Given the conversation history, please give me 3 questions that user might ask next!
|
||||
Your answer should be wrapped in three sticks which follows the following format:
|
||||
\`\`\`
|
||||
<question 1>
|
||||
@@ -547,7 +566,7 @@ export const createBackendEnvFile = async (
|
||||
| "framework"
|
||||
| "dataSources"
|
||||
| "template"
|
||||
| "externalPort"
|
||||
| "port"
|
||||
| "tools"
|
||||
| "observability"
|
||||
>,
|
||||
@@ -564,7 +583,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),
|
||||
@@ -579,18 +598,10 @@ export const createBackendEnvFile = async (
|
||||
export const createFrontendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
customApiPath?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
},
|
||||
) => {
|
||||
const defaultFrontendEnvs = [
|
||||
{
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description: "The backend API for chat endpoint.",
|
||||
value: opts.customApiPath
|
||||
? opts.customApiPath
|
||||
: "http://localhost:8000/api/chat",
|
||||
},
|
||||
{
|
||||
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
|
||||
description: "Let's the user change indexes in LlamaCloud projects",
|
||||
|
||||
+3
-3
@@ -118,7 +118,8 @@ const prepareContextData = async (
|
||||
const destPath = path.join(
|
||||
root,
|
||||
"data",
|
||||
path.basename(dataSourceConfig.url.toString()),
|
||||
dataSourceConfig.filename ??
|
||||
path.basename(dataSourceConfig.url.toString()),
|
||||
);
|
||||
await downloadFile(dataSourceConfig.url.toString(), destPath);
|
||||
} else {
|
||||
@@ -192,7 +193,7 @@ export const installTemplate = async (
|
||||
if (
|
||||
props.template === "streaming" ||
|
||||
props.template === "multiagent" ||
|
||||
props.template === "extractor"
|
||||
props.template === "reflex"
|
||||
) {
|
||||
await createBackendEnvFile(props.root, props);
|
||||
}
|
||||
@@ -225,7 +226,6 @@ export const installTemplate = async (
|
||||
} else {
|
||||
// this is a frontend for a full-stack app, create .env file with model information
|
||||
await createFrontendEnvFile(props.root, {
|
||||
customApiPath: props.customApiPath,
|
||||
vectorDb: props.vectorDb,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,13 @@ type ModelData = {
|
||||
};
|
||||
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];
|
||||
|
||||
@@ -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",
|
||||
|
||||
+68
-52
@@ -20,6 +20,7 @@ interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
constraints?: Record<string, string>;
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
@@ -36,28 +37,31 @@ const getAdditionalDependencies = (
|
||||
case "mongo": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-mongodb",
|
||||
version: "^0.3.1",
|
||||
version: "^0.6.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pg": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-postgres",
|
||||
version: "^0.2.5",
|
||||
version: "^0.3.2",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pinecone": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: "^0.2.1",
|
||||
version: "^0.4.1",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "milvus": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-milvus",
|
||||
version: "^0.2.0",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymilvus",
|
||||
@@ -68,35 +72,38 @@ const getAdditionalDependencies = (
|
||||
case "astra": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-astra-db",
|
||||
version: "^0.2.0",
|
||||
version: "^0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "qdrant": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-qdrant",
|
||||
version: "^0.3.0",
|
||||
version: "^0.4.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "chroma": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-chroma",
|
||||
version: "^0.2.0",
|
||||
version: "^0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-weaviate",
|
||||
version: "^1.1.1",
|
||||
version: "^1.2.3",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "llamacloud":
|
||||
dependencies.push({
|
||||
name: "llama-index-indices-managed-llama-cloud",
|
||||
version: "^0.3.1",
|
||||
version: "^0.6.3",
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -115,13 +122,13 @@ const getAdditionalDependencies = (
|
||||
case "web":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-web",
|
||||
version: "^0.2.2",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
break;
|
||||
case "db":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-database",
|
||||
version: "^0.2.0",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymysql",
|
||||
@@ -160,15 +167,15 @@ const getAdditionalDependencies = (
|
||||
if (templateType !== "multiagent") {
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-openai",
|
||||
version: "^0.2.0",
|
||||
version: "^0.3.2",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-openai",
|
||||
version: "^0.2.3",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: "^0.3.0",
|
||||
version: "^0.4.0",
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -279,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
|
||||
@@ -368,33 +380,37 @@ export const installPythonDependencies = (
|
||||
};
|
||||
|
||||
export const installPythonTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
modelConfig,
|
||||
dataSources,
|
||||
tools,
|
||||
postInstallAction,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
observability,
|
||||
modelConfig,
|
||||
agents,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "appName"
|
||||
| "root"
|
||||
| "framework"
|
||||
| "template"
|
||||
| "framework"
|
||||
| "vectorDb"
|
||||
| "postInstallAction"
|
||||
| "modelConfig"
|
||||
| "dataSources"
|
||||
| "tools"
|
||||
| "postInstallAction"
|
||||
| "useLlamaParse"
|
||||
| "useCase"
|
||||
| "observability"
|
||||
| "modelConfig"
|
||||
| "agents"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
let templatePath;
|
||||
if (template === "extractor") {
|
||||
templatePath = path.join(templatesDir, "types", "extractor", framework);
|
||||
if (template === "reflex") {
|
||||
templatePath = path.join(templatesDir, "types", "reflex");
|
||||
} else {
|
||||
templatePath = path.join(templatesDir, "types", "streaming", framework);
|
||||
}
|
||||
@@ -460,37 +476,39 @@ export const installPythonTemplate = async ({
|
||||
cwd: path.join(compPath, "engines", "python", engine),
|
||||
});
|
||||
|
||||
// Copy agent code
|
||||
if (template === "multiagent") {
|
||||
if (agents) {
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "agents", "python", agents),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
"There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy router code
|
||||
await copyRouterCode(root, tools ?? []);
|
||||
}
|
||||
|
||||
// Copy multiagents overrides
|
||||
if (template === "multiagent") {
|
||||
// Copy multi-agent code
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "multiagent", "python"),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
}
|
||||
|
||||
if (template === "multiagent" || template === "reflex") {
|
||||
if (useCase) {
|
||||
const sourcePath =
|
||||
template === "multiagent"
|
||||
? path.join(compPath, "agents", "python", useCase)
|
||||
: path.join(compPath, "reflex", useCase);
|
||||
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: sourcePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Adding additional dependencies");
|
||||
|
||||
const addOnDependencies = getAdditionalDependencies(
|
||||
@@ -512,7 +530,10 @@ export const installPythonTemplate = async ({
|
||||
if (observability === "llamatrace") {
|
||||
addOnDependencies.push({
|
||||
name: "llama-index-callbacks-arize-phoenix",
|
||||
version: "^0.2.1",
|
||||
version: "^0.3.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -533,9 +554,4 @@ export const installPythonTemplate = async ({
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
installPythonDependencies();
|
||||
}
|
||||
|
||||
// Copy deployment files for python
|
||||
await copy("**", root, {
|
||||
cwd: path.join(compPath, "deployments", "python"),
|
||||
});
|
||||
};
|
||||
|
||||
+46
-64
@@ -1,40 +1,39 @@
|
||||
import { ChildProcess, SpawnOptions, spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { TemplateFramework } from "./types";
|
||||
import { SpawnOptions, spawn } from "child_process";
|
||||
import { TemplateFramework, TemplateType } 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}` },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,40 +58,24 @@ export function runTSApp(appPath: string, port: number) {
|
||||
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
template: string,
|
||||
frontend: boolean,
|
||||
template: TemplateType,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
externalPort?: number,
|
||||
): Promise<any> {
|
||||
const processes: ChildProcess[] = [];
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort =
|
||||
framework === "nextjs" || template === "reflex" ? 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 === "reflex"
|
||||
? 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);
|
||||
}
|
||||
|
||||
+15
-36
@@ -41,7 +41,7 @@ export const supportedTools: Tool[] = [
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-google",
|
||||
version: "^0.2.0",
|
||||
version: "^0.3.0",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi"],
|
||||
@@ -62,17 +62,16 @@ 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: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for DuckDuckGo search tool.",
|
||||
value: `You are a DuckDuckGo search agent.
|
||||
You can use the duckduckgo search tool to get information from the web to answer user questions.
|
||||
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
|
||||
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
|
||||
},
|
||||
],
|
||||
@@ -83,18 +82,11 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-wikipedia",
|
||||
version: "^0.2.0",
|
||||
version: "^0.3.0",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LLAMAHUB,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for wiki tool.",
|
||||
value: `You are a Wikipedia agent. You help users to get information from Wikipedia.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Weather",
|
||||
@@ -102,13 +94,6 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for weather tool.",
|
||||
value: `You are a weather forecast agent. You help users to get the weather forecast for a given location.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Document generator",
|
||||
@@ -139,7 +124,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.11b38",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -170,7 +155,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.11b38",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -211,14 +196,6 @@ For better results, you can specify the region parameter to get results from a s
|
||||
},
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for openapi action tool.",
|
||||
value:
|
||||
"You are an OpenAPI action agent. You help users to make requests to the provided OpenAPI schema.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Image Generator",
|
||||
@@ -231,11 +208,6 @@ For better results, you can specify the region parameter to get results from a s
|
||||
description:
|
||||
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
|
||||
},
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for image generator tool.",
|
||||
value: `You are an image generator agent. You help users to generate images using the Stability API.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -353,9 +325,16 @@ export const writeToolsConfig = async (
|
||||
yaml.stringify(configContent),
|
||||
);
|
||||
} else {
|
||||
// For Typescript, we treat llamahub tools as local tools
|
||||
const tsConfigContent = {
|
||||
local: {
|
||||
...configContent.local,
|
||||
...configContent.llamahub,
|
||||
},
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(configPath, "tools.json"),
|
||||
JSON.stringify(configContent, null, 2),
|
||||
JSON.stringify(tsConfigContent, null, 2),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+13
-6
@@ -20,11 +20,11 @@ export type ModelConfig = {
|
||||
isConfigured(): boolean;
|
||||
};
|
||||
export type TemplateType =
|
||||
| "extractor"
|
||||
| "streaming"
|
||||
| "community"
|
||||
| "llamapack"
|
||||
| "multiagent";
|
||||
| "multiagent"
|
||||
| "reflex";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateUI = "html" | "shadcn";
|
||||
export type TemplateVectorDB =
|
||||
@@ -49,14 +49,22 @@ export type TemplateDataSource = {
|
||||
};
|
||||
export type TemplateDataSourceType = "file" | "web" | "db";
|
||||
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
|
||||
export type TemplateAgents = "financial_report" | "blog" | "form_filling";
|
||||
export type TemplateUseCase =
|
||||
| "financial_report"
|
||||
| "blog"
|
||||
| "deep_research"
|
||||
| "form_filling"
|
||||
| "extractor"
|
||||
| "contract_review";
|
||||
// Config for both file and folder
|
||||
export type FileSourceConfig =
|
||||
| {
|
||||
path: string;
|
||||
filename?: string;
|
||||
}
|
||||
| {
|
||||
url: URL;
|
||||
filename?: string;
|
||||
};
|
||||
export type WebSourceConfig = {
|
||||
baseUrl?: string;
|
||||
@@ -89,16 +97,15 @@ export interface InstallTemplateArgs {
|
||||
framework: TemplateFramework;
|
||||
ui: TemplateUI;
|
||||
dataSources: TemplateDataSource[];
|
||||
customApiPath?: string;
|
||||
modelConfig: ModelConfig;
|
||||
llamaCloudKey?: string;
|
||||
useLlamaParse?: boolean;
|
||||
communityProjectConfig?: CommunityProjectConfig;
|
||||
llamapack?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
externalPort?: number;
|
||||
port?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
agents?: TemplateAgents;
|
||||
useCase?: TemplateUseCase;
|
||||
}
|
||||
|
||||
+112
-45
@@ -6,7 +6,7 @@ import { assetRelocator, copy } from "../helpers/copy";
|
||||
import { callPackageManager } from "../helpers/install";
|
||||
import { templatesDir } from "./dir";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { InstallTemplateArgs } from "./types";
|
||||
import { InstallTemplateArgs, ModelProvider, TemplateVectorDB } from "./types";
|
||||
|
||||
/**
|
||||
* Install a LlamaIndex internal template to a given `root` directory.
|
||||
@@ -26,7 +26,8 @@ export const installTSTemplate = async ({
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
agents,
|
||||
useCase,
|
||||
modelConfig,
|
||||
}: InstallTemplateArgs & { backend: boolean }) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
@@ -58,11 +59,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(
|
||||
@@ -133,26 +132,29 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(multiagentPath, "workflow"),
|
||||
});
|
||||
|
||||
// Copy agents use case code for multiagent template
|
||||
if (agents) {
|
||||
console.log("\nCopying agent:", agents, "\n");
|
||||
|
||||
const agentsCodePath = path.join(
|
||||
compPath,
|
||||
"agents",
|
||||
"typescript",
|
||||
agents,
|
||||
);
|
||||
// Copy use case code for multiagent template
|
||||
if (useCase) {
|
||||
console.log("\nCopying use case:", useCase, "\n");
|
||||
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
|
||||
const useCaseCodePath = path.join(useCasePath, "workflow");
|
||||
|
||||
// Copy use case codes
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: agentsCodePath,
|
||||
cwd: useCaseCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// Copy use case files to project root
|
||||
await copy("*.*", path.join(root), {
|
||||
parents: true,
|
||||
cwd: useCasePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
"There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -180,6 +182,12 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
|
||||
});
|
||||
|
||||
// copy provider settings
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "providers", "typescript", modelConfig.provider),
|
||||
});
|
||||
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
tools = tools ?? [];
|
||||
@@ -238,16 +246,78 @@ export const installTSTemplate = async ({
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
backend,
|
||||
modelConfig,
|
||||
});
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
if (
|
||||
backend &&
|
||||
(postInstallAction === "runApp" || postInstallAction === "dependencies")
|
||||
) {
|
||||
await installTSDependencies(packageJson, packageManager, isOnline);
|
||||
}
|
||||
};
|
||||
|
||||
// Copy deployment files for typescript
|
||||
await copy("**", root, {
|
||||
cwd: path.join(compPath, "deployments", "typescript"),
|
||||
});
|
||||
const providerDependencies: {
|
||||
[key in ModelProvider]?: Record<string, string>;
|
||||
} = {
|
||||
openai: {
|
||||
"@llamaindex/openai": "^0.1.52",
|
||||
},
|
||||
gemini: {
|
||||
"@llamaindex/google": "^0.0.7",
|
||||
},
|
||||
ollama: {
|
||||
"@llamaindex/ollama": "^0.0.40",
|
||||
},
|
||||
mistral: {
|
||||
"@llamaindex/mistral": "^0.0.5",
|
||||
},
|
||||
"azure-openai": {
|
||||
"@llamaindex/openai": "^0.1.52",
|
||||
},
|
||||
groq: {
|
||||
"@llamaindex/groq": "^0.0.51",
|
||||
"@llamaindex/huggingface": "^0.0.36", // groq uses huggingface as default embedding model
|
||||
},
|
||||
anthropic: {
|
||||
"@llamaindex/anthropic": "^0.1.0",
|
||||
"@llamaindex/huggingface": "^0.0.36", // anthropic uses huggingface as default embedding model
|
||||
},
|
||||
};
|
||||
|
||||
const vectorDbDependencies: Record<TemplateVectorDB, Record<string, string>> = {
|
||||
astra: {
|
||||
"@llamaindex/astra": "^0.0.5",
|
||||
},
|
||||
chroma: {
|
||||
"@llamaindex/chroma": "^0.0.5",
|
||||
},
|
||||
llamacloud: {},
|
||||
milvus: {
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.6",
|
||||
"@llamaindex/milvus": "^0.1.0",
|
||||
},
|
||||
mongo: {
|
||||
mongodb: "6.7.0",
|
||||
"@llamaindex/mongodb": "^0.0.5",
|
||||
},
|
||||
none: {},
|
||||
pg: {
|
||||
pg: "^8.12.0",
|
||||
pgvector: "^0.2.0",
|
||||
"@llamaindex/postgres": "^0.0.33",
|
||||
},
|
||||
pinecone: {
|
||||
"@llamaindex/pinecone": "^0.0.5",
|
||||
},
|
||||
qdrant: {
|
||||
"@qdrant/js-client-rest": "^1.11.0",
|
||||
"@llamaindex/qdrant": "^0.1.0",
|
||||
},
|
||||
weaviate: {
|
||||
"@llamaindex/weaviate": "^0.0.5",
|
||||
},
|
||||
};
|
||||
|
||||
async function updatePackageJson({
|
||||
@@ -259,6 +329,8 @@ async function updatePackageJson({
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
backend,
|
||||
modelConfig,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "root"
|
||||
@@ -268,8 +340,10 @@ async function updatePackageJson({
|
||||
| "ui"
|
||||
| "observability"
|
||||
| "vectorDb"
|
||||
| "modelConfig"
|
||||
> & {
|
||||
relativeEngineDestPath: string;
|
||||
backend: boolean;
|
||||
}): Promise<any> {
|
||||
const packageJsonFile = path.join(root, "package.json");
|
||||
const packageJson: any = JSON.parse(
|
||||
@@ -309,32 +383,25 @@ async function updatePackageJson({
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "pg") {
|
||||
if (backend) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
pg: "^8.12.0",
|
||||
pgvector: "^0.2.0",
|
||||
"@llamaindex/readers": "^2.0.0",
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "qdrant") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@qdrant/js-client-rest": "^1.11.0",
|
||||
};
|
||||
}
|
||||
if (vectorDb === "mongo") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
mongodb: "^6.7.0",
|
||||
};
|
||||
}
|
||||
if (vectorDb && vectorDb in vectorDbDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...vectorDbDependencies[vectorDb],
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "milvus") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.6",
|
||||
};
|
||||
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...providerDependencies[modelConfig.provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (observability === "traceloop") {
|
||||
|
||||
@@ -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(
|
||||
@@ -209,10 +202,10 @@ const program = new Command(packageJson.name)
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--agents <agents>",
|
||||
"--use-case <useCase>",
|
||||
`
|
||||
|
||||
Select which agents to use for the multi-agent template (e.g: financial_report, blog).
|
||||
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
|
||||
`,
|
||||
)
|
||||
.allowUnknownOption()
|
||||
@@ -222,7 +215,7 @@ const options = program.opts();
|
||||
|
||||
if (
|
||||
process.argv.includes("--no-llama-parse") ||
|
||||
options.template === "extractor"
|
||||
options.template === "reflex"
|
||||
) {
|
||||
options.useLlamaParse = false;
|
||||
}
|
||||
@@ -333,7 +326,6 @@ async function run(): Promise<void> {
|
||||
...answers,
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
externalPort: options.externalPort,
|
||||
});
|
||||
|
||||
if (answers.postInstallAction === "VSCode") {
|
||||
@@ -362,14 +354,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.3.11",
|
||||
"version": "0.4.0",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "^20.11.7",
|
||||
"@types/prompts": "2.0.1",
|
||||
"@types/prompts": "2.4.2",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/validate-npm-package-name": "3.0.0",
|
||||
"async-retry": "1.3.1",
|
||||
|
||||
Generated
+8
-5
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^20.11.7
|
||||
version: 20.12.10
|
||||
'@types/prompts':
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
'@types/tar':
|
||||
specifier: 6.1.5
|
||||
version: 6.1.5
|
||||
@@ -298,8 +298,8 @@ packages:
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
'@types/prompts@2.0.1':
|
||||
resolution: {integrity: sha512-AhtMcmETelF8wFDV1ucbChKhLgsc+ytXZXkNz/nnTAMSDeqsjALknEFxi7ZtLgS/G8bV2rp90LhDW5SGACimIQ==}
|
||||
'@types/prompts@2.4.2':
|
||||
resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==}
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||
@@ -2208,7 +2208,10 @@ snapshots:
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/prompts@2.0.1': {}
|
||||
'@types/prompts@2.4.2':
|
||||
dependencies:
|
||||
'@types/node': 20.12.10
|
||||
kleur: 3.0.3
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
dependencies:
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getDataSourceChoices = (
|
||||
);
|
||||
}
|
||||
|
||||
if (framework === "fastapi" && template !== "extractor") {
|
||||
if (framework === "fastapi" && template !== "reflex") {
|
||||
choices.push({
|
||||
title: "Use website content (requires Chrome)",
|
||||
value: "web",
|
||||
|
||||
+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
|
||||
|
||||
+69
-42
@@ -1,7 +1,8 @@
|
||||
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 { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
|
||||
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getProjectOptions } from "../helpers/repo";
|
||||
@@ -32,7 +33,7 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
title: "Multi-agent app (using workflows)",
|
||||
value: "multiagent",
|
||||
},
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{ title: "Fullstack python template with Reflex", value: "reflex" },
|
||||
{
|
||||
title: `Community template from ${styledRepo}`,
|
||||
value: "community",
|
||||
@@ -94,11 +95,29 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.template === "extractor") {
|
||||
// Extractor template only supports FastAPI, empty data sources, and llamacloud
|
||||
if (program.template === "reflex") {
|
||||
// Reflex template only supports FastAPI, empty data sources, and llamacloud
|
||||
// So we just use example file for extractor template, this allows user to choose vector database later
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
program.framework = "fastapi";
|
||||
// Ask for which Reflex use case to use
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to build?",
|
||||
choices: [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
if (!program.framework) {
|
||||
@@ -122,24 +141,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",
|
||||
@@ -177,32 +189,50 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
program.observability = observability;
|
||||
}
|
||||
|
||||
// Ask agents
|
||||
if (program.template === "multiagent" && !program.agents) {
|
||||
const { agents } = await prompts(
|
||||
if (
|
||||
(program.template === "reflex" || program.template === "multiagent") &&
|
||||
!program.useCase
|
||||
) {
|
||||
const choices =
|
||||
program.template === "reflex"
|
||||
? [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{ title: "Blog writer (Write a blog post)", value: "blog" },
|
||||
];
|
||||
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "agents",
|
||||
message: "Which agents would you like to use?",
|
||||
choices: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{
|
||||
title: "Blog writer (Write a blog post)",
|
||||
value: "blog_writer",
|
||||
},
|
||||
],
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.agents = agents;
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
// Configure framework and data sources for Reflex template
|
||||
if (program.template === "reflex") {
|
||||
program.framework = "fastapi";
|
||||
|
||||
program.dataSources =
|
||||
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
@@ -228,8 +258,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
program.vectorDb = vectorDb;
|
||||
}
|
||||
|
||||
if (program.vectorDb === "llamacloud") {
|
||||
// When using a LlamaCloud index, don't ask for data sources just copy an example file
|
||||
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
|
||||
// When using a LlamaCloud index and no data sources are provided, just copy an example file
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
}
|
||||
|
||||
@@ -360,11 +390,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = true;
|
||||
} else {
|
||||
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
|
||||
if (
|
||||
program.useLlamaParse === undefined &&
|
||||
program.template !== "extractor"
|
||||
) {
|
||||
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
|
||||
if (program.useLlamaParse === undefined && program.template !== "reflex") {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
const { useLlamaParse } = await prompts(
|
||||
@@ -386,7 +413,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(
|
||||
|
||||
+113
-53
@@ -1,5 +1,10 @@
|
||||
import prompts from "prompts";
|
||||
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import {
|
||||
AI_REPORTS,
|
||||
EXAMPLE_10K_SEC_FILES,
|
||||
EXAMPLE_FILE,
|
||||
EXAMPLE_GDPR,
|
||||
} from "../helpers/datasources";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getTools } from "../helpers/tools";
|
||||
import { ModelConfig, TemplateFramework } from "../helpers/types";
|
||||
@@ -12,7 +17,9 @@ type AppType =
|
||||
| "financial_report_agent"
|
||||
| "form_filling"
|
||||
| "extractor"
|
||||
| "data_scientist";
|
||||
| "contract_review"
|
||||
| "data_scientist"
|
||||
| "deep_research";
|
||||
|
||||
type SimpleAnswers = {
|
||||
appType: AppType;
|
||||
@@ -29,19 +36,56 @@ export const askSimpleQuestions = async (
|
||||
type: "select",
|
||||
name: "appType",
|
||||
message: "What app do you want to build?",
|
||||
hint: "🤖: Agent, 🔀: Workflow",
|
||||
choices: [
|
||||
{ title: "Agentic RAG", value: "rag" },
|
||||
{ title: "Data Scientist", value: "data_scientist" },
|
||||
{
|
||||
title: "Financial Report Generator (using Workflows)",
|
||||
title: "🤖 Agentic RAG",
|
||||
value: "rag",
|
||||
description:
|
||||
"Chatbot that answers questions based on provided documents.",
|
||||
},
|
||||
{
|
||||
title: "🤖 Data Scientist",
|
||||
value: "data_scientist",
|
||||
description:
|
||||
"Agent that analyzes data and generates visualizations by using a code interpreter.",
|
||||
},
|
||||
{
|
||||
title: "🤖 Code Artifact Agent",
|
||||
value: "code_artifact",
|
||||
description:
|
||||
"Agent that writes code, runs it in a sandbox, and shows the output in the chat UI.",
|
||||
},
|
||||
{
|
||||
title: "🤖 Information Extractor",
|
||||
value: "extractor",
|
||||
description:
|
||||
"Extracts information from documents and returns it as a structured JSON object.",
|
||||
},
|
||||
{
|
||||
title: "🔀 Financial Report Generator",
|
||||
value: "financial_report_agent",
|
||||
description:
|
||||
"Generates a financial report by analyzing the provided 10-K SEC data. Uses a code interpreter to create charts or to conduct further analysis.",
|
||||
},
|
||||
{
|
||||
title: "Form Filler (using Workflows)",
|
||||
title: "🔀 Financial 10k SEC Form Filler",
|
||||
value: "form_filling",
|
||||
description:
|
||||
"Extracts information from 10k SEC data and uses it to fill out a CSV form.",
|
||||
},
|
||||
{
|
||||
title: "🔀 Contract Reviewer",
|
||||
value: "contract_review",
|
||||
description:
|
||||
"Extracts and reviews contracts to ensure compliance with GDPR regulations",
|
||||
},
|
||||
{
|
||||
title: "🔀 Deep Researcher",
|
||||
value: "deep_research",
|
||||
description:
|
||||
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
|
||||
},
|
||||
{ title: "Code Artifact Agent", value: "code_artifact" },
|
||||
{ title: "Information Extractor", value: "extractor" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
@@ -51,51 +95,52 @@ export const askSimpleQuestions = async (
|
||||
let llamaCloudKey = args.llamaCloudKey;
|
||||
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 { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
if (
|
||||
appType !== "extractor" &&
|
||||
appType !== "contract_review" &&
|
||||
appType !== "deep_research"
|
||||
) {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
|
||||
const results = await convertAnswers(args, {
|
||||
@@ -127,14 +172,14 @@ const convertAnswers = async (
|
||||
AppType,
|
||||
Pick<
|
||||
QuestionResults,
|
||||
"template" | "tools" | "frontend" | "dataSources" | "agents"
|
||||
"template" | "tools" | "frontend" | "dataSources" | "useCase"
|
||||
> & {
|
||||
modelConfig?: ModelConfig;
|
||||
}
|
||||
> = {
|
||||
rag: {
|
||||
template: "streaming",
|
||||
tools: getTools(["duckduckgo"]),
|
||||
tools: getTools(["weather"]),
|
||||
frontend: true,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
@@ -154,7 +199,7 @@ const convertAnswers = async (
|
||||
},
|
||||
financial_report_agent: {
|
||||
template: "multiagent",
|
||||
agents: "financial_report",
|
||||
useCase: "financial_report",
|
||||
tools: getTools(["document_generator", "interpreter"]),
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
frontend: true,
|
||||
@@ -162,18 +207,33 @@ const convertAnswers = async (
|
||||
},
|
||||
form_filling: {
|
||||
template: "multiagent",
|
||||
agents: "form_filling",
|
||||
useCase: "form_filling",
|
||||
tools: getTools(["form_filling"]),
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
frontend: true,
|
||||
modelConfig: MODEL_GPT4o,
|
||||
},
|
||||
extractor: {
|
||||
template: "extractor",
|
||||
template: "reflex",
|
||||
useCase: "extractor",
|
||||
tools: [],
|
||||
frontend: false,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
contract_review: {
|
||||
template: "reflex",
|
||||
useCase: "contract_review",
|
||||
tools: [],
|
||||
frontend: false,
|
||||
dataSources: [EXAMPLE_GDPR],
|
||||
},
|
||||
deep_research: {
|
||||
template: "multiagent",
|
||||
useCase: "deep_research",
|
||||
tools: [],
|
||||
frontend: true,
|
||||
dataSources: [AI_REPORTS],
|
||||
},
|
||||
};
|
||||
const results = lookup[answers.appType];
|
||||
return {
|
||||
|
||||
+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!
|
||||
@@ -8,9 +8,9 @@ This example is using three agents to generate a blog post:
|
||||
|
||||
There are three different methods how the agents can interact to reach their goal:
|
||||
|
||||
1. [Choreography](./app/examples/choreography.py) - the agents decide themselves to delegate a task to another agent
|
||||
1. [Orchestrator](./app/examples/orchestrator.py) - a central orchestrator decides which agent should execute a task
|
||||
1. [Explicit Workflow](./app/examples/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks
|
||||
1. [Choreography](./app/agents/choreography.py) - the agents decide themselves to delegate a task to another agent
|
||||
1. [Orchestrator](./app/agents/orchestrator.py) - a central orchestrator decides which agent should execute a task
|
||||
1. [Explicit Workflow](./app/agents/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -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,14 +47,18 @@ 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
|
||||
```
|
||||
|
||||
## Deployments
|
||||
|
||||
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
|
||||
@@ -6,42 +5,24 @@ from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import QueryEngineTool, ToolMetadata
|
||||
from app.engine.tools.query_engine import get_query_engine_tool
|
||||
|
||||
|
||||
def _create_query_engine_tool(params=None) -> QueryEngineTool:
|
||||
"""
|
||||
Provide an agent worker that can be used to query the index.
|
||||
"""
|
||||
# Add query tool if index exists
|
||||
index_config = IndexConfig(**(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
return None
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
query_engine = index.as_query_engine(
|
||||
**({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
return QueryEngineTool(
|
||||
query_engine=query_engine,
|
||||
metadata=ToolMetadata(
|
||||
name="query_index",
|
||||
description="""
|
||||
Use this tool to retrieve information about the text corpus from the index.
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_research_tools(**kwargs) -> QueryEngineTool:
|
||||
def _get_research_tools(**kwargs):
|
||||
"""
|
||||
Researcher take responsibility for retrieving information.
|
||||
Try init wikipedia or duckduckgo tool if available.
|
||||
"""
|
||||
tools = []
|
||||
query_engine_tool = _create_query_engine_tool(**kwargs)
|
||||
if query_engine_tool is not None:
|
||||
tools.append(query_engine_tool)
|
||||
# Create query engine tool
|
||||
index_config = IndexConfig(**kwargs)
|
||||
index = get_index(index_config)
|
||||
if index is not None:
|
||||
query_engine_tool = get_query_engine_tool(index=index)
|
||||
if query_engine_tool is not None:
|
||||
tools.append(query_engine_tool)
|
||||
|
||||
# Create duckduckgo tool
|
||||
researcher_tool_names = [
|
||||
"duckduckgo_search",
|
||||
"duckduckgo_image_search",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .blog import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
+5
-4
@@ -4,17 +4,18 @@ from typing import List, Optional
|
||||
|
||||
from app.agents.choreography import create_choreography
|
||||
from app.agents.orchestrator import create_orchestrator
|
||||
from app.agents.workflow import create_workflow
|
||||
from app.agents.workflow import create_workflow as create_blog_workflow
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.workflow import Workflow
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def get_chat_engine(
|
||||
def create_workflow(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
# TODO: the EXAMPLE_TYPE could be passed as a chat config parameter?
|
||||
# Chat filters are not supported yet
|
||||
kwargs.pop("filters", None)
|
||||
agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
|
||||
match agent_type:
|
||||
case "choreography":
|
||||
@@ -22,7 +23,7 @@ def get_chat_engine(
|
||||
case "orchestrator":
|
||||
agent = create_orchestrator(chat_history, **kwargs)
|
||||
case _:
|
||||
agent = create_workflow(chat_history, **kwargs)
|
||||
agent = create_blog_workflow(chat_history, **kwargs)
|
||||
|
||||
logger.info(f"Using agent pattern: {agent_type}")
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@ class Planner:
|
||||
# gather completed sub-tasks and response pairs
|
||||
completed_outputs_str = ""
|
||||
for sub_task_name, task_output in completed_sub_task.items():
|
||||
task_str = f"{sub_task_name}:\n" f"\t{task_output!s}\n"
|
||||
task_str = f"{sub_task_name}:\n\t{task_output!s}\n"
|
||||
completed_outputs_str += task_str
|
||||
|
||||
# get a string for the remaining sub-tasks
|
||||
+2
-2
@@ -42,9 +42,9 @@ class AgentRunEvent(Event):
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"name": self.name,
|
||||
"agent": self.name,
|
||||
"type": self.event_type.value,
|
||||
"msg": self.msg,
|
||||
"text": self.msg,
|
||||
"data": self.data,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, setup the environment with poetry:
|
||||
|
||||
> **_Note:_** This step is not needed if you are using the dev-container.
|
||||
|
||||
```shell
|
||||
poetry install
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
|
||||
Second, generate the embeddings of the documents in the `./data` directory:
|
||||
|
||||
```shell
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
## Use Case: Deep Research over own documents
|
||||
|
||||
The workflow performs deep research by retrieving and analyzing documents from the [data](./data) directory from multiple perspectives. The project includes a sample PDF about AI investment in 2024 to help you get started. You can also add your own documents by placing them in the data directory and running the generate script again to index them.
|
||||
|
||||
After starting the server, go to [http://localhost:8000](http://localhost:8000) and send a message to the agent to write a blog post.
|
||||
E.g: "AI investment in 2024"
|
||||
|
||||
To update the workflow, you can edit the [deep_research.py](./app/workflows/deep_research.py) file.
|
||||
|
||||
By default, the workflow retrieves 10 results from your documents. To customize the amount of information covered in the answer, you can adjust the `TOP_K` environment variable in the `.env` file. A higher value will retrieve more results from your documents, potentially providing more comprehensive answers.
|
||||
|
||||
## Deployments
|
||||
|
||||
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,3 @@
|
||||
from .deep_research import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
@@ -0,0 +1,183 @@
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from llama_index.core.base.llms.types import (
|
||||
CompletionResponse,
|
||||
CompletionResponseAsyncGen,
|
||||
)
|
||||
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.schema import MetadataMode, Node, NodeWithScore
|
||||
from llama_index.core.settings import Settings
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AnalysisDecision(BaseModel):
|
||||
decision: Literal["research", "write", "cancel"] = Field(
|
||||
description="Whether to continue research, write a report, or cancel the research after several retries"
|
||||
)
|
||||
research_questions: Optional[List[str]] = Field(
|
||||
description="""
|
||||
If the decision is to research, provide a list of questions to research that related to the user request.
|
||||
Maximum 3 questions. Set to null or empty if writing a report or cancel the research.
|
||||
""",
|
||||
default_factory=list,
|
||||
)
|
||||
cancel_reason: Optional[str] = Field(
|
||||
description="The reason for cancellation if the decision is to cancel research.",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
async def plan_research(
|
||||
memory: SimpleComposableMemory,
|
||||
context_nodes: List[Node],
|
||||
user_request: str,
|
||||
total_questions: int,
|
||||
) -> AnalysisDecision:
|
||||
analyze_prompt = """
|
||||
You are a professor who is guiding a researcher to research a specific request/problem.
|
||||
Your task is to decide on a research plan for the researcher.
|
||||
|
||||
The possible actions are:
|
||||
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
|
||||
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
|
||||
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
|
||||
|
||||
The workflow should be:
|
||||
+ Always begin by providing some initial questions for the researcher to investigate.
|
||||
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
|
||||
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
|
||||
|
||||
Here are the context:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
<Conversation context>
|
||||
{conversation_context}
|
||||
</Conversation context>
|
||||
|
||||
{enhanced_prompt}
|
||||
|
||||
Now, provide your decision in the required format for this user request:
|
||||
<User request>
|
||||
{user_request}
|
||||
</User request>
|
||||
"""
|
||||
# Manually craft the prompt to avoid LLM hallucination
|
||||
enhanced_prompt = ""
|
||||
if total_questions == 0:
|
||||
# Avoid writing a report without any research context
|
||||
enhanced_prompt = """
|
||||
|
||||
The student has no questions to research. Let start by asking some questions.
|
||||
"""
|
||||
elif total_questions > 6:
|
||||
# Avoid asking too many questions (when the data is not ready for writing a report)
|
||||
enhanced_prompt = f"""
|
||||
|
||||
The student has researched {total_questions} questions. Should cancel the research if the context is not enough to write a report.
|
||||
"""
|
||||
|
||||
conversation_context = "\n".join(
|
||||
[f"{message.role}: {message.content}" for message in memory.get_all()]
|
||||
)
|
||||
context_str = "\n".join(
|
||||
[node.get_content(metadata_mode=MetadataMode.LLM) for node in context_nodes]
|
||||
)
|
||||
res = await Settings.llm.astructured_predict(
|
||||
output_cls=AnalysisDecision,
|
||||
prompt=PromptTemplate(template=analyze_prompt),
|
||||
user_request=user_request,
|
||||
context_str=context_str,
|
||||
conversation_context=conversation_context,
|
||||
enhanced_prompt=enhanced_prompt,
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
async def research(
|
||||
question: str,
|
||||
context_nodes: List[NodeWithScore],
|
||||
) -> str:
|
||||
prompt = """
|
||||
You are a researcher who is in the process of answering the question.
|
||||
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
|
||||
Always add citations to the sentence/point/paragraph using the id of the provided content.
|
||||
The citation should follow this format: [citation:id]() where id is the id of the content.
|
||||
|
||||
E.g:
|
||||
If we have a context like this:
|
||||
<Citation id='abc-xyz'>
|
||||
Baby llama is called cria
|
||||
</Citation id='abc-xyz'>
|
||||
|
||||
And your answer uses the content, then the citation should be:
|
||||
- Baby llama is called cria [citation:abc-xyz]()
|
||||
|
||||
Here is the provided context for the question:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>`
|
||||
|
||||
No prior knowledge, just use the provided context to answer the question: {question}
|
||||
"""
|
||||
context_str = "\n".join(
|
||||
[_get_text_node_content_for_citation(node) for node in context_nodes]
|
||||
)
|
||||
res = await Settings.llm.acomplete(
|
||||
prompt=prompt.format(question=question, context_str=context_str),
|
||||
)
|
||||
return res.text
|
||||
|
||||
|
||||
async def write_report(
|
||||
memory: SimpleComposableMemory,
|
||||
user_request: str,
|
||||
stream: bool = False,
|
||||
) -> CompletionResponse | CompletionResponseAsyncGen:
|
||||
report_prompt = """
|
||||
You are a researcher writing a report based on a user request and the research context.
|
||||
You have researched various perspectives related to the user request.
|
||||
The report should provide a comprehensive outline covering all important points from the researched perspectives.
|
||||
Create a well-structured outline for the research report that covers all the answers.
|
||||
|
||||
# IMPORTANT when writing in markdown format:
|
||||
+ Use tables or figures where appropriate to enhance presentation.
|
||||
+ Preserve all citation syntax (the `[citation:id]()` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
|
||||
+ Do not add links, a table of contents, or a references section to the report.
|
||||
|
||||
<User request>
|
||||
{user_request}
|
||||
</User request>
|
||||
|
||||
<Research context>
|
||||
{research_context}
|
||||
</Research context>
|
||||
|
||||
Now, write a report addressing the user request based on the research provided following the format and guidelines above.
|
||||
"""
|
||||
research_context = "\n".join(
|
||||
[f"{message.role}: {message.content}" for message in memory.get_all()]
|
||||
)
|
||||
|
||||
llm_complete_func = (
|
||||
Settings.llm.astream_complete if stream else Settings.llm.acomplete
|
||||
)
|
||||
|
||||
res = await llm_complete_func(
|
||||
prompt=report_prompt.format(
|
||||
user_request=user_request,
|
||||
research_context=research_context,
|
||||
),
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def _get_text_node_content_for_citation(node: NodeWithScore) -> str:
|
||||
"""
|
||||
Construct node content for LLM with citation flag.
|
||||
"""
|
||||
node_id = node.node.node_id
|
||||
content = f"<Citation id='{node_id}'>\n{node.get_content(metadata_mode=MetadataMode.LLM)}</Citation id='{node_id}'>"
|
||||
return content
|
||||
@@ -0,0 +1,328 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from llama_index.core.indices.base import BaseIndex
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
|
||||
from llama_index.core.schema import Node
|
||||
from llama_index.core.types import ChatMessage, MessageRole
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.workflows.agents import plan_research, research, write_report
|
||||
from app.workflows.events import SourceNodesEvent
|
||||
from app.workflows.models import (
|
||||
CollectAnswersEvent,
|
||||
DataEvent,
|
||||
PlanResearchEvent,
|
||||
ReportEvent,
|
||||
ResearchEvent,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def create_workflow(
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> Workflow:
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
raise ValueError(
|
||||
"Index is not found. Try run generation script to create the index first."
|
||||
)
|
||||
|
||||
return DeepResearchWorkflow(
|
||||
index=index,
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
|
||||
class DeepResearchWorkflow(Workflow):
|
||||
"""
|
||||
A workflow to research and analyze documents from multiple perspectives and write a comprehensive report.
|
||||
|
||||
Requirements:
|
||||
- An indexed documents containing the knowledge base related to the topic
|
||||
|
||||
Steps:
|
||||
1. Retrieve information from the knowledge base
|
||||
2. Analyze the retrieved information and provide questions for answering
|
||||
3. Answer the questions
|
||||
4. Write the report based on the research results
|
||||
"""
|
||||
|
||||
memory: SimpleComposableMemory
|
||||
context_nodes: List[Node]
|
||||
index: BaseIndex
|
||||
user_request: str
|
||||
stream: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
index: BaseIndex,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.index = index
|
||||
self.context_nodes = []
|
||||
self.memory = SimpleComposableMemory.from_defaults(
|
||||
primary_memory=ChatMemoryBuffer.from_defaults(),
|
||||
)
|
||||
|
||||
@step
|
||||
async def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent:
|
||||
"""
|
||||
Initiate the workflow: memory, tools, agent
|
||||
"""
|
||||
self.stream = ev.get("stream", True)
|
||||
self.user_request = ev.get("user_msg")
|
||||
chat_history = ev.get("chat_history")
|
||||
if chat_history is not None:
|
||||
self.memory.put_messages(chat_history)
|
||||
|
||||
await ctx.set("total_questions", 0)
|
||||
|
||||
# Add user message to memory
|
||||
self.memory.put_messages(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role=MessageRole.USER,
|
||||
content=self.user_request,
|
||||
)
|
||||
]
|
||||
)
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "retrieve",
|
||||
"state": "inprogress",
|
||||
},
|
||||
)
|
||||
)
|
||||
retriever = self.index.as_retriever(
|
||||
similarity_top_k=int(os.getenv("TOP_K", 10)),
|
||||
)
|
||||
nodes = retriever.retrieve(self.user_request)
|
||||
self.context_nodes.extend(nodes)
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "retrieve",
|
||||
"state": "done",
|
||||
},
|
||||
)
|
||||
)
|
||||
# Send source nodes to the stream
|
||||
# Use SourceNodesEvent to display source nodes in the UI.
|
||||
ctx.write_event_to_stream(
|
||||
SourceNodesEvent(
|
||||
nodes=nodes,
|
||||
)
|
||||
)
|
||||
return PlanResearchEvent()
|
||||
|
||||
@step
|
||||
async def analyze(
|
||||
self, ctx: Context, ev: PlanResearchEvent
|
||||
) -> ResearchEvent | ReportEvent | StopEvent:
|
||||
"""
|
||||
Analyze the retrieved information
|
||||
"""
|
||||
logger.info("Analyzing the retrieved information")
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "analyze",
|
||||
"state": "inprogress",
|
||||
},
|
||||
)
|
||||
)
|
||||
total_questions = await ctx.get("total_questions")
|
||||
res = await plan_research(
|
||||
memory=self.memory,
|
||||
context_nodes=self.context_nodes,
|
||||
user_request=self.user_request,
|
||||
total_questions=total_questions,
|
||||
)
|
||||
if res.decision == "cancel":
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "analyze",
|
||||
"state": "done",
|
||||
},
|
||||
)
|
||||
)
|
||||
return StopEvent(
|
||||
result=res.cancel_reason,
|
||||
)
|
||||
elif res.decision == "write":
|
||||
# Writing a report without any research context is not allowed.
|
||||
# It's a LLM hallucination.
|
||||
if total_questions == 0:
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "analyze",
|
||||
"state": "done",
|
||||
},
|
||||
)
|
||||
)
|
||||
return StopEvent(
|
||||
result="Sorry, I have a problem when analyzing the retrieved information. Please try again.",
|
||||
)
|
||||
|
||||
self.memory.put(
|
||||
message=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="No more idea to analyze. We should report the answers.",
|
||||
)
|
||||
)
|
||||
ctx.send_event(ReportEvent())
|
||||
else:
|
||||
total_questions += len(res.research_questions)
|
||||
await ctx.set("total_questions", total_questions) # For tracking
|
||||
await ctx.set(
|
||||
"waiting_questions", len(res.research_questions)
|
||||
) # For waiting questions to be answered
|
||||
self.memory.put(
|
||||
message=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="We need to find answers to the following questions:\n"
|
||||
+ "\n".join(res.research_questions),
|
||||
)
|
||||
)
|
||||
for question in res.research_questions:
|
||||
question_id = str(uuid.uuid4())
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "answer",
|
||||
"state": "pending",
|
||||
"id": question_id,
|
||||
"question": question,
|
||||
"answer": None,
|
||||
},
|
||||
)
|
||||
)
|
||||
ctx.send_event(
|
||||
ResearchEvent(
|
||||
question_id=question_id,
|
||||
question=question,
|
||||
context_nodes=self.context_nodes,
|
||||
)
|
||||
)
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "analyze",
|
||||
"state": "done",
|
||||
},
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
@step(num_workers=2)
|
||||
async def answer(self, ctx: Context, ev: ResearchEvent) -> CollectAnswersEvent:
|
||||
"""
|
||||
Answer the question
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "answer",
|
||||
"state": "inprogress",
|
||||
"id": ev.question_id,
|
||||
"question": ev.question,
|
||||
},
|
||||
)
|
||||
)
|
||||
try:
|
||||
answer = await research(
|
||||
context_nodes=ev.context_nodes,
|
||||
question=ev.question,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error answering question {ev.question}: {e}")
|
||||
answer = f"Got error when answering the question: {ev.question}"
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "answer",
|
||||
"state": "done",
|
||||
"id": ev.question_id,
|
||||
"question": ev.question,
|
||||
"answer": answer,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return CollectAnswersEvent(
|
||||
question_id=ev.question_id,
|
||||
question=ev.question,
|
||||
answer=answer,
|
||||
)
|
||||
|
||||
@step
|
||||
async def collect_answers(
|
||||
self, ctx: Context, ev: CollectAnswersEvent
|
||||
) -> PlanResearchEvent:
|
||||
"""
|
||||
Collect answers to all questions
|
||||
"""
|
||||
num_questions = await ctx.get("waiting_questions")
|
||||
results = ctx.collect_events(
|
||||
ev,
|
||||
expected=[CollectAnswersEvent] * num_questions,
|
||||
)
|
||||
if results is None:
|
||||
return None
|
||||
for result in results:
|
||||
self.memory.put(
|
||||
message=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=f"<Question>{result.question}</Question>\n<Answer>{result.answer}</Answer>",
|
||||
)
|
||||
)
|
||||
await ctx.set("waiting_questions", 0)
|
||||
self.memory.put(
|
||||
message=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Researched all the questions. Now, i need to analyze if it's ready to write a report or need to research more.",
|
||||
)
|
||||
)
|
||||
return PlanResearchEvent()
|
||||
|
||||
@step
|
||||
async def report(self, ctx: Context, ev: ReportEvent) -> StopEvent:
|
||||
"""
|
||||
Report the answers
|
||||
"""
|
||||
res = await write_report(
|
||||
memory=self.memory,
|
||||
user_request=self.user_request,
|
||||
stream=self.stream,
|
||||
)
|
||||
return StopEvent(
|
||||
result=res,
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
from llama_index.core.workflow import Event
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Workflow events
|
||||
class PlanResearchEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
class ResearchEvent(Event):
|
||||
question_id: str
|
||||
question: str
|
||||
context_nodes: List[NodeWithScore]
|
||||
|
||||
|
||||
class CollectAnswersEvent(Event):
|
||||
question_id: str
|
||||
question: str
|
||||
answer: str
|
||||
|
||||
|
||||
class ReportEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
# Events that are streamed to the frontend and rendered there
|
||||
class DeepResearchEventData(BaseModel):
|
||||
event: Literal["retrieve", "analyze", "answer"]
|
||||
state: Literal["pending", "inprogress", "done", "error"]
|
||||
id: Optional[str] = None
|
||||
question: Optional[str] = None
|
||||
answer: Optional[str] = None
|
||||
|
||||
|
||||
class DataEvent(Event):
|
||||
type: Literal["deep_research_event"]
|
||||
data: DeepResearchEventData
|
||||
|
||||
def to_response(self):
|
||||
return self.model_dump()
|
||||
@@ -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`.
|
||||
@@ -33,16 +33,20 @@ curl --location 'localhost:8000/api/chat' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/financial_report/workflow.py`. The API auto-updates as you save the files.
|
||||
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
|
||||
```
|
||||
|
||||
## Deployments
|
||||
|
||||
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Tuple
|
||||
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
|
||||
def _get_analyst_params() -> Tuple[List[type[FunctionTool]], str, str]:
|
||||
tools = []
|
||||
prompt_instructions = dedent(
|
||||
"""
|
||||
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 a textual format like 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.
|
||||
"""
|
||||
)
|
||||
description = "Expert in analyzing financial data"
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
# Check if the interpreter tool is configured
|
||||
if "interpret" in configured_tools.keys():
|
||||
tools.append(configured_tools["interpret"])
|
||||
prompt_instructions += dedent("""
|
||||
You are able to visualize the financial data using code interpreter tool.
|
||||
It's very useful to create and include visualizations to the report (make sure you include the right code and data for the visualization).
|
||||
Never include any code into the report, just the visualization.
|
||||
""")
|
||||
description += (
|
||||
", able to visualize the financial data using code interpreter tool."
|
||||
)
|
||||
return tools, prompt_instructions, description
|
||||
|
||||
|
||||
def create_analyst(chat_history: List[ChatMessage]):
|
||||
tools, prompt_instructions, description = _get_analyst_params()
|
||||
|
||||
return FunctionCallingAgent(
|
||||
name="analyst",
|
||||
tools=tools,
|
||||
description=description,
|
||||
system_prompt=dedent(prompt_instructions),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -1,44 +0,0 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Tuple
|
||||
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import BaseTool
|
||||
|
||||
|
||||
def _get_reporter_params(
|
||||
chat_history: List[ChatMessage],
|
||||
) -> Tuple[List[type[BaseTool]], str, str]:
|
||||
tools: List[type[BaseTool]] = []
|
||||
description = "Expert in representing a financial report"
|
||||
prompt_instructions = dedent(
|
||||
"""
|
||||
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 provided, add them to the right sections that are most relevant.
|
||||
Use only the provided information to create the report. Do not make up any information yourself.
|
||||
Finally, the report should be presented in markdown format.
|
||||
"""
|
||||
)
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
if "generate_document" in configured_tools: # type: ignore
|
||||
tools.append(configured_tools["generate_document"]) # type: ignore
|
||||
prompt_instructions += (
|
||||
"\nYou are also able to generate a file document (PDF/HTML) of the report."
|
||||
)
|
||||
description += " and generate a file document (PDF/HTML) of the report."
|
||||
return tools, description, prompt_instructions
|
||||
|
||||
|
||||
def create_reporter(chat_history: List[ChatMessage]):
|
||||
tools, description, prompt_instructions = _get_reporter_params(chat_history)
|
||||
return FunctionCallingAgent(
|
||||
name="reporter",
|
||||
tools=tools,
|
||||
description=description,
|
||||
system_prompt=prompt_instructions,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import BaseTool, QueryEngineTool, ToolMetadata
|
||||
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
|
||||
|
||||
|
||||
def _create_query_engine_tools(params=None) -> Optional[list[type[BaseTool]]]:
|
||||
"""
|
||||
Provide an agent worker that can be used to query the index.
|
||||
"""
|
||||
# Add query tool if index exists
|
||||
index_config = IndexConfig(**(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
return None
|
||||
|
||||
top_k = int(os.getenv("TOP_K", 5))
|
||||
|
||||
# Construct query engine tools
|
||||
tools = []
|
||||
# If index is LlamaCloudIndex, we need to add chunk and doc retriever tools
|
||||
if isinstance(index, LlamaCloudIndex):
|
||||
# Document retriever
|
||||
doc_retriever = index.as_query_engine(
|
||||
retriever_mode="files_via_content",
|
||||
similarity_top_k=top_k,
|
||||
)
|
||||
chunk_retriever = index.as_query_engine(
|
||||
retriever_mode="chunks",
|
||||
similarity_top_k=top_k,
|
||||
)
|
||||
tools.append(
|
||||
QueryEngineTool(
|
||||
query_engine=doc_retriever,
|
||||
metadata=ToolMetadata(
|
||||
name="document_retriever",
|
||||
description=dedent(
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
tools.append(
|
||||
QueryEngineTool(
|
||||
query_engine=chunk_retriever,
|
||||
metadata=ToolMetadata(
|
||||
name="chunk_retriever",
|
||||
description=dedent(
|
||||
"""
|
||||
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:
|
||||
query_engine = index.as_query_engine(
|
||||
**({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
tools.append(
|
||||
QueryEngineTool(
|
||||
query_engine=query_engine,
|
||||
metadata=ToolMetadata(
|
||||
name="retrieve_information",
|
||||
description="Use this tool to retrieve information about the text corpus from the index.",
|
||||
),
|
||||
)
|
||||
)
|
||||
return tools
|
||||
|
||||
|
||||
def create_researcher(chat_history: List[ChatMessage], **kwargs):
|
||||
"""
|
||||
Researcher is an agent that take responsibility for using tools to complete a given task.
|
||||
"""
|
||||
tools = _create_query_engine_tools(**kwargs)
|
||||
|
||||
if tools is None:
|
||||
raise ValueError("No tools found for researcher agent")
|
||||
|
||||
return FunctionCallingAgent(
|
||||
name="researcher",
|
||||
tools=tools,
|
||||
description="expert in retrieving any unknown content from the corpus",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
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 response with: "There is no new information, please reuse the information from the conversation."
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -1,177 +0,0 @@
|
||||
from textwrap import dedent
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
from app.agents.analyst import create_analyst
|
||||
from app.agents.reporter import create_reporter
|
||||
from app.agents.researcher import create_researcher
|
||||
from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(
|
||||
chat_history=chat_history,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
analyst = create_analyst(chat_history=chat_history)
|
||||
|
||||
reporter = create_reporter(chat_history=chat_history)
|
||||
|
||||
workflow = FinancialReportWorkflow(timeout=360, chat_history=chat_history)
|
||||
|
||||
workflow.add_workflows(
|
||||
researcher=researcher,
|
||||
analyst=analyst,
|
||||
reporter=reporter,
|
||||
)
|
||||
return workflow
|
||||
|
||||
|
||||
class ResearchEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class AnalyzeEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class ReportEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class FinancialReportWorkflow(Workflow):
|
||||
def __init__(
|
||||
self, timeout: int = 360, chat_history: Optional[List[ChatMessage]] = None
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.chat_history = chat_history or []
|
||||
|
||||
@step()
|
||||
async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent | ReportEvent:
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
# start the workflow with researching about a topic
|
||||
ctx.data["task"] = ev.input
|
||||
ctx.data["user_input"] = ev.input
|
||||
|
||||
# Decision-making process
|
||||
decision = await self._decide_workflow(ev.input, self.chat_history)
|
||||
|
||||
if decision != "publish":
|
||||
return ResearchEvent(input=f"Research for this task: {ev.input}")
|
||||
else:
|
||||
chat_history_str = "\n".join(
|
||||
[f"{msg.role}: {msg.content}" for msg in self.chat_history]
|
||||
)
|
||||
return ReportEvent(
|
||||
input=f"Create a report based on the chat history\n{chat_history_str}\n\n and task: {ev.input}"
|
||||
)
|
||||
|
||||
async def _decide_workflow(
|
||||
self, input: str, chat_history: List[ChatMessage]
|
||||
) -> str:
|
||||
# TODO: Refactor this by using prompt generation
|
||||
prompt_template = PromptTemplate(
|
||||
dedent(
|
||||
"""
|
||||
You are an expert in decision-making, helping people create financial reports for the provided data.
|
||||
If the user doesn't need to add or update anything, respond with 'publish'.
|
||||
Otherwise, respond with 'research'.
|
||||
|
||||
Here is the chat history:
|
||||
{chat_history}
|
||||
|
||||
The current user request is:
|
||||
{input}
|
||||
|
||||
Given the chat history and the new user request, decide whether to create a report based on existing information.
|
||||
Decision (respond with either 'not_publish' or 'publish'):
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
chat_history_str = "\n".join(
|
||||
[f"{msg.role}: {msg.content}" for msg in chat_history]
|
||||
)
|
||||
prompt = prompt_template.format(chat_history=chat_history_str, input=input)
|
||||
|
||||
output = await Settings.llm.acomplete(prompt)
|
||||
decision = output.text.strip().lower()
|
||||
|
||||
return "publish" if decision == "publish" else "research"
|
||||
|
||||
@step()
|
||||
async def research(
|
||||
self, ctx: Context, ev: ResearchEvent, researcher: FunctionCallingAgent
|
||||
) -> AnalyzeEvent:
|
||||
result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input)
|
||||
content = result.response.message.content
|
||||
return AnalyzeEvent(
|
||||
input=dedent(
|
||||
f"""
|
||||
Given the following research content:
|
||||
{content}
|
||||
Provide a comprehensive analysis of the data for the user's request: {ctx.data["task"]}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
@step()
|
||||
async def analyze(
|
||||
self, ctx: Context, ev: AnalyzeEvent, analyst: FunctionCallingAgent
|
||||
) -> ReportEvent | StopEvent:
|
||||
result: AgentRunResult = await self.run_agent(ctx, analyst, ev.input)
|
||||
content = result.response.message.content
|
||||
return ReportEvent(
|
||||
input=dedent(
|
||||
f"""
|
||||
Given the following analysis:
|
||||
{content}
|
||||
Create a report for the user's request: {ctx.data["task"]}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
@step()
|
||||
async def report(
|
||||
self, ctx: Context, ev: ReportEvent, reporter: FunctionCallingAgent
|
||||
) -> StopEvent:
|
||||
try:
|
||||
result: AgentRunResult = await self.run_agent(
|
||||
ctx, reporter, ev.input, streaming=ctx.data["streaming"]
|
||||
)
|
||||
return StopEvent(result=result)
|
||||
except Exception as e:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=reporter.name,
|
||||
msg=f"Error creating a report: {e}",
|
||||
)
|
||||
)
|
||||
return StopEvent(result=None)
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
ctx: Context,
|
||||
agent: FunctionCallingAgent,
|
||||
input: str,
|
||||
streaming: bool = False,
|
||||
) -> AgentRunResult | AsyncGenerator:
|
||||
handler = agent.run(input=input, streaming=streaming)
|
||||
# bubble all events while running the executor to the planner
|
||||
async for event in handler.stream_events():
|
||||
# Don't write the StopEvent from sub task to the stream
|
||||
if type(event) is not StopEvent:
|
||||
ctx.write_event_to_stream(event)
|
||||
return await handler
|
||||
@@ -1,12 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from app.agents.workflow import create_workflow
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.workflow import Workflow
|
||||
|
||||
|
||||
def get_chat_engine(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
agent_workflow = create_workflow(chat_history, **kwargs)
|
||||
return agent_workflow
|
||||
@@ -0,0 +1,3 @@
|
||||
from .financial_report import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.tools.query_engine import get_query_engine_tool
|
||||
from app.workflows.events import AgentRunEvent
|
||||
from app.workflows.tools import (
|
||||
call_tools,
|
||||
chat_with_tools,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> Workflow:
|
||||
# Create query engine tool
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
raise ValueError(
|
||||
"Index is not found. Try run generation script to create the index first."
|
||||
)
|
||||
query_engine_tool = get_query_engine_tool(index=index)
|
||||
|
||||
configured_tools: Dict[str, FunctionTool] = ToolFactory.from_env(map_result=True) # type: ignore
|
||||
code_interpreter_tool = configured_tools.get("interpret")
|
||||
document_generator_tool = configured_tools.get("generate_document")
|
||||
|
||||
return FinancialReportWorkflow(
|
||||
query_engine_tool=query_engine_tool,
|
||||
code_interpreter_tool=code_interpreter_tool,
|
||||
document_generator_tool=document_generator_tool,
|
||||
)
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: List[ChatMessage]
|
||||
response: bool = False
|
||||
|
||||
|
||||
class ResearchEvent(Event):
|
||||
input: list[ToolSelection]
|
||||
|
||||
|
||||
class AnalyzeEvent(Event):
|
||||
input: list[ToolSelection] | ChatMessage
|
||||
|
||||
|
||||
class ReportEvent(Event):
|
||||
input: list[ToolSelection]
|
||||
|
||||
|
||||
class FinancialReportWorkflow(Workflow):
|
||||
"""
|
||||
A workflow to generate a financial report using indexed documents.
|
||||
|
||||
Requirements:
|
||||
- Indexed documents containing financial data and a query engine tool to search them
|
||||
- A code interpreter tool to analyze data and generate reports
|
||||
- A document generator tool to create report files
|
||||
|
||||
Steps:
|
||||
1. LLM Input: The LLM determines the next step based on function calling.
|
||||
For example, if the model requests the query engine tool, it returns a ResearchEvent;
|
||||
if it requests document generation, it returns a ReportEvent.
|
||||
2. Research: Uses the query engine to find relevant chunks from indexed documents.
|
||||
After gathering information, it requests analysis (step 3).
|
||||
3. Analyze: Uses a custom prompt to analyze research results and can call the code
|
||||
interpreter tool for visualization or calculation. Returns results to the LLM.
|
||||
4. Report: Uses the document generator tool to create a report. Returns results to the LLM.
|
||||
"""
|
||||
|
||||
_default_system_prompt = """
|
||||
You are a financial analyst who are given a set of tools to help you.
|
||||
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
|
||||
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
|
||||
"""
|
||||
stream: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_engine_tool: QueryEngineTool,
|
||||
code_interpreter_tool: FunctionTool,
|
||||
document_generator_tool: FunctionTool,
|
||||
llm: Optional[FunctionCallingLLM] = None,
|
||||
timeout: int = 360,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.system_prompt = system_prompt or self._default_system_prompt
|
||||
self.query_engine_tool = query_engine_tool
|
||||
self.code_interpreter_tool = code_interpreter_tool
|
||||
self.document_generator_tool = document_generator_tool
|
||||
assert query_engine_tool is not None, (
|
||||
"Query engine tool is not found. Try run generation script or upload a document file first."
|
||||
)
|
||||
assert code_interpreter_tool is not None, "Code interpreter tool is required"
|
||||
assert document_generator_tool is not None, (
|
||||
"Document generator tool is required"
|
||||
)
|
||||
self.tools = [
|
||||
self.query_engine_tool,
|
||||
self.code_interpreter_tool,
|
||||
self.document_generator_tool,
|
||||
]
|
||||
self.llm: FunctionCallingLLM = llm or Settings.llm
|
||||
assert isinstance(self.llm, FunctionCallingLLM)
|
||||
self.memory = ChatMemoryBuffer.from_defaults(llm=self.llm)
|
||||
|
||||
@step()
|
||||
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
self.stream = ev.get("stream", True)
|
||||
user_msg = ev.get("user_msg")
|
||||
chat_history = ev.get("chat_history")
|
||||
|
||||
if chat_history is not None:
|
||||
self.memory.put_messages(chat_history)
|
||||
|
||||
# Add user message to memory
|
||||
self.memory.put(ChatMessage(role=MessageRole.USER, content=user_msg))
|
||||
|
||||
if self.system_prompt:
|
||||
system_msg = ChatMessage(
|
||||
role=MessageRole.SYSTEM, content=self.system_prompt
|
||||
)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def handle_llm_input( # type: ignore
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ResearchEvent | AnalyzeEvent | ReportEvent | StopEvent:
|
||||
"""
|
||||
Handle an LLM input and decide the next step.
|
||||
"""
|
||||
# Always use the latest chat history from the input
|
||||
chat_history: list[ChatMessage] = ev.input
|
||||
|
||||
# Get tool calls
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
self.tools, # type: ignore
|
||||
chat_history,
|
||||
)
|
||||
if not response.has_tool_calls():
|
||||
if self.stream:
|
||||
return StopEvent(result=response.generator)
|
||||
else:
|
||||
return StopEvent(result=await response.full_response())
|
||||
# calling different tools at the same time is not supported at the moment
|
||||
# add an error message to tell the AI to process step by step
|
||||
if response.is_calling_different_tools():
|
||||
self.memory.put(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Cannot call different tools at the same time. Try calling one tool at a time.",
|
||||
)
|
||||
)
|
||||
return InputEvent(input=self.memory.get())
|
||||
self.memory.put(response.tool_call_message)
|
||||
match response.tool_name():
|
||||
case self.code_interpreter_tool.metadata.name:
|
||||
return AnalyzeEvent(input=response.tool_calls)
|
||||
case self.document_generator_tool.metadata.name:
|
||||
return ReportEvent(input=response.tool_calls)
|
||||
case self.query_engine_tool.metadata.name:
|
||||
return ResearchEvent(input=response.tool_calls)
|
||||
case _:
|
||||
raise ValueError(f"Unknown tool: {response.tool_name()}")
|
||||
|
||||
@step()
|
||||
async def research(self, ctx: Context, ev: ResearchEvent) -> AnalyzeEvent:
|
||||
"""
|
||||
Do a research to gather information for the user's request.
|
||||
A researcher should have these tools: query engine, search engine, etc.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg="Starting research",
|
||||
)
|
||||
)
|
||||
tool_calls = ev.input
|
||||
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Researcher",
|
||||
tools=[self.query_engine_tool],
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return AnalyzeEvent(
|
||||
input=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="I've finished the research. Please analyze the result.",
|
||||
),
|
||||
)
|
||||
|
||||
@step()
|
||||
async def analyze(self, ctx: Context, ev: AnalyzeEvent) -> InputEvent:
|
||||
"""
|
||||
Analyze the research result.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Analyst",
|
||||
msg="Starting analysis",
|
||||
)
|
||||
)
|
||||
event_requested_by_workflow_llm = isinstance(ev.input, list)
|
||||
# Requested by the workflow LLM Input step, it's a tool call
|
||||
if event_requested_by_workflow_llm:
|
||||
# Set the tool calls
|
||||
tool_calls = ev.input
|
||||
else:
|
||||
# Otherwise, it's triggered by the research step
|
||||
# Use a custom prompt and independent memory for the analyst agent
|
||||
analysis_prompt = """
|
||||
You are a financial analyst, you are given a research result and a set of tools to help you.
|
||||
Always use the given information, don't make up anything yourself. If there is not enough information, you can asking for more information.
|
||||
If you have enough numerical information, it's good to include some charts/visualizations to the report so you can use the code interpreter tool to generate a report.
|
||||
"""
|
||||
# This is handled by analyst agent
|
||||
# Clone the shared memory to avoid conflicting with the workflow.
|
||||
chat_history = self.memory.get()
|
||||
chat_history.append(
|
||||
ChatMessage(role=MessageRole.SYSTEM, content=analysis_prompt)
|
||||
)
|
||||
chat_history.append(ev.input) # type: ignore
|
||||
# Check if the analyst agent needs to call tools
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
[self.code_interpreter_tool],
|
||||
chat_history,
|
||||
)
|
||||
if not response.has_tool_calls():
|
||||
# If no tool call, fallback analyst message to the workflow
|
||||
analyst_msg = ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=await response.full_response(),
|
||||
)
|
||||
self.memory.put(analyst_msg)
|
||||
return InputEvent(input=self.memory.get())
|
||||
else:
|
||||
# Set the tool calls and the tool call message to the memory
|
||||
tool_calls = response.tool_calls
|
||||
self.memory.put(response.tool_call_message)
|
||||
|
||||
# Call tools
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Analyst",
|
||||
tools=[self.code_interpreter_tool],
|
||||
tool_calls=tool_calls, # type: ignore
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
|
||||
# Fallback to the input with the latest chat history
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def report(self, ctx: Context, ev: ReportEvent) -> InputEvent:
|
||||
"""
|
||||
Generate a report based on the analysis result.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Reporter",
|
||||
msg="Starting report generation",
|
||||
)
|
||||
)
|
||||
tool_calls = ev.input
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Reporter",
|
||||
tools=[self.document_generator_tool],
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
|
||||
# After the tool calls, fallback to the input with the latest chat history
|
||||
return InputEvent(input=self.memory.get())
|
||||
@@ -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
|
||||
@@ -39,16 +39,20 @@ curl --location 'localhost:8000/api/chat' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "What can you do?" }] }'
|
||||
```
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/agents/form_filling.py`. The API auto-updates as you save the files.
|
||||
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
|
||||
```
|
||||
|
||||
## Deployments
|
||||
|
||||
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
from app.engine.index import get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.tools.form_filling import CellValue, MissingCell
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
||||
from llama_index.core.indices.vector_store import VectorStoreIndex
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
|
||||
from llama_index.core.tools.types import ToolOutput
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
def create_workflow(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
index: VectorStoreIndex = get_index()
|
||||
if index is None:
|
||||
query_engine_tool = None
|
||||
else:
|
||||
top_k = int(os.getenv("TOP_K", 10))
|
||||
query_engine = index.as_query_engine(similarity_top_k=top_k)
|
||||
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
|
||||
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
extractor_tool = configured_tools.get("extract_questions")
|
||||
filling_tool = configured_tools.get("fill_form")
|
||||
|
||||
if extractor_tool is None or filling_tool is None:
|
||||
raise ValueError("Extractor or filling tool is not found!")
|
||||
|
||||
workflow = FormFillingWorkflow(
|
||||
query_engine_tool=query_engine_tool,
|
||||
extractor_tool=extractor_tool,
|
||||
filling_tool=filling_tool,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
|
||||
return workflow
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: List[ChatMessage]
|
||||
response: bool = False
|
||||
|
||||
|
||||
class ExtractMissingCellsEvent(Event):
|
||||
tool_call: ToolSelection
|
||||
|
||||
|
||||
class FindAnswersEvent(Event):
|
||||
missing_cells: list[MissingCell]
|
||||
|
||||
|
||||
class FillEvent(Event):
|
||||
tool_call: ToolSelection
|
||||
|
||||
|
||||
class AgentRunEventType(Enum):
|
||||
TEXT = "text"
|
||||
PROGRESS = "progress"
|
||||
|
||||
|
||||
class AgentRunEvent(Event):
|
||||
name: str
|
||||
msg: str
|
||||
event_type: AgentRunEventType = Field(default=AgentRunEventType.TEXT)
|
||||
data: Optional[dict] = None
|
||||
|
||||
def to_response(self) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"agent": self.name,
|
||||
"type": self.event_type.value,
|
||||
"text": self.msg,
|
||||
"data": self.data,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FormFillingWorkflow(Workflow):
|
||||
"""
|
||||
A predefined workflow for filling missing cells in a CSV file.
|
||||
Required tools:
|
||||
- query_engine: A query engine to query for the answers to the questions.
|
||||
- extract_question: Extract missing cells in a CSV file and generate questions to fill them.
|
||||
- answer_question: Query for the answers to the questions.
|
||||
|
||||
Flow:
|
||||
1. Extract missing cells in a CSV file and generate questions to fill them.
|
||||
2. Query for the answers to the questions.
|
||||
3. Fill the missing cells with the answers.
|
||||
"""
|
||||
|
||||
_default_system_prompt = """
|
||||
You are a helpful assistant who helps fill missing cells in a CSV file.
|
||||
Only use provided data, never make up any information yourself. Fill N/A if the answer is not found.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_engine_tool: QueryEngineTool,
|
||||
extractor_tool: FunctionTool,
|
||||
filling_tool: FunctionTool,
|
||||
llm: Optional[FunctionCallingLLM] = None,
|
||||
timeout: int = 360,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.system_prompt = system_prompt or self._default_system_prompt
|
||||
self.chat_history = chat_history or []
|
||||
self.query_engine_tool = query_engine_tool
|
||||
self.extractor_tool = extractor_tool
|
||||
self.filling_tool = filling_tool
|
||||
self.llm: FunctionCallingLLM = llm or Settings.llm
|
||||
if not isinstance(self.llm, FunctionCallingLLM):
|
||||
raise ValueError("FormFillingWorkflow only supports FunctionCallingLLM.")
|
||||
self.memory = ChatMemoryBuffer.from_defaults(
|
||||
llm=self.llm, chat_history=self.chat_history
|
||||
)
|
||||
|
||||
@step()
|
||||
async def start(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
ctx.data["input"] = ev.input
|
||||
|
||||
if self.system_prompt:
|
||||
system_msg = ChatMessage(
|
||||
role=MessageRole.SYSTEM, content=self.system_prompt
|
||||
)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
user_input = ev.input
|
||||
user_msg = ChatMessage(role=MessageRole.USER, content=user_input)
|
||||
self.memory.put(user_msg)
|
||||
|
||||
chat_history = self.memory.get()
|
||||
return InputEvent(input=chat_history)
|
||||
|
||||
@step(pass_context=True)
|
||||
async def handle_llm_input( # type: ignore
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ExtractMissingCellsEvent | FillEvent | StopEvent:
|
||||
"""
|
||||
Handle an LLM input and decide the next step.
|
||||
"""
|
||||
chat_history: list[ChatMessage] = ev.input
|
||||
|
||||
generator = self._tool_call_generator(chat_history)
|
||||
|
||||
# Check for immediate tool call
|
||||
is_tool_call = await generator.__anext__()
|
||||
if is_tool_call:
|
||||
full_response = await generator.__anext__()
|
||||
tool_calls = self.llm.get_tool_calls_from_response(full_response) # type: ignore
|
||||
for tool_call in tool_calls:
|
||||
if tool_call.tool_name == self.extractor_tool.metadata.get_name():
|
||||
ctx.send_event(ExtractMissingCellsEvent(tool_call=tool_call))
|
||||
elif tool_call.tool_name == self.filling_tool.metadata.get_name():
|
||||
ctx.send_event(FillEvent(tool_call=tool_call))
|
||||
else:
|
||||
# If no tool call, return the generator
|
||||
return StopEvent(result=generator)
|
||||
|
||||
@step()
|
||||
async def extract_missing_cells(
|
||||
self, ctx: Context, ev: ExtractMissingCellsEvent
|
||||
) -> InputEvent | FindAnswersEvent:
|
||||
"""
|
||||
Extract missing cells in a CSV file and generate questions to fill them.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Extractor",
|
||||
msg="Extracting missing cells",
|
||||
)
|
||||
)
|
||||
# Call the extract questions tool
|
||||
response = self._call_tool(
|
||||
ctx,
|
||||
agent_name="Extractor",
|
||||
tool=self.extractor_tool,
|
||||
tool_selection=ev.tool_call,
|
||||
)
|
||||
if response.is_error:
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
missing_cells = response.raw_output.get("missing_cells", [])
|
||||
message = ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=str(missing_cells),
|
||||
additional_kwargs={
|
||||
"tool_call_id": ev.tool_call.tool_id,
|
||||
"name": ev.tool_call.tool_name,
|
||||
},
|
||||
)
|
||||
self.memory.put(message)
|
||||
|
||||
if self.query_engine_tool is None:
|
||||
# Fallback to input that query engine tool is not found so that cannot answer questions
|
||||
self.memory.put(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Extracted missing cells but query engine tool is not found so cannot answer questions. Ask user to upload file or connect to a knowledge base.",
|
||||
)
|
||||
)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
# Forward missing cells information to find answers step
|
||||
return FindAnswersEvent(missing_cells=missing_cells)
|
||||
|
||||
@step()
|
||||
async def find_answers(self, ctx: Context, ev: FindAnswersEvent) -> InputEvent:
|
||||
"""
|
||||
Call answer questions tool to query for the answers to the questions.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg="Finding answers for missing cells",
|
||||
)
|
||||
)
|
||||
missing_cells = ev.missing_cells
|
||||
# If missing cells information is not found, fallback to other tools
|
||||
# It means that the extractor tool has not been called yet
|
||||
# Fallback to input
|
||||
if missing_cells is None:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg="Error: Missing cells information not found. Fallback to other tools.",
|
||||
)
|
||||
)
|
||||
message = ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content="Error: Missing cells information not found.",
|
||||
additional_kwargs={
|
||||
"tool_call_id": ev.tool_call.tool_id,
|
||||
"name": ev.tool_call.tool_name,
|
||||
},
|
||||
)
|
||||
self.memory.put(message)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
cell_values: list[CellValue] = []
|
||||
# Iterate over missing cells and query for the answers
|
||||
# and stream the progress
|
||||
progress_id = str(uuid.uuid4())
|
||||
total_steps = len(missing_cells)
|
||||
for i, cell in enumerate(missing_cells):
|
||||
if cell.question_to_answer is None:
|
||||
continue
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg=f"Querying for: {cell.question_to_answer}",
|
||||
event_type=AgentRunEventType.PROGRESS,
|
||||
data={
|
||||
"id": progress_id,
|
||||
"total": total_steps,
|
||||
"current": i,
|
||||
},
|
||||
)
|
||||
)
|
||||
# Call query engine tool directly
|
||||
answer = await self.query_engine_tool.acall(query=cell.question_to_answer)
|
||||
cell_values.append(
|
||||
CellValue(
|
||||
row_index=cell.row_index,
|
||||
column_index=cell.column_index,
|
||||
value=str(answer),
|
||||
)
|
||||
)
|
||||
self.memory.put(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=str(cell_values),
|
||||
)
|
||||
)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def fill_cells(self, ctx: Context, ev: FillEvent) -> InputEvent:
|
||||
"""
|
||||
Call fill cells tool to fill the missing cells with the answers.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Processor",
|
||||
msg="Filling missing cells",
|
||||
)
|
||||
)
|
||||
# Call the fill cells tool
|
||||
result = self._call_tool(
|
||||
ctx,
|
||||
agent_name="Processor",
|
||||
tool=self.filling_tool,
|
||||
tool_selection=ev.tool_call,
|
||||
)
|
||||
if result.is_error:
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
message = ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=str(result.raw_output),
|
||||
additional_kwargs={
|
||||
"tool_call_id": ev.tool_call.tool_id,
|
||||
"name": ev.tool_call.tool_name,
|
||||
},
|
||||
)
|
||||
self.memory.put(message)
|
||||
return InputEvent(input=self.memory.get(), response=True)
|
||||
|
||||
async def _tool_call_generator(
|
||||
self, chat_history: list[ChatMessage]
|
||||
) -> AsyncGenerator[ChatMessage | bool, None]:
|
||||
response_stream = await self.llm.astream_chat_with_tools(
|
||||
[self.extractor_tool, self.filling_tool],
|
||||
chat_history=chat_history,
|
||||
)
|
||||
|
||||
full_response = None
|
||||
yielded_indicator = False
|
||||
async for chunk in response_stream:
|
||||
if "tool_calls" not in chunk.message.additional_kwargs:
|
||||
# Yield a boolean to indicate whether the response is a tool call
|
||||
if not yielded_indicator:
|
||||
yield False
|
||||
yielded_indicator = True
|
||||
|
||||
# if not a tool call, yield the chunks!
|
||||
yield chunk
|
||||
elif not yielded_indicator:
|
||||
# Yield the indicator for a tool call
|
||||
yield True
|
||||
yielded_indicator = True
|
||||
|
||||
full_response = chunk
|
||||
|
||||
# Write the full response to memory and yield it
|
||||
if full_response:
|
||||
self.memory.put(full_response.message)
|
||||
yield full_response
|
||||
|
||||
def _call_tool(
|
||||
self,
|
||||
ctx: Context,
|
||||
agent_name: str,
|
||||
tool: FunctionTool,
|
||||
tool_selection: ToolSelection,
|
||||
) -> ToolOutput:
|
||||
"""
|
||||
Safely call a tool and handle errors.
|
||||
"""
|
||||
try:
|
||||
response: ToolOutput = tool.call(**tool_selection.tool_kwargs)
|
||||
return response
|
||||
except Exception as e:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=f"Error: {str(e)}",
|
||||
)
|
||||
)
|
||||
message = ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=f"Error: {str(e)}",
|
||||
additional_kwargs={
|
||||
"tool_call_id": tool_selection.tool_id,
|
||||
"name": tool.metadata.get_name(),
|
||||
},
|
||||
)
|
||||
self.memory.put(message)
|
||||
return ToolOutput(
|
||||
content=f"Error: {str(e)}",
|
||||
tool_name=tool.metadata.get_name(),
|
||||
raw_input=tool_selection.tool_kwargs,
|
||||
raw_output=None,
|
||||
is_error=True,
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from app.agents.form_filling import create_workflow
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.workflow import Workflow
|
||||
|
||||
|
||||
def get_chat_engine(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
return create_workflow(chat_history=chat_history, **kwargs)
|
||||
@@ -0,0 +1,3 @@
|
||||
from .form_filling import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
@@ -0,0 +1,236 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.tools.query_engine import get_query_engine_tool
|
||||
from app.workflows.events import AgentRunEvent
|
||||
from app.workflows.tools import (
|
||||
call_tools,
|
||||
chat_with_tools,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> Workflow:
|
||||
# Create query engine tool
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
query_engine_tool = None
|
||||
else:
|
||||
query_engine_tool = get_query_engine_tool(index=index)
|
||||
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
extractor_tool = configured_tools.get("extract_questions") # type: ignore
|
||||
filling_tool = configured_tools.get("fill_form") # type: ignore
|
||||
|
||||
workflow = FormFillingWorkflow(
|
||||
query_engine_tool=query_engine_tool,
|
||||
extractor_tool=extractor_tool, # type: ignore
|
||||
filling_tool=filling_tool, # type: ignore
|
||||
)
|
||||
|
||||
return workflow
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: List[ChatMessage]
|
||||
response: bool = False
|
||||
|
||||
|
||||
class ExtractMissingCellsEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class FindAnswersEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class FillEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class FormFillingWorkflow(Workflow):
|
||||
"""
|
||||
A predefined workflow for filling missing cells in a CSV file.
|
||||
Required tools:
|
||||
- query_engine: A query engine to query for the answers to the questions.
|
||||
- extract_question: Extract missing cells in a CSV file and generate questions to fill them.
|
||||
- answer_question: Query for the answers to the questions.
|
||||
|
||||
Flow:
|
||||
1. Extract missing cells in a CSV file and generate questions to fill them.
|
||||
2. Query for the answers to the questions.
|
||||
3. Fill the missing cells with the answers.
|
||||
"""
|
||||
|
||||
_default_system_prompt = """
|
||||
You are a helpful assistant who helps fill missing cells in a CSV file.
|
||||
Only extract missing cells from CSV files.
|
||||
Only use provided data - never make up any information yourself. Fill N/A if an answer is not found.
|
||||
If there is no query engine 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.
|
||||
"""
|
||||
stream: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_engine_tool: Optional[QueryEngineTool],
|
||||
extractor_tool: FunctionTool,
|
||||
filling_tool: FunctionTool,
|
||||
llm: Optional[FunctionCallingLLM] = None,
|
||||
timeout: int = 360,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.system_prompt = system_prompt or self._default_system_prompt
|
||||
self.query_engine_tool = query_engine_tool
|
||||
self.extractor_tool = extractor_tool
|
||||
self.filling_tool = filling_tool
|
||||
if self.extractor_tool is None or self.filling_tool is None:
|
||||
raise ValueError("Extractor and filling tools are required.")
|
||||
self.tools = [self.extractor_tool, self.filling_tool]
|
||||
if self.query_engine_tool is not None:
|
||||
self.tools.append(self.query_engine_tool) # type: ignore
|
||||
self.llm: FunctionCallingLLM = llm or Settings.llm
|
||||
if not isinstance(self.llm, FunctionCallingLLM):
|
||||
raise ValueError("FormFillingWorkflow only supports FunctionCallingLLM.")
|
||||
self.memory = ChatMemoryBuffer.from_defaults(llm=self.llm)
|
||||
|
||||
@step()
|
||||
async def start(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
self.stream = ev.get("stream", True)
|
||||
user_msg = ev.get("user_msg", "")
|
||||
chat_history = ev.get("chat_history", [])
|
||||
|
||||
if chat_history:
|
||||
self.memory.put_messages(chat_history)
|
||||
|
||||
self.memory.put(ChatMessage(role=MessageRole.USER, content=user_msg))
|
||||
|
||||
if self.system_prompt:
|
||||
system_msg = ChatMessage(
|
||||
role=MessageRole.SYSTEM, content=self.system_prompt
|
||||
)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def handle_llm_input( # type: ignore
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ExtractMissingCellsEvent | FillEvent | StopEvent:
|
||||
"""
|
||||
Handle an LLM input and decide the next step.
|
||||
"""
|
||||
chat_history: list[ChatMessage] = ev.input
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
self.tools,
|
||||
chat_history,
|
||||
)
|
||||
if not response.has_tool_calls():
|
||||
if self.stream:
|
||||
return StopEvent(result=response.generator)
|
||||
else:
|
||||
return StopEvent(result=await response.full_response())
|
||||
# calling different tools at the same time is not supported at the moment
|
||||
# add an error message to tell the AI to process step by step
|
||||
if response.is_calling_different_tools():
|
||||
self.memory.put(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Cannot call different tools at the same time. Try calling one tool at a time.",
|
||||
)
|
||||
)
|
||||
return InputEvent(input=self.memory.get())
|
||||
self.memory.put(response.tool_call_message)
|
||||
match response.tool_name():
|
||||
case self.extractor_tool.metadata.name:
|
||||
return ExtractMissingCellsEvent(tool_calls=response.tool_calls)
|
||||
case self.query_engine_tool.metadata.name:
|
||||
return FindAnswersEvent(tool_calls=response.tool_calls)
|
||||
case self.filling_tool.metadata.name:
|
||||
return FillEvent(tool_calls=response.tool_calls)
|
||||
case _:
|
||||
raise ValueError(f"Unknown tool: {response.tool_name()}")
|
||||
|
||||
@step()
|
||||
async def extract_missing_cells(
|
||||
self, ctx: Context, ev: ExtractMissingCellsEvent
|
||||
) -> InputEvent | FindAnswersEvent:
|
||||
"""
|
||||
Extract missing cells in a CSV file and generate questions to fill them.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Extractor",
|
||||
msg="Extracting missing cells",
|
||||
)
|
||||
)
|
||||
# Call the extract questions tool
|
||||
tool_messages = await call_tools(
|
||||
agent_name="Extractor",
|
||||
tools=[self.extractor_tool],
|
||||
ctx=ctx,
|
||||
tool_calls=ev.tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def find_answers(self, ctx: Context, ev: FindAnswersEvent) -> InputEvent:
|
||||
"""
|
||||
Call answer questions tool to query for the answers to the questions.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg="Finding answers for missing cells",
|
||||
)
|
||||
)
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Researcher",
|
||||
tools=[self.query_engine_tool],
|
||||
tool_calls=ev.tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def fill_cells(self, ctx: Context, ev: FillEvent) -> InputEvent:
|
||||
"""
|
||||
Call fill cells tool to fill the missing cells with the answers.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Processor",
|
||||
msg="Filling missing cells",
|
||||
)
|
||||
)
|
||||
tool_messages = await call_tools(
|
||||
agent_name="Processor",
|
||||
tools=[self.filling_tool],
|
||||
ctx=ctx,
|
||||
tool_calls=ev.tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
@@ -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 { getQueryEngineTool } 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 queryEngineTool = await getQueryEngineTool();
|
||||
const tools = [
|
||||
await getTool("wikipedia_tool"),
|
||||
await getTool("duckduckgo_search"),
|
||||
await getTool("image_generator"),
|
||||
queryEngineTool,
|
||||
].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,28 @@
|
||||
import { ChatMessage, ToolCallLLM } from "llamaindex";
|
||||
import { getTool } from "../engine/tools";
|
||||
import { FinancialReportWorkflow } from "./fin-report";
|
||||
import { getQueryEngineTool } from "./tools";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
|
||||
export async function createWorkflow(options: {
|
||||
chatHistory: ChatMessage[];
|
||||
llm?: ToolCallLLM;
|
||||
}) {
|
||||
const queryEngineTool = await getQueryEngineTool();
|
||||
const codeInterpreterTool = await getTool("interpreter");
|
||||
const documentGeneratorTool = await getTool("document_generator");
|
||||
|
||||
if (!queryEngineTool || !codeInterpreterTool || !documentGeneratorTool) {
|
||||
throw new Error("One or more required tools are not defined");
|
||||
}
|
||||
|
||||
return new FinancialReportWorkflow({
|
||||
chatHistory: options.chatHistory,
|
||||
queryEngineTool,
|
||||
codeInterpreterTool,
|
||||
documentGeneratorTool,
|
||||
llm: options.llm,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
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;
|
||||
queryEngineTool: BaseToolWithCall;
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
|
||||
constructor(options: {
|
||||
llm?: ToolCallLLM;
|
||||
chatHistory: ChatMessage[];
|
||||
queryEngineTool: 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.queryEngineTool = options.queryEngineTool;
|
||||
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,
|
||||
this.queryEngineTool,
|
||||
];
|
||||
|
||||
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
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.queryEngineTool.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.queryEngineTool],
|
||||
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,27 @@
|
||||
import { ChatMessage, ToolCallLLM } from "llamaindex";
|
||||
import { getTool } from "../engine/tools";
|
||||
import { FormFillingWorkflow } from "./form-filling";
|
||||
import { getQueryEngineTool } from "./tools";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
|
||||
export async function createWorkflow(options: {
|
||||
chatHistory: ChatMessage[];
|
||||
llm?: ToolCallLLM;
|
||||
}) {
|
||||
const extractorTool = await getTool("extract_missing_cells");
|
||||
const fillMissingCellsTool = await getTool("fill_missing_cells");
|
||||
|
||||
if (!extractorTool || !fillMissingCellsTool) {
|
||||
throw new Error("One or more required tools are not defined");
|
||||
}
|
||||
|
||||
return new FormFillingWorkflow({
|
||||
chatHistory: options.chatHistory,
|
||||
queryEngineTool: (await getQueryEngineTool()) || undefined,
|
||||
extractorTool,
|
||||
fillMissingCellsTool,
|
||||
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;
|
||||
queryEngineTool?: BaseToolWithCall;
|
||||
fillMissingCellsTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
|
||||
constructor(options: {
|
||||
llm?: ToolCallLLM;
|
||||
chatHistory: ChatMessage[];
|
||||
extractorTool: BaseToolWithCall;
|
||||
queryEngineTool?: 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.queryEngineTool = options.queryEngineTool;
|
||||
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.queryEngineTool) {
|
||||
tools.push(this.queryEngineTool);
|
||||
}
|
||||
|
||||
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
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.queryEngineTool &&
|
||||
this.queryEngineTool.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.queryEngineTool) {
|
||||
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.queryEngineTool],
|
||||
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,18 +1,18 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from llama_index.core.agent import AgentRunner
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import BaseTool
|
||||
from llama_index.core.tools.query_engine import QueryEngineTool
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.tools.query_engine import get_query_engine_tool
|
||||
|
||||
|
||||
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
|
||||
def get_chat_engine(params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
tools: List[BaseTool] = []
|
||||
callback_manager = CallbackManager(handlers=event_handlers or [])
|
||||
|
||||
@@ -20,10 +20,7 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
|
||||
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is not None:
|
||||
query_engine = index.as_query_engine(
|
||||
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
|
||||
query_engine_tool = get_query_engine_tool(index, **kwargs)
|
||||
tools.append(query_engine_tool)
|
||||
|
||||
# Add additional tools
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuid
|
||||
from typing import List, Optional
|
||||
|
||||
from app.services.file import DocumentFile, FileService
|
||||
from e2b_code_interpreter import CodeInterpreter
|
||||
from e2b_code_interpreter import Sandbox
|
||||
from e2b_code_interpreter.models import Logs
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel
|
||||
@@ -61,7 +61,7 @@ class E2BCodeInterpreter:
|
||||
Lazily initialize the interpreter.
|
||||
"""
|
||||
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
|
||||
self.interpreter = CodeInterpreter(api_key=self.api_key)
|
||||
self.interpreter = Sandbox(api_key=self.api_key)
|
||||
if len(sandbox_files) > 0:
|
||||
for file_path in sandbox_files:
|
||||
file_name = os.path.basename(file_path)
|
||||
@@ -159,11 +159,11 @@ class E2BCodeInterpreter:
|
||||
if self.interpreter is None:
|
||||
self._init_interpreter(sandbox_files)
|
||||
|
||||
if self.interpreter and self.interpreter.notebook:
|
||||
if self.interpreter:
|
||||
logger.info(
|
||||
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
|
||||
f"\n{'=' * 50}\n> Running following AI-generated code:\n{code}\n{'=' * 50}"
|
||||
)
|
||||
exec = self.interpreter.notebook.exec_cell(code)
|
||||
exec = self.interpreter.run_code(code)
|
||||
|
||||
if exec.error:
|
||||
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from llama_index.core import get_response_synthesizer
|
||||
from llama_index.core.base.base_query_engine import BaseQueryEngine
|
||||
from llama_index.core.base.response.schema import RESPONSE_TYPE, Response
|
||||
from llama_index.core.multi_modal_llms import MultiModalLLM
|
||||
from llama_index.core.prompts.base import BasePromptTemplate
|
||||
from llama_index.core.prompts.default_prompt_selectors import (
|
||||
DEFAULT_TEXT_QA_PROMPT_SEL,
|
||||
)
|
||||
from llama_index.core.query_engine.multi_modal import _get_image_and_text_nodes
|
||||
from llama_index.core.response_synthesizers.base import BaseSynthesizer, QueryTextType
|
||||
from llama_index.core.schema import (
|
||||
ImageNode,
|
||||
NodeWithScore,
|
||||
)
|
||||
from llama_index.core.tools.query_engine import QueryEngineTool
|
||||
from llama_index.core.types import RESPONSE_TEXT_TYPE
|
||||
|
||||
from app.settings import get_multi_modal_llm
|
||||
|
||||
|
||||
def create_query_engine(index, **kwargs) -> BaseQueryEngine:
|
||||
"""
|
||||
Create a query engine for the given index.
|
||||
|
||||
Args:
|
||||
index: The index to create a query engine for.
|
||||
params (optional): Additional parameters for the query engine, e.g: similarity_top_k
|
||||
"""
|
||||
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
if top_k != 0 and kwargs.get("filters") is None:
|
||||
kwargs["similarity_top_k"] = top_k
|
||||
multimodal_llm = get_multi_modal_llm()
|
||||
if multimodal_llm:
|
||||
kwargs["response_synthesizer"] = MultiModalSynthesizer(
|
||||
multimodal_model=multimodal_llm,
|
||||
)
|
||||
|
||||
# If index is index is LlamaCloudIndex
|
||||
# use auto_routed mode for better query results
|
||||
if index.__class__.__name__ == "LlamaCloudIndex":
|
||||
if kwargs.get("retrieval_mode") is None:
|
||||
kwargs["retrieval_mode"] = "auto_routed"
|
||||
if multimodal_llm:
|
||||
kwargs["retrieve_image_nodes"] = True
|
||||
return index.as_query_engine(**kwargs)
|
||||
|
||||
|
||||
def get_query_engine_tool(
|
||||
index,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueryEngineTool:
|
||||
"""
|
||||
Get a query engine tool for the given index.
|
||||
|
||||
Args:
|
||||
index: The index to create a query engine for.
|
||||
name (optional): The name of the tool.
|
||||
description (optional): The description of the tool.
|
||||
"""
|
||||
if name is None:
|
||||
name = "query_index"
|
||||
if description is None:
|
||||
description = (
|
||||
"Use this tool to retrieve information about the text corpus from an index."
|
||||
)
|
||||
query_engine = create_query_engine(index, **kwargs)
|
||||
return QueryEngineTool.from_defaults(
|
||||
query_engine=query_engine,
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
class MultiModalSynthesizer(BaseSynthesizer):
|
||||
"""
|
||||
A synthesizer that summarizes text nodes and uses a multi-modal LLM to generate a response.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
multimodal_model: MultiModalLLM,
|
||||
response_synthesizer: Optional[BaseSynthesizer] = None,
|
||||
text_qa_template: Optional[BasePromptTemplate] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._multi_modal_llm = multimodal_model
|
||||
self._response_synthesizer = response_synthesizer or get_response_synthesizer()
|
||||
self._text_qa_template = text_qa_template or DEFAULT_TEXT_QA_PROMPT_SEL
|
||||
|
||||
def _get_prompts(self, **kwargs) -> Dict[str, Any]:
|
||||
return {
|
||||
"text_qa_template": self._text_qa_template,
|
||||
}
|
||||
|
||||
def _update_prompts(self, prompts: Dict[str, Any]) -> None:
|
||||
if "text_qa_template" in prompts:
|
||||
self._text_qa_template = prompts["text_qa_template"]
|
||||
|
||||
async def aget_response(
|
||||
self,
|
||||
*args,
|
||||
**response_kwargs: Any,
|
||||
) -> RESPONSE_TEXT_TYPE:
|
||||
return await self._response_synthesizer.aget_response(*args, **response_kwargs)
|
||||
|
||||
def get_response(self, *args, **kwargs) -> RESPONSE_TEXT_TYPE:
|
||||
return self._response_synthesizer.get_response(*args, **kwargs)
|
||||
|
||||
async def asynthesize(
|
||||
self,
|
||||
query: QueryTextType,
|
||||
nodes: List[NodeWithScore],
|
||||
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
|
||||
**response_kwargs: Any,
|
||||
) -> RESPONSE_TYPE:
|
||||
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
|
||||
|
||||
if len(image_nodes) == 0:
|
||||
return await self._response_synthesizer.asynthesize(query, text_nodes)
|
||||
|
||||
# Summarize the text nodes to avoid exceeding the token limit
|
||||
text_response = str(
|
||||
await self._response_synthesizer.asynthesize(query, text_nodes)
|
||||
)
|
||||
|
||||
fmt_prompt = self._text_qa_template.format(
|
||||
context_str=text_response,
|
||||
query_str=query.query_str, # type: ignore
|
||||
)
|
||||
|
||||
llm_response = await self._multi_modal_llm.acomplete(
|
||||
prompt=fmt_prompt,
|
||||
image_documents=[
|
||||
image_node.node
|
||||
for image_node in image_nodes
|
||||
if isinstance(image_node.node, ImageNode)
|
||||
],
|
||||
)
|
||||
|
||||
return Response(
|
||||
response=str(llm_response),
|
||||
source_nodes=nodes,
|
||||
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
|
||||
)
|
||||
|
||||
def synthesize(
|
||||
self,
|
||||
query: QueryTextType,
|
||||
nodes: List[NodeWithScore],
|
||||
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
|
||||
**response_kwargs: Any,
|
||||
) -> RESPONSE_TYPE:
|
||||
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
|
||||
|
||||
if len(image_nodes) == 0:
|
||||
return self._response_synthesizer.synthesize(query, text_nodes)
|
||||
|
||||
# Summarize the text nodes to avoid exceeding the token limit
|
||||
text_response = str(self._response_synthesizer.synthesize(query, text_nodes))
|
||||
|
||||
fmt_prompt = self._text_qa_template.format(
|
||||
context_str=text_response,
|
||||
query_str=query.query_str, # type: ignore
|
||||
)
|
||||
|
||||
llm_response = self._multi_modal_llm.complete(
|
||||
prompt=fmt_prompt,
|
||||
image_documents=[
|
||||
image_node.node
|
||||
for image_node in image_nodes
|
||||
if isinstance(image_node.node, ImageNode)
|
||||
],
|
||||
)
|
||||
|
||||
return Response(
|
||||
response=str(llm_response),
|
||||
source_nodes=nodes,
|
||||
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
|
||||
)
|
||||
@@ -9,7 +9,7 @@ from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
|
||||
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
|
||||
def get_chat_engine(params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
@@ -33,10 +33,9 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
|
||||
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
|
||||
),
|
||||
)
|
||||
|
||||
retriever = index.as_retriever(
|
||||
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
if top_k != 0 and kwargs.get("similarity_top_k") is None:
|
||||
kwargs["similarity_top_k"] = top_k
|
||||
retriever = index.as_retriever(**kwargs)
|
||||
|
||||
return CondensePlusContextChatEngine(
|
||||
llm=llm,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
BaseChatEngine,
|
||||
BaseToolWithCall,
|
||||
LLMAgent,
|
||||
QueryEngineTool,
|
||||
} from "llamaindex";
|
||||
import { BaseChatEngine, BaseToolWithCall, LLMAgent } from "llamaindex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getDataSource } from "./index";
|
||||
import { generateFilters } from "./queryFilter";
|
||||
import { createTools } from "./tools";
|
||||
import { createQueryEngineTool } from "./tools/query-engine";
|
||||
|
||||
export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
@@ -17,17 +12,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
// Delete this code if you don't have a data source
|
||||
const index = await getDataSource(params);
|
||||
if (index) {
|
||||
tools.push(
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
preFilters: generateFilters(documentIds || []),
|
||||
}),
|
||||
metadata: {
|
||||
name: "data_query_engine",
|
||||
description: `A query engine for documents from your data source.`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
tools.push(createQueryEngineTool(index, { documentIds }));
|
||||
}
|
||||
|
||||
const configFile = path.join("config", "tools.json");
|
||||
|
||||
@@ -103,6 +103,7 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
const artifact = await this.generateArtifact(
|
||||
input.requirement,
|
||||
input.oldCode,
|
||||
input.sandboxFiles, // help the generated code use exact files
|
||||
);
|
||||
if (input.sandboxFiles) {
|
||||
artifact.files = input.sandboxFiles;
|
||||
@@ -117,10 +118,12 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
async generateArtifact(
|
||||
query: string,
|
||||
oldCode?: string,
|
||||
attachments?: string[],
|
||||
): Promise<CodeArtifact> {
|
||||
const userMessage = `
|
||||
${query}
|
||||
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
|
||||
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
|
||||
`;
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: CODE_GENERATION_PROMPT },
|
||||
|
||||
@@ -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,15 +1,23 @@
|
||||
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";
|
||||
import { WeatherTool, WeatherToolParams } from "./weather";
|
||||
import { WikipediaTool, WikipediaToolParams } from "./wikipedia";
|
||||
|
||||
type ToolCreator = (config: unknown) => Promise<BaseToolWithCall[]>;
|
||||
|
||||
@@ -19,12 +27,13 @@ export async function createTools(toolConfig: {
|
||||
}): Promise<BaseToolWithCall[]> {
|
||||
// add local tools from the 'tools' folder (if configured)
|
||||
const tools = await createLocalTools(toolConfig.local);
|
||||
// add tools from LlamaIndexTS (if configured)
|
||||
tools.push(...(await ToolsFactory.createTools(toolConfig.llamahub)));
|
||||
return tools;
|
||||
}
|
||||
|
||||
const toolFactory: Record<string, ToolCreator> = {
|
||||
"wikipedia.WikipediaToolSpec": async (config: unknown) => {
|
||||
return [new WikipediaTool(config as WikipediaToolParams)];
|
||||
},
|
||||
weather: async (config: unknown) => {
|
||||
return [new WeatherTool(config as WeatherToolParams)];
|
||||
},
|
||||
@@ -54,6 +63,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 +85,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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CodeInterpreter, Logs, Result } from "@e2b/code-interpreter";
|
||||
import { Logs, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import fs from "fs";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
@@ -82,7 +82,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
private apiKey?: string;
|
||||
private fileServerURLPrefix?: string;
|
||||
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
|
||||
codeInterpreter?: CodeInterpreter;
|
||||
codeInterpreter?: Sandbox;
|
||||
|
||||
constructor(params?: InterpreterToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
@@ -104,24 +104,27 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
|
||||
public async initInterpreter(input: InterpreterParameter) {
|
||||
if (!this.codeInterpreter) {
|
||||
this.codeInterpreter = await CodeInterpreter.create({
|
||||
this.codeInterpreter = await Sandbox.create({
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
}
|
||||
// upload files to sandbox
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
await this.codeInterpreter?.files.write(filePath, content);
|
||||
// upload files to sandbox when it's initialized
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
|
||||
const arrayBuffer = new Uint8Array(content).buffer;
|
||||
await this.codeInterpreter?.files.write(filePath, arrayBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
}
|
||||
|
||||
return this.codeInterpreter;
|
||||
}
|
||||
|
||||
@@ -148,7 +151,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${input.code}\n${"=".repeat(50)}`,
|
||||
);
|
||||
const interpreter = await this.initInterpreter(input);
|
||||
const exec = await interpreter.notebook.execCell(input.code);
|
||||
const exec = await interpreter.runCode(input.code);
|
||||
if (exec.error) console.error("[Code Interpreter error]", exec.error);
|
||||
const extraResult = await this.getExtraResult(exec.results[0]);
|
||||
const result: InterpreterToolOutput = {
|
||||
@@ -167,7 +170,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.codeInterpreter?.close();
|
||||
await this.codeInterpreter?.kill();
|
||||
}
|
||||
|
||||
private async getExtraResult(
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
BaseQueryEngine,
|
||||
CloudRetrieveParams,
|
||||
LlamaCloudIndex,
|
||||
MetadataFilters,
|
||||
QueryEngineTool,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { generateFilters } from "../queryFilter";
|
||||
|
||||
interface QueryEngineParams {
|
||||
documentIds?: string[];
|
||||
topK?: number;
|
||||
}
|
||||
|
||||
export function createQueryEngineTool(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
params?: QueryEngineParams,
|
||||
name?: string,
|
||||
description?: string,
|
||||
): QueryEngineTool {
|
||||
return new QueryEngineTool({
|
||||
queryEngine: createQueryEngine(index, params),
|
||||
metadata: {
|
||||
name: name || "query_engine",
|
||||
description:
|
||||
description ||
|
||||
`Use this tool to retrieve information about the text corpus from an index.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createQueryEngine(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
params?: QueryEngineParams,
|
||||
): BaseQueryEngine {
|
||||
const baseQueryParams = {
|
||||
similarityTopK:
|
||||
params?.topK ??
|
||||
(process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined),
|
||||
};
|
||||
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
return index.asQueryEngine({
|
||||
...baseQueryParams,
|
||||
retrieval_mode: "auto_routed",
|
||||
preFilters: generateFilters(
|
||||
params?.documentIds || [],
|
||||
) as CloudRetrieveParams["filters"],
|
||||
});
|
||||
}
|
||||
|
||||
return index.asQueryEngine({
|
||||
...baseQueryParams,
|
||||
preFilters: generateFilters(params?.documentIds || []) as MetadataFilters,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import type { BaseTool, ToolMetadata } from "llamaindex";
|
||||
import { default as wiki } from "wikipedia";
|
||||
|
||||
type WikipediaParameter = {
|
||||
query: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
export type WikipediaToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<WikipediaParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WikipediaParameter>> = {
|
||||
name: "wikipedia_tool",
|
||||
description: "A tool that uses a query engine to search Wikipedia.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "The query to search for",
|
||||
},
|
||||
lang: {
|
||||
type: "string",
|
||||
description: "The language to search in",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
export class WikipediaTool implements BaseTool<WikipediaParameter> {
|
||||
private readonly DEFAULT_LANG = "en";
|
||||
metadata: ToolMetadata<JSONSchemaType<WikipediaParameter>>;
|
||||
|
||||
constructor(params?: WikipediaToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async loadData(
|
||||
page: string,
|
||||
lang: string = this.DEFAULT_LANG,
|
||||
): Promise<string> {
|
||||
wiki.setLang(lang);
|
||||
const pageResult = await wiki.page(page, { autoSuggest: false });
|
||||
const content = await pageResult.content();
|
||||
return content;
|
||||
}
|
||||
|
||||
async call({
|
||||
query,
|
||||
lang = this.DEFAULT_LANG,
|
||||
}: WikipediaParameter): Promise<string> {
|
||||
const searchResult = await wiki.search(query);
|
||||
if (searchResult.results.length === 0) return "No search results.";
|
||||
return await this.loadData(searchResult.results[0].title, lang);
|
||||
}
|
||||
}
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
FILE_EXT_TO_READER,
|
||||
SimpleDirectoryReader,
|
||||
} from "llamaindex/readers/index";
|
||||
} from "@llamaindex/readers/directory";
|
||||
|
||||
export const DATA_DIR = "./data";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LlamaParseReader } from "llamaindex";
|
||||
import {
|
||||
FILE_EXT_TO_READER,
|
||||
SimpleDirectoryReader,
|
||||
} from "llamaindex/readers/index";
|
||||
} from "@llamaindex/readers/directory";
|
||||
import { LlamaParseReader } from "llamaindex";
|
||||
|
||||
export const DATA_DIR = "./data";
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class EventCallback(ABC):
|
||||
"""
|
||||
Base class for event callbacks during event streaming.
|
||||
"""
|
||||
|
||||
async def run(self, event: Any) -> Any:
|
||||
"""
|
||||
Called for each event in the stream.
|
||||
Default behavior: pass through the event unchanged.
|
||||
"""
|
||||
return event
|
||||
|
||||
async def on_complete(self, final_response: str) -> Any:
|
||||
"""
|
||||
Called when the stream is complete.
|
||||
Default behavior: return None.
|
||||
"""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def from_default(self, *args, **kwargs) -> "EventCallback":
|
||||
"""
|
||||
Create a new instance of the processor from default values.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,42 @@
|
||||
import logging
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
|
||||
from app.api.callbacks.base import EventCallback
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class LlamaCloudFileDownload(EventCallback):
|
||||
"""
|
||||
Processor for handling LlamaCloud file downloads from source nodes.
|
||||
Only work if LlamaCloud service code is available.
|
||||
"""
|
||||
|
||||
def __init__(self, background_tasks: BackgroundTasks):
|
||||
self.background_tasks = background_tasks
|
||||
|
||||
async def run(self, event: Any) -> Any:
|
||||
if hasattr(event, "to_response"):
|
||||
event_response = event.to_response()
|
||||
if event_response.get("type") == "sources" and hasattr(event, "nodes"):
|
||||
await self._process_response_nodes(event.nodes)
|
||||
return event
|
||||
|
||||
async def _process_response_nodes(self, source_nodes: List[NodeWithScore]):
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService # type: ignore
|
||||
|
||||
LLamaCloudFileService.download_files_from_nodes(
|
||||
source_nodes, self.background_tasks
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_default(
|
||||
cls, background_tasks: BackgroundTasks
|
||||
) -> "LlamaCloudFileDownload":
|
||||
return cls(background_tasks=background_tasks)
|
||||
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.api.callbacks.base import EventCallback
|
||||
from app.api.routers.models import ChatData
|
||||
from app.api.services.suggestion import NextQuestionSuggestion
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class SuggestNextQuestions(EventCallback):
|
||||
"""Processor for generating next question suggestions."""
|
||||
|
||||
def __init__(self, chat_data: ChatData):
|
||||
self.chat_data = chat_data
|
||||
self.accumulated_text = ""
|
||||
|
||||
async def on_complete(self, final_response: str) -> Any:
|
||||
if final_response == "":
|
||||
return None
|
||||
|
||||
questions = await NextQuestionSuggestion.suggest_next_questions(
|
||||
self.chat_data.messages, final_response
|
||||
)
|
||||
if questions:
|
||||
return {
|
||||
"type": "suggested_questions",
|
||||
"data": questions,
|
||||
}
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_default(cls, chat_data: ChatData) -> "SuggestNextQuestions":
|
||||
return cls(chat_data=chat_data)
|
||||
@@ -0,0 +1,66 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from llama_index.core.workflow.handler import WorkflowHandler
|
||||
|
||||
from app.api.callbacks.base import EventCallback
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class StreamHandler:
|
||||
"""
|
||||
Streams events from a workflow handler through a chain of callbacks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workflow_handler: WorkflowHandler,
|
||||
callbacks: Optional[List[EventCallback]] = None,
|
||||
):
|
||||
self.workflow_handler = workflow_handler
|
||||
self.callbacks = callbacks or []
|
||||
self.accumulated_text = ""
|
||||
|
||||
def vercel_stream(self):
|
||||
"""Create a streaming response with Vercel format."""
|
||||
from app.api.routers.vercel_response import VercelStreamResponse
|
||||
|
||||
return VercelStreamResponse(stream_handler=self)
|
||||
|
||||
async def cancel_run(self):
|
||||
"""Cancel the workflow handler."""
|
||||
await self.workflow_handler.cancel_run()
|
||||
|
||||
async def stream_events(self):
|
||||
"""Stream events through the processor chain."""
|
||||
try:
|
||||
async for event in self.workflow_handler.stream_events():
|
||||
# Process the event through each processor
|
||||
for callback in self.callbacks:
|
||||
event = await callback.run(event)
|
||||
yield event
|
||||
|
||||
# After all events are processed, call on_complete for each callback
|
||||
for callback in self.callbacks:
|
||||
result = await callback.on_complete(self.accumulated_text)
|
||||
if result:
|
||||
yield result
|
||||
|
||||
except Exception as e:
|
||||
# Make sure to cancel the workflow on error
|
||||
await self.workflow_handler.cancel_run()
|
||||
raise e
|
||||
|
||||
async def accumulate_text(self, text: str):
|
||||
"""Accumulate text from the workflow handler."""
|
||||
self.accumulated_text += text
|
||||
|
||||
@classmethod
|
||||
def from_default(
|
||||
cls,
|
||||
handler: WorkflowHandler,
|
||||
callbacks: Optional[List[EventCallback]] = None,
|
||||
) -> "StreamHandler":
|
||||
"""Create a new instance with the given workflow handler and callbacks."""
|
||||
return cls(workflow_handler=handler, callbacks=callbacks)
|
||||
@@ -1,11 +1,15 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
|
||||
|
||||
from app.api.callbacks.llamacloud import LlamaCloudFileDownload
|
||||
from app.api.callbacks.next_question import SuggestNextQuestions
|
||||
from app.api.callbacks.stream_handler import StreamHandler
|
||||
from app.api.routers.models import (
|
||||
ChatData,
|
||||
)
|
||||
from app.api.routers.vercel_response import VercelStreamResponse
|
||||
from app.engine.engine import get_chat_engine
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
|
||||
from app.engine.query_filter import generate_filters
|
||||
from app.workflows import create_workflow
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
|
||||
@@ -22,20 +26,27 @@ async def chat(
|
||||
last_message_content = data.get_last_message_content()
|
||||
messages = data.get_history_messages(include_agent_messages=True)
|
||||
|
||||
# The chat API supports passing private document filters and chat params
|
||||
# but agent workflow does not support them yet
|
||||
# ignore chat params and use all documents for now
|
||||
# TODO: generate filters based on doc_ids
|
||||
doc_ids = data.get_chat_document_ids()
|
||||
filters = generate_filters(doc_ids)
|
||||
params = data.data or {}
|
||||
engine = get_chat_engine(chat_history=messages, params=params)
|
||||
|
||||
event_handler = engine.run(input=last_message_content, streaming=True)
|
||||
return VercelStreamResponse(
|
||||
request=request,
|
||||
chat_data=data,
|
||||
event_handler=event_handler,
|
||||
events=engine.stream_events(),
|
||||
workflow = create_workflow(
|
||||
params=params,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
handler = workflow.run(
|
||||
user_msg=last_message_content,
|
||||
chat_history=messages,
|
||||
stream=True,
|
||||
)
|
||||
return StreamHandler.from_default(
|
||||
handler=handler,
|
||||
callbacks=[
|
||||
LlamaCloudFileDownload.from_default(background_tasks),
|
||||
SuggestNextQuestions.from_default(data),
|
||||
],
|
||||
).vercel_stream()
|
||||
except Exception as e:
|
||||
logger.exception("Error in chat engine", exc_info=True)
|
||||
raise HTTPException(
|
||||
|
||||
@@ -1,120 +1,99 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import AsyncGenerator, Awaitable, List
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from aiostream import stream
|
||||
from app.api.routers.models import ChatData, Message
|
||||
from app.api.services.suggestion import NextQuestionSuggestion
|
||||
from fastapi import Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from llama_index.core.agent.workflow.workflow_events import AgentStream
|
||||
from llama_index.core.workflow import StopEvent
|
||||
|
||||
from app.api.callbacks.stream_handler import StreamHandler
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class VercelStreamResponse(StreamingResponse):
|
||||
"""
|
||||
Base class to convert the response from the chat engine to the streaming format expected by Vercel
|
||||
Converts preprocessed events into Vercel-compatible streaming response format.
|
||||
"""
|
||||
|
||||
TEXT_PREFIX = "0:"
|
||||
DATA_PREFIX = "8:"
|
||||
ERROR_PREFIX = "3:"
|
||||
|
||||
def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs):
|
||||
self.request = request
|
||||
self.chat_data = chat_data
|
||||
content = self.content_generator(*args, **kwargs)
|
||||
super().__init__(content=content)
|
||||
|
||||
async def content_generator(self, event_handler, events):
|
||||
stream = self._create_stream(
|
||||
self.request, self.chat_data, event_handler, events
|
||||
)
|
||||
is_stream_started = False
|
||||
try:
|
||||
async with stream.stream() as streamer:
|
||||
async for output in streamer:
|
||||
if not is_stream_started:
|
||||
is_stream_started = True
|
||||
# Stream a blank message to start the stream
|
||||
yield self.convert_text("")
|
||||
|
||||
yield output
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Stopping workflow")
|
||||
await event_handler.cancel_run()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Unexpected error in content_generator: {str(e)}", exc_info=True
|
||||
)
|
||||
finally:
|
||||
logger.info("The stream has been stopped!")
|
||||
|
||||
def _create_stream(
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
chat_data: ChatData,
|
||||
event_handler: Awaitable,
|
||||
events: AsyncGenerator,
|
||||
verbose: bool = True,
|
||||
stream_handler: StreamHandler,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# Yield the text response
|
||||
async def _chat_response_generator():
|
||||
result = await event_handler
|
||||
final_response = ""
|
||||
self.handler = stream_handler
|
||||
super().__init__(content=self.content_generator())
|
||||
|
||||
if isinstance(result, AsyncGenerator):
|
||||
async for token in result:
|
||||
final_response += str(token.delta)
|
||||
yield self.convert_text(token.delta)
|
||||
else:
|
||||
if hasattr(result, "response"):
|
||||
content = result.response.message.content
|
||||
if content:
|
||||
for token in content:
|
||||
final_response += str(token)
|
||||
yield self.convert_text(token)
|
||||
async def content_generator(self):
|
||||
"""Generate Vercel-formatted content from preprocessed events."""
|
||||
stream_started = False
|
||||
try:
|
||||
async for event in self.handler.stream_events():
|
||||
if not stream_started:
|
||||
# Start the stream with an empty message
|
||||
stream_started = True
|
||||
yield self.convert_text("")
|
||||
|
||||
# Generate next questions if next question prompt is configured
|
||||
question_data = await self._generate_next_questions(
|
||||
chat_data.messages, final_response
|
||||
)
|
||||
if question_data:
|
||||
yield self.convert_data(question_data)
|
||||
|
||||
# TODO: stream sources
|
||||
|
||||
# Yield the events from the event handler
|
||||
async def _event_generator():
|
||||
async for event in events:
|
||||
event_response = event.to_response()
|
||||
if verbose:
|
||||
logger.debug(event_response)
|
||||
if event_response is not None:
|
||||
# Handle different types of events
|
||||
if isinstance(event, (AgentStream, StopEvent)):
|
||||
async for chunk in self._stream_text(event):
|
||||
await self.handler.accumulate_text(chunk)
|
||||
yield self.convert_text(chunk)
|
||||
elif isinstance(event, dict):
|
||||
yield self.convert_data(event)
|
||||
elif hasattr(event, "to_response"):
|
||||
event_response = event.to_response()
|
||||
yield self.convert_data(event_response)
|
||||
else:
|
||||
yield self.convert_data(event.model_dump())
|
||||
|
||||
combine = stream.merge(_chat_response_generator(), _event_generator())
|
||||
return combine
|
||||
except asyncio.CancelledError:
|
||||
logger.warning("Client cancelled the request!")
|
||||
await self.handler.cancel_run()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in stream response: {e}")
|
||||
yield self.convert_error(str(e))
|
||||
await self.handler.cancel_run()
|
||||
|
||||
async def _stream_text(
|
||||
self, event: AgentStream | StopEvent
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Accept stream text from either AgentStream or StopEvent with string or AsyncGenerator result
|
||||
"""
|
||||
if isinstance(event, AgentStream):
|
||||
yield self.convert_text(event.delta)
|
||||
elif isinstance(event, StopEvent):
|
||||
if isinstance(event.result, str):
|
||||
yield event.result
|
||||
elif isinstance(event.result, AsyncGenerator):
|
||||
async for chunk in event.result:
|
||||
if isinstance(chunk, str):
|
||||
yield chunk
|
||||
elif hasattr(chunk, "delta"):
|
||||
yield chunk.delta
|
||||
|
||||
@classmethod
|
||||
def convert_text(cls, token: str):
|
||||
def convert_text(cls, token: str) -> str:
|
||||
"""Convert text event to Vercel format."""
|
||||
# Escape newlines and double quotes to avoid breaking the stream
|
||||
token = json.dumps(token)
|
||||
return f"{cls.TEXT_PREFIX}{token}\n"
|
||||
|
||||
@classmethod
|
||||
def convert_data(cls, data: dict):
|
||||
def convert_data(cls, data: dict) -> str:
|
||||
"""Convert data event to Vercel format."""
|
||||
data_str = json.dumps(data)
|
||||
return f"{cls.DATA_PREFIX}[{data_str}]\n"
|
||||
|
||||
@staticmethod
|
||||
async def _generate_next_questions(chat_history: List[Message], response: str):
|
||||
questions = await NextQuestionSuggestion.suggest_next_questions(
|
||||
chat_history, response
|
||||
)
|
||||
if questions:
|
||||
return {
|
||||
"type": "suggested_questions",
|
||||
"data": questions,
|
||||
}
|
||||
return None
|
||||
@classmethod
|
||||
def convert_error(cls, error: str) -> str:
|
||||
"""Convert error event to Vercel format."""
|
||||
error_str = json.dumps(error)
|
||||
return f"{cls.ERROR_PREFIX}{error_str}\n"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
from llama_index.core.workflow import Event
|
||||
|
||||
from app.api.routers.models import SourceNodes
|
||||
|
||||
|
||||
class AgentRunEventType(Enum):
|
||||
TEXT = "text"
|
||||
PROGRESS = "progress"
|
||||
|
||||
|
||||
class AgentRunEvent(Event):
|
||||
name: str
|
||||
msg: str
|
||||
event_type: AgentRunEventType = AgentRunEventType.TEXT
|
||||
data: Optional[dict] = None
|
||||
|
||||
def to_response(self) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"agent": self.name,
|
||||
"type": self.event_type.value,
|
||||
"text": self.msg,
|
||||
"data": self.data,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class SourceNodesEvent(Event):
|
||||
nodes: List[NodeWithScore]
|
||||
|
||||
def to_response(self):
|
||||
return {
|
||||
"type": "sources",
|
||||
"data": {
|
||||
"nodes": [
|
||||
SourceNodes.from_source_node(node).model_dump()
|
||||
for node in self.nodes
|
||||
]
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from app.workflows.events import AgentRunEvent
|
||||
from app.workflows.tools import ToolCallResponse, call_tools, chat_with_tools
|
||||
from llama_index.core.base.llms.types import ChatMessage
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools.types import BaseTool
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: list[ChatMessage]
|
||||
|
||||
|
||||
class ToolCallEvent(Event):
|
||||
input: ToolCallResponse
|
||||
|
||||
|
||||
class FunctionCallingAgent(Workflow):
|
||||
"""
|
||||
A simple workflow to request LLM with tools independently.
|
||||
You can share the previous chat history to provide the context for the LLM.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
llm: FunctionCallingLLM | None = None,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
tools: List[BaseTool] | None = None,
|
||||
system_prompt: str | None = None,
|
||||
verbose: bool = False,
|
||||
timeout: float = 360.0,
|
||||
name: str,
|
||||
write_events: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, verbose=verbose, timeout=timeout, **kwargs) # type: ignore
|
||||
self.tools = tools or []
|
||||
self.name = name
|
||||
self.write_events = write_events
|
||||
|
||||
if llm is None:
|
||||
llm = Settings.llm
|
||||
self.llm = llm
|
||||
if not self.llm.metadata.is_function_calling_model:
|
||||
raise ValueError("The provided LLM must support function calling.")
|
||||
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
self.memory = ChatMemoryBuffer.from_defaults(
|
||||
llm=self.llm, chat_history=chat_history
|
||||
)
|
||||
self.sources = [] # type: ignore
|
||||
|
||||
@step()
|
||||
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
# clear sources
|
||||
self.sources = []
|
||||
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
|
||||
# set system prompt
|
||||
if self.system_prompt is not None:
|
||||
system_msg = ChatMessage(role="system", content=self.system_prompt)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
# get user input
|
||||
user_input = ev.input
|
||||
user_msg = ChatMessage(role="user", content=user_input)
|
||||
self.memory.put(user_msg)
|
||||
|
||||
if self.write_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(name=self.name, msg=f"Start to work on: {user_input}")
|
||||
)
|
||||
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def handle_llm_input(
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ToolCallEvent | StopEvent:
|
||||
chat_history = ev.input
|
||||
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
self.tools,
|
||||
chat_history,
|
||||
)
|
||||
is_tool_call = isinstance(response, ToolCallResponse)
|
||||
if not is_tool_call:
|
||||
if ctx.data["streaming"]:
|
||||
return StopEvent(result=response)
|
||||
else:
|
||||
full_response = ""
|
||||
async for chunk in response.generator:
|
||||
full_response += chunk.message.content
|
||||
return StopEvent(result=full_response)
|
||||
return ToolCallEvent(input=response)
|
||||
|
||||
@step()
|
||||
async def handle_tool_calls(self, ctx: Context, ev: ToolCallEvent) -> InputEvent:
|
||||
tool_calls = ev.input.tool_calls
|
||||
tool_call_message = ev.input.tool_call_message
|
||||
self.memory.put(tool_call_message)
|
||||
tool_messages = await call_tools(self.name, self.tools, ctx, tool_calls)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
@@ -0,0 +1,230 @@
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, AsyncGenerator, Callable, Optional
|
||||
|
||||
from llama_index.core.base.llms.types import ChatMessage, ChatResponse, MessageRole
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.tools import (
|
||||
BaseTool,
|
||||
FunctionTool,
|
||||
ToolOutput,
|
||||
ToolSelection,
|
||||
)
|
||||
from llama_index.core.workflow import Context
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.workflows.events import AgentRunEvent, AgentRunEventType
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class ContextAwareTool(FunctionTool, ABC):
|
||||
@abstractmethod
|
||||
async def acall(self, ctx: Context, input: Any) -> ToolOutput: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class ChatWithToolsResponse(BaseModel):
|
||||
"""
|
||||
A tool call response from chat_with_tools.
|
||||
"""
|
||||
|
||||
tool_calls: Optional[list[ToolSelection]]
|
||||
tool_call_message: Optional[ChatMessage]
|
||||
generator: Optional[AsyncGenerator[ChatResponse | None, None]]
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
def is_calling_different_tools(self) -> bool:
|
||||
tool_names = {tool_call.tool_name for tool_call in self.tool_calls}
|
||||
return len(tool_names) > 1
|
||||
|
||||
def has_tool_calls(self) -> bool:
|
||||
return self.tool_calls is not None and len(self.tool_calls) > 0
|
||||
|
||||
def tool_name(self) -> str:
|
||||
assert self.has_tool_calls()
|
||||
assert not self.is_calling_different_tools()
|
||||
return self.tool_calls[0].tool_name
|
||||
|
||||
async def full_response(self) -> str:
|
||||
assert self.generator is not None
|
||||
full_response = ""
|
||||
async for chunk in self.generator:
|
||||
content = chunk.message.content
|
||||
if content:
|
||||
full_response += content
|
||||
return full_response
|
||||
|
||||
|
||||
async def chat_with_tools( # type: ignore
|
||||
llm: FunctionCallingLLM,
|
||||
tools: list[BaseTool],
|
||||
chat_history: list[ChatMessage],
|
||||
) -> ChatWithToolsResponse:
|
||||
"""
|
||||
Request LLM to call tools or not.
|
||||
This function doesn't change the memory.
|
||||
"""
|
||||
generator = _tool_call_generator(llm, tools, chat_history)
|
||||
is_tool_call = await generator.__anext__()
|
||||
if is_tool_call:
|
||||
# Last chunk is the full response
|
||||
# Wait for the last chunk
|
||||
full_response = None
|
||||
async for chunk in generator:
|
||||
full_response = chunk
|
||||
assert isinstance(full_response, ChatResponse)
|
||||
return ChatWithToolsResponse(
|
||||
tool_calls=llm.get_tool_calls_from_response(full_response),
|
||||
tool_call_message=full_response.message,
|
||||
generator=None,
|
||||
)
|
||||
else:
|
||||
return ChatWithToolsResponse(
|
||||
tool_calls=None,
|
||||
tool_call_message=None,
|
||||
generator=generator,
|
||||
)
|
||||
|
||||
|
||||
async def call_tools(
|
||||
ctx: Context,
|
||||
agent_name: str,
|
||||
tools: list[BaseTool],
|
||||
tool_calls: list[ToolSelection],
|
||||
emit_agent_events: bool = True,
|
||||
) -> list[ChatMessage]:
|
||||
if len(tool_calls) == 0:
|
||||
return []
|
||||
|
||||
tools_by_name = {tool.metadata.get_name(): tool for tool in tools}
|
||||
if len(tool_calls) == 1:
|
||||
return [
|
||||
await call_tool(
|
||||
ctx,
|
||||
tools_by_name[tool_calls[0].tool_name],
|
||||
tool_calls[0],
|
||||
lambda msg: ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=msg,
|
||||
)
|
||||
),
|
||||
)
|
||||
]
|
||||
# Multiple tool calls, show progress
|
||||
tool_msgs: list[ChatMessage] = []
|
||||
|
||||
progress_id = str(uuid.uuid4())
|
||||
total_steps = len(tool_calls)
|
||||
if emit_agent_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=f"Making {total_steps} tool calls",
|
||||
)
|
||||
)
|
||||
for i, tool_call in enumerate(tool_calls):
|
||||
tool = tools_by_name.get(tool_call.tool_name)
|
||||
if not tool:
|
||||
tool_msgs.append(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=f"Tool {tool_call.tool_name} does not exist",
|
||||
)
|
||||
)
|
||||
continue
|
||||
tool_msg = await call_tool(
|
||||
ctx,
|
||||
tool,
|
||||
tool_call,
|
||||
event_emitter=lambda msg: ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=msg,
|
||||
event_type=AgentRunEventType.PROGRESS,
|
||||
data={
|
||||
"id": progress_id,
|
||||
"total": total_steps,
|
||||
"current": i,
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
tool_msgs.append(tool_msg)
|
||||
return tool_msgs
|
||||
|
||||
|
||||
async def call_tool(
|
||||
ctx: Context,
|
||||
tool: BaseTool,
|
||||
tool_call: ToolSelection,
|
||||
event_emitter: Optional[Callable[[str], None]],
|
||||
) -> ChatMessage:
|
||||
if event_emitter:
|
||||
event_emitter(
|
||||
f"Calling tool {tool_call.tool_name}, {str(tool_call.tool_kwargs)}"
|
||||
)
|
||||
try:
|
||||
if isinstance(tool, ContextAwareTool):
|
||||
if ctx is None:
|
||||
raise ValueError("Context is required for context aware tool")
|
||||
# inject context for calling an context aware tool
|
||||
response = await tool.acall(ctx=ctx, **tool_call.tool_kwargs)
|
||||
else:
|
||||
response = await tool.acall(**tool_call.tool_kwargs) # type: ignore
|
||||
return ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=str(response.raw_output),
|
||||
additional_kwargs={
|
||||
"tool_call_id": tool_call.tool_id,
|
||||
"name": tool.metadata.get_name(),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Got error in tool {tool_call.tool_name}: {str(e)}")
|
||||
if event_emitter:
|
||||
event_emitter(f"Got error in tool {tool_call.tool_name}: {str(e)}")
|
||||
return ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=f"Error: {str(e)}",
|
||||
additional_kwargs={
|
||||
"tool_call_id": tool_call.tool_id,
|
||||
"name": tool.metadata.get_name(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _tool_call_generator(
|
||||
llm: FunctionCallingLLM,
|
||||
tools: list[BaseTool],
|
||||
chat_history: list[ChatMessage],
|
||||
) -> AsyncGenerator[ChatResponse | bool, None]:
|
||||
response_stream = await llm.astream_chat_with_tools(
|
||||
tools,
|
||||
chat_history=chat_history,
|
||||
allow_parallel_tool_calls=False,
|
||||
)
|
||||
|
||||
full_response = None
|
||||
yielded_indicator = False
|
||||
async for chunk in response_stream:
|
||||
if "tool_calls" not in chunk.message.additional_kwargs:
|
||||
# Yield a boolean to indicate whether the response is a tool call
|
||||
if not yielded_indicator:
|
||||
yield False
|
||||
yielded_indicator = True
|
||||
|
||||
# if not a tool call, yield the chunks!
|
||||
yield chunk # type: ignore
|
||||
elif not yielded_indicator:
|
||||
# Yield the indicator for a tool call
|
||||
yield True
|
||||
yielded_indicator = True
|
||||
|
||||
full_response = chunk
|
||||
|
||||
if full_response:
|
||||
yield full_response # type: ignore
|
||||
@@ -1,36 +1,47 @@
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, streamToResponse } from "ai";
|
||||
import { LlamaIndexAdapter, Message } 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);
|
||||
|
||||
const streamResponse = LlamaIndexAdapter.toDataStreamResponse(stream, {
|
||||
data: dataStream,
|
||||
});
|
||||
if (streamResponse.body) {
|
||||
const reader = streamResponse.body.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.write(value);
|
||||
}
|
||||
}
|
||||
} 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 { LlamaIndexAdapter, 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,
|
||||
});
|
||||
const { stream, dataStream } =
|
||||
await createStreamFromWorkflowContext(context);
|
||||
return LlamaIndexAdapter.toDataStreamResponse(stream, {
|
||||
data: dataStream,
|
||||
});
|
||||
return new StreamingTextResponse(stream, {}, agentStreamData);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return NextResponse.json(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user