From 4b980b6bd852bbde4ea369f792c63c7fd0c8326c Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 13 Nov 2024 17:28:23 +0700 Subject: [PATCH 01/32] rebase --- create-app.ts | 13 ++--- templates/.gitignore | 1 + templates/types/extractor/fastapi/gitignore | 1 + templates/types/streaming/express/gitignore | 1 + templates/types/streaming/fastapi/gitignore | 1 + .../types/streaming/fastapi/pyproject.toml | 2 + templates/types/streaming/fastapi/run.py | 52 +++++++++++++++++++ 7 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 templates/types/streaming/fastapi/run.py diff --git a/create-app.ts b/create-app.ts index 144fdc7e2..5d410be77 100644 --- a/create-app.ts +++ b/create-app.ts @@ -7,7 +7,6 @@ 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"; @@ -92,11 +91,10 @@ export async function createApp({ if (frontend) { // install backend - const backendRoot = path.join(root, "backend"); - await makeDir(backendRoot); - await installTemplate({ ...args, root: backendRoot, backend: true }); + await makeDir(root); + await installTemplate({ ...args, root: root, backend: true }); // install frontend - const frontendRoot = path.join(root, "frontend"); + const frontendRoot = path.join(root, ".frontend"); await makeDir(frontendRoot); await installTemplate({ ...args, @@ -105,11 +103,6 @@ export async function createApp({ 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 }); } diff --git a/templates/.gitignore b/templates/.gitignore index ec6c67b63..43edea526 100644 --- a/templates/.gitignore +++ b/templates/.gitignore @@ -1,3 +1,4 @@ __pycache__ poetry.lock storage +.frontend/ diff --git a/templates/types/extractor/fastapi/gitignore b/templates/types/extractor/fastapi/gitignore index ae22d348e..1acc5d432 100644 --- a/templates/types/extractor/fastapi/gitignore +++ b/templates/types/extractor/fastapi/gitignore @@ -2,3 +2,4 @@ __pycache__ storage .env output +.frontend/ diff --git a/templates/types/streaming/express/gitignore b/templates/types/streaming/express/gitignore index ba3d7e255..57a87acc9 100644 --- a/templates/types/streaming/express/gitignore +++ b/templates/types/streaming/express/gitignore @@ -3,3 +3,4 @@ node_modules/ output/ +.frontend/ diff --git a/templates/types/streaming/fastapi/gitignore b/templates/types/streaming/fastapi/gitignore index ae22d348e..b22a4b7dd 100644 --- a/templates/types/streaming/fastapi/gitignore +++ b/templates/types/streaming/fastapi/gitignore @@ -2,3 +2,4 @@ __pycache__ storage .env output +.frontend/ \ No newline at end of file diff --git a/templates/types/streaming/fastapi/pyproject.toml b/templates/types/streaming/fastapi/pyproject.toml index 400991c52..8bc825f54 100644 --- a/templates/types/streaming/fastapi/pyproject.toml +++ b/templates/types/streaming/fastapi/pyproject.toml @@ -7,6 +7,8 @@ readme = "README.md" [tool.poetry.scripts] generate = "app.engine.generate:generate_datasource" +build = "run:build" +dev = "run:dev" [tool.poetry.dependencies] python = ">=3.11,<3.13" diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py new file mode 100644 index 000000000..90c430ad2 --- /dev/null +++ b/templates/types/streaming/fastapi/run.py @@ -0,0 +1,52 @@ +import argparse +import os +from typing import Optional + + +def check_npm_installed(): + """ + Check if npm is installed on the system. + """ + if os.system("npm --version > /dev/null 2>&1") != 0: + raise SystemError("npm is not installed. Please install Node.js and npm first.") + + +def build(): + """ + Build the frontend and copy the static files to the backend. + """ + check_npm_installed() + print("\n===> Installing frontend dependencies. It might take a while...") + os.system("cd .frontend && npm i") + print("\n===> Building the frontend") + os.system("cd .frontend && npm run build") + os.system("mkdir -p static && rm -rf static/* && cp -r .frontend/out/* static") + print( + "\n===> Built frontend successfully!" + "\n Run: 'poetry run dev' to start the server" + "\n Don't forget to update the .env file!" + ) + + +def dev(): + """ + Start fastapi server. + """ + print("===> Starting the development server") + os.system("poetry run python main.py") + + +def main(command: Optional[str]): + if command == "build": + build() + elif command == "dev": + dev() + else: + raise ValueError(f"Invalid command: {command}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("command", type=str, nargs="?") + args = parser.parse_args() + main(args.command) From 2693eb154267d182c9066a3030ebc1276f1004aa Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 13 Nov 2024 18:08:33 +0700 Subject: [PATCH 02/32] simply --- create-app.ts | 5 +++-- templates/types/extractor/fastapi/gitignore | 3 +-- templates/types/streaming/express/gitignore | 3 +-- templates/types/streaming/fastapi/run.py | 18 ------------------ 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/create-app.ts b/create-app.ts index 5d410be77..3f32d0545 100644 --- a/create-app.ts +++ b/create-app.ts @@ -91,10 +91,11 @@ export async function createApp({ if (frontend) { // install backend - await makeDir(root); await installTemplate({ ...args, root: root, backend: true }); // install frontend - const frontendRoot = path.join(root, ".frontend"); + // TODO: Update Express to serve static files from frontend + const frontendFolder = framework === "fastapi" ? ".frontend" : "frontend"; + const frontendRoot = path.join(root, frontendFolder); await makeDir(frontendRoot); await installTemplate({ ...args, diff --git a/templates/types/extractor/fastapi/gitignore b/templates/types/extractor/fastapi/gitignore index 1acc5d432..f29bdf870 100644 --- a/templates/types/extractor/fastapi/gitignore +++ b/templates/types/extractor/fastapi/gitignore @@ -1,5 +1,4 @@ __pycache__ storage .env -output -.frontend/ +output \ No newline at end of file diff --git a/templates/types/streaming/express/gitignore b/templates/types/streaming/express/gitignore index 57a87acc9..aac690a28 100644 --- a/templates/types/streaming/express/gitignore +++ b/templates/types/streaming/express/gitignore @@ -2,5 +2,4 @@ .env node_modules/ -output/ -.frontend/ +output/ \ No newline at end of file diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 90c430ad2..43e15ea99 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,6 +1,4 @@ -import argparse import os -from typing import Optional def check_npm_installed(): @@ -34,19 +32,3 @@ def dev(): """ print("===> Starting the development server") os.system("poetry run python main.py") - - -def main(command: Optional[str]): - if command == "build": - build() - elif command == "dev": - dev() - else: - raise ValueError(f"Invalid command: {command}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("command", type=str, nargs="?") - args = parser.parse_args() - main(args.command) From bed49791b20d1374697e54b34c65e2154a224ac2 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 13 Nov 2024 18:36:15 +0700 Subject: [PATCH 03/32] add static mount --- templates/types/streaming/fastapi/gitignore | 3 ++- templates/types/streaming/fastapi/main.py | 15 ++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/templates/types/streaming/fastapi/gitignore b/templates/types/streaming/fastapi/gitignore index b22a4b7dd..f11c1851f 100644 --- a/templates/types/streaming/fastapi/gitignore +++ b/templates/types/streaming/fastapi/gitignore @@ -2,4 +2,5 @@ __pycache__ storage .env output -.frontend/ \ No newline at end of file +.frontend/ +static/ \ No newline at end of file diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py index cf1a4e8c0..7660a1019 100644 --- a/templates/types/streaming/fastapi/main.py +++ b/templates/types/streaming/fastapi/main.py @@ -13,7 +13,6 @@ 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 app = FastAPI() @@ -34,28 +33,26 @@ 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") +# Mount static files from the frontend +mount_static_files("static", "/", html=True) -app.include_router(api_router, prefix="/api") if __name__ == "__main__": app_host = os.getenv("APP_HOST", "0.0.0.0") From 98f80544b0c468dde426bdd0e2e93b5939b8f1ff Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 10:19:52 +0700 Subject: [PATCH 04/32] update log --- templates/types/extractor/fastapi/gitignore | 2 +- templates/types/streaming/express/gitignore | 2 +- templates/types/streaming/fastapi/gitignore | 2 +- .../types/streaming/fastapi/pyproject.toml | 1 + templates/types/streaming/fastapi/run.py | 21 ++++++++++++------- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/templates/types/extractor/fastapi/gitignore b/templates/types/extractor/fastapi/gitignore index f29bdf870..ae22d348e 100644 --- a/templates/types/extractor/fastapi/gitignore +++ b/templates/types/extractor/fastapi/gitignore @@ -1,4 +1,4 @@ __pycache__ storage .env -output \ No newline at end of file +output diff --git a/templates/types/streaming/express/gitignore b/templates/types/streaming/express/gitignore index aac690a28..ba3d7e255 100644 --- a/templates/types/streaming/express/gitignore +++ b/templates/types/streaming/express/gitignore @@ -2,4 +2,4 @@ .env node_modules/ -output/ \ No newline at end of file +output/ diff --git a/templates/types/streaming/fastapi/gitignore b/templates/types/streaming/fastapi/gitignore index f11c1851f..0f7a2f1af 100644 --- a/templates/types/streaming/fastapi/gitignore +++ b/templates/types/streaming/fastapi/gitignore @@ -3,4 +3,4 @@ storage .env output .frontend/ -static/ \ No newline at end of file +static/ diff --git a/templates/types/streaming/fastapi/pyproject.toml b/templates/types/streaming/fastapi/pyproject.toml index 8bc825f54..b969f9098 100644 --- a/templates/types/streaming/fastapi/pyproject.toml +++ b/templates/types/streaming/fastapi/pyproject.toml @@ -18,6 +18,7 @@ python-dotenv = "^1.0.0" 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" diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 43e15ea99..595887a45 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,5 +1,7 @@ import os +import rich + def check_npm_installed(): """ @@ -14,21 +16,24 @@ def build(): Build the frontend and copy the static files to the backend. """ check_npm_installed() - print("\n===> Installing frontend dependencies. It might take a while...") + # Show in bold (using rich library) + rich.print( + "\n[bold]Installing frontend dependencies. It might take a while...[/bold]" + ) os.system("cd .frontend && npm i") - print("\n===> Building the frontend") + rich.print("\n[bold]Building the frontend[/bold]") os.system("cd .frontend && npm run build") os.system("mkdir -p static && rm -rf static/* && cp -r .frontend/out/* static") - print( - "\n===> Built frontend successfully!" - "\n Run: 'poetry run dev' to start the server" - "\n Don't forget to update the .env file!" + rich.print( + "\n[bold]Built frontend successfully![/bold]" + "\n[bold]Run: 'poetry run dev' to start the app[/bold]" + "\n[bold]Don't forget to update the .env file![/bold]" ) def dev(): """ - Start fastapi server. + Start fastapi app. """ - print("===> Starting the development server") + rich.print("\n[bold]Starting app[/bold]") os.system("poetry run python main.py") From 4af77c6b79bf51be6a9f4679c379b124249c9199 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 10:53:45 +0700 Subject: [PATCH 05/32] add readme --- templates/types/streaming/fastapi/README-template.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/templates/types/streaming/fastapi/README-template.md b/templates/types/streaming/fastapi/README-template.md index 7969ff0ea..8f34f3f6e 100644 --- a/templates/types/streaming/fastapi/README-template.md +++ b/templates/types/streaming/fastapi/README-template.md @@ -21,10 +21,16 @@ Second, generate the embeddings of the documents in the `./data` directory (if t poetry run generate ``` -Third, run the development server: +Third, if you want to use the app with a chat UI, you will need to build the frontend once. If not, you can skip this step: ``` -python main.py +poetry run build +``` + +Finally, run the app: + +``` +poetry run dev ``` The example provides two different API endpoints: @@ -50,7 +56,7 @@ 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. +If you have built the frontend, open [http://localhost:8000](http://localhost:8000) in your browser to access the chat UI. If not, open [http://localhost:8000/docs](http://localhost:8000/docs) to view the Swagger UI documentation for 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`: From 51a8cdf3f7f158d9361d4c2ff888f4e184943567 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 11:57:29 +0700 Subject: [PATCH 06/32] update CL scripts --- create-app.ts | 6 +- helpers/env-variables.ts | 4 +- helpers/run-app.ts | 86 +++++++++++-------- helpers/types.ts | 2 +- helpers/typescript.ts | 5 +- index.ts | 2 +- templates/types/streaming/fastapi/run.py | 2 + templates/types/streaming/nextjs/package.json | 4 +- 8 files changed, 64 insertions(+), 47 deletions(-) diff --git a/create-app.ts b/create-app.ts index 3f32d0545..77d3d67fd 100644 --- a/create-app.ts +++ b/create-app.ts @@ -34,7 +34,7 @@ export async function createApp({ communityProjectConfig, llamapack, vectorDb, - externalPort, + port, postInstallAction, dataSources, tools, @@ -80,7 +80,7 @@ export async function createApp({ communityProjectConfig, llamapack, vectorDb, - externalPort, + port, postInstallAction, dataSources, tools, @@ -101,7 +101,7 @@ export async function createApp({ ...args, root: frontendRoot, framework: "nextjs", - customApiPath: `http://localhost:${externalPort ?? 8000}/api/chat`, + customApiPath: `http://localhost:${port ?? 8000}/api/chat`, backend: false, }); } else { diff --git a/helpers/env-variables.ts b/helpers/env-variables.ts index 4a554ff04..ddac770d3 100644 --- a/helpers/env-variables.ts +++ b/helpers/env-variables.ts @@ -553,7 +553,7 @@ export const createBackendEnvFile = async ( | "framework" | "dataSources" | "template" - | "externalPort" + | "port" | "tools" | "observability" >, @@ -570,7 +570,7 @@ export const createBackendEnvFile = async ( ...getModelEnvs(opts.modelConfig), ...getEngineEnvs(), ...getVectorDBEnvs(opts.vectorDb, opts.framework), - ...getFrameworkEnvs(opts.framework, opts.externalPort), + ...getFrameworkEnvs(opts.framework, opts.port), ...getToolEnvs(opts.tools), ...getTemplateEnvs(opts.template), ...getObservabilityEnvs(opts.observability), diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 2ec4e762f..786cf5208 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -1,26 +1,28 @@ import { ChildProcess, SpawnOptions, spawn } from "child_process"; -import path from "path"; 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 => { + 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); + } + resolve(); + }) + .on("error", function (err) { + console.log("Error when running child process: ", err); + reject(err); + }); + }); }; export function runReflexApp( @@ -50,6 +52,21 @@ export function runFastAPIApp(appPath: string, port: number) { }); } +export function buildFrontend(appPath: string, framework: TemplateFramework) { + const packageManager = framework === "fastapi" ? "poetry" : "npm"; + if (framework === "express") { + return createProcess(packageManager, ["run", "build"], { + stdio: "inherit", + cwd: appPath, + }); + } else { + return createProcess(packageManager, ["run", "build"], { + stdio: "inherit", + cwd: appPath, + }); + } +} + export function runTSApp(appPath: string, port: number) { return createProcess("npm", ["run", "dev"], { stdio: "inherit", @@ -65,35 +82,30 @@ export async function runApp( framework: TemplateFramework, port?: number, externalPort?: number, -): Promise { +): Promise { + // Setup cleanup const processes: ChildProcess[] = []; - - // 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"); + try { + // Build frontend first if needed + if (frontend && (template === "streaming" || template === "multiagent")) { + await buildFrontend(appPath, framework); + } - if (template === "extractor") { - processes.push(runReflexApp(appPath, port, externalPort)); - } - 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)); + // Then start the server + if (template === "extractor") { + await runReflexApp(appPath, port, externalPort); + } else if (template === "streaming" || template === "multiagent") { + const appRunner = framework === "fastapi" ? runFastAPIApp : runTSApp; + const defaultPort = framework === "nextjs" ? 3000 : 8000; + await appRunner(appPath, port || defaultPort); } + } catch (error) { + console.error("Failed to run app:", error); + throw error; } - - return Promise.all(processes); } diff --git a/helpers/types.ts b/helpers/types.ts index bcaf5b062..53e1cdbab 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -96,7 +96,7 @@ export interface InstallTemplateArgs { communityProjectConfig?: CommunityProjectConfig; llamapack?: string; vectorDb?: TemplateVectorDB; - externalPort?: number; + port?: number; postInstallAction?: TemplatePostInstallAction; tools?: Tool[]; observability?: TemplateObservability; diff --git a/helpers/typescript.ts b/helpers/typescript.ts index 7d08ed447..d0064bfd2 100644 --- a/helpers/typescript.ts +++ b/helpers/typescript.ts @@ -241,7 +241,10 @@ export const installTSTemplate = async ({ vectorDb, }); - if (postInstallAction === "runApp" || postInstallAction === "dependencies") { + if ( + backend && + (postInstallAction === "runApp" || postInstallAction === "dependencies") + ) { await installTSDependencies(packageJson, packageManager, isOnline); } diff --git a/index.ts b/index.ts index de7f5b649..ae894de7d 100644 --- a/index.ts +++ b/index.ts @@ -333,7 +333,7 @@ async function run(): Promise { ...answers, appPath: resolvedProjectPath, packageManager, - externalPort: options.externalPort, + port: options.port, }); if (answers.postInstallAction === "VSCode") { diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 595887a45..c0e7b59c6 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -21,8 +21,10 @@ def build(): "\n[bold]Installing frontend dependencies. It might take a while...[/bold]" ) os.system("cd .frontend && npm i") + rich.print("\n[bold]Building the frontend[/bold]") os.system("cd .frontend && npm run build") + os.system("mkdir -p static && rm -rf static/* && cp -r .frontend/out/* static") rich.print( "\n[bold]Built frontend successfully![/bold]" diff --git a/templates/types/streaming/nextjs/package.json b/templates/types/streaming/nextjs/package.json index ebb77ba41..9fdb8feea 100644 --- a/templates/types/streaming/nextjs/package.json +++ b/templates/types/streaming/nextjs/package.json @@ -30,8 +30,8 @@ "llamaindex": "0.8.2", "lucide-react": "^0.294.0", "next": "^15.0.3", - "react": "19.0.0-rc-5c56b873-20241107", - "react-dom": "19.0.0-rc-5c56b873-20241107", + "react": "^18.2.0", + "react-dom": "^18.2.0", "papaparse": "^5.4.1", "supports-color": "^8.1.1", "tailwind-merge": "^2.1.0", From e9554faca35e9c6d81a32ff13f1b3c6d21aa3a3e Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 14:04:36 +0700 Subject: [PATCH 07/32] remove frontend for express --- create-app.ts | 13 +++++-------- helpers/devcontainer.ts | 4 ++-- helpers/run-app.ts | 15 ++++----------- questions/questions.ts | 13 +++---------- templates/types/streaming/fastapi/run.py | 4 ++-- 5 files changed, 16 insertions(+), 33 deletions(-) diff --git a/create-app.ts b/create-app.ts index 77d3d67fd..c90c8c5d0 100644 --- a/create-app.ts +++ b/create-app.ts @@ -89,13 +89,12 @@ export async function createApp({ agents, }; - if (frontend) { - // install backend - await installTemplate({ ...args, root: root, backend: true }); + // Install backend + await installTemplate({ ...args, backend: true }); + + if (frontend && framework === "fastapi") { // install frontend - // TODO: Update Express to serve static files from frontend - const frontendFolder = framework === "fastapi" ? ".frontend" : "frontend"; - const frontendRoot = path.join(root, frontendFolder); + const frontendRoot = path.join(root, ".frontend"); await makeDir(frontendRoot); await installTemplate({ ...args, @@ -104,8 +103,6 @@ export async function createApp({ customApiPath: `http://localhost:${port ?? 8000}/api/chat`, backend: false, }); - } else { - await installTemplate({ ...args, backend: true }); } await writeDevcontainer(root, templatesDir, framework, frontend); diff --git a/helpers/devcontainer.ts b/helpers/devcontainer.ts index 415741198..e1dfe8e75 100644 --- a/helpers/devcontainer.ts +++ b/helpers/devcontainer.ts @@ -15,8 +15,8 @@ function renderDevcontainerContent( if (frontend) { devcontainerJson.postCreateCommand = framework === "fastapi" - ? "cd backend && poetry install && cd ../frontend && npm install" - : "cd backend && npm install && cd ../frontend && npm install"; + ? "poetry install && poetry run build" + : "npm install"; } else { devcontainerJson.postCreateCommand = framework === "fastapi" ? "poetry install" : "npm install"; diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 786cf5208..11b80655d 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -54,17 +54,10 @@ export function runFastAPIApp(appPath: string, port: number) { export function buildFrontend(appPath: string, framework: TemplateFramework) { const packageManager = framework === "fastapi" ? "poetry" : "npm"; - if (framework === "express") { - return createProcess(packageManager, ["run", "build"], { - stdio: "inherit", - cwd: appPath, - }); - } else { - return createProcess(packageManager, ["run", "build"], { - stdio: "inherit", - cwd: appPath, - }); - } + return createProcess(packageManager, ["run", "build"], { + stdio: "inherit", + cwd: appPath, + }); } export function runTSApp(appPath: string, port: number) { diff --git a/questions/questions.ts b/questions/questions.ts index 45c262f49..dea1e52d0 100644 --- a/questions/questions.ts +++ b/questions/questions.ts @@ -1,4 +1,4 @@ -import { blue, green } from "picocolors"; +import { blue } from "picocolors"; import prompts from "prompts"; import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant"; import { EXAMPLE_FILE } from "../helpers/datasources"; @@ -122,24 +122,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", diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index c0e7b59c6..dfe0fc5ee 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -20,10 +20,10 @@ def build(): rich.print( "\n[bold]Installing frontend dependencies. It might take a while...[/bold]" ) - os.system("cd .frontend && npm i") + os.system("cd .frontend && pnpm i") rich.print("\n[bold]Building the frontend[/bold]") - os.system("cd .frontend && npm run build") + os.system("cd .frontend && pnpm build") os.system("mkdir -p static && rm -rf static/* && cp -r .frontend/out/* static") rich.print( From 72b8dc1088692bfe83f9a64ca62c9c9d480a292f Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 14:20:53 +0700 Subject: [PATCH 08/32] add support pnpm --- templates/types/streaming/fastapi/run.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index dfe0fc5ee..2248450f3 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -3,27 +3,32 @@ import rich -def check_npm_installed(): +def check_package_manager(): """ - Check if npm is installed on the system. + Check for available package managers and return the preferred one. + Returns 'pnpm' if installed, falls back to 'npm'. """ - if os.system("npm --version > /dev/null 2>&1") != 0: - raise SystemError("npm is not installed. Please install Node.js and npm first.") + if os.system("pnpm --version > /dev/null 2>&1") == 0: + return "pnpm" + if os.system("npm --version > /dev/null 2>&1") == 0: + return "npm" + raise SystemError( + "Neither pnpm nor npm is installed. Please install Node.js and a package manager first." + ) def build(): """ Build the frontend and copy the static files to the backend. """ - check_npm_installed() - # Show in bold (using rich library) + package_manager = check_package_manager() rich.print( - "\n[bold]Installing frontend dependencies. It might take a while...[/bold]" + f"\n[bold]Installing frontend dependencies using {package_manager}. It might take a while...[/bold]" ) - os.system("cd .frontend && pnpm i") + os.system(f"cd .frontend && {package_manager} i") rich.print("\n[bold]Building the frontend[/bold]") - os.system("cd .frontend && pnpm build") + os.system(f"cd .frontend && {package_manager} run build") os.system("mkdir -p static && rm -rf static/* && cp -r .frontend/out/* static") rich.print( From 57e3e77f7ad9a5fa8ea06894cf45068902971c2f Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 14:33:20 +0700 Subject: [PATCH 09/32] remove e2e test frontend for express --- e2e/shared/streaming_template.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/shared/streaming_template.spec.ts b/e2e/shared/streaming_template.spec.ts index b34d4fede..4564a30d9 100644 --- a/e2e/shared/streaming_template.spec.ts +++ b/e2e/shared/streaming_template.spec.ts @@ -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"; From 875f1ff49dfd07de7895c2f528599ff9ab7d1127 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 14:47:33 +0700 Subject: [PATCH 10/32] Fix issue on CI --- templates/types/streaming/fastapi/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 2248450f3..20c79c25a 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -8,9 +8,9 @@ def check_package_manager(): Check for available package managers and return the preferred one. Returns 'pnpm' if installed, falls back to 'npm'. """ - if os.system("pnpm --version > /dev/null 2>&1") == 0: + if os.system("pnpm --version") == 0: return "pnpm" - if os.system("npm --version > /dev/null 2>&1") == 0: + if os.system("npm --version") == 0: return "npm" raise SystemError( "Neither pnpm nor npm is installed. Please install Node.js and a package manager first." From b1a9db2a5714664dd8edd9858e65a29233db2d84 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Mon, 18 Nov 2024 18:09:10 +0700 Subject: [PATCH 11/32] fix e2e --- e2e/shared/streaming_template.spec.ts | 5 +---- e2e/utils.ts | 30 +++++++-------------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/e2e/shared/streaming_template.spec.ts b/e2e/shared/streaming_template.spec.ts index 4564a30d9..1dedc0ea4 100644 --- a/e2e/shared/streaming_template.spec.ts +++ b/e2e/shared/streaming_template.spec.ts @@ -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, @@ -102,7 +99,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: [ diff --git a/e2e/utils.ts b/e2e/utils.ts index 799daf49f..e5d527e80 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -25,7 +25,7 @@ export type RunCreateLlamaOptions = { dataSource: string; vectorDb: TemplateVectorDB; port: number; - externalPort: number; + externalPort?: number; postInstallAction: TemplatePostInstallAction; templateUI?: TemplateUI; appType?: AppType; @@ -93,8 +93,6 @@ export async function runCreateLlama({ "--use-pnpm", "--port", port, - "--external-port", - externalPort, "--post-install-action", postInstallAction, "--tools", @@ -103,6 +101,10 @@ export async function runCreateLlama({ "none", ]; + if (externalPort) { + commandArgs.push("--external-port", externalPort); + } + if (templateUI) { commandArgs.push("--ui", templateUI); } @@ -142,12 +144,9 @@ export async function runCreateLlama({ // Wait for app to start if (postInstallAction === "runApp") { - await checkAppHasStarted( - appType === "--frontend", - templateFramework, - port, - externalPort, - ); + const portsToWait = externalPort ? [port, externalPort] : [port]; + + await waitPorts(portsToWait); } else if (postInstallAction === "dependencies") { await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved } else { @@ -167,19 +166,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 { const waitForPort = async (port: number): Promise => { await waitPort({ From 7c8757dd37e63210aca1267a89efa84301a1a7a7 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 09:10:19 +0700 Subject: [PATCH 12/32] remove e2e frontend tests for express --- e2e/shared/multiagent_template.spec.ts | 11 +++++++++-- e2e/shared/streaming_template.spec.ts | 9 +++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/e2e/shared/multiagent_template.spec.ts b/e2e/shared/multiagent_template.spec.ts index 52721cf5c..1aacfc499 100644 --- a/e2e/shared/multiagent_template.spec.ts +++ b/e2e/shared/multiagent_template.spec.ts @@ -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"]; @@ -61,6 +61,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 +73,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}`); diff --git a/e2e/shared/streaming_template.spec.ts b/e2e/shared/streaming_template.spec.ts index 1dedc0ea4..f961a2f99 100644 --- a/e2e/shared/streaming_template.spec.ts +++ b/e2e/shared/streaming_template.spec.ts @@ -65,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(); }); @@ -74,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([ From 78c77982412c208d9707524fd8146c7a37d823b5 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 09:15:15 +0700 Subject: [PATCH 13/32] improve code --- helpers/run-app.ts | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 11b80655d..b1ac9db90 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -1,4 +1,4 @@ -import { ChildProcess, SpawnOptions, spawn } from "child_process"; +import { SpawnOptions, spawn } from "child_process"; import { TemplateFramework } from "./types"; const createProcess = ( @@ -43,18 +43,18 @@ export function runReflexApp( }); } -export function runFastAPIApp(appPath: string, port: number) { - const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port]; - - return createProcess("poetry", commandArgs, { +export function buildFrontend(appPath: string, framework: TemplateFramework) { + const packageManager = framework === "fastapi" ? "poetry" : "npm"; + return createProcess(packageManager, ["run", "build"], { stdio: "inherit", cwd: appPath, }); } -export function buildFrontend(appPath: string, framework: TemplateFramework) { - const packageManager = framework === "fastapi" ? "poetry" : "npm"; - return createProcess(packageManager, ["run", "build"], { +export function runFastAPIApp(appPath: string, port: number) { + const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port]; + + return createProcess("poetry", commandArgs, { stdio: "inherit", cwd: appPath, }); @@ -76,20 +76,13 @@ export async function runApp( port?: number, externalPort?: number, ): Promise { - // Setup cleanup - const processes: ChildProcess[] = []; - process.on("exit", () => { - console.log("Killing app processes..."); - processes.forEach((p) => p.kill()); - }); - try { - // Build frontend first if needed + // Build frontend if needed if (frontend && (template === "streaming" || template === "multiagent")) { await buildFrontend(appPath, framework); } - // Then start the server + // Start the app if (template === "extractor") { await runReflexApp(appPath, port, externalPort); } else if (template === "streaming" || template === "multiagent") { From 18e7c0ec42c9704de570ae01aace160aea619bdb Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 09:32:32 +0700 Subject: [PATCH 14/32] better run command --- helpers/run-app.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/helpers/run-app.ts b/helpers/run-app.ts index b1ac9db90..7f1394d67 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -52,11 +52,10 @@ export function buildFrontend(appPath: string, framework: TemplateFramework) { } 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}` }, }); } From 8aa26e923ee66e2879f9ea1ba2ae1772770c1310 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 09:55:17 +0700 Subject: [PATCH 15/32] remove externalPort --- e2e/python/resolve_dependencies.spec.ts | 4 -- e2e/shared/extractor_template.spec.ts | 1 - e2e/shared/multiagent_template.spec.ts | 3 -- e2e/typescript/resolve_dependencies.spec.ts | 1 - e2e/utils.ts | 10 +---- helpers/run-app.ts | 47 ++++++++++----------- index.ts | 8 ---- questions/types.ts | 2 +- 8 files changed, 25 insertions(+), 51 deletions(-) diff --git a/e2e/python/resolve_dependencies.spec.ts b/e2e/python/resolve_dependencies.spec.ts index f1e5ddaf4..1d7e12199 100644 --- a/e2e/python/resolve_dependencies.spec.ts +++ b/e2e/python/resolve_dependencies.spec.ts @@ -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", diff --git a/e2e/shared/extractor_template.spec.ts b/e2e/shared/extractor_template.spec.ts index 643fc6dde..10bcce293 100644 --- a/e2e/shared/extractor_template.spec.ts +++ b/e2e/shared/extractor_template.spec.ts @@ -38,7 +38,6 @@ if ( dataSource: "--example-file", vectorDb: "none", port: frontendPort, - externalPort: backendPort, postInstallAction: "runApp", }); name = result.projectName; diff --git a/e2e/shared/multiagent_template.spec.ts b/e2e/shared/multiagent_template.spec.ts index 1aacfc499..ccb33539b 100644 --- a/e2e/shared/multiagent_template.spec.ts +++ b/e2e/shared/multiagent_template.spec.ts @@ -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, diff --git a/e2e/typescript/resolve_dependencies.spec.ts b/e2e/typescript/resolve_dependencies.spec.ts index 9ae8aa7a1..c8223be60 100644 --- a/e2e/typescript/resolve_dependencies.spec.ts +++ b/e2e/typescript/resolve_dependencies.spec.ts @@ -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", diff --git a/e2e/utils.ts b/e2e/utils.ts index e5d527e80..4d1dd6283 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -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, @@ -101,10 +99,6 @@ export async function runCreateLlama({ "none", ]; - if (externalPort) { - commandArgs.push("--external-port", externalPort); - } - if (templateUI) { commandArgs.push("--ui", templateUI); } @@ -144,9 +138,7 @@ export async function runCreateLlama({ // Wait for app to start if (postInstallAction === "runApp") { - const portsToWait = externalPort ? [port, externalPort] : [port]; - - await waitPorts(portsToWait); + await waitPorts([port]); } else if (postInstallAction === "dependencies") { await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved } else { diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 7f1394d67..b03e53a99 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -25,27 +25,26 @@ const createProcess = ( }); }; -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()); - } - return createProcess("poetry", commandArgs, { +export function buildFrontend(appPath: string, framework: TemplateFramework) { + const packageManager = framework === "fastapi" ? "poetry" : "npm"; + return createProcess(packageManager, ["run", "build"], { stdio: "inherit", cwd: appPath, }); } -export function buildFrontend(appPath: string, framework: TemplateFramework) { - const packageManager = framework === "fastapi" ? "poetry" : "npm"; - return createProcess(packageManager, ["run", "build"], { +export function runReflexApp(appPath: string, port: number) { + const backendPort = port + 1; + const commandArgs = [ + "run", + "reflex", + "run", + "--frontend-port", + port.toString(), + "--backend-port", + backendPort.toString(), + ]; + return createProcess("poetry", commandArgs, { stdio: "inherit", cwd: appPath, }); @@ -73,7 +72,6 @@ export async function runApp( frontend: boolean, framework: TemplateFramework, port?: number, - externalPort?: number, ): Promise { try { // Build frontend if needed @@ -82,13 +80,14 @@ export async function runApp( } // Start the app - if (template === "extractor") { - await runReflexApp(appPath, port, externalPort); - } else if (template === "streaming" || template === "multiagent") { - const appRunner = framework === "fastapi" ? runFastAPIApp : runTSApp; - const defaultPort = framework === "nextjs" ? 3000 : 8000; - await appRunner(appPath, port || defaultPort); - } + const defaultPort = framework === "nextjs" ? 3000 : 8000; + 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; diff --git a/index.ts b/index.ts index ae894de7d..8b3bcf214 100644 --- a/index.ts +++ b/index.ts @@ -134,13 +134,6 @@ const program = new Command(packageJson.name) ` Select UI port. -`, - ) - .option( - "--external-port ", - ` - - Select external port. `, ) .option( @@ -368,7 +361,6 @@ Please check ${cyan( answers.frontend, answers.framework, options.port, - options.externalPort, ); } } diff --git a/questions/types.ts b/questions/types.ts index 1ea45c186..4d0acd890 100644 --- a/questions/types.ts +++ b/questions/types.ts @@ -2,7 +2,7 @@ import { InstallAppArgs } from "../create-app"; export type QuestionResults = Omit< InstallAppArgs, - "appPath" | "packageManager" | "externalPort" + "appPath" | "packageManager" >; export type PureQuestionArgs = { From 61117be439dd0971048e6db7248a0e234af03822 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 10:07:55 +0700 Subject: [PATCH 16/32] improve run script --- templates/types/streaming/fastapi/run.py | 70 +++++++++++++++++------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 20c79c25a..11c2a9dc2 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,16 +1,23 @@ -import os +import shutil +from pathlib import Path +from shutil import which +from subprocess import DEVNULL, CalledProcessError, run import rich -def check_package_manager(): +def check_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 name of the available package manager ('pnpm' or 'npm') """ - if os.system("pnpm --version") == 0: + if which("pnpm") is not None: return "pnpm" - if os.system("npm --version") == 0: + if which("npm") is not None: return "npm" raise SystemError( "Neither pnpm nor npm is installed. Please install Node.js and a package manager first." @@ -20,27 +27,52 @@ def check_package_manager(): def build(): """ Build the frontend and copy the static files to the backend. + + Raises: + SystemError: If any build step fails """ - package_manager = check_package_manager() - rich.print( - f"\n[bold]Installing frontend dependencies using {package_manager}. It might take a while...[/bold]" - ) - os.system(f"cd .frontend && {package_manager} i") + frontend_dir = Path(".frontend") + static_dir = Path("static") - rich.print("\n[bold]Building the frontend[/bold]") - os.system(f"cd .frontend && {package_manager} run build") + try: + package_manager = check_package_manager() + rich.print( + f"\n[bold]Installing frontend dependencies using {package_manager}. It might take a while...[/bold]" + ) + run([package_manager, "install"], cwd=frontend_dir, check=True, stderr=DEVNULL) - os.system("mkdir -p static && rm -rf static/* && cp -r .frontend/out/* static") - rich.print( - "\n[bold]Built frontend successfully![/bold]" - "\n[bold]Run: 'poetry run dev' to start the app[/bold]" - "\n[bold]Don't forget to update the .env file![/bold]" - ) + 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 dev' 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(): """ - Start fastapi app. + Start fastapi app using poetry's virtual environment. + + Raises: + SystemError: If the app fails to start """ rich.print("\n[bold]Starting app[/bold]") - os.system("poetry run python main.py") + try: + run(["poetry", "run", "python", "main.py"], check=True) + except KeyboardInterrupt: + rich.print("\n[bold yellow]Shutting down...[/bold yellow]") + return + except Exception as e: + raise SystemError(f"Failed to start app: {str(e)}") from e From 0c0bbf08af51e75e19946b71cd10c3b44f8e0fb2 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 10:19:19 +0700 Subject: [PATCH 17/32] improve code --- helpers/run-app.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/run-app.ts b/helpers/run-app.ts index b03e53a99..902fb98b1 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -15,8 +15,9 @@ const createProcess = ( if (code !== 0) { console.log(`Child process exited with code=${code}`); reject(code); + } else { + resolve(); } - resolve(); }) .on("error", function (err) { console.log("Error when running child process: ", err); From 96f9f7258aaecf621d2911c6ed6200d5f2a142c6 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 10:38:05 +0700 Subject: [PATCH 18/32] add changeset and fix run script issue in Windows --- .changeset/popular-dryers-check.md | 5 +++++ templates/types/streaming/fastapi/run.py | 23 ++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 .changeset/popular-dryers-check.md diff --git a/.changeset/popular-dryers-check.md b/.changeset/popular-dryers-check.md new file mode 100644 index 000000000..59c3b5698 --- /dev/null +++ b/.changeset/popular-dryers-check.md @@ -0,0 +1,5 @@ +--- +"create-llama": patch +--- + +Simplify the code of the FastAPI fullstack app diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 11c2a9dc2..5c232cfc4 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,7 +1,7 @@ import shutil from pathlib import Path from shutil import which -from subprocess import DEVNULL, CalledProcessError, run +from subprocess import CalledProcessError, run import rich @@ -15,10 +15,18 @@ def check_package_manager() -> str: Returns: str: The name of the available package manager ('pnpm' or 'npm') """ - if which("pnpm") is not None: - return "pnpm" - if which("npm") is not None: - return "npm" + # On Windows, we need to check for .cmd extensions + pnpm_cmds = ["pnpm", "pnpm.cmd"] + npm_cmds = ["npm", "npm.cmd"] + + for cmd in pnpm_cmds: + if which(cmd) is not None: + return cmd + + for cmd in npm_cmds: + if which(cmd) is not None: + return cmd + raise SystemError( "Neither pnpm nor npm is installed. Please install Node.js and a package manager first." ) @@ -39,7 +47,8 @@ def build(): rich.print( f"\n[bold]Installing frontend dependencies using {package_manager}. It might take a while...[/bold]" ) - run([package_manager, "install"], cwd=frontend_dir, check=True, stderr=DEVNULL) + # Simple command execution without shell=True or capture_output + run([package_manager, "install"], cwd=frontend_dir, check=True) rich.print("\n[bold]Building the frontend[/bold]") run([package_manager, "run", "build"], cwd=frontend_dir, check=True) @@ -70,7 +79,7 @@ def dev(): """ rich.print("\n[bold]Starting app[/bold]") try: - run(["poetry", "run", "python", "main.py"], check=True) + run(["poetry", "run", "python", "main.py"], check=True, shell=True) except KeyboardInterrupt: rich.print("\n[bold yellow]Shutting down...[/bold yellow]") return From 3f61cc5b417ae9f35cc7bbcbcc0188161dd5c416 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 10:45:36 +0700 Subject: [PATCH 19/32] fix Windows issue --- templates/types/streaming/fastapi/run.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 5c232cfc4..eb0935cd1 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -13,19 +13,21 @@ def check_package_manager() -> str: Raises SystemError if neither is installed. Returns: - str: The name of the available package manager ('pnpm' or 'npm') + 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: - if which(cmd) is not None: - return cmd + cmd_path = which(cmd) + if cmd_path is not None: + return cmd_path for cmd in npm_cmds: - if which(cmd) is not None: - return cmd + 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." @@ -45,9 +47,9 @@ def build(): try: package_manager = check_package_manager() rich.print( - f"\n[bold]Installing frontend dependencies using {package_manager}. It might take a while...[/bold]" + f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]" ) - # Simple command execution without shell=True or capture_output + # Use the full path to the package manager run([package_manager, "install"], cwd=frontend_dir, check=True) rich.print("\n[bold]Building the frontend[/bold]") From acb293c12bf61163520ae089cf77bcb34e651cfd Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 11:11:43 +0700 Subject: [PATCH 20/32] remove shell --- templates/types/streaming/fastapi/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index eb0935cd1..fa6b2a2cc 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -81,7 +81,7 @@ def dev(): """ rich.print("\n[bold]Starting app[/bold]") try: - run(["poetry", "run", "python", "main.py"], check=True, shell=True) + run(["poetry", "run", "python", "main.py"], check=True) except KeyboardInterrupt: rich.print("\n[bold yellow]Shutting down...[/bold yellow]") return From f373311d1f484179127c454e4c4902d05a004a0b Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 11:27:01 +0700 Subject: [PATCH 21/32] simplify reflex port --- e2e/shared/extractor_template.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/e2e/shared/extractor_template.spec.ts b/e2e/shared/extractor_template.spec.ts index 10bcce293..698d80527 100644 --- a/e2e/shared/extractor_template.spec.ts +++ b/e2e/shared/extractor_template.spec.ts @@ -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,15 +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, + port: appPort, postInstallAction: "runApp", }); name = result.projectName; @@ -53,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, }); From 26dfec6435a9907ba1bab022518ad1707beac06d Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 11:45:58 +0700 Subject: [PATCH 22/32] remove backend port reflex --- helpers/run-app.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 902fb98b1..89c0224ab 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -35,15 +35,12 @@ export function buildFrontend(appPath: string, framework: TemplateFramework) { } export function runReflexApp(appPath: string, port: number) { - const backendPort = port + 1; const commandArgs = [ "run", "reflex", "run", "--frontend-port", port.toString(), - "--backend-port", - backendPort.toString(), ]; return createProcess("poetry", commandArgs, { stdio: "inherit", From 60f546eb6f3e96a566c34c1c9eafd6e37f0b3d1a Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 16:58:58 +0700 Subject: [PATCH 23/32] update dev mode --- templates/.gitignore | 1 - .../fastapi/app/middlewares/frontend.py | 69 ++++++ templates/types/streaming/fastapi/gitignore | 1 - templates/types/streaming/fastapi/main.py | 27 +-- .../types/streaming/fastapi/pyproject.toml | 6 +- templates/types/streaming/fastapi/run.py | 201 ++++++++++++++---- 6 files changed, 252 insertions(+), 53 deletions(-) create mode 100644 templates/types/streaming/fastapi/app/middlewares/frontend.py diff --git a/templates/.gitignore b/templates/.gitignore index 43edea526..ec6c67b63 100644 --- a/templates/.gitignore +++ b/templates/.gitignore @@ -1,4 +1,3 @@ __pycache__ poetry.lock storage -.frontend/ diff --git a/templates/types/streaming/fastapi/app/middlewares/frontend.py b/templates/types/streaming/fastapi/app/middlewares/frontend.py new file mode 100644 index 000000000..d723c651a --- /dev/null +++ b/templates/types/streaming/fastapi/app/middlewares/frontend.py @@ -0,0 +1,69 @@ +import logging +import os +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, excluded_paths: Set[str]): + self.app = app + self.excluded_paths = excluded_paths + self.frontend_endpoint = os.getenv("FRONTEND_ENDPOINT", "http://localhost:3000") + + async def _request_frontend(self, request: Request, path: str): + async with httpx.AsyncClient() 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) diff --git a/templates/types/streaming/fastapi/gitignore b/templates/types/streaming/fastapi/gitignore index 0f7a2f1af..dec47a300 100644 --- a/templates/types/streaming/fastapi/gitignore +++ b/templates/types/streaming/fastapi/gitignore @@ -2,5 +2,4 @@ __pycache__ storage .env output -.frontend/ static/ diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py index 7660a1019..1ebd133a0 100644 --- a/templates/types/streaming/fastapi/main.py +++ b/templates/types/streaming/fastapi/main.py @@ -9,6 +9,7 @@ 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 @@ -23,16 +24,6 @@ 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=["*"], - ) - def mount_static_files(directory, path, html=False): if os.path.exists(directory): @@ -50,8 +41,20 @@ def mount_static_files(directory, path, html=False): mount_static_files(DATA_DIR, "/api/files/data") # Mount the output files from tools mount_static_files("output", "/api/files/output") -# Mount static files from the frontend -mount_static_files("static", "/", html=True) + +if environment == "dev": + app.add_middleware( + FrontendProxyMiddleware, + excluded_paths=set(route.path for route in app.router.routes), + ) + logger.warning("Running in development mode - allowing CORS for all origins") + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) if __name__ == "__main__": diff --git a/templates/types/streaming/fastapi/pyproject.toml b/templates/types/streaming/fastapi/pyproject.toml index b969f9098..f0d1426c1 100644 --- a/templates/types/streaming/fastapi/pyproject.toml +++ b/templates/types/streaming/fastapi/pyproject.toml @@ -7,8 +7,9 @@ readme = "README.md" [tool.poetry.scripts] generate = "app.engine.generate:generate_datasource" -build = "run:build" -dev = "run:dev" +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" @@ -22,6 +23,7 @@ rich = "^13.9.4" [tool.poetry.group.dev.dependencies] mypy = "^1.8.0" +psutil = "^6.1.0" [build-system] requires = ["poetry-core"] diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index fa6b2a2cc..18225f598 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,39 +1,15 @@ +import asyncio import shutil +import time +from asyncio.subprocess import Process from pathlib import Path from shutil import which from subprocess import CalledProcessError, run +import psutil import rich -def check_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 build(): """ Build the frontend and copy the static files to the backend. @@ -45,7 +21,7 @@ def build(): static_dir = Path("static") try: - package_manager = check_package_manager() + 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]" ) @@ -73,17 +49,168 @@ def build(): def dev(): + asyncio.run(start_development_servers()) + + +def prod(): + # TODO: Implement production mode + raise NotImplementedError("Production mode is not implemented yet") + + +async def start_development_servers(): """ - Start fastapi app using poetry's virtual environment. + Start both frontend and backend development servers. + Frontend runs with hot reloading, backend runs FastAPI server. Raises: - SystemError: If the app fails to start + SystemError: If either server fails to start """ - rich.print("\n[bold]Starting app[/bold]") + rich.print("\n[bold]Starting development servers[/bold]") + try: - run(["poetry", "run", "python", "main.py"], check=True) - except KeyboardInterrupt: - rich.print("\n[bold yellow]Shutting down...[/bold yellow]") - return + frontend_process, frontend_port = await _run_frontend() + frontend_endpoint = f"http://localhost:{frontend_port}" + backend_process = await _run_backend(frontend_endpoint) + + try: + # Wait for processes to complete + await asyncio.gather(frontend_process.wait(), backend_process.wait()) + except (asyncio.CancelledError, KeyboardInterrupt): + rich.print("\n[bold yellow]Shutting down...[/bold yellow]") + finally: + # Terminate both processes + for process in (frontend_process, backend_process): + 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 app: {str(e)}") from e + raise SystemError(f"Failed to start development servers: {str(e)}") from e + + +async def _run_frontend() -> 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 + """ + package_manager = _get_node_package_manager() + frontend_process = await asyncio.create_subprocess_exec( + package_manager, + "run", + "dev", + cwd=".frontend", + ) + rich.print( + f"[bold]Waiting for frontend to start, process id: {frontend_process.pid}[/bold]" + ) + frontend_port = await _get_process_port(frontend_process.pid) + rich.print( + f"\n[bold green]Frontend dev server running on port {frontend_port}[/bold green]" + ) + return frontend_process, frontend_port + + +async def _run_backend(frontend_endpoint: str) -> Process: + """ + Start the backend development server. + + Args: + frontend_port: The port number the frontend is running on + Returns: + Process: The backend process + """ + poetry_executable = _get_poetry_executable() + return await asyncio.create_subprocess_exec( + poetry_executable, + "run", + "python", + "main.py", + env={"FRONTEND_ENDPOINT": frontend_endpoint}, + ) + + +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: + 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.") + + +async def _get_process_port(pid: int, timeout: int = 30) -> int: + """ + Get the port number that a process or its children are listening on. + Specifically for Node.js frontend development servers. + + Args: + pid: Process ID to check + timeout: Maximum time to wait for port detection in seconds + + Returns: + int: The port number the process is listening on + + Raises: + TimeoutError: If no port is detected within the timeout period + ProcessLookupError: If the process doesn't exist + """ + start_time = time.time() + + while time.time() - start_time < timeout: + try: + # Get the parent process and all its children + parent = psutil.Process(pid) + processes = [parent] + parent.children(recursive=True) + + # Check each process for listening ports + for process in processes: + try: + connections = process.connections() + # Look for listening TCP connections + for conn in connections: + if conn.status == "LISTEN": + return conn.laddr.port + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + except (psutil.NoSuchProcess, psutil.AccessDenied): + raise ProcessLookupError(f"Process {pid} not found or access denied") + + # Wait a bit before checking again + await asyncio.sleep(0.5) + + raise TimeoutError( + f"Could not detect port for process {pid} within {timeout} seconds" + ) From 1b993b937d4b65dbbada708d97c431aab40f587e Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 17:52:41 +0700 Subject: [PATCH 24/32] update runner script --- .../types/streaming/fastapi/pyproject.toml | 1 - templates/types/streaming/fastapi/run.py | 131 ++++++++++-------- 2 files changed, 71 insertions(+), 61 deletions(-) diff --git a/templates/types/streaming/fastapi/pyproject.toml b/templates/types/streaming/fastapi/pyproject.toml index f0d1426c1..ad2d8ad74 100644 --- a/templates/types/streaming/fastapi/pyproject.toml +++ b/templates/types/streaming/fastapi/pyproject.toml @@ -23,7 +23,6 @@ rich = "^13.9.4" [tool.poetry.group.dev.dependencies] mypy = "^1.8.0" -psutil = "^6.1.0" [build-system] requires = ["poetry-core"] diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 18225f598..51e81a6f5 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,14 +1,16 @@ import asyncio import shutil -import time +import socket from asyncio.subprocess import Process from pathlib import Path from shutil import which from subprocess import CalledProcessError, run -import psutil import rich +FRONTEND_DIR = Path(".frontend") +DEFAULT_FRONTEND_PORT = 3000 + def build(): """ @@ -17,25 +19,20 @@ def build(): Raises: SystemError: If any build step fails """ - frontend_dir = Path(".frontend") static_dir = Path("static") try: 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]" - ) - # Use the full path to the package manager - run([package_manager, "install"], cwd=frontend_dir, check=True) + _install_frontend_dependencies() rich.print("\n[bold]Building the frontend[/bold]") - run([package_manager, "run", "build"], cwd=frontend_dir, check=True) + 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) + shutil.copytree(FRONTEND_DIR / "out", static_dir, dirs_exist_ok=True) rich.print( "\n[bold]Built frontend successfully![/bold]" @@ -90,28 +87,45 @@ async def start_development_servers(): raise SystemError(f"Failed to start development servers: {str(e)}") from e -async def _run_frontend() -> tuple[Process, int]: +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", - cwd=".frontend", + "-p", + str(port), + cwd=FRONTEND_DIR, ) rich.print( - f"[bold]Waiting for frontend to start, process id: {frontend_process.pid}[/bold]" + f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]" ) - frontend_port = await _get_process_port(frontend_process.pid) - rich.print( - f"\n[bold green]Frontend dev server running on port {frontend_port}[/bold green]" - ) - return frontend_process, frontend_port + # 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 + print("Return code:", frontend_process.returncode) + 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(frontend_endpoint: str) -> Process: @@ -123,6 +137,7 @@ async def _run_backend(frontend_endpoint: str) -> Process: Returns: Process: The backend process """ + rich.print("\n[bold]Starting backend FastAPI server...[/bold]") poetry_executable = _get_poetry_executable() return await asyncio.create_subprocess_exec( poetry_executable, @@ -133,6 +148,14 @@ async def _run_backend(frontend_endpoint: str) -> Process: ) +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. @@ -162,6 +185,14 @@ def _get_node_package_manager() -> str: 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) @@ -170,47 +201,27 @@ def _get_poetry_executable() -> str: raise SystemError("Poetry is not installed. Please install Poetry first.") -async def _get_process_port(pid: int, timeout: int = 30) -> int: +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: """ - Get the port number that a process or its children are listening on. - Specifically for Node.js frontend development servers. - - Args: - pid: Process ID to check - timeout: Maximum time to wait for port detection in seconds - - Returns: - int: The port number the process is listening on - - Raises: - TimeoutError: If no port is detected within the timeout period - ProcessLookupError: If the process doesn't exist + Find a free port starting from the given port number. """ - start_time = time.time() - - while time.time() - start_time < timeout: - try: - # Get the parent process and all its children - parent = psutil.Process(pid) - processes = [parent] + parent.children(recursive=True) - - # Check each process for listening ports - for process in processes: - try: - connections = process.connections() - # Look for listening TCP connections - for conn in connections: - if conn.status == "LISTEN": - return conn.laddr.port - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - except (psutil.NoSuchProcess, psutil.AccessDenied): - raise ProcessLookupError(f"Process {pid} not found or access denied") - - # Wait a bit before checking again - await asyncio.sleep(0.5) - - raise TimeoutError( - f"Could not detect port for process {pid} within {timeout} seconds" - ) + for port in range(start_port, 65535): + if _is_bindable_port(port): + return port + raise SystemError("No free port found") From e2e53e211c6385373dc7f6d7b542085f8bbb87ae Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 19:17:28 +0700 Subject: [PATCH 25/32] add prod support and update docs --- .../agents/python/blog/README-template.md | 6 +- .../financial_report/README-template.md | 6 +- .../python/form_filling/README-template.md | 6 +- .../streaming/fastapi/README-template.md | 16 ++--- .../types/streaming/fastapi/app/config.py | 3 + .../fastapi/app/middlewares/frontend.py | 10 ++- templates/types/streaming/fastapi/main.py | 25 +++++-- templates/types/streaming/fastapi/run.py | 70 ++++++++++++++++--- 8 files changed, 102 insertions(+), 40 deletions(-) diff --git a/templates/components/agents/python/blog/README-template.md b/templates/components/agents/python/blog/README-template.md index 5d17a5ac8..c600bc79c 100644 --- a/templates/components/agents/python/blog/README-template.md +++ b/templates/components/agents/python/blog/README-template.md @@ -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`. @@ -49,10 +49,10 @@ You can start editing the API by modifying `app/api/routers/chat.py` or `app/exa 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`: +The API allows CORS for all origins to simplify development. For **production**, you should run: ``` -ENVIRONMENT=prod poetry run python main.py +poetry run prod ``` ## Learn More diff --git a/templates/components/agents/python/financial_report/README-template.md b/templates/components/agents/python/financial_report/README-template.md index 0f3beb238..671f62548 100644 --- a/templates/components/agents/python/financial_report/README-template.md +++ b/templates/components/agents/python/financial_report/README-template.md @@ -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`. @@ -37,10 +37,10 @@ You can start editing the API by modifying `app/api/routers/chat.py` or `app/wor 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`: +The API allows CORS for all origins to simplify development. For **production**, you should run: ``` -ENVIRONMENT=prod poetry run python main.py +poetry run prod ``` ## Learn More diff --git a/templates/components/agents/python/form_filling/README-template.md b/templates/components/agents/python/form_filling/README-template.md index be6ec38e3..cf5b53472 100644 --- a/templates/components/agents/python/form_filling/README-template.md +++ b/templates/components/agents/python/form_filling/README-template.md @@ -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 @@ -43,10 +43,10 @@ You can start editing the API by modifying `app/api/routers/chat.py` or `app/wor 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`: +The API allows CORS for all origins to simplify development. For **production**, you should run: ``` -ENVIRONMENT=prod poetry run python main.py +poetry run prod ``` ## Learn More diff --git a/templates/types/streaming/fastapi/README-template.md b/templates/types/streaming/fastapi/README-template.md index 8f34f3f6e..adc501c15 100644 --- a/templates/types/streaming/fastapi/README-template.md +++ b/templates/types/streaming/fastapi/README-template.md @@ -21,18 +21,14 @@ Second, generate the embeddings of the documents in the `./data` directory (if t poetry run generate ``` -Third, if you want to use the app with a chat UI, you will need to build the frontend once. If not, you can skip this step: - -``` -poetry run build -``` - -Finally, run the app: +Third, run the app: ``` 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 @@ -56,12 +52,10 @@ curl --location 'localhost:8000/api/chat/request' \ You can start editing the API endpoints by modifying `app/api/routers/chat.py`. The endpoints auto-update as you save the file. You can delete the endpoint you're not using. -If you have built the frontend, open [http://localhost:8000](http://localhost:8000) in your browser to access the chat UI. If not, open [http://localhost:8000/docs](http://localhost:8000/docs) to view the Swagger UI documentation for 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`: +The API allows CORS for all origins to simplify development. For **production**, you should run: ``` -ENVIRONMENT=prod python main.py +poetry run prod ``` ## Using Docker diff --git a/templates/types/streaming/fastapi/app/config.py b/templates/types/streaming/fastapi/app/config.py index 29fa8d9a2..31daa5117 100644 --- a/templates/types/streaming/fastapi/app/config.py +++ b/templates/types/streaming/fastapi/app/config.py @@ -1 +1,4 @@ +import os + DATA_DIR = "data" +STATIC_DIR = os.getenv("STATIC_DIR", "static") diff --git a/templates/types/streaming/fastapi/app/middlewares/frontend.py b/templates/types/streaming/fastapi/app/middlewares/frontend.py index d723c651a..b9ec43609 100644 --- a/templates/types/streaming/fastapi/app/middlewares/frontend.py +++ b/templates/types/streaming/fastapi/app/middlewares/frontend.py @@ -1,5 +1,4 @@ import logging -import os from typing import Set import httpx @@ -14,10 +13,15 @@ class FrontendProxyMiddleware: Proxy requests to the frontend development server """ - def __init__(self, app, excluded_paths: Set[str]): + def __init__( + self, + app, + frontend_endpoint: str, + excluded_paths: Set[str], + ): self.app = app self.excluded_paths = excluded_paths - self.frontend_endpoint = os.getenv("FRONTEND_ENDPOINT", "http://localhost:3000") + self.frontend_endpoint = frontend_endpoint async def _request_frontend(self, request: Request, path: str): async with httpx.AsyncClient() as client: diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py index 1ebd133a0..567421851 100644 --- a/templates/types/streaming/fastapi/main.py +++ b/templates/types/streaming/fastapi/main.py @@ -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() @@ -14,6 +14,7 @@ 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 app = FastAPI() @@ -43,10 +44,20 @@ def mount_static_files(directory, path, html=False): mount_static_files("output", "/api/files/output") if environment == "dev": - app.add_middleware( - FrontendProxyMiddleware, - excluded_paths=set(route.path for route in app.router.routes), - ) + 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.router.routes), + ) + else: + logger.warning("No frontend endpoint - starting API server only") + + @app.get("/") + async def redirect_to_docs(): + return RedirectResponse(url="/docs") + logger.warning("Running in development mode - allowing CORS for all origins") app.add_middleware( CORSMiddleware, @@ -55,7 +66,9 @@ def mount_static_files(directory, path, html=False): allow_methods=["*"], allow_headers=["*"], ) - +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") diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 51e81a6f5..35b7aa488 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -1,4 +1,5 @@ import asyncio +import os import shutil import socket from asyncio.subprocess import Process @@ -6,10 +7,15 @@ from shutil import which from subprocess import CalledProcessError, run +import dotenv import rich -FRONTEND_DIR = Path(".frontend") +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(): @@ -50,8 +56,7 @@ def dev(): def prod(): - # TODO: Implement production mode - raise NotImplementedError("Production mode is not implemented yet") + asyncio.run(start_production_server()) async def start_development_servers(): @@ -65,18 +70,31 @@ async def start_development_servers(): rich.print("\n[bold]Starting development servers[/bold]") try: - frontend_process, frontend_port = await _run_frontend() - frontend_endpoint = f"http://localhost:{frontend_port}" - backend_process = await _run_backend(frontend_endpoint) + 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(frontend_process.wait(), backend_process.wait()) + 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 (frontend_process, backend_process): + for process in processes: process.terminate() try: await asyncio.wait_for(process.wait(), timeout=5) @@ -87,6 +105,28 @@ async def start_development_servers(): 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, @@ -117,7 +157,6 @@ async def _run_frontend( for _ in range(timeout): await asyncio.sleep(1) # Check if the frontend is accessible (port is open) or frontend_process is running - print("Return code:", frontend_process.returncode) if frontend_process.returncode is not None: raise RuntimeError("Could not start frontend dev server") if not _is_bindable_port(port): @@ -128,7 +167,9 @@ async def _run_frontend( raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds") -async def _run_backend(frontend_endpoint: str) -> Process: +async def _run_backend( + envs: dict[str, str | None] = {}, +) -> Process: """ Start the backend development server. @@ -137,6 +178,8 @@ async def _run_backend(frontend_endpoint: str) -> Process: 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( @@ -144,7 +187,7 @@ async def _run_backend(frontend_endpoint: str) -> Process: "run", "python", "main.py", - env={"FRONTEND_ENDPOINT": frontend_endpoint}, + env=envs, ) @@ -225,3 +268,8 @@ def _find_free_port(start_port: int) -> int: 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() From 6c8497e9439e09eec4290a73887a920c97292b55 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 19:43:32 +0700 Subject: [PATCH 26/32] add timeout for sending request in proxy --- .../types/streaming/fastapi/app/middlewares/frontend.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/templates/types/streaming/fastapi/app/middlewares/frontend.py b/templates/types/streaming/fastapi/app/middlewares/frontend.py index b9ec43609..83321c97c 100644 --- a/templates/types/streaming/fastapi/app/middlewares/frontend.py +++ b/templates/types/streaming/fastapi/app/middlewares/frontend.py @@ -23,8 +23,13 @@ def __init__( self.excluded_paths = excluded_paths self.frontend_endpoint = frontend_endpoint - async def _request_frontend(self, request: Request, path: str): - async with httpx.AsyncClient() as client: + 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}" From e72b9cc98859f0af78178f7ec6b38b91439d1b28 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 19 Nov 2024 19:56:44 +0700 Subject: [PATCH 27/32] fix mypy --- templates/types/streaming/fastapi/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py index 567421851..30d92cf29 100644 --- a/templates/types/streaming/fastapi/main.py +++ b/templates/types/streaming/fastapi/main.py @@ -49,7 +49,9 @@ def mount_static_files(directory, path, html=False): app.add_middleware( FrontendProxyMiddleware, frontend_endpoint=frontend_endpoint, - excluded_paths=set(route.path for route in app.router.routes), + excluded_paths=set( + route.path for route in app.routes if hasattr(route, "path") + ), ) else: logger.warning("No frontend endpoint - starting API server only") From 424e0c1b91ce1721a8e48f58ae44181b31d26097 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 20 Nov 2024 10:37:41 +0700 Subject: [PATCH 28/32] update docs and remove cors --- .../components/agents/python/blog/README-template.md | 4 ++-- .../agents/python/financial_report/README-template.md | 4 ++-- .../agents/python/form_filling/README-template.md | 4 ++-- templates/types/streaming/fastapi/README-template.md | 2 +- templates/types/streaming/fastapi/main.py | 10 ---------- templates/types/streaming/fastapi/run.py | 2 +- 6 files changed, 8 insertions(+), 18 deletions(-) diff --git a/templates/components/agents/python/blog/README-template.md b/templates/components/agents/python/blog/README-template.md index c600bc79c..b4e04a4ba 100644 --- a/templates/components/agents/python/blog/README-template.md +++ b/templates/components/agents/python/blog/README-template.md @@ -47,9 +47,9 @@ 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. For **production**, you should run: +To start the app in **production**, run: ``` poetry run prod diff --git a/templates/components/agents/python/financial_report/README-template.md b/templates/components/agents/python/financial_report/README-template.md index 671f62548..7b8122eb9 100644 --- a/templates/components/agents/python/financial_report/README-template.md +++ b/templates/components/agents/python/financial_report/README-template.md @@ -35,9 +35,9 @@ 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. For **production**, you should run: +To start the app in **production**, run: ``` poetry run prod diff --git a/templates/components/agents/python/form_filling/README-template.md b/templates/components/agents/python/form_filling/README-template.md index cf5b53472..6c6717aac 100644 --- a/templates/components/agents/python/form_filling/README-template.md +++ b/templates/components/agents/python/form_filling/README-template.md @@ -41,9 +41,9 @@ 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. For **production**, you should run: +To start the app in **production**, run: ``` poetry run prod diff --git a/templates/types/streaming/fastapi/README-template.md b/templates/types/streaming/fastapi/README-template.md index adc501c15..c04ef0b37 100644 --- a/templates/types/streaming/fastapi/README-template.md +++ b/templates/types/streaming/fastapi/README-template.md @@ -52,7 +52,7 @@ 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. -The API allows CORS for all origins to simplify development. For **production**, you should run: +To start the app in **production**, run: ``` poetry run prod diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py index 30d92cf29..9796e0f0a 100644 --- a/templates/types/streaming/fastapi/main.py +++ b/templates/types/streaming/fastapi/main.py @@ -13,7 +13,6 @@ 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 @@ -59,15 +58,6 @@ def mount_static_files(directory, path, html=False): @app.get("/") async def redirect_to_docs(): return RedirectResponse(url="/docs") - - logger.warning("Running in development mode - allowing CORS for all origins") - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) else: # Mount the frontend static files (production) mount_static_files(STATIC_DIR, "/", html=True) diff --git a/templates/types/streaming/fastapi/run.py b/templates/types/streaming/fastapi/run.py index 35b7aa488..8286c373c 100644 --- a/templates/types/streaming/fastapi/run.py +++ b/templates/types/streaming/fastapi/run.py @@ -42,7 +42,7 @@ def build(): rich.print( "\n[bold]Built frontend successfully![/bold]" - "\n[bold]Run: 'poetry run dev' to start the app[/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: From ed0f2b6a95a292b4bb8ca4a383acef1173125161 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 20 Nov 2024 10:53:24 +0700 Subject: [PATCH 29/32] remove build --- helpers/devcontainer.ts | 28 ++++++---------------------- helpers/run-app.ts | 13 ------------- 2 files changed, 6 insertions(+), 35 deletions(-) diff --git a/helpers/devcontainer.ts b/helpers/devcontainer.ts index e1dfe8e75..153add212 100644 --- a/helpers/devcontainer.ts +++ b/helpers/devcontainer.ts @@ -5,36 +5,21 @@ 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" - ? "poetry install && poetry run build" - : "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); @@ -54,7 +39,6 @@ export const writeDevcontainer = async ( const devcontainerContent = renderDevcontainerContent( templatesDir, framework, - frontend, ); fs.mkdirSync(devcontainerDir); await fs.promises.writeFile( diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 89c0224ab..7d7b0cf01 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -26,14 +26,6 @@ const createProcess = ( }); }; -export function buildFrontend(appPath: string, framework: TemplateFramework) { - const packageManager = framework === "fastapi" ? "poetry" : "npm"; - return createProcess(packageManager, ["run", "build"], { - stdio: "inherit", - cwd: appPath, - }); -} - export function runReflexApp(appPath: string, port: number) { const commandArgs = [ "run", @@ -72,11 +64,6 @@ export async function runApp( port?: number, ): Promise { try { - // Build frontend if needed - if (frontend && (template === "streaming" || template === "multiagent")) { - await buildFrontend(appPath, framework); - } - // Start the app const defaultPort = framework === "nextjs" ? 3000 : 8000; const appRunner = From d3e0fb006ed8dd7abf010ab85c771ed227b2e4fc Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 20 Nov 2024 11:29:00 +0700 Subject: [PATCH 30/32] update for reflex --- helpers/run-app.ts | 5 +++-- index.ts | 8 +------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/helpers/run-app.ts b/helpers/run-app.ts index 7d7b0cf01..991a9790d 100644 --- a/helpers/run-app.ts +++ b/helpers/run-app.ts @@ -59,13 +59,14 @@ export function runTSApp(appPath: string, port: number) { export async function runApp( appPath: string, template: string, - frontend: boolean, framework: TemplateFramework, port?: number, ): Promise { try { // Start the app - const defaultPort = framework === "nextjs" ? 3000 : 8000; + const defaultPort = + framework === "nextjs" || template === "extractor" ? 3000 : 8000; + const appRunner = template === "extractor" ? runReflexApp diff --git a/index.ts b/index.ts index 8b3bcf214..8b87b1199 100644 --- a/index.ts +++ b/index.ts @@ -355,13 +355,7 @@ Please check ${cyan( } } else if (answers.postInstallAction === "runApp") { console.log(`Running app in ${root}...`); - await runApp( - root, - answers.template, - answers.frontend, - answers.framework, - options.port, - ); + await runApp(root, answers.template, answers.framework, options.port); } } From 603f576a935502604e1eabec2a8a750b5eebbb2e Mon Sep 17 00:00:00 2001 From: Marcus Schiesser Date: Wed, 20 Nov 2024 11:54:39 +0700 Subject: [PATCH 31/32] Update .changeset/popular-dryers-check.md --- .changeset/popular-dryers-check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/popular-dryers-check.md b/.changeset/popular-dryers-check.md index 59c3b5698..ec91439a6 100644 --- a/.changeset/popular-dryers-check.md +++ b/.changeset/popular-dryers-check.md @@ -2,4 +2,4 @@ "create-llama": patch --- -Simplify the code of the FastAPI fullstack app +Improve DX for Python template (use one deployment instead of two) From 8079ba4b45c1bb58891c1a8da77fe2411cf0671d Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 20 Nov 2024 12:13:43 +0700 Subject: [PATCH 32/32] remove legacy fullstack files --- templates/.gitignore | 3 --- templates/README-fullstack.md | 18 ------------------ 2 files changed, 21 deletions(-) delete mode 100644 templates/.gitignore delete mode 100644 templates/README-fullstack.md diff --git a/templates/.gitignore b/templates/.gitignore deleted file mode 100644 index ec6c67b63..000000000 --- a/templates/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -__pycache__ -poetry.lock -storage diff --git a/templates/README-fullstack.md b/templates/README-fullstack.md deleted file mode 100644 index 5a41b8cfc..000000000 --- a/templates/README-fullstack.md +++ /dev/null @@ -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!