Compare commits

...

17 Commits

Author SHA1 Message Date
Thuc Pham 97a7d9bc25 chore: move @llamaindex/server to chat-ui repo (#709) 2025-07-16 09:15:42 +08:00
github-actions[bot] 2f085c1c95 Release 0.6.3 (#708)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-15 10:10:46 +07:00
Thuc Pham fec752eb63 refactor: llamacloud configs (#707)
* refactor: llamacloud configs

* refactor (ts-proxy): update create llama for 2 types of server file

* update CL for ts

* update doc

* fix api/files path

* update document

* Create gorgeous-squids-run.md
2025-07-15 09:33:22 +07:00
github-actions[bot] 63f5f6f956 Release 0.6.2 (#706)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-11 13:59:42 +08:00
Thuc Pham 93e2abe301 fix: unused imports and format (#705) 2025-07-11 12:08:10 +08:00
Thuc Pham 28b46be22a chore: replace Python examples with llama-deploy (#701) 2025-07-11 11:50:54 +08:00
github-actions[bot] b618e91e99 Release 0.2.10 (#704)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-10 16:43:18 +07:00
Thuc Pham 91ce4e1236 feat: support file server for python llamadeploy (#703)
* feat: support file server for python llamadeploy

* Create wise-ways-knock.md

* release chat-ui
2025-07-10 16:38:00 +07:00
github-actions[bot] 2b85420370 Release 0.2.9 (#699)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-01 14:58:46 +07:00
Thuc Pham 52cc37f206 feat: flag to enable useChatWorkflow (#697)
* feat: flag to enable useChatWorkflow

* add USE_CHAT_WORKFLOW config

* require CHAT_DEPLOYMENT and CHAT_WORKFLOW

* handleError

* onError

* revert error

* skip all api handling if in proxy mode

* revert adding option

* modify config

* not require workflow factory

* llamadeploy config

* fix proxy

* fix: serialize in config.js

* try modify next config

* fix: need basePath for other nextjs endpoints

* add condition

* use constants as central place to modify basePath

* add comment

* update constants

* check workflow factory

* Create brown-readers-whisper.md

* bump chat-ui

* fix lint
2025-07-01 14:38:30 +07:00
Thuc Pham 952b5b4908 fix: @jridgewell/sourcemap-codec issue (#700) 2025-06-30 17:17:49 +07:00
Andy Fowler e8004fd711 Fix broken devcontainer due to deleted repo (#698) 2025-06-30 10:56:27 +07:00
github-actions[bot] 48f6d849e6 Release 0.6.0 (#694)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-19 17:22:29 +07:00
Marcus Schiesser 02a9db3d40 chore: remove template and usellamaparse params 2025-06-19 16:19:16 +07:00
Marcus Schiesser 8fa8c3bad8 feat: readd asking for models for simple.ts (#693) 2025-06-19 09:58:47 +07:00
github-actions[bot] a221bc60f7 Release 0.1.23 (#690)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-13 17:51:48 +07:00
Thuc Pham 6c0fb51557 fix: stream not stop after sending HumanInputEvent (#689)
* fix: stream not stop after sending HumanInputEvent

* Create polite-bugs-develop.md

* decide to run e2e:ts:streaming or e2e:ts:server based on matrix.template-types

* fix scripts

* update changeset
2025-06-13 15:04:30 +07:00
426 changed files with 3381 additions and 31393 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
chore: bump @llamaindex/server 0.3.0 in templates
+9 -48
View File
@@ -22,8 +22,7 @@ jobs:
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["fastapi"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
template-types: ["streaming", "llamaindexserver"]
vectordbs: ["none", "llamacloud"]
defaults:
run:
shell: bash
@@ -64,23 +63,13 @@ jobs:
run: pnpm run pack-install
working-directory: packages/create-llama
- name: Build and store server package
run: |
pnpm run build
wheel_file=$(ls dist/*.whl | head -n 1)
mkdir -p "${{ runner.temp }}"
cp "$wheel_file" "${{ runner.temp }}/"
echo "SERVER_PACKAGE_PATH=${{ runner.temp }}/$(basename "$wheel_file")" >> $GITHUB_ENV
working-directory: python/llama-index-server
- name: Run Playwright tests for Python
run: pnpm run e2e:python
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
TEMPLATE_TYPE: ${{ matrix.template-types }}
VECTORDB: ${{ matrix.vectordbs }}
PYTHONIOENCODING: utf-8
PYTHONLEGACYWINDOWSSTDIO: utf-8
SERVER_PACKAGE_PATH: ${{ env.SERVER_PACKAGE_PATH }}
@@ -89,7 +78,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-${{ matrix.template-types }}
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs }}
path: packages/create-llama/playwright-report/
overwrite: true
retention-days: 30
@@ -100,12 +89,10 @@ jobs:
strategy:
fail-fast: true
matrix:
node-version: [20, 22]
python-version: ["3.11"]
node-version: [22]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
template-types: ["streaming", "llamaindexserver"]
vectordbs: ["none", "llamacloud"]
defaults:
run:
shell: bash
@@ -113,16 +100,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Add uv to PATH # Ensure uv is available in subsequent steps
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: pnpm/action-setup@v3
- name: Setup Node.js ${{ matrix.node-version }}
@@ -146,36 +123,20 @@ jobs:
run: pnpm run pack-install
working-directory: packages/create-llama
- name: Build server
run: pnpm run build
working-directory: packages/server
- name: Pack @llamaindex/server package
run: |
pnpm pack --pack-destination "${{ runner.temp }}"
if [ "${{ runner.os }}" == "Windows" ]; then
file=$(find "${{ runner.temp }}" -name "llamaindex-server-*.tgz" | head -n 1)
mv "$file" "${{ runner.temp }}/llamaindex-server.tgz"
else
mv ${{ runner.temp }}/llamaindex-server-*.tgz ${{ runner.temp }}/llamaindex-server.tgz
fi
working-directory: packages/server
- name: Run Playwright tests for TypeScript
run: pnpm run e2e:typescript
run: |
pnpm run e2e:ts
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
TEMPLATE_TYPE: ${{ matrix.template-types }}
SERVER_PACKAGE_PATH: ${{ runner.temp }}/llamaindex-server.tgz
VECTORDB: ${{ matrix.vectordbs }}
working-directory: packages/create-llama
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}-${{ matrix.template-types }}
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs}}-node${{ matrix.node-version }}
path: packages/create-llama/playwright-report/
overwrite: true
retention-days: 30
@@ -44,10 +44,6 @@ jobs:
- name: Run build
run: pnpm run build
- name: Run Typecheck for examples
run: pnpm run typecheck
working-directory: packages/server/examples
- name: Run Python format check
uses: chartboost/ruff-action@v1
with:
-2
View File
@@ -6,8 +6,6 @@ cache/
build/
.next/
out/
packages/server/server/
packages/server/project/
**/playwright-report/
**/test-results/
-17
View File
@@ -11,7 +11,6 @@ Create-llama is a monorepo containing CLI tools and server frameworks for buildi
### Monorepo Structure
- **`packages/create-llama/`**: Main CLI tool for scaffolding LlamaIndex applications
- **`packages/server/`**: TypeScript/Next.js server framework (`@llamaindex/server`)
- **`python/llama-index-server/`**: Python/FastAPI server framework
- **Root**: Workspace configuration and shared development tools
@@ -44,15 +43,6 @@ npm run e2e # Playwright tests for generated projects
npm run clean # Clean build artifacts and template caches
```
### TypeScript Server Package
```bash
cd packages/server
pnpm dev # Watch mode with bunchee
pnpm build # Multi-step build: ESM/CJS + Next.js + static assets
pnpm clean # Clean all build outputs
```
### Python Server Package
```bash
@@ -84,13 +74,6 @@ The CLI uses a sophisticated template system in `packages/create-llama/templates
## Server Framework Architecture
### TypeScript Server (`@llamaindex/server`)
- **Core**: `LlamaIndexServer` class wrapping Next.js with workflow support
- **Frontend**: React-based chat UI with shadcn/ui components
- **API**: `/api/chat` endpoint with streaming responses
- **Build Process**: Complex multi-step build including static assets for Python integration
### Python Server (`llama-index-server`)
- **Core**: `LlamaIndexServer` class extending FastAPI
+12 -16
View File
@@ -25,13 +25,10 @@ to start the development server. You can then visit [http://localhost:3000](http
## What you'll get
- A set of pre-configured use cases to get you started, e.g. Agentic RAG, Data Analysis, Report Generation, etc.
- A Next.js-powered front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
- Your choice of two back-ends:
- **Next.js**: if you select this option, youll have a full-stack Next.js application that you can deploy to a host like [Vercel](https://vercel.com/) in just a few clicks. This uses [LlamaIndex.TS](https://www.npmjs.com/package/llamaindex), our TypeScript library.
- **Python FastAPI**: if you select this option, youll get a separate backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like [Render](https://render.com/) or [fly.io](https://fly.io/). The separate Next.js front-end will connect to this backend.
- Each back-end has two endpoints:
- One streaming chat endpoint, that allow you to send the state of your chat and receive additional responses
- One endpoint to upload private files which can be used in your chat
- A front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
- Your choice of two frameworks:
- **Next.js**: if you select this option, youll have a full-stack Next.js application that you can deploy to a host like [Vercel](https://vercel.com/) in just a few clicks. This uses [LlamaIndex.TS](https://www.npmjs.com/package/llamaindex), our TypeScript library with [LlamaIndex Server for TS](https://npmjs.com/package/@llamaindex/server).
- **Python FastAPI**: if you select this option, youll get full-stack Python application powered by the [llama-index Python package](https://pypi.org/project/llama-index/) and [LlamaIndex Server for Python](https://pypi.org/project/llama-index-server/)
- The app uses OpenAI by default, so you'll need an OpenAI API key, or you can customize it to use any of the dozens of LLMs we support.
Here's how it looks like:
@@ -40,11 +37,11 @@ https://github.com/user-attachments/assets/d57af1a1-d99b-4e9c-98d9-4cbd1327eff8
## Using your data
Optionally, you can supply your own data; the app will index it and make use of it, e.g. to answer questions. Your generated app will have a folder called `data` (If you're using Express or Python and generate a frontend, it will be `./backend/data`).
Optionally, you can supply your own data; the app will index it and make use of it, e.g. to answer questions. Your generated app will have a folder called `data`.
The app will ingest any supported files you put in this directory. Your Next.js and Express apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
The app will ingest any supported files you put in this directory. Your Next.js apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
Before you can use your data, you need to index it. If you're using the Next.js or Express apps, run:
Before you can use your data, you need to index it. If you're using the Next.js apps, run:
```bash
npm run generate
@@ -60,11 +57,11 @@ uv run generate
## Customizing the AI models
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
The app will default to OpenAI's `gpt-4.1` LLM and `text-embedding-3-large` embedding model.
If you want to use different OpenAI models, add the `--ask-models` CLI parameter.
If you want to use different models, add the `--ask-models` CLI parameter.
You can also replace OpenAI with one of our [dozens of other supported LLMs](https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules.html).
You can also replace one of the default models with one of our [dozens of other supported LLMs](https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules.html).
To do so, you have to manually change the generated code (edit the `settings.ts` file for Typescript projects or the `settings.py` file for Python projects)
@@ -90,11 +87,10 @@ Need to install the following packages:
create-llama@latest
Ok to proceed? (y) y
✔ What is your project named? … my-app
✔ What app do you want to build? Agentic RAG
✔ What use case do you want to build? Agentic RAG
✔ What language do you want to use? Python (FastAPI)
✔ Do you want to use LlamaCloud services? … No / Yes
✔ Please provide your LlamaCloud API key (leave blank to skip): …
✔ Please provide your OpenAI API key (leave blank to skip): …
? How would you like to proceed? - Use arrow-keys. Return to submit.
Just generate code (~1 sec)
Start in VSCode (~1 sec)
@@ -115,7 +111,7 @@ non-interactively. For a list of the latest options, call `create-llama --help`.
The generated code is using the LlamaIndex Server, which serves LlamaIndex Workflows and Agent Workflows via an API server. See the following docs for more information:
- [LlamaIndex Server For TypeScript](./packages/server/README.md)
- [LlamaIndex Server For TypeScript](https://github.com/run-llama/chat-ui/tree/main/packages/server)
- [LlamaIndex Server For Python](./python/llama-index-server/README.md)
Inspired by and adapted from [create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
-16
View File
@@ -31,19 +31,6 @@ export default tseslint.config(
"@typescript-eslint/ban-ts-comment": "off",
},
},
{
files: ["packages/server/**"],
rules: {
"no-irregular-whitespace": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": [
"error",
{
ignoreRestArgs: true,
},
],
},
},
{
ignores: [
"python/**",
@@ -57,9 +44,6 @@ export default tseslint.config(
"**/out/**",
"**/node_modules/**",
"**/build/**",
"packages/server/server/**",
"packages/server/project/**",
"packages/server/bin/**",
],
},
);
+30
View File
@@ -1,5 +1,35 @@
# create-llama
## 0.6.3
### Patch Changes
- fec752e: refactor: llamacloud configs
## 0.6.2
### Patch Changes
- 28b46be: chore: replace Python examples with llama-deploy
- 93e2abe: fix: unused imports and format
## 0.6.1
### Patch Changes
- 952b5b4: fix: peer deps and sourcemap issues made ts server start fail
- e8004fd: Fix: broken devcontainer due to deleted repo
## 0.6.0
### Minor Changes
- 8fa8c3b: Removed deprecated templates and simplified code
### Patch Changes
- 8fa8c3b: Feat: re-add --ask-models
## 0.5.22
### Patch Changes
+3 -68
View File
@@ -2,42 +2,33 @@ import path from "path";
import { green, yellow } from "picocolors";
import { tryGitInit } from "./helpers/git";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { getOnline } from "./helpers/is-online";
import { isWriteable } from "./helpers/is-writeable";
import { makeDir } from "./helpers/make-dir";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
import type { InstallTemplateArgs } from "./helpers";
import { installTemplate } from "./helpers";
import { templatesDir } from "./helpers/dir";
import { toolsRequireConfig } from "./helpers/tools";
import { configVSCode } from "./helpers/vscode";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "port"
"appName" | "root" | "port"
> & {
appPath: string;
frontend: boolean;
};
export async function createApp({
template,
framework,
ui,
appPath,
packageManager,
frontend,
modelConfig,
llamaCloudKey,
communityProjectConfig,
llamapack,
vectorDb,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
useCase,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
@@ -59,9 +50,6 @@ export async function createApp({
process.exit(1);
}
const useYarn = packageManager === "yarn";
const isOnline = !useYarn || (await getOnline());
console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
console.log();
@@ -70,36 +58,18 @@ export async function createApp({
root,
template,
framework,
ui,
packageManager,
isOnline,
modelConfig,
llamaCloudKey,
communityProjectConfig,
llamapack,
vectorDb,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
useCase,
};
// Install backend
await installTemplate({ ...args, backend: true });
if (frontend && framework === "fastapi" && template !== "llamaindexserver") {
// install frontend
const frontendRoot = path.join(root, ".frontend");
await makeDir(frontendRoot);
await installTemplate({
...args,
root: frontendRoot,
framework: "nextjs",
backend: false,
});
}
await installTemplate(args);
await configVSCode(root, templatesDir, framework);
@@ -109,18 +79,6 @@ export async function createApp({
console.log();
}
if (toolsRequireConfig(tools) && template !== "llamaindexserver") {
const configFile =
framework === "fastapi" ? "config/tools.yaml" : "config/tools.json";
console.log(
yellow(
`You have selected tools that require configuration. Please configure them in the ${terminalLink(
configFile,
`file://${root}/${configFile}`,
)} file.`,
),
);
}
console.log("");
console.log(`${green("Success!")} Created ${appName} at ${appPath}`);
@@ -131,8 +89,6 @@ export async function createApp({
)} and learn how to get started.`,
);
outputObservability(args.observability);
if (
dataSources.some((dataSource) => dataSource.type === "file") &&
process.platform === "linux"
@@ -149,24 +105,3 @@ export async function createApp({
console.log();
}
function outputObservability(observability?: TemplateObservability) {
switch (observability) {
case "traceloop":
console.log(
`\n${yellow("Observability")}: Visit the ${terminalLink(
"documentation",
"https://traceloop.com/docs/openllmetry/integrations",
)} to set up the environment variables and start seeing execution traces.`,
);
break;
case "llamatrace":
console.log(
`\n${yellow("Observability")}: LlamaTrace has been configured for your project. Visit the ${terminalLink(
"LlamaTrace dashboard",
"https://llamatrace.com/login",
)} to view your traces and monitor your application.`,
);
break;
}
}
@@ -3,284 +3,109 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { TemplateFramework, TemplateType, TemplateUseCase, TemplateVectorDB } from "../../helpers/types";
import { TemplateFramework, TemplateUseCase, TemplateVectorDB } from "../../helpers";
import { ALL_PYTHON_USE_CASES } from "../../helpers/use-case";
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const templateType: TemplateType = process.env.TEMPLATE_TYPE
? (process.env.TEMPLATE_TYPE as TemplateType)
: "llamaindexserver";
const useCases: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateFramework: TemplateFramework = "fastapi";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
const useCases: TemplateUseCase[] = vectorDb === "llamacloud" ? [
"agentic_rag", "deep_research", "financial_report"
] : ALL_PYTHON_USE_CASES
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
// Test for streaming template
test.describe("StreamingTemplate", () => {
test.skip(templateType !== "streaming", `skipping streaming test for ${templateType}`);
if (
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
) {
// vectorDBs, tools, and data source combinations to test
const vectorDbs: TemplateVectorDB[] = [
"mongo",
"pg",
"pinecone",
"milvus",
"astra",
"qdrant",
"chroma",
"weaviate",
];
const toolOptions = [
"wikipedia.WikipediaToolSpec",
"google.GoogleSearchToolSpec",
"document_generator",
"artifact",
];
const dataSources = [
"--example-file",
"--web-source https://www.example.com",
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
];
const observabilityOptions = ["llamatrace", "traceloop"];
// Test vector databases
for (const vectorDb of vectorDbs) {
test(`vectorDB: ${vectorDb} ${templateType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb,
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (vectorDb !== "none") {
if (vectorDb === "pg") {
expect(pyprojectContent).toContain(
"llama-index-vector-stores-postgres",
);
} else {
expect(pyprojectContent).toContain(
`llama-index-vector-stores-${vectorDb}`,
);
}
}
});
}
// // Test tools
for (const tool of toolOptions) {
test(`tool: ${tool} ${templateType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: tool,
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (tool === "wikipedia.WikipediaToolSpec") {
expect(pyprojectContent).toContain("wikipedia");
}
if (tool === "google.GoogleSearchToolSpec") {
expect(pyprojectContent).toContain("google");
}
});
}
// // Test data sources
for (const dataSource of dataSources) {
test(`data source: ${dataSource} ${templateType}`, async () => {
const dataSourceType = dataSource.split(" ")[0];
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource,
vectorDb: "none",
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (dataSource.includes("--web-source")) {
expect(pyprojectContent).toContain("llama-index-readers-web");
}
if (dataSource.includes("--db-source")) {
expect(pyprojectContent).toContain("llama-index-readers-database");
}
});
}
// Test observability options
for (const observability of observabilityOptions) {
test.describe(`observability: ${observability} ${templateType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability,
},
});
});
}
}
});
test.describe("LlamaIndexServer", async () => {
test.skip(templateType !== "llamaindexserver", `skipping llamaindexserver test for ${templateType}`);
test.skip(dataSource !== "--example-file", `skipping llamaindexserver test for ${dataSource}`);
for (const useCase of useCases) {
test.describe("LlamaIndexServer", async () => {
for (const useCase of useCases) {
test(`should pass mypy for use case: ${useCase}`, async () => {
const cwd = await createTestDir();
await createAndCheckLlamaProject({
options: {
cwd,
templateType: "llamaindexserver",
templateFramework,
dataSource,
vectorDb: "none",
tools: "none",
vectorDb,
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
useCase,
},
});
}
});
});
}
});
});
async function createAndCheckLlamaProject({
options,
}: {
options: RunCreateLlamaOptions;
}): Promise<{ pyprojectPath: string; projectPath: string }> {
const result = await runCreateLlama(options);
const name = result.projectName;
const projectPath = path.join(options.cwd, name);
async function createAndCheckLlamaProject({
options,
}: {
options: RunCreateLlamaOptions;
}): Promise<{ pyprojectPath: string; projectPath: string }> {
const result = await runCreateLlama(options);
const name = result.projectName;
const projectPath = path.join(options.cwd, name);
// Check if the app folder exists
expect(fs.existsSync(projectPath)).toBeTruthy();
// Check if the app folder exists
expect(fs.existsSync(projectPath)).toBeTruthy();
// Check if pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
// Check if pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
// Modify environment for the command
const commandEnv = {
...process.env,
};
// Modify environment for the command
const commandEnv = {
...process.env,
};
console.log("Running uv venv...");
try {
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
"uv venv",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv venv stdout:", venvStdout);
console.error("uv venv stderr:", venvStderr);
} catch (error) {
console.error("Error running uv venv:", error);
throw error; // Re-throw error to fail the test
}
console.log("Running uv sync...");
try {
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
"uv sync --all-extras",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv sync stdout:", syncStdout);
console.error("uv sync stderr:", syncStderr);
} catch (error) {
console.error("Error running uv sync:", error);
throw error; // Re-throw error to fail the test
}
console.log("Running uv run mypy ....");
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"uv run mypy .",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv run mypy stdout:", mypyStdout);
console.error("uv run mypy stderr:", mypyStderr);
// Assuming mypy success means no output or specific success message
// Adjust checks based on actual expected mypy output
} catch (error) {
console.error("Error running mypy:", error);
throw error;
}
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
console.log("Running uv venv...");
try {
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
"uv venv",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv venv stdout:", venvStdout);
console.error("uv venv stderr:", venvStderr);
} catch (error) {
console.error("Error running uv venv:", error);
throw error; // Re-throw error to fail the test
}
});
console.log("Running uv sync...");
try {
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
"uv sync --all-extras",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv sync stdout:", syncStdout);
console.error("uv sync stderr:", syncStderr);
} catch (error) {
console.error("Error running uv sync:", error);
throw error; // Re-throw error to fail the test
}
console.log("Running uv run mypy ....");
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"uv run mypy .",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv run mypy stdout:", mypyStdout);
console.error("uv run mypy stderr:", mypyStderr);
// Assuming mypy success means no output or specific success message
// Adjust checks based on actual expected mypy output
} catch (error) {
console.error("Error running mypy:", error);
throw error;
}
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
}
@@ -1,67 +1,49 @@
import { expect, test } from "@playwright/test";
import { ChildProcess, execSync } from "child_process";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateFramework,
TemplatePostInstallAction,
TemplateUI,
} from "../../helpers";
import { createTestDir, runCreateLlama, type AppType } from "../utils";
import { type TemplateFramework, type TemplateVectorDB } from "../../helpers";
import {
ALL_PYTHON_USE_CASES,
ALL_TYPESCRIPT_USE_CASES,
} from "../../helpers/use-case";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? (process.env.DATASOURCE as string)
: "--example-file";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const allUseCases =
templateFramework === "nextjs"
? ALL_TYPESCRIPT_USE_CASES
: ALL_PYTHON_USE_CASES;
const isPythonLlamaDeploy = templateFramework === "fastapi";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = "--frontend";
const userMessage = "Write a blog post about physical standards for letters";
const templateUseCases = [
"agentic_rag",
"financial_report",
"deep_research",
"code_generator",
// "hitl",
];
const ejectDir = "next";
for (const useCase of templateUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
dataSource === "--no-files" || templateFramework === "express",
"The llamaindexserver template currently only works with nextjs, fastapi. We also only run on Linux to speed up tests.",
);
const useLlamaParse = dataSource === "--llamacloud";
for (const useCase of allUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${vectorDb}`, async () => {
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "llamaindexserver",
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
postInstallAction: isPythonLlamaDeploy ? "dependencies" : "runApp",
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
useLlamaParse,
});
name = result.projectName;
appProcess = result.appProcess;
@@ -74,9 +56,10 @@ for (const useCase of templateUseCases) {
test("Frontend should have a title", async ({ page }) => {
test.skip(
templatePostInstallAction !== "runApp" ||
templateFramework === "express",
isPythonLlamaDeploy,
"Skip frontend tests for Python LllamaDeploy",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 5 * 60 * 1000,
@@ -87,11 +70,10 @@ for (const useCase of templateUseCases) {
page,
}) => {
test.skip(
templatePostInstallAction !== "runApp" ||
useCase === "financial_report" ||
useCase === "financial_report" ||
useCase === "deep_research" ||
templateFramework === "express",
"Skip chat tests for financial report and deep research.",
isPythonLlamaDeploy,
"Skip chat tests for financial report and deep research. Also skip for Python LlamaDeploy",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
@@ -112,28 +94,6 @@ for (const useCase of templateUseCases) {
expect(response.ok()).toBeTruthy();
});
test("Should successfully eject, install dependencies and build without errors", async () => {
test.skip(
templateFramework !== "nextjs" ||
useCase !== "code_generator" ||
dataSource === "--llamacloud",
"Eject test only applies to Next.js framework, code generator use case, and non-llamacloud",
);
// Run eject command
execSync("npm run eject", { cwd: path.join(cwd, name) });
// Verify next directory exists
const nextDirExists = fs.existsSync(path.join(cwd, name, ejectDir));
expect(nextDirExists).toBeTruthy();
// Install dependencies in next directory
execSync("npm install", { cwd: path.join(cwd, name, ejectDir) });
// Run build
execSync("npm run build", { cwd: path.join(cwd, name, ejectDir) });
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
@@ -1,63 +0,0 @@
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import { TemplateFramework, TemplateUseCase } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
// The reflex template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
for (const useCase of templateUseCases) {
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
let appPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create reflex app
test.beforeAll(async () => {
cwd = await createTestDir();
appPort = Math.floor(Math.random() * 10000) + 10000;
const result = await runCreateLlama({
cwd,
templateType: "reflex",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: appPort,
postInstallAction: "runApp",
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
});
test.afterAll(async () => {
appProcess.kill();
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${appPort}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 2000 * 60,
});
});
});
}
}
@@ -1,127 +0,0 @@
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateFramework,
TemplatePostInstallAction,
TemplateUI,
} from "../../helpers";
import { createTestDir, runCreateLlama, type AppType } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage =
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
const isNode18 = process.version.startsWith("v18");
const isLlamaCloud = dataSource === "--llamacloud";
// llamacloud is using File API which is not supported on node 18
if (isNode18 && isLlamaCloud) {
test.skip(true, "Skipping tests for Node 18 and LlamaCloud data source");
}
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "streaming",
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
llamaCloudProjectName,
llamaCloudIndexName,
});
name = result.projectName;
appProcess = result.appProcess;
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
test("Frontend should be able to submit a message and receive a response", async ({
page,
}) => {
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const [response] = await Promise.all([
page.waitForResponse(
(res) => {
return res.url().includes("/api/chat") && res.status() === 200;
},
{
timeout: 1000 * 60,
},
),
page.click("form button[type=submit]"),
]);
const text = await response.text();
console.log("AI response when submitting message: ", text);
expect(response.ok()).toBeTruthy();
});
test("Backend frameworks should response when calling non-streaming chat API", async ({
request,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(templateFramework === "nextjs");
const response = await request.post(
`http://localhost:${port}/api/chat/request`,
{
data: {
messages: [
{
role: "user",
content: userMessage,
},
],
},
},
);
const text = await response.text();
console.log("AI response when calling API: ", text);
expect(response.ok()).toBeTruthy();
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
@@ -0,0 +1,70 @@
import { expect, test } from "@playwright/test";
import { ChildProcess, execSync } from "child_process";
import fs from "fs";
import path from "path";
import { type TemplateFramework, type TemplateVectorDB } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = "nextjs";
const useCase = "code_generator";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const ejectDir = "next";
test.describe.skip(
`Test eject command for ${useCase} ${templateFramework} ${vectorDb}`,
async () => {
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateFramework,
vectorDb,
port,
postInstallAction: "dependencies",
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
});
name = result.projectName;
appProcess = result.appProcess;
});
test("Should successfully eject, install dependencies and build without errors", async ({
page,
}) => {
test.skip(
vectorDb === "llamacloud",
"Eject test only works with non-llamacloud",
);
// Run eject command
execSync("npm run eject", { cwd: path.join(cwd, name) });
// Verify next directory exists
const nextDirExists = fs.existsSync(path.join(cwd, name, ejectDir));
expect(nextDirExists).toBeTruthy();
// Install dependencies in next directory
execSync("npm install", { cwd: path.join(cwd, name, ejectDir) });
// Run build
execSync("npm run build", { cwd: path.join(cwd, name, ejectDir) });
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
},
);
@@ -3,124 +3,51 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { NO_DATA_USE_CASES } from "../../helpers/constant";
import {
TemplateFramework,
TemplateType,
TemplateUseCase,
TemplateVectorDB,
} from "../../helpers/types";
import { ALL_TYPESCRIPT_USE_CASES } from "../../helpers/use-case";
import { createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "nextjs";
const templateType: TemplateType = process.env.TEMPLATE_TYPE
? (process.env.TEMPLATE_TYPE as TemplateType)
: "llamaindexserver";
const useCases: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// vectorDBs combinations to test
const vectorDbs: TemplateVectorDB[] = [
"mongo",
"pg",
"qdrant",
"pinecone",
"milvus",
"astra",
"chroma",
"llamacloud",
"weaviate",
];
const templateFramework: TemplateFramework = "nextjs";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
test.describe("Test resolve TS dependencies", () => {
test.describe.configure({ retries: 0 });
// Test vector DBs without LlamaParse
for (const vectorDb of vectorDbs) {
const optionDescription = `templateType: ${templateType}, vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
test(`Vector DB test - ${optionDescription}`, async () => {
// skip vectordb test for llamaindexserver
test.skip(
templateType === "llamaindexserver",
"skipping vectorDB test for llamaindexserver",
);
await runTest({
templateType: templateType,
useLlamaParse: false, // Disable LlamaParse for vectorDB test
vectorDb: vectorDb,
for (const useCase of ALL_TYPESCRIPT_USE_CASES) {
const optionDescription = `useCase: ${useCase}, vectorDb: ${vectorDb}`;
test.describe(`${optionDescription}`, () => {
test(`${optionDescription}`, async () => {
await runTest({
useCase: useCase,
vectorDb: vectorDb,
});
});
});
}
// No vectorDB, with LlamaParse and useCase
// Only need to test use case with example data source
if (dataSource === "--example-file") {
for (const useCase of useCases) {
const optionDescription = `templateType: ${templateType}, useCase: ${useCase}`;
test.describe(`useCase test - ${optionDescription}`, () => {
test.skip(
templateType === "streaming",
"Skipping use case test for streaming template.",
);
test(`no llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
useLlamaParse: false,
useCase: useCase,
});
});
// Skipping llamacloud for the use case doesn't use index.
if (!useCase || !NO_DATA_USE_CASES.includes(useCase)) {
test(`llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
useLlamaParse: true,
useCase: useCase,
});
});
}
});
}
}
});
async function runTest(options: {
templateType: TemplateType;
useLlamaParse: boolean;
useCase?: TemplateUseCase;
vectorDb?: TemplateVectorDB;
useCase: TemplateUseCase;
vectorDb: TemplateVectorDB;
}) {
const cwd = await createTestDir();
const result = await runCreateLlama({
cwd: cwd,
templateType: options.templateType,
templateFramework: templateFramework,
dataSource: dataSource,
vectorDb: options.vectorDb ?? "none",
vectorDb: options.vectorDb,
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
tools: undefined,
useLlamaParse: options.useLlamaParse,
useCase: options.useCase,
});
const name = result.projectName;
+5 -70
View File
@@ -6,13 +6,9 @@ import waitPort from "wait-port";
import {
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
TemplateUI,
TemplateVectorDB,
} from "../helpers";
export type AppType = "--frontend" | "--no-frontend" | "";
export type CreateLlamaResult = {
projectName: string;
appProcess: ChildProcess;
@@ -20,72 +16,36 @@ export type CreateLlamaResult = {
export type RunCreateLlamaOptions = {
cwd: string;
templateType: TemplateType;
templateFramework: TemplateFramework;
dataSource: string;
vectorDb: TemplateVectorDB;
port: number;
postInstallAction: TemplatePostInstallAction;
templateUI?: TemplateUI;
appType?: AppType;
useCase: string;
llamaCloudProjectName?: string;
llamaCloudIndexName?: string;
tools?: string;
useLlamaParse?: boolean;
observability?: string;
useCase?: string;
};
export async function runCreateLlama({
cwd,
templateType,
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction,
templateUI,
appType,
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
tools,
useLlamaParse,
observability,
useCase,
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
throw new Error(
"Setting the OPENAI_API_KEY and LLAMA_CLOUD_API_KEY is mandatory to run tests",
);
}
const name = [
templateType,
templateFramework,
dataSource.split(" ")[0],
templateUI,
appType,
].join("-");
// Handle different data source types
const dataSourceArgs = [];
if (dataSource.includes("--web-source")) {
const webSource = dataSource.split(" ")[1];
dataSourceArgs.push("--web-source", webSource);
} else if (dataSource.includes("--db-source")) {
const dbSource = dataSource.split(" ")[1];
dataSourceArgs.push("--db-source", dbSource);
} else {
dataSourceArgs.push(dataSource);
}
const name = [templateFramework, useCase, vectorDb].join("-");
const commandArgs = [
"create-llama",
name,
"--template",
templateType,
"--framework",
templateFramework,
...dataSourceArgs,
"--vector-db",
vectorDb,
"--use-npm",
@@ -93,35 +53,10 @@ export async function runCreateLlama({
port,
"--post-install-action",
postInstallAction,
"--tools",
tools ?? "none",
"--observability",
"none",
"--use-case",
useCase,
];
if (templateUI) {
commandArgs.push("--ui", templateUI);
}
if (appType) {
commandArgs.push(appType);
}
if (useLlamaParse) {
commandArgs.push("--use-llama-parse");
} else {
commandArgs.push("--no-llama-parse");
}
if (observability) {
commandArgs.push("--observability", observability);
}
if (
(templateType === "multiagent" ||
templateType === "reflex" ||
templateType === "llamaindexserver") &&
useCase
) {
commandArgs.push("--use-case", useCase);
}
const command = commandArgs.join(" ");
console.log(`running command '${command}' in ${cwd}`);
const appProcess = exec(command, {
-15
View File
@@ -1,15 +0,0 @@
import { TemplateUseCase } from "./types";
export const COMMUNITY_OWNER = "run-llama";
export const COMMUNITY_REPO = "create_llama_projects";
export const LLAMA_PACK_OWNER = "run-llama";
export const LLAMA_PACK_REPO = "llama_index";
export const LLAMA_PACK_FOLDER = "llama-index-packs";
export const LLAMA_PACK_FOLDER_PATH = `${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/main/${LLAMA_PACK_FOLDER}`;
// these use cases don't have data folder, so no need to run generate and no need to getIndex
export const NO_DATA_USE_CASES: TemplateUseCase[] = [
"code_generator",
"document_generator",
"hitl",
];
+1 -92
View File
@@ -1,8 +1,6 @@
import fs from "fs/promises";
import path from "path";
import yaml, { Document } from "yaml";
import { templatesDir } from "./dir";
import { DbSourceConfig, TemplateDataSource, WebSourceConfig } from "./types";
import { TemplateDataSource } from "./types";
export const EXAMPLE_FILE: TemplateDataSource = {
type: "file",
@@ -51,92 +49,3 @@ export const AI_REPORTS: TemplateDataSource = {
filename: "EPRS_ATA_2024_760392_EN.pdf",
},
};
export function getDataSources(
files?: string,
exampleFile?: boolean,
): TemplateDataSource[] | undefined {
let dataSources: TemplateDataSource[] | undefined = undefined;
if (files) {
// If user specified files option, then the program should use context engine
dataSources = files.split(",").map((filePath) => ({
type: "file",
config: {
path: filePath,
},
}));
}
if (exampleFile) {
dataSources = [...(dataSources ? dataSources : []), EXAMPLE_FILE];
}
return dataSources;
}
export async function writeLoadersConfig(
root: string,
dataSources: TemplateDataSource[],
useLlamaParse?: boolean,
) {
const loaderConfig: Record<string, any> = {};
// Always set file loader config
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
if (dataSources.some((ds) => ds.type === "web")) {
loaderConfig.web = createWebLoaderConfig(dataSources);
}
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
if (dbLoaders.length > 0) {
loaderConfig.db = createDbLoaderConfig(dbLoaders);
}
// Create a new Document with the loaderConfig
const yamlDoc = new Document(loaderConfig);
// Write loaders config
const loaderConfigPath = path.join(root, "config", "loaders.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(loaderConfigPath, yaml.stringify(yamlDoc));
}
function createWebLoaderConfig(dataSources: TemplateDataSource[]): any {
const webLoaderConfig: Record<string, any> = {};
// Create config for browser driver arguments
webLoaderConfig.driver_arguments = [
"--no-sandbox",
"--disable-dev-shm-usage",
];
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
webLoaderConfig.urls = urlConfigs;
return webLoaderConfig;
}
function createFileLoaderConfig(useLlamaParse?: boolean): any {
return {
use_llama_parse: useLlamaParse,
};
}
function createDbLoaderConfig(dbLoaders: TemplateDataSource[]): any {
return dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
}
+66 -233
View File
@@ -1,29 +1,17 @@
import fs from "fs/promises";
import path from "path";
import { TOOL_SYSTEM_PROMPT_ENV_VAR, Tool } from "./tools";
import {
EnvVar,
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateObservability,
TemplateType,
TemplateUseCase,
TemplateVectorDB,
} from "./types";
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
const DEFAULT_SYSTEM_PROMPT =
"You are a helpful assistant who helps users with their questions.";
const DATA_SOURCES_PROMPT =
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
export type EnvVar = {
name?: string;
description?: string;
value?: string;
};
import { USE_CASE_CONFIGS } from "./use-case";
const renderEnvVar = (envVars: EnvVar[]): string => {
return envVars.reduce(
@@ -237,13 +225,16 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
}
};
const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
const getModelEnvs = (
modelConfig: ModelConfig,
framework: TemplateFramework,
template: TemplateType,
useCase: TemplateUseCase,
): EnvVar[] => {
const isPythonLlamaDeploy =
framework === "fastapi" && template === "llamaindexserver";
return [
{
name: "MODEL_PROVIDER",
description: "The provider for the AI models to use.",
value: modelConfig.provider,
},
{
name: "MODEL",
description: "The name of LLM model to use.",
@@ -254,15 +245,25 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
description: "Name of the embedding model to use.",
value: modelConfig.embeddingModel,
},
{
name: "EMBEDDING_DIM",
description: "Dimension of the embedding model to use.",
value: modelConfig.dimensions.toString(),
},
{
name: "CONVERSATION_STARTERS",
description: "The questions to help users get started (multi-line).",
},
...(isPythonLlamaDeploy
? [
{
name: "NEXT_PUBLIC_STARTER_QUESTIONS",
description:
"Initial questions to display in the chat (`starterQuestions`)",
value: JSON.stringify(
USE_CASE_CONFIGS[useCase]?.starterQuestions ?? [],
),
},
]
: [
{
name: "CONVERSATION_STARTERS",
description:
"The questions to help users get started (multi-line).",
},
]),
...(USE_CASE_CONFIGS[useCase]?.additionalEnvVars ?? []),
...(modelConfig.provider === "openai"
? [
{
@@ -270,14 +271,18 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
description: "The OpenAI API key to use.",
value: modelConfig.apiKey,
},
{
name: "LLM_TEMPERATURE",
description: "Temperature for sampling from the model.",
},
{
name: "LLM_MAX_TOKENS",
description: "Maximum number of tokens to generate.",
},
...(isPythonLlamaDeploy
? []
: [
{
name: "LLM_TEMPERATURE",
description: "Temperature for sampling from the model.",
},
{
name: "LLM_MAX_TOKENS",
description: "Maximum number of tokens to generate.",
},
]),
]
: []),
...(modelConfig.provider === "anthropic"
@@ -386,26 +391,12 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
const getFrameworkEnvs = (
framework: TemplateFramework,
template: TemplateType,
template?: TemplateType,
port?: number,
): EnvVar[] => {
const sPort = port?.toString() || "8000";
const result: EnvVar[] =
template !== "llamaindexserver"
? [
{
name: "FILESERVER_URL_PREFIX",
description:
"FILESERVER_URL_PREFIX is the URL prefix of the server storing the images generated by the interpreter.",
value:
framework === "nextjs"
? // FIXME: if we are using nextjs, port should be 3000
"http://localhost:3000/api/files"
: `http://localhost:${sPort}/api/files`,
},
]
: [];
if (framework === "fastapi") {
const result: EnvVar[] = [];
if (framework === "fastapi" && template !== "llamaindexserver") {
result.push(
...[
{
@@ -421,149 +412,10 @@ const getFrameworkEnvs = (
],
);
}
if (framework === "nextjs" && template !== "llamaindexserver") {
result.push({
name: "NEXT_PUBLIC_CHAT_API",
description:
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
});
}
return result;
};
const getEngineEnvs = (): EnvVar[] => {
return [
{
name: "TOP_K",
description:
"The number of similar embeddings to return when retrieving documents.",
},
];
};
const getToolEnvs = (tools?: Tool[]): EnvVar[] => {
if (!tools?.length) return [];
const toolEnvs: EnvVar[] = [];
tools.forEach((tool) => {
if (tool.envVars?.length) {
toolEnvs.push(
// Don't include the system prompt env var here
// It should be handled separately by merging with the default system prompt
...tool.envVars.filter(
(env) => env.name !== TOOL_SYSTEM_PROMPT_ENV_VAR,
),
);
}
});
return toolEnvs;
};
const getSystemPromptEnv = (
tools?: Tool[],
dataSources?: TemplateDataSource[],
template?: TemplateType,
): EnvVar[] => {
const systemPromptEnv: EnvVar[] = [];
// build tool system prompt by merging all tool system prompts
// multiagent template doesn't need system prompt
if (template !== "multiagent") {
let toolSystemPrompt = "";
tools?.forEach((tool) => {
const toolSystemPromptEnv = tool.envVars?.find(
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
);
if (toolSystemPromptEnv) {
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
}
});
const systemPrompt =
'"' +
DEFAULT_SYSTEM_PROMPT +
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
'"';
systemPromptEnv.push({
name: "SYSTEM_PROMPT",
description: "The system prompt for the AI model.",
value: systemPrompt,
});
}
if (tools?.length == 0 && (dataSources?.length ?? 0 > 0)) {
const citationPrompt = `'You have provided information from a knowledge base that has been passed to you in nodes of information.
Each node has useful metadata such as node ID, file name, page, etc.
Please add the citation to the data node for each sentence or paragraph that you reference in the provided information.
The citation format is: . [citation:<node_id>]()
Where the <node_id> is the unique identifier of the data node.
Example:
We have two nodes:
node_id: xyz
file_name: llama.pdf
node_id: abc
file_name: animal.pdf
User question: Tell me a fun fact about Llama.
Your answer:
A baby llama is called "Cria" [citation:xyz]().
It often live in desert [citation:abc]().
It\\'s cute animal.
'`;
systemPromptEnv.push({
name: "SYSTEM_CITATION_PROMPT",
description:
"An additional system prompt to add citation when responding to user questions.",
value: citationPrompt,
});
}
return systemPromptEnv;
};
const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
const nextQuestionEnvs: EnvVar[] = [
{
name: "NEXT_QUESTION_PROMPT",
description: `Customize prompt to generate the next question suggestions based on the conversation history.
Disable this prompt to disable the next question suggestions feature.`,
value: `"You're a helpful assistant! Your task is to suggest the next question that user might ask.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`"`,
},
];
if (template === "multiagent" || template === "streaming") {
return nextQuestionEnvs;
}
return [];
};
const getObservabilityEnvs = (
observability?: TemplateObservability,
): EnvVar[] => {
if (observability === "llamatrace") {
return [
{
name: "PHOENIX_API_KEY",
description:
"API key for LlamaTrace observability. Retrieve from https://llamatrace.com/login",
},
];
}
return [];
};
export const createBackendEnvFile = async (
root: string,
opts: Pick<
@@ -575,9 +427,8 @@ export const createBackendEnvFile = async (
| "dataSources"
| "template"
| "port"
| "tools"
| "observability"
| "useLlamaParse"
| "useCase"
>,
) => {
// Init env values
@@ -593,45 +444,27 @@ export const createBackendEnvFile = async (
]
: []),
...getVectorDBEnvs(opts.vectorDb, opts.framework, opts.template),
...getToolEnvs(opts.tools),
...getFrameworkEnvs(opts.framework, opts.template, opts.port),
// Add environment variables of each component
...(opts.template === "llamaindexserver"
? [
{
name: "OPENAI_API_KEY",
description: "The OpenAI API key to use.",
value: opts.modelConfig.apiKey,
},
]
: [
// don't use this stuff for llama-indexserver
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
]),
...getModelEnvs(
opts.modelConfig,
opts.framework,
opts.template,
opts.useCase,
),
];
// Render and write env file
const content = renderEnvVar(envVars);
await fs.writeFile(path.join(root, envFileName), content);
const isPythonLlamaDeploy =
opts.framework === "fastapi" && opts.template === "llamaindexserver";
// each llama-deploy service will need a .env inside its directory
// this .env will be copied along with workflow code when service is deployed
// so that we need to put the .env file inside src/ instead of root
const envPath = isPythonLlamaDeploy
? path.join(root, "src", envFileName)
: path.join(root, envFileName);
await fs.writeFile(envPath, content);
console.log(`Created '${envFileName}' file. Please check the settings.`);
};
export const createFrontendEnvFile = async (
root: string,
opts: {
vectorDb?: TemplateVectorDB;
},
) => {
const defaultFrontendEnvs = [
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description: "Let's the user change indexes in LlamaCloud projects",
value: opts.vectorDb === "llamacloud" ? "true" : "false",
},
];
const content = renderEnvVar(defaultFrontendEnvs);
await fs.writeFile(path.join(root, ".env"), content);
};
+42 -96
View File
@@ -4,22 +4,16 @@ import path from "path";
import picocolors, { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { NO_DATA_USE_CASES } from "./constant";
import { writeLoadersConfig } from "./datasources";
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
import { createBackendEnvFile } from "./env-variables";
import { PackageManager } from "./get-pkg-manager";
import { installLlamapackProject } from "./llama-pack";
import { makeDir } from "./make-dir";
import { installPythonTemplate } from "./python";
import { downloadAndExtractRepo } from "./repo";
import { ConfigFileType, writeToolsConfig } from "./tools";
import {
FileSourceConfig,
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateUseCase,
TemplateVectorDB,
} from "./types";
import { installTSTemplate } from "./typescript";
@@ -58,11 +52,11 @@ const checkForGenerateScript = (
async function generateContextData(
framework: TemplateFramework,
modelConfig: ModelConfig,
dataSources: TemplateDataSource[],
packageManager?: PackageManager,
vectorDb?: TemplateVectorDB,
llamaCloudKey?: string,
useLlamaParse?: boolean,
useCase?: TemplateUseCase,
) {
if (packageManager) {
const runGenerate = `${cyan(
@@ -100,8 +94,7 @@ async function generateContextData(
} else {
console.log(`Running ${runGenerate} to generate the context data.`);
const shouldRunGenerate =
!useCase || !NO_DATA_USE_CASES.includes(useCase);
const shouldRunGenerate = dataSources.length > 0;
if (shouldRunGenerate) {
await callPackageManager(packageManager, true, ["run", "generate"]);
@@ -124,8 +117,13 @@ const downloadFile = async (url: string, destPath: string) => {
const prepareContextData = async (
root: string,
dataSources: TemplateDataSource[],
isPythonLlamaDeploy: boolean,
) => {
await makeDir(path.join(root, "data"));
const dataDir = isPythonLlamaDeploy
? path.join(root, "ui", "data")
: path.join(root, "data");
await makeDir(dataDir);
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// If the path is URLs, download the data and save it to the data directory
@@ -135,8 +133,7 @@ const prepareContextData = async (
dataSourceConfig.url.toString(),
);
const destPath = path.join(
root,
"data",
dataDir,
dataSourceConfig.filename ??
path.basename(dataSourceConfig.url.toString()),
);
@@ -144,108 +141,57 @@ const prepareContextData = async (
} else {
// Copy local data
console.log("Copying data from path:", dataSourceConfig.path);
const destPath = path.join(
root,
"data",
path.basename(dataSourceConfig.path),
);
const destPath = path.join(dataDir, path.basename(dataSourceConfig.path));
await fsExtra.copy(dataSourceConfig.path, destPath);
}
}
};
const installCommunityProject = async ({
root,
communityProjectConfig,
}: Pick<InstallTemplateArgs, "root" | "communityProjectConfig">) => {
const { owner, repo, branch, filePath } = communityProjectConfig!;
console.log("\nInstalling community project:", filePath || repo);
await downloadAndExtractRepo(root, {
username: owner,
name: repo,
branch,
filePath: filePath || "",
});
};
export const installTemplate = async (
props: InstallTemplateArgs & { backend: boolean },
) => {
export const installTemplate = async (props: InstallTemplateArgs) => {
process.chdir(props.root);
if (props.template === "community" && props.communityProjectConfig) {
await installCommunityProject(props);
return;
}
if (props.template === "llamapack" && props.llamapack) {
await installLlamapackProject(props);
return;
}
if (props.framework === "fastapi") {
await installPythonTemplate(props);
} else {
await installTSTemplate(props);
}
// write configurations
if (props.template !== "llamaindexserver") {
await writeToolsConfig(
props.root,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
const isPythonLlamaDeploy =
props.framework === "fastapi" && props.template === "llamaindexserver";
// This is a backend, so we need to copy the test data and create the env file.
// Copy the environment file to the target directory.
await createBackendEnvFile(props.root, props);
await prepareContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
isPythonLlamaDeploy,
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
console.log("\nGenerating context data...\n");
await generateContextData(
props.framework,
props.modelConfig,
props.dataSources,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
if (props.vectorDb !== "llamacloud") {
// write loaders configuration (currently Python only)
// not needed for LlamaCloud as it has its own loaders
await writeLoadersConfig(
props.root,
props.dataSources,
props.useLlamaParse,
);
}
}
if (props.backend) {
// This is a backend, so we need to copy the test data and create the env file.
// Copy the environment file to the target directory.
if (props.template !== "community" && props.template !== "llamapack") {
await createBackendEnvFile(props.root, props);
}
await prepareContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
console.log("\nGenerating context data...\n");
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
props.useCase,
);
}
// Create outputs directory
if (!isPythonLlamaDeploy) {
// Create outputs directory (llama-deploy doesn't need this)
await makeDir(path.join(props.root, "output/tools"));
await makeDir(path.join(props.root, "output/uploaded"));
await makeDir(path.join(props.root, "output/llamacloud"));
} else {
// this is a frontend for a full-stack app, create .env file with model information
await createFrontendEnvFile(props.root, {
vectorDb: props.vectorDb,
});
}
};
-148
View File
@@ -1,148 +0,0 @@
import fs from "fs/promises";
import got from "got";
import path from "path";
import { parse } from "smol-toml";
import {
LLAMA_PACK_FOLDER,
LLAMA_PACK_FOLDER_PATH,
LLAMA_PACK_OWNER,
LLAMA_PACK_REPO,
} from "./constant";
import { copy } from "./copy";
import { templatesDir } from "./dir";
import { addDependencies, installPythonDependencies } from "./python";
import { getRepoRawContent } from "./repo";
import { InstallTemplateArgs } from "./types";
const getLlamaPackFolderSHA = async () => {
const url = `https://api.github.com/repos/${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/contents`;
const response = await got(url, {
responseType: "json",
});
const data = response.body as any[];
const llamaPackFolder = data.find((item) => item.name === LLAMA_PACK_FOLDER);
return llamaPackFolder.sha;
};
const getLLamaPackFolderTree = async (
sha: string,
): Promise<
Array<{
path: string;
}>
> => {
const url = `https://api.github.com/repos/${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/git/trees/${sha}?recursive=1`;
const response = await got(url, {
responseType: "json",
});
return (response.body as any).tree;
};
export async function getAvailableLlamapackOptions(): Promise<
{
name: string;
folderPath: string;
}[]
> {
const EXAMPLE_RELATIVE_PATH = "/examples/example.py";
const PACK_FOLDER_SUBFIX = "llama-index-packs";
const llamaPackFolderSHA = await getLlamaPackFolderSHA();
const llamaPackTree = await getLLamaPackFolderTree(llamaPackFolderSHA);
// Return options that have example files
const exampleFiles = llamaPackTree.filter((item) =>
item.path.endsWith(EXAMPLE_RELATIVE_PATH),
);
const options = exampleFiles.map((file) => {
const packFolder = file.path.substring(
0,
file.path.indexOf(EXAMPLE_RELATIVE_PATH),
);
const packName = packFolder.substring(PACK_FOLDER_SUBFIX.length + 1);
return {
name: packName,
folderPath: packFolder,
};
});
return options;
}
const copyLlamapackEmptyProject = async ({
root,
}: Pick<InstallTemplateArgs, "root">) => {
const templatePath = path.join(
templatesDir,
"components/sample-projects/llamapack",
);
await copy("**", root, {
parents: true,
cwd: templatePath,
});
};
const copyData = async ({
root,
}: Pick<InstallTemplateArgs, "root" | "llamapack">) => {
const dataPath = path.join(templatesDir, "components/data");
await copy("**", path.join(root, "data"), {
parents: true,
cwd: dataPath,
});
};
const installLlamapackExample = async ({
root,
llamapack,
}: Pick<InstallTemplateArgs, "root" | "llamapack">) => {
const exampleFileName = "example.py";
const readmeFileName = "README.md";
const projectTomlFileName = "pyproject.toml";
const exampleFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/examples/${exampleFileName}`;
const readmeFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/${readmeFileName}`;
const projectTomlFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/${projectTomlFileName}`;
// Download example.py from llamapack and save to root
const exampleContent = await getRepoRawContent(exampleFilePath);
await fs.writeFile(path.join(root, exampleFileName), exampleContent);
// Download README.md from llamapack and combine with README-template.md,
// save to root and then delete template file
const readmeContent = await getRepoRawContent(readmeFilePath);
const readmeTemplateContent = await fs.readFile(
path.join(root, "README-template.md"),
"utf-8",
);
await fs.writeFile(
path.join(root, readmeFileName),
`${readmeContent}\n${readmeTemplateContent}`,
);
await fs.unlink(path.join(root, "README-template.md"));
// Download pyproject.toml from llamapack, parse it to get package name and version,
// then add it as a dependency to current toml file in the project
const projectTomlContent = await getRepoRawContent(projectTomlFilePath);
const fileParsed = parse(projectTomlContent) as any;
const packageName = fileParsed.tool.poetry.name;
const packageVersion = fileParsed.tool.poetry.version;
await addDependencies(root, [
{
name: packageName,
version: packageVersion,
},
]);
};
export const installLlamapackProject = async ({
root,
llamapack,
postInstallAction,
}: Pick<InstallTemplateArgs, "root" | "llamapack" | "postInstallAction">) => {
console.log("\nInstalling Llamapack project:", llamapack!);
await copyLlamapackEmptyProject({ root });
await copyData({ root });
await installLlamapackExample({ root, llamapack });
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
};
+12
View File
@@ -0,0 +1,12 @@
import { ModelConfig } from "./types";
export const getGpt41ModelConfig = (): ModelConfig => ({
provider: "openai",
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-4.1",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
return !!process.env.OPENAI_API_KEY;
},
});
-35
View File
@@ -1,35 +0,0 @@
import { execSync } from "child_process";
import fs from "fs";
export function isPoetryAvailable(): boolean {
try {
execSync("poetry --version", { stdio: "ignore" });
return true;
} catch (_) {}
return false;
}
export function tryPoetryInstall(noRoot: boolean): boolean {
try {
execSync(`poetry install${noRoot ? " --no-root" : ""}`, {
stdio: "inherit",
});
return true;
} catch (_) {}
return false;
}
export function tryPoetryRun(command: string): boolean {
try {
execSync(`poetry run ${command}`, { stdio: "inherit" });
return true;
} catch (_) {}
return false;
}
export function isHavingPoetryLockFile(): boolean {
try {
return fs.existsSync("poetry.lock");
} catch (_) {}
return false;
}
@@ -31,17 +31,9 @@ const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type AnthropicQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askAnthropicQuestions({
askModels,
apiKey,
}: AnthropicQuestionsParams): Promise<ModelConfigParams> {
export async function askAnthropicQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
apiKey: process.env.ANTHROPIC_API_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -69,35 +61,33 @@ export async function askAnthropicQuestions({
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions =
EMBEDDING_MODELS[
embeddingModel as HuggingFaceEmbeddingModelType
].dimensions;
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions =
EMBEDDING_MODELS[
embeddingModel as HuggingFaceEmbeddingModelType
].dimensions;
return config;
}
@@ -1,5 +1,5 @@
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { ModelConfigParams } from ".";
import { questionHandlers } from "../../questions/utils";
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
@@ -51,12 +51,9 @@ const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
const DEFAULT_MODEL = "gpt-4o";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
export async function askAzureQuestions({
openAiKey,
askModels,
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
export async function askAzureQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: openAiKey || process.env.AZURE_OPENAI_KEY,
apiKey: process.env.AZURE_OPENAI_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
@@ -66,32 +63,30 @@ export async function askAzureQuestions({
},
};
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: getAvailableModelChoices(),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: getAvailableModelChoices(),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: getAvailableEmbeddingModelChoices(),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: getAvailableEmbeddingModelChoices(),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
return config;
}
@@ -2,7 +2,15 @@ import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
const MODELS = [
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.0-flash",
"gemini-2.0-flash-lite",
"gemini-1.5-pro-latest",
"gemini-pro",
"gemini-pro-vision",
];
type ModelData = {
dimensions: number;
};
@@ -15,17 +23,9 @@ const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type GeminiQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askGeminiQuestions({
askModels,
apiKey,
}: GeminiQuestionsParams): Promise<ModelConfigParams> {
export async function askGeminiQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
apiKey: process.env.GOOGLE_API_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -53,32 +53,30 @@ export async function askGeminiQuestions({
config.apiKey = key || process.env.GOOGLE_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
return config;
}
+29 -39
View File
@@ -71,17 +71,9 @@ const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type GroqQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askGroqQuestions({
askModels,
apiKey,
}: GroqQuestionsParams): Promise<ModelConfigParams> {
export async function askGroqQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
apiKey: process.env.GROQ_API_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -109,37 +101,35 @@ export async function askGroqQuestions({
config.apiKey = key || process.env.GROQ_API_KEY;
}
if (askModels) {
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: modelChoices,
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: modelChoices,
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions =
EMBEDDING_MODELS[
embeddingModel as HuggingFaceEmbeddingModelType
].dimensions;
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions =
EMBEDDING_MODELS[
embeddingModel as HuggingFaceEmbeddingModelType
].dimensions;
return config;
}
@@ -21,13 +21,7 @@ const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type HuggingfaceQuestionsParams = {
askModels: boolean;
};
export async function askHuggingfaceQuestions({
askModels,
}: HuggingfaceQuestionsParams): Promise<ModelConfigParams> {
export async function askHuggingfaceQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
@@ -37,32 +31,30 @@ export async function askHuggingfaceQuestions({
},
};
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which Hugging Face model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which Hugging Face model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
return config;
}
@@ -1,6 +1,6 @@
import prompts from "prompts";
import { questionHandlers } from "../../questions/utils";
import { ModelConfig, ModelProvider, TemplateFramework } from "../types";
import { ModelConfig, TemplateFramework } from "../types";
import { askAnthropicQuestions } from "./anthropic";
import { askAzureQuestions } from "./azure";
import { askGeminiQuestions } from "./gemini";
@@ -11,81 +11,68 @@ import { askMistralQuestions } from "./mistral";
import { askOllamaQuestions } from "./ollama";
import { askOpenAIQuestions } from "./openai";
const DEFAULT_MODEL_PROVIDER = "openai";
export type ModelConfigQuestionsParams = {
openAiKey?: string;
askModels: boolean;
framework?: TemplateFramework;
};
export type ModelConfigParams = Omit<ModelConfig, "provider">;
export async function askModelConfig({
askModels,
openAiKey,
framework,
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
if (askModels) {
const choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
{ title: "Ollama", value: "ollama" },
{ title: "Anthropic", value: "anthropic" },
{ title: "Gemini", value: "gemini" },
{ title: "Mistral", value: "mistral" },
{ title: "AzureOpenAI", value: "azure-openai" },
];
const choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
{ title: "Ollama", value: "ollama" },
{ title: "Anthropic", value: "anthropic" },
{ title: "Gemini", value: "gemini" },
{ title: "Mistral", value: "mistral" },
{ title: "AzureOpenAI", value: "azure-openai" },
];
if (framework === "fastapi") {
choices.push({ title: "T-Systems", value: "t-systems" });
choices.push({ title: "Huggingface", value: "huggingface" });
}
const { provider } = await prompts(
{
type: "select",
name: "provider",
message: "Which model provider would you like to use",
choices: choices,
initial: 0,
},
questionHandlers,
);
modelProvider = provider;
if (framework === "fastapi") {
choices.push({ title: "T-Systems", value: "t-systems" });
choices.push({ title: "Huggingface", value: "huggingface" });
}
const { provider: modelProvider } = await prompts(
{
type: "select",
name: "provider",
message: "Which model provider would you like to use",
choices: choices,
initial: 0,
},
questionHandlers,
);
let modelConfig: ModelConfigParams;
switch (modelProvider) {
case "ollama":
modelConfig = await askOllamaQuestions({ askModels });
modelConfig = await askOllamaQuestions();
break;
case "groq":
modelConfig = await askGroqQuestions({ askModels });
modelConfig = await askGroqQuestions();
break;
case "anthropic":
modelConfig = await askAnthropicQuestions({ askModels });
modelConfig = await askAnthropicQuestions();
break;
case "gemini":
modelConfig = await askGeminiQuestions({ askModels });
modelConfig = await askGeminiQuestions();
break;
case "mistral":
modelConfig = await askMistralQuestions({ askModels });
modelConfig = await askMistralQuestions();
break;
case "azure-openai":
modelConfig = await askAzureQuestions({ askModels });
modelConfig = await askAzureQuestions();
break;
case "t-systems":
modelConfig = await askLLMHubQuestions({ askModels });
modelConfig = await askLLMHubQuestions();
break;
case "huggingface":
modelConfig = await askHuggingfaceQuestions({ askModels });
modelConfig = await askHuggingfaceQuestions();
break;
default:
modelConfig = await askOpenAIQuestions({
openAiKey,
askModels,
});
modelConfig = await askOpenAIQuestions();
}
return {
...modelConfig,
@@ -31,17 +31,9 @@ const LLMHUB_EMBEDDING_MODELS = [
"text-embedding-bge-m3",
];
type LLMHubQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askLLMHubQuestions({
askModels,
apiKey,
}: LLMHubQuestionsParams): Promise<ModelConfigParams> {
export async function askLLMHubQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
apiKey: process.env.T_SYSTEMS_LLMHUB_API_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
@@ -61,11 +53,10 @@ export async function askLLMHubQuestions({
{
type: "text",
name: "key",
message: askModels
? "Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):"
: "Please provide your LLMHub API key (leave blank to skip):",
message:
"Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):",
validate: (value: string) => {
if (askModels && !value) {
if (!value) {
if (process.env.T_SYSTEMS_LLMHUB_API_KEY) {
return true;
}
@@ -79,32 +70,30 @@ export async function askLLMHubQuestions({
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: await getAvailableModelChoices(false, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: await getAvailableModelChoices(false, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: await getAvailableModelChoices(true, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: await getAvailableModelChoices(true, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
return config;
}
@@ -14,17 +14,9 @@ const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type MistralQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askMistralQuestions({
askModels,
apiKey,
}: MistralQuestionsParams): Promise<ModelConfigParams> {
export async function askMistralQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
apiKey: process.env.MISTRAL_API_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -52,32 +44,30 @@ export async function askMistralQuestions({
config.apiKey = key || process.env.MISTRAL_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
return config;
}
@@ -17,13 +17,7 @@ const EMBEDDING_MODELS: Record<string, ModelData> = {
};
const DEFAULT_EMBEDDING_MODEL: string = Object.keys(EMBEDDING_MODELS)[0];
type OllamaQuestionsParams = {
askModels: boolean;
};
export async function askOllamaQuestions({
askModels,
}: OllamaQuestionsParams): Promise<ModelConfigParams> {
export async function askOllamaQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
@@ -33,34 +27,32 @@ export async function askOllamaQuestions({
},
};
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
await ensureModel(model);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
await ensureModel(model);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
await ensureModel(embeddingModel);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
await ensureModel(embeddingModel);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
return config;
}
@@ -2,8 +2,7 @@ import got from "got";
import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { isCI } from "../../questions";
import { ModelConfigParams } from ".";
import { questionHandlers } from "../../questions/utils";
const OPENAI_API_URL = "https://api.openai.com/v1";
@@ -11,12 +10,9 @@ const OPENAI_API_URL = "https://api.openai.com/v1";
const DEFAULT_MODEL = "gpt-4o-mini";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
export async function askOpenAIQuestions({
openAiKey,
askModels,
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
export async function askOpenAIQuestions(): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: openAiKey,
apiKey: process.env.OPENAI_API_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
@@ -31,16 +27,15 @@ export async function askOpenAIQuestions({
},
};
if (!config.apiKey && !isCI) {
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
name: "key",
message: askModels
? "Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):"
: "Please provide your OpenAI API key (leave blank to skip):",
message:
"Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):",
validate: (value: string) => {
if (askModels && !value) {
if (!value) {
if (process.env.OPENAI_API_KEY) {
return true;
}
@@ -54,32 +49,30 @@ export async function askOpenAIQuestions({
config.apiKey = key || process.env.OPENAI_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: await getAvailableModelChoices(false, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: await getAvailableModelChoices(false, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: await getAvailableModelChoices(true, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
}
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: await getAvailableModelChoices(true, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
return config;
}
+89 -245
View File
@@ -5,37 +5,35 @@ import { parse, stringify } from "smol-toml";
import terminalLink from "terminal-link";
import { isUvAvailable, tryUvSync } from "./uv";
import { isCI } from "ci-info";
import { assetRelocator, copy } from "./copy";
import { templatesDir } from "./dir";
import { Tool } from "./tools";
import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateObservability,
TemplateType,
TemplateVectorDB,
} from "./types";
interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
}
import { Dependency, InstallTemplateArgs } from "./types";
import { USE_CASE_CONFIGS } from "./use-case";
const getAdditionalDependencies = (
modelConfig: ModelConfig,
vectorDb?: TemplateVectorDB,
dataSources?: TemplateDataSource[],
tools?: Tool[],
templateType?: TemplateType,
observability?: TemplateObservability,
// eslint-disable-next-line max-params
opts: Pick<
InstallTemplateArgs,
| "framework"
| "template"
| "useCase"
| "modelConfig"
| "vectorDb"
| "dataSources"
>,
) => {
const { framework, template, useCase, modelConfig, vectorDb, dataSources } =
opts;
const dependencies: Dependency[] = [];
const isPythonLlamaDeploy =
framework === "fastapi" && template === "llamaindexserver";
const useCaseDependencies =
USE_CASE_CONFIGS[useCase]?.additionalDependencies ?? [];
if (isPythonLlamaDeploy && useCaseDependencies.length > 0) {
dependencies.push(...useCaseDependencies);
}
// Add vector db dependencies
switch (vectorDb) {
case "mongo": {
@@ -152,14 +150,6 @@ const getAdditionalDependencies = (
}
}
// Add tools dependencies
console.log("Adding tools dependencies");
tools?.forEach((tool) => {
tool.dependencies?.forEach((dep) => {
dependencies.push(dep);
});
});
switch (modelConfig.provider) {
case "ollama":
dependencies.push({
@@ -172,20 +162,14 @@ const getAdditionalDependencies = (
});
break;
case "openai":
if (templateType !== "multiagent") {
dependencies.push({
name: "llama-index-llms-openai",
version: ">=0.3.2,<0.4.0",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: ">=0.3.1,<0.4.0",
});
dependencies.push({
name: "llama-index-agent-openai",
version: ">=0.4.0,<0.5.0",
});
}
dependencies.push({
name: "llama-index-llms-openai",
version: ">=0.3.2,<0.4.0",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: ">=0.3.1,<0.4.0",
});
break;
case "groq":
dependencies.push({
@@ -209,12 +193,12 @@ const getAdditionalDependencies = (
break;
case "gemini":
dependencies.push({
name: "llama-index-llms-gemini",
version: ">=0.4.0,<0.5.0",
name: "llama-index-llms-google-genai",
version: ">=0.2.0,<0.3.0",
});
dependencies.push({
name: "llama-index-embeddings-gemini",
version: ">=0.3.0,<0.4.0",
name: "llama-index-embeddings-google-genai",
version: ">=0.2.0,<0.3.0",
});
break;
case "mistral":
@@ -264,28 +248,9 @@ const getAdditionalDependencies = (
break;
}
if (observability && observability !== "none") {
if (observability === "traceloop") {
dependencies.push({
name: "traceloop-sdk",
version: ">=0.15.11",
});
}
if (observability === "llamatrace") {
dependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: ">=0.3.0,<0.4.0",
});
}
}
// If app template is llama-index-server and CI and SERVER_PACKAGE_PATH is set,
// add @llamaindex/server to dependencies
if (
templateType === "llamaindexserver" &&
isCI &&
process.env.SERVER_PACKAGE_PATH
) {
if (process.env.SERVER_PACKAGE_PATH) {
dependencies.push({
name: "llama-index-server",
version: `@file://${process.env.SERVER_PACKAGE_PATH}`,
@@ -295,17 +260,6 @@ const getAdditionalDependencies = (
return dependencies;
};
const copyRouterCode = async (root: string, tools: Tool[]) => {
// Copy sandbox router if the artifact tool is selected
if (tools?.some((t) => t.name === "artifact")) {
await copy("sandbox.py", path.join(root, "app", "api", "routers"), {
parents: true,
cwd: path.join(templatesDir, "components", "routers", "python"),
rename: assetRelocator,
});
}
};
export const addDependencies = async (
projectDir: string,
dependencies: Dependency[],
@@ -446,132 +400,15 @@ export const installPythonDependencies = () => {
}
};
const installLegacyPythonTemplate = async ({
root,
template,
vectorDb,
dataSources,
tools,
useCase,
observability,
}: Pick<
InstallTemplateArgs,
| "root"
| "template"
| "vectorDb"
| "dataSources"
| "tools"
| "useCase"
| "observability"
>) => {
const compPath = path.join(templatesDir, "components");
const enginePath = path.join(root, "app", "engine");
// Copy selected vector DB
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "vectordbs", "python", vectorDb ?? "none"),
});
if (vectorDb !== "llamacloud") {
// Copy all loaders to enginePath
// Not needed for LlamaCloud as it has its own loaders
const loaderPath = path.join(enginePath, "loaders");
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
}
// Copy settings.py to app
await copy("**", path.join(root, "app"), {
cwd: path.join(compPath, "settings", "python"),
});
// Copy services
if (template == "streaming" || template == "multiagent") {
await copy("**", path.join(root, "app", "api", "services"), {
cwd: path.join(compPath, "services", "python"),
});
}
// Copy engine code
if (template === "streaming" || template === "multiagent") {
// Select and copy engine code based on data sources and tools
let engine;
// Multiagent always uses agent engine
if (template === "multiagent") {
engine = "agent";
} else {
// For streaming, use chat engine by default
// Unless tools are selected, in which case use agent engine
if (dataSources.length > 0 && (!tools || tools.length === 0)) {
console.log(
"\nNo tools selected - use optimized context chat engine\n",
);
engine = "chat";
} else {
engine = "agent";
}
}
// Copy engine code
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", engine),
});
// Copy router code
await copyRouterCode(root, tools ?? []);
}
// Copy multiagents overrides
if (template === "multiagent") {
await copy("**", path.join(root), {
cwd: path.join(compPath, "multiagent", "python"),
});
}
if (template === "multiagent" || template === "reflex") {
if (useCase) {
const sourcePath =
template === "multiagent"
? path.join(compPath, "agents", "python", useCase)
: path.join(compPath, "reflex", useCase);
await copy("**", path.join(root), {
parents: true,
cwd: sourcePath,
rename: assetRelocator,
});
} else {
console.log(
red(
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
}
if (observability && observability !== "none") {
const templateObservabilityPath = path.join(
templatesDir,
"components",
"observability",
"python",
observability,
);
await copy("**", path.join(root, "app"), {
cwd: templateObservabilityPath,
});
}
};
const installLlamaIndexServerTemplate = async ({
root,
useCase,
useLlamaParse,
}: Pick<InstallTemplateArgs, "root" | "useCase" | "useLlamaParse">) => {
modelConfig,
}: Pick<
InstallTemplateArgs,
"root" | "useCase" | "useLlamaParse" | "modelConfig"
>) => {
if (!useCase) {
console.log(
red(
@@ -581,37 +418,54 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("*.py", path.join(root, "app"), {
const srcDir = path.join(root, "src");
const uiDir = path.join(root, "ui");
// copy workflow code to src folder
await copy("*.py", srcDir, {
parents: true,
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
});
// Copy custom UI component code
await copy(`*`, path.join(root, "components"), {
// copy model provider settings to src folder
await copy("**", srcDir, {
cwd: path.join(
templatesDir,
"components",
"providers",
"python",
modelConfig.provider,
),
});
// copy ts server to ui folder
const tsProxyDir = path.join(templatesDir, "components", "ts-proxy");
await copy("package.json", uiDir, {
parents: true,
cwd: tsProxyDir,
});
const serverFileLocation = useLlamaParse
? path.join(tsProxyDir, "llamacloud")
: path.join(tsProxyDir);
await copy("index.ts", uiDir, {
parents: true,
cwd: serverFileLocation,
});
// Copy custom UI components to ui/components folder
await copy(`*`, path.join(uiDir, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
// Copy layout components to layout folder in root
await copy("*", path.join(root, "layout"), {
// Copy layout components to ui/layout folder
await copy("*", path.join(uiDir, "layout"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "layout"),
});
if (useLlamaParse) {
await copy("index.py", path.join(root, "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"python",
),
});
// TODO: Consider moving generate.py to app folder.
await copy("generate.py", path.join(root), {
await copy("**", srcDir, {
parents: true,
cwd: path.join(
templatesDir,
@@ -629,6 +483,12 @@ const installLlamaIndexServerTemplate = async ({
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
rename: assetRelocator,
});
// Clean up, remove generate.py and index.py for non-data use cases
if (["code_generator", "document_generator", "hitl"].includes(useCase)) {
await fs.unlink(path.join(srcDir, "generate.py"));
await fs.unlink(path.join(srcDir, "index.py"));
}
};
export const installPythonTemplate = async ({
@@ -640,10 +500,8 @@ export const installPythonTemplate = async ({
postInstallAction,
modelConfig,
dataSources,
tools,
useLlamaParse,
useCase,
observability,
}: Pick<
InstallTemplateArgs,
| "appName"
@@ -654,18 +512,11 @@ export const installPythonTemplate = async ({
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "tools"
| "useLlamaParse"
| "useCase"
| "observability"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "reflex") {
templatePath = path.join(templatesDir, "types", "reflex");
} else {
templatePath = path.join(templatesDir, "types", template, framework);
}
const templatePath = path.join(templatesDir, "types", template, framework);
await copy("**", root, {
parents: true,
cwd: templatePath,
@@ -677,28 +528,21 @@ export const installPythonTemplate = async ({
root,
useCase,
useLlamaParse,
modelConfig,
});
} else {
await installLegacyPythonTemplate({
root,
template,
vectorDb,
dataSources,
tools,
useCase,
observability,
});
throw new Error(`Template ${template} not supported`);
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
const addOnDependencies = getAdditionalDependencies({
framework,
template,
useCase,
modelConfig,
vectorDb,
dataSources,
tools,
template,
observability,
);
});
await addDependencies(root, addOnDependencies);
-134
View File
@@ -1,134 +0,0 @@
import { createWriteStream, promises } from "fs";
import got from "got";
import { tmpdir } from "os";
import { join } from "path";
import { Stream } from "stream";
import tar from "tar";
import { promisify } from "util";
import { makeDir } from "./make-dir";
import { CommunityProjectConfig } from "./types";
export type RepoInfo = {
username: string;
name: string;
branch: string;
filePath: string;
};
const pipeline = promisify(Stream.pipeline);
async function downloadTar(url: string) {
const tempFile = join(tmpdir(), `next.js-cna-example.temp-${Date.now()}`);
await pipeline(got.stream(url), createWriteStream(tempFile));
return tempFile;
}
export async function downloadAndExtractRepo(
root: string,
{ username, name, branch, filePath }: RepoInfo,
) {
await makeDir(root);
const tempFile = await downloadTar(
`https://codeload.github.com/${username}/${name}/tar.gz/${branch}`,
);
await tar.x({
file: tempFile,
cwd: root,
strip: filePath ? filePath.split("/").length + 1 : 1,
filter: (p) =>
p.startsWith(
`${name}-${branch.replace(/\//g, "-")}${
filePath ? `/${filePath}/` : "/"
}`,
),
});
await promises.unlink(tempFile);
}
const getRepoInfo = async (owner: string, repo: string) => {
const repoInfoRes = await got(
`https://api.github.com/repos/${owner}/${repo}`,
{
responseType: "json",
},
);
const data = repoInfoRes.body as any;
return data;
};
export async function getProjectOptions(
owner: string,
repo: string,
): Promise<
{
value: CommunityProjectConfig;
title: string;
}[]
> {
// TODO: consider using octokit (https://github.com/octokit) if more changes are needed in the future
const getCommunityProjectConfig = async (
item: any,
): Promise<CommunityProjectConfig | null> => {
// if item is a folder, return the path with default owner, repo, and main branch
if (item.type === "dir")
return {
owner,
repo,
branch: "main",
filePath: item.path,
};
// check if it's a submodule (has size = 0 and different owner & repo)
if (item.type === "file") {
if (item.size !== 0) return null; // submodules have size = 0
// get owner and repo from git_url
const { git_url } = item;
const startIndex = git_url.indexOf("repos/") + 6;
const endIndex = git_url.indexOf("/git");
const ownerRepoStr = git_url.substring(startIndex, endIndex);
const [owner, repo] = ownerRepoStr.split("/");
// quick fetch repo info to get the default branch
const { default_branch } = await getRepoInfo(owner, repo);
// return the path with default owner, repo, and main branch (path is empty for submodules)
return {
owner,
repo,
branch: default_branch,
};
}
return null;
};
const url = `https://api.github.com/repos/${owner}/${repo}/contents`;
const response = await got(url, {
responseType: "json",
});
const data = response.body as any[];
const projectConfigs: CommunityProjectConfig[] = [];
for (const item of data) {
const communityProjectConfig = await getCommunityProjectConfig(item);
if (communityProjectConfig) projectConfigs.push(communityProjectConfig);
}
return projectConfigs.map((config) => {
return {
value: config,
title: config.filePath || config.repo, // for submodules, use repo name as title
};
});
}
export async function getRepoRawContent(repoFilePath: string) {
const url = `https://raw.githubusercontent.com/${repoFilePath}`;
const response = await got(url, {
responseType: "text",
});
return response.body;
}
+62 -29
View File
@@ -1,4 +1,5 @@
import { SpawnOptions, spawn } from "child_process";
import { SpawnOptions, exec, spawn } from "child_process";
import waitPort from "wait-port";
import { TemplateFramework, TemplateType } from "./types";
const createProcess = (
@@ -26,31 +27,12 @@ const createProcess = (
});
};
export function runReflexApp(appPath: string, port: number) {
const commandArgs = [
"run",
"reflex",
"run",
"--frontend-port",
port.toString(),
];
return createProcess("uv", commandArgs, {
stdio: "inherit",
cwd: appPath,
});
}
export function runFastAPIApp(
appPath: string,
port: number,
template: TemplateType,
) {
let commandArgs: string[];
if (template === "streaming") {
commandArgs = ["run", "dev"];
} else {
commandArgs = ["run", "fastapi", "dev", "--port", `${port}`];
}
const commandArgs = ["run", "fastapi", "dev", "--port", `${port}`];
return createProcess("uv", commandArgs, {
stdio: "inherit",
cwd: appPath,
@@ -66,6 +48,58 @@ export function runTSApp(appPath: string, port: number) {
});
}
// TODO: support run multiple LlamaDeploy server in the same machine
async function runPythonLlamaDeployServer(
appPath: string,
port: number = 4501,
) {
console.log("Starting llama_deploy server...", port);
const serverProcess = exec("uv run -m llama_deploy.apiserver", {
cwd: appPath,
env: {
...process.env,
LLAMA_DEPLOY_APISERVER_PORT: `${port}`,
},
});
// Pipe output to console
serverProcess.stdout?.pipe(process.stdout);
serverProcess.stderr?.pipe(process.stderr);
// Wait for the server to be ready
console.log("Waiting for server to be ready...");
await waitPort({ port, host: "localhost", timeout: 30000 });
// create the deployment with explicit host configuration
console.log("llama_deploy server started, creating deployment...", port);
await createProcess(
"uv",
[
"run",
"llamactl",
"-s",
`http://localhost:${port}`,
"deploy",
"llama_deploy.yml",
],
{
stdio: "inherit",
cwd: appPath,
shell: true,
},
);
console.log(`Deployment created successfully!`);
// Keep the main process alive and handle cleanup
return new Promise(() => {
process.on("SIGINT", () => {
console.log("\nShutting down...");
serverProcess.kill();
process.exit(0);
});
});
}
export async function runApp(
appPath: string,
template: TemplateType,
@@ -74,15 +108,14 @@ export async function runApp(
): Promise<void> {
try {
// Start the app
const defaultPort =
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
const defaultPort = framework === "nextjs" ? 3000 : 8000;
const appRunner =
template === "reflex"
? runReflexApp
: framework === "fastapi"
? runFastAPIApp
: runTSApp;
if (template === "llamaindexserver" && framework === "fastapi") {
await runPythonLlamaDeployServer(appPath, port);
return;
}
const appRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
await appRunner(appPath, port || defaultPort, template);
} catch (error) {
console.error("Failed to run app:", error);
-340
View File
@@ -1,340 +0,0 @@
import fs from "fs/promises";
import path from "path";
import { red } from "picocolors";
import yaml from "yaml";
import { EnvVar } from "./env-variables";
import { makeDir } from "./make-dir";
import { TemplateFramework } from "./types";
export const TOOL_SYSTEM_PROMPT_ENV_VAR = "TOOL_SYSTEM_PROMPT";
export enum ToolType {
LLAMAHUB = "llamahub",
LOCAL = "local",
}
export type Tool = {
display: string;
name: string;
config?: Record<string, any>;
dependencies?: ToolDependencies[];
supportedFrameworks?: Array<TemplateFramework>;
type: ToolType;
envVars?: EnvVar[];
};
export type ToolDependencies = {
name: string;
version?: string;
};
export const supportedTools: Tool[] = [
{
display: "Google Search",
name: "google.GoogleSearchToolSpec",
config: {
engine:
"Your search engine id, see https://developers.google.com/custom-search/v1/overview#prerequisites",
key: "Your search api key",
num: 2,
},
dependencies: [
{
name: "llama-index-tools-google",
version: ">=0.3.0,<0.4.0",
},
],
supportedFrameworks: ["fastapi"],
type: ToolType.LLAMAHUB,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for google search tool.",
value: `You are a Google search agent. You help users to get information from Google search.`,
},
],
},
{
// For python app, we will use a local DuckDuckGo search tool (instead of DuckDuckGo search tool in LlamaHub)
// to get the same results as the TS app.
display: "DuckDuckGo Search",
name: "duckduckgo",
dependencies: [
{
name: "duckduckgo-search",
version: ">=6.3.5,<7.0.0",
},
],
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for DuckDuckGo search tool.",
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
},
],
},
{
display: "Wikipedia",
name: "wikipedia.WikipediaToolSpec",
dependencies: [
{
name: "llama-index-tools-wikipedia",
version: ">=0.3.0,<0.4.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LLAMAHUB,
},
{
display: "Weather",
name: "weather",
dependencies: [],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
},
{
display: "Document generator",
name: "document_generator",
supportedFrameworks: ["fastapi", "nextjs", "express"],
dependencies: [
{
name: "xhtml2pdf",
version: ">=0.2.14,<0.3.0",
},
{
name: "markdown",
version: ">=3.7.0,<4.0.0",
},
],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for document generator tool.",
value: `If user request for a report or a post, use document generator tool to create a file and reply with the link to the file.`,
},
],
},
{
display: "Code Interpreter",
name: "interpreter",
dependencies: [
{
name: "e2b_code_interpreter",
version: ">=1.1.1,<1.2.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: "E2B_API_KEY",
description:
"E2B_API_KEY key is required to run code interpreter tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for code interpreter tool.",
value: `-You are a Python interpreter that can run any python code in a secure environment.
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
- You are given tasks to complete and you run python code to solve them.
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
- You can install any pip package (if it exists) by running a cell with pip install.`,
},
],
},
{
display: "Artifact Code Generator",
name: "artifact",
// Using pre-release version of e2b_code_interpreter
// TODO: Update to stable version when 0.0.11 is released
dependencies: [
{
name: "e2b_code_interpreter",
version: ">=1.1.1,<1.2.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: "E2B_API_KEY",
description:
"E2B_API_KEY key is required to run artifact code generator tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for artifact code generator tool.",
value:
"You are a code assistant that can generate and execute code using its tools. Don't generate code yourself, use the provided tools instead. Do not show the code or sandbox url in chat, just describe the steps to build the application based on the code that is generated by your tools. Do not describe how to run the code, just the steps to build the application.",
},
],
},
{
display: "OpenAPI action",
name: "openapi_action.OpenAPIActionToolSpec",
dependencies: [
{
name: "llama-index-tools-openapi",
version: "0.2.0",
},
{
name: "jsonschema",
version: ">=4.22.0,<5.0.0",
},
{
name: "llama-index-tools-requests",
version: "0.2.0",
},
],
config: {
openapi_uri: "The URL or file path of the OpenAPI schema",
},
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
},
{
display: "Image Generator",
name: "img_gen",
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: "STABILITY_API_KEY",
description:
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
},
],
},
{
display: "Azure Code Interpreter",
name: "azure_code_interpreter.AzureCodeInterpreterToolSpec",
supportedFrameworks: ["fastapi", "nextjs", "express"],
type: ToolType.LLAMAHUB,
dependencies: [
{
name: "llama-index-tools-azure-code-interpreter",
version: "0.2.0",
},
],
envVars: [
{
name: "AZURE_POOL_MANAGEMENT_ENDPOINT",
description:
"Please follow this guideline to create and get the pool management endpoint: https://learn.microsoft.com/azure/container-apps/sessions?tabs=azure-cli",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for Azure code interpreter tool.",
value: `-You are a Python interpreter that can run any python code in a secure environment.
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
- You are given tasks to complete and you run python code to solve them.
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
- You can install any pip package (if it exists) by running a cell with pip install.`,
},
],
},
{
display: "Form Filling",
name: "form_filling",
supportedFrameworks: ["fastapi"],
type: ToolType.LOCAL,
dependencies: [
{
name: "pandas",
version: ">=2.2.3,<3.0.0",
},
{
name: "tabulate",
version: ">=0.9.0,<1.0.0",
},
],
},
];
export const getTool = (toolName: string): Tool | undefined => {
return supportedTools.find((tool) => tool.name === toolName);
};
export const getTools = (toolsName: string[]): Tool[] => {
const tools: Tool[] = [];
for (const toolName of toolsName) {
const tool = getTool(toolName);
if (!tool) {
console.log(
red(
`Error: Tool '${toolName}' is not supported. Supported tools are: ${supportedTools
.map((t) => t.name)
.join(", ")}`,
),
);
process.exit(1);
}
tools.push(tool);
}
return tools;
};
export const toolRequiresConfig = (tool: Tool): boolean => {
const hasConfig = Object.keys(tool.config || {}).length > 0;
const hasEmptyEnvVar = tool.envVars?.some((envVar) => !envVar.value) ?? false;
return hasConfig || hasEmptyEnvVar;
};
export const toolsRequireConfig = (tools?: Tool[]): boolean => {
if (tools) {
return tools?.some(toolRequiresConfig);
}
return false;
};
export enum ConfigFileType {
YAML = "yaml",
JSON = "json",
}
export const writeToolsConfig = async (
root: string,
tools: Tool[] = [],
type: ConfigFileType = ConfigFileType.YAML,
) => {
const configContent: {
[key in ToolType]: Record<string, any>;
} = {
local: {},
llamahub: {},
};
tools.forEach((tool) => {
if (tool.type === ToolType.LLAMAHUB) {
configContent.llamahub[tool.name] = tool.config ?? {};
}
if (tool.type === ToolType.LOCAL) {
configContent.local[tool.name] = tool.config ?? {};
}
});
const configPath = path.join(root, "config");
await makeDir(configPath);
if (type === ConfigFileType.YAML) {
await fs.writeFile(
path.join(configPath, "tools.yaml"),
yaml.stringify(configContent),
);
} else {
// For Typescript, we treat llamahub tools as local tools
const tsConfigContent = {
local: {
...configContent.local,
...configContent.llamahub,
},
};
await fs.writeFile(
path.join(configPath, "tools.json"),
JSON.stringify(tsConfigContent, null, 2),
);
}
};
+19 -31
View File
@@ -1,5 +1,4 @@
import { PackageManager } from "../helpers/get-pkg-manager";
import { Tool } from "./tools";
export type ModelProvider =
| "openai"
@@ -19,15 +18,8 @@ export type ModelConfig = {
dimensions: number;
isConfigured(): boolean;
};
export type TemplateType =
| "streaming"
| "community"
| "llamapack"
| "multiagent"
| "reflex"
| "llamaindexserver";
export type TemplateType = "llamaindexserver";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
| "none"
| "mongo"
@@ -49,18 +41,14 @@ export type TemplateDataSource = {
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db";
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
export type TemplateUseCase =
| "financial_report"
| "blog"
| "deep_research"
| "form_filling"
| "extractor"
| "contract_review"
| "agentic_rag"
| "code_generator"
| "document_generator"
| "hitl";
// Config for both file and folder
export type FileSourceConfig =
| {
@@ -86,31 +74,31 @@ export type TemplateDataSourceConfig =
| WebSourceConfig
| DbSourceConfig;
export type CommunityProjectConfig = {
owner: string;
repo: string;
branch: string;
filePath?: string;
};
export interface InstallTemplateArgs {
appName: string;
root: string;
packageManager: PackageManager;
isOnline: boolean;
template: TemplateType;
framework: TemplateFramework;
ui: TemplateUI;
dataSources: TemplateDataSource[];
modelConfig: ModelConfig;
llamaCloudKey?: string;
useLlamaParse?: boolean;
communityProjectConfig?: CommunityProjectConfig;
llamapack?: string;
vectorDb?: TemplateVectorDB;
useLlamaParse: boolean;
vectorDb: TemplateVectorDB;
port?: number;
postInstallAction?: TemplatePostInstallAction;
tools?: Tool[];
observability?: TemplateObservability;
useCase?: TemplateUseCase;
postInstallAction: TemplatePostInstallAction;
useCase: TemplateUseCase;
}
export type EnvVar = {
name?: string;
description?: string;
value?: string;
};
export interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
}
+54 -356
View File
@@ -1,10 +1,9 @@
import fs from "fs/promises";
import os from "os";
import path from "path";
import { bold, cyan, red, yellow } from "picocolors";
import { bold, cyan, red } from "picocolors";
import { assetRelocator, copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { NO_DATA_USE_CASES } from "./constant";
import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { InstallTemplateArgs, ModelProvider, TemplateVectorDB } from "./types";
@@ -13,7 +12,12 @@ const installLlamaIndexServerTemplate = async ({
root,
useCase,
vectorDb,
}: Pick<InstallTemplateArgs, "root" | "useCase" | "vectorDb">) => {
modelConfig,
dataSources,
}: Pick<
InstallTemplateArgs,
"root" | "useCase" | "vectorDb" | "modelConfig" | "dataSources"
>) => {
if (!useCase) {
console.log(
red(
@@ -32,6 +36,17 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
// copy model provider settings to app folder
await copy("**", path.join(root, "src", "app"), {
cwd: path.join(
templatesDir,
"components",
"providers",
"typescript",
modelConfig.provider,
),
});
await copy("**", path.join(root), {
cwd: path.join(
templatesDir,
@@ -57,7 +72,7 @@ const installLlamaIndexServerTemplate = async ({
// Override generate.ts if workflow use case doesn't use custom UI
if (vectorDb === "llamacloud") {
await copy("generate.ts", path.join(root, "src"), {
await copy("**", path.join(root, "src"), {
parents: true,
cwd: path.join(
templatesDir,
@@ -68,235 +83,15 @@ const installLlamaIndexServerTemplate = async ({
"typescript",
),
});
await copy("index.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"typescript",
),
rename: () => "data.ts",
});
}
// Simplify use case code
if (useCase && NO_DATA_USE_CASES.includes(useCase)) {
// Artifact use case doesn't use index.
if (vectorDb === "none" && dataSources.length === 0) {
// use case without data sources doesn't use index.
// We don't need data.ts, generate.ts
await fs.rm(path.join(root, "src", "app", "data.ts"));
// TODO: Remove generate index in generate.ts and package.json if possible
}
};
const installLegacyTSTemplate = async ({
root,
template,
backend,
framework,
ui,
vectorDb,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
relativeEngineDestPath,
}: InstallTemplateArgs & {
backend: boolean;
relativeEngineDestPath: string;
}) => {
/**
* If next.js is used, update its configuration if necessary
*/
if (framework === "nextjs") {
const nextConfigJsonFile = path.join(root, "next.config.json");
const nextConfigJson: any = JSON.parse(
await fs.readFile(nextConfigJsonFile, "utf8"),
);
if (!backend) {
// update next.config.json for static site generation
nextConfigJson.output = "export";
nextConfigJson.images = { unoptimized: true };
console.log("\nUsing static site generation\n");
} else {
if (vectorDb === "milvus") {
nextConfigJson.serverExternalPackages =
nextConfigJson.serverExternalPackages ?? [];
nextConfigJson.serverExternalPackages.push("@zilliz/milvus2-sdk-node");
}
}
await fs.writeFile(
nextConfigJsonFile,
JSON.stringify(nextConfigJson, null, 2) + os.EOL,
);
const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
if (observability === "traceloop") {
const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
await fs.rm(webpackConfigDefaultFile);
await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
} else {
await fs.rm(webpackConfigOtelFile);
}
}
// copy observability component
if (observability && observability !== "none") {
const chosenObservabilityPath = path.join(
templatesDir,
"components",
"observability",
"typescript",
observability,
);
const relativeObservabilityPath = framework === "nextjs" ? "app" : "src";
await copy(
"**",
path.join(root, relativeObservabilityPath, "observability"),
{ cwd: chosenObservabilityPath },
);
}
const compPath = path.join(templatesDir, "components");
const enginePath = path.join(root, relativeEngineDestPath, "engine");
// copy llamaindex code for TS templates
await copy("**", path.join(root, relativeEngineDestPath, "llamaindex"), {
parents: true,
cwd: path.join(compPath, "llamaindex", "typescript"),
});
// copy vector db component
if (vectorDb === "llamacloud") {
console.log(
`\nUsing managed index from LlamaCloud. Ensure the ${yellow("LLAMA_CLOUD_* environment variables are set correctly.")}`,
);
} else {
console.log("\nUsing vector DB:", vectorDb ?? "none");
}
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
});
if (template === "multiagent") {
const multiagentPath = path.join(compPath, "multiagent", "typescript");
// copy workflow code for multiagent template
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: path.join(multiagentPath, "workflow"),
});
// Copy use case code for multiagent template
if (useCase) {
console.log("\nCopying use case:", useCase, "\n");
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
const useCaseCodePath = path.join(useCasePath, "workflow");
// Copy use case codes
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: useCaseCodePath,
rename: assetRelocator,
});
// Copy use case files to project root
await copy("*.*", path.join(root), {
parents: true,
cwd: useCasePath,
rename: assetRelocator,
});
} else {
console.log(
red(
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
if (framework === "nextjs") {
// patch route.ts file
await copy("**", path.join(root, relativeEngineDestPath), {
parents: true,
cwd: path.join(multiagentPath, "nextjs"),
});
} else if (framework === "express") {
// patch chat.controller.ts file
await copy("**", path.join(root, relativeEngineDestPath), {
parents: true,
cwd: path.join(multiagentPath, "express"),
});
}
}
// copy loader component (TS only supports llama_parse and file for now)
const loaderFolder = useLlamaParse ? "llama_parse" : "file";
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
});
// copy provider settings
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "providers", "typescript", modelConfig.provider),
});
// Select and copy engine code based on data sources and tools
let engine;
tools = tools ?? [];
// multiagent template always uses agent engine
if (template === "multiagent") {
engine = "agent";
} else if (dataSources.length > 0 && tools.length === 0) {
console.log("\nNo tools selected - use optimized context chat engine\n");
engine = "chat";
} else {
engine = "agent";
}
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "typescript", engine),
});
// copy settings to engine folder
await copy("**", enginePath, {
cwd: path.join(compPath, "settings", "typescript"),
});
/**
* Copy the selected UI files to the target directory and reference it.
*/
if (framework === "nextjs" && ui !== "shadcn") {
console.log("\nUsing UI:", ui, "\n");
const uiPath = path.join(compPath, "ui", ui);
const destUiPath = path.join(root, "app", "components", "ui");
// remove the default ui folder
await fs.rm(destUiPath, { recursive: true });
// copy the selected ui folder
await copy("**", destUiPath, {
parents: true,
cwd: uiPath,
rename: assetRelocator,
});
}
/** Modify frontend code to use custom API path */
if (framework === "nextjs" && !backend) {
console.log(
"\nUsing external API for frontend, removing API code and configuration\n",
);
// remove the default api folder and config folder
await fs.rm(path.join(root, "app", "api"), { recursive: true });
await fs.rm(path.join(root, "config"), { recursive: true, force: true });
// TODO: split generate.ts into generate for index and generate for ui and remove generate for index here too
// then we can also remove it for llamacloud
}
};
@@ -307,20 +102,14 @@ export const installTSTemplate = async ({
appName,
root,
packageManager,
isOnline,
template,
framework,
ui,
vectorDb,
postInstallAction,
backend,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
}: InstallTemplateArgs & { backend: boolean }) => {
}: InstallTemplateArgs) => {
console.log(bold(`Using ${packageManager}.`));
/**
@@ -336,57 +125,39 @@ export const installTSTemplate = async ({
rename: assetRelocator,
});
const relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
if (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
vectorDb,
});
} else {
await installLegacyTSTemplate({
appName,
root,
packageManager,
isOnline,
template,
backend,
framework,
ui,
vectorDb,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
relativeEngineDestPath,
dataSources,
});
if (vectorDb === "llamacloud") {
// replace index.ts with llamacloud/index.ts
await fs.rm(path.join(root, "src", "index.ts"));
await copy("index.ts", path.join(root, "src"), {
parents: true,
cwd: path.join(root, "src", "llamacloud"),
});
}
// remove llamacloud folder
await fs.rm(path.join(root, "src", "llamacloud"), { recursive: true });
} else {
throw new Error(`Template ${template} not supported`);
}
const packageJson = await updatePackageJson({
root,
appName,
dataSources,
relativeEngineDestPath,
framework,
ui,
observability,
vectorDb,
backend,
modelConfig,
template,
});
if (
backend &&
(postInstallAction === "runApp" || postInstallAction === "dependencies")
) {
await installTSDependencies(packageJson, packageManager, isOnline);
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
await installTSDependencies(packageJson, packageManager, true);
}
};
@@ -455,30 +226,12 @@ const vectorDbDependencies: Record<TemplateVectorDB, Record<string, string>> = {
async function updatePackageJson({
root,
appName,
dataSources,
relativeEngineDestPath,
framework,
ui,
observability,
vectorDb,
backend,
modelConfig,
template,
}: Pick<
InstallTemplateArgs,
| "root"
| "appName"
| "dataSources"
| "framework"
| "ui"
| "observability"
| "vectorDb"
| "modelConfig"
| "template"
> & {
relativeEngineDestPath: string;
backend: boolean;
}): Promise<any> {
"root" | "appName" | "vectorDb" | "modelConfig"
>): Promise<any> {
const packageJsonFile = path.join(root, "package.json");
const packageJson: any = JSON.parse(
await fs.readFile(packageJsonFile, "utf8"),
@@ -486,77 +239,22 @@ async function updatePackageJson({
packageJson.name = appName;
packageJson.version = "0.1.0";
if (relativeEngineDestPath && template !== "llamaindexserver") {
// TODO: move script to {root}/scripts for all frameworks
// add generate script if using context engine
packageJson.scripts = {
...packageJson.scripts,
generate: `tsx ${path.join(
relativeEngineDestPath,
"engine",
"generate.ts",
)}`,
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "~3.1.4",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
packageJson.dependencies = {
...packageJson.dependencies,
...vectorDbDependencies[vectorDb],
};
}
if (framework === "nextjs" && ui === "html") {
// remove shadcn dependencies if html ui is selected
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
packageJson.dependencies = {
...packageJson.dependencies,
"tailwind-merge": undefined,
"@radix-ui/react-slot": undefined,
"class-variance-authority": undefined,
clsx: undefined,
"lucide-react": undefined,
remark: undefined,
"remark-code-import": undefined,
"remark-gfm": undefined,
"remark-math": undefined,
"react-markdown": undefined,
"highlight.js": undefined,
};
}
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "~3.1.4",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
packageJson.dependencies = {
...packageJson.dependencies,
...vectorDbDependencies[vectorDb],
};
}
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
packageJson.dependencies = {
...packageJson.dependencies,
...providerDependencies[modelConfig.provider],
};
}
}
if (observability === "traceloop") {
packageJson.dependencies = {
...packageJson.dependencies,
"@traceloop/node-server-sdk": "^0.5.19",
};
packageJson.devDependencies = {
...packageJson.devDependencies,
"node-loader": "^2.0.0",
};
}
// if having custom server package tgz file, use it for testing @llamaindex/server
const serverPackagePath = process.env.SERVER_PACKAGE_PATH;
if (serverPackagePath && template === "llamaindexserver") {
const relativePath = path.relative(process.cwd(), serverPackagePath);
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/server": `file:${relativePath}`,
...providerDependencies[modelConfig.provider],
};
}
+84
View File
@@ -0,0 +1,84 @@
import { Dependency, EnvVar, TemplateUseCase } from "./types";
export const ALL_TYPESCRIPT_USE_CASES: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
export const ALL_PYTHON_USE_CASES: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
];
export const USE_CASE_CONFIGS: Record<
TemplateUseCase,
{
starterQuestions: string[];
additionalEnvVars?: EnvVar[];
additionalDependencies?: Dependency[];
}
> = {
agentic_rag: {
starterQuestions: [
"Letter standard in the document",
"Summarize the document",
],
},
financial_report: {
starterQuestions: [
"Compare Apple and Tesla financial performance",
"Generate a PDF report for Tesla financial",
],
additionalEnvVars: [
{
name: "E2B_API_KEY",
description: "The E2B API key to use to use code interpreter tool",
},
],
additionalDependencies: [
{
name: "e2b-code-interpreter",
version: ">=1.1.1,<2.0.0",
},
{
name: "markdown",
version: ">=3.7,<4.0",
},
{
name: "xhtml2pdf",
version: ">=0.2.17,<1.0.0",
},
],
},
deep_research: {
starterQuestions: [
"Research about Apple and Tesla",
"Financial performance of Tesla",
],
},
code_generator: {
starterQuestions: [
"Generate a code for a simple calculator",
"Generate a code for a todo list app",
],
},
document_generator: {
starterQuestions: [
"Generate a document about LlamaIndex",
"Generate a document about LLM",
],
},
hitl: {
starterQuestions: [
"List all the files in the current directory",
"Check git status",
],
},
};
+6 -139
View File
@@ -7,12 +7,10 @@ import prompts from "prompts";
import terminalLink from "terminal-link";
import checkForUpdate from "update-check";
import { createApp } from "./create-app";
import { EXAMPLE_FILE, getDataSources } from "./helpers/datasources";
import { getPkgManager } from "./helpers/get-pkg-manager";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { initializeGlobalAgent } from "./helpers/proxy";
import { runApp } from "./helpers/run-app";
import { getTools } from "./helpers/tools";
import { validateNpmName } from "./helpers/validate-pkg";
import packageJson from "./package.json";
import { askQuestions } from "./questions/index";
@@ -56,13 +54,6 @@ const program = new Command(packageJson.name)
`
Explicitly tell the CLI to bootstrap the application using Yarn
`,
)
.option(
"--template <template>",
`
Select a template to bootstrap the application with.
`,
)
.option(
@@ -70,62 +61,6 @@ const program = new Command(packageJson.name)
`
Select a framework to bootstrap the application with.
`,
)
.option(
"--files <path>",
`
Specify the path to a local file or folder for chatting.
`,
)
.option(
"--example-file",
`
Select to use an example PDF as data source.
`,
)
.option(
"--web-source <url>",
`
Specify a website URL to use as a data source.
`,
)
.option(
"--db-source <connection-string>",
`
Specify a database connection string to use as a data source.
`,
)
.option(
"--open-ai-key <key>",
`
Provide an OpenAI API key.
`,
)
.option(
"--ui <ui>",
`
Select a UI to bootstrap the application with.
`,
)
.option(
"--frontend",
`
Generate a frontend for your backend.
`,
)
.option(
"--no-frontend",
`
Do not generate a frontend for your backend.
`,
)
.option(
@@ -147,27 +82,6 @@ const program = new Command(packageJson.name)
`
Select which vector database you would like to use, such as 'none', 'pg' or 'mongo'. The default option is not to use a vector database and use the local filesystem instead ('none').
`,
)
.option(
"--tools <tools>",
`
Specify the tools you want to use by providing a comma-separated list. For example, 'wikipedia.WikipediaToolSpec,google.GoogleSearchToolSpec'. Use 'none' to not using any tools.
`,
(tools, _) => {
if (tools === "none") {
return [];
} else {
return getTools(tools.split(","));
}
},
)
.option(
"--use-llama-parse",
`
Enable LlamaParse.
`,
)
.option(
@@ -177,26 +91,12 @@ const program = new Command(packageJson.name)
Provide a LlamaCloud API key.
`,
)
.option(
"--observability <observability>",
`
Specify observability tools to use. Eg: none, opentelemetry
`,
)
.option(
"--ask-models",
`
Allow interactive selection of LLM and embedding models of different model providers.
`,
false,
)
.option(
"--pro",
`
Deprecated: Allow interactive selection of all features.
`,
false,
)
@@ -204,7 +104,7 @@ const program = new Command(packageJson.name)
"--use-case <useCase>",
`
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
Select which use case to use for the template (e.g: financial_report, blog).
`,
)
.allowUnknownOption()
@@ -212,42 +112,6 @@ const program = new Command(packageJson.name)
const options = program.opts();
if (
process.argv.includes("--no-llama-parse") ||
options.template === "reflex"
) {
options.useLlamaParse = false;
}
if (process.argv.includes("--no-files")) {
options.dataSources = [];
} else if (process.argv.includes("--example-file")) {
options.dataSources = getDataSources(options.files, options.exampleFile);
} else if (process.argv.includes("--llamacloud")) {
options.dataSources = [EXAMPLE_FILE];
options.vectorDb = "llamacloud";
} else if (process.argv.includes("--web-source")) {
options.dataSources = [
{
type: "web",
config: {
baseUrl: options.webSource,
prefix: options.webSource,
depth: 1,
},
},
];
} else if (process.argv.includes("--db-source")) {
options.dataSources = [
{
type: "db",
config: {
uri: options.dbSource,
queries: options.dbQuery || "SELECT * FROM mytable",
},
},
];
}
const packageManager = !!options.useNpm
? "npm"
: !!options.usePnpm
@@ -256,6 +120,9 @@ const packageManager = !!options.useNpm
? "yarn"
: getPkgManager();
// options above must use all the properties of QuestionArgs
const cliArgs = options as unknown as QuestionArgs;
async function run(): Promise<void> {
if (typeof projectPath === "string") {
projectPath = projectPath.trim();
@@ -319,7 +186,7 @@ async function run(): Promise<void> {
process.exit(1);
}
const answers = await askQuestions(options as unknown as QuestionArgs);
const answers = await askQuestions(cliArgs);
await createApp({
...answers,
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.22",
"version": "0.6.3",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -30,7 +30,7 @@
"dev": "ncc build ./index.ts -w -o dist/",
"e2e": "playwright test",
"e2e:python": "playwright test e2e/shared e2e/python",
"e2e:typescript": "playwright test e2e/shared e2e/typescript",
"e2e:ts": "playwright test e2e/shared e2e/typescript",
"pack-install": "bash ./scripts/pack.sh"
},
"dependencies": {
@@ -44,7 +44,6 @@
"@types/validate-npm-package-name": "3.0.0",
"async-retry": "1.3.1",
"async-sema": "3.0.1",
"ci-info": "github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
"commander": "12.1.0",
"cross-spawn": "7.0.3",
"fast-glob": "3.3.1",
-30
View File
@@ -1,30 +0,0 @@
import { askModelConfig } from "../helpers/providers";
import { QuestionArgs, QuestionResults } from "./types";
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: undefined,
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
tools: [],
};
export async function getCIQuestionResults(
program: QuestionArgs,
): Promise<QuestionResults> {
return {
...defaults,
...program,
modelConfig: await askModelConfig({
openAiKey: program.openAiKey,
askModels: false,
framework: program.framework,
}),
};
}
@@ -1,64 +0,0 @@
import {
TemplateDataSource,
TemplateFramework,
TemplateType,
} from "../helpers";
import { supportedContextFileTypes } from "./utils";
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
template?: TemplateType,
) => {
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No datasource",
value: "none",
});
choices.push({
title:
process.platform !== "linux"
? "Use an example PDF"
: "Use an example PDF (you can add your own data files later)",
value: "exampleFile",
});
}
// Linux has many distros so we won't support file/folder picker for now
if (process.platform !== "linux") {
choices.push(
{
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
value: "file",
},
{
title:
process.platform === "win32"
? "Use a local folder"
: "Use local folders",
value: "folder",
},
);
}
if (framework === "fastapi" && template !== "reflex") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
return choices;
};
+154 -20
View File
@@ -1,28 +1,162 @@
import ciInfo from "ci-info";
import { bold, yellow } from "picocolors";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
import prompts from "prompts";
import { askModelConfig } from "../helpers/providers";
import {
TemplateFramework,
TemplateUseCase,
TemplateVectorDB,
} from "../helpers/types";
import { QuestionArgs, QuestionResults } from "./types";
export const isCI = ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1";
import { useCaseConfiguration } from "./usecases";
import { askPostInstallAction, questionHandlers } from "./utils";
export const askQuestions = async (
args: QuestionArgs,
): Promise<QuestionResults> => {
if (isCI) {
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
console.log(
yellow(
`Pro mode is deprecated. Please use the new templates using the ${bold("LlamaIndexServer")} by not specifying pro mode.`,
),
);
const {
useCase: useCaseFromArgs,
framework: frameworkFromArgs,
llamaCloudKey: llamaCloudKeyFromArgs,
vectorDb: vectorDbFromArgs,
postInstallAction: postInstallActionFromArgs,
askModels: askModelsFromArgs,
} = args;
await askProQuestions(args);
return args as unknown as QuestionResults;
const { useCase } = await prompts(
[
{
type: useCaseFromArgs ? null : "select",
name: "useCase",
message: "What use case do you want to build?",
choices: [
{
title: "Agentic RAG",
value: "agentic_rag",
description:
"Chatbot that answers questions based on provided documents.",
},
{
title: "Financial Report",
value: "financial_report",
description:
"Agent that analyzes data and generates visualizations by using a code interpreter.",
},
{
title: "Deep Research",
value: "deep_research",
description:
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
},
{
title: "Code Generator",
value: "code_generator",
description: "Build a Vercel v0 styled code generator.",
},
{
title: "Document Generator",
value: "document_generator",
description: "Build a OpenAI canvas-styled document generator.",
},
{
title: "Human in the Loop",
value: "hitl",
description:
"Build a CLI command workflow that is reviewed by a human before execution",
},
],
initial: 0,
},
],
questionHandlers,
);
const { framework } = await prompts(
{
type: frameworkFromArgs ? null : "select",
name: "framework",
message: "What language do you want to use?",
choices: [
// For Python Human in the Loop use case, please refer to this chat-ui example:
// https://github.com/run-llama/chat-ui/blob/main/examples/llamadeploy/chat/src/cli_workflow.py
...(useCase !== "hitl"
? [{ title: "Python (FastAPI)", value: "fastapi" }]
: []),
{ title: "Typescript (NextJS)", value: "nextjs" },
],
initial: 0,
},
questionHandlers,
);
const finalUseCase = (useCaseFromArgs ?? useCase) as TemplateUseCase;
const finalFramework = (frameworkFromArgs ?? framework) as TemplateFramework;
if (!finalUseCase) {
throw new Error("Use case is required");
}
const results = await askSimpleQuestions(args);
return results;
if (!finalFramework) {
throw new Error("Framework is required");
}
// lookup configuration for the use case
const useCaseConfig = useCaseConfiguration[finalUseCase];
// Ask for model provider
let modelConfig = useCaseConfig.modelConfig;
if (askModelsFromArgs) {
modelConfig = await askModelConfig({
framework: finalFramework,
});
}
// Ask for LlamaCloud
let llamaCloudKey = llamaCloudKeyFromArgs ?? process.env.LLAMA_CLOUD_API_KEY;
let vectorDb: TemplateVectorDB = vectorDbFromArgs ?? "none";
if (
!vectorDbFromArgs &&
useCaseConfig.dataSources &&
!["code_generator", "document_generator", "hitl"].includes(finalUseCase) // these use cases don't use data so no need to ask for LlamaCloud
) {
const { useLlamaCloud } = await prompts(
{
type: "toggle",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud?",
active: "Yes",
inactive: "No",
initial: false,
},
questionHandlers,
);
if (useLlamaCloud && !llamaCloudKey) {
const { llamaCloudKey: llamaCloudKeyFromPrompt } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
llamaCloudKey = llamaCloudKeyFromPrompt;
}
vectorDb = useLlamaCloud ? "llamacloud" : "none";
}
const result = {
...useCaseConfig,
framework: finalFramework,
useCase: finalUseCase,
modelConfig,
llamaCloudKey,
useLlamaParse: vectorDb === "llamacloud",
vectorDb,
};
const postInstallAction =
postInstallActionFromArgs ?? (await askPostInstallAction(result));
return {
...result,
postInstallAction,
};
};
@@ -1,459 +0,0 @@
import { blue } from "picocolors";
import prompts from "prompts";
import { isCI } from ".";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
import { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
import { askModelConfig } from "../helpers/providers";
import { getProjectOptions } from "../helpers/repo";
import { supportedTools, toolRequiresConfig } from "../helpers/tools";
import { getDataSourceChoices } from "./datasources";
import { getVectorDbChoices } from "./stores";
import { QuestionArgs } from "./types";
import {
askPostInstallAction,
onPromptState,
questionHandlers,
selectLocalContextData,
} from "./utils";
export const askProQuestions = async (program: QuestionArgs) => {
if (!program.template) {
const styledRepo = blue(
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
);
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
{
title: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ title: "Fullstack python template with Reflex", value: "reflex" },
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
],
initial: 0,
},
questionHandlers,
);
program.template = template;
}
if (program.template === "community") {
const projectOptions = await getProjectOptions(
COMMUNITY_OWNER,
COMMUNITY_REPO,
);
const { communityProjectConfig } = await prompts(
{
type: "select",
name: "communityProjectConfig",
message: "Select community template",
choices: projectOptions.map(({ title, value }) => ({
title,
value: JSON.stringify(value), // serialize value to string in terminal
})),
initial: 0,
},
questionHandlers,
);
const projectConfig = JSON.parse(communityProjectConfig);
program.communityProjectConfig = projectConfig;
return; // early return - no further questions needed for community projects
}
if (program.template === "llamapack") {
const availableLlamaPacks = await getAvailableLlamapackOptions();
const { llamapack } = await prompts(
{
type: "select",
name: "llamapack",
message: "Select LlamaPack",
choices: availableLlamaPacks.map((pack) => ({
title: pack.name,
value: pack.folderPath,
})),
initial: 0,
},
questionHandlers,
);
program.llamapack = llamapack;
if (!program.postInstallAction) {
program.postInstallAction = await askPostInstallAction(program);
}
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "reflex") {
// Reflex template only supports FastAPI, empty data sources, and llamacloud
// So we just use example file for extractor template, this allows user to choose vector database later
program.dataSources = [EXAMPLE_FILE];
program.framework = "fastapi";
// Ask for which Reflex use case to use
const { useCase } = await prompts(
{
type: "select",
name: "useCase",
message: "Which use case would you like to build?",
choices: [
{ title: "Structured Extractor", value: "extractor" },
{
title: "Contract review (using Workflow)",
value: "contract_review",
},
],
initial: 0,
},
questionHandlers,
);
program.useCase = useCase;
}
if (!program.framework) {
const choices = [
{ title: "NextJS", value: "nextjs" },
{ title: "Express", value: "express" },
{ title: "FastAPI (Python)", value: "fastapi" },
];
const { framework } = await prompts(
{
type: "select",
name: "framework",
message: "Which framework would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.framework = framework;
}
if (
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 { frontend } = await prompts({
onState: onPromptState,
type: "toggle",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
initial: false,
active: "Yes",
inactive: "No",
});
program.frontend = Boolean(frontend);
}
} else {
program.frontend = false;
}
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = "shadcn";
}
}
if (!program.observability && program.template === "streaming") {
const { observability } = await prompts(
{
type: "select",
name: "observability",
message: "Would you like to set up observability?",
choices: [
{ title: "No", value: "none" },
...(program.framework === "fastapi"
? [{ title: "LlamaTrace", value: "llamatrace" }]
: []),
{ title: "Traceloop", value: "traceloop" },
],
initial: 0,
},
questionHandlers,
);
program.observability = observability;
}
if (
(program.template === "reflex" || program.template === "multiagent") &&
!program.useCase
) {
const choices =
program.template === "reflex"
? [
{ title: "Structured Extractor", value: "extractor" },
{
title: "Contract review (using Workflow)",
value: "contract_review",
},
]
: [
{
title: "Financial report (generate a financial report)",
value: "financial_report",
},
{
title: "Form filling (fill missing value in a CSV file)",
value: "form_filling",
},
{ title: "Blog writer (Write a blog post)", value: "blog" },
];
const { useCase } = await prompts(
{
type: "select",
name: "useCase",
message: "Which use case would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.useCase = useCase;
}
// Configure framework and data sources for Reflex template
if (program.template === "reflex") {
program.framework = "fastapi";
program.dataSources =
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
}
if (!program.modelConfig) {
const modelConfig = await askModelConfig({
openAiKey: program.openAiKey,
askModels: program.askModels ?? false,
framework: program.framework,
});
program.modelConfig = modelConfig;
}
if (!program.vectorDb) {
const { vectorDb } = await prompts(
{
type: "select",
name: "vectorDb",
message: "Would you like to use a vector database?",
choices: getVectorDbChoices(program.framework),
initial: 0,
},
questionHandlers,
);
program.vectorDb = vectorDb;
}
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
// When using a LlamaCloud index and no data sources are provided, just copy an example file
program.dataSources = [EXAMPLE_FILE];
}
if (!program.dataSources) {
program.dataSources = [];
// continue asking user for data sources if none are initially provided
while (true) {
const firstQuestion = program.dataSources.length === 0;
const choices = getDataSourceChoices(
program.framework,
program.dataSources,
program.template,
);
if (choices.length === 0) break;
const { selectedSource } = await prompts(
{
type: "select",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices,
initial: firstQuestion ? 1 : 0,
},
questionHandlers,
);
if (selectedSource === "no" || selectedSource === "none") {
// user doesn't want another data source or any data source
break;
}
switch (selectedSource) {
case "exampleFile": {
program.dataSources.push(EXAMPLE_FILE);
break;
}
case "file":
case "folder": {
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "file",
config: {
path: p,
},
});
}
break;
}
case "web": {
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
},
questionHandlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
break;
}
case "db": {
const dbPrompts: prompts.PromptObject<string>[] = [
{
type: "text",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
validate: (value: string) => {
if (!value) {
return "Please provide a valid connection string";
} else if (
!(
value.startsWith("mysql+pymysql://") ||
value.startsWith("postgresql+psycopg://")
)
) {
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
}
return true;
},
},
// Only ask for a query, user can provide more complex queries in the config file later
{
type: (prev) => (prev ? "text" : null),
name: "queries",
message: "Please enter the SQL query to fetch data:",
initial: "SELECT * FROM mytable",
},
];
program.dataSources.push({
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
break;
}
}
}
}
const isUsingLlamaCloud = program.vectorDb === "llamacloud";
// Asking for LlamaParse if user selected file data source
if (isUsingLlamaCloud) {
// default to use LlamaParse if using LlamaCloud
program.useLlamaParse = true;
} else {
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
if (program.useLlamaParse === undefined && program.template !== "reflex") {
// if already set useLlamaParse, don't ask again
if (program.dataSources.some((ds) => ds.type === "file")) {
const { useLlamaParse } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
active: "Yes",
inactive: "No",
},
questionHandlers,
);
program.useLlamaParse = useLlamaParse;
}
}
}
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
if (isUsingLlamaCloud || program.useLlamaParse) {
if (!program.llamaCloudKey && !isCI) {
// if already set, don't ask again
// Ask for LlamaCloud API key
const { llamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
program.llamaCloudKey = llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
}
if (
!program.tools &&
(program.template === "streaming" || program.template === "multiagent")
) {
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
value: tool.name,
}));
const { toolsName } = await prompts({
type: "multiselect",
name: "toolsName",
message:
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
choices: toolChoices,
});
const tools = toolsName?.map((tool: string) =>
supportedTools.find((t) => t.name === tool),
);
program.tools = tools;
}
if (!program.postInstallAction) {
program.postInstallAction = await askPostInstallAction(program);
}
};
-208
View File
@@ -1,208 +0,0 @@
import prompts from "prompts";
import { NO_DATA_USE_CASES } from "../helpers/constant";
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import { askModelConfig } from "../helpers/providers";
import { getTools } from "../helpers/tools";
import { ModelConfig, TemplateFramework } from "../helpers/types";
import { PureQuestionArgs, QuestionResults } from "./types";
import { askPostInstallAction, questionHandlers } from "./utils";
type AppType =
| "agentic_rag"
| "financial_report"
| "deep_research"
| "code_generator"
| "document_generator"
| "hitl";
type SimpleAnswers = {
appType: AppType;
language: TemplateFramework;
useLlamaCloud: boolean;
llamaCloudKey?: string;
};
export const askSimpleQuestions = async (
args: PureQuestionArgs,
): Promise<QuestionResults> => {
const { appType } = await prompts(
{
type: "select",
name: "appType",
message: "What use case do you want to build?",
choices: [
{
title: "Agentic RAG",
value: "agentic_rag",
description:
"Chatbot that answers questions based on provided documents.",
},
{
title: "Financial Report",
value: "financial_report",
description:
"Agent that analyzes data and generates visualizations by using a code interpreter.",
},
{
title: "Deep Research",
value: "deep_research",
description:
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
},
{
title: "Code Generator",
value: "code_generator",
description: "Build a Vercel v0 styled code generator.",
},
{
title: "Document Generator",
value: "document_generator",
description: "Build a OpenAI canvas-styled document generator.",
},
{
title: "Human in the Loop",
value: "hitl",
description:
"Build a CLI command workflow that is reviewed by a human before execution",
},
],
},
questionHandlers,
);
let language: TemplateFramework = "fastapi";
let llamaCloudKey = args.llamaCloudKey;
let useLlamaCloud = false;
const { language: newLanguage } = await prompts(
{
type: "select",
name: "language",
message: "What language do you want to use?",
choices: [
{ title: "Python (FastAPI)", value: "fastapi" },
{ title: "Typescript (NextJS)", value: "nextjs" },
],
},
questionHandlers,
);
language = newLanguage;
const shouldAskLlamaCloud = !NO_DATA_USE_CASES.includes(appType);
if (shouldAskLlamaCloud) {
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
{
type: "toggle",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud services?",
initial: false,
active: "Yes",
inactive: "No",
hint: "see https://www.llamaindex.ai/enterprise for more info",
},
questionHandlers,
);
useLlamaCloud = newUseLlamaCloud;
}
if (useLlamaCloud && !llamaCloudKey) {
// Ask for LlamaCloud API key, if not set
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
const results = await convertAnswers(args, {
appType,
language,
useLlamaCloud,
llamaCloudKey,
});
results.postInstallAction = await askPostInstallAction(results);
return results;
};
const convertAnswers = async (
args: PureQuestionArgs,
answers: SimpleAnswers,
): Promise<QuestionResults> => {
const MODEL_GPT41: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4.1",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
return !!args.openAiKey;
},
};
const lookup: Record<
AppType,
Pick<QuestionResults, "template" | "tools" | "dataSources" | "useCase"> & {
modelConfig?: ModelConfig;
}
> = {
agentic_rag: {
template: "llamaindexserver",
dataSources: [EXAMPLE_FILE],
},
financial_report: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: getTools(["interpreter", "document_generator"]),
modelConfig: MODEL_GPT41,
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: [],
modelConfig: MODEL_GPT41,
},
code_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
document_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
hitl: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
};
const results = lookup[answers.appType];
return {
framework: answers.language,
useCase: answers.appType,
ui: "shadcn",
llamaCloudKey: answers.llamaCloudKey,
useLlamaParse: answers.useLlamaCloud,
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
...results,
modelConfig:
results.modelConfig ??
(await askModelConfig({
openAiKey: args.openAiKey,
askModels: args.askModels ?? false,
framework: answers.language,
})),
frontend: true,
};
};
+12 -5
View File
@@ -1,15 +1,22 @@
import { InstallAppArgs } from "../create-app";
import {
TemplateFramework,
TemplatePostInstallAction,
TemplateUseCase,
TemplateVectorDB,
} from "../helpers";
export type QuestionResults = Omit<
InstallAppArgs,
"appPath" | "packageManager"
>;
export type PureQuestionArgs = {
export type QuestionArgs = {
useCase?: TemplateUseCase;
framework?: TemplateFramework;
askModels?: boolean;
pro?: boolean;
openAiKey?: string;
llamaCloudKey?: string;
port?: number;
postInstallAction?: TemplatePostInstallAction;
vectorDb?: TemplateVectorDB;
};
export type QuestionArgs = QuestionResults & PureQuestionArgs;
@@ -0,0 +1,42 @@
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import { getGpt41ModelConfig } from "../helpers/models";
import { ModelConfig, TemplateUseCase } from "../helpers/types";
import { QuestionResults } from "./types";
export const useCaseConfiguration: Record<
TemplateUseCase,
Pick<QuestionResults, "template" | "dataSources"> & {
modelConfig: ModelConfig;
}
> = {
agentic_rag: {
template: "llamaindexserver",
dataSources: [EXAMPLE_FILE],
modelConfig: getGpt41ModelConfig(),
},
financial_report: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
modelConfig: getGpt41ModelConfig(),
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
modelConfig: getGpt41ModelConfig(),
},
code_generator: {
template: "llamaindexserver",
dataSources: [],
modelConfig: getGpt41ModelConfig(),
},
document_generator: {
template: "llamaindexserver",
dataSources: [],
modelConfig: getGpt41ModelConfig(),
},
hitl: {
template: "llamaindexserver",
dataSources: [],
modelConfig: getGpt41ModelConfig(),
},
};
+3 -9
View File
@@ -4,7 +4,6 @@ import path from "path";
import { red } from "picocolors";
import prompts from "prompts";
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
import { toolsRequireConfig } from "../helpers/tools";
import { QuestionResults } from "./types";
export const supportedContextFileTypes = [
@@ -127,7 +126,7 @@ export const questionHandlers = {
// Ask for next action after installation
export async function askPostInstallAction(
args: QuestionResults,
args: Omit<QuestionResults, "postInstallAction">,
): Promise<TemplatePostInstallAction> {
const actionChoices = [
{
@@ -144,19 +143,14 @@ export async function askPostInstallAction(
},
];
const modelConfigured = !args.llamapack && args.modelConfig.isConfigured();
const modelConfigured = args.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = args.useLlamaParse
? args.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = args.vectorDb && args.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(args.tools)
) {
if (!hasVectorDb && modelConfigured && llamaCloudKeyConfigured) {
actionChoices.push({
title: "Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
@@ -1,36 +0,0 @@
import os
from typing import List
from llama_index.core.agent import AgentRunner
from llama_index.core.callbacks import CallbackManager
from llama_index.core.settings import Settings
from llama_index.core.tools import BaseTool
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
def get_chat_engine(params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
tools: List[BaseTool] = []
callback_manager = CallbackManager(handlers=event_handlers or [])
# Add query tool if index exists
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
index = get_index(index_config)
if index is not None:
query_engine_tool = get_query_engine_tool(index, **kwargs)
tools.append(query_engine_tool)
# Add additional tools
configured_tools: List[BaseTool] = ToolFactory.from_env()
tools.extend(configured_tools)
return AgentRunner.from_llm(
llm=Settings.llm,
tools=tools,
system_prompt=system_prompt,
callback_manager=callback_manager,
verbose=True,
)
@@ -1,78 +0,0 @@
import importlib
import os
from typing import Dict, List, Union
import yaml # type: ignore
from llama_index.core.tools.function_tool import FunctionTool
from llama_index.core.tools.tool_spec.base import BaseToolSpec
class ToolType:
LLAMAHUB = "llamahub"
LOCAL = "local"
class ToolFactory:
TOOL_SOURCE_PACKAGE_MAP = {
ToolType.LLAMAHUB: "llama_index.tools",
ToolType.LOCAL: "app.engine.tools",
}
@staticmethod
def load_tools(tool_type: str, tool_name: str, config: dict) -> List[FunctionTool]:
source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type]
try:
if "ToolSpec" in tool_name:
tool_package, tool_cls_name = tool_name.split(".")
module_name = f"{source_package}.{tool_package}"
module = importlib.import_module(module_name)
tool_class = getattr(module, tool_cls_name)
tool_spec: BaseToolSpec = tool_class(**config)
return tool_spec.to_tool_list()
else:
module = importlib.import_module(f"{source_package}.{tool_name}")
tools = module.get_tools(**config)
if not all(isinstance(tool, FunctionTool) for tool in tools):
raise ValueError(
f"The module {module} does not contain valid tools"
)
return tools
except ImportError as e:
raise ValueError(f"Failed to import tool {tool_name}: {e}")
except AttributeError as e:
raise ValueError(f"Failed to load tool {tool_name}: {e}")
@staticmethod
def from_env(
map_result: bool = False,
) -> Union[Dict[str, List[FunctionTool]], List[FunctionTool]]:
"""
Load tools from the configured file.
Args:
map_result: If True, return a map of tool names to their corresponding tools.
Returns:
A dictionary of tool names to lists of FunctionTools if map_result is True,
otherwise a list of FunctionTools.
"""
tools: Union[Dict[str, FunctionTool], List[FunctionTool]] = (
{} if map_result else []
)
if os.path.exists("config/tools.yaml"):
with open("config/tools.yaml", "r") as f:
tool_configs = yaml.safe_load(f)
for tool_type, config_entries in tool_configs.items():
for tool_name, config in config_entries.items():
loaded_tools = ToolFactory.load_tools(
tool_type, tool_name, config
)
if map_result:
tools.update( # type: ignore
{tool.metadata.name: tool for tool in loaded_tools}
)
else:
tools.extend(loaded_tools) # type: ignore
return tools
@@ -1,111 +0,0 @@
import logging
from typing import Dict, List, Optional
from llama_index.core.base.llms.types import ChatMessage
from llama_index.core.settings import Settings
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# Prompt based on https://github.com/e2b-dev/ai-artifacts
CODE_GENERATION_PROMPT = """You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: nextjs@14.2.5, typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, nuxt@3.13.0, tailwindcss. Port: 3000.
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
Make sure to use the correct syntax for the programming language you're using.
"""
class CodeArtifact(BaseModel):
commentary: str = Field(
...,
description="Describe what you're about to do and the steps you want to take for generating the artifact in great detail.",
)
template: str = Field(
..., description="Name of the template used to generate the artifact."
)
title: str = Field(..., description="Short title of the artifact. Max 3 words.")
description: str = Field(
..., description="Short description of the artifact. Max 1 sentence."
)
additional_dependencies: List[str] = Field(
...,
description="Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.",
)
has_additional_dependencies: bool = Field(
...,
description="Detect if additional dependencies that are not included in the template are required by the artifact.",
)
install_dependencies_command: str = Field(
...,
description="Command to install additional dependencies required by the artifact.",
)
port: Optional[int] = Field(
...,
description="Port number used by the resulted artifact. Null when no ports are exposed.",
)
file_path: str = Field(
..., description="Relative path to the file, including the file name."
)
code: str = Field(
...,
description="Code generated by the artifact. Only runnable code is allowed.",
)
class CodeGeneratorTool:
def __init__(self):
pass
def artifact(
self,
query: str,
sandbox_files: Optional[List[str]] = None,
old_code: Optional[str] = None,
) -> Dict:
"""Generate a code artifact based on the provided input.
Args:
query (str): A description of the application you want to build.
sandbox_files (Optional[List[str]], optional): A list of sandbox file paths. Defaults to None. Include these files if the code requires them.
old_code (Optional[str], optional): The existing code to be modified. Defaults to None.
Returns:
Dict: A dictionary containing information about the generated artifact.
"""
if old_code:
user_message = f"{query}\n\nThe existing code is: \n```\n{old_code}\n```"
else:
user_message = query
if sandbox_files:
user_message += f"\n\nThe provided files are: \n{str(sandbox_files)}"
messages: List[ChatMessage] = [
ChatMessage(role="system", content=CODE_GENERATION_PROMPT),
ChatMessage(role="user", content=user_message),
]
try:
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact) # type: ignore
response = sllm.chat(messages)
data: CodeArtifact = response.raw
data_dict = data.model_dump()
if sandbox_files:
data_dict["files"] = sandbox_files
return data_dict
except Exception as e:
logger.error(f"Failed to generate artifact: {str(e)}")
raise e
def get_tools(**kwargs):
return [FunctionTool.from_defaults(fn=CodeGeneratorTool().artifact)]
@@ -1,70 +0,0 @@
from llama_index.core.tools.function_tool import FunctionTool
def duckduckgo_search(
query: str,
region: str = "wt-wt",
max_results: int = 10,
):
"""
Use this function to search for any query in DuckDuckGo.
Args:
query (str): The query to search in DuckDuckGo.
region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
max_results Optional(int): The maximum number of results to be returned. Default is 10.
"""
try:
from duckduckgo_search import DDGS
except ImportError:
raise ImportError(
"duckduckgo_search package is required to use this function."
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
)
results = []
with DDGS() as ddg:
results = list(
ddg.text(
keywords=query,
region=region,
max_results=max_results,
)
)
return results
def duckduckgo_image_search(
query: str,
region: str = "wt-wt",
max_results: int = 10,
):
"""
Use this function to search for images in DuckDuckGo.
Args:
query (str): The query to search in DuckDuckGo.
region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
max_results Optional(int): The maximum number of results to be returned. Default is 10.
"""
try:
from duckduckgo_search import DDGS
except ImportError:
raise ImportError(
"duckduckgo_search package is required to use this function."
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
)
with DDGS() as ddg:
results = list(
ddg.images(
keywords=query,
region=region,
max_results=max_results,
)
)
return results
def get_tools(**kwargs):
return [
FunctionTool.from_defaults(duckduckgo_search),
FunctionTool.from_defaults(duckduckgo_image_search),
]
@@ -1,224 +0,0 @@
import logging
import os
import uuid
from textwrap import dedent
from typing import Optional
import pandas as pd
from app.services.file import FileService
from llama_index.core import Settings
from llama_index.core.prompts import PromptTemplate
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class MissingCell(BaseModel):
"""
A missing cell in a table.
"""
row_index: int = Field(description="The index of the row of the missing cell")
column_index: int = Field(description="The index of the column of the missing cell")
question_to_answer: str = Field(
description="The question to answer to fill the missing cell"
)
class MissingCells(BaseModel):
"""
A list of missing cells.
"""
missing_cells: list[MissingCell] = Field(description="The missing cells")
class CellValue(BaseModel):
row_index: int = Field(description="The row index of the cell")
column_index: int = Field(description="The column index of the cell")
value: str = Field(
description="The value of the cell. Should be a concise value (numerical value or specific value)"
)
class FormFillingTool:
"""
Fill out missing cells in a CSV file using information from the knowledge base.
"""
save_dir: str = os.path.join("output", "tools")
# Default prompt for extracting questions
# Replace the default prompt with a custom prompt by setting the EXTRACT_QUESTIONS_PROMPT environment variable.
_default_extract_questions_prompt = dedent(
"""
You are a data analyst. You are given a table with missing cells.
Your task is to identify the missing cells and the questions needed to fill them.
IMPORTANT: Column indices should be 0-based, where the first data column is index 1
(index 0 is typically the row names/index column).
# Instructions:
- Understand the entire content of the table and the topics of the table.
- Identify the missing cells and the meaning of the data in the cells.
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
# Example:
# | | Name | Age | City |
# |----|------|-----|------|
# | 0 | John | | Paris|
# | 1 | Mary | | |
# | 2 | | 30 | |
#
# Your thoughts:
# - The table is about people's names, ages, and cities.
# - Row: 1, Column: 1 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
# - Row: 1, Column: 2 (City column), Question: "In which city does Mary live? Please provide only the city name."
Please provide your answer in the requested format.
# Here is your task:
- Table content:
{table_content}
- Your answer:
"""
)
def extract_questions(
self,
file_path: Optional[str] = None,
file_content: Optional[str] = None,
) -> dict:
"""
Use this tool to extract missing cells in a CSV file and generate questions to fill them.
Pass either the path to the CSV file or the content of the CSV file.
Args:
file_path (Optional[str]): The local file path to the CSV file to extract missing cells from (Don't pass a sandbox path).
file_content (Optional[str]): The content of the CSV file to extract missing cells from.
Returns:
dict: A dictionary containing the missing cells and their corresponding questions.
"""
extract_questions_prompt = os.getenv(
"EXTRACT_QUESTIONS_PROMPT", self._default_extract_questions_prompt
)
if file_path is None and file_content is None:
raise ValueError("Either `file_path` or `file_content` must be provided")
table_content = None
if file_path:
file_name, file_extension = self._get_file_name_and_extension(
file_path, file_content
)
try:
df = pd.read_csv(file_path)
except FileNotFoundError as e:
return {
"error": str(e),
"message": "Please check and update the file path and ensure it's a local path - not a sandbox path.",
}
table_content = df.to_markdown()
if table_content is None:
raise ValueError("Could not convert the table to markdown")
if file_content:
table_content = file_content
if table_content is None:
raise ValueError("Table content not found")
response: MissingCells = Settings.llm.structured_predict(
output_cls=MissingCells,
prompt=PromptTemplate(extract_questions_prompt),
table_content=table_content,
)
return response.model_dump()
def fill_form(
self,
cell_values: list[CellValue],
file_path: Optional[str] = None,
file_content: Optional[str] = None,
) -> dict:
"""
Use this tool to fill cell values into a CSV file.
Requires cell values to be used for filling out, as well as either the path to the CSV file or the content of the CSV file.
Args:
cell_values (list[CellValue]): The cell values used to fill out the CSV file (call `extract_questions` and query engine to construct the cell values).
file_path (Optional[str]): The local file path to the CSV file that should be filled out (not as sandbox path).
file_content (Optional[str]): The content of the CSV file that should be filled out.
Returns:
dict: A dictionary containing the content and metadata of the filled-out file.
"""
file_name, file_extension = self._get_file_name_and_extension(
file_path, file_content
)
df = pd.read_csv(file_path)
# Fill the dataframe with the cell values
filled_df = df.copy()
for cell_value in cell_values:
if not isinstance(cell_value, CellValue):
cell_value = CellValue(**cell_value)
filled_df.iloc[cell_value.row_index, cell_value.column_index] = (
cell_value.value
)
# Save the filled table to a new CSV file
csv_content: str = filled_df.to_csv(index=False)
file_metadata = FileService.save_file(
content=csv_content,
file_name=f"{file_name}_filled.csv",
save_dir=self.save_dir,
)
new_content: str = filled_df.to_markdown()
result = {
"filled_content": new_content,
"filled_file": file_metadata,
}
return result
def _get_file_name_and_extension(
self, file_path: Optional[str], file_content: Optional[str]
) -> tuple[str, str]:
if file_path is None and file_content is None:
raise ValueError("Either `file_path` or `file_content` must be provided")
if file_path is None:
file_name = str(uuid.uuid4())
file_extension = ".csv"
else:
file_name, file_extension = os.path.splitext(file_path)
if file_extension != ".csv":
raise ValueError("Form filling is only supported for CSV files")
return file_name, file_extension
def _save_output(self, file_name: str, output: str) -> dict:
"""
Save the output to a file.
"""
file_metadata = FileService.save_file(
content=output,
file_name=file_name,
save_dir=self.save_dir,
)
return file_metadata.model_dump()
def get_tools(**kwargs):
tool = FormFillingTool()
return [
FunctionTool.from_defaults(tool.extract_questions),
FunctionTool.from_defaults(tool.fill_form),
]
@@ -1,109 +0,0 @@
import logging
import os
import uuid
from typing import Optional
import requests # type: ignore
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class ImageGeneratorToolOutput(BaseModel):
is_success: bool = Field(
...,
description="Whether the image generation was successful.",
)
image_url: Optional[str] = Field(
None,
description="The URL of the generated image.",
)
error_message: Optional[str] = Field(
None,
description="The error message if the image generation failed.",
)
class ImageGeneratorTool:
_IMG_OUTPUT_FORMAT = "webp"
_IMG_OUTPUT_DIR = "output/tools"
_IMG_GEN_API = "https://api.stability.ai/v2beta/stable-image/generate/core"
def __init__(self, api_key: str = None):
if not api_key:
api_key = os.getenv("STABILITY_API_KEY")
self._api_key = api_key
self.fileserver_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
if self._api_key is None:
raise ValueError(
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys"
)
if self.fileserver_url_prefix is None:
raise ValueError("FILESERVER_URL_PREFIX is required.")
def _prepare_output_dir(self):
"""
Create the output directory if it doesn't exist
"""
if not os.path.exists(self._IMG_OUTPUT_DIR):
os.makedirs(self._IMG_OUTPUT_DIR, exist_ok=True)
def _save_image(self, image_data: bytes):
self._prepare_output_dir()
filename = f"{uuid.uuid4()}.{self._IMG_OUTPUT_FORMAT}"
output_path = os.path.join(self._IMG_OUTPUT_DIR, filename)
with open(output_path, "wb") as f:
f.write(image_data)
url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{self._IMG_OUTPUT_DIR}/{filename}"
logger.info(f"Saved image to {output_path}.\nURL: {url}")
return url
def _call_stability_api(self, prompt: str):
headers = {
"authorization": f"Bearer {self._api_key}",
"accept": "image/*",
}
data = {
"prompt": prompt,
"output_format": self._IMG_OUTPUT_FORMAT,
}
response = requests.post(
self._IMG_GEN_API,
headers=headers,
files={"none": ""},
data=data,
)
response.raise_for_status()
return response
def generate_image(self, prompt: str) -> ImageGeneratorToolOutput:
"""
Use this tool to generate an image based on the prompt.
Args:
prompt (str): The prompt to generate the image from.
"""
try:
# Call the Stability API
response = self._call_stability_api(prompt)
# Save the image and get the URL
image_url = self._save_image(response.content)
return ImageGeneratorToolOutput(
is_success=True,
image_url=image_url,
)
except Exception as e:
logger.exception(e, exc_info=True)
return ImageGeneratorToolOutput(
is_success=False,
error_message=str(e),
)
def get_tools(**kwargs):
return [FunctionTool.from_defaults(ImageGeneratorTool(**kwargs).generate_image)]
@@ -1,80 +0,0 @@
from typing import Dict, List, Tuple
from llama_index.tools.openapi import OpenAPIToolSpec
from llama_index.tools.requests import RequestsToolSpec
class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec):
"""
A combination of OpenAPI and Requests tool specs that can parse OpenAPI specs and make requests.
openapi_uri: str: The file path or URL to the OpenAPI spec.
domain_headers: dict: Whitelist domains and the headers to use.
"""
spec_functions = OpenAPIToolSpec.spec_functions + RequestsToolSpec.spec_functions
# Cached parsed specs by URI
_specs: Dict[str, Tuple[Dict, List[str]]] = {}
def __init__(self, openapi_uri: str, domain_headers: dict = None, **kwargs):
if domain_headers is None:
domain_headers = {}
if openapi_uri not in self._specs:
openapi_spec, servers = self._load_openapi_spec(openapi_uri)
self._specs[openapi_uri] = (openapi_spec, servers)
else:
openapi_spec, servers = self._specs[openapi_uri]
# Add the servers to the domain headers if they are not already present
for server in servers:
if server not in domain_headers:
domain_headers[server] = {}
OpenAPIToolSpec.__init__(self, spec=openapi_spec)
RequestsToolSpec.__init__(self, domain_headers)
@staticmethod
def _load_openapi_spec(uri: str) -> Tuple[Dict, List[str]]:
"""
Load an OpenAPI spec from a URI.
Args:
uri (str): A file path or URL to the OpenAPI spec.
Returns:
List[Document]: A list of Document objects.
"""
from urllib.parse import urlparse
import yaml # type: ignore
if uri.startswith("http"):
import requests # type: ignore
response = requests.get(uri)
if response.status_code != 200:
raise ValueError(
"Could not initialize OpenAPIActionToolSpec: "
f"Failed to load OpenAPI spec from {uri}, status code: {response.status_code}"
)
spec = yaml.safe_load(response.text)
elif uri.startswith("file"):
filepath = urlparse(uri).path
with open(filepath, "r") as file:
spec = yaml.safe_load(file)
else:
raise ValueError(
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI URI provided. "
"Only HTTP and file path are supported."
)
# Add the servers to the whitelist
try:
servers = [
urlparse(server["url"]).netloc for server in spec.get("servers", [])
]
except KeyError as e:
raise ValueError(
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI spec provided. "
"Could not get `servers` from the spec."
) from e
return spec, servers
@@ -1,187 +0,0 @@
import os
from typing import Any, Dict, List, Optional, Sequence
from llama_index.core import get_response_synthesizer
from llama_index.core.base.base_query_engine import BaseQueryEngine
from llama_index.core.base.response.schema import RESPONSE_TYPE, Response
from llama_index.core.multi_modal_llms import MultiModalLLM
from llama_index.core.prompts.base import BasePromptTemplate
from llama_index.core.prompts.default_prompt_selectors import (
DEFAULT_TEXT_QA_PROMPT_SEL,
)
from llama_index.core.query_engine.multi_modal import _get_image_and_text_nodes
from llama_index.core.response_synthesizers.base import BaseSynthesizer, QueryTextType
from llama_index.core.schema import (
ImageNode,
NodeWithScore,
)
from llama_index.core.tools.query_engine import QueryEngineTool
from llama_index.core.types import RESPONSE_TEXT_TYPE
from app.settings import get_multi_modal_llm
def create_query_engine(index, **kwargs) -> BaseQueryEngine:
"""
Create a query engine for the given index.
Args:
index: The index to create a query engine for.
params (optional): Additional parameters for the query engine, e.g: similarity_top_k
"""
top_k = int(os.getenv("TOP_K", 0))
if top_k != 0 and kwargs.get("filters") is None:
kwargs["similarity_top_k"] = top_k
multimodal_llm = get_multi_modal_llm()
if multimodal_llm:
kwargs["response_synthesizer"] = MultiModalSynthesizer(
multimodal_model=multimodal_llm,
)
# If index is index is LlamaCloudIndex
# use auto_routed mode for better query results
if index.__class__.__name__ == "LlamaCloudIndex":
if kwargs.get("retrieval_mode") is None:
kwargs["retrieval_mode"] = "auto_routed"
if multimodal_llm:
kwargs["retrieve_image_nodes"] = True
return index.as_query_engine(**kwargs)
def get_query_engine_tool(
index,
name: Optional[str] = None,
description: Optional[str] = None,
**kwargs,
) -> QueryEngineTool:
"""
Get a query engine tool for the given index.
Args:
index: The index to create a query engine for.
name (optional): The name of the tool.
description (optional): The description of the tool.
"""
if name is None:
name = "query_index"
if description is None:
description = (
"Use this tool to retrieve information about the text corpus from an index."
)
query_engine = create_query_engine(index, **kwargs)
return QueryEngineTool.from_defaults(
query_engine=query_engine,
name=name,
description=description,
)
class MultiModalSynthesizer(BaseSynthesizer):
"""
A synthesizer that summarizes text nodes and uses a multi-modal LLM to generate a response.
"""
def __init__(
self,
multimodal_model: MultiModalLLM,
response_synthesizer: Optional[BaseSynthesizer] = None,
text_qa_template: Optional[BasePromptTemplate] = None,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self._multi_modal_llm = multimodal_model
self._response_synthesizer = response_synthesizer or get_response_synthesizer()
self._text_qa_template = text_qa_template or DEFAULT_TEXT_QA_PROMPT_SEL
def _get_prompts(self, **kwargs) -> Dict[str, Any]:
return {
"text_qa_template": self._text_qa_template,
}
def _update_prompts(self, prompts: Dict[str, Any]) -> None:
if "text_qa_template" in prompts:
self._text_qa_template = prompts["text_qa_template"]
async def aget_response(
self,
*args,
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
return await self._response_synthesizer.aget_response(*args, **response_kwargs)
def get_response(self, *args, **kwargs) -> RESPONSE_TEXT_TYPE:
return self._response_synthesizer.get_response(*args, **kwargs)
async def asynthesize(
self,
query: QueryTextType,
nodes: List[NodeWithScore],
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
**response_kwargs: Any,
) -> RESPONSE_TYPE:
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
if len(image_nodes) == 0:
return await self._response_synthesizer.asynthesize(query, text_nodes)
# Summarize the text nodes to avoid exceeding the token limit
text_response = str(
await self._response_synthesizer.asynthesize(query, text_nodes)
)
fmt_prompt = self._text_qa_template.format(
context_str=text_response,
query_str=query.query_str, # type: ignore
)
llm_response = await self._multi_modal_llm.acomplete(
prompt=fmt_prompt,
image_documents=[
image_node.node
for image_node in image_nodes
if isinstance(image_node.node, ImageNode)
],
)
return Response(
response=str(llm_response),
source_nodes=nodes,
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
)
def synthesize(
self,
query: QueryTextType,
nodes: List[NodeWithScore],
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
**response_kwargs: Any,
) -> RESPONSE_TYPE:
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
if len(image_nodes) == 0:
return self._response_synthesizer.synthesize(query, text_nodes)
# Summarize the text nodes to avoid exceeding the token limit
text_response = str(self._response_synthesizer.synthesize(query, text_nodes))
fmt_prompt = self._text_qa_template.format(
context_str=text_response,
query_str=query.query_str, # type: ignore
)
llm_response = self._multi_modal_llm.complete(
prompt=fmt_prompt,
image_documents=[
image_node.node
for image_node in image_nodes
if isinstance(image_node.node, ImageNode)
],
)
return Response(
response=str(llm_response),
source_nodes=nodes,
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
)
@@ -1,74 +0,0 @@
"""Open Meteo weather map tool spec."""
import logging
import pytz # type: ignore
import requests # type: ignore
from llama_index.core.tools import FunctionTool
logger = logging.getLogger(__name__)
class OpenMeteoWeather:
geo_api = "https://geocoding-api.open-meteo.com/v1"
weather_api = "https://api.open-meteo.com/v1"
@classmethod
def _get_geo_location(cls, location: str) -> dict:
"""Get geo location from location name."""
params = {"name": location, "count": 10, "language": "en", "format": "json"}
response = requests.get(f"{cls.geo_api}/search", params=params)
if response.status_code != 200:
raise Exception(f"Failed to fetch geo location: {response.status_code}")
else:
data = response.json()
result = data["results"][0]
geo_location = {
"id": result["id"],
"name": result["name"],
"latitude": result["latitude"],
"longitude": result["longitude"],
}
return geo_location
@classmethod
def get_weather_information(cls, location: str) -> dict:
"""Use this function to get the weather of any given location.
Note that the weather code should follow WMO Weather interpretation codes (WW):
0: Clear sky
1, 2, 3: Mainly clear, partly cloudy, and overcast
45, 48: Fog and depositing rime fog
51, 53, 55: Drizzle: Light, moderate, and dense intensity
56, 57: Freezing Drizzle: Light and dense intensity
61, 63, 65: Rain: Slight, moderate and heavy intensity
66, 67: Freezing Rain: Light and heavy intensity
71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
77: Snow grains
80, 81, 82: Rain showers: Slight, moderate, and violent
85, 86: Snow showers slight and heavy
95: Thunderstorm: Slight or moderate
96, 99: Thunderstorm with slight and heavy hail
"""
logger.info(
f"Calling open-meteo api to get weather information of location: {location}"
)
geo_location = cls._get_geo_location(location)
timezone = pytz.timezone("UTC").zone
params = {
"latitude": geo_location["latitude"],
"longitude": geo_location["longitude"],
"current": "temperature_2m,weather_code",
"hourly": "temperature_2m,weather_code",
"daily": "weather_code",
"timezone": timezone,
}
response = requests.get(f"{cls.weather_api}/forecast", params=params)
if response.status_code != 200:
raise Exception(
f"Failed to fetch weather information: {response.status_code}"
)
return response.json()
def get_tools(**kwargs):
return [FunctionTool.from_defaults(OpenMeteoWeather.get_weather_information)]
@@ -1,47 +0,0 @@
import os
from app.engine.index import IndexConfig, get_index
from app.engine.node_postprocessors import NodeCitationProcessor
from fastapi import HTTPException
from llama_index.core.callbacks import CallbackManager
from llama_index.core.chat_engine import CondensePlusContextChatEngine
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.settings import Settings
def get_chat_engine(params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
top_k = int(os.getenv("TOP_K", 0))
llm = Settings.llm
memory = ChatMemoryBuffer.from_defaults(
token_limit=llm.metadata.context_window - 256
)
callback_manager = CallbackManager(handlers=event_handlers or [])
node_postprocessors = []
if citation_prompt:
node_postprocessors = [NodeCitationProcessor()]
system_prompt = f"{system_prompt}\n{citation_prompt}"
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
index = get_index(index_config)
if index is None:
raise HTTPException(
status_code=500,
detail=str(
"StorageContext is empty - call 'uv run generate' to generate the storage first"
),
)
if top_k != 0 and kwargs.get("similarity_top_k") is None:
kwargs["similarity_top_k"] = top_k
retriever = index.as_retriever(**kwargs)
return CondensePlusContextChatEngine(
llm=llm,
memory=memory,
system_prompt=system_prompt,
retriever=retriever,
node_postprocessors=node_postprocessors, # type: ignore
callback_manager=callback_manager,
)
@@ -1,21 +0,0 @@
from typing import List, Optional
from llama_index.core import QueryBundle
from llama_index.core.postprocessor.types import BaseNodePostprocessor
from llama_index.core.schema import NodeWithScore
class NodeCitationProcessor(BaseNodePostprocessor):
"""
Append node_id into metadata for citation purpose.
Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
"""
def _postprocess_nodes(
self,
nodes: List[NodeWithScore],
query_bundle: Optional[QueryBundle] = None,
) -> List[NodeWithScore]:
for node_score in nodes:
node_score.node.metadata["node_id"] = node_score.node.node_id
return nodes
@@ -1,36 +0,0 @@
import { BaseChatEngine, BaseToolWithCall, LLMAgent } from "llamaindex";
import fs from "node:fs/promises";
import path from "node:path";
import { getDataSource } from "./index";
import { createTools } from "./tools";
import { createQueryEngineTool } from "./tools/query-engine";
export async function createChatEngine(documentIds?: string[], params?: any) {
const tools: BaseToolWithCall[] = [];
// Add a query engine tool if we have a data source
// Delete this code if you don't have a data source
const index = await getDataSource(params);
if (index) {
tools.push(createQueryEngineTool(index, { documentIds }));
}
const configFile = path.join("config", "tools.json");
let toolConfig: any;
try {
// add tools from config file if it exists
toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
} catch (e) {
console.info(`Could not read ${configFile} file. Using no tools.`);
}
if (toolConfig) {
tools.push(...(await createTools(toolConfig)));
}
const agent = new LLMAgent({
tools,
systemPrompt: process.env.SYSTEM_PROMPT,
}) as unknown as BaseChatEngine;
return agent;
}
@@ -1,146 +0,0 @@
import type { JSONSchemaType } from "ajv";
import {
BaseTool,
ChatMessage,
JSONValue,
Settings,
ToolMetadata,
} from "llamaindex";
// prompt based on https://github.com/e2b-dev/ai-artifacts
const CODE_GENERATION_PROMPT = `You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:\n
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: nextjs@14.2.5, typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, nuxt@3.13.0, tailwindcss. Port: 3000.
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
Provide detail information about the artifact you're about to generate in the following JSON format with the following keys:
commentary: Describe what you're about to do and the steps you want to take for generating the artifact in great detail.
template: Name of the template used to generate the artifact.
title: Short title of the artifact. Max 3 words.
description: Short description of the artifact. Max 1 sentence.
additional_dependencies: Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.
has_additional_dependencies: Detect if additional dependencies that are not included in the template are required by the artifact.
install_dependencies_command: Command to install additional dependencies required by the artifact.
port: Port number used by the resulted artifact. Null when no ports are exposed.
file_path: Relative path to the file, including the file name.
code: Code generated by the artifact. Only runnable code is allowed.
Make sure to use the correct syntax for the programming language you're using. Make sure to generate only one code file. If you need to use CSS, make sure to include the CSS in the code file using Tailwind CSS syntax.
`;
// detail information to execute code
export type CodeArtifact = {
commentary: string;
template: string;
title: string;
description: string;
additional_dependencies: string[];
has_additional_dependencies: boolean;
install_dependencies_command: string;
port: number | null;
file_path: string;
code: string;
files?: string[];
};
export type CodeGeneratorParameter = {
requirement: string;
oldCode?: string;
sandboxFiles?: string[];
};
export type CodeGeneratorToolParams = {
metadata?: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
};
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
{
name: "artifact",
description: `Generate a code artifact based on the input. Don't call this tool if the user has not asked for code generation. E.g. if the user asks to write a description or specification, don't call this tool.`,
parameters: {
type: "object",
properties: {
requirement: {
type: "string",
description: "The description of the application you want to build.",
},
oldCode: {
type: "string",
description: "The existing code to be modified",
nullable: true,
},
sandboxFiles: {
type: "array",
description:
"A list of sandbox file paths. Include these files if the code requires them.",
items: {
type: "string",
},
nullable: true,
},
},
required: ["requirement"],
},
};
export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
metadata: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
constructor(params?: CodeGeneratorToolParams) {
this.metadata = params?.metadata || DEFAULT_META_DATA;
}
async call(input: CodeGeneratorParameter) {
try {
const artifact = await this.generateArtifact(
input.requirement,
input.oldCode,
input.sandboxFiles, // help the generated code use exact files
);
if (input.sandboxFiles) {
artifact.files = input.sandboxFiles;
}
return artifact as JSONValue;
} catch (error) {
return { isError: true };
}
}
// Generate artifact (code, environment, dependencies, etc.)
async generateArtifact(
query: string,
oldCode?: string,
attachments?: string[],
): Promise<CodeArtifact> {
const userMessage = `
${query}
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
`;
const messages: ChatMessage[] = [
{ role: "system", content: CODE_GENERATION_PROMPT },
{ role: "user", content: userMessage },
];
try {
const response = await Settings.llm.chat({ messages });
const content = response.message.content.toString();
const jsonContent = content
.replace(/^```json\s*|\s*```$/g, "")
.replace(/^`+|`+$/g, "")
.trim();
const artifact = JSON.parse(jsonContent) as CodeArtifact;
return artifact;
} catch (error) {
console.log("Failed to generate artifact", error);
throw error;
}
}
}
@@ -1,142 +0,0 @@
import { JSONSchemaType } from "ajv";
import { BaseTool, ToolMetadata } from "llamaindex";
import { marked } from "marked";
import path from "node:path";
import { saveDocument } from "../../llamaindex/documents/helper";
const OUTPUT_DIR = "output/tools";
type DocumentParameter = {
originalContent: string;
fileName: string;
};
const DEFAULT_METADATA: ToolMetadata<JSONSchemaType<DocumentParameter>> = {
name: "document_generator",
description:
"Generate HTML document from markdown content. Return a file url to the document",
parameters: {
type: "object",
properties: {
originalContent: {
type: "string",
description: "The original markdown content to convert.",
},
fileName: {
type: "string",
description: "The name of the document file (without extension).",
},
},
required: ["originalContent", "fileName"],
},
};
const COMMON_STYLES = `
body {
font-family: Arial, sans-serif;
line-height: 1.3;
color: #333;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1em;
margin-bottom: 0.5em;
}
p {
margin-bottom: 0.7em;
}
code {
background-color: #f4f4f4;
padding: 2px 4px;
border-radius: 4px;
}
pre {
background-color: #f4f4f4;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 1em;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
img {
max-width: 90%;
height: auto;
display: block;
margin: 1em auto;
border-radius: 10px;
}
`;
const HTML_SPECIFIC_STYLES = `
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
`;
const HTML_TEMPLATE = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
${COMMON_STYLES}
${HTML_SPECIFIC_STYLES}
</style>
</head>
<body>
{{content}}
</body>
</html>
`;
export interface DocumentGeneratorParams {
metadata?: ToolMetadata<JSONSchemaType<DocumentParameter>>;
}
export class DocumentGenerator implements BaseTool<DocumentParameter> {
metadata: ToolMetadata<JSONSchemaType<DocumentParameter>>;
constructor(params: DocumentGeneratorParams) {
this.metadata = params.metadata ?? DEFAULT_METADATA;
}
private static async generateHtmlContent(
originalContent: string,
): Promise<string> {
return await marked(originalContent);
}
private static generateHtmlDocument(htmlContent: string): string {
return HTML_TEMPLATE.replace("{{content}}", htmlContent);
}
async call(input: DocumentParameter): Promise<string> {
const { originalContent, fileName } = input;
const htmlContent =
await DocumentGenerator.generateHtmlContent(originalContent);
const fileContent = DocumentGenerator.generateHtmlDocument(htmlContent);
const filePath = path.join(OUTPUT_DIR, `${fileName}.html`);
return `URL: ${await saveDocument(filePath, fileContent)}`;
}
}
export function getTools(): BaseTool[] {
return [new DocumentGenerator({})];
}
@@ -1,78 +0,0 @@
import { JSONSchemaType } from "ajv";
import { search } from "duck-duck-scrape";
import { BaseTool, ToolMetadata } from "llamaindex";
export type DuckDuckGoParameter = {
query: string;
region?: string;
maxResults?: number;
};
export type DuckDuckGoToolParams = {
metadata?: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
};
const DEFAULT_SEARCH_METADATA: ToolMetadata<
JSONSchemaType<DuckDuckGoParameter>
> = {
name: "duckduckgo_search",
description:
"Use this function to search for information (only text) in the internet using DuckDuckGo.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The query to search in DuckDuckGo.",
},
region: {
type: "string",
description:
"Optional, The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...",
nullable: true,
},
maxResults: {
type: "number",
description:
"Optional, The maximum number of results to be returned. Default is 10.",
nullable: true,
},
},
required: ["query"],
},
};
type DuckDuckGoSearchResult = {
title: string;
description: string;
url: string;
};
export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
metadata: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
constructor(params: DuckDuckGoToolParams) {
this.metadata = params.metadata ?? DEFAULT_SEARCH_METADATA;
}
async call(input: DuckDuckGoParameter) {
const { query, region, maxResults = 10 } = input;
const options = region ? { region } : {};
// Temporarily sleep to reduce overloading the DuckDuckGo
await new Promise((resolve) => setTimeout(resolve, 1000));
const searchResults = await search(query, options);
return searchResults.results.slice(0, maxResults).map((result) => {
return {
title: result.title,
description: result.description,
url: result.url,
} as DuckDuckGoSearchResult;
});
}
}
export function getTools() {
return [new DuckDuckGoSearchTool({})];
}
@@ -1,296 +0,0 @@
import { JSONSchemaType } from "ajv";
import fs from "fs";
import { BaseTool, Settings, ToolMetadata } from "llamaindex";
import Papa from "papaparse";
import path from "path";
import { saveDocument } from "../../llamaindex/documents/helper";
type ExtractMissingCellsParameter = {
filePath: string;
};
export type MissingCell = {
rowIndex: number;
columnIndex: number;
question: string;
};
const CSV_EXTRACTION_PROMPT = `You are a data analyst. You are given a table with missing cells.
Your task is to identify the missing cells and the questions needed to fill them.
IMPORTANT: Column indices should be 0-based
# Instructions:
- Understand the entire content of the table and the topics of the table.
- Identify the missing cells and the meaning of the data in the cells.
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
- Finally, only return the answer in JSON format with the following schema:
{
"missing_cells": [
{
"rowIndex": number,
"columnIndex": number,
"question": string
}
]
}
- If there are no missing cells, return an empty array.
- The answer is only the JSON object, nothing else and don't wrap it inside markdown code block.
# Example:
# | | Name | Age | City |
# |----|------|-----|------|
# | 0 | John | | Paris|
# | 1 | Mary | | |
# | 2 | | 30 | |
#
# Your thoughts:
# - The table is about people's names, ages, and cities.
# - Row: 1, Column: 2 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
# - Row: 1, Column: 3 (City column), Question: "In which city does Mary live? Please provide only the city name."
# Your answer:
# {
# "missing_cells": [
# {
# "rowIndex": 1,
# "columnIndex": 2,
# "question": "How old is Mary? Please provide only the numerical answer."
# },
# {
# "rowIndex": 1,
# "columnIndex": 3,
# "question": "In which city does Mary live? Please provide only the city name."
# }
# ]
# }
# Here is your task:
- Table content:
{table_content}
- Your answer:
`;
const DEFAULT_METADATA: ToolMetadata<
JSONSchemaType<ExtractMissingCellsParameter>
> = {
name: "extract_missing_cells",
description: `Use this tool to extract missing cells in a CSV file and generate questions to fill them. This tool only works with local file path.`,
parameters: {
type: "object",
properties: {
filePath: {
type: "string",
description: "The local file path to the CSV file.",
},
},
required: ["filePath"],
},
};
export interface ExtractMissingCellsParams {
metadata?: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
}
export class ExtractMissingCellsTool
implements BaseTool<ExtractMissingCellsParameter>
{
metadata: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
defaultExtractionPrompt: string;
constructor(params: ExtractMissingCellsParams) {
this.metadata = params.metadata ?? DEFAULT_METADATA;
this.defaultExtractionPrompt = CSV_EXTRACTION_PROMPT;
}
private readCsvFile(filePath: string): Promise<string[][]> {
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
reject(err);
return;
}
const parsedData = Papa.parse<string[]>(data, {
skipEmptyLines: false,
});
if (parsedData.errors.length) {
reject(parsedData.errors);
return;
}
// Ensure all rows have the same number of columns as the header
const maxColumns = parsedData.data[0].length;
const paddedRows = parsedData.data.map((row) => {
return [...row, ...Array(maxColumns - row.length).fill("")];
});
resolve(paddedRows);
});
});
}
private formatToMarkdownTable(data: string[][]): string {
if (data.length === 0) return "";
const maxColumns = data[0].length;
const headerRow = `| ${data[0].join(" | ")} |`;
const separatorRow = `| ${Array(maxColumns).fill("---").join(" | ")} |`;
const dataRows = data.slice(1).map((row) => {
return `| ${row.join(" | ")} |`;
});
return [headerRow, separatorRow, ...dataRows].join("\n");
}
async call(input: ExtractMissingCellsParameter): Promise<MissingCell[]> {
const { filePath } = input;
let tableContent: string[][];
try {
tableContent = await this.readCsvFile(filePath);
} catch (error) {
throw new Error(
`Failed to read CSV file. Make sure that you are reading a local file path (not a sandbox path).`,
);
}
const prompt = this.defaultExtractionPrompt.replace(
"{table_content}",
this.formatToMarkdownTable(tableContent),
);
const llm = Settings.llm;
const response = await llm.complete({
prompt,
});
const rawAnswer = response.text;
const parsedResponse = JSON.parse(rawAnswer) as {
missing_cells: MissingCell[];
};
if (!parsedResponse.missing_cells) {
throw new Error(
"The answer is not in the correct format. There should be a missing_cells array.",
);
}
const answer = parsedResponse.missing_cells;
return answer;
}
}
type FillMissingCellsParameter = {
filePath: string;
cells: {
rowIndex: number;
columnIndex: number;
answer: string;
}[];
};
const FILL_CELLS_METADATA: ToolMetadata<
JSONSchemaType<FillMissingCellsParameter>
> = {
name: "fill_missing_cells",
description: `Use this tool to fill missing cells in a CSV file with provided answers. This tool only works with local file path.`,
parameters: {
type: "object",
properties: {
filePath: {
type: "string",
description: "The local file path to the CSV file.",
},
cells: {
type: "array",
items: {
type: "object",
properties: {
rowIndex: { type: "number" },
columnIndex: { type: "number" },
answer: { type: "string" },
},
required: ["rowIndex", "columnIndex", "answer"],
},
description: "Array of cells to fill with their answers",
},
},
required: ["filePath", "cells"],
},
};
export interface FillMissingCellsParams {
metadata?: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
}
export class FillMissingCellsTool
implements BaseTool<FillMissingCellsParameter>
{
metadata: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
constructor(params: FillMissingCellsParams = {}) {
this.metadata = params.metadata ?? FILL_CELLS_METADATA;
}
async call(input: FillMissingCellsParameter): Promise<string> {
const { filePath, cells } = input;
// Read the CSV file
const fileContent = await new Promise<string>((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
// Parse CSV with PapaParse
const parseResult = Papa.parse<string[]>(fileContent, {
header: false, // Ensure the header is not treated as a separate object
skipEmptyLines: false, // Ensure empty lines are not skipped
});
if (parseResult.errors.length) {
throw new Error(
"Failed to parse CSV file: " + parseResult.errors[0].message,
);
}
const rows = parseResult.data;
// Fill the cells with answers
for (const cell of cells) {
// Adjust rowIndex to start from 1 for data rows
const adjustedRowIndex = cell.rowIndex + 1;
if (
adjustedRowIndex < rows.length &&
cell.columnIndex < rows[adjustedRowIndex].length
) {
rows[adjustedRowIndex][cell.columnIndex] = cell.answer;
}
}
// Convert back to CSV format
const updatedContent = Papa.unparse(rows, {
delimiter: parseResult.meta.delimiter,
});
// Use the helper function to write the file
const parsedPath = path.parse(filePath);
const newFileName = `${parsedPath.name}-filled${parsedPath.ext}`;
const newFilePath = path.join("output/tools", newFileName);
const newFileUrl = await saveDocument(newFilePath, updatedContent);
return (
"Successfully filled missing cells in the CSV file. File URL to show to the user: " +
newFileUrl
);
}
}
@@ -1,112 +0,0 @@
import type { JSONSchemaType } from "ajv";
import { FormData } from "formdata-node";
import fs from "fs";
import got from "got";
import { BaseTool, ToolMetadata } from "llamaindex";
import path from "node:path";
import { Readable } from "stream";
export type ImgGeneratorParameter = {
prompt: string;
};
export type ImgGeneratorToolParams = {
metadata?: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>>;
};
export type ImgGeneratorToolOutput = {
isSuccess: boolean;
imageUrl?: string;
errorMessage?: string;
};
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>> = {
name: "image_generator",
description: `Use this function to generate an image based on the prompt.`,
parameters: {
type: "object",
properties: {
prompt: {
type: "string",
description: "The prompt to generate the image",
},
},
required: ["prompt"],
},
};
export class ImgGeneratorTool implements BaseTool<ImgGeneratorParameter> {
readonly IMG_OUTPUT_FORMAT = "webp";
readonly IMG_OUTPUT_DIR = "output/tools";
readonly IMG_GEN_API =
"https://api.stability.ai/v2beta/stable-image/generate/core";
metadata: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>>;
constructor(params?: ImgGeneratorToolParams) {
this.checkRequiredEnvVars();
this.metadata = params?.metadata || DEFAULT_META_DATA;
}
async call(input: ImgGeneratorParameter): Promise<ImgGeneratorToolOutput> {
return await this.generateImage(input.prompt);
}
private generateImage = async (
prompt: string,
): Promise<ImgGeneratorToolOutput> => {
try {
const buffer = await this.promptToImgBuffer(prompt);
const imageUrl = this.saveImage(buffer);
return { isSuccess: true, imageUrl };
} catch (error) {
console.error(error);
return {
isSuccess: false,
errorMessage: "Failed to generate image. Please try again.",
};
}
};
private promptToImgBuffer = async (prompt: string) => {
const form = new FormData();
form.append("prompt", prompt);
form.append("output_format", this.IMG_OUTPUT_FORMAT);
const buffer = await got
.post(this.IMG_GEN_API, {
// Not sure why it shows an type error when passing form to body
// Although I follow document: https://github.com/sindresorhus/got/blob/main/documentation/2-options.md#body
// Tt still works fine, so I make casting to unknown to avoid the typescript warning
// Found a similar issue: https://github.com/sindresorhus/got/discussions/1877
body: form as unknown as Buffer | Readable | string,
headers: {
Authorization: `Bearer ${process.env.STABILITY_API_KEY}`,
Accept: "image/*",
},
})
.buffer();
return buffer;
};
private saveImage = (buffer: Buffer) => {
const filename = `${crypto.randomUUID()}.${this.IMG_OUTPUT_FORMAT}`;
const outputPath = path.join(this.IMG_OUTPUT_DIR, filename);
fs.writeFileSync(outputPath, buffer);
const url = `${process.env.FILESERVER_URL_PREFIX}/${this.IMG_OUTPUT_DIR}/${filename}`;
console.log(`Saved image to ${outputPath}.\nURL: ${url}`);
return url;
};
private checkRequiredEnvVars = () => {
if (!process.env.STABILITY_API_KEY) {
throw new Error(
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
);
}
if (!process.env.FILESERVER_URL_PREFIX) {
throw new Error(
"FILESERVER_URL_PREFIX is required to display file output after generation",
);
}
};
}
@@ -1,103 +0,0 @@
import { BaseToolWithCall } from "llamaindex";
import fs from "node:fs/promises";
import path from "node:path";
import { CodeGeneratorTool, CodeGeneratorToolParams } from "./code-generator";
import {
DocumentGenerator,
DocumentGeneratorParams,
} from "./document-generator";
import { DuckDuckGoSearchTool, DuckDuckGoToolParams } from "./duckduckgo";
import {
ExtractMissingCellsParams,
ExtractMissingCellsTool,
FillMissingCellsParams,
FillMissingCellsTool,
} from "./form-filling";
import { ImgGeneratorTool, ImgGeneratorToolParams } from "./img-gen";
import { InterpreterTool, InterpreterToolParams } from "./interpreter";
import { OpenAPIActionTool } from "./openapi-action";
import { WeatherTool, WeatherToolParams } from "./weather";
import { WikipediaTool, WikipediaToolParams } from "./wikipedia";
type ToolCreator = (config: unknown) => Promise<BaseToolWithCall[]>;
export async function createTools(toolConfig: {
local: Record<string, unknown>;
llamahub: any;
}): Promise<BaseToolWithCall[]> {
// add local tools from the 'tools' folder (if configured)
const tools = await createLocalTools(toolConfig.local);
return tools;
}
const toolFactory: Record<string, ToolCreator> = {
"wikipedia.WikipediaToolSpec": async (config: unknown) => {
return [new WikipediaTool(config as WikipediaToolParams)];
},
weather: async (config: unknown) => {
return [new WeatherTool(config as WeatherToolParams)];
},
interpreter: async (config: unknown) => {
return [new InterpreterTool(config as InterpreterToolParams)];
},
"openapi_action.OpenAPIActionToolSpec": async (config: unknown) => {
const { openapi_uri, domain_headers } = config as {
openapi_uri: string;
domain_headers: Record<string, Record<string, string>>;
};
const openAPIActionTool = new OpenAPIActionTool(
openapi_uri,
domain_headers,
);
return await openAPIActionTool.toToolFunctions();
},
duckduckgo: async (config: unknown) => {
return [new DuckDuckGoSearchTool(config as DuckDuckGoToolParams)];
},
img_gen: async (config: unknown) => {
return [new ImgGeneratorTool(config as ImgGeneratorToolParams)];
},
artifact: async (config: unknown) => {
return [new CodeGeneratorTool(config as CodeGeneratorToolParams)];
},
document_generator: async (config: unknown) => {
return [new DocumentGenerator(config as DocumentGeneratorParams)];
},
form_filling: async (config: unknown) => {
return [
new ExtractMissingCellsTool(config as ExtractMissingCellsParams),
new FillMissingCellsTool(config as FillMissingCellsParams),
];
},
};
async function createLocalTools(
localConfig: Record<string, unknown>,
): Promise<BaseToolWithCall[]> {
const tools: BaseToolWithCall[] = [];
for (const [key, toolConfig] of Object.entries(localConfig)) {
if (key in toolFactory) {
const newTools = await toolFactory[key](toolConfig);
tools.push(...newTools);
}
}
return tools;
}
export async function getConfiguredTools(
configPath?: string,
): Promise<BaseToolWithCall[]> {
const configFile = path.join(configPath ?? "config", "tools.json");
const toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
const tools = await createTools(toolConfig);
return tools;
}
export async function getTool(
toolName: string,
): Promise<BaseToolWithCall | undefined> {
const tools = await getConfiguredTools();
return tools.find((tool) => tool.metadata.name === toolName);
}
@@ -1,249 +0,0 @@
import { Logs, Result, Sandbox } from "@e2b/code-interpreter";
import type { JSONSchemaType } from "ajv";
import fs from "fs";
import { BaseTool, ToolMetadata } from "llamaindex";
import crypto from "node:crypto";
import path from "node:path";
export type InterpreterParameter = {
code: string;
sandboxFiles?: string[];
retryCount?: number;
};
export type InterpreterToolParams = {
metadata?: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
apiKey?: string;
fileServerURLPrefix?: string;
};
export type InterpreterToolOutput = {
isError: boolean;
logs: Logs;
text?: string;
extraResult: InterpreterExtraResult[];
retryCount?: number;
};
type InterpreterExtraType =
| "html"
| "markdown"
| "svg"
| "png"
| "jpeg"
| "pdf"
| "latex"
| "json"
| "javascript";
export type InterpreterExtraResult = {
type: InterpreterExtraType;
content?: string;
filename?: string;
url?: string;
};
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
name: "interpreter",
description: `Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
You have a maximum of 3 retries to get the code to run successfully.
`,
parameters: {
type: "object",
properties: {
code: {
type: "string",
description: "The python code to execute in a single cell.",
},
sandboxFiles: {
type: "array",
description:
"List of local file paths to be used by the code. The tool will throw an error if a file is not found.",
items: {
type: "string",
},
nullable: true,
},
retryCount: {
type: "number",
description: "The number of times the tool has been retried",
default: 0,
nullable: true,
},
},
required: ["code"],
},
};
export class InterpreterTool implements BaseTool<InterpreterParameter> {
private readonly outputDir = "output/tools";
private readonly uploadedFilesDir = "output/uploaded";
private apiKey?: string;
private fileServerURLPrefix?: string;
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
codeInterpreter?: Sandbox;
constructor(params?: InterpreterToolParams) {
this.metadata = params?.metadata || DEFAULT_META_DATA;
this.apiKey = params?.apiKey || process.env.E2B_API_KEY;
this.fileServerURLPrefix =
params?.fileServerURLPrefix || process.env.FILESERVER_URL_PREFIX;
if (!this.apiKey) {
throw new Error(
"E2B_API_KEY key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key",
);
}
if (!this.fileServerURLPrefix) {
throw new Error(
"FILESERVER_URL_PREFIX is required to display file output from sandbox",
);
}
}
public async initInterpreter(input: InterpreterParameter) {
if (!this.codeInterpreter) {
this.codeInterpreter = await Sandbox.create({
apiKey: this.apiKey,
});
// upload files to sandbox when it's initialized
if (input.sandboxFiles) {
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
try {
for (const filePath of input.sandboxFiles) {
const fileName = path.basename(filePath);
const localFilePath = path.join(this.uploadedFilesDir, fileName);
const content = fs.readFileSync(localFilePath);
const arrayBuffer = new Uint8Array(content).buffer;
await this.codeInterpreter?.files.write(filePath, arrayBuffer);
}
} catch (error) {
console.error("Got error when uploading files to sandbox", error);
}
}
}
return this.codeInterpreter;
}
public async codeInterpret(
input: InterpreterParameter,
): Promise<InterpreterToolOutput> {
console.log(
`Sandbox files: ${input.sandboxFiles}. Retry count: ${input.retryCount}`,
);
if (input.retryCount && input.retryCount >= 3) {
return {
isError: true,
logs: {
stdout: [],
stderr: [],
},
text: "Max retries reached",
extraResult: [],
};
}
console.log(
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${input.code}\n${"=".repeat(50)}`,
);
const interpreter = await this.initInterpreter(input);
const exec = await interpreter.runCode(input.code);
if (exec.error) console.error("[Code Interpreter error]", exec.error);
const extraResult = await this.getExtraResult(exec.results[0]);
const result: InterpreterToolOutput = {
isError: !!exec.error,
logs: exec.logs,
text: exec.text,
extraResult,
retryCount: input.retryCount ? input.retryCount + 1 : 1,
};
return result;
}
async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
const result = await this.codeInterpret(input);
return result;
}
async close() {
await this.codeInterpreter?.kill();
}
private async getExtraResult(
res?: Result,
): Promise<InterpreterExtraResult[]> {
if (!res) return [];
const output: InterpreterExtraResult[] = [];
try {
const formats = res.formats(); // formats available for the result. Eg: ['png', ...]
const results = formats.map((f) => res[f as keyof Result]); // get base64 data for each format
// save base64 data to file and return the url
for (let i = 0; i < formats.length; i++) {
const ext = formats[i];
const data = results[i];
switch (ext) {
case "png":
case "jpeg":
case "svg":
case "pdf": {
const { filename } = this.saveToDisk(data, ext);
output.push({
type: ext as InterpreterExtraType,
filename,
url: this.getFileUrl(filename),
});
break;
}
default:
output.push({
type: ext as InterpreterExtraType,
content: data,
});
break;
}
}
} catch (error) {
console.error("Error when parsing e2b response", error);
}
return output;
}
// Consider saving to cloud storage instead but it may cost more for you
// See: https://e2b.dev/docs/sandbox/api/filesystem#write-to-file
private saveToDisk(
base64Data: string,
ext: string,
): {
outputPath: string;
filename: string;
} {
const filename = `${crypto.randomUUID()}.${ext}`; // generate a unique filename
const buffer = Buffer.from(base64Data, "base64");
const outputPath = this.getOutputPath(filename);
fs.writeFileSync(outputPath, buffer);
console.log(`Saved file to ${outputPath}`);
return {
outputPath,
filename,
};
}
private getOutputPath(filename: string): string {
// if outputDir doesn't exist, create it
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}
return path.join(this.outputDir, filename);
}
private getFileUrl(filename: string): string {
return `${this.fileServerURLPrefix}/${this.outputDir}/${filename}`;
}
}
@@ -1,164 +0,0 @@
import SwaggerParser from "@apidevtools/swagger-parser";
import { JSONSchemaType } from "ajv";
import got from "got";
import { FunctionTool, JSONValue, ToolMetadata } from "llamaindex";
interface DomainHeaders {
[key: string]: { [header: string]: string };
}
type Input = {
url: string;
params: object;
};
type APIInfo = {
description: string;
title: string;
};
export class OpenAPIActionTool {
// cache the loaded specs by URL
private static specs: Record<string, any> = {};
private readonly INVALID_URL_PROMPT =
"This url did not include a hostname or scheme. Please determine the complete URL and try again.";
private createLoadSpecMetaData = (info: APIInfo) => {
return {
name: "load_openapi_spec",
description: `Use this to retrieve the OpenAPI spec for the API named ${info.title} with the following description: ${info.description}. Call it before making any requests to the API.`,
};
};
private readonly createMethodCallMetaData = (
method: "POST" | "PATCH" | "GET",
info: APIInfo,
) => {
return {
name: `${method.toLowerCase()}_request`,
description: `Use this to call the ${method} method on the API named ${info.title}`,
parameters: {
type: "object",
properties: {
url: {
type: "string",
description: `The url to make the ${method} request against`,
},
params: {
type: "object",
description:
method === "GET"
? "the URL parameters to provide with the get request"
: `the key-value pairs to provide with the ${method} request`,
},
},
required: ["url"],
},
} as ToolMetadata<JSONSchemaType<Input>>;
};
constructor(
public openapi_uri: string,
public domainHeaders: DomainHeaders = {},
) {}
async loadOpenapiSpec(url: string): Promise<any> {
const api = await SwaggerParser.validate(url);
return {
servers: "servers" in api ? api.servers : "",
info: { description: api.info.description, title: api.info.title },
endpoints: api.paths,
};
}
async getRequest(input: Input): Promise<JSONValue> {
if (!this.validUrl(input.url)) {
return this.INVALID_URL_PROMPT;
}
try {
const data = await got
.get(input.url, {
headers: this.getHeadersForUrl(input.url),
searchParams: input.params as URLSearchParams,
})
.json();
return data as JSONValue;
} catch (error) {
return error as JSONValue;
}
}
async postRequest(input: Input): Promise<JSONValue> {
if (!this.validUrl(input.url)) {
return this.INVALID_URL_PROMPT;
}
try {
const res = await got.post(input.url, {
headers: this.getHeadersForUrl(input.url),
json: input.params,
});
return res.body as JSONValue;
} catch (error) {
return error as JSONValue;
}
}
async patchRequest(input: Input): Promise<JSONValue> {
if (!this.validUrl(input.url)) {
return this.INVALID_URL_PROMPT;
}
try {
const res = await got.patch(input.url, {
headers: this.getHeadersForUrl(input.url),
json: input.params,
});
return res.body as JSONValue;
} catch (error) {
return error as JSONValue;
}
}
public async toToolFunctions() {
if (!OpenAPIActionTool.specs[this.openapi_uri]) {
console.log(`Loading spec for URL: ${this.openapi_uri}`);
const spec = await this.loadOpenapiSpec(this.openapi_uri);
OpenAPIActionTool.specs[this.openapi_uri] = spec;
}
const spec = OpenAPIActionTool.specs[this.openapi_uri];
// TODO: read endpoints with parameters from spec and create one tool for each endpoint
// For now, we just create a tool for each HTTP method which does not work well for passing parameters
return [
FunctionTool.from(() => {
return spec;
}, this.createLoadSpecMetaData(spec.info)),
FunctionTool.from(
this.getRequest.bind(this),
this.createMethodCallMetaData("GET", spec.info),
),
FunctionTool.from(
this.postRequest.bind(this),
this.createMethodCallMetaData("POST", spec.info),
),
FunctionTool.from(
this.patchRequest.bind(this),
this.createMethodCallMetaData("PATCH", spec.info),
),
];
}
private validUrl(url: string): boolean {
const parsed = new URL(url);
return !!parsed.protocol && !!parsed.hostname;
}
private getDomain(url: string): string {
const parsed = new URL(url);
return parsed.hostname;
}
private getHeadersForUrl(url: string): { [header: string]: string } {
const domain = this.getDomain(url);
return this.domainHeaders[domain] || {};
}
}
@@ -1,57 +0,0 @@
import {
BaseQueryEngine,
CloudRetrieveParams,
LlamaCloudIndex,
MetadataFilters,
QueryEngineTool,
VectorStoreIndex,
} from "llamaindex";
import { generateFilters } from "../queryFilter";
interface QueryEngineParams {
documentIds?: string[];
topK?: number;
}
export function createQueryEngineTool(
index: VectorStoreIndex | LlamaCloudIndex,
params?: QueryEngineParams,
name?: string,
description?: string,
): QueryEngineTool {
return new QueryEngineTool({
queryEngine: createQueryEngine(index, params),
metadata: {
name: name || "query_engine",
description:
description ||
`Use this tool to retrieve information about the text corpus from an index.`,
},
});
}
function createQueryEngine(
index: VectorStoreIndex | LlamaCloudIndex,
params?: QueryEngineParams,
): BaseQueryEngine {
const baseQueryParams = {
similarityTopK:
params?.topK ??
(process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined),
};
if (index instanceof LlamaCloudIndex) {
return index.asQueryEngine({
...baseQueryParams,
retrieval_mode: "auto_routed",
preFilters: generateFilters(
params?.documentIds || [],
) as CloudRetrieveParams["filters"],
});
}
return index.asQueryEngine({
...baseQueryParams,
preFilters: generateFilters(params?.documentIds || []) as MetadataFilters,
});
}
@@ -1,81 +0,0 @@
import type { JSONSchemaType } from "ajv";
import { BaseTool, ToolMetadata } from "llamaindex";
interface GeoLocation {
id: string;
name: string;
latitude: number;
longitude: number;
}
export type WeatherParameter = {
location: string;
};
export type WeatherToolParams = {
metadata?: ToolMetadata<JSONSchemaType<WeatherParameter>>;
};
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WeatherParameter>> = {
name: "get_weather_information",
description: `
Use this function to get the weather of any given location.
Note that the weather code should follow WMO Weather interpretation codes (WW):
0: Clear sky
1, 2, 3: Mainly clear, partly cloudy, and overcast
45, 48: Fog and depositing rime fog
51, 53, 55: Drizzle: Light, moderate, and dense intensity
56, 57: Freezing Drizzle: Light and dense intensity
61, 63, 65: Rain: Slight, moderate and heavy intensity
66, 67: Freezing Rain: Light and heavy intensity
71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
77: Snow grains
80, 81, 82: Rain showers: Slight, moderate, and violent
85, 86: Snow showers slight and heavy
95: Thunderstorm: Slight or moderate
96, 99: Thunderstorm with slight and heavy hail
`,
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The location to get the weather information",
},
},
required: ["location"],
},
};
export class WeatherTool implements BaseTool<WeatherParameter> {
metadata: ToolMetadata<JSONSchemaType<WeatherParameter>>;
private getGeoLocation = async (location: string): Promise<GeoLocation> => {
const apiUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${location}&count=10&language=en&format=json`;
const response = await fetch(apiUrl);
const data = await response.json();
const { id, name, latitude, longitude } = data.results[0];
return { id, name, latitude, longitude };
};
private getWeatherByLocation = async (location: string) => {
console.log(
"Calling open-meteo api to get weather information of location:",
location,
);
const { latitude, longitude } = await this.getGeoLocation(location);
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weather_code&hourly=temperature_2m,weather_code&daily=weather_code&timezone=${timezone}`;
const response = await fetch(apiUrl);
const data = await response.json();
return data;
};
constructor(params?: WeatherToolParams) {
this.metadata = params?.metadata || DEFAULT_META_DATA;
}
async call(input: WeatherParameter) {
return await this.getWeatherByLocation(input.location);
}
}
@@ -1,60 +0,0 @@
import type { JSONSchemaType } from "ajv";
import type { BaseTool, ToolMetadata } from "llamaindex";
import { default as wiki } from "wikipedia";
type WikipediaParameter = {
query: string;
lang?: string;
};
export type WikipediaToolParams = {
metadata?: ToolMetadata<JSONSchemaType<WikipediaParameter>>;
};
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WikipediaParameter>> = {
name: "wikipedia_tool",
description: "A tool that uses a query engine to search Wikipedia.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The query to search for",
},
lang: {
type: "string",
description: "The language to search in",
nullable: true,
},
},
required: ["query"],
},
};
export class WikipediaTool implements BaseTool<WikipediaParameter> {
private readonly DEFAULT_LANG = "en";
metadata: ToolMetadata<JSONSchemaType<WikipediaParameter>>;
constructor(params?: WikipediaToolParams) {
this.metadata = params?.metadata || DEFAULT_META_DATA;
}
async loadData(
page: string,
lang: string = this.DEFAULT_LANG,
): Promise<string> {
wiki.setLang(lang);
const pageResult = await wiki.page(page, { autoSuggest: false });
const content = await pageResult.content();
return content;
}
async call({
query,
lang = this.DEFAULT_LANG,
}: WikipediaParameter): Promise<string> {
const searchResult = await wiki.search(query);
if (searchResult.results.length === 0) return "No search results.";
return await this.loadData(searchResult.results[0].title, lang);
}
}
@@ -1,32 +0,0 @@
import { ContextChatEngine, Settings } from "llamaindex";
import { getDataSource } from "./index";
import { nodeCitationProcessor } from "./nodePostprocessors";
import { generateFilters } from "./queryFilter";
export async function createChatEngine(documentIds?: string[], params?: any) {
const index = await getDataSource(params);
if (!index) {
throw new Error(
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
);
}
const retriever = index.asRetriever({
similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined,
filters: generateFilters(documentIds || []),
});
const systemPrompt = process.env.SYSTEM_PROMPT;
const citationPrompt = process.env.SYSTEM_CITATION_PROMPT;
const prompt =
[systemPrompt, citationPrompt].filter((p) => p).join("\n") || undefined;
const nodePostprocessors = citationPrompt
? [nodeCitationProcessor]
: undefined;
return new ContextChatEngine({
chatModel: Settings.llm,
retriever,
systemPrompt: prompt,
nodePostprocessors,
});
}
@@ -1,26 +0,0 @@
import {
BaseNodePostprocessor,
MessageContent,
NodeWithScore,
} from "llamaindex";
class NodeCitationProcessor implements BaseNodePostprocessor {
/**
* Append node_id into metadata for citation purpose.
* Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
*/
async postprocessNodes(
nodes: NodeWithScore[],
query?: MessageContent,
): Promise<NodeWithScore[]> {
for (const nodeScore of nodes) {
if (!nodeScore.node || !nodeScore.node.metadata) {
continue; // Skip nodes with missing properties
}
nodeScore.node.metadata["node_id"] = nodeScore.node.id_;
}
return nodes;
}
}
export const nodeCitationProcessor = new NodeCitationProcessor();
@@ -1,105 +0,0 @@
import { Document } from "llamaindex";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { getExtractors } from "../../engine/loader";
import { DocumentFile } from "../streaming/annotations";
const MIME_TYPE_TO_EXT: Record<string, string> = {
"application/pdf": "pdf",
"text/plain": "txt",
"text/csv": "csv",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"docx",
};
export const UPLOADED_FOLDER = "output/uploaded";
export async function storeAndParseFile(
name: string,
fileBuffer: Buffer,
mimeType: string,
): Promise<DocumentFile> {
const file = await storeFile(name, fileBuffer, mimeType);
const documents: Document[] = await parseFile(fileBuffer, name, mimeType);
// Update document IDs in the file metadata
file.refs = documents.map((document) => document.id_ as string);
return file;
}
export async function storeFile(
name: string,
fileBuffer: Buffer,
mimeType: string,
) {
const fileExt = MIME_TYPE_TO_EXT[mimeType];
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
const fileId = crypto.randomUUID();
const newFilename = `${sanitizeFileName(name)}_${fileId}.${fileExt}`;
const filepath = path.join(UPLOADED_FOLDER, newFilename);
const fileUrl = await saveDocument(filepath, fileBuffer);
return {
id: fileId,
name: newFilename,
size: fileBuffer.length,
type: fileExt,
url: fileUrl,
refs: [] as string[],
} as DocumentFile;
}
export async function parseFile(
fileBuffer: Buffer,
filename: string,
mimeType: string,
) {
const documents = await loadDocuments(fileBuffer, mimeType);
for (const document of documents) {
document.metadata = {
...document.metadata,
file_name: filename,
private: "true", // to separate private uploads from public documents
};
}
return documents;
}
async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
const extractors = getExtractors();
const reader = extractors[MIME_TYPE_TO_EXT[mimeType]];
if (!reader) {
throw new Error(`Unsupported document type: ${mimeType}`);
}
console.log(`Processing uploaded document of type: ${mimeType}`);
return await reader.loadDataAsContent(fileBuffer);
}
// Save document to file server and return the file url
export async function saveDocument(filepath: string, content: string | Buffer) {
if (path.isAbsolute(filepath)) {
throw new Error("Absolute file paths are not allowed.");
}
if (!process.env.FILESERVER_URL_PREFIX) {
throw new Error("FILESERVER_URL_PREFIX environment variable is not set.");
}
const dirPath = path.dirname(filepath);
await fs.promises.mkdir(dirPath, { recursive: true });
if (typeof content === "string") {
await fs.promises.writeFile(filepath, content, "utf-8");
} else {
await fs.promises.writeFile(filepath, content);
}
const fileurl = `${process.env.FILESERVER_URL_PREFIX}/${filepath}`;
console.log(`Saved document to ${filepath}. Reachable at URL: ${fileurl}`);
return fileurl;
}
function sanitizeFileName(fileName: string) {
// Remove file extension and sanitize
return fileName.split(".")[0].replace(/[^a-zA-Z0-9_-]/g, "_");
}
@@ -1,48 +0,0 @@
import {
Document,
IngestionPipeline,
Settings,
SimpleNodeParser,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
export async function runPipeline(
currentIndex: VectorStoreIndex | null,
documents: Document[],
) {
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
const pipeline = new IngestionPipeline({
transformations: [
new SimpleNodeParser({
chunkSize: Settings.chunkSize,
chunkOverlap: Settings.chunkOverlap,
}),
Settings.embedModel,
],
});
const nodes = await pipeline.run({ documents });
if (currentIndex) {
await currentIndex.insertNodes(nodes);
currentIndex.storageContext.docStore.persist();
console.log("Added nodes to the vector store.");
return documents.map((document) => document.id_);
} else {
// Initialize a new index with the documents
console.log(
"Got empty index, created new index with the uploaded documents",
);
const persistDir = process.env.STORAGE_CACHE_DIR;
if (!persistDir) {
throw new Error("STORAGE_CACHE_DIR environment variable is required!");
}
const storageContext = await storageContextFromDefaults({
persistDir,
});
const newIndex = await VectorStoreIndex.fromDocuments(documents, {
storageContext,
});
await newIndex.storageContext.docStore.persist();
return documents.map((document) => document.id_);
}
}
@@ -1,65 +0,0 @@
import {
Document,
LLamaCloudFileService,
LlamaCloudIndex,
VectorStoreIndex,
} from "llamaindex";
import { DocumentFile } from "../streaming/annotations";
import { parseFile, storeFile } from "./helper";
import { runPipeline } from "./pipeline";
export async function uploadDocument(
index: VectorStoreIndex | LlamaCloudIndex | null,
name: string,
raw: string,
): Promise<DocumentFile> {
const [header, content] = raw.split(",");
const mimeType = header.replace("data:", "").replace(";base64", "");
const fileBuffer = Buffer.from(content, "base64");
// Store file
const fileMetadata = await storeFile(name, fileBuffer, mimeType);
// Do not index csv files
if (mimeType === "text/csv") {
return fileMetadata;
}
let documentIds: string[] = [];
if (index instanceof LlamaCloudIndex) {
// trigger LlamaCloudIndex API to upload the file and run the pipeline
const projectId = await index.getProjectId();
const pipelineId = await index.getPipelineId();
try {
documentIds = [
await LLamaCloudFileService.addFileToPipeline(
projectId,
pipelineId,
new File([fileBuffer], name, { type: mimeType }),
{ private: "true" },
),
];
} catch (error) {
if (
error instanceof ReferenceError &&
error.message.includes("File is not defined")
) {
throw new Error(
"File class is not supported in the current Node.js version. Please use Node.js 20 or higher.",
);
}
throw error;
}
} else {
// run the pipeline for other vector store indexes
const documents: Document[] = await parseFile(
fileBuffer,
fileMetadata.name,
mimeType,
);
documentIds = await runPipeline(index, documents);
}
// Update file metadata with document IDs
fileMetadata.refs = documentIds;
return fileMetadata;
}
@@ -1,251 +0,0 @@
import { JSONValue, Message } from "ai";
import {
ChatMessage,
MessageContent,
MessageContentDetail,
MessageType,
} from "llamaindex";
import { UPLOADED_FOLDER } from "../documents/helper";
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
export type DocumentFile = {
id: string;
name: string;
size: number;
type: string;
url: string;
refs?: string[];
};
type Annotation = {
type: string;
data: object;
};
export function isValidMessages(messages: Message[]): boolean {
const lastMessage =
messages && messages.length > 0 ? messages[messages.length - 1] : null;
return lastMessage !== null && lastMessage.role === "user";
}
export function retrieveDocumentIds(messages: Message[]): string[] {
// retrieve document Ids from the annotations of all messages (if any)
const documentFiles = retrieveDocumentFiles(messages);
return documentFiles.map((file) => file.refs || []).flat();
}
export function retrieveDocumentFiles(messages: Message[]): DocumentFile[] {
const annotations = getAllAnnotations(messages);
if (annotations.length === 0) return [];
const files: DocumentFile[] = [];
for (const { type, data } of annotations) {
if (
type === "document_file" &&
"files" in data &&
Array.isArray(data.files)
) {
files.push(...data.files);
}
}
return files;
}
export function retrieveMessageContent(messages: Message[]): MessageContent {
const userMessage = messages[messages.length - 1];
return [
{
type: "text",
text: userMessage.content,
},
...retrieveLatestArtifact(messages),
...convertAnnotations(messages),
];
}
export function convertToChatHistory(messages: Message[]): ChatMessage[] {
if (!messages || !Array.isArray(messages)) {
return [];
}
const agentHistory = retrieveAgentHistoryMessage(messages);
if (agentHistory) {
const previousMessages = messages.slice(0, -1);
return [...previousMessages, agentHistory].map((msg) => ({
role: msg.role as MessageType,
content: msg.content,
}));
}
return messages.map((msg) => ({
role: msg.role as MessageType,
content: msg.content,
}));
}
function retrieveAgentHistoryMessage(
messages: Message[],
maxAgentMessages = 10,
): ChatMessage | null {
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
messages,
{ role: "assistant", type: "agent" },
).slice(-maxAgentMessages);
if (agentAnnotations.length > 0) {
const messageContent =
"Here is the previous conversation of agents:\n" +
agentAnnotations.map((annotation) => annotation.data.text).join("\n");
return {
role: "assistant",
content: messageContent,
};
}
return null;
}
function getFileContent(file: DocumentFile): string {
let defaultContent = `=====File: ${file.name}=====\n`;
// Include file URL if it's available
const urlPrefix = process.env.FILESERVER_URL_PREFIX;
let urlContent = "";
if (urlPrefix) {
if (file.url) {
urlContent = `File URL: ${file.url}\n`;
} else {
urlContent = `File URL (instruction: do not update this file URL yourself): ${urlPrefix}/output/uploaded/${file.name}\n`;
}
} else {
console.warn(
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server",
);
}
defaultContent += urlContent;
// Include document IDs if it's available
if (file.refs) {
defaultContent += `Document IDs: ${file.refs}\n`;
}
// Include sandbox file paths
const sandboxFilePath = `/tmp/${file.name}`;
defaultContent += `Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): ${sandboxFilePath}\n`;
// Include local file path
const localFilePath = `${UPLOADED_FOLDER}/${file.name}`;
defaultContent += `Local file path (instruction: use for local tool that requires a local path): ${localFilePath}\n`;
return defaultContent;
}
function getAllAnnotations(messages: Message[]): Annotation[] {
return messages.flatMap((message) =>
(message.annotations ?? []).map((annotation) =>
getValidAnnotation(annotation),
),
);
}
// get latest artifact from annotations to append to the user message
function retrieveLatestArtifact(messages: Message[]): MessageContentDetail[] {
const annotations = getAllAnnotations(messages);
if (annotations.length === 0) return [];
for (const { type, data } of annotations.reverse()) {
if (
type === "tools" &&
"toolCall" in data &&
"toolOutput" in data &&
typeof data.toolCall === "object" &&
typeof data.toolOutput === "object" &&
data.toolCall !== null &&
data.toolOutput !== null &&
"name" in data.toolCall &&
data.toolCall.name === "artifact"
) {
const toolOutput = data.toolOutput as { output?: { code?: string } };
if (toolOutput.output?.code) {
return [
{
type: "text",
text: `The existing code is:\n\`\`\`\n${toolOutput.output.code}\n\`\`\``,
},
];
}
}
}
return [];
}
function convertAnnotations(messages: Message[]): MessageContentDetail[] {
// get all annotations from user messages
const annotations: Annotation[] = messages
.filter((message) => message.role === "user" && message.annotations)
.flatMap((message) => message.annotations?.map(getValidAnnotation) || []);
if (annotations.length === 0) return [];
const content: MessageContentDetail[] = [];
annotations.forEach(({ type, data }) => {
// convert image
if (type === "image" && "url" in data && typeof data.url === "string") {
content.push({
type: "image_url",
image_url: {
url: data.url,
},
});
}
// convert the content of files to a text message
if (
type === "document_file" &&
"files" in data &&
Array.isArray(data.files)
) {
const fileContent = data.files.map(getFileContent).join("\n");
content.push({
type: "text",
text: fileContent,
});
}
});
return content;
}
function getValidAnnotation(annotation: JSONValue): Annotation {
if (
!(
annotation &&
typeof annotation === "object" &&
"type" in annotation &&
typeof annotation.type === "string" &&
"data" in annotation &&
annotation.data &&
typeof annotation.data === "object"
)
) {
throw new Error("Client sent invalid annotation. Missing data and type");
}
return { type: annotation.type, data: annotation.data };
}
// validate and get all annotations of a specific type or role from the frontend messages
export function getAnnotations<
T extends Annotation["data"] = Annotation["data"],
>(
messages: Message[],
options?: {
role?: Message["role"]; // message role
type?: Annotation["type"]; // annotation type
},
): {
type: string;
data: T;
}[] {
const messagesByRole = options?.role
? messages.filter((msg) => msg.role === options?.role)
: messages;
const annotations = getAllAnnotations(messagesByRole);
const annotationsByType = options?.type
? annotations.filter((a) => a.type === options.type)
: annotations;
return annotationsByType as { type: string; data: T }[];
}
@@ -1,182 +0,0 @@
import { StreamData } from "ai";
import {
CallbackManager,
LLamaCloudFileService,
Metadata,
MetadataMode,
NodeWithScore,
ToolCall,
ToolOutput,
} from "llamaindex";
import path from "node:path";
import { DATA_DIR } from "../../engine/loader";
import { downloadFile } from "./file";
const LLAMA_CLOUD_DOWNLOAD_FOLDER = "output/llamacloud";
export function appendSourceData(
data: StreamData,
sourceNodes?: NodeWithScore<Metadata>[],
) {
if (!sourceNodes?.length) return;
try {
const nodes = sourceNodes.map((node) => ({
metadata: node.node.metadata,
id: node.node.id_,
score: node.score ?? null,
url: getNodeUrl(node.node.metadata),
text: node.node.getContent(MetadataMode.NONE),
}));
data.appendMessageAnnotation({
type: "sources",
data: {
nodes,
},
});
} catch (error) {
console.error("Error appending source data:", error);
}
}
export function appendEventData(data: StreamData, title?: string) {
if (!title) return;
data.appendMessageAnnotation({
type: "events",
data: {
title,
},
});
}
export function appendToolData(
data: StreamData,
toolCall: ToolCall,
toolOutput: ToolOutput,
) {
data.appendMessageAnnotation({
type: "tools",
data: {
toolCall: {
id: toolCall.id,
name: toolCall.name,
input: toolCall.input,
},
toolOutput: {
output: toolOutput.output,
isError: toolOutput.isError,
},
},
});
}
export function createCallbackManager(stream: StreamData) {
const callbackManager = new CallbackManager();
callbackManager.on("retrieve-end", (data) => {
const { nodes, query } = data.detail;
appendSourceData(stream, nodes);
appendEventData(stream, `Retrieving context for query: '${query.query}'`);
appendEventData(
stream,
`Retrieved ${nodes.length} sources to use as context for the query`,
);
downloadFilesFromNodes(nodes); // don't await to avoid blocking chat streaming
});
callbackManager.on("llm-tool-call", (event) => {
const { name, input } = event.detail.toolCall;
const inputString = Object.entries(input)
.map(([key, value]) => `${key}: ${value}`)
.join(", ");
appendEventData(
stream,
`Using tool: '${name}' with inputs: '${inputString}'`,
);
});
callbackManager.on("llm-tool-result", (event) => {
const { toolCall, toolResult } = event.detail;
appendToolData(stream, toolCall, toolResult);
});
return callbackManager;
}
function getNodeUrl(metadata: Metadata) {
if (!process.env.FILESERVER_URL_PREFIX) {
console.warn(
"FILESERVER_URL_PREFIX is not set. File URLs will not be generated.",
);
}
const fileName = metadata["file_name"];
if (fileName && process.env.FILESERVER_URL_PREFIX) {
// file_name exists and file server is configured
const pipelineId = metadata["pipeline_id"];
if (pipelineId) {
const name = toDownloadedName(pipelineId, fileName);
return `${process.env.FILESERVER_URL_PREFIX}/${LLAMA_CLOUD_DOWNLOAD_FOLDER}/${name}`;
}
const isPrivate = metadata["private"] === "true";
if (isPrivate) {
return `${process.env.FILESERVER_URL_PREFIX}/output/uploaded/${fileName}`;
}
const filePath = metadata["file_path"];
const dataDir = path.resolve(DATA_DIR);
if (filePath && dataDir) {
const relativePath = path.relative(dataDir, filePath);
return `${process.env.FILESERVER_URL_PREFIX}/data/${relativePath}`;
}
}
// fallback to URL in metadata (e.g. for websites)
return metadata["URL"];
}
async function downloadFilesFromNodes(nodes: NodeWithScore<Metadata>[]) {
try {
const files = nodesToLlamaCloudFiles(nodes);
for (const { pipelineId, fileName, downloadedName } of files) {
const downloadUrl = await LLamaCloudFileService.getFileUrl(
pipelineId,
fileName,
);
if (downloadUrl) {
await downloadFile(
downloadUrl,
downloadedName,
LLAMA_CLOUD_DOWNLOAD_FOLDER,
);
}
}
} catch (error) {
console.error("Error downloading files from nodes:", error);
}
}
function nodesToLlamaCloudFiles(nodes: NodeWithScore<Metadata>[]) {
const files: Array<{
pipelineId: string;
fileName: string;
downloadedName: string;
}> = [];
for (const node of nodes) {
const pipelineId = node.node.metadata["pipeline_id"];
const fileName = node.node.metadata["file_name"];
if (!pipelineId || !fileName) continue;
const isDuplicate = files.some(
(f) => f.pipelineId === pipelineId && f.fileName === fileName,
);
if (!isDuplicate) {
files.push({
pipelineId,
fileName,
downloadedName: toDownloadedName(pipelineId, fileName),
});
}
}
return files;
}
function toDownloadedName(pipelineId: string, fileName: string) {
return `${pipelineId}$${fileName}`;
}
@@ -1,35 +0,0 @@
import fs from "node:fs";
import https from "node:https";
import path from "node:path";
export async function downloadFile(
urlToDownload: string,
filename: string,
folder = "output/uploaded",
) {
try {
const downloadedPath = path.join(folder, filename);
// Check if file already exists
if (fs.existsSync(downloadedPath)) return;
const file = fs.createWriteStream(downloadedPath);
https
.get(urlToDownload, (response) => {
response.pipe(file);
file.on("finish", () => {
file.close(() => {
console.log("File downloaded successfully");
});
});
})
.on("error", (err) => {
fs.unlink(downloadedPath, () => {
console.error("Error downloading file:", err);
throw err;
});
});
} catch (error) {
throw new Error(`Error downloading file: ${error}`);
}
}
@@ -1,43 +0,0 @@
import { ChatMessage, Settings } from "llamaindex";
export async function generateNextQuestions(conversation: ChatMessage[]) {
const llm = Settings.llm;
const NEXT_QUESTION_PROMPT = process.env.NEXT_QUESTION_PROMPT;
if (!NEXT_QUESTION_PROMPT) {
return [];
}
// Format conversation
const conversationText = conversation
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const message = NEXT_QUESTION_PROMPT.replace(
"{conversation}",
conversationText,
);
try {
const response = await llm.complete({ prompt: message });
const questions = extractQuestions(response.text);
return questions;
} catch (error) {
console.error("Error when generating the next questions: ", error);
return [];
}
}
// TODO: instead of parsing the LLM's result we can use structured predict, once LITS supports it
function extractQuestions(text: string): string[] {
// Extract the text inside the triple backticks
// @ts-ignore
const contentMatch = text.match(/```(.*?)```/s);
const content = contentMatch ? contentMatch[1] : "";
// Split the content by newlines to get each question
const questions = content
.split("\n")
.map((question) => question.trim())
.filter((question) => question !== "");
return questions;
}
@@ -1,32 +0,0 @@
import logging
from abc import ABC, abstractmethod
from typing import Any
logger = logging.getLogger("uvicorn")
class EventCallback(ABC):
"""
Base class for event callbacks during event streaming.
"""
async def run(self, event: Any) -> Any:
"""
Called for each event in the stream.
Default behavior: pass through the event unchanged.
"""
return event
async def on_complete(self, final_response: str) -> Any:
"""
Called when the stream is complete.
Default behavior: return None.
"""
return None
@abstractmethod
def from_default(self, *args, **kwargs) -> "EventCallback":
"""
Create a new instance of the processor from default values.
"""
pass
@@ -1,42 +0,0 @@
import logging
from typing import Any, List
from fastapi import BackgroundTasks
from llama_index.core.schema import NodeWithScore
from app.api.callbacks.base import EventCallback
logger = logging.getLogger("uvicorn")
class LlamaCloudFileDownload(EventCallback):
"""
Processor for handling LlamaCloud file downloads from source nodes.
Only work if LlamaCloud service code is available.
"""
def __init__(self, background_tasks: BackgroundTasks):
self.background_tasks = background_tasks
async def run(self, event: Any) -> Any:
if hasattr(event, "to_response"):
event_response = event.to_response()
if event_response.get("type") == "sources" and hasattr(event, "nodes"):
await self._process_response_nodes(event.nodes)
return event
async def _process_response_nodes(self, source_nodes: List[NodeWithScore]):
try:
from app.engine.service import LLamaCloudFileService # type: ignore
LLamaCloudFileService.download_files_from_nodes(
source_nodes, self.background_tasks
)
except ImportError:
pass
@classmethod
def from_default(
cls, background_tasks: BackgroundTasks
) -> "LlamaCloudFileDownload":
return cls(background_tasks=background_tasks)
@@ -1,34 +0,0 @@
import logging
from typing import Any
from app.api.callbacks.base import EventCallback
from app.api.routers.models import ChatData
from app.api.services.suggestion import NextQuestionSuggestion
logger = logging.getLogger("uvicorn")
class SuggestNextQuestions(EventCallback):
"""Processor for generating next question suggestions."""
def __init__(self, chat_data: ChatData):
self.chat_data = chat_data
self.accumulated_text = ""
async def on_complete(self, final_response: str) -> Any:
if final_response == "":
return None
questions = await NextQuestionSuggestion.suggest_next_questions(
self.chat_data.messages, final_response
)
if questions:
return {
"type": "suggested_questions",
"data": questions,
}
return None
@classmethod
def from_default(cls, chat_data: ChatData) -> "SuggestNextQuestions":
return cls(chat_data=chat_data)
@@ -1,66 +0,0 @@
import logging
from typing import List, Optional
from llama_index.core.workflow.handler import WorkflowHandler
from app.api.callbacks.base import EventCallback
logger = logging.getLogger("uvicorn")
class StreamHandler:
"""
Streams events from a workflow handler through a chain of callbacks.
"""
def __init__(
self,
workflow_handler: WorkflowHandler,
callbacks: Optional[List[EventCallback]] = None,
):
self.workflow_handler = workflow_handler
self.callbacks = callbacks or []
self.accumulated_text = ""
def vercel_stream(self):
"""Create a streaming response with Vercel format."""
from app.api.routers.vercel_response import VercelStreamResponse
return VercelStreamResponse(stream_handler=self)
async def cancel_run(self):
"""Cancel the workflow handler."""
await self.workflow_handler.cancel_run()
async def stream_events(self):
"""Stream events through the processor chain."""
try:
async for event in self.workflow_handler.stream_events():
# Process the event through each processor
for callback in self.callbacks:
event = await callback.run(event)
yield event
# After all events are processed, call on_complete for each callback
for callback in self.callbacks:
result = await callback.on_complete(self.accumulated_text)
if result:
yield result
except Exception as e:
# Make sure to cancel the workflow on error
await self.workflow_handler.cancel_run()
raise e
async def accumulate_text(self, text: str):
"""Accumulate text from the workflow handler."""
self.accumulated_text += text
@classmethod
def from_default(
cls,
handler: WorkflowHandler,
callbacks: Optional[List[EventCallback]] = None,
) -> "StreamHandler":
"""Create a new instance with the given workflow handler and callbacks."""
return cls(workflow_handler=handler, callbacks=callbacks)
@@ -1,55 +0,0 @@
import logging
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from app.api.callbacks.llamacloud import LlamaCloudFileDownload
from app.api.callbacks.next_question import SuggestNextQuestions
from app.api.callbacks.stream_handler import StreamHandler
from app.api.routers.models import (
ChatData,
)
from app.engine.query_filter import generate_filters
from app.workflows import create_workflow
chat_router = r = APIRouter()
logger = logging.getLogger("uvicorn")
@r.post("")
async def chat(
request: Request,
data: ChatData,
background_tasks: BackgroundTasks,
):
try:
last_message_content = data.get_last_message_content()
messages = data.get_history_messages(include_agent_messages=True)
doc_ids = data.get_chat_document_ids()
filters = generate_filters(doc_ids)
params = data.data or {}
workflow = create_workflow(
params=params,
filters=filters,
)
handler = workflow.run(
user_msg=last_message_content,
chat_history=messages,
stream=True,
)
return StreamHandler.from_default(
handler=handler,
callbacks=[
LlamaCloudFileDownload.from_default(background_tasks),
SuggestNextQuestions.from_default(data),
],
).vercel_stream()
except Exception as e:
logger.exception("Error in chat engine", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error in chat engine: {e}",
) from e
@@ -1,99 +0,0 @@
import asyncio
import json
import logging
from typing import AsyncGenerator
from fastapi.responses import StreamingResponse
from llama_index.core.agent.workflow.workflow_events import AgentStream
from llama_index.core.workflow import StopEvent
from app.api.callbacks.stream_handler import StreamHandler
logger = logging.getLogger("uvicorn")
class VercelStreamResponse(StreamingResponse):
"""
Converts preprocessed events into Vercel-compatible streaming response format.
"""
TEXT_PREFIX = "0:"
DATA_PREFIX = "8:"
ERROR_PREFIX = "3:"
def __init__(
self,
stream_handler: StreamHandler,
*args,
**kwargs,
):
self.handler = stream_handler
super().__init__(content=self.content_generator())
async def content_generator(self):
"""Generate Vercel-formatted content from preprocessed events."""
stream_started = False
try:
async for event in self.handler.stream_events():
if not stream_started:
# Start the stream with an empty message
stream_started = True
yield self.convert_text("")
# Handle different types of events
if isinstance(event, (AgentStream, StopEvent)):
async for chunk in self._stream_text(event):
await self.handler.accumulate_text(chunk)
yield self.convert_text(chunk)
elif isinstance(event, dict):
yield self.convert_data(event)
elif hasattr(event, "to_response"):
event_response = event.to_response()
yield self.convert_data(event_response)
else:
yield self.convert_data(event.model_dump())
except asyncio.CancelledError:
logger.warning("Client cancelled the request!")
await self.handler.cancel_run()
except Exception as e:
logger.error(f"Error in stream response: {e}")
yield self.convert_error(str(e))
await self.handler.cancel_run()
async def _stream_text(
self, event: AgentStream | StopEvent
) -> AsyncGenerator[str, None]:
"""
Accept stream text from either AgentStream or StopEvent with string or AsyncGenerator result
"""
if isinstance(event, AgentStream):
yield self.convert_text(event.delta)
elif isinstance(event, StopEvent):
if isinstance(event.result, str):
yield event.result
elif isinstance(event.result, AsyncGenerator):
async for chunk in event.result:
if isinstance(chunk, str):
yield chunk
elif hasattr(chunk, "delta"):
yield chunk.delta
@classmethod
def convert_text(cls, token: str) -> str:
"""Convert text event to Vercel format."""
# Escape newlines and double quotes to avoid breaking the stream
token = json.dumps(token)
return f"{cls.TEXT_PREFIX}{token}\n"
@classmethod
def convert_data(cls, data: dict) -> str:
"""Convert data event to Vercel format."""
data_str = json.dumps(data)
return f"{cls.DATA_PREFIX}[{data_str}]\n"
@classmethod
def convert_error(cls, error: str) -> str:
"""Convert error event to Vercel format."""
error_str = json.dumps(error)
return f"{cls.ERROR_PREFIX}{error_str}\n"
@@ -1,45 +0,0 @@
from enum import Enum
from typing import List, Optional
from llama_index.core.schema import NodeWithScore
from llama_index.core.workflow import Event
from app.api.routers.models import SourceNodes
class AgentRunEventType(Enum):
TEXT = "text"
PROGRESS = "progress"
class AgentRunEvent(Event):
name: str
msg: str
event_type: AgentRunEventType = AgentRunEventType.TEXT
data: Optional[dict] = None
def to_response(self) -> dict:
return {
"type": "agent",
"data": {
"agent": self.name,
"type": self.event_type.value,
"text": self.msg,
"data": self.data,
},
}
class SourceNodesEvent(Event):
nodes: List[NodeWithScore]
def to_response(self):
return {
"type": "sources",
"data": {
"nodes": [
SourceNodes.from_source_node(node).model_dump()
for node in self.nodes
]
},
}
@@ -1,121 +0,0 @@
from typing import Any, List, Optional
from app.workflows.events import AgentRunEvent
from app.workflows.tools import ToolCallResponse, call_tools, chat_with_tools
from llama_index.core.base.llms.types import ChatMessage
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.settings import Settings
from llama_index.core.tools.types import BaseTool
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
class InputEvent(Event):
input: list[ChatMessage]
class ToolCallEvent(Event):
input: ToolCallResponse
class FunctionCallingAgent(Workflow):
"""
A simple workflow to request LLM with tools independently.
You can share the previous chat history to provide the context for the LLM.
"""
def __init__(
self,
*args: Any,
llm: FunctionCallingLLM | None = None,
chat_history: Optional[List[ChatMessage]] = None,
tools: List[BaseTool] | None = None,
system_prompt: str | None = None,
verbose: bool = False,
timeout: float = 360.0,
name: str,
write_events: bool = True,
**kwargs: Any,
) -> None:
super().__init__(*args, verbose=verbose, timeout=timeout, **kwargs) # type: ignore
self.tools = tools or []
self.name = name
self.write_events = write_events
if llm is None:
llm = Settings.llm
self.llm = llm
if not self.llm.metadata.is_function_calling_model:
raise ValueError("The provided LLM must support function calling.")
self.system_prompt = system_prompt
self.memory = ChatMemoryBuffer.from_defaults(
llm=self.llm, chat_history=chat_history
)
self.sources = [] # type: ignore
@step()
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
# clear sources
self.sources = []
# set streaming
ctx.data["streaming"] = getattr(ev, "streaming", False)
# set system prompt
if self.system_prompt is not None:
system_msg = ChatMessage(role="system", content=self.system_prompt)
self.memory.put(system_msg)
# get user input
user_input = ev.input
user_msg = ChatMessage(role="user", content=user_input)
self.memory.put(user_msg)
if self.write_events:
ctx.write_event_to_stream(
AgentRunEvent(name=self.name, msg=f"Start to work on: {user_input}")
)
return InputEvent(input=self.memory.get())
@step()
async def handle_llm_input(
self,
ctx: Context,
ev: InputEvent,
) -> ToolCallEvent | StopEvent:
chat_history = ev.input
response = await chat_with_tools(
self.llm,
self.tools,
chat_history,
)
is_tool_call = isinstance(response, ToolCallResponse)
if not is_tool_call:
if ctx.data["streaming"]:
return StopEvent(result=response)
else:
full_response = ""
async for chunk in response.generator:
full_response += chunk.message.content
return StopEvent(result=full_response)
return ToolCallEvent(input=response)
@step()
async def handle_tool_calls(self, ctx: Context, ev: ToolCallEvent) -> InputEvent:
tool_calls = ev.input.tool_calls
tool_call_message = ev.input.tool_call_message
self.memory.put(tool_call_message)
tool_messages = await call_tools(self.name, self.tools, ctx, tool_calls)
self.memory.put_messages(tool_messages)
return InputEvent(input=self.memory.get())
@@ -1,51 +0,0 @@
import { LlamaIndexAdapter, Message } from "ai";
import { Request, Response } from "express";
import {
convertToChatHistory,
retrieveMessageContent,
} from "./llamaindex/streaming/annotations";
import { createWorkflow } from "./workflow/factory";
import { createStreamFromWorkflowContext } from "./workflow/stream";
export const chat = async (req: Request, res: Response) => {
try {
const { messages }: { messages: Message[] } = req.body;
if (!messages || messages.length === 0) {
return res.status(400).json({
error: "messages are required in the request body",
});
}
const chatHistory = convertToChatHistory(messages);
const userMessageContent = retrieveMessageContent(messages);
const workflow = await createWorkflow({ chatHistory });
const context = workflow.run({
message: userMessageContent,
streaming: true,
});
const { stream, dataStream } =
await createStreamFromWorkflowContext(context);
const streamResponse = LlamaIndexAdapter.toDataStreamResponse(stream, {
data: dataStream,
});
if (streamResponse.body) {
const reader = streamResponse.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
res.end();
return;
}
res.write(value);
}
}
} catch (error) {
console.error("[LlamaIndex]", error);
return res.status(500).json({
detail: (error as Error).message,
});
}
};
@@ -1,58 +0,0 @@
import { initObservability } from "@/app/observability";
import { LlamaIndexAdapter, type Message } from "ai";
import { NextRequest, NextResponse } from "next/server";
import { initSettings } from "./engine/settings";
import {
convertToChatHistory,
isValidMessages,
retrieveMessageContent,
} from "./llamaindex/streaming/annotations";
import { createWorkflow } from "./workflow/factory";
import { createStreamFromWorkflowContext } from "./workflow/stream";
initObservability();
initSettings();
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { messages }: { messages: Message[]; data?: any } = body;
if (!isValidMessages(messages)) {
return NextResponse.json(
{
error:
"messages are required in the request body and the last message must be from the user",
},
{ status: 400 },
);
}
const chatHistory = convertToChatHistory(messages);
const userMessageContent = retrieveMessageContent(messages);
const workflow = await createWorkflow({ chatHistory });
const context = workflow.run({
message: userMessageContent,
streaming: true,
});
const { stream, dataStream } =
await createStreamFromWorkflowContext(context);
return LlamaIndexAdapter.toDataStreamResponse(stream, {
data: dataStream,
});
} catch (error) {
console.error("[LlamaIndex]", error);
return NextResponse.json(
{
detail: (error as Error).message,
},
{
status: 500,
},
);
}
}
@@ -1,179 +0,0 @@
import {
HandlerContext,
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/workflow";
import {
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponseChunk,
QueryEngineTool,
Settings,
ToolCall,
ToolCallLLM,
} from "llamaindex";
import { callTools, chatWithTools } from "./tools";
import { AgentInput, AgentRunEvent } from "./type";
class InputEvent extends WorkflowEvent<{
input: ChatMessage[];
}> {}
class ToolCallEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
type FunctionCallingAgentContextData = {
streaming: boolean;
};
export type FunctionCallingAgentInput = AgentInput & {
displayName: string;
};
export class FunctionCallingAgent extends Workflow<
FunctionCallingAgentContextData,
FunctionCallingAgentInput,
string | AsyncGenerator<boolean | ChatResponseChunk<object>>
> {
name: string;
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
tools: BaseToolWithCall[] | QueryEngineTool[];
systemPrompt?: string;
writeEvents: boolean;
role?: string;
constructor(options: {
name: string;
llm?: ToolCallLLM;
chatHistory?: ChatMessage[];
tools?: BaseToolWithCall[];
systemPrompt?: string;
writeEvents?: boolean;
role?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.name = options?.name;
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
if (!(this.llm instanceof ToolCallLLM)) {
throw new Error("LLM is not a ToolCallLLM");
}
this.memory = new ChatMemoryBuffer({
llm: this.llm,
chatHistory: options.chatHistory,
});
this.tools = options?.tools ?? [];
this.systemPrompt = options.systemPrompt;
this.writeEvents = options?.writeEvents ?? true;
this.role = options?.role;
// add steps
this.addStep(
{
inputs: [StartEvent<AgentInput>],
outputs: [InputEvent],
},
this.prepareChatHistory,
);
this.addStep(
{
inputs: [InputEvent],
outputs: [ToolCallEvent, StopEvent],
},
this.handleLLMInput,
);
this.addStep(
{
inputs: [ToolCallEvent],
outputs: [InputEvent],
},
this.handleToolCalls,
);
}
private get chatHistory() {
return this.memory.getMessages();
}
prepareChatHistory = async (
ctx: HandlerContext<FunctionCallingAgentContextData>,
ev: StartEvent<AgentInput>,
): Promise<InputEvent> => {
const { message, streaming } = ev.data;
ctx.data.streaming = streaming ?? false;
this.writeEvent(`Start to work on: ${message}`, ctx);
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
this.memory.put({ role: "user", content: message });
return new InputEvent({ input: this.chatHistory });
};
handleLLMInput = async (
ctx: HandlerContext<FunctionCallingAgentContextData>,
ev: InputEvent,
): Promise<StopEvent<string | AsyncGenerator> | ToolCallEvent> => {
const toolCallResponse = await chatWithTools(
this.llm,
this.tools,
this.chatHistory,
);
if (toolCallResponse.toolCallMessage) {
this.memory.put(toolCallResponse.toolCallMessage);
}
if (toolCallResponse.hasToolCall()) {
return new ToolCallEvent({ toolCalls: toolCallResponse.toolCalls });
}
if (ctx.data.streaming) {
if (!toolCallResponse.responseGenerator) {
throw new Error("No streaming response");
}
return new StopEvent(toolCallResponse.responseGenerator);
}
const fullResponse = await toolCallResponse.asFullResponse();
this.memory.put(fullResponse);
return new StopEvent(fullResponse.content.toString());
};
handleToolCalls = async (
ctx: HandlerContext<FunctionCallingAgentContextData>,
ev: ToolCallEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: this.tools,
toolCalls,
ctx,
agentName: this.name,
});
for (const msg of toolMsgs) {
this.memory.put(msg);
}
return new InputEvent({ input: this.memory.getMessages() });
};
writeEvent = (
msg: string,
ctx: HandlerContext<FunctionCallingAgentContextData>,
) => {
if (!this.writeEvents) return;
ctx.sendEvent(
new AgentRunEvent({ agent: this.name, text: msg, type: "text" }),
);
};
}
@@ -1,69 +0,0 @@
import {
StopEvent,
WorkflowContext,
WorkflowEvent,
} from "@llamaindex/workflow";
import { StreamData } from "ai";
import { ChatResponseChunk, EngineResponse } from "llamaindex";
import { ReadableStream } from "stream/web";
import { AgentRunEvent } from "./type";
export async function createStreamFromWorkflowContext<Input, Output, Context>(
context: WorkflowContext<Input, Output, Context>,
): Promise<{ stream: ReadableStream<EngineResponse>; dataStream: StreamData }> {
const dataStream = new StreamData();
let generator: AsyncGenerator<ChatResponseChunk> | undefined;
const closeStreams = (controller: ReadableStreamDefaultController) => {
controller.close();
dataStream.close();
};
const stream = new ReadableStream<EngineResponse>({
async start(controller) {
// Kickstart the stream by sending an empty string
controller.enqueue({ delta: "" } as EngineResponse);
},
async pull(controller) {
while (!generator) {
// get next event from workflow context
const { value: event, done } =
await context[Symbol.asyncIterator]().next();
if (done) {
closeStreams(controller);
return;
}
generator = handleEvent(event, dataStream);
}
const { value: chunk, done } = await generator.next();
if (done) {
closeStreams(controller);
return;
}
const delta = chunk.delta ?? "";
if (delta) {
controller.enqueue({ delta } as EngineResponse);
}
},
});
return { stream, dataStream };
}
function handleEvent(
event: WorkflowEvent<any>,
dataStream: StreamData,
): AsyncGenerator<ChatResponseChunk> | undefined {
// Handle for StopEvent
if (event instanceof StopEvent) {
return event.data as AsyncGenerator<ChatResponseChunk>;
}
// Handle for AgentRunEvent
if (event instanceof AgentRunEvent) {
dataStream.appendMessageAnnotation({
type: "agent",
data: event.data,
});
}
}
@@ -1,300 +0,0 @@
import { HandlerContext } from "@llamaindex/workflow";
import {
BaseToolWithCall,
callTool,
ChatMessage,
ChatResponse,
ChatResponseChunk,
PartialToolCall,
QueryEngineTool,
ToolCall,
ToolCallLLM,
ToolCallLLMMessageOptions,
} from "llamaindex";
import crypto from "node:crypto";
import { getDataSource } from "../engine";
import { createQueryEngineTool } from "../engine/tools/query-engine";
import { AgentRunEvent } from "./type";
export const getQueryEngineTool = async (): Promise<QueryEngineTool | null> => {
const index = await getDataSource();
if (!index) {
return null;
}
return createQueryEngineTool(index);
};
/**
* Call multiple tools and return the tool messages
*/
export const callTools = async <T>({
tools,
toolCalls,
ctx,
agentName,
writeEvent = true,
}: {
toolCalls: ToolCall[];
tools: BaseToolWithCall[];
ctx: HandlerContext<T>;
agentName: string;
writeEvent?: boolean;
}): Promise<ChatMessage[]> => {
const toolMsgs: ChatMessage[] = [];
if (toolCalls.length === 0) {
return toolMsgs;
}
if (toolCalls.length === 1) {
const tool = tools.find((tool) => tool.metadata.name === toolCalls[0].name);
if (!tool) {
throw new Error(`Tool ${toolCalls[0].name} not found`);
}
return [
await callSingleTool(
tool,
toolCalls[0],
writeEvent
? (msg: string) => {
ctx.sendEvent(
new AgentRunEvent({
agent: agentName,
text: msg,
type: "text",
}),
);
}
: undefined,
),
];
}
// Multiple tool calls, show events in progress
const progressId = crypto.randomUUID();
const totalSteps = toolCalls.length;
let currentStep = 0;
for (const toolCall of toolCalls) {
const tool = tools.find((tool) => tool.metadata.name === toolCall.name);
if (!tool) {
throw new Error(`Tool ${toolCall.name} not found`);
}
const toolMsg = await callSingleTool(tool, toolCall, (msg: string) => {
ctx.sendEvent(
new AgentRunEvent({
agent: agentName,
text: msg,
type: "progress",
data: {
id: progressId,
total: totalSteps,
current: currentStep,
},
}),
);
currentStep++;
});
toolMsgs.push(toolMsg);
}
return toolMsgs;
};
export const callSingleTool = async (
tool: BaseToolWithCall,
toolCall: ToolCall,
eventEmitter?: (msg: string) => void,
): Promise<ChatMessage> => {
if (eventEmitter) {
eventEmitter(
`Calling tool ${toolCall.name} with input: ${JSON.stringify(toolCall.input)}`,
);
}
const toolOutput = await callTool(tool, toolCall, {
log: () => {},
error: (...args: unknown[]) => {
console.error(`Tool ${toolCall.name} got error:`, ...args);
if (eventEmitter) {
eventEmitter(`Tool ${toolCall.name} got error: ${args.join(" ")}`);
}
return {
content: JSON.stringify({
error: args.join(" "),
}),
role: "user",
options: {
toolResult: {
id: toolCall.id,
result: JSON.stringify({
error: args.join(" "),
}),
isError: true,
},
},
};
},
warn: () => {},
});
return {
content: JSON.stringify(toolOutput.output),
role: "user",
options: {
toolResult: {
result: toolOutput.output,
isError: toolOutput.isError,
id: toolCall.id,
},
},
};
};
class ChatWithToolsResponse {
toolCalls: ToolCall[];
toolCallMessage?: ChatMessage;
responseGenerator?: AsyncGenerator<ChatResponseChunk>;
constructor(options: {
toolCalls: ToolCall[];
toolCallMessage?: ChatMessage;
responseGenerator?: AsyncGenerator<ChatResponseChunk>;
}) {
this.toolCalls = options.toolCalls;
this.toolCallMessage = options.toolCallMessage;
this.responseGenerator = options.responseGenerator;
}
hasMultipleTools() {
const uniqueToolNames = new Set(this.getToolNames());
return uniqueToolNames.size > 1;
}
hasToolCall() {
return this.toolCalls.length > 0;
}
getToolNames() {
return this.toolCalls.map((toolCall) => toolCall.name);
}
async asFullResponse(): Promise<ChatMessage> {
if (!this.responseGenerator) {
throw new Error("No response generator");
}
let fullResponse = "";
for await (const chunk of this.responseGenerator) {
fullResponse += chunk.delta;
}
return {
role: "assistant",
content: fullResponse,
};
}
}
export const chatWithTools = async (
llm: ToolCallLLM,
tools: BaseToolWithCall[],
messages: ChatMessage[],
): Promise<ChatWithToolsResponse> => {
const responseGenerator = async function* (): AsyncGenerator<
boolean | ChatResponseChunk,
void,
unknown
> {
const responseStream = await llm.chat({ messages, tools, stream: true });
let fullResponse = null;
let yieldedIndicator = false;
const toolCallMap = new Map();
for await (const chunk of responseStream) {
const hasToolCalls = chunk.options && "toolCall" in chunk.options;
if (!hasToolCalls) {
if (!yieldedIndicator) {
yield false;
yieldedIndicator = true;
}
yield chunk;
} else if (!yieldedIndicator) {
yield true;
yieldedIndicator = true;
}
if (chunk.options && "toolCall" in chunk.options) {
for (const toolCall of chunk.options.toolCall as PartialToolCall[]) {
if (toolCall.id) {
toolCallMap.set(toolCall.id, toolCall);
}
}
}
if (
hasToolCalls &&
(chunk.raw as any)?.choices?.[0]?.finish_reason !== null
) {
// Update the fullResponse with the tool calls
const toolCalls = Array.from(toolCallMap.values());
fullResponse = {
...chunk,
options: {
...chunk.options,
toolCall: toolCalls,
},
};
}
}
if (fullResponse) {
yield fullResponse;
}
};
const generator = responseGenerator();
const isToolCall = await generator.next();
if (isToolCall.value) {
// If it's a tool call, we need to wait for the full response
let fullResponse = null;
for await (const chunk of generator) {
fullResponse = chunk;
}
if (fullResponse) {
const responseChunk = fullResponse as ChatResponseChunk;
const toolCalls = getToolCallsFromResponse(responseChunk);
return new ChatWithToolsResponse({
toolCalls,
toolCallMessage: {
options: responseChunk.options,
role: "assistant",
content: "",
},
});
} else {
throw new Error("Cannot get tool calls from response");
}
}
return new ChatWithToolsResponse({
toolCalls: [],
responseGenerator: generator as AsyncGenerator<ChatResponseChunk>,
});
};
export const getToolCallsFromResponse = (
response:
| ChatResponse<ToolCallLLMMessageOptions>
| ChatResponseChunk<ToolCallLLMMessageOptions>,
): ToolCall[] => {
let options;
if ("message" in response) {
options = response.message.options;
} else {
options = response.options;
}
if (options && "toolCall" in options) {
return options.toolCall as ToolCall[];
}
return [];
};
@@ -1,24 +0,0 @@
import { WorkflowEvent } from "@llamaindex/workflow";
import { MessageContent } from "llamaindex";
export type AgentInput = {
message: MessageContent;
streaming?: boolean;
};
export type AgentRunEventType = "text" | "progress";
export type ProgressEventData = {
id: string;
total: number;
current: number;
};
export type AgentRunEventData = ProgressEventData;
export class AgentRunEvent extends WorkflowEvent<{
agent: string;
text: string;
type: AgentRunEventType;
data?: AgentRunEventData;
}> {}
@@ -1,12 +0,0 @@
import llama_index.core
import os
def init_observability():
PHOENIX_API_KEY = os.getenv("PHOENIX_API_KEY")
if not PHOENIX_API_KEY:
raise ValueError("PHOENIX_API_KEY environment variable is not set")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"api_key={PHOENIX_API_KEY}"
llama_index.core.set_global_handler(
"arize_phoenix", endpoint="https://llamatrace.com/v1/traces"
)
@@ -1,5 +0,0 @@
from traceloop.sdk import Traceloop
def init_observability():
Traceloop.init()
@@ -1,12 +0,0 @@
import * as traceloop from "@traceloop/node-server-sdk";
import * as LlamaIndex from "llamaindex";
export const initObservability = () => {
traceloop.initialize({
appName: "llama-app",
disableBatch: true,
instrumentModules: {
llamaIndex: LlamaIndex,
},
});
};
@@ -0,0 +1,21 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.fastembed import FastEmbedEmbedding
from llama_index.llms.anthropic import Anthropic
EMBEDDING_MODEL_MAP = {
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "sentence-transformers/all-mpnet-base-v2",
}
def init_settings():
if os.getenv("ANTHROPIC_API_KEY") is None:
raise RuntimeError("ANTHROPIC_API_KEY is missing in environment variables")
Settings.llm = Anthropic(model=os.getenv("MODEL") or "claude-3-sonnet")
# This will download the model automatically if it is not already downloaded
embed_model_name = EMBEDDING_MODEL_MAP[
os.getenv("EMBEDDING_MODEL") or "all-MiniLM-L6-v2"
]
Settings.embed_model = FastEmbedEmbedding(model_name=embed_model_name)

Some files were not shown because too many files have changed in this diff Show More