mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 15:03:35 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4b4338f54 | |||
| b4e41aa526 | |||
| 860b9d46d4 | |||
| f73d46bf10 | |||
| eec237c5fe | |||
| 5450096e96 | |||
| 163492f189 | |||
| a84743c576 | |||
| fc5e56efa5 | |||
| a7a6592441 | |||
| af21426952 | |||
| 9077cae2f5 |
@@ -1,5 +1,38 @@
|
||||
# create-llama
|
||||
|
||||
## 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
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ export async function createApp({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function createApp({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
useCase,
|
||||
};
|
||||
|
||||
// Install backend
|
||||
|
||||
@@ -1,60 +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 appPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
let cwd: string;
|
||||
|
||||
// Create extractor app
|
||||
test.beforeAll(async () => {
|
||||
cwd = await createTestDir();
|
||||
appPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "extractor",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: appPort,
|
||||
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:${appPort}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 2000 * 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -18,10 +18,10 @@ const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateAgents = ["financial_report", "blog", "form_filling"];
|
||||
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.",
|
||||
@@ -46,7 +46,7 @@ for (const agents of templateAgents) {
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
agents,
|
||||
useCase,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
@@ -71,8 +71,8 @@ for (const agents of templateAgents) {
|
||||
}) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
agents === "financial_report" ||
|
||||
agents === "form_filling" ||
|
||||
useCase === "financial_report" ||
|
||||
useCase === "form_filling" ||
|
||||
templateFramework === "express",
|
||||
"Skip chat tests for financial report and form filling.",
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -33,7 +33,7 @@ export type RunCreateLlamaOptions = {
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
agents?: string;
|
||||
useCase?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
@@ -51,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(
|
||||
@@ -88,7 +88,7 @@ export async function runCreateLlama({
|
||||
...dataSourceArgs,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--use-pnpm",
|
||||
"--use-npm",
|
||||
"--port",
|
||||
port,
|
||||
"--post-install-action",
|
||||
@@ -113,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(" ");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -470,11 +470,11 @@ const getSystemPromptEnv = (
|
||||
});
|
||||
|
||||
const systemPrompt =
|
||||
"'" +
|
||||
'"' +
|
||||
DEFAULT_SYSTEM_PROMPT +
|
||||
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
|
||||
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
|
||||
"'";
|
||||
'"';
|
||||
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_PROMPT",
|
||||
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
|
||||
+36
-30
@@ -380,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);
|
||||
}
|
||||
@@ -472,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(
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { SpawnOptions, spawn } from "child_process";
|
||||
import { TemplateFramework } from "./types";
|
||||
import { TemplateFramework, TemplateType } from "./types";
|
||||
|
||||
const createProcess = (
|
||||
command: string,
|
||||
@@ -58,17 +58,17 @@ export function runTSApp(appPath: string, port: number) {
|
||||
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
template: string,
|
||||
template: TemplateType,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort =
|
||||
framework === "nextjs" || template === "extractor" ? 3000 : 8000;
|
||||
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
|
||||
|
||||
const appRunner =
|
||||
template === "extractor"
|
||||
template === "reflex"
|
||||
? runReflexApp
|
||||
: framework === "fastapi"
|
||||
? runFastAPIApp
|
||||
|
||||
+2
-2
@@ -124,7 +124,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.11b38",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -155,7 +155,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.11b38",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
|
||||
+12
-4
@@ -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;
|
||||
@@ -99,5 +107,5 @@ export interface InstallTemplateArgs {
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
agents?: TemplateAgents;
|
||||
useCase?: TemplateUseCase;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const installTSTemplate = async ({
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
agents,
|
||||
useCase,
|
||||
}: InstallTemplateArgs & { backend: boolean }) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
@@ -131,16 +131,16 @@ 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 useCasePath = path.join(compPath, "agents", "typescript", agents);
|
||||
const agentsCodePath = path.join(useCasePath, "workflow");
|
||||
// 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 agent codes
|
||||
// Copy use case codes
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: agentsCodePath,
|
||||
cwd: useCaseCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
@@ -153,7 +153,7 @@ export const installTSTemplate = async ({
|
||||
} 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);
|
||||
|
||||
@@ -202,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()
|
||||
@@ -215,7 +215,7 @@ const options = program.opts();
|
||||
|
||||
if (
|
||||
process.argv.includes("--no-llama-parse") ||
|
||||
options.template === "extractor"
|
||||
options.template === "reflex"
|
||||
) {
|
||||
options.useLlamaParse = false;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.3.22",
|
||||
"version": "0.3.27",
|
||||
"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",
|
||||
|
||||
+64
-31
@@ -2,7 +2,7 @@ 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";
|
||||
@@ -33,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",
|
||||
@@ -95,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) {
|
||||
@@ -171,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) {
|
||||
@@ -222,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];
|
||||
}
|
||||
|
||||
@@ -354,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(
|
||||
|
||||
+99
-36
@@ -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,7 +95,11 @@ export const askSimpleQuestions = async (
|
||||
let llamaCloudKey = args.llamaCloudKey;
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (appType !== "extractor") {
|
||||
if (
|
||||
appType !== "extractor" &&
|
||||
appType !== "contract_review" &&
|
||||
appType !== "deep_research"
|
||||
) {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
@@ -65,34 +113,34 @@ export const askSimpleQuestions = async (
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
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: "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: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
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;
|
||||
}
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
|
||||
const results = await convertAnswers(args, {
|
||||
@@ -124,7 +172,7 @@ const convertAnswers = async (
|
||||
AppType,
|
||||
Pick<
|
||||
QuestionResults,
|
||||
"template" | "tools" | "frontend" | "dataSources" | "agents"
|
||||
"template" | "tools" | "frontend" | "dataSources" | "useCase"
|
||||
> & {
|
||||
modelConfig?: ModelConfig;
|
||||
}
|
||||
@@ -151,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,
|
||||
@@ -159,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,158 @@
|
||||
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="Questions to research if continuing research. Maximum 3 questions. Set to null or empty if writing a report.",
|
||||
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,
|
||||
) -> AnalysisDecision:
|
||||
analyze_prompt = PromptTemplate(
|
||||
"""
|
||||
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.
|
||||
<User request>
|
||||
{user_request}
|
||||
</User request>
|
||||
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
<Conversation context>
|
||||
{conversation_context}
|
||||
</Conversation context>
|
||||
"""
|
||||
)
|
||||
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=analyze_prompt,
|
||||
user_request=user_request,
|
||||
context_str=context_str,
|
||||
conversation_context=conversation_context,
|
||||
)
|
||||
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,309 @@
|
||||
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(
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
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,
|
||||
chat_history=chat_history,
|
||||
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,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
stream: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.index = index
|
||||
self.context_nodes = []
|
||||
self.stream = stream
|
||||
self.chat_history = chat_history
|
||||
self.memory = SimpleComposableMemory.from_defaults(
|
||||
primary_memory=ChatMemoryBuffer.from_defaults(
|
||||
chat_history=chat_history,
|
||||
),
|
||||
)
|
||||
|
||||
@step
|
||||
def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent:
|
||||
"""
|
||||
Initiate the workflow: memory, tools, agent
|
||||
"""
|
||||
self.user_request = ev.get("input")
|
||||
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(
|
||||
context_nodes=self.context_nodes,
|
||||
)
|
||||
|
||||
@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",
|
||||
},
|
||||
)
|
||||
)
|
||||
res = await plan_research(
|
||||
memory=self.memory,
|
||||
context_nodes=self.context_nodes,
|
||||
user_request=self.user_request,
|
||||
)
|
||||
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":
|
||||
self.memory.put(
|
||||
message=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="No more idea to analyze. We should report the answers.",
|
||||
)
|
||||
)
|
||||
ctx.send_event(ReportEvent())
|
||||
else:
|
||||
await ctx.set("n_questions", len(res.research_questions))
|
||||
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("n_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("n_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
|
||||
"""
|
||||
logger.info("Writing the report")
|
||||
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()
|
||||
+6
-6
@@ -108,13 +108,13 @@ class FinancialReportWorkflow(Workflow):
|
||||
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 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"
|
||||
assert document_generator_tool is not None, (
|
||||
"Document generator tool is required"
|
||||
)
|
||||
self.tools = [
|
||||
self.query_engine_tool,
|
||||
self.code_interpreter_tool,
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,26 +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);
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -150,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 = {
|
||||
@@ -169,7 +170,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.codeInterpreter?.close();
|
||||
await this.codeInterpreter?.kill();
|
||||
}
|
||||
|
||||
private async getExtraResult(
|
||||
|
||||
@@ -38,6 +38,7 @@ async def chat(
|
||||
return VercelStreamResponse(
|
||||
request=request,
|
||||
chat_data=data,
|
||||
background_tasks=background_tasks,
|
||||
event_handler=event_handler,
|
||||
events=workflow.stream_events(),
|
||||
)
|
||||
|
||||
@@ -4,10 +4,12 @@ import logging
|
||||
from typing import AsyncGenerator, Awaitable, List
|
||||
|
||||
from aiostream import stream
|
||||
from fastapi import BackgroundTasks, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
|
||||
from app.api.routers.models import ChatData, Message
|
||||
from app.api.services.suggestion import NextQuestionSuggestion
|
||||
from fastapi import Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
@@ -21,9 +23,17 @@ class VercelStreamResponse(StreamingResponse):
|
||||
DATA_PREFIX = "8:"
|
||||
ERROR_PREFIX = "3:"
|
||||
|
||||
def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
chat_data: ChatData,
|
||||
background_tasks: BackgroundTasks,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.request = request
|
||||
self.chat_data = chat_data
|
||||
self.background_tasks = background_tasks
|
||||
content = self.content_generator(*args, **kwargs)
|
||||
super().__init__(content=content)
|
||||
|
||||
@@ -78,6 +88,9 @@ class VercelStreamResponse(StreamingResponse):
|
||||
for token in content:
|
||||
final_response += str(token)
|
||||
yield self.convert_text(token)
|
||||
else:
|
||||
final_response += str(result)
|
||||
yield self.convert_text(result)
|
||||
|
||||
# Generate next questions if next question prompt is configured
|
||||
question_data = await self._generate_next_questions(
|
||||
@@ -86,8 +99,6 @@ class VercelStreamResponse(StreamingResponse):
|
||||
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:
|
||||
@@ -96,10 +107,30 @@ class VercelStreamResponse(StreamingResponse):
|
||||
logger.debug(event_response)
|
||||
if event_response is not None:
|
||||
yield self.convert_data(event_response)
|
||||
if event_response.get("type") == "sources":
|
||||
self._process_response_nodes(event.nodes, self.background_tasks)
|
||||
|
||||
combine = stream.merge(_chat_response_generator(), _event_generator())
|
||||
return combine
|
||||
|
||||
@staticmethod
|
||||
def _process_response_nodes(
|
||||
source_nodes: List[NodeWithScore],
|
||||
background_tasks: BackgroundTasks,
|
||||
):
|
||||
try:
|
||||
# Start background tasks to download documents from LlamaCloud if needed
|
||||
from app.engine.service import LLamaCloudFileService # type: ignore
|
||||
|
||||
LLamaCloudFileService.download_files_from_nodes(
|
||||
source_nodes, background_tasks
|
||||
)
|
||||
except ImportError:
|
||||
logger.debug(
|
||||
"LlamaCloud is not configured. Skipping post processing of nodes"
|
||||
)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def convert_text(cls, token: str):
|
||||
# Escape newlines and double quotes to avoid breaking the stream
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
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"
|
||||
@@ -25,3 +28,18 @@ class AgentRunEvent(Event):
|
||||
"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,59 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Reflex](https://reflex.dev/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) featuring automated contract review and compliance analysis use case.
|
||||
|
||||
## 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 example document in the `./data` directory:
|
||||
|
||||
```shell
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
Third, start app with `reflex` command:
|
||||
|
||||
```shell
|
||||
poetry run reflex run
|
||||
```
|
||||
|
||||
To deploy the application, refer to the Reflex deployment guide: https://reflex.dev/docs/hosting/deploy-quick-start/
|
||||
|
||||
### UI
|
||||
|
||||
The application provides an interactive web interface accessible at http://localhost:3000 for testing the contract review workflow.
|
||||
|
||||
To get started:
|
||||
|
||||
1. Upload a contract document:
|
||||
|
||||
- Use the provided [example_vendor_agreement.md](./example_vendor_agreement.md) for testing
|
||||
- Or upload your own document (supported formats: PDF, TXT, Markdown, DOCX)
|
||||
|
||||
2. Review Process:
|
||||
- The system will automatically analyze your document against compliance guidelines
|
||||
- By default, it uses [GDPR](./data/gdpr.pdf) as the compliance benchmark
|
||||
- Custom guidelines can be used by adding your policy documents to the `./data` directory and running `poetry run generate` to update the embeddings
|
||||
|
||||
The interface will display the analysis results for the compliance of the contract document.
|
||||
|
||||
### Development
|
||||
|
||||
You can start editing the backend workflow by modifying the [`ContractReviewWorkflow`](./app/services/contract_reviewer.py).
|
||||
|
||||
For UI, you can start looking at the [`AppState`](./app/ui/states/app.py) code and navigating to the appropriate components.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,47 @@
|
||||
DATA_DIR = "data"
|
||||
UPLOADED_DIR = "output/uploaded"
|
||||
|
||||
# Workflow prompts
|
||||
CONTRACT_EXTRACT_PROMPT = """\
|
||||
You are given contract data below. \
|
||||
Please extract out relevant information from the contract into the defined schema - the schema is defined as a function call.\
|
||||
|
||||
{contract_data}
|
||||
"""
|
||||
|
||||
CONTRACT_MATCH_PROMPT = """\
|
||||
Given the following contract clause and the corresponding relevant guideline text, evaluate the compliance \
|
||||
and provide a JSON object that matches the ClauseComplianceCheck schema.
|
||||
|
||||
**Contract Clause:**
|
||||
{clause_text}
|
||||
|
||||
**Matched Guideline Text(s):**
|
||||
{guideline_text}
|
||||
"""
|
||||
|
||||
|
||||
COMPLIANCE_REPORT_SYSTEM_PROMPT = """\
|
||||
You are a compliance reporting assistant. Your task is to generate a final compliance report \
|
||||
based on the results of clause compliance checks against \
|
||||
a given set of guidelines.
|
||||
|
||||
Analyze the provided compliance results and produce a structured report according to the specified schema.
|
||||
Ensure that if there are no noncompliant clauses, the report clearly indicates full compliance.
|
||||
"""
|
||||
|
||||
COMPLIANCE_REPORT_USER_PROMPT = """\
|
||||
A set of clauses within a contract were checked against GDPR compliance guidelines for the following vendor: {vendor_name}.
|
||||
The set of noncompliant clauses are given below.
|
||||
|
||||
Each section includes:
|
||||
- **Clause:** The exact text of the contract clause.
|
||||
- **Guideline:** The relevant GDPR guideline text.
|
||||
- **Compliance Status:** Should be `False` for noncompliant clauses.
|
||||
- **Notes:** Additional information or explanations.
|
||||
|
||||
{compliance_results}
|
||||
|
||||
Based on the above compliance results, generate a final compliance report following the `ComplianceReport` schema below.
|
||||
If there are no noncompliant clauses, the report should indicate that the contract is fully compliant.
|
||||
"""
|
||||
@@ -0,0 +1,85 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ContractClause(BaseModel):
|
||||
clause_text: str = Field(..., description="The exact text of the clause.")
|
||||
mentions_data_processing: bool = Field(
|
||||
False,
|
||||
description="True if the clause involves personal data collection or usage.",
|
||||
)
|
||||
mentions_data_transfer: bool = Field(
|
||||
False,
|
||||
description="True if the clause involves transferring personal data, especially to third parties or across borders.",
|
||||
)
|
||||
requires_consent: bool = Field(
|
||||
False,
|
||||
description="True if the clause explicitly states that user consent is needed for data activities.",
|
||||
)
|
||||
specifies_purpose: bool = Field(
|
||||
False,
|
||||
description="True if the clause specifies a clear purpose for data handling or transfer.",
|
||||
)
|
||||
mentions_safeguards: bool = Field(
|
||||
False,
|
||||
description="True if the clause mentions security measures or other safeguards for data.",
|
||||
)
|
||||
|
||||
|
||||
class ContractExtraction(BaseModel):
|
||||
vendor_name: Optional[str] = Field(
|
||||
None, description="The vendor's name if identifiable."
|
||||
)
|
||||
effective_date: Optional[str] = Field(
|
||||
None, description="The effective date of the agreement, if available."
|
||||
)
|
||||
governing_law: Optional[str] = Field(
|
||||
None, description="The governing law of the contract, if stated."
|
||||
)
|
||||
clauses: List[ContractClause] = Field(
|
||||
..., description="List of extracted clauses and their relevant indicators."
|
||||
)
|
||||
|
||||
|
||||
class GuidelineMatch(BaseModel):
|
||||
guideline_text: str = Field(
|
||||
...,
|
||||
description="The single most relevant guideline excerpt related to this clause.",
|
||||
)
|
||||
similarity_score: float = Field(
|
||||
...,
|
||||
description="Similarity score indicating how closely the guideline matches the clause, e.g., between 0 and 1.",
|
||||
)
|
||||
relevance_explanation: Optional[str] = Field(
|
||||
None, description="Brief explanation of why this guideline is relevant."
|
||||
)
|
||||
|
||||
|
||||
class ClauseComplianceCheck(BaseModel):
|
||||
clause_text: str = Field(
|
||||
..., description="The exact text of the clause from the contract."
|
||||
)
|
||||
matched_guideline: Optional[GuidelineMatch] = Field(
|
||||
None, description="The most relevant guideline extracted via vector retrieval."
|
||||
)
|
||||
compliant: bool = Field(
|
||||
...,
|
||||
description="Indicates whether the clause is considered compliant with the referenced guideline.",
|
||||
)
|
||||
notes: Optional[str] = Field(
|
||||
None, description="Additional commentary or recommendations."
|
||||
)
|
||||
|
||||
|
||||
class ComplianceReport(BaseModel):
|
||||
vendor_name: Optional[str] = Field(
|
||||
None, description="The vendor's name if identified from the contract."
|
||||
)
|
||||
overall_compliant: bool = Field(
|
||||
..., description="Indicates if the contract is considered overall compliant."
|
||||
)
|
||||
summary_notes: str = Field(
|
||||
...,
|
||||
description="Always give a general summary or recommendations for achieving full compliance.",
|
||||
)
|
||||
@@ -0,0 +1,361 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from llama_index.core import SimpleDirectoryReader
|
||||
from llama_index.core.llms import LLM
|
||||
from llama_index.core.prompts import ChatPromptTemplate
|
||||
from llama_index.core.retrievers import BaseRetriever
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
from app.config import (
|
||||
COMPLIANCE_REPORT_SYSTEM_PROMPT,
|
||||
COMPLIANCE_REPORT_USER_PROMPT,
|
||||
CONTRACT_EXTRACT_PROMPT,
|
||||
CONTRACT_MATCH_PROMPT,
|
||||
)
|
||||
from app.engine.index import get_index
|
||||
from app.models import (
|
||||
ClauseComplianceCheck,
|
||||
ComplianceReport,
|
||||
ContractClause,
|
||||
ContractExtraction,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_workflow():
|
||||
index = get_index()
|
||||
if index is None:
|
||||
raise RuntimeError(
|
||||
"Index not found! Please run `poetry run generate` to populate an index first."
|
||||
)
|
||||
return ContractReviewWorkflow(
|
||||
guideline_retriever=index.as_retriever(),
|
||||
llm=Settings.llm,
|
||||
verbose=True,
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
|
||||
class Step(Enum):
|
||||
PARSE_CONTRACT = "parse_contract"
|
||||
ANALYZE_CLAUSES = "analyze_clauses"
|
||||
HANDLE_CLAUSE = "handle_clause"
|
||||
GENERATE_REPORT = "generate_report"
|
||||
|
||||
|
||||
class ContractExtractionEvent(Event):
|
||||
contract_extraction: ContractExtraction
|
||||
|
||||
|
||||
class MatchGuidelineEvent(Event):
|
||||
request_id: str
|
||||
clause: ContractClause
|
||||
vendor_name: str
|
||||
|
||||
|
||||
class MatchGuidelineResultEvent(Event):
|
||||
result: ClauseComplianceCheck
|
||||
|
||||
|
||||
class GenerateReportEvent(Event):
|
||||
match_results: List[ClauseComplianceCheck]
|
||||
|
||||
|
||||
class LogEvent(Event):
|
||||
msg: str
|
||||
step: Step
|
||||
data: dict = {}
|
||||
is_step_completed: bool = False
|
||||
|
||||
|
||||
class ContractReviewWorkflow(Workflow):
|
||||
"""Contract review workflow."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
guideline_retriever: BaseRetriever,
|
||||
llm: LLM | None = None,
|
||||
similarity_top_k: int = 20,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.guideline_retriever = guideline_retriever
|
||||
|
||||
self.llm = llm or Settings.llm
|
||||
self.similarity_top_k = similarity_top_k
|
||||
|
||||
# if not exists, create
|
||||
out_path = Path("output") / "workflow_output"
|
||||
if not out_path.exists():
|
||||
out_path.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(str(out_path), 0o0777)
|
||||
self.output_dir = out_path
|
||||
|
||||
@step
|
||||
async def parse_contract(
|
||||
self, ctx: Context, ev: StartEvent
|
||||
) -> ContractExtractionEvent:
|
||||
"""Parse the contract."""
|
||||
uploaded_contract_path = Path(ev.contract_path)
|
||||
contract_file_name = uploaded_contract_path.name
|
||||
# Set contract file name in context
|
||||
await ctx.set("contract_file_name", contract_file_name)
|
||||
|
||||
# Parse and read the contract to documents
|
||||
docs = SimpleDirectoryReader(
|
||||
input_files=[str(uploaded_contract_path)]
|
||||
).load_data()
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg=f"Loaded document: {contract_file_name}",
|
||||
step=Step.PARSE_CONTRACT,
|
||||
data={
|
||||
"saved_path": str(uploaded_contract_path),
|
||||
"parsed_data": None,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Parse the contract into a structured model
|
||||
# See the ContractExtraction model for information we want to extract
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg="Extracting information from the document",
|
||||
step=Step.PARSE_CONTRACT,
|
||||
data={
|
||||
"saved_path": str(uploaded_contract_path),
|
||||
"parsed_data": None,
|
||||
},
|
||||
)
|
||||
)
|
||||
prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_EXTRACT_PROMPT)])
|
||||
contract_extraction = await self.llm.astructured_predict(
|
||||
ContractExtraction,
|
||||
prompt,
|
||||
contract_data="\n".join(
|
||||
[d.get_content(metadata_mode="all") for d in docs] # type: ignore
|
||||
),
|
||||
)
|
||||
if not isinstance(contract_extraction, ContractExtraction):
|
||||
raise ValueError(f"Invalid extraction from contract: {contract_extraction}")
|
||||
|
||||
# save output template to file
|
||||
contract_extraction_path = Path(f"{self.output_dir}/{contract_file_name}.json")
|
||||
with open(contract_extraction_path, "w") as fp:
|
||||
fp.write(contract_extraction.model_dump_json())
|
||||
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg="Extracted successfully",
|
||||
step=Step.PARSE_CONTRACT,
|
||||
is_step_completed=True,
|
||||
data={
|
||||
"saved_path": str(contract_extraction_path),
|
||||
"parsed_data": contract_extraction.model_dump_json(),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return ContractExtractionEvent(contract_extraction=contract_extraction)
|
||||
|
||||
@step
|
||||
async def dispatch_guideline_match( # type: ignore
|
||||
self, ctx: Context, ev: ContractExtractionEvent
|
||||
) -> MatchGuidelineEvent:
|
||||
"""For each clause in the contract, find relevant guidelines.
|
||||
|
||||
Use a map-reduce pattern, send each parsed clause as a MatchGuidelineEvent.
|
||||
"""
|
||||
await ctx.set("num_clauses", len(ev.contract_extraction.clauses))
|
||||
await ctx.set("vendor_name", ev.contract_extraction.vendor_name)
|
||||
|
||||
for clause in ev.contract_extraction.clauses:
|
||||
request_id = str(uuid.uuid4())
|
||||
ctx.send_event(
|
||||
MatchGuidelineEvent(
|
||||
request_id=request_id,
|
||||
clause=clause,
|
||||
vendor_name=ev.contract_extraction.vendor_name or "Not identified",
|
||||
)
|
||||
)
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg=f"Created {len(ev.contract_extraction.clauses)} tasks for analyzing with the guidelines",
|
||||
step=Step.ANALYZE_CLAUSES,
|
||||
)
|
||||
)
|
||||
|
||||
@step
|
||||
async def handle_guideline_match(
|
||||
self, ctx: Context, ev: MatchGuidelineEvent
|
||||
) -> MatchGuidelineResultEvent:
|
||||
"""Handle matching clause against guideline."""
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg=f"Handling clause for request {ev.request_id}",
|
||||
step=Step.HANDLE_CLAUSE,
|
||||
data={
|
||||
"request_id": ev.request_id,
|
||||
"clause_text": ev.clause.clause_text,
|
||||
"is_compliant": None,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# retrieve matching guideline
|
||||
query = f"""\
|
||||
Find the relevant guideline from {ev.vendor_name} that aligns with the following contract clause:
|
||||
|
||||
{ev.clause.clause_text}
|
||||
"""
|
||||
guideline_docs = self.guideline_retriever.retrieve(query)
|
||||
guideline_text = "\n\n".join([g.get_content() for g in guideline_docs])
|
||||
|
||||
# extract compliance from contract into a structured model
|
||||
# see ClauseComplianceCheck model for the schema
|
||||
prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_MATCH_PROMPT)])
|
||||
compliance_output = await self.llm.astructured_predict(
|
||||
ClauseComplianceCheck,
|
||||
prompt,
|
||||
clause_text=ev.clause.model_dump_json(),
|
||||
guideline_text=guideline_text,
|
||||
)
|
||||
|
||||
if not isinstance(compliance_output, ClauseComplianceCheck):
|
||||
raise ValueError(f"Invalid compliance check: {compliance_output}")
|
||||
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg=f"Completed compliance check for request {ev.request_id}",
|
||||
step=Step.HANDLE_CLAUSE,
|
||||
is_step_completed=True,
|
||||
data={
|
||||
"request_id": ev.request_id,
|
||||
"clause_text": ev.clause.clause_text,
|
||||
"is_compliant": compliance_output.compliant,
|
||||
"result": compliance_output,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return MatchGuidelineResultEvent(result=compliance_output)
|
||||
|
||||
@step
|
||||
async def gather_guideline_match(
|
||||
self, ctx: Context, ev: MatchGuidelineResultEvent
|
||||
) -> GenerateReportEvent | None:
|
||||
"""Handle matching clause against guideline."""
|
||||
num_clauses = await ctx.get("num_clauses")
|
||||
events = ctx.collect_events(ev, [MatchGuidelineResultEvent] * num_clauses)
|
||||
if events is None:
|
||||
return None
|
||||
|
||||
match_results = [e.result for e in events]
|
||||
# save match results
|
||||
contract_file_name = await ctx.get("contract_file_name")
|
||||
match_results_path = Path(
|
||||
f"{self.output_dir}/match_results_{contract_file_name}.jsonl"
|
||||
)
|
||||
with open(match_results_path, "w") as fp:
|
||||
for mr in match_results:
|
||||
fp.write(mr.model_dump_json() + "\n")
|
||||
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg=f"Processed {len(match_results)} clauses",
|
||||
step=Step.ANALYZE_CLAUSES,
|
||||
is_step_completed=True,
|
||||
data={"saved_path": str(match_results_path)},
|
||||
)
|
||||
)
|
||||
return GenerateReportEvent(match_results=[e.result for e in events])
|
||||
|
||||
@step
|
||||
async def generate_output(self, ctx: Context, ev: GenerateReportEvent) -> StopEvent:
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg="Generating Compliance Report",
|
||||
step=Step.GENERATE_REPORT,
|
||||
data={"is_completed": False},
|
||||
)
|
||||
)
|
||||
|
||||
# if all clauses are compliant, return a compliant result
|
||||
non_compliant_results = [r for r in ev.match_results if not r.compliant]
|
||||
|
||||
# generate compliance results string
|
||||
result_tmpl = """
|
||||
1. **Clause**: {clause}
|
||||
2. **Guideline:** {guideline}
|
||||
3. **Compliance Status:** {compliance_status}
|
||||
4. **Notes:** {notes}
|
||||
"""
|
||||
non_compliant_strings = []
|
||||
for nr in non_compliant_results:
|
||||
non_compliant_strings.append(
|
||||
result_tmpl.format(
|
||||
clause=nr.clause_text,
|
||||
guideline=nr.matched_guideline.guideline_text
|
||||
if nr.matched_guideline is not None
|
||||
else "No relevant guideline found",
|
||||
compliance_status=nr.compliant,
|
||||
notes=nr.notes,
|
||||
)
|
||||
)
|
||||
non_compliant_str = "\n\n".join(non_compliant_strings)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", COMPLIANCE_REPORT_SYSTEM_PROMPT),
|
||||
("user", COMPLIANCE_REPORT_USER_PROMPT),
|
||||
]
|
||||
)
|
||||
compliance_report = await self.llm.astructured_predict(
|
||||
ComplianceReport,
|
||||
prompt,
|
||||
compliance_results=non_compliant_str,
|
||||
vendor_name=await ctx.get("vendor_name"),
|
||||
)
|
||||
|
||||
# Save compliance report to file
|
||||
contract_file_name = await ctx.get("contract_file_name")
|
||||
compliance_report_path = Path(
|
||||
f"{self.output_dir}/report_{contract_file_name}.json"
|
||||
)
|
||||
with open(compliance_report_path, "w") as fp:
|
||||
fp.write(compliance_report.model_dump_json())
|
||||
|
||||
ctx.write_event_to_stream(
|
||||
LogEvent(
|
||||
msg=f"Compliance report saved to {compliance_report_path}",
|
||||
step=Step.GENERATE_REPORT,
|
||||
is_step_completed=True,
|
||||
data={
|
||||
"saved_path": str(compliance_report_path),
|
||||
"result": compliance_report,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return StopEvent(
|
||||
result={
|
||||
"report": compliance_report,
|
||||
"non_compliant_results": non_compliant_results,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from .upload import upload_component
|
||||
from .workflow import guideline_component, load_contract_component, report_component
|
||||
|
||||
__all__ = [
|
||||
"upload_component",
|
||||
"load_contract_component",
|
||||
"guideline_component",
|
||||
"report_component",
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
from .card import card_component
|
||||
|
||||
__all__ = [
|
||||
"card_component",
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
import reflex as rx
|
||||
|
||||
|
||||
def card_component(
|
||||
title: str,
|
||||
children: rx.Component,
|
||||
show_loading: bool = False,
|
||||
) -> rx.Component:
|
||||
return rx.card(
|
||||
rx.hstack(
|
||||
rx.cond(show_loading, rx.spinner(size="2")),
|
||||
rx.text(title, size="4"),
|
||||
align_items="center",
|
||||
gap="2",
|
||||
),
|
||||
rx.divider(orientation="horizontal"),
|
||||
rx.container(children),
|
||||
width="100%",
|
||||
background_color="var(--gray-3)",
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
import reflex as rx
|
||||
|
||||
from app.ui.components.shared import card_component
|
||||
from app.ui.states.app import AppState
|
||||
|
||||
|
||||
def upload_component() -> rx.Component:
|
||||
return card_component(
|
||||
title="Upload",
|
||||
children=rx.container(
|
||||
rx.vstack(
|
||||
rx.upload(
|
||||
rx.vstack(
|
||||
rx.text("Drag and drop files here or click to select files"),
|
||||
),
|
||||
on_drop=AppState.handle_upload(
|
||||
rx.upload_files(upload_id="upload1")
|
||||
),
|
||||
id="upload1",
|
||||
border="1px dotted rgb(107,99,246)",
|
||||
padding="1rem",
|
||||
),
|
||||
rx.cond(
|
||||
AppState.uploaded_file != None, # noqa: E711
|
||||
rx.text(AppState.uploaded_file.file_name), # type: ignore
|
||||
rx.text("No file uploaded"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from .guideline import guideline_component
|
||||
from .load import load_contract_component
|
||||
from .report import report_component
|
||||
|
||||
__all__ = [
|
||||
"guideline_component",
|
||||
"load_contract_component",
|
||||
"report_component",
|
||||
]
|
||||
@@ -0,0 +1,113 @@
|
||||
from typing import List
|
||||
|
||||
import reflex as rx
|
||||
|
||||
from app.models import ClauseComplianceCheck
|
||||
from app.ui.components.shared import card_component
|
||||
from app.ui.states.workflow import GuidelineData, GuidelineHandlerState, GuidelineState
|
||||
|
||||
|
||||
def guideline_handler_component(item: List) -> rx.Component:
|
||||
_id: str = item[0]
|
||||
status: GuidelineData = item[1]
|
||||
|
||||
return rx.hover_card.root(
|
||||
rx.hover_card.trigger(
|
||||
rx.card(
|
||||
rx.stack(
|
||||
rx.container(
|
||||
rx.cond(
|
||||
~status.is_completed,
|
||||
rx.spinner(size="1"),
|
||||
rx.cond(
|
||||
status.is_compliant,
|
||||
rx.icon(tag="check", color="green"),
|
||||
rx.icon(tag="x", color="red"),
|
||||
),
|
||||
),
|
||||
),
|
||||
rx.flex(
|
||||
rx.text(status.clause_text, size="1"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
rx.hover_card.content(
|
||||
rx.cond(
|
||||
status.is_completed,
|
||||
guideline_result_component(status.result), # type: ignore
|
||||
rx.spinner(size="1"),
|
||||
),
|
||||
side="right",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def guideline_result_component(result: ClauseComplianceCheck) -> rx.Component:
|
||||
return rx.inset(
|
||||
rx.table.root(
|
||||
rx.table.header(
|
||||
rx.table.row(
|
||||
rx.table.cell("Clause"),
|
||||
rx.table.cell("Guideline"),
|
||||
),
|
||||
),
|
||||
rx.table.body(
|
||||
rx.table.row(
|
||||
# rx.table.cell("Clause"),
|
||||
rx.table.cell(
|
||||
rx.text(result.clause_text, size="1"),
|
||||
), # type: ignore
|
||||
rx.table.cell(
|
||||
rx.text(result.matched_guideline.guideline_text, size="1"), # type: ignore
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
rx.container(
|
||||
rx.cond(
|
||||
result.compliant,
|
||||
rx.text(
|
||||
result.notes, # type: ignore
|
||||
size="2",
|
||||
color="green",
|
||||
),
|
||||
rx.text(
|
||||
result.notes, # type: ignore
|
||||
size="2",
|
||||
color="red",
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def guideline_component() -> rx.Component:
|
||||
return rx.cond(
|
||||
GuidelineState.is_started,
|
||||
card_component(
|
||||
title="Analyze the document with provided guidelines",
|
||||
children=rx.vstack(
|
||||
rx.vstack(
|
||||
rx.foreach(
|
||||
GuidelineState.log,
|
||||
lambda log: rx.box(
|
||||
rx.text(log["msg"]),
|
||||
),
|
||||
),
|
||||
),
|
||||
rx.cond(
|
||||
GuidelineHandlerState.has_data(), # type: ignore
|
||||
rx.grid(
|
||||
rx.foreach(
|
||||
GuidelineHandlerState.data,
|
||||
guideline_handler_component,
|
||||
),
|
||||
columns="2",
|
||||
spacing="1",
|
||||
),
|
||||
),
|
||||
),
|
||||
show_loading=GuidelineState.is_running,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
import reflex as rx
|
||||
|
||||
from app.ui.components.shared import card_component
|
||||
from app.ui.states.workflow import ContractLoaderState
|
||||
|
||||
|
||||
def load_contract_component() -> rx.Component:
|
||||
return rx.cond(
|
||||
ContractLoaderState.is_started,
|
||||
card_component(
|
||||
title="Parse contract",
|
||||
children=rx.vstack(
|
||||
rx.foreach(
|
||||
ContractLoaderState.log,
|
||||
lambda log: rx.box(
|
||||
rx.text(log["msg"]),
|
||||
),
|
||||
),
|
||||
),
|
||||
show_loading=ContractLoaderState.is_running,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
import reflex as rx
|
||||
|
||||
from app.ui.components.shared import card_component
|
||||
from app.ui.states.workflow import ReportState
|
||||
|
||||
|
||||
def report_component() -> rx.Component:
|
||||
return rx.cond(
|
||||
ReportState.is_running,
|
||||
card_component(
|
||||
title="Conclusion",
|
||||
show_loading=~ReportState.is_completed, # type: ignore
|
||||
children=rx.cond(
|
||||
ReportState.is_completed,
|
||||
rx.vstack(
|
||||
rx.box(
|
||||
rx.inset(
|
||||
rx.table.root(
|
||||
rx.table.body(
|
||||
rx.table.row(
|
||||
rx.table.cell("Vendor"),
|
||||
rx.table.cell(ReportState.result.vendor_name), # type: ignore
|
||||
),
|
||||
rx.table.row(
|
||||
rx.table.cell("Overall Compliance"),
|
||||
rx.table.cell(
|
||||
rx.cond(
|
||||
ReportState.result.overall_compliant,
|
||||
rx.text("Compliant", color="green"),
|
||||
rx.text("Non-compliant", color="red"),
|
||||
)
|
||||
),
|
||||
),
|
||||
rx.table.row(
|
||||
rx.table.cell("Summary Notes"),
|
||||
rx.table.cell(ReportState.result.summary_notes), # type: ignore
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
rx.vstack(
|
||||
rx.text("Analyzing compliance results for final conclusion..."),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
import reflex as rx
|
||||
|
||||
from app.ui.components import (
|
||||
guideline_component,
|
||||
load_contract_component,
|
||||
report_component,
|
||||
upload_component,
|
||||
)
|
||||
from app.ui.templates import template
|
||||
|
||||
|
||||
@template(
|
||||
route="/",
|
||||
title="Structure extractor",
|
||||
)
|
||||
def index() -> rx.Component:
|
||||
"""The main index page."""
|
||||
return rx.vstack(
|
||||
rx.vstack(
|
||||
rx.heading("Built by LlamaIndex", size="6"),
|
||||
rx.text(
|
||||
"Upload a contract to start the review process.",
|
||||
),
|
||||
background_color="var(--gray-3)",
|
||||
align_items="left",
|
||||
justify_content="left",
|
||||
width="100%",
|
||||
padding="1rem",
|
||||
),
|
||||
rx.container(
|
||||
rx.vstack(
|
||||
# Upload
|
||||
upload_component(),
|
||||
# Workflow
|
||||
rx.vstack(
|
||||
load_contract_component(),
|
||||
guideline_component(),
|
||||
report_component(),
|
||||
width="100%",
|
||||
),
|
||||
),
|
||||
width="100%",
|
||||
padding="1rem",
|
||||
),
|
||||
width="100%",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
from .app import AppState
|
||||
from .workflow import (
|
||||
ContractLoaderState,
|
||||
GuidelineHandlerState,
|
||||
GuidelineState,
|
||||
ReportState,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AppState",
|
||||
"ContractLoaderState",
|
||||
"GuidelineHandlerState",
|
||||
"GuidelineState",
|
||||
"ReportState",
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import reflex as rx
|
||||
|
||||
from app.config import UPLOADED_DIR
|
||||
from app.services.contract_reviewer import LogEvent, Step, get_workflow
|
||||
from app.ui.states.workflow import (
|
||||
ContractLoaderState,
|
||||
GuidelineHandlerState,
|
||||
GuidelineState,
|
||||
ReportState,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UploadedFile(rx.Base):
|
||||
file_name: str
|
||||
size: int
|
||||
|
||||
|
||||
class AppState(rx.State):
|
||||
"""
|
||||
Whole main state for the app.
|
||||
Handle for file upload, trigger workflow and produce workflow events.
|
||||
"""
|
||||
|
||||
uploaded_file: Optional[UploadedFile] = None
|
||||
|
||||
@rx.event
|
||||
async def handle_upload(self, files: List[rx.UploadFile]):
|
||||
if len(files) > 1:
|
||||
yield rx.toast.error(
|
||||
"You can only upload one file at a time", position="top-center"
|
||||
)
|
||||
return
|
||||
try:
|
||||
file = files[0]
|
||||
upload_data = await file.read()
|
||||
outfile = os.path.join(UPLOADED_DIR, file.filename)
|
||||
with open(outfile, "wb") as f:
|
||||
f.write(upload_data)
|
||||
self.uploaded_file = UploadedFile(
|
||||
file_name=file.filename, size=len(upload_data)
|
||||
)
|
||||
yield AppState.reset_workflow
|
||||
yield AppState.trigger_workflow
|
||||
except Exception as e:
|
||||
yield rx.toast.error(str(e), position="top-center")
|
||||
|
||||
@rx.event
|
||||
def reset_workflow(self):
|
||||
yield ContractLoaderState.reset_state
|
||||
yield GuidelineState.reset_state
|
||||
yield GuidelineHandlerState.reset_state
|
||||
yield ReportState.reset_state
|
||||
|
||||
@rx.event(background=True)
|
||||
async def trigger_workflow(self):
|
||||
"""
|
||||
Trigger backend to start reviewing the contract in a loop.
|
||||
Get the event from the loop and update the state.
|
||||
"""
|
||||
if self.uploaded_file is None:
|
||||
yield rx.toast.error("No file uploaded", position="top-center")
|
||||
else:
|
||||
uploaded_file_path = os.path.join(
|
||||
UPLOADED_DIR, self.uploaded_file.file_name
|
||||
)
|
||||
|
||||
try:
|
||||
workflow = get_workflow()
|
||||
handler = workflow.run(
|
||||
contract_path=uploaded_file_path,
|
||||
)
|
||||
async for event in handler.stream_events():
|
||||
if isinstance(event, LogEvent):
|
||||
match event.step:
|
||||
case Step.PARSE_CONTRACT:
|
||||
yield ContractLoaderState.add_log(event)
|
||||
case Step.ANALYZE_CLAUSES:
|
||||
yield GuidelineState.add_log(event)
|
||||
case Step.HANDLE_CLAUSE:
|
||||
yield GuidelineHandlerState.add_log(event)
|
||||
case Step.GENERATE_REPORT:
|
||||
yield ReportState.add_log(event)
|
||||
# Wait for workflow completion and propagate any exceptions
|
||||
_ = await handler
|
||||
except Exception as e:
|
||||
logger.error(f"Error in trigger_workflow: {e}")
|
||||
yield rx.toast.error(str(e), position="top-center")
|
||||
yield AppState.reset_workflow
|
||||
@@ -0,0 +1,137 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import reflex as rx
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.models import ClauseComplianceCheck, ComplianceReport
|
||||
from app.services.contract_reviewer import LogEvent
|
||||
from rxconfig import config as rx_config
|
||||
|
||||
|
||||
class ContractLoaderState(rx.State):
|
||||
is_running: bool = False
|
||||
is_started: bool = False
|
||||
log: List[Dict[str, Any]] = []
|
||||
|
||||
@rx.event
|
||||
async def add_log(self, log: LogEvent):
|
||||
if not self.is_started:
|
||||
yield ContractLoaderState.start
|
||||
self.log.append(log.model_dump())
|
||||
if log.is_step_completed:
|
||||
yield ContractLoaderState.stop
|
||||
|
||||
def has_log(self):
|
||||
return len(self.log) > 0
|
||||
|
||||
@rx.event
|
||||
def start(self):
|
||||
self.is_running = True
|
||||
self.is_started = True
|
||||
|
||||
@rx.event
|
||||
def stop(self):
|
||||
self.is_running = False
|
||||
|
||||
@rx.event
|
||||
def reset_state(self):
|
||||
self.is_running = False
|
||||
self.is_started = False
|
||||
self.log = []
|
||||
|
||||
|
||||
class GuidelineState(rx.State):
|
||||
is_running: bool = False
|
||||
is_started: bool = False
|
||||
log: List[Dict[str, Any]] = []
|
||||
|
||||
@rx.event
|
||||
def add_log(self, log: LogEvent):
|
||||
if not self.is_started:
|
||||
yield GuidelineState.start
|
||||
self.log.append(log.model_dump())
|
||||
if log.is_step_completed:
|
||||
yield GuidelineState.stop
|
||||
|
||||
def has_log(self):
|
||||
return len(self.log) > 0
|
||||
|
||||
@rx.event
|
||||
def reset_state(self):
|
||||
self.is_running = False
|
||||
self.is_started = False
|
||||
self.log = []
|
||||
|
||||
@rx.event
|
||||
def start(self):
|
||||
self.is_running = True
|
||||
self.is_started = True
|
||||
|
||||
@rx.event
|
||||
def stop(self):
|
||||
self.is_running = False
|
||||
|
||||
|
||||
class GuidelineData(BaseModel):
|
||||
is_completed: bool
|
||||
is_compliant: Optional[bool]
|
||||
clause_text: Optional[str]
|
||||
result: Optional[ClauseComplianceCheck] = None
|
||||
|
||||
|
||||
class GuidelineHandlerState(rx.State):
|
||||
data: Dict[str, GuidelineData] = {}
|
||||
|
||||
def has_data(self):
|
||||
return len(self.data) > 0
|
||||
|
||||
@rx.event
|
||||
def add_log(self, log: LogEvent):
|
||||
_id = log.data.get("request_id")
|
||||
if _id is None:
|
||||
return
|
||||
is_compliant = log.data.get("is_compliant", None)
|
||||
self.data[_id] = GuidelineData(
|
||||
is_completed=log.is_step_completed,
|
||||
is_compliant=is_compliant,
|
||||
clause_text=log.data.get("clause_text", None),
|
||||
result=log.data.get("result", None),
|
||||
)
|
||||
if log.is_step_completed:
|
||||
yield self.stop(request_id=_id)
|
||||
|
||||
@rx.event
|
||||
def reset_state(self):
|
||||
self.data = {}
|
||||
|
||||
@rx.event
|
||||
def stop(self, request_id: str):
|
||||
# Update the item in the data to be completed
|
||||
self.data[request_id].is_completed = True
|
||||
|
||||
|
||||
class ReportState(rx.State):
|
||||
is_running: bool = False
|
||||
is_completed: bool = False
|
||||
saved_path: str = ""
|
||||
result: Optional[ComplianceReport] = None
|
||||
|
||||
@rx.var()
|
||||
def download_url(self) -> str:
|
||||
return f"{rx_config.api_url}/api/download/{self.saved_path}"
|
||||
|
||||
@rx.event
|
||||
def add_log(self, log: LogEvent):
|
||||
if not self.is_running:
|
||||
self.is_running = True
|
||||
if log.is_step_completed:
|
||||
self.is_completed = True
|
||||
self.saved_path = log.data.get("saved_path")
|
||||
self.result = log.data.get("result")
|
||||
|
||||
@rx.event
|
||||
def reset_state(self):
|
||||
self.is_running = False
|
||||
self.is_completed = False
|
||||
self.saved_path = ""
|
||||
self.result = None
|
||||
@@ -0,0 +1,142 @@
|
||||
# ACME Vendor Agreement
|
||||
|
||||
**Effective Date:** January 1, 2024
|
||||
|
||||
## Parties:
|
||||
|
||||
- **Client:** LlamaCo ("Client")
|
||||
- **Vendor:** ACME Office Supply, Inc. ("Vendor")
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This Vendor Agreement ("Agreement") sets forth the terms and conditions under which ACME Office Supply, Inc. will provide office supplies, consumables, related goods ("Products"), and associated data processing services to LlamaCo.
|
||||
|
||||
## 2. Definitions
|
||||
|
||||
- **Personal Data:** Any information relating to an identified or identifiable natural person ('data subject').
|
||||
- **Processing:** Any operation performed on Personal Data, including collection, storage, modification, transfer, or deletion.
|
||||
- **Data Controller:** LlamaCo, who determines the purposes and means of processing Personal Data.
|
||||
- **Data Processor:** ACME Office Supply, Inc., who processes Personal Data on behalf of the Controller.
|
||||
|
||||
## 3. Data Protection and Privacy
|
||||
|
||||
### 3.1 Scope of Processing
|
||||
|
||||
Vendor shall process Personal Data only:
|
||||
|
||||
- To fulfill orders and manage deliveries
|
||||
- To provide customer support services
|
||||
- To maintain business records
|
||||
- To comply with legal obligations
|
||||
|
||||
### 3.2 Data Subject Rights
|
||||
|
||||
Vendor shall:
|
||||
|
||||
- Respond to data subject requests within 30 days
|
||||
- Provide data in a structured, commonly used format
|
||||
- Implement measures to facilitate data portability
|
||||
- Assist with data subject rights requests at no additional cost
|
||||
|
||||
### 3.3 Data Transfers and Storage
|
||||
|
||||
- Vendor maintains primary data centers in the United States
|
||||
- Vendor may transfer data to any country where it maintains operations
|
||||
- No prior notification required for new data storage locations
|
||||
- Vendor will rely on its standard data transfer mechanisms
|
||||
- Data may be processed by staff operating outside the EEA
|
||||
|
||||
### 3.4 Subprocessors
|
||||
|
||||
- Vendor may engage subprocessors without prior Client approval
|
||||
- Subprocessors may be located in any jurisdiction globally
|
||||
- Notice of new subprocessors provided within 30 days of engagement
|
||||
- Client has no right to object to new subprocessors
|
||||
|
||||
## 4. Security Measures
|
||||
|
||||
### 4.1 Technical and Organizational Measures
|
||||
|
||||
Vendor shall implement appropriate measures including:
|
||||
|
||||
- Encryption of Personal Data in transit and at rest
|
||||
- Access controls and authentication
|
||||
- Regular security testing and assessments
|
||||
- Employee training on data protection
|
||||
- Incident response procedures
|
||||
|
||||
### 4.2 Data Breaches
|
||||
|
||||
Vendor shall:
|
||||
|
||||
- Notify Client of any Personal Data breach within 72 hours
|
||||
- Provide details necessary to meet regulatory requirements
|
||||
- Cooperate with Client's breach investigation
|
||||
- Maintain records of all data breaches
|
||||
|
||||
## 5. Data Retention
|
||||
|
||||
### 5.1 Retention Period
|
||||
|
||||
- Personal Data retained only as long as necessary
|
||||
- Standard retention period of 3 years after last transaction
|
||||
- Deletion of Personal Data upon written request
|
||||
- Backup copies retained for maximum of 6 months
|
||||
|
||||
### 5.2 Termination
|
||||
|
||||
Upon termination of services:
|
||||
|
||||
- Return all Personal Data in standard format
|
||||
- Delete existing copies within 30 days
|
||||
- Provide written confirmation of deletion
|
||||
- Cease all processing activities
|
||||
|
||||
## 6. Compliance and Audit
|
||||
|
||||
### 6.1 Documentation
|
||||
|
||||
Vendor shall maintain:
|
||||
|
||||
- Records of all processing activities
|
||||
- Security measure documentation
|
||||
- Data transfer mechanisms
|
||||
- Subprocessor agreements
|
||||
|
||||
### 6.2 Audits
|
||||
|
||||
- Annual compliance audits permitted
|
||||
- 30 days notice required for audits
|
||||
- Vendor to provide necessary documentation
|
||||
- Client bears reasonable audit costs
|
||||
|
||||
## 7. Liability and Indemnification
|
||||
|
||||
### 7.1 Liability
|
||||
|
||||
- Vendor liable for data protection violations
|
||||
- Reasonable compensation for damages
|
||||
- Coverage for regulatory fines where applicable
|
||||
- Joint liability as required by law
|
||||
|
||||
## 8. Governing Law
|
||||
|
||||
This Agreement shall be governed by the laws of Ireland, without regard to its conflict of laws principles.
|
||||
|
||||
---
|
||||
|
||||
IN WITNESS WHEREOF, the parties have executed this Agreement as of the Effective Date.
|
||||
|
||||
**LlamaCo**
|
||||
|
||||
By: **_
|
||||
Name: [Authorized Representative]
|
||||
Title: [Title]
|
||||
Date: _**
|
||||
|
||||
**ACME Office Supply, Inc.**
|
||||
|
||||
By: **_
|
||||
Name: [Authorized Representative]
|
||||
Title: [Title]
|
||||
Date: _**
|
||||
@@ -0,0 +1,44 @@
|
||||
[tool]
|
||||
[tool.poetry]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [ "Marcus Schiesser <mail@marcusschiesser.de>" ]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
generate = "app.engine.generate:generate_datasource"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11,<4.0"
|
||||
fastapi = "^0.109.1"
|
||||
python-dotenv = "^1.0.0"
|
||||
pydantic = "<2.10"
|
||||
llama-index = "^0.12.1"
|
||||
cachetools = "^5.3.3"
|
||||
reflex = "^0.6.2.post1"
|
||||
|
||||
[tool.poetry.dependencies.uvicorn]
|
||||
extras = [ "standard" ]
|
||||
version = "^0.23.2"
|
||||
|
||||
[tool.poetry.dependencies.docx2txt]
|
||||
version = "^0.8"
|
||||
|
||||
[tool.poetry.dependencies.llama-index-llms-openai]
|
||||
version = "^0.3.2"
|
||||
|
||||
[tool.poetry.dependencies.llama-index-embeddings-openai]
|
||||
version = "^0.3.1"
|
||||
|
||||
[tool.poetry.dependencies.llama-index-agent-openai]
|
||||
version = "^0.4.0"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest-asyncio = "^0.25.0"
|
||||
pytest = "^8.3.4"
|
||||
|
||||
[build-system]
|
||||
requires = [ "poetry-core" ]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -0,0 +1 @@
|
||||
from .index import index as index
|
||||
@@ -17,11 +17,11 @@ import logging
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import asdict
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.engine.tools.artifact import CodeArtifact
|
||||
from app.services.file import FileService
|
||||
from e2b_code_interpreter import CodeInterpreter, Sandbox # type: ignore
|
||||
from e2b_code_interpreter import Sandbox # type: ignore
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -60,6 +60,14 @@ class FileUpload(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
SUPPORTED_TEMPLATES = [
|
||||
"nextjs-developer",
|
||||
"vue-developer",
|
||||
"streamlit-developer",
|
||||
"gradio-developer",
|
||||
]
|
||||
|
||||
|
||||
@sandbox_router.post("")
|
||||
async def create_sandbox(request: Request):
|
||||
request_data = await request.json()
|
||||
@@ -79,33 +87,22 @@ async def create_sandbox(request: Request):
|
||||
status_code=400, detail="Could not create artifact from the request data"
|
||||
)
|
||||
|
||||
sbx = None
|
||||
|
||||
# Create an interpreter or a sandbox
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
sbx = CodeInterpreter(api_key=os.getenv("E2B_API_KEY"), timeout=SANDBOX_TIMEOUT)
|
||||
logger.debug(f"Created code interpreter {sbx}")
|
||||
else:
|
||||
sbx = Sandbox(
|
||||
api_key=os.getenv("E2B_API_KEY"),
|
||||
template=artifact.template,
|
||||
metadata={"template": artifact.template, "user_id": "default"},
|
||||
timeout=SANDBOX_TIMEOUT,
|
||||
)
|
||||
logger.debug(f"Created sandbox {sbx}")
|
||||
sbx = Sandbox(
|
||||
api_key=os.getenv("E2B_API_KEY"),
|
||||
template=(
|
||||
None if artifact.template not in SUPPORTED_TEMPLATES else artifact.template
|
||||
),
|
||||
metadata={"template": artifact.template, "user_id": "default"},
|
||||
timeout=SANDBOX_TIMEOUT,
|
||||
)
|
||||
logger.debug(f"Created sandbox {sbx}")
|
||||
|
||||
# Install packages
|
||||
if artifact.has_additional_dependencies:
|
||||
if isinstance(sbx, CodeInterpreter):
|
||||
sbx.notebook.exec_cell(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in code interpreter {sbx}"
|
||||
)
|
||||
elif isinstance(sbx, Sandbox):
|
||||
sbx.commands.run(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
|
||||
)
|
||||
sbx.commands.run(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
|
||||
)
|
||||
|
||||
# Copy files
|
||||
if len(sandbox_files) > 0:
|
||||
@@ -122,7 +119,7 @@ async def create_sandbox(request: Request):
|
||||
|
||||
# Execute code or return a URL to the running sandbox
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
result = sbx.notebook.exec_cell(artifact.code or "")
|
||||
result = sbx.run_code(artifact.code or "")
|
||||
output_urls = _download_cell_results(result.results)
|
||||
runtime_error = asdict(result.error) if result.error else None
|
||||
return ExecutionResult(
|
||||
@@ -145,7 +142,7 @@ async def create_sandbox(request: Request):
|
||||
|
||||
|
||||
def _upload_files(
|
||||
sandbox: Union[CodeInterpreter, Sandbox],
|
||||
sandbox: Sandbox,
|
||||
sandbox_files: List[str] = [],
|
||||
) -> None:
|
||||
for file_path in sandbox_files:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -0,0 +1,65 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import DATA_DIR
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class SourceNodes(BaseModel):
|
||||
id: str
|
||||
metadata: Dict[str, Any]
|
||||
score: Optional[float]
|
||||
text: str
|
||||
url: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def from_source_node(cls, source_node: NodeWithScore):
|
||||
metadata = source_node.node.metadata
|
||||
url = cls.get_url_from_metadata(metadata)
|
||||
|
||||
return cls(
|
||||
id=source_node.node.node_id,
|
||||
metadata=metadata,
|
||||
score=source_node.score,
|
||||
text=source_node.node.text, # type: ignore
|
||||
url=url,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_url_from_metadata(cls, metadata: Dict[str, Any]) -> Optional[str]:
|
||||
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if not url_prefix:
|
||||
logger.warning(
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
|
||||
)
|
||||
file_name = metadata.get("file_name")
|
||||
|
||||
if file_name and url_prefix:
|
||||
# file_name exists and file server is configured
|
||||
pipeline_id = metadata.get("pipeline_id")
|
||||
if pipeline_id:
|
||||
# file is from LlamaCloud
|
||||
file_name = f"{pipeline_id}${file_name}"
|
||||
return f"{url_prefix}/output/llamacloud/{file_name}"
|
||||
is_private = metadata.get("private", "false") == "true"
|
||||
if is_private:
|
||||
# file is a private upload
|
||||
return f"{url_prefix}/output/uploaded/{file_name}"
|
||||
# file is from calling the 'generate' script
|
||||
# Get the relative path of file_path to data_dir
|
||||
file_path = metadata.get("file_path")
|
||||
data_dir = os.path.abspath(DATA_DIR)
|
||||
if file_path and data_dir:
|
||||
relative_path = os.path.relpath(file_path, data_dir)
|
||||
return f"{url_prefix}/data/{relative_path}"
|
||||
# fallback to URL in metadata (e.g. for websites)
|
||||
return metadata.get("URL")
|
||||
|
||||
@classmethod
|
||||
def from_source_nodes(cls, source_nodes: List[NodeWithScore]):
|
||||
return [cls.from_source_node(node) for node in source_nodes]
|
||||
@@ -1,22 +1,18 @@
|
||||
# flake8: noqa: E402
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
import reflex as rx
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.api.routers.extractor import extractor_router
|
||||
from app.api.routers.main import api_router
|
||||
from app.settings import init_settings
|
||||
from app.ui.pages import * # Keep this import all pages in the app # noqa: F403
|
||||
|
||||
init_settings()
|
||||
|
||||
|
||||
def add_routers(app: FastAPI):
|
||||
app.include_router(extractor_router, prefix="/api/extractor")
|
||||
|
||||
|
||||
app = rx.App()
|
||||
add_routers(app.api)
|
||||
app.api.include_router(api_router)
|
||||
@@ -0,0 +1,281 @@
|
||||
import base64
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from llama_index.core import VectorStoreIndex
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.readers.file.base import (
|
||||
_try_loading_included_file_formats as get_file_loaders_map,
|
||||
)
|
||||
from llama_index.core.schema import Document
|
||||
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
|
||||
from llama_index.readers.file import FlatReader
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PRIVATE_STORE_PATH = str(Path("output", "uploaded"))
|
||||
TOOL_STORE_PATH = str(Path("output", "tools"))
|
||||
LLAMA_CLOUD_STORE_PATH = str(Path("output", "llamacloud"))
|
||||
|
||||
|
||||
class DocumentFile(BaseModel):
|
||||
id: str
|
||||
name: str # Stored file name
|
||||
type: str = None
|
||||
size: int = None
|
||||
url: str = None
|
||||
path: Optional[str] = Field(
|
||||
None,
|
||||
description="The stored file path. Used internally in the server.",
|
||||
exclude=True,
|
||||
)
|
||||
refs: Optional[List[str]] = Field(
|
||||
None, description="The document ids in the index."
|
||||
)
|
||||
|
||||
|
||||
class FileService:
|
||||
"""
|
||||
To store the files uploaded by the user and add them to the index.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def process_private_file(
|
||||
cls,
|
||||
file_name: str,
|
||||
base64_content: str,
|
||||
params: Optional[dict] = None,
|
||||
) -> DocumentFile:
|
||||
"""
|
||||
Store the uploaded file and index it if necessary.
|
||||
"""
|
||||
try:
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
except ImportError as e:
|
||||
raise ValueError("IndexConfig or get_index is not found") from e
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
|
||||
# Preprocess and store the file
|
||||
file_data, extension = cls._preprocess_base64_file(base64_content)
|
||||
|
||||
document_file = cls.save_file(
|
||||
file_data,
|
||||
file_name=file_name,
|
||||
save_dir=PRIVATE_STORE_PATH,
|
||||
)
|
||||
|
||||
# Don't index csv files (they are handled by tools)
|
||||
if extension == "csv":
|
||||
return document_file
|
||||
else:
|
||||
# Insert the file into the index and update document ids to the file metadata
|
||||
if isinstance(index, LlamaCloudIndex):
|
||||
doc_id = cls._add_file_to_llama_cloud_index(
|
||||
index, document_file.name, file_data
|
||||
)
|
||||
# Add document ids to the file metadata
|
||||
document_file.refs = [doc_id]
|
||||
else:
|
||||
documents = cls._load_file_to_documents(document_file)
|
||||
cls._add_documents_to_vector_store_index(documents, index)
|
||||
# Add document ids to the file metadata
|
||||
document_file.refs = [doc.doc_id for doc in documents]
|
||||
|
||||
# Return the file metadata
|
||||
return document_file
|
||||
|
||||
@classmethod
|
||||
def save_file(
|
||||
cls,
|
||||
content: bytes | str,
|
||||
file_name: str,
|
||||
save_dir: Optional[str] = None,
|
||||
) -> DocumentFile:
|
||||
"""
|
||||
Save the content to a file in the local file server (accessible via URL)
|
||||
|
||||
Args:
|
||||
content (bytes | str): The content to save, either bytes or string.
|
||||
file_name (str): The original name of the file.
|
||||
save_dir (Optional[str]): The relative path from the current working directory. Defaults to the `output/uploaded` directory.
|
||||
Returns:
|
||||
The metadata of the saved file.
|
||||
"""
|
||||
if save_dir is None:
|
||||
save_dir = os.path.join("output", "uploaded")
|
||||
|
||||
file_id = str(uuid.uuid4())
|
||||
name, extension = os.path.splitext(file_name)
|
||||
extension = extension.lstrip(".")
|
||||
sanitized_name = _sanitize_file_name(name)
|
||||
if extension == "":
|
||||
raise ValueError("File is not supported!")
|
||||
new_file_name = f"{sanitized_name}_{file_id}.{extension}"
|
||||
|
||||
file_path = os.path.join(save_dir, new_file_name)
|
||||
|
||||
if isinstance(content, str):
|
||||
content = content.encode()
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(content)
|
||||
except PermissionError as e:
|
||||
logger.error(
|
||||
f"Permission denied when writing to file {file_path}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
except IOError as e:
|
||||
logger.error(
|
||||
f"IO error occurred when writing to file {file_path}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error when writing to file {file_path}: {str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"Saved file to {file_path}")
|
||||
|
||||
file_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if file_url_prefix is None:
|
||||
logger.warning(
|
||||
"FILESERVER_URL_PREFIX is not set, fallback to http://localhost:8000/api/files"
|
||||
)
|
||||
file_url_prefix = "http://localhost:8000/api/files"
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
file_url = os.path.join(
|
||||
file_url_prefix,
|
||||
save_dir,
|
||||
new_file_name,
|
||||
)
|
||||
|
||||
return DocumentFile(
|
||||
id=file_id,
|
||||
name=new_file_name,
|
||||
type=extension,
|
||||
size=file_size,
|
||||
path=file_path,
|
||||
url=file_url,
|
||||
refs=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
|
||||
header, data = base64_content.split(",", 1)
|
||||
mime_type = header.split(";")[0].split(":", 1)[1]
|
||||
extension = mimetypes.guess_extension(mime_type).lstrip(".")
|
||||
# File data as bytes
|
||||
return base64.b64decode(data), extension
|
||||
|
||||
@staticmethod
|
||||
def _load_file_to_documents(file: DocumentFile) -> List[Document]:
|
||||
"""
|
||||
Load the file from the private directory and return the documents
|
||||
"""
|
||||
_, extension = os.path.splitext(file.name)
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
# Load file to documents
|
||||
# If LlamaParse is enabled, use it to parse the file
|
||||
# Otherwise, use the default file loaders
|
||||
reader = _get_llamaparse_parser()
|
||||
if reader is None:
|
||||
reader_cls = _default_file_loaders_map().get(f".{extension}")
|
||||
if reader_cls is None:
|
||||
raise ValueError(f"File extension {extension} is not supported")
|
||||
reader = reader_cls()
|
||||
if file.path is None:
|
||||
raise ValueError("Document file path is not set")
|
||||
documents = reader.load_data(Path(file.path))
|
||||
# Add custom metadata
|
||||
for doc in documents:
|
||||
doc.metadata["file_name"] = file.name
|
||||
doc.metadata["private"] = "true"
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def _add_documents_to_vector_store_index(
|
||||
documents: List[Document], index: VectorStoreIndex
|
||||
) -> None:
|
||||
"""
|
||||
Add the documents to the vector store index
|
||||
"""
|
||||
pipeline = IngestionPipeline()
|
||||
nodes = pipeline.run(documents=documents)
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
if index is None:
|
||||
index = VectorStoreIndex(nodes=nodes)
|
||||
else:
|
||||
index.insert_nodes(nodes=nodes)
|
||||
index.storage_context.persist(
|
||||
persist_dir=os.environ.get("STORAGE_DIR", "storage")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_file_to_llama_cloud_index(
|
||||
index: LlamaCloudIndex,
|
||||
file_name: str,
|
||||
file_data: bytes,
|
||||
) -> str:
|
||||
"""
|
||||
Add the file to the LlamaCloud index.
|
||||
LlamaCloudIndex is a managed index so we can directly use the files.
|
||||
"""
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService # type: ignore
|
||||
except ImportError as e:
|
||||
raise ValueError("LlamaCloudFileService is not found") from e
|
||||
|
||||
# LlamaCloudIndex is a managed index so we can directly use the files
|
||||
upload_file = (file_name, BytesIO(file_data))
|
||||
doc_id = LLamaCloudFileService.add_file_to_pipeline(
|
||||
index.project.id,
|
||||
index.pipeline.id,
|
||||
upload_file,
|
||||
custom_metadata={},
|
||||
wait_for_processing=True,
|
||||
)
|
||||
return doc_id
|
||||
|
||||
|
||||
def _sanitize_file_name(file_name: str) -> str:
|
||||
"""
|
||||
Sanitize the file name by replacing all non-alphanumeric characters with underscores
|
||||
"""
|
||||
sanitized_name = re.sub(r"[^a-zA-Z0-9.]", "_", file_name)
|
||||
return sanitized_name
|
||||
|
||||
|
||||
def _get_llamaparse_parser():
|
||||
from app.engine.loaders import load_configs
|
||||
from app.engine.loaders.file import FileLoaderConfig, llama_parse_parser
|
||||
|
||||
config = load_configs()
|
||||
file_loader_config = FileLoaderConfig(**config["file"])
|
||||
if file_loader_config.use_llama_parse:
|
||||
return llama_parse_parser()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _default_file_loaders_map():
|
||||
default_loaders = get_file_loaders_map()
|
||||
default_loaders[".txt"] = FlatReader
|
||||
default_loaders[".csv"] = FlatReader
|
||||
return default_loaders
|
||||
@@ -16,15 +16,15 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "4.0.3",
|
||||
"ai": "^4.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"duck-duck-scrape": "^2.2.5",
|
||||
"express": "^4.18.2",
|
||||
"llamaindex": "0.8.2",
|
||||
"pdf2json": "3.0.5",
|
||||
"pdf2json": "^3.0.5",
|
||||
"ajv": "^8.12.0",
|
||||
"@e2b/code-interpreter": "0.0.9-beta.3",
|
||||
"@e2b/code-interpreter": "^1.0.4",
|
||||
"got": "^14.4.1",
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"formdata-node": "^6.0.3",
|
||||
@@ -45,7 +45,7 @@
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"tsx": "^4.7.2",
|
||||
"tsup": "8.1.0",
|
||||
"tsup": "^8.1.0",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
CodeInterpreter,
|
||||
ExecutionError,
|
||||
Result,
|
||||
Sandbox,
|
||||
} from "@e2b/code-interpreter";
|
||||
import { ExecutionError, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import { Request, Response } from "express";
|
||||
import path from "node:path";
|
||||
import { saveDocument } from "./llamaindex/documents/helper";
|
||||
|
||||
type CodeArtifact = {
|
||||
@@ -39,6 +35,8 @@ const sandboxTimeout = 10 * 60 * 1000; // 10 minute in ms
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const OUTPUT_DIR = path.join("output", "tools");
|
||||
|
||||
export type ExecutionResult = {
|
||||
template: string;
|
||||
stdout: string[];
|
||||
@@ -48,60 +46,53 @@ export type ExecutionResult = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// see https://github.com/e2b-dev/fragments/tree/main/sandbox-templates
|
||||
const SUPPORTED_TEMPLATES = [
|
||||
"nextjs-developer",
|
||||
"vue-developer",
|
||||
"streamlit-developer",
|
||||
"gradio-developer",
|
||||
];
|
||||
|
||||
export const sandbox = async (req: Request, res: Response) => {
|
||||
const { artifact }: { artifact: CodeArtifact } = req.body;
|
||||
|
||||
let sbx: Sandbox | CodeInterpreter | undefined = undefined;
|
||||
|
||||
// Create a interpreter or a sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
sbx = await CodeInterpreter.create({
|
||||
metadata: { template: artifact.template },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created code interpreter", sbx.sandboxID);
|
||||
let sbx: Sandbox;
|
||||
const sandboxOpts = {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
};
|
||||
if (SUPPORTED_TEMPLATES.includes(artifact.template)) {
|
||||
sbx = await Sandbox.create(artifact.template, sandboxOpts);
|
||||
} else {
|
||||
sbx = await Sandbox.create(artifact.template, {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created sandbox", sbx.sandboxID);
|
||||
sbx = await Sandbox.create(sandboxOpts);
|
||||
}
|
||||
console.log("Created sandbox", sbx.sandboxId);
|
||||
|
||||
// Install packages
|
||||
if (artifact.has_additional_dependencies) {
|
||||
if (sbx instanceof CodeInterpreter) {
|
||||
await sbx.notebook.execCell(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in code interpreter ${sbx.sandboxID}`,
|
||||
);
|
||||
} else if (sbx instanceof Sandbox) {
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxID}`,
|
||||
);
|
||||
}
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Copy code to fs
|
||||
if (artifact.code && Array.isArray(artifact.code)) {
|
||||
artifact.code.forEach(async (file) => {
|
||||
await sbx.files.write(file.file_path, file.file_content);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxId}`);
|
||||
});
|
||||
} else {
|
||||
await sbx.files.write(artifact.file_path, artifact.code);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxId}`);
|
||||
}
|
||||
|
||||
// Execute code or return a URL to the running sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
const result = await (sbx as CodeInterpreter).notebook.execCell(
|
||||
artifact.code || "",
|
||||
);
|
||||
await (sbx as CodeInterpreter).close();
|
||||
const result = await sbx.runCode(artifact.code || "");
|
||||
await sbx.kill();
|
||||
const outputUrls = await downloadCellResults(result.results);
|
||||
|
||||
return res.status(200).json({
|
||||
template: artifact.template,
|
||||
stdout: result.logs.stdout,
|
||||
@@ -121,17 +112,23 @@ async function downloadCellResults(
|
||||
cellResults?: Result[],
|
||||
): Promise<Array<{ url: string; filename: string }>> {
|
||||
if (!cellResults) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
cellResults.map(async (res) => {
|
||||
const formats = res.formats(); // available formats in the result
|
||||
const formatResults = await Promise.all(
|
||||
formats.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(filename, buffer);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
formats
|
||||
.filter((ext) => ["png", "svg", "jpeg", "pdf"].includes(ext))
|
||||
.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(
|
||||
path.join(OUTPUT_DIR, filename),
|
||||
buffer,
|
||||
);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
);
|
||||
return formatResults;
|
||||
}),
|
||||
|
||||
@@ -14,10 +14,33 @@ dotenv.load_dotenv()
|
||||
|
||||
|
||||
FRONTEND_DIR = Path(os.getenv("FRONTEND_DIR", ".frontend"))
|
||||
DEFAULT_FRONTEND_PORT = 3000
|
||||
APP_HOST = os.getenv("APP_HOST", "localhost")
|
||||
APP_PORT = int(
|
||||
os.getenv("APP_PORT", 8000)
|
||||
) # Allocated to backend but also for access to the app, please change it in .env
|
||||
DEFAULT_FRONTEND_PORT = (
|
||||
3000 # Not for access directly, but for proxying to the backend in development
|
||||
)
|
||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", "static"))
|
||||
|
||||
|
||||
class NodePackageManager(str):
|
||||
def __new__(cls, value: str) -> "NodePackageManager":
|
||||
return super().__new__(cls, value)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return Path(self).stem
|
||||
|
||||
@property
|
||||
def is_pnpm(self) -> bool:
|
||||
return self.name == "pnpm"
|
||||
|
||||
@property
|
||||
def is_npm(self) -> bool:
|
||||
return self.name == "npm"
|
||||
|
||||
|
||||
def build():
|
||||
"""
|
||||
Build the frontend and copy the static files to the backend.
|
||||
@@ -146,22 +169,20 @@ async def _run_frontend(
|
||||
package_manager,
|
||||
"run",
|
||||
"dev",
|
||||
"--" if package_manager.is_npm else "",
|
||||
"-p",
|
||||
str(port),
|
||||
cwd=FRONTEND_DIR,
|
||||
)
|
||||
rich.print(
|
||||
f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]"
|
||||
)
|
||||
rich.print("\n[bold]Waiting for frontend to start...")
|
||||
# Block until the frontend is accessible
|
||||
for _ in range(timeout):
|
||||
await asyncio.sleep(1)
|
||||
# Check if the frontend is accessible (port is open) or frontend_process is running
|
||||
if frontend_process.returncode is not None:
|
||||
raise RuntimeError("Could not start frontend dev server")
|
||||
if not _is_bindable_port(port):
|
||||
if _is_server_running(port):
|
||||
rich.print(
|
||||
f"\n[bold green]Frontend dev server is running on port {port}[/bold green]"
|
||||
"\n[bold]Frontend dev server is running. Please wait a while for the app to be ready...[/bold]"
|
||||
)
|
||||
return frontend_process, port
|
||||
raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds")
|
||||
@@ -173,33 +194,50 @@ async def _run_backend(
|
||||
"""
|
||||
Start the backend development server.
|
||||
|
||||
Args:
|
||||
frontend_port: The port number the frontend is running on
|
||||
Returns:
|
||||
Process: The backend process
|
||||
"""
|
||||
# Merge environment variables
|
||||
envs = {**os.environ, **(envs or {})}
|
||||
rich.print("\n[bold]Starting backend FastAPI server...[/bold]")
|
||||
# Check if the port is free
|
||||
if not _is_port_available(APP_PORT):
|
||||
raise SystemError(
|
||||
f"Port {APP_PORT} is not available! Please change the port in .env file or kill the process running on this port."
|
||||
)
|
||||
rich.print(f"\n[bold]Starting app on port {APP_PORT}...[/bold]")
|
||||
poetry_executable = _get_poetry_executable()
|
||||
return await asyncio.create_subprocess_exec(
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
poetry_executable,
|
||||
"run",
|
||||
"python",
|
||||
"main.py",
|
||||
env=envs,
|
||||
)
|
||||
# Wait for port is started
|
||||
timeout = 30
|
||||
for _ in range(timeout):
|
||||
await asyncio.sleep(1)
|
||||
if process.returncode is not None:
|
||||
raise RuntimeError("Could not start backend dev server")
|
||||
if _is_server_running(APP_PORT):
|
||||
rich.print(
|
||||
f"\n[bold green]App is running. You now can access it at http://{APP_HOST}:{APP_PORT}[/bold green]"
|
||||
)
|
||||
return process
|
||||
# Timeout, kill the process
|
||||
process.terminate()
|
||||
raise TimeoutError(f"Backend dev server failed to start within {timeout} seconds")
|
||||
|
||||
|
||||
def _install_frontend_dependencies():
|
||||
package_manager = _get_node_package_manager()
|
||||
rich.print(
|
||||
f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]"
|
||||
f"\n[bold]Installing frontend dependencies using {package_manager.name}. It might take a while...[/bold]"
|
||||
)
|
||||
run([package_manager, "install"], cwd=".frontend", check=True)
|
||||
|
||||
|
||||
def _get_node_package_manager() -> str:
|
||||
def _get_node_package_manager() -> NodePackageManager:
|
||||
"""
|
||||
Check for available package managers and return the preferred one.
|
||||
Returns 'pnpm' if installed, falls back to 'npm'.
|
||||
@@ -215,12 +253,12 @@ def _get_node_package_manager() -> str:
|
||||
for cmd in pnpm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
return NodePackageManager(cmd_path)
|
||||
|
||||
for cmd in npm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
return NodePackageManager(cmd_path)
|
||||
|
||||
raise SystemError(
|
||||
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
|
||||
@@ -244,28 +282,27 @@ def _get_poetry_executable() -> str:
|
||||
raise SystemError("Poetry is not installed. Please install Poetry first.")
|
||||
|
||||
|
||||
def _is_bindable_port(port: int) -> bool:
|
||||
"""Check if a port is available by attempting to connect to it."""
|
||||
def _is_port_available(port: int) -> bool:
|
||||
"""Check if a port is available for binding."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
# Try to connect to the port
|
||||
s.connect(("localhost", port))
|
||||
# If we can connect, port is in use
|
||||
return False
|
||||
return False # Port is in use, so not available
|
||||
except ConnectionRefusedError:
|
||||
# Connection refused means port is available
|
||||
return True
|
||||
return True # Port is available
|
||||
except socket.error:
|
||||
# Other socket errors also likely mean port is available
|
||||
return True
|
||||
return True # Other socket errors likely mean port is available
|
||||
|
||||
|
||||
def _is_server_running(port: int) -> bool:
|
||||
"""Check if a server is running on the specified port."""
|
||||
return not _is_port_available(port)
|
||||
|
||||
|
||||
def _find_free_port(start_port: int) -> int:
|
||||
"""
|
||||
Find a free port starting from the given port number.
|
||||
"""
|
||||
"""Find a free port starting from the given port number."""
|
||||
for port in range(start_port, 65535):
|
||||
if _is_bindable_port(port):
|
||||
if _is_port_available(port):
|
||||
return port
|
||||
raise SystemError("No free port found")
|
||||
|
||||
|
||||
@@ -13,12 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
CodeInterpreter,
|
||||
ExecutionError,
|
||||
Result,
|
||||
Sandbox,
|
||||
} from "@e2b/code-interpreter";
|
||||
import { ExecutionError, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { saveDocument } from "../chat/llamaindex/documents/helper";
|
||||
@@ -41,6 +36,8 @@ const sandboxTimeout = 10 * 60 * 1000; // 10 minute in ms
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const OUTPUT_DIR = path.join("output", "tools");
|
||||
|
||||
export type ExecutionResult = {
|
||||
template: string;
|
||||
stdout: string[];
|
||||
@@ -50,39 +47,35 @@ export type ExecutionResult = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// see https://github.com/e2b-dev/fragments/tree/main/sandbox-templates
|
||||
const SUPPORTED_TEMPLATES = [
|
||||
"nextjs-developer",
|
||||
"vue-developer",
|
||||
"streamlit-developer",
|
||||
"gradio-developer",
|
||||
];
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { artifact }: { artifact: CodeArtifact } = await req.json();
|
||||
|
||||
let sbx: Sandbox | CodeInterpreter | undefined = undefined;
|
||||
|
||||
// Create a interpreter or a sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
sbx = await CodeInterpreter.create({
|
||||
metadata: { template: artifact.template },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created code interpreter", sbx.sandboxID);
|
||||
let sbx: Sandbox;
|
||||
const sandboxOpts = {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
};
|
||||
if (SUPPORTED_TEMPLATES.includes(artifact.template)) {
|
||||
sbx = await Sandbox.create(artifact.template, sandboxOpts);
|
||||
} else {
|
||||
sbx = await Sandbox.create(artifact.template, {
|
||||
metadata: { template: artifact.template, userID: "default" },
|
||||
timeoutMs: sandboxTimeout,
|
||||
});
|
||||
console.log("Created sandbox", sbx.sandboxID);
|
||||
sbx = await Sandbox.create(sandboxOpts);
|
||||
}
|
||||
console.log("Created sandbox", sbx.sandboxId);
|
||||
|
||||
// Install packages
|
||||
if (artifact.has_additional_dependencies) {
|
||||
if (sbx instanceof CodeInterpreter) {
|
||||
await sbx.notebook.execCell(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in code interpreter ${sbx.sandboxID}`,
|
||||
);
|
||||
} else if (sbx instanceof Sandbox) {
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxID}`,
|
||||
);
|
||||
}
|
||||
await sbx.commands.run(artifact.install_dependencies_command);
|
||||
console.log(
|
||||
`Installed dependencies: ${artifact.additional_dependencies.join(", ")} in sandbox ${sbx.sandboxId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Copy files
|
||||
@@ -94,7 +87,7 @@ export async function POST(req: Request) {
|
||||
|
||||
const arrayBuffer = new Uint8Array(fileContent).buffer;
|
||||
await sbx.files.write(sandboxFilePath, arrayBuffer);
|
||||
console.log(`Copied file to ${sandboxFilePath} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${sandboxFilePath} in ${sbx.sandboxId}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,19 +95,17 @@ export async function POST(req: Request) {
|
||||
if (artifact.code && Array.isArray(artifact.code)) {
|
||||
artifact.code.forEach(async (file) => {
|
||||
await sbx.files.write(file.file_path, file.file_content);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${file.file_path} in ${sbx.sandboxId}`);
|
||||
});
|
||||
} else {
|
||||
await sbx.files.write(artifact.file_path, artifact.code);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxID}`);
|
||||
console.log(`Copied file to ${artifact.file_path} in ${sbx.sandboxId}`);
|
||||
}
|
||||
|
||||
// Execute code or return a URL to the running sandbox
|
||||
if (artifact.template === "code-interpreter-multilang") {
|
||||
const result = await (sbx as CodeInterpreter).notebook.execCell(
|
||||
artifact.code || "",
|
||||
);
|
||||
await (sbx as CodeInterpreter).close();
|
||||
const result = await sbx.runCode(artifact.code || "");
|
||||
await sbx.kill();
|
||||
const outputUrls = await downloadCellResults(result.results);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
@@ -139,17 +130,23 @@ async function downloadCellResults(
|
||||
cellResults?: Result[],
|
||||
): Promise<Array<{ url: string; filename: string }>> {
|
||||
if (!cellResults) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
cellResults.map(async (res) => {
|
||||
const formats = res.formats(); // available formats in the result
|
||||
const formatResults = await Promise.all(
|
||||
formats.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(filename, buffer);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
formats
|
||||
.filter((ext) => ["png", "svg", "jpeg", "pdf"].includes(ext))
|
||||
.map(async (ext) => {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const base64 = res[ext as keyof Result];
|
||||
const buffer = Buffer.from(base64, "base64");
|
||||
const fileurl = await saveDocument(
|
||||
path.join(OUTPUT_DIR, filename),
|
||||
buffer,
|
||||
);
|
||||
return { url: fileurl, filename };
|
||||
}),
|
||||
);
|
||||
return formatResults;
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useChatMessage,
|
||||
useChatUI,
|
||||
} from "@llamaindex/chat-ui";
|
||||
import { DeepResearchCard } from "./custom/deep-research-card";
|
||||
import { Markdown } from "./custom/markdown";
|
||||
import { ToolAnnotations } from "./tools/chat-tools";
|
||||
|
||||
@@ -22,6 +23,11 @@ export function ChatMessageContent() {
|
||||
/>
|
||||
),
|
||||
},
|
||||
// add the deep research card
|
||||
{
|
||||
position: ContentPosition.CHAT_EVENTS,
|
||||
component: <DeepResearchCard message={message} />,
|
||||
},
|
||||
{
|
||||
// add the tool annotations after events
|
||||
position: ContentPosition.AFTER_EVENTS,
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { Message } from "@llamaindex/chat-ui";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
CircleDashed,
|
||||
Clock,
|
||||
NotebookPen,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../../collapsible";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Markdown } from "./markdown";
|
||||
|
||||
// Streaming event types
|
||||
type EventState = "pending" | "inprogress" | "done" | "error";
|
||||
|
||||
type DeepResearchEvent = {
|
||||
type: "deep_research_event";
|
||||
data: {
|
||||
event: "retrieve" | "analyze" | "answer";
|
||||
state: EventState;
|
||||
id?: string;
|
||||
question?: string;
|
||||
answer?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
// UI state types
|
||||
type QuestionState = {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string | null;
|
||||
state: EventState;
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
type DeepResearchCardState = {
|
||||
retrieve: {
|
||||
state: EventState | null;
|
||||
};
|
||||
analyze: {
|
||||
state: EventState | null;
|
||||
questions: QuestionState[];
|
||||
};
|
||||
};
|
||||
|
||||
interface DeepResearchCardProps {
|
||||
message: Message;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const stateIcon: Record<EventState, React.ReactNode> = {
|
||||
pending: <Clock className="h-4 w-4 text-yellow-500" />,
|
||||
inprogress: <CircleDashed className="h-4 w-4 text-blue-500 animate-spin" />,
|
||||
done: <CheckCircle2 className="h-4 w-4 text-green-500" />,
|
||||
error: <AlertCircle className="h-4 w-4 text-red-500" />,
|
||||
};
|
||||
|
||||
// Transform the state based on the event without mutations
|
||||
const transformState = (
|
||||
state: DeepResearchCardState,
|
||||
event: DeepResearchEvent,
|
||||
): DeepResearchCardState => {
|
||||
switch (event.data.event) {
|
||||
case "answer": {
|
||||
const { id, question, answer } = event.data;
|
||||
if (!id || !question) return state;
|
||||
|
||||
const updatedQuestions = state.analyze.questions.map((q) => {
|
||||
if (q.id !== id) return q;
|
||||
return {
|
||||
...q,
|
||||
state: event.data.state,
|
||||
answer: answer ?? q.answer,
|
||||
};
|
||||
});
|
||||
|
||||
const newQuestion = !state.analyze.questions.some((q) => q.id === id)
|
||||
? [
|
||||
{
|
||||
id,
|
||||
question,
|
||||
answer: answer ?? null,
|
||||
state: event.data.state,
|
||||
isOpen: false,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
...state,
|
||||
analyze: {
|
||||
...state.analyze,
|
||||
questions: [...updatedQuestions, ...newQuestion],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case "retrieve":
|
||||
case "analyze":
|
||||
return {
|
||||
...state,
|
||||
[event.data.event]: {
|
||||
...state[event.data.event],
|
||||
state: event.data.state,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Convert deep research events to state
|
||||
const deepResearchEventsToState = (
|
||||
events: DeepResearchEvent[] | undefined,
|
||||
): DeepResearchCardState => {
|
||||
if (!events?.length) {
|
||||
return {
|
||||
retrieve: { state: null },
|
||||
analyze: { state: null, questions: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: DeepResearchCardState = {
|
||||
retrieve: { state: null },
|
||||
analyze: { state: null, questions: [] },
|
||||
};
|
||||
|
||||
return events.reduce(
|
||||
(acc: DeepResearchCardState, event: DeepResearchEvent) =>
|
||||
transformState(acc, event),
|
||||
initialState,
|
||||
);
|
||||
};
|
||||
|
||||
export function DeepResearchCard({
|
||||
message,
|
||||
className,
|
||||
}: DeepResearchCardProps) {
|
||||
const deepResearchEvents = message.annotations as
|
||||
| DeepResearchEvent[]
|
||||
| undefined;
|
||||
const hasDeepResearchEvents = deepResearchEvents?.some(
|
||||
(event) => event.type === "deep_research_event",
|
||||
);
|
||||
|
||||
const state = useMemo(
|
||||
() => deepResearchEventsToState(deepResearchEvents),
|
||||
[deepResearchEvents],
|
||||
);
|
||||
|
||||
if (!hasDeepResearchEvents) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm p-5 space-y-6 w-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{state.retrieve.state !== null && (
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Search className="h-5 w-5" />
|
||||
<span>
|
||||
{state.retrieve.state === "inprogress"
|
||||
? "Searching..."
|
||||
: "Search completed"}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.analyze.state !== null && (
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<NotebookPen className="h-5 w-5" />
|
||||
<span>
|
||||
{state.analyze.state === "inprogress"
|
||||
? "Analyzing..."
|
||||
: "Analysis"}
|
||||
</span>
|
||||
</h3>
|
||||
{state.analyze.questions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{state.analyze.questions.map((question: QuestionState) => (
|
||||
<Collapsible key={question.id}>
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="flex items-center gap-2 p-3 hover:bg-accent transition-colors rounded-lg border">
|
||||
<div className="flex-shrink-0">
|
||||
{stateIcon[question.state]}
|
||||
</div>
|
||||
<span className="font-medium text-left flex-1">
|
||||
{question.question}
|
||||
</span>
|
||||
<ChevronDown className="h-5 w-5 transition-transform ui-expanded:rotate-180" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
{question.answer && (
|
||||
<CollapsibleContent>
|
||||
<div className="p-3 border border-t-0 rounded-b-lg">
|
||||
<Markdown content={question.answer} />
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export interface WeatherData {
|
||||
const weatherCodeDisplayMap: Record<
|
||||
string,
|
||||
{
|
||||
icon: JSX.Element;
|
||||
icon: React.ReactNode;
|
||||
status: string;
|
||||
}
|
||||
> = {
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@e2b/code-interpreter": "0.0.9-beta.3",
|
||||
"@e2b/code-interpreter": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@llamaindex/chat-ui": "0.0.12",
|
||||
"ai": "4.0.3",
|
||||
"@llamaindex/chat-ui": "0.0.14",
|
||||
"ai": "^4.0.3",
|
||||
"ajv": "^8.12.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -27,9 +27,9 @@
|
||||
"got": "^14.4.1",
|
||||
"llamaindex": "0.8.2",
|
||||
"lucide-react": "^0.460.0",
|
||||
"next": "^15.0.3",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"next": "^15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"supports-color": "^8.1.1",
|
||||
"tailwind-merge": "^2.1.0",
|
||||
@@ -39,15 +39,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@llamaindex/workflow": "^0.0.3",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"eslint-config-next": "^15.1.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
Reference in New Issue
Block a user