mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-18 21:14:37 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f17449b90a | |||
| 28c8808ce3 | |||
| 0a7dfcf84b | |||
| 6e70e327d3 | |||
| 8b371d8347 | |||
| 30fe269575 | |||
| 49c35b834b | |||
| 82c2580ee5 | |||
| fc5b266a40 | |||
| f8f97d2c00 |
@@ -1,5 +1,27 @@
|
||||
# create-llama
|
||||
|
||||
## 0.3.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 28c8808: Add fly.io deployment
|
||||
- 0a7dfcf: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend
|
||||
|
||||
## 0.3.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8b371d8: Set pydantic version to <2.10 to avoid incompatibility with llama-index.
|
||||
- 30fe269: Deactive duckduckgo tool for TS
|
||||
- 30fe269: Replace DuckDuckGo by Wikipedia tool for agentic template
|
||||
|
||||
## 0.3.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fc5b266: Improve DX for Python template (use one deployment instead of two)
|
||||
- f8f97d2: Add support for python 3.13
|
||||
|
||||
## 0.3.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+8
-20
@@ -7,17 +7,16 @@ import { getOnline } from "./helpers/is-online";
|
||||
import { isWriteable } from "./helpers/is-writeable";
|
||||
import { makeDir } from "./helpers/make-dir";
|
||||
|
||||
import fs from "fs";
|
||||
import terminalLink from "terminal-link";
|
||||
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
|
||||
import { installTemplate } from "./helpers";
|
||||
import { writeDevcontainer } from "./helpers/devcontainer";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { toolsRequireConfig } from "./helpers/tools";
|
||||
import { configVSCode } from "./helpers/vscode";
|
||||
|
||||
export type InstallAppArgs = Omit<
|
||||
InstallTemplateArgs,
|
||||
"appName" | "root" | "isOnline" | "customApiPath"
|
||||
"appName" | "root" | "isOnline" | "port"
|
||||
> & {
|
||||
appPath: string;
|
||||
frontend: boolean;
|
||||
@@ -35,7 +34,6 @@ export async function createApp({
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
@@ -81,7 +79,6 @@ export async function createApp({
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
@@ -90,31 +87,22 @@ export async function createApp({
|
||||
agents,
|
||||
};
|
||||
|
||||
if (frontend) {
|
||||
// install backend
|
||||
const backendRoot = path.join(root, "backend");
|
||||
await makeDir(backendRoot);
|
||||
await installTemplate({ ...args, root: backendRoot, backend: true });
|
||||
// Install backend
|
||||
await installTemplate({ ...args, backend: true });
|
||||
|
||||
if (frontend && framework === "fastapi") {
|
||||
// install frontend
|
||||
const frontendRoot = path.join(root, "frontend");
|
||||
const frontendRoot = path.join(root, ".frontend");
|
||||
await makeDir(frontendRoot);
|
||||
await installTemplate({
|
||||
...args,
|
||||
root: frontendRoot,
|
||||
framework: "nextjs",
|
||||
customApiPath: `http://localhost:${externalPort ?? 8000}/api/chat`,
|
||||
backend: false,
|
||||
});
|
||||
// copy readme for fullstack
|
||||
await fs.promises.copyFile(
|
||||
path.join(templatesDir, "README-fullstack.md"),
|
||||
path.join(root, "README.md"),
|
||||
);
|
||||
} else {
|
||||
await installTemplate({ ...args, backend: true });
|
||||
}
|
||||
|
||||
await writeDevcontainer(root, templatesDir, framework, frontend);
|
||||
await configVSCode(root, templatesDir, framework);
|
||||
|
||||
process.chdir(root);
|
||||
if (tryGitInit(root)) {
|
||||
|
||||
@@ -63,7 +63,6 @@ if (
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
@@ -101,7 +100,6 @@ if (
|
||||
vectorDb: "none",
|
||||
tools: tool,
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
@@ -135,7 +133,6 @@ if (
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
@@ -169,7 +166,6 @@ if (
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
|
||||
@@ -20,8 +20,7 @@ if (
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
test.describe("Test extractor template", async () => {
|
||||
let frontendPort: number;
|
||||
let backendPort: number;
|
||||
let appPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
let cwd: string;
|
||||
@@ -29,16 +28,14 @@ if (
|
||||
// Create extractor app
|
||||
test.beforeAll(async () => {
|
||||
cwd = await createTestDir();
|
||||
frontendPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
backendPort = frontendPort + 1;
|
||||
appPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "extractor",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: frontendPort,
|
||||
externalPort: backendPort,
|
||||
port: appPort,
|
||||
postInstallAction: "runApp",
|
||||
});
|
||||
name = result.projectName;
|
||||
@@ -54,7 +51,7 @@ if (
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${frontendPort}`);
|
||||
await page.goto(`http://localhost:${appPort}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 2000 * 60,
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
const dataSource: string = "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateAgents = ["financial_report", "blog", "form_filling"];
|
||||
|
||||
@@ -27,7 +27,6 @@ for (const agents of templateAgents) {
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
@@ -36,7 +35,6 @@ for (const agents of templateAgents) {
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
@@ -45,7 +43,6 @@ for (const agents of templateAgents) {
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -61,6 +58,10 @@ for (const agents of templateAgents) {
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
@@ -69,7 +70,10 @@ for (const agents of templateAgents) {
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
agents === "financial_report" || agents === "form_filling",
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
agents === "financial_report" ||
|
||||
agents === "form_filling" ||
|
||||
templateFramework === "express",
|
||||
"Skip chat tests for financial report and form filling.",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
|
||||
@@ -22,7 +22,7 @@ const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
|
||||
@@ -35,7 +35,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
}
|
||||
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
@@ -44,7 +43,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
@@ -53,7 +51,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -68,8 +65,11 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
@@ -77,7 +77,9 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
@@ -102,7 +104,7 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${externalPort}/api/chat/request`,
|
||||
`http://localhost:${port}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
|
||||
@@ -56,7 +56,6 @@ test.describe("Test resolve TS dependencies", () => {
|
||||
dataSource: dataSource,
|
||||
vectorDb: vectorDb,
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
|
||||
+1
-23
@@ -25,7 +25,6 @@ export type RunCreateLlamaOptions = {
|
||||
dataSource: string;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
externalPort: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
templateUI?: TemplateUI;
|
||||
appType?: AppType;
|
||||
@@ -44,7 +43,6 @@ export async function runCreateLlama({
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
@@ -93,8 +91,6 @@ export async function runCreateLlama({
|
||||
"--use-pnpm",
|
||||
"--port",
|
||||
port,
|
||||
"--external-port",
|
||||
externalPort,
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--tools",
|
||||
@@ -142,12 +138,7 @@ export async function runCreateLlama({
|
||||
|
||||
// Wait for app to start
|
||||
if (postInstallAction === "runApp") {
|
||||
await checkAppHasStarted(
|
||||
appType === "--frontend",
|
||||
templateFramework,
|
||||
port,
|
||||
externalPort,
|
||||
);
|
||||
await waitPorts([port]);
|
||||
} else if (postInstallAction === "dependencies") {
|
||||
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
|
||||
} else {
|
||||
@@ -167,19 +158,6 @@ export async function createTestDir() {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
async function checkAppHasStarted(
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
) {
|
||||
const portsToWait = frontend
|
||||
? [port, externalPort]
|
||||
: [framework === "nextjs" ? port : externalPort];
|
||||
await waitPorts(portsToWait);
|
||||
}
|
||||
|
||||
async function waitPorts(ports: number[]): Promise<void> {
|
||||
const waitForPort = async (port: number): Promise<void> => {
|
||||
await waitPort({
|
||||
|
||||
@@ -61,6 +61,9 @@ export const assetRelocator = (name: string) => {
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
case "vscode_settings.json": {
|
||||
return "settings.json";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -407,6 +407,13 @@ const getFrameworkEnvs = (
|
||||
],
|
||||
);
|
||||
}
|
||||
if (framework === "nextjs") {
|
||||
result.push({
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description:
|
||||
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -553,7 +560,7 @@ export const createBackendEnvFile = async (
|
||||
| "framework"
|
||||
| "dataSources"
|
||||
| "template"
|
||||
| "externalPort"
|
||||
| "port"
|
||||
| "tools"
|
||||
| "observability"
|
||||
>,
|
||||
@@ -570,7 +577,7 @@ export const createBackendEnvFile = async (
|
||||
...getModelEnvs(opts.modelConfig),
|
||||
...getEngineEnvs(),
|
||||
...getVectorDBEnvs(opts.vectorDb, opts.framework),
|
||||
...getFrameworkEnvs(opts.framework, opts.externalPort),
|
||||
...getFrameworkEnvs(opts.framework, opts.port),
|
||||
...getToolEnvs(opts.tools),
|
||||
...getTemplateEnvs(opts.template),
|
||||
...getObservabilityEnvs(opts.observability),
|
||||
@@ -585,18 +592,10 @@ export const createBackendEnvFile = async (
|
||||
export const createFrontendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
customApiPath?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
},
|
||||
) => {
|
||||
const defaultFrontendEnvs = [
|
||||
{
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description: "The backend API for chat endpoint.",
|
||||
value: opts.customApiPath
|
||||
? opts.customApiPath
|
||||
: "http://localhost:8000/api/chat",
|
||||
},
|
||||
{
|
||||
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
|
||||
description: "Let's the user change indexes in LlamaCloud projects",
|
||||
|
||||
@@ -225,7 +225,6 @@ export const installTemplate = async (
|
||||
} else {
|
||||
// this is a frontend for a full-stack app, create .env file with model information
|
||||
await createFrontendEnvFile(props.root, {
|
||||
customApiPath: props.customApiPath,
|
||||
vectorDb: props.vectorDb,
|
||||
});
|
||||
}
|
||||
|
||||
+17
-7
@@ -20,6 +20,7 @@ interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
constraints?: Record<string, string>;
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
@@ -51,6 +52,9 @@ const getAdditionalDependencies = (
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: "^0.2.1",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -76,6 +80,9 @@ const getAdditionalDependencies = (
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-qdrant",
|
||||
version: "^0.3.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -279,14 +286,19 @@ const mergePoetryDependencies = (
|
||||
value.version = dependency.version ?? value.version;
|
||||
value.extras = dependency.extras ?? value.extras;
|
||||
|
||||
// Merge constraints if they exist
|
||||
if (dependency.constraints) {
|
||||
value = { ...value, ...dependency.constraints };
|
||||
}
|
||||
|
||||
if (value.version === undefined) {
|
||||
throw new Error(
|
||||
`Dependency "${dependency.name}" is missing attribute "version"!`,
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize separately only if extras are provided
|
||||
if (value.extras && value.extras.length > 0) {
|
||||
// Serialize as object if there are any additional properties
|
||||
if (Object.keys(value).length > 1) {
|
||||
existingDependencies[dependency.name] = value;
|
||||
} else {
|
||||
// Otherwise, serialize just the version string
|
||||
@@ -513,6 +525,9 @@ export const installPythonTemplate = async ({
|
||||
addOnDependencies.push({
|
||||
name: "llama-index-callbacks-arize-phoenix",
|
||||
version: "^0.2.1",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -533,9 +548,4 @@ export const installPythonTemplate = async ({
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
installPythonDependencies();
|
||||
}
|
||||
|
||||
// Copy deployment files for python
|
||||
await copy("**", root, {
|
||||
cwd: path.join(compPath, "deployments", "python"),
|
||||
});
|
||||
};
|
||||
|
||||
+44
-62
@@ -1,40 +1,39 @@
|
||||
import { ChildProcess, SpawnOptions, spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { SpawnOptions, spawn } from "child_process";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
const createProcess = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: SpawnOptions,
|
||||
) => {
|
||||
return spawn(command, args, {
|
||||
...options,
|
||||
shell: true,
|
||||
})
|
||||
.on("exit", function (code) {
|
||||
if (code !== 0) {
|
||||
console.log(`Child process exited with code=${code}`);
|
||||
process.exit(1);
|
||||
}
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
spawn(command, args, {
|
||||
...options,
|
||||
shell: true,
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.log("Error when running chill process: ", err);
|
||||
process.exit(1);
|
||||
});
|
||||
.on("exit", function (code) {
|
||||
if (code !== 0) {
|
||||
console.log(`Child process exited with code=${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.log("Error when running child process: ", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function runReflexApp(
|
||||
appPath: string,
|
||||
frontendPort?: number,
|
||||
backendPort?: number,
|
||||
) {
|
||||
const commandArgs = ["run", "reflex", "run"];
|
||||
if (frontendPort) {
|
||||
commandArgs.push("--frontend-port", frontendPort.toString());
|
||||
}
|
||||
if (backendPort) {
|
||||
commandArgs.push("--backend-port", backendPort.toString());
|
||||
}
|
||||
export function runReflexApp(appPath: string, port: number) {
|
||||
const commandArgs = [
|
||||
"run",
|
||||
"reflex",
|
||||
"run",
|
||||
"--frontend-port",
|
||||
port.toString(),
|
||||
];
|
||||
return createProcess("poetry", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
@@ -42,11 +41,10 @@ export function runReflexApp(
|
||||
}
|
||||
|
||||
export function runFastAPIApp(appPath: string, port: number) {
|
||||
const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port];
|
||||
|
||||
return createProcess("poetry", commandArgs, {
|
||||
return createProcess("poetry", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
env: { ...process.env, APP_PORT: `${port}` },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,39 +59,23 @@ export function runTSApp(appPath: string, port: number) {
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
template: string,
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
externalPort?: number,
|
||||
): Promise<any> {
|
||||
const processes: ChildProcess[] = [];
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort =
|
||||
framework === "nextjs" || template === "extractor" ? 3000 : 8000;
|
||||
|
||||
// Callback to kill all sub processes if the main process is killed
|
||||
process.on("exit", () => {
|
||||
console.log("Killing app processes...");
|
||||
processes.forEach((p) => p.kill());
|
||||
});
|
||||
|
||||
// Default sub app paths
|
||||
const backendPath = path.join(appPath, "backend");
|
||||
const frontendPath = path.join(appPath, "frontend");
|
||||
|
||||
if (template === "extractor") {
|
||||
processes.push(runReflexApp(appPath, port, externalPort));
|
||||
const appRunner =
|
||||
template === "extractor"
|
||||
? runReflexApp
|
||||
: framework === "fastapi"
|
||||
? runFastAPIApp
|
||||
: runTSApp;
|
||||
await appRunner(appPath, port || defaultPort);
|
||||
} catch (error) {
|
||||
console.error("Failed to run app:", error);
|
||||
throw error;
|
||||
}
|
||||
if (template === "streaming" || template === "multiagent") {
|
||||
if (framework === "fastapi" || framework === "express") {
|
||||
const backendRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
|
||||
if (frontend) {
|
||||
processes.push(backendRunner(backendPath, externalPort || 8000));
|
||||
processes.push(runTSApp(frontendPath, port || 3000));
|
||||
} else {
|
||||
processes.push(backendRunner(appPath, externalPort || 8000));
|
||||
}
|
||||
} else if (framework === "nextjs") {
|
||||
processes.push(runTSApp(appPath, port || 3000));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(processes);
|
||||
}
|
||||
|
||||
+2
-2
@@ -62,10 +62,10 @@ export const supportedTools: Tool[] = [
|
||||
dependencies: [
|
||||
{
|
||||
name: "duckduckgo-search",
|
||||
version: "6.1.7",
|
||||
version: "^6.3.5",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
|
||||
+1
-2
@@ -89,14 +89,13 @@ export interface InstallTemplateArgs {
|
||||
framework: TemplateFramework;
|
||||
ui: TemplateUI;
|
||||
dataSources: TemplateDataSource[];
|
||||
customApiPath?: string;
|
||||
modelConfig: ModelConfig;
|
||||
llamaCloudKey?: string;
|
||||
useLlamaParse?: boolean;
|
||||
communityProjectConfig?: CommunityProjectConfig;
|
||||
llamapack?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
externalPort?: number;
|
||||
port?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
|
||||
@@ -241,14 +241,12 @@ export const installTSTemplate = async ({
|
||||
vectorDb,
|
||||
});
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
if (
|
||||
backend &&
|
||||
(postInstallAction === "runApp" || postInstallAction === "dependencies")
|
||||
) {
|
||||
await installTSDependencies(packageJson, packageManager, isOnline);
|
||||
}
|
||||
|
||||
// Copy deployment files for typescript
|
||||
await copy("**", root, {
|
||||
cwd: path.join(compPath, "deployments", "typescript"),
|
||||
});
|
||||
};
|
||||
|
||||
async function updatePackageJson({
|
||||
|
||||
@@ -1,40 +1,26 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { assetRelocator, copy } from "./copy";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
function renderDevcontainerContent(
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
frontend: boolean,
|
||||
) {
|
||||
const devcontainerJson: any = JSON.parse(
|
||||
fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
|
||||
);
|
||||
|
||||
// Modify postCreateCommand
|
||||
if (frontend) {
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi"
|
||||
? "cd backend && poetry install && cd ../frontend && npm install"
|
||||
: "cd backend && npm install && cd ../frontend && npm install";
|
||||
} else {
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi" ? "poetry install" : "npm install";
|
||||
}
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi" ? "poetry install" : "npm install";
|
||||
|
||||
// Modify containerEnv
|
||||
if (framework === "fastapi") {
|
||||
if (frontend) {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
|
||||
};
|
||||
} else {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
|
||||
};
|
||||
}
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(devcontainerJson, null, 2);
|
||||
@@ -44,7 +30,6 @@ export const writeDevcontainer = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
frontend: boolean,
|
||||
) => {
|
||||
const devcontainerDir = path.join(root, ".devcontainer");
|
||||
if (fs.existsSync(devcontainerDir)) {
|
||||
@@ -54,7 +39,6 @@ export const writeDevcontainer = async (
|
||||
const devcontainerContent = renderDevcontainerContent(
|
||||
templatesDir,
|
||||
framework,
|
||||
frontend,
|
||||
);
|
||||
fs.mkdirSync(devcontainerDir);
|
||||
await fs.promises.writeFile(
|
||||
@@ -62,3 +46,25 @@ export const writeDevcontainer = async (
|
||||
devcontainerContent,
|
||||
);
|
||||
};
|
||||
|
||||
export const copyVSCodeSettings = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
) => {
|
||||
const vscodeDir = path.join(root, ".vscode");
|
||||
await copy("vscode_settings.json", vscodeDir, {
|
||||
cwd: templatesDir,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
};
|
||||
|
||||
export const configVSCode = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
) => {
|
||||
await writeDevcontainer(root, templatesDir, framework);
|
||||
if (framework === "fastapi") {
|
||||
await copyVSCodeSettings(root, templatesDir);
|
||||
}
|
||||
};
|
||||
@@ -134,13 +134,6 @@ const program = new Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select UI port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--external-port <external>",
|
||||
`
|
||||
|
||||
Select external port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -333,7 +326,6 @@ async function run(): Promise<void> {
|
||||
...answers,
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
externalPort: options.externalPort,
|
||||
});
|
||||
|
||||
if (answers.postInstallAction === "VSCode") {
|
||||
@@ -362,14 +354,7 @@ Please check ${cyan(
|
||||
}
|
||||
} else if (answers.postInstallAction === "runApp") {
|
||||
console.log(`Running app in ${root}...`);
|
||||
await runApp(
|
||||
root,
|
||||
answers.template,
|
||||
answers.frontend,
|
||||
answers.framework,
|
||||
options.port,
|
||||
options.externalPort,
|
||||
);
|
||||
await runApp(root, answers.template, answers.framework, options.port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.3.14",
|
||||
"version": "0.3.17",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
+3
-10
@@ -1,4 +1,4 @@
|
||||
import { blue, green } from "picocolors";
|
||||
import { blue } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { isCI } from ".";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
|
||||
@@ -123,24 +123,17 @@ export const askProQuestions = async (program: QuestionArgs) => {
|
||||
}
|
||||
|
||||
if (
|
||||
(program.framework === "express" || program.framework === "fastapi") &&
|
||||
program.framework === "fastapi" &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
if (program.frontend === undefined) {
|
||||
const styledNextJS = blue("NextJS");
|
||||
const styledBackend = green(
|
||||
program.framework === "express"
|
||||
? "Express "
|
||||
: program.framework === "fastapi"
|
||||
? "FastAPI (Python) "
|
||||
: "",
|
||||
);
|
||||
const { frontend } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "frontend",
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ const convertAnswers = async (
|
||||
> = {
|
||||
rag: {
|
||||
template: "streaming",
|
||||
tools: getTools(["duckduckgo"]),
|
||||
tools: getTools(["wikipedia.WikipediaToolSpec"]),
|
||||
frontend: true,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { InstallAppArgs } from "../create-app";
|
||||
|
||||
export type QuestionResults = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager" | "externalPort"
|
||||
"appPath" | "packageManager"
|
||||
>;
|
||||
|
||||
export type PureQuestionArgs = {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
__pycache__
|
||||
poetry.lock
|
||||
storage
|
||||
@@ -1,18 +0,0 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, startup the backend as described in the [backend README](./backend/README.md).
|
||||
|
||||
Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md).
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -32,7 +32,7 @@ poetry run generate
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
Per default, the example is using the explicit workflow. You can change the example by setting the `EXAMPLE_TYPE` environment variable to `choreography` or `orchestrator`.
|
||||
@@ -47,14 +47,18 @@ curl --location 'localhost:8000/api/chat' \
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/examples/workflow.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## Deployments
|
||||
|
||||
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
@@ -21,7 +21,7 @@ poetry run generate
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
The example provides one streaming API endpoint `/api/chat`.
|
||||
@@ -35,14 +35,18 @@ curl --location 'localhost:8000/api/chat' \
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/financial_report.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -16,7 +16,7 @@ Make sure you have the `OPENAI_API_KEY` set.
|
||||
Second, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
## Use Case: Filling Financial CSV Template
|
||||
@@ -41,14 +41,18 @@ curl --location 'localhost:8000/api/chat' \
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/form_filling.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -60,10 +60,12 @@ class NextQuestionSuggestion:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _extract_questions(cls, text: str) -> List[str]:
|
||||
def _extract_questions(cls, text: str) -> List[str] | None:
|
||||
content_match = re.search(r"```(.*?)```", text, re.DOTALL)
|
||||
content = content_match.group(1) if content_match else ""
|
||||
return content.strip().split("\n")
|
||||
content = content_match.group(1) if content_match else None
|
||||
if not content:
|
||||
return None
|
||||
return [q.strip() for q in content.split("\n") if q.strip()]
|
||||
|
||||
@classmethod
|
||||
async def suggest_next_questions(
|
||||
|
||||
@@ -13,6 +13,7 @@ python = "^3.11,<4.0"
|
||||
fastapi = "^0.109.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
python-dotenv = "^1.0.0"
|
||||
pydantic = "<2.10"
|
||||
llama-index = "^0.11.1"
|
||||
cachetools = "^5.3.3"
|
||||
reflex = "^0.6.2.post1"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
## Deployments
|
||||
|
||||
### Using [Fly.io](https://fly.io/):
|
||||
|
||||
First, install [flyctl](https://fly.io/docs/flyctl/install/) and then authenticate with your Fly.io account:
|
||||
|
||||
```shell
|
||||
fly auth login
|
||||
```
|
||||
|
||||
Then, run this command and follow the prompts to deploy the app.:
|
||||
|
||||
```shell
|
||||
fly launch
|
||||
```
|
||||
|
||||
And to open the app in your browser:
|
||||
|
||||
```shell
|
||||
fly apps open
|
||||
```
|
||||
|
||||
> **Note**: The app will use the values from the `.env` file by default to simplify the deployment. Make sure all the needed environment variables in the [.env](.env) file are set. For production environments, you should not use the `.env` file, but [set the variables in Fly.io](https://fly.io/docs/rails/the-basics/configuration/) instead.
|
||||
|
||||
#### Documents
|
||||
|
||||
If you're having documents in the `./data` folder, run the following command to generate vector embeddings of the documents:
|
||||
|
||||
```
|
||||
fly console --machine <machine_id> --command "poetry run generate"
|
||||
```
|
||||
|
||||
Where `machine_id` is the ID of the machine where the app is running. You can show the running machines with the `fly machines` command.
|
||||
|
||||
> **Note**: Using documents will make the app stateful. As Fly.io is a stateless app, you should use [LlamaCloud](https://docs.cloud.llamaindex.ai/llamacloud/getting_started) or a vector database to store the embeddings of the documents. This applies also for document uploads by the user.
|
||||
|
||||
### Using Docker
|
||||
|
||||
First, build an image for the app:
|
||||
|
||||
```
|
||||
docker build -t <your_image_name> .
|
||||
```
|
||||
|
||||
Then, start the app by running the image:
|
||||
|
||||
```
|
||||
docker run \
|
||||
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/storage:/app/storage \ # Use your file system to store vector embeddings
|
||||
-p 8000:8000 \
|
||||
<your_image_name>
|
||||
```
|
||||
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
#### Documents
|
||||
|
||||
If you're having documents in the `./data` folder, run the following command to generate vector embeddings of the documents:
|
||||
|
||||
```
|
||||
docker run \
|
||||
--rm \
|
||||
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/data:/app/data \ # Use your local folder to read the data
|
||||
-v $(pwd)/storage:/app/storage \ # Use your file system to store the vector database
|
||||
<your_image_name> \
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
The app will then be able to answer questions about the documents in the `./data` folder.
|
||||
+27
-3
@@ -1,4 +1,19 @@
|
||||
FROM python:3.11 as build
|
||||
# ====================================
|
||||
# Build the frontend
|
||||
# ====================================
|
||||
FROM node:20 AS frontend
|
||||
|
||||
WORKDIR /app/frontend
|
||||
|
||||
COPY .frontend /app/frontend
|
||||
|
||||
RUN npm install && npm run build
|
||||
|
||||
|
||||
# ====================================
|
||||
# Backend
|
||||
# ====================================
|
||||
FROM python:3.11 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -19,8 +34,17 @@ COPY ./pyproject.toml ./poetry.lock* /app/
|
||||
RUN poetry install --no-root --no-cache --only main
|
||||
|
||||
# ====================================
|
||||
FROM build as release
|
||||
# Release
|
||||
# ====================================
|
||||
FROM build AS release
|
||||
|
||||
COPY --from=frontend /app/frontend/out /app/static
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
# Remove frontend code
|
||||
RUN rm -rf .frontend
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["poetry", "run", "prod"]
|
||||
@@ -21,12 +21,14 @@ Second, generate the embeddings of the documents in the `./data` directory (if t
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
Third, run the app:
|
||||
|
||||
```
|
||||
python main.py
|
||||
poetry run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
|
||||
|
||||
The example provides two different API endpoints:
|
||||
|
||||
1. `/api/chat` - a streaming chat endpoint
|
||||
@@ -50,47 +52,15 @@ curl --location 'localhost:8000/api/chat/request' \
|
||||
|
||||
You can start editing the API endpoints by modifying `app/api/routers/chat.py`. The endpoints auto-update as you save the file. You can delete the endpoint you're not using.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod python main.py
|
||||
poetry run prod
|
||||
```
|
||||
|
||||
## Using Docker
|
||||
## Deployments
|
||||
|
||||
1. Build an image for the FastAPI app:
|
||||
|
||||
```
|
||||
docker build -t <your_backend_image_name> .
|
||||
```
|
||||
|
||||
2. Generate embeddings:
|
||||
|
||||
Parse the data and generate the vector embeddings if the `./data` folder exists - otherwise, skip this step:
|
||||
|
||||
```
|
||||
docker run \
|
||||
--rm \
|
||||
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/data:/app/data \ # Use your local folder to read the data
|
||||
-v $(pwd)/storage:/app/storage \ # Use your file system to store the vector database
|
||||
<your_backend_image_name> \
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
3. Start the API:
|
||||
|
||||
```
|
||||
docker run \
|
||||
-v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/storage:/app/storage \ # Use your file system to store gea vector database
|
||||
-p 8000:8000 \
|
||||
<your_backend_image_name>
|
||||
```
|
||||
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
|
||||
|
||||
## Learn More
|
||||
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import os
|
||||
|
||||
DATA_DIR = "data"
|
||||
STATIC_DIR = os.getenv("STATIC_DIR", "static")
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import logging
|
||||
from typing import Set
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class FrontendProxyMiddleware:
|
||||
"""
|
||||
Proxy requests to the frontend development server
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
frontend_endpoint: str,
|
||||
excluded_paths: Set[str],
|
||||
):
|
||||
self.app = app
|
||||
self.excluded_paths = excluded_paths
|
||||
self.frontend_endpoint = frontend_endpoint
|
||||
|
||||
async def _request_frontend(
|
||||
self,
|
||||
request: Request,
|
||||
path: str,
|
||||
timeout: float = 60.0,
|
||||
):
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
url = f"{self.frontend_endpoint}/{path}"
|
||||
if request.query_params:
|
||||
url = f"{url}?{request.query_params}"
|
||||
|
||||
headers = dict(request.headers)
|
||||
try:
|
||||
body = await request.body() if request.method != "GET" else None
|
||||
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
response_headers = dict(response.headers)
|
||||
response_headers.pop("content-encoding", None)
|
||||
response_headers.pop("content-length", None)
|
||||
|
||||
return StreamingResponse(
|
||||
response.iter_bytes(),
|
||||
status_code=response.status_code,
|
||||
headers=response_headers,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Proxy error: {str(e)}")
|
||||
raise
|
||||
|
||||
def _is_excluded_path(self, path: str) -> bool:
|
||||
return any(
|
||||
path.startswith(excluded_path) for excluded_path in self.excluded_paths
|
||||
)
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
request = Request(scope, receive)
|
||||
path = request.url.path
|
||||
|
||||
if self._is_excluded_path(path):
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
response = await self._request_frontend(request, path.lstrip("/"))
|
||||
return await response(scope, receive, send)
|
||||
@@ -2,3 +2,4 @@ __pycache__
|
||||
storage
|
||||
.env
|
||||
output
|
||||
static/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# flake8: noqa: E402
|
||||
from app.config import DATA_DIR
|
||||
from app.config import DATA_DIR, STATIC_DIR
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
@@ -9,10 +9,10 @@ import os
|
||||
|
||||
import uvicorn
|
||||
from app.api.routers import api_router
|
||||
from app.middlewares.frontend import FrontendProxyMiddleware
|
||||
from app.observability import init_observability
|
||||
from app.settings import init_settings
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
@@ -24,38 +24,43 @@ init_observability()
|
||||
environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
if environment == "dev":
|
||||
logger.warning("Running in development mode - allowing CORS for all origins")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Redirect to documentation page when accessing base URL
|
||||
@app.get("/")
|
||||
async def redirect_to_docs():
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
|
||||
def mount_static_files(directory, path):
|
||||
def mount_static_files(directory, path, html=False):
|
||||
if os.path.exists(directory):
|
||||
logger.info(f"Mounting static files '{directory}' at '{path}'")
|
||||
app.mount(
|
||||
path,
|
||||
StaticFiles(directory=directory, check_dir=False),
|
||||
StaticFiles(directory=directory, check_dir=False, html=html),
|
||||
name=f"{directory}-static",
|
||||
)
|
||||
|
||||
|
||||
app.include_router(api_router, prefix="/api")
|
||||
|
||||
# Mount the data files to serve the file viewer
|
||||
mount_static_files(DATA_DIR, "/api/files/data")
|
||||
# Mount the output files from tools
|
||||
mount_static_files("output", "/api/files/output")
|
||||
|
||||
app.include_router(api_router, prefix="/api")
|
||||
if environment == "dev":
|
||||
frontend_endpoint = os.getenv("FRONTEND_ENDPOINT")
|
||||
if frontend_endpoint:
|
||||
app.add_middleware(
|
||||
FrontendProxyMiddleware,
|
||||
frontend_endpoint=frontend_endpoint,
|
||||
excluded_paths=set(
|
||||
route.path for route in app.routes if hasattr(route, "path")
|
||||
),
|
||||
)
|
||||
else:
|
||||
logger.warning("No frontend endpoint - starting API server only")
|
||||
|
||||
@app.get("/")
|
||||
async def redirect_to_docs():
|
||||
return RedirectResponse(url="/docs")
|
||||
else:
|
||||
# Mount the frontend static files (production)
|
||||
mount_static_files(STATIC_DIR, "/", html=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app_host = os.getenv("APP_HOST", "0.0.0.0")
|
||||
|
||||
@@ -7,15 +7,20 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
generate = "app.engine.generate:generate_datasource"
|
||||
dev = "run:dev" # Starts the app in dev mode
|
||||
prod = "run:prod" # Starts the app in prod mode
|
||||
build = "run:build" # Builds the frontend assets and copies them to the static directory
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.11,<3.13"
|
||||
python = ">=3.11,<3.14"
|
||||
fastapi = "^0.109.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
python-dotenv = "^1.0.0"
|
||||
pydantic = "<2.10"
|
||||
aiostream = "^0.5.2"
|
||||
cachetools = "^5.3.3"
|
||||
llama-index = "^0.11.17"
|
||||
rich = "^13.9.4"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = "^1.8.0"
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
from asyncio.subprocess import Process
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
import dotenv
|
||||
import rich
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
FRONTEND_DIR = Path(os.getenv("FRONTEND_DIR", ".frontend"))
|
||||
DEFAULT_FRONTEND_PORT = 3000
|
||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", "static"))
|
||||
|
||||
|
||||
def build():
|
||||
"""
|
||||
Build the frontend and copy the static files to the backend.
|
||||
|
||||
Raises:
|
||||
SystemError: If any build step fails
|
||||
"""
|
||||
static_dir = Path("static")
|
||||
|
||||
try:
|
||||
package_manager = _get_node_package_manager()
|
||||
_install_frontend_dependencies()
|
||||
|
||||
rich.print("\n[bold]Building the frontend[/bold]")
|
||||
run([package_manager, "run", "build"], cwd=FRONTEND_DIR, check=True)
|
||||
|
||||
if static_dir.exists():
|
||||
shutil.rmtree(static_dir)
|
||||
static_dir.mkdir(exist_ok=True)
|
||||
|
||||
shutil.copytree(FRONTEND_DIR / "out", static_dir, dirs_exist_ok=True)
|
||||
|
||||
rich.print(
|
||||
"\n[bold]Built frontend successfully![/bold]"
|
||||
"\n[bold]Run: 'poetry run prod' to start the app[/bold]"
|
||||
"\n[bold]Don't forget to update the .env file![/bold]"
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
raise SystemError(f"Build failed during {e.cmd}") from e
|
||||
except Exception as e:
|
||||
raise SystemError(f"Build failed: {str(e)}") from e
|
||||
|
||||
|
||||
def dev():
|
||||
asyncio.run(start_development_servers())
|
||||
|
||||
|
||||
def prod():
|
||||
asyncio.run(start_production_server())
|
||||
|
||||
|
||||
async def start_development_servers():
|
||||
"""
|
||||
Start both frontend and backend development servers.
|
||||
Frontend runs with hot reloading, backend runs FastAPI server.
|
||||
|
||||
Raises:
|
||||
SystemError: If either server fails to start
|
||||
"""
|
||||
rich.print("\n[bold]Starting development servers[/bold]")
|
||||
|
||||
try:
|
||||
processes = []
|
||||
if _is_frontend_included():
|
||||
frontend_process, frontend_port = await _run_frontend()
|
||||
processes.append(frontend_process)
|
||||
backend_process = await _run_backend(
|
||||
envs={
|
||||
"ENVIRONMENT": "dev",
|
||||
"FRONTEND_ENDPOINT": f"http://localhost:{frontend_port}",
|
||||
},
|
||||
)
|
||||
processes.append(backend_process)
|
||||
else:
|
||||
backend_process = await _run_backend(
|
||||
envs={"ENVIRONMENT": "dev"},
|
||||
)
|
||||
processes.append(backend_process)
|
||||
|
||||
try:
|
||||
# Wait for processes to complete
|
||||
await asyncio.gather(*[process.wait() for process in processes])
|
||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||
rich.print("\n[bold yellow]Shutting down...[/bold yellow]")
|
||||
finally:
|
||||
# Terminate both processes
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
|
||||
except Exception as e:
|
||||
raise SystemError(f"Failed to start development servers: {str(e)}") from e
|
||||
|
||||
|
||||
async def start_production_server():
|
||||
if _is_frontend_included():
|
||||
is_frontend_built = (FRONTEND_DIR / "out" / "index.html").exists()
|
||||
is_frontend_static_dir_exists = STATIC_DIR.exists()
|
||||
if not is_frontend_built or not is_frontend_static_dir_exists:
|
||||
build()
|
||||
|
||||
try:
|
||||
process = await _run_backend(
|
||||
envs={"ENVIRONMENT": "prod"},
|
||||
)
|
||||
await process.wait()
|
||||
except Exception as e:
|
||||
raise SystemError(f"Failed to start production server: {str(e)}") from e
|
||||
finally:
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
|
||||
|
||||
async def _run_frontend(
|
||||
port: int = DEFAULT_FRONTEND_PORT,
|
||||
timeout: int = 5,
|
||||
) -> tuple[Process, int]:
|
||||
"""
|
||||
Start the frontend development server and return its process and port.
|
||||
|
||||
Returns:
|
||||
tuple[Process, int]: The frontend process and the port it's running on
|
||||
"""
|
||||
# Install dependencies
|
||||
_install_frontend_dependencies()
|
||||
|
||||
port = _find_free_port(start_port=DEFAULT_FRONTEND_PORT)
|
||||
package_manager = _get_node_package_manager()
|
||||
frontend_process = await asyncio.create_subprocess_exec(
|
||||
package_manager,
|
||||
"run",
|
||||
"dev",
|
||||
"-p",
|
||||
str(port),
|
||||
cwd=FRONTEND_DIR,
|
||||
)
|
||||
rich.print(
|
||||
f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]"
|
||||
)
|
||||
# Block until the frontend is accessible
|
||||
for _ in range(timeout):
|
||||
await asyncio.sleep(1)
|
||||
# Check if the frontend is accessible (port is open) or frontend_process is running
|
||||
if frontend_process.returncode is not None:
|
||||
raise RuntimeError("Could not start frontend dev server")
|
||||
if not _is_bindable_port(port):
|
||||
rich.print(
|
||||
f"\n[bold green]Frontend dev server is running on port {port}[/bold green]"
|
||||
)
|
||||
return frontend_process, port
|
||||
raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds")
|
||||
|
||||
|
||||
async def _run_backend(
|
||||
envs: dict[str, str | None] = {},
|
||||
) -> Process:
|
||||
"""
|
||||
Start the backend development server.
|
||||
|
||||
Args:
|
||||
frontend_port: The port number the frontend is running on
|
||||
Returns:
|
||||
Process: The backend process
|
||||
"""
|
||||
# Merge environment variables
|
||||
envs = {**os.environ, **(envs or {})}
|
||||
rich.print("\n[bold]Starting backend FastAPI server...[/bold]")
|
||||
poetry_executable = _get_poetry_executable()
|
||||
return await asyncio.create_subprocess_exec(
|
||||
poetry_executable,
|
||||
"run",
|
||||
"python",
|
||||
"main.py",
|
||||
env=envs,
|
||||
)
|
||||
|
||||
|
||||
def _install_frontend_dependencies():
|
||||
package_manager = _get_node_package_manager()
|
||||
rich.print(
|
||||
f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]"
|
||||
)
|
||||
run([package_manager, "install"], cwd=".frontend", check=True)
|
||||
|
||||
|
||||
def _get_node_package_manager() -> str:
|
||||
"""
|
||||
Check for available package managers and return the preferred one.
|
||||
Returns 'pnpm' if installed, falls back to 'npm'.
|
||||
Raises SystemError if neither is installed.
|
||||
|
||||
Returns:
|
||||
str: The full path to the available package manager executable
|
||||
"""
|
||||
# On Windows, we need to check for .cmd extensions
|
||||
pnpm_cmds = ["pnpm", "pnpm.cmd"]
|
||||
npm_cmds = ["npm", "npm.cmd"]
|
||||
|
||||
for cmd in pnpm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
|
||||
for cmd in npm_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
|
||||
raise SystemError(
|
||||
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
|
||||
)
|
||||
|
||||
|
||||
def _get_poetry_executable() -> str:
|
||||
"""
|
||||
Check for available Poetry executables and return the preferred one.
|
||||
Returns 'poetry' if installed, falls back to 'poetry.cmd'.
|
||||
Raises SystemError if neither is installed.
|
||||
|
||||
Returns:
|
||||
str: The full path to the available Poetry executable
|
||||
"""
|
||||
poetry_cmds = ["poetry", "poetry.cmd"]
|
||||
for cmd in poetry_cmds:
|
||||
cmd_path = which(cmd)
|
||||
if cmd_path is not None:
|
||||
return cmd_path
|
||||
raise SystemError("Poetry is not installed. Please install Poetry first.")
|
||||
|
||||
|
||||
def _is_bindable_port(port: int) -> bool:
|
||||
"""Check if a port is available by attempting to connect to it."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
# Try to connect to the port
|
||||
s.connect(("localhost", port))
|
||||
# If we can connect, port is in use
|
||||
return False
|
||||
except ConnectionRefusedError:
|
||||
# Connection refused means port is available
|
||||
return True
|
||||
except socket.error:
|
||||
# Other socket errors also likely mean port is available
|
||||
return True
|
||||
|
||||
|
||||
def _find_free_port(start_port: int) -> int:
|
||||
"""
|
||||
Find a free port starting from the given port number.
|
||||
"""
|
||||
for port in range(start_port, 65535):
|
||||
if _is_bindable_port(port):
|
||||
return port
|
||||
raise SystemError("No free port found")
|
||||
|
||||
|
||||
def _is_frontend_included() -> bool:
|
||||
"""Check if the app has frontend"""
|
||||
return FRONTEND_DIR.exists()
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM node:20-alpine as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.* ./
|
||||
RUN npm install
|
||||
|
||||
# Build the application
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ====================================
|
||||
FROM build as release
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.envFile": ""
|
||||
}
|
||||
Reference in New Issue
Block a user