Compare commits

...

95 Commits

Author SHA1 Message Date
leehuwuj 75fa673456 Merge remote-tracking branch 'origin' into ms/use-inquirer 2025-01-22 17:22:44 +07:00
leehuwuj 7d8195c492 add group separator 2025-01-22 16:47:16 +07:00
Ravi Kumar 18c8d2540c added EMBEDDING_DIM if available or undefined to fallback to default config (#487) 2025-01-22 12:00:26 +07:00
leehuwuj 3a10379069 revert changes 2025-01-22 11:34:19 +07:00
leehuwuj 411fb6f289 revert changes 2025-01-22 11:11:40 +07:00
github-actions[bot] d4b4338f54 Release 0.3.27 (#492)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-22 10:59:19 +07:00
Huu Le b4e41aa526 feat: Add deep research use case (Python) (#482)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-01-22 10:22:49 +07:00
leehuwuj df7ac7f699 simplify changes 2025-01-21 17:40:55 +07:00
leehuwuj 0a1a36c9b2 fix parsing 2025-01-21 16:39:45 +07:00
Marcus Schiesser 8ae4b4d8f0 replace prompts with inquirer 2025-01-21 11:09:57 +07:00
github-actions[bot] 860b9d46d4 Release 0.3.26 (#484)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-17 17:14:45 +07:00
Huu Le f73d46bf10 fix missing multiagent code (#483) 2025-01-17 16:59:05 +07:00
github-actions[bot] eec237c5fe Release 0.3.25 (#477)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-27 13:11:44 +07:00
Thuc Pham 5450096e96 bump: react 19 stable (#476) 2024-12-27 13:01:59 +07:00
github-actions[bot] 163492f189 Release 0.3.24 (#472)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-27 09:54:29 +07:00
Huu Le a84743c576 add LlamaCloud support for reflex template (#473) 2024-12-26 15:09:16 +07:00
Thuc Pham fc5e56efa5 bump: code interpreter v1 (#469) 2024-12-26 15:06:00 +07:00
Huu Le a7a6592441 Fix the npm issue when running a fullstack Python app (#471) 2024-12-25 10:28:50 +07:00
github-actions[bot] af21426952 Release 0.3.23 (#470)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-24 16:40:23 +07:00
Huu Le 9077cae2f5 feat: Add legal document review use case (#467) 2024-12-24 15:38:37 +07:00
github-actions[bot] 765d2c4fff Release 0.3.22 (#463)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-12 10:57:51 +07:00
Marcus Schiesser 25667d45e9 feat: Make OpenAPI spec usable by custom GPTs (#462) 2024-12-11 17:10:23 +07:00
Sergey Lyapustin d31910a303 Fixed NEXT_QUESTION_PROMPT to suggest user questions. (#461) 2024-12-09 10:46:06 +07:00
github-actions[bot] 9852e7399c Release 0.3.21 (#459)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 16:41:24 +07:00
Marcus Schiesser 95227a7539 feat: add simple query endpoint (#458) 2024-12-06 16:12:52 +07:00
github-actions[bot] 71f29ea85d Release 0.3.20 (#457)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 12:15:32 +07:00
Huu Le 27d2499aff Bump llamacloud index and fix issues (#456) 2024-12-03 17:03:30 +07:00
github-actions[bot] a07f320e6d Release 0.3.19 (#455)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-02 11:39:29 +07:00
Huu Le f9a057ddde feat: add support for multimodal indexes (#453)
---------
Co-authored-by: thucpn <thucsh2@gmail.com>
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-29 18:02:14 +07:00
Thuc Pham aedd73d8c0 bump: chat-ui (#454) 2024-11-29 11:57:48 +07:00
github-actions[bot] da4505aff7 Release 0.3.18 (#451)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-27 16:56:27 +07:00
Huu Le 63e961e635 Refactor query engine tool code and use auto_routed mode for LlamaCloudIndex (#450)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-27 16:35:50 +07:00
Thuc Pham fe90a7e7ee chore: bump ai v4 (#449) 2024-11-27 12:26:53 +07:00
Huu Le 02b2473103 feat: Improve FastAPI agentic template (#447)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-26 10:54:22 +07:00
github-actions[bot] f17449b90a Release 0.3.17 (#446)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-22 16:36:36 +07:00
Huu Le 28c8808ce3 feat: Add fly.io deployment (#443)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-22 16:34:37 +07:00
Marcus Schiesser 0a7dfcf84b feat: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend (#445) 2024-11-22 11:06:38 +07:00
github-actions[bot] 6e70e327d3 Release 0.3.16 (#440)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-21 11:41:02 +07:00
Huu Le 8b371d8347 chore: fix incompatible with pydantic (#442) 2024-11-21 11:38:52 +07:00
Huu Le 30fe269575 Update duckduckgo tool option (#439) 2024-11-20 17:26:42 +07:00
Marcus Schiesser 49c35b834b docs: improve python readme 2024-11-20 13:30:08 +07:00
github-actions[bot] 82c2580ee5 Release 0.3.15 (#438)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-20 12:47:24 +07:00
Huu Le fc5b266a40 Simplify FastAPI fullstack template by using one deployment (#423)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-20 12:38:06 +07:00
Huu Le f8f97d2c00 Add support for Python 3.13 (#436) 2024-11-20 09:58:39 +07:00
github-actions[bot] 9c2e094883 Release 0.3.14 (#425)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-19 13:36:00 +07:00
Thuc Pham 00f0b3ae03 fix: dont include new message in chat history (#432) 2024-11-18 19:07:54 +07:00
Thuc Pham 4663dec81d chore: bump react19 rc (#430) 2024-11-18 16:47:51 +07:00
Huu Le 7f14e47f56 feat: Improve CI (#431) 2024-11-18 16:41:45 +07:00
Thuc Pham 6925676013 feat: use latest chat UI (#418)
---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-14 11:48:10 +08:00
Thuc Pham 44b34fb464 chore: update nextjs v15, react v19 and eslint v9 (#420) 2024-11-14 09:47:35 +07:00
github-actions[bot] a108911fc1 Release 0.3.13 (#424)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-13 20:36:32 +08:00
Huu Le 282eaa07fc Fix: ts upload file does not create index and document store (#422) 2024-11-13 19:47:28 +08:00
Marcus Schiesser 80db5f7c46 add help comment 2024-11-13 14:50:23 +08:00
github-actions[bot] 7a22c9f56d Release 0.3.12 (#416)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-13 13:28:23 +07:00
Huu Le 8431b788ad feat: Add form filling use case for TS and optimize workflows (#417) 2024-11-13 12:45:57 +07:00
Marcus Schiesser 2b712cebec chore: remove dead code 2024-11-07 10:13:47 +08:00
Huu Le 6edea6af5c enhance workflow code for Python (#412)
* enhance workflow shared code

* fix streaming

* refactor code

* add missing helper

* update

* update form filling

* add filters

* simplify the code

* simplify the code

* simplify the code

* update form filling

* update e2e

* update function calling agent

* fix unneeded condition

* Create light-parrots-work.md

* revert change on using functioncallingagent

* update readme

* clean code

* extract call one tool function

* update for blog use case

* fix streaming

* fix e2e

* fix missing await

* improve tools code

* improve assertion code

* skip form filling test for TS framework

* update for tools helper

---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-06 14:38:12 +07:00
Tom Aarsen d79d1652d1 Add new example HF embedding models (#415)
from https://huggingface.co/models?library=sentence-transformers
2024-11-05 16:12:07 +07:00
github-actions[bot] 8ebd8d7039 Release 0.3.11 (#409)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-04 16:41:34 +07:00
Marcus Schiesser 2b8aaa835d Add support for local models via Hugging Face (#414) 2024-11-04 16:39:27 +07:00
Huu Le 1fe21f85bd chore: Fix highlight.js issue with Next.js static build (#413) 2024-11-04 14:25:26 +07:00
Marcus Schiesser b9570b2eb9 fix: use generic LLMAgent instead of OpenAIAgent (adds support for Gemini and Anthropic for Agentic RAG) (#410) 2024-11-04 11:34:13 +07:00
Thuc Pham 00009ae53e feat: import pdf css (#408) 2024-11-01 17:21:08 +07:00
github-actions[bot] 63558c11fa Release 0.3.10 (#407)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-01 16:07:15 +07:00
Thuc Pham 9172fed2e8 feat: bump LITS 0.8.2 (#406)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-01 15:06:31 +07:00
Thuc Pham 78ccde78fc feat: integrate llamaindex chat-ui (#399)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-01 12:19:29 +07:00
github-actions[bot] 02510703d8 Release 0.3.9 (#405)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 16:05:33 +07:00
Huu Le ed59927bd0 feat: Add form filling use case for Python (#403)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-31 16:01:53 +07:00
Thuc Pham 9f866aa981 fix: use uploaded filename to build file url (#404) 2024-10-30 14:47:11 +07:00
github-actions[bot] b8f78612b8 Release 0.3.8 (#396)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 16:47:26 +07:00
Huu Le 4a8346900d feat: Add multi-agent financial report use case for TS (#394) 2024-10-25 16:44:56 +07:00
github-actions[bot] 42e63842d0 Release 0.3.7 (#395)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 14:34:55 +07:00
Huu Le fa803787e3 relative url (#393) 2024-10-25 14:13:34 +07:00
github-actions[bot] c5559d8e59 Release 0.3.6 (#392)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-23 17:51:46 +07:00
Huu Le 0182368744 Fix: UI streaming issue (#391) 2024-10-23 17:38:48 +07:00
github-actions[bot] ff46bd6153 Release 0.3.5 (#390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-23 16:40:11 +07:00
Huu Le 2209409cdb Feature: Update multi-agent template to use financial report use case (#386)
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-23 16:36:12 +07:00
github-actions[bot] 623f8b811b Release 0.3.4 (#389)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 17:25:00 +07:00
Huu Le 384a1368dd Add mypy checker for importing and update CI condition (#387) 2024-10-22 17:00:52 +07:00
github-actions[bot] 189c0e3f6c Release 0.3.3 (#383)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 10:50:58 +07:00
Huu Le 99b8247bc9 Enhance data type (#378) 2024-10-17 16:37:14 +07:00
github-actions[bot] 74c5a15450 Release 0.3.2 (#381)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-17 11:39:38 +07:00
Marcus Schiesser 9293e330ac Update demo video in README.md 2024-10-17 11:38:22 +07:00
Marcus Schiesser 6d1b6b9372 docs: readme update for pro mode 2024-10-17 11:13:00 +07:00
github-actions[bot] a8162a9269 Release 0.3.1 (#377)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-16 15:23:09 +07:00
Huu Le f3577c50d6 add data scientist use case (directly using uploaded files) (#355)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2024-10-16 14:00:59 +07:00
Huu Le a5f5c9dc9c fix always ask post installation action (#376) 2024-10-16 09:52:25 +07:00
Huu Le 2be68d1c7f ci: activate llamacloud for TS (#372)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-15 13:40:47 +07:00
Thuc Pham 8c80cc05ce fix: enhance performance for codeblock (#347)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-15 12:21:08 +07:00
github-actions[bot] dfd4fd58ab Release 0.3.0 (#368)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-14 16:25:37 +07:00
Thuc Pham 0a69fe09fa fix: missing params when init Astra vectorstore (#373) 2024-10-14 16:03:41 +07:00
Marcus Schiesser de88b32208 fix: remove llamacloud for extractor 2024-10-14 15:35:59 +07:00
Marcus Schiesser ef88bff211 chore: upgrade reflex 2024-10-14 15:09:16 +07:00
Marcus Schiesser 7562cb48d6 docs: changeset 2024-10-14 13:41:22 +07:00
Marcus Schiesser 9dde6d0288 feat: simplify questions asked (#370) 2024-10-14 13:35:39 +07:00
257 changed files with 10590 additions and 5414 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
docs: chroma env variables
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs", "express"]
datasources: ["--no-files", "--example-file"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
shell: bash
+4
View File
@@ -51,3 +51,7 @@ e2e/cache
# build artifacts
create-llama-*.tgz
# vscode
.vscode
!.vscode/settings.json
+193
View File
@@ -1,5 +1,198 @@
# create-llama
## 0.3.27
### Patch Changes
- b4e41aa: Add deep research over own documents use case (Python)
## 0.3.26
### Patch Changes
- f73d46b: Fix missing copy of the multiagent code
## 0.3.25
### Patch Changes
- 5450096: bump: react 19 stable
## 0.3.24
### Patch Changes
- a84743c: Change --agents paramameter to --use-case
- a84743c: Add LlamaCloud support for Reflex templates
- a7a6592: Fix the npm issue on the full-stack Python template
- fc5e56e: bump: code interpreter v1
## 0.3.23
### Patch Changes
- 9077cae: Add contract review use case (Python)
## 0.3.22
### Patch Changes
- 25667d4: Make OpenAPI spec usable by custom GPTs
## 0.3.21
### Patch Changes
- 95227a7: Add query endpoint
## 0.3.20
### Patch Changes
- 27d2499: Bump the LlamaCloud library and fix breaking changes (Python).
## 0.3.19
### Patch Changes
- f9a057d: Add support multimodal indexes (e.g. from LlamaCloud)
- aedd73d: bump: chat-ui
## 0.3.18
### Patch Changes
- fe90a7e: chore: bump ai v4
- 02b2473: Show streaming errors in Python, optimize system prompts for tool usage and set the weather tool as default for the Agentic RAG use case
- 63e961e: Use auto_routed retriever mode for LlamaCloudIndex
## 0.3.17
### Patch Changes
- 28c8808: Add fly.io deployment
- 0a7dfcf: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend
## 0.3.16
### Patch Changes
- 8b371d8: Set pydantic version to <2.10 to avoid incompatibility with llama-index.
- 30fe269: Deactive duckduckgo tool for TS
- 30fe269: Replace DuckDuckGo by Wikipedia tool for agentic template
## 0.3.15
### Patch Changes
- fc5b266: Improve DX for Python template (use one deployment instead of two)
- f8f97d2: Add support for python 3.13
## 0.3.14
### Patch Changes
- 00f0b3a: fix: dont include user message in chat history
- 4663dec: chore: bump react19 rc
- 44b34fb: chore: update eslint 9, nextjs 15, react 19
- 6925676: feat: use latest chat UI
## 0.3.13
### Patch Changes
- 282eaa0: Ensure that the index and document store are created when uploading a file with no available index.
## 0.3.12
### Patch Changes
- 6edea6a: Optimize generated workflow code for Python
- 8431b78: Optimize Typescript multi-agent code
- 8431b78: Add form filling use case (Typescript)
## 0.3.11
### Patch Changes
- 2b8aaa8: Add support for local models via Hugging Face
- b9570b2: Fix: use generic LLMAgent instead of OpenAIAgent (adds support for Gemini and Anthropic for Agentic RAG)
- 1fe21f8: Fix the highlight.js issue with the Next.js static build
- 00009ae: feat: import pdf css
## 0.3.10
### Patch Changes
- 9172fed: feat: bump LITS 0.8.2
- 78ccde7: feat: use llamaindex chat-ui for nextjs frontend
## 0.3.9
### Patch Changes
- ed59927: Add form filling use case (Python)
## 0.3.8
### Patch Changes
- 4a83469: Add multi-agent financial report for Typescript (and update LITS to 0.7.10)
## 0.3.7
### Patch Changes
- fa80378: DocumentInfo working with relative URLs
## 0.3.6
### Patch Changes
- 0182368: Fix the streaming issue to prevent the UI from hanging.
## 0.3.5
### Patch Changes
- 2209409: Add financial report as the default use case in the multi-agent template (Python).
## 0.3.4
### Patch Changes
- 384a136: Fix import error if the artifact tool is selected
## 0.3.3
### Patch Changes
- 99b8247: Simplify and unify handling file uploads
## 0.3.2
### Patch Changes
- 6d1b6b9: Update README.md for pro mode
## 0.3.1
### Patch Changes
- f3577c5: Fix event streaming is blocked
- f3577c5: Add upload file to sandbox (artifact and code interpreter)
## 0.3.0
### Minor Changes
- 7562cb4: Simplified default questions and added pro mode
### Patch Changes
- 0a69fe0: fix: missing params when init Astra vectorstore
- 98a82b0: docs: chroma env variables
## 0.2.19
### Patch Changes
+31 -41
View File
@@ -12,7 +12,7 @@ npx create-llama@latest
to get started, or watch this video for a demo session:
https://github.com/user-attachments/assets/dd3edc36-4453-4416-91c2-d24326c6c167
<img src="https://github.com/user-attachments/assets/c4a7fe18-8e30-498a-96f8-78127dd706b9" width="100%">
Once your app is generated, run
@@ -24,14 +24,14 @@ 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 3 back-ends:
- 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.
- **Express**: if you want a more traditional Node.js application you can generate an Express backend. This also uses LlamaIndex.TS.
- **Python FastAPI**: if you select this option, youll get a backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like Render or fly.io.
- The back-end has two endpoints (one streaming, the other one non-streaming) that allow you to send the state of your chat and receive additional responses
- You add arbitrary data sources to your chat, like local files, websites, or data retrieved from a database.
- Turn your chat into an AI agent by adding tools (functions called by the LLM).
- **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
- 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,9 +40,9 @@ https://github.com/user-attachments/assets/d57af1a1-d99b-4e9c-98d9-4cbd1327eff8
## Using your data
You can supply your own data; the app will index it and 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` (If you're using Express or Python and generate a frontend, it will be `./backend/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 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.
Before you can use your data, you need to index it. If you're using the Next.js or Express apps, run:
@@ -58,10 +58,6 @@ If you're using the Python backend, you can trigger indexing of your data by cal
poetry run generate
```
## Want a front-end?
Optionally generate a frontend if you've selected the Python or Express back-ends. If you do so, `create-llama` will generate two folders: `frontend`, for your Next.js-based frontend code, and `backend` containing your API.
## Customizing the AI models
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
@@ -94,46 +90,40 @@ Need to install the following packages:
create-llama@latest
Ok to proceed? (y) y
✔ What is your project named? … my-app
✔ Which template would you like to use? Agentic RAG (e.g. chat with docs)
✔ Which framework would you like to use? NextJS
Would you like to set up observability? No
✔ What app 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): …
✔ Which data source would you like to use? Use an example PDF
✔ Would you like to add another data source? No
✔ Would you like to use LlamaParse (improved parser for RAG - requires API key)? … no / yes
✔ Would you like to use a vector database? No, just store the data in the file system
✔ Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter Weather
? How would you like to proceed? - Use arrow-keys. Return to submit.
Just generate code (~1 sec)
Start in VSCode (~1 sec)
Generate code and install dependencies (~2 min)
Generate code, install dependencies, and run the app (~2 min)
Just generate code (~1 sec)
Start in VSCode (~1 sec)
Generate code and install dependencies (~2 min)
```
### Running non-interactively
You can also pass command line arguments to set up a new project
non-interactively. See `create-llama --help`:
non-interactively. For a list of the latest options, call `create-llama --help`.
```bash
create-llama <project-directory> [options]
### Running in pro mode
Options:
-V, --version output the version number
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
--use-npm
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
Explicitly tell the CLI to bootstrap the app using npm
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
- Image Generator: Creates images based on text descriptions
- Web Search: Performs web searches to retrieve up-to-date information
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
--use-pnpm
Explicitly tell the CLI to bootstrap the app using pnpm
--use-yarn
Explicitly tell the CLI to bootstrap the app using Yarn
```
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
## LlamaIndex Documentation
+10 -20
View File
@@ -7,17 +7,16 @@ import { getOnline } from "./helpers/is-online";
import { isWriteable } from "./helpers/is-writeable";
import { makeDir } from "./helpers/make-dir";
import fs from "fs";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
import { installTemplate } from "./helpers";
import { writeDevcontainer } from "./helpers/devcontainer";
import { templatesDir } from "./helpers/dir";
import { toolsRequireConfig } from "./helpers/tools";
import { configVSCode } from "./helpers/vscode";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "customApiPath"
"appName" | "root" | "isOnline" | "port"
> & {
appPath: string;
frontend: boolean;
@@ -35,12 +34,12 @@ export async function createApp({
communityProjectConfig,
llamapack,
vectorDb,
externalPort,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
useCase,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
@@ -80,39 +79,30 @@ export async function createApp({
communityProjectConfig,
llamapack,
vectorDb,
externalPort,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
useCase,
};
if (frontend) {
// install backend
const backendRoot = path.join(root, "backend");
await makeDir(backendRoot);
await installTemplate({ ...args, root: backendRoot, backend: true });
// Install backend
await installTemplate({ ...args, backend: true });
if (frontend && framework === "fastapi") {
// install frontend
const frontendRoot = path.join(root, "frontend");
const frontendRoot = path.join(root, ".frontend");
await makeDir(frontendRoot);
await installTemplate({
...args,
root: frontendRoot,
framework: "nextjs",
customApiPath: `http://localhost:${externalPort ?? 8000}/api/chat`,
backend: false,
});
// copy readme for fullstack
await fs.promises.copyFile(
path.join(templatesDir, "README-fullstack.md"),
path.join(root, "README.md"),
);
} else {
await installTemplate({ ...args, backend: true });
}
await writeDevcontainer(root, templatesDir, framework, frontend);
await configVSCode(root, templatesDir, framework);
process.chdir(root);
if (tryGitInit(root)) {
-4
View File
@@ -63,7 +63,6 @@ if (
vectorDb,
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
@@ -101,7 +100,6 @@ if (
vectorDb: "none",
tools: tool,
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
@@ -135,7 +133,6 @@ if (
vectorDb: "none",
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
@@ -169,7 +166,6 @@ if (
vectorDb: "none",
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
-63
View File
@@ -1,63 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import { TemplateFramework } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// The extractor template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
test.describe("Test extractor template", async () => {
let frontendPort: number;
let backendPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create extractor app
test.beforeAll(async () => {
cwd = await createTestDir();
frontendPort = Math.floor(Math.random() * 10000) + 10000;
backendPort = frontendPort + 1;
const result = await runCreateLlama({
cwd,
templateType: "extractor",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: frontendPort,
externalPort: backendPort,
postInstallAction: "runApp",
});
name = result.projectName;
appProcess = result.appProcess;
});
test.afterAll(async () => {
appProcess.kill();
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${frontendPort}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 2000 * 60,
});
});
});
}
+73 -61
View File
@@ -16,70 +16,82 @@ const templateFramework: TemplateFramework = process.env.FRAMEWORK
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage = "Write a blog post about physical standards for letters";
const templateUseCases = ["financial_report", "blog", "form_filling"];
test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
);
let port: number;
let externalPort: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
externalPort = port + 1;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "multiagent",
templateFramework,
dataSource,
vectorDb,
port,
externalPort,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
});
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 }) => {
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 the start of a streamed response", async ({
page,
}) => {
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const responsePromise = page.waitForResponse((res) =>
res.url().includes("/api/chat"),
for (const useCase of templateUseCases) {
test.describe(`Test multiagent template ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
);
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
await page.click("form button[type=submit]");
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "multiagent",
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
});
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
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 the start of a streamed response", async ({
page,
}) => {
test.skip(
templatePostInstallAction !== "runApp" ||
useCase === "financial_report" ||
useCase === "form_filling" ||
templateFramework === "express",
"Skip chat tests for financial report and form filling.",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const responsePromise = page.waitForResponse((res) =>
res.url().includes("/api/chat"),
);
await page.click("form button[type=submit]");
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
}
+64
View File
@@ -0,0 +1,64 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import { TemplateFramework, TemplateUseCase } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
// The reflex template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
for (const useCase of templateUseCases) {
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
let appPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create reflex app
test.beforeAll(async () => {
cwd = await createTestDir();
appPort = Math.floor(Math.random() * 10000) + 10000;
const result = await runCreateLlama({
cwd,
templateType: "reflex",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: appPort,
postInstallAction: "runApp",
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
});
test.afterAll(async () => {
appProcess.kill();
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${appPort}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 2000 * 60,
});
});
});
}
}
+9 -7
View File
@@ -22,7 +22,7 @@ const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage =
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
@@ -35,7 +35,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
}
let port: number;
let externalPort: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
@@ -44,7 +43,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
externalPort = port + 1;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
@@ -53,7 +51,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
dataSource,
vectorDb,
port,
externalPort,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
@@ -68,8 +65,11 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
@@ -77,7 +77,9 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
test("Frontend should be able to submit a message and receive a response", async ({
page,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const [response] = await Promise.all([
@@ -102,7 +104,7 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
test.skip(templatePostInstallAction !== "runApp");
test.skip(templateFramework === "nextjs");
const response = await request.post(
`http://localhost:${externalPort}/api/chat/request`,
`http://localhost:${port}/api/chat/request`,
{
data: {
messages: [
@@ -56,7 +56,6 @@ test.describe("Test resolve TS dependencies", () => {
dataSource: dataSource,
vectorDb: vectorDb,
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
+7 -28
View File
@@ -25,7 +25,6 @@ export type RunCreateLlamaOptions = {
dataSource: string;
vectorDb: TemplateVectorDB;
port: number;
externalPort: number;
postInstallAction: TemplatePostInstallAction;
templateUI?: TemplateUI;
appType?: AppType;
@@ -34,6 +33,7 @@ export type RunCreateLlamaOptions = {
tools?: string;
useLlamaParse?: boolean;
observability?: string;
useCase?: string;
};
export async function runCreateLlama({
@@ -43,7 +43,6 @@ export async function runCreateLlama({
dataSource,
vectorDb,
port,
externalPort,
postInstallAction,
templateUI,
appType,
@@ -52,6 +51,7 @@ export async function runCreateLlama({
tools,
useLlamaParse,
observability,
useCase,
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
throw new Error(
@@ -88,21 +88,15 @@ export async function runCreateLlama({
...dataSourceArgs,
"--vector-db",
vectorDb,
"--open-ai-key",
process.env.OPENAI_API_KEY,
"--use-pnpm",
"--use-npm",
"--port",
port,
"--external-port",
externalPort,
"--post-install-action",
postInstallAction,
"--tools",
tools ?? "none",
"--observability",
"none",
"--llama-cloud-key",
process.env.LLAMA_CLOUD_API_KEY,
];
if (templateUI) {
@@ -119,6 +113,9 @@ export async function runCreateLlama({
if (observability) {
commandArgs.push("--observability", observability);
}
if ((templateType === "multiagent" || templateType === "reflex") && useCase) {
commandArgs.push("--use-case", useCase);
}
const command = commandArgs.join(" ");
console.log(`running command '${command}' in ${cwd}`);
@@ -141,12 +138,7 @@ export async function runCreateLlama({
// Wait for app to start
if (postInstallAction === "runApp") {
await checkAppHasStarted(
appType === "--frontend",
templateFramework,
port,
externalPort,
);
await waitPorts([port]);
} else if (postInstallAction === "dependencies") {
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
} else {
@@ -166,19 +158,6 @@ export async function createTestDir() {
return cwd;
}
// eslint-disable-next-line max-params
async function checkAppHasStarted(
frontend: boolean,
framework: TemplateFramework,
port: number,
externalPort: number,
) {
const portsToWait = frontend
? [port, externalPort]
: [framework === "nextjs" ? port : externalPort];
await waitPorts(portsToWait);
}
async function waitPorts(ports: number[]): Promise<void> {
const waitForPort = async (port: number): Promise<void> => {
await waitPort({
+3
View File
@@ -61,6 +61,9 @@ export const assetRelocator = (name: string) => {
case "README-template.md": {
return "README.md";
}
case "vscode_settings.json": {
return "settings.json";
}
default: {
return name;
}
+41
View File
@@ -11,6 +11,47 @@ export const EXAMPLE_FILE: TemplateDataSource = {
},
};
export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
{
type: "file",
config: {
url: new URL(
"https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf",
),
filename: "apple_10k_report.pdf",
},
},
{
type: "file",
config: {
url: new URL(
"https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
),
filename: "tesla_10k_report.pdf",
},
},
];
export const EXAMPLE_GDPR: TemplateDataSource = {
type: "file",
config: {
url: new URL(
"https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32016R0679",
),
filename: "gdpr.pdf",
},
};
export const AI_REPORTS: TemplateDataSource = {
type: "file",
config: {
url: new URL(
"https://www.europarl.europa.eu/RegData/etudes/ATAG/2024/760392/EPRS_ATA(2024)760392_EN.pdf",
),
filename: "EPRS_ATA_2024_760392_EN.pdf",
},
};
export function getDataSources(
files?: string,
exampleFile?: boolean,
+43 -18
View File
@@ -13,6 +13,12 @@ import {
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
const DEFAULT_SYSTEM_PROMPT =
"You are a helpful assistant who helps users with their questions.";
const DATA_SOURCES_PROMPT =
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
export type EnvVar = {
name?: string;
description?: string;
@@ -217,7 +223,13 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
},
];
default:
return [];
return [
{
name: "STORAGE_CACHE_DIR",
description: "The directory to store the local storage cache.",
value: ".cache",
},
];
}
};
@@ -336,6 +348,20 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
},
]
: []),
...(modelConfig.provider === "huggingface"
? [
{
name: "EMBEDDING_BACKEND",
description:
"The backend to use for the Sentence Transformers embedding model, either 'torch', 'onnx', or 'openvino'. Defaults to 'onnx'.",
},
{
name: "EMBEDDING_TRUST_REMOTE_CODE",
description:
"Whether to trust remote code for the embedding model, required for some models with custom code.",
},
]
: []),
...(modelConfig.provider === "t-systems"
? [
{
@@ -387,6 +413,13 @@ const getFrameworkEnvs = (
],
);
}
if (framework === "nextjs") {
result.push({
name: "NEXT_PUBLIC_CHAT_API",
description:
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
});
}
return result;
};
@@ -422,9 +455,6 @@ const getSystemPromptEnv = (
dataSources?: TemplateDataSource[],
template?: TemplateType,
): EnvVar[] => {
const defaultSystemPrompt =
"You are a helpful assistant who helps users with their questions.";
const systemPromptEnv: EnvVar[] = [];
// build tool system prompt by merging all tool system prompts
// multiagent template doesn't need system prompt
@@ -439,9 +469,12 @@ const getSystemPromptEnv = (
}
});
const systemPrompt = toolSystemPrompt
? `\"${toolSystemPrompt}\"`
: defaultSystemPrompt;
const systemPrompt =
'"' +
DEFAULT_SYSTEM_PROMPT +
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
'"';
systemPromptEnv.push({
name: "SYSTEM_PROMPT",
@@ -492,7 +525,7 @@ Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that you might ask next!
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
@@ -533,7 +566,7 @@ export const createBackendEnvFile = async (
| "framework"
| "dataSources"
| "template"
| "externalPort"
| "port"
| "tools"
| "observability"
>,
@@ -550,7 +583,7 @@ export const createBackendEnvFile = async (
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getVectorDBEnvs(opts.vectorDb, opts.framework),
...getFrameworkEnvs(opts.framework, opts.externalPort),
...getFrameworkEnvs(opts.framework, opts.port),
...getToolEnvs(opts.tools),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
@@ -565,18 +598,10 @@ export const createBackendEnvFile = async (
export const createFrontendEnvFile = async (
root: string,
opts: {
customApiPath?: string;
vectorDb?: TemplateVectorDB;
},
) => {
const defaultFrontendEnvs = [
{
name: "NEXT_PUBLIC_CHAT_API",
description: "The backend API for chat endpoint.",
value: opts.customApiPath
? opts.customApiPath
: "http://localhost:8000/api/chat",
},
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description: "Let's the user change indexes in LlamaCloud projects",
+30 -8
View File
@@ -96,6 +96,12 @@ async function generateContextData(
}
}
const downloadFile = async (url: string, destPath: string) => {
const response = await fetch(url);
const fileBuffer = await response.arrayBuffer();
await fsExtra.writeFile(destPath, Buffer.from(fileBuffer));
};
const prepareContextData = async (
root: string,
dataSources: TemplateDataSource[],
@@ -103,12 +109,29 @@ const prepareContextData = async (
await makeDir(path.join(root, "data"));
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// Copy local data
const dataPath = dataSourceConfig.path;
const destPath = path.join(root, "data", path.basename(dataPath));
console.log("Copying data from path:", dataPath);
await fsExtra.copy(dataPath, destPath);
// If the path is URLs, download the data and save it to the data directory
if ("url" in dataSourceConfig) {
console.log(
"Downloading file from URL:",
dataSourceConfig.url.toString(),
);
const destPath = path.join(
root,
"data",
dataSourceConfig.filename ??
path.basename(dataSourceConfig.url.toString()),
);
await downloadFile(dataSourceConfig.url.toString(), destPath);
} else {
// Copy local data
console.log("Copying data from path:", dataSourceConfig.path);
const destPath = path.join(
root,
"data",
path.basename(dataSourceConfig.path),
);
await fsExtra.copy(dataSourceConfig.path, destPath);
}
}
};
@@ -170,7 +193,7 @@ export const installTemplate = async (
if (
props.template === "streaming" ||
props.template === "multiagent" ||
props.template === "extractor"
props.template === "reflex"
) {
await createBackendEnvFile(props.root, props);
}
@@ -203,7 +226,6 @@ export const installTemplate = async (
} else {
// this is a frontend for a full-stack app, create .env file with model information
await createFrontendEnvFile(props.root, {
customApiPath: props.customApiPath,
vectorDb: props.vectorDb,
});
}
+12 -20
View File
@@ -1,7 +1,6 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import inquirer from "inquirer";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { toChoice } from "../../questions/utils";
const MODELS = [
"claude-3-opus",
@@ -58,43 +57,36 @@ export async function askAnthropicQuestions({
};
if (!config.apiKey) {
const { key } = await prompts(
const { key } = await inquirer.prompt([
{
type: "text",
type: "input",
name: "key",
message:
"Please provide your Anthropic API key (or leave blank to use ANTHROPIC_API_KEY env variable):",
},
questionHandlers,
);
]);
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
}
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
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(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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[
+10 -18
View File
@@ -1,7 +1,5 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import inquirer from "inquirer";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { questionHandlers } from "../../questions";
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
"gpt-35-turbo": { openAIModel: "gpt-3.5-turbo" },
@@ -67,31 +65,25 @@ export async function askAzureQuestions({
},
};
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
name: "model",
message: "Which LLM model would you like to use?",
choices: getAvailableModelChoices(),
initial: 0,
},
questionHandlers,
);
]);
config.model = model;
const { embeddingModel } = await prompts(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: getAvailableEmbeddingModelChoices(),
initial: 0,
},
questionHandlers,
);
]);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
}
@@ -101,14 +93,14 @@ export async function askAzureQuestions({
function getAvailableModelChoices() {
return Object.keys(ALL_AZURE_OPENAI_CHAT_MODELS).map((key) => ({
title: key,
name: key,
value: key,
}));
}
function getAvailableEmbeddingModelChoices() {
return Object.keys(ALL_AZURE_OPENAI_EMBEDDING_MODELS).map((key) => ({
title: key,
name: key,
value: key,
}));
}
+12 -20
View File
@@ -1,7 +1,6 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import inquirer from "inquirer";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { toChoice } from "../../questions/utils";
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
type ModelData = {
@@ -42,43 +41,36 @@ export async function askGeminiQuestions({
};
if (!config.apiKey) {
const { key } = await prompts(
const { key } = await inquirer.prompt([
{
type: "text",
type: "input",
name: "key",
message:
"Please provide your Google API key (or leave blank to use GOOGLE_API_KEY env variable):",
},
questionHandlers,
);
]);
config.apiKey = key || process.env.GOOGLE_API_KEY;
}
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
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(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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;
}
+12 -20
View File
@@ -1,7 +1,6 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import inquirer from "inquirer";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { toChoice } from "../../questions/utils";
import got from "got";
import ora from "ora";
@@ -98,45 +97,38 @@ export async function askGroqQuestions({
};
if (!config.apiKey) {
const { key } = await prompts(
const { key } = await inquirer.prompt([
{
type: "text",
type: "input",
name: "key",
message:
"Please provide your Groq API key (or leave blank to use GROQ_API_KEY env variable):",
},
questionHandlers,
);
]);
config.apiKey = key || process.env.GROQ_API_KEY;
}
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
if (askModels) {
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
const { model } = await prompts(
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
name: "model",
message: "Which LLM model would you like to use?",
choices: modelChoices,
initial: 0,
},
questionHandlers,
);
]);
config.model = model;
const { embeddingModel } = await prompts(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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[
+64
View File
@@ -0,0 +1,64 @@
import inquirer from "inquirer";
import { ModelConfigParams } from ".";
import { toChoice } from "../../questions/utils";
const MODELS = ["HuggingFaceH4/zephyr-7b-alpha"];
type ModelData = {
dimensions: number;
};
const EMBEDDING_MODELS: Record<string, ModelData> = {
"BAAI/bge-small-en-v1.5": { dimensions: 384 },
"BAAI/bge-base-en-v1.5": { dimensions: 768 },
"BAAI/bge-large-en-v1.5": { dimensions: 1024 },
"sentence-transformers/all-MiniLM-L6-v2": { dimensions: 384 },
"sentence-transformers/all-mpnet-base-v2": { dimensions: 768 },
"intfloat/multilingual-e5-large": { dimensions: 1024 },
"mixedbread-ai/mxbai-embed-large-v1": { dimensions: 1024 },
"nomic-ai/nomic-embed-text-v1.5": { dimensions: 768 },
};
const DEFAULT_MODEL = MODELS[0];
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> {
const config: ModelConfigParams = {
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
isConfigured(): boolean {
return true;
},
};
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "list",
name: "model",
message: "Which Hugging Face model would you like to use?",
choices: MODELS.map(toChoice),
},
]);
config.model = model;
const { embeddingModel } = await inquirer.prompt([
{
type: "list",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
},
]);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
return config;
}
+18 -17
View File
@@ -1,11 +1,10 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { questionHandlers } from "../../questions";
import inquirer from "inquirer";
import { ModelConfig, ModelProvider, TemplateFramework } from "../types";
import { askAnthropicQuestions } from "./anthropic";
import { askAzureQuestions } from "./azure";
import { askGeminiQuestions } from "./gemini";
import { askGroqQuestions } from "./groq";
import { askHuggingfaceQuestions } from "./huggingface";
import { askLLMHubQuestions } from "./llmhub";
import { askMistralQuestions } from "./mistral";
import { askOllamaQuestions } from "./ollama";
@@ -27,30 +26,29 @@ export async function askModelConfig({
framework,
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
if (askModels && !ciInfo.isCI) {
if (askModels) {
let 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" },
{ name: "OpenAI", value: "openai" },
{ name: "Groq", value: "groq" },
{ name: "Ollama", value: "ollama" },
{ name: "Anthropic", value: "anthropic" },
{ name: "Gemini", value: "gemini" },
{ name: "Mistral", value: "mistral" },
{ name: "AzureOpenAI", value: "azure-openai" },
];
if (framework === "fastapi") {
choices.push({ title: "T-Systems", value: "t-systems" });
choices.push({ name: "T-Systems", value: "t-systems" });
choices.push({ name: "Huggingface", value: "huggingface" });
}
const { provider } = await prompts(
const { provider } = await inquirer.prompt([
{
type: "select",
type: "list",
name: "provider",
message: "Which model provider would you like to use",
choices: choices,
initial: 0,
},
questionHandlers,
);
]);
modelProvider = provider;
}
@@ -77,6 +75,9 @@ export async function askModelConfig({
case "t-systems":
modelConfig = await askLLMHubQuestions({ askModels });
break;
case "huggingface":
modelConfig = await askHuggingfaceQuestions({ askModels });
break;
default:
modelConfig = await askOpenAIQuestions({
openAiKey,
+12 -21
View File
@@ -1,10 +1,8 @@
import ciInfo from "ci-info";
import got from "got";
import inquirer from "inquirer";
import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers } from "../../questions";
export const TSYSTEMS_LLMHUB_API_URL =
"https://llm-server.llmhub.t-systems.net/v2";
@@ -58,9 +56,9 @@ export async function askLLMHubQuestions({
};
if (!config.apiKey) {
const { key } = await prompts(
const { key } = await inquirer.prompt([
{
type: "text",
type: "input",
name: "key",
message: askModels
? "Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):"
@@ -75,36 +73,29 @@ export async function askLLMHubQuestions({
return true;
},
},
questionHandlers,
);
]);
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
}
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
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(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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);
}
@@ -144,7 +135,7 @@ async function getAvailableModelChoices(
)
.map((el: any) => {
return {
title: el.id,
name: el.id,
value: el.id,
};
});
+12 -20
View File
@@ -1,7 +1,6 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import inquirer from "inquirer";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { toChoice } from "../../questions/utils";
const MODELS = ["mistral-tiny", "mistral-small", "mistral-medium"];
type ModelData = {
@@ -41,43 +40,36 @@ export async function askMistralQuestions({
};
if (!config.apiKey) {
const { key } = await prompts(
const { key } = await inquirer.prompt([
{
type: "text",
type: "input",
name: "key",
message:
"Please provide your Mistral API key (or leave blank to use MISTRAL_API_KEY env variable):",
},
questionHandlers,
);
]);
config.apiKey = key || process.env.MISTRAL_API_KEY;
}
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
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(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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;
}
+9 -16
View File
@@ -1,9 +1,8 @@
import ciInfo from "ci-info";
import inquirer from "inquirer";
import ollama, { type ModelResponse } from "ollama";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
import { toChoice } from "../../questions/utils";
type ModelData = {
dimensions: number;
@@ -34,32 +33,26 @@ export async function askOllamaQuestions({
},
};
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
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(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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;
+14 -22
View File
@@ -1,10 +1,9 @@
import ciInfo from "ci-info";
import got from "got";
import inquirer from "inquirer";
import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { questionHandlers } from "../../questions";
import { isCI } from "../../questions";
const OPENAI_API_URL = "https://api.openai.com/v1";
@@ -31,10 +30,10 @@ export async function askOpenAIQuestions({
},
};
if (!config.apiKey) {
const { key } = await prompts(
if (!config.apiKey && !isCI) {
const { key } = await inquirer.prompt([
{
type: "text",
type: "input",
name: "key",
message: askModels
? "Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):"
@@ -49,36 +48,29 @@ export async function askOpenAIQuestions({
return true;
},
},
questionHandlers,
);
]);
config.apiKey = key || process.env.OPENAI_API_KEY;
}
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
if (askModels) {
const { model } = await inquirer.prompt([
{
type: "select",
type: "list",
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(
const { embeddingModel } = await inquirer.prompt([
{
type: "select",
type: "list",
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);
}
@@ -118,7 +110,7 @@ async function getAvailableModelChoices(
)
.map((el: any) => {
return {
title: el.id,
name: el.id,
value: el.id,
};
});
+88 -37
View File
@@ -20,6 +20,7 @@ interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
}
const getAdditionalDependencies = (
@@ -36,28 +37,31 @@ const getAdditionalDependencies = (
case "mongo": {
dependencies.push({
name: "llama-index-vector-stores-mongodb",
version: "^0.3.1",
version: "^0.6.0",
});
break;
}
case "pg": {
dependencies.push({
name: "llama-index-vector-stores-postgres",
version: "^0.2.5",
version: "^0.3.2",
});
break;
}
case "pinecone": {
dependencies.push({
name: "llama-index-vector-stores-pinecone",
version: "^0.2.1",
version: "^0.4.1",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "milvus": {
dependencies.push({
name: "llama-index-vector-stores-milvus",
version: "^0.2.0",
version: "^0.3.0",
});
dependencies.push({
name: "pymilvus",
@@ -68,31 +72,40 @@ const getAdditionalDependencies = (
case "astra": {
dependencies.push({
name: "llama-index-vector-stores-astra-db",
version: "^0.2.0",
version: "^0.4.0",
});
break;
}
case "qdrant": {
dependencies.push({
name: "llama-index-vector-stores-qdrant",
version: "^0.3.0",
version: "^0.4.0",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "chroma": {
dependencies.push({
name: "llama-index-vector-stores-chroma",
version: "^0.2.0",
version: "^0.4.0",
});
break;
}
case "weaviate": {
dependencies.push({
name: "llama-index-vector-stores-weaviate",
version: "^1.1.1",
version: "^1.2.3",
});
break;
}
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.6.3",
});
break;
}
// Add data source dependencies
@@ -109,13 +122,13 @@ const getAdditionalDependencies = (
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: "^0.2.2",
version: "^0.3.0",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.2.0",
version: "^0.3.0",
});
dependencies.push({
name: "pymysql",
@@ -127,12 +140,6 @@ const getAdditionalDependencies = (
version: "^2.9.9",
});
break;
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.3.1",
});
break;
}
}
}
@@ -160,15 +167,15 @@ const getAdditionalDependencies = (
if (templateType !== "multiagent") {
dependencies.push({
name: "llama-index-llms-openai",
version: "^0.2.0",
version: "^0.3.2",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: "^0.2.3",
version: "^0.3.1",
});
dependencies.push({
name: "llama-index-agent-openai",
version: "^0.3.0",
version: "^0.4.0",
});
}
break;
@@ -234,6 +241,21 @@ const getAdditionalDependencies = (
version: "0.2.4",
});
break;
case "huggingface":
dependencies.push({
name: "llama-index-llms-huggingface",
version: "^0.3.5",
});
dependencies.push({
name: "llama-index-embeddings-huggingface",
version: "^0.3.1",
});
dependencies.push({
name: "optimum",
version: "^1.23.3",
extras: ["onnxruntime"],
});
break;
case "t-systems":
dependencies.push({
name: "llama-index-agent-openai",
@@ -264,14 +286,19 @@ const mergePoetryDependencies = (
value.version = dependency.version ?? value.version;
value.extras = dependency.extras ?? value.extras;
// Merge constraints if they exist
if (dependency.constraints) {
value = { ...value, ...dependency.constraints };
}
if (value.version === undefined) {
throw new Error(
`Dependency "${dependency.name}" is missing attribute "version"!`,
);
}
// Serialize separately only if extras are provided
if (value.extras && value.extras.length > 0) {
// Serialize as object if there are any additional properties
if (Object.keys(value).length > 1) {
existingDependencies[dependency.name] = value;
} else {
// Otherwise, serialize just the version string
@@ -353,31 +380,37 @@ export const installPythonDependencies = (
};
export const installPythonTemplate = async ({
appName,
root,
template,
framework,
vectorDb,
postInstallAction,
modelConfig,
dataSources,
tools,
postInstallAction,
useLlamaParse,
useCase,
observability,
modelConfig,
}: Pick<
InstallTemplateArgs,
| "appName"
| "root"
| "framework"
| "template"
| "framework"
| "vectorDb"
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "tools"
| "postInstallAction"
| "useLlamaParse"
| "useCase"
| "observability"
| "modelConfig"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "extractor") {
templatePath = path.join(templatesDir, "types", "extractor", framework);
if (template === "reflex") {
templatePath = path.join(templatesDir, "types", "reflex");
} else {
templatePath = path.join(templatesDir, "types", "streaming", framework);
}
@@ -447,15 +480,35 @@ export const installPythonTemplate = async ({
await copyRouterCode(root, tools ?? []);
}
// Copy multiagents overrides
if (template === "multiagent") {
// Copy multi-agent code
await copy("**", path.join(root), {
parents: true,
cwd: path.join(compPath, "multiagent", "python"),
rename: assetRelocator,
});
}
if (template === "multiagent" || template === "reflex") {
if (useCase) {
const sourcePath =
template === "multiagent"
? path.join(compPath, "agents", "python", useCase)
: path.join(compPath, "reflex", useCase);
await copy("**", path.join(root), {
parents: true,
cwd: sourcePath,
rename: assetRelocator,
});
} else {
console.log(
red(
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
@@ -477,7 +530,10 @@ export const installPythonTemplate = async ({
if (observability === "llamatrace") {
addOnDependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.2.1",
version: "^0.3.0",
constraints: {
python: ">=3.11,<3.13",
},
});
}
@@ -498,9 +554,4 @@ export const installPythonTemplate = async ({
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
// Copy deployment files for python
await copy("**", root, {
cwd: path.join(compPath, "deployments", "python"),
});
};
+46 -64
View File
@@ -1,40 +1,39 @@
import { ChildProcess, SpawnOptions, spawn } from "child_process";
import path from "path";
import { TemplateFramework } from "./types";
import { SpawnOptions, spawn } from "child_process";
import { TemplateFramework, TemplateType } from "./types";
const createProcess = (
command: string,
args: string[],
options: SpawnOptions,
) => {
return spawn(command, args, {
...options,
shell: true,
})
.on("exit", function (code) {
if (code !== 0) {
console.log(`Child process exited with code=${code}`);
process.exit(1);
}
): Promise<void> => {
return new Promise((resolve, reject) => {
spawn(command, args, {
...options,
shell: true,
})
.on("error", function (err) {
console.log("Error when running chill process: ", err);
process.exit(1);
});
.on("exit", function (code) {
if (code !== 0) {
console.log(`Child process exited with code=${code}`);
reject(code);
} else {
resolve();
}
})
.on("error", function (err) {
console.log("Error when running child process: ", err);
reject(err);
});
});
};
export function runReflexApp(
appPath: string,
frontendPort?: number,
backendPort?: number,
) {
const commandArgs = ["run", "reflex", "run"];
if (frontendPort) {
commandArgs.push("--frontend-port", frontendPort.toString());
}
if (backendPort) {
commandArgs.push("--backend-port", backendPort.toString());
}
export function runReflexApp(appPath: string, port: number) {
const commandArgs = [
"run",
"reflex",
"run",
"--frontend-port",
port.toString(),
];
return createProcess("poetry", commandArgs, {
stdio: "inherit",
cwd: appPath,
@@ -42,11 +41,10 @@ export function runReflexApp(
}
export function runFastAPIApp(appPath: string, port: number) {
const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port];
return createProcess("poetry", commandArgs, {
return createProcess("poetry", ["run", "dev"], {
stdio: "inherit",
cwd: appPath,
env: { ...process.env, APP_PORT: `${port}` },
});
}
@@ -60,40 +58,24 @@ export function runTSApp(appPath: string, port: number) {
export async function runApp(
appPath: string,
template: string,
frontend: boolean,
template: TemplateType,
framework: TemplateFramework,
port?: number,
externalPort?: number,
): Promise<any> {
const processes: ChildProcess[] = [];
): Promise<void> {
try {
// Start the app
const defaultPort =
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
// Callback to kill all sub processes if the main process is killed
process.on("exit", () => {
console.log("Killing app processes...");
processes.forEach((p) => p.kill());
});
// Default sub app paths
const backendPath = path.join(appPath, "backend");
const frontendPath = path.join(appPath, "frontend");
if (template === "extractor") {
processes.push(runReflexApp(appPath, port, externalPort));
const appRunner =
template === "reflex"
? runReflexApp
: framework === "fastapi"
? runFastAPIApp
: runTSApp;
await appRunner(appPath, port || defaultPort);
} catch (error) {
console.error("Failed to run app:", error);
throw error;
}
if (template === "streaming" || template === "multiagent") {
if (framework === "fastapi" || framework === "express") {
const backendRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
if (frontend) {
processes.push(backendRunner(backendPath, externalPort || 8000));
processes.push(runTSApp(frontendPath, port || 3000));
} else {
processes.push(backendRunner(appPath, externalPort || 8000));
}
} else if (framework === "nextjs") {
processes.push(runTSApp(appPath, port || 3000));
}
}
return Promise.all(processes);
}
+23 -35
View File
@@ -41,7 +41,7 @@ export const supportedTools: Tool[] = [
dependencies: [
{
name: "llama-index-tools-google",
version: "^0.2.0",
version: "^0.3.0",
},
],
supportedFrameworks: ["fastapi"],
@@ -62,17 +62,16 @@ export const supportedTools: Tool[] = [
dependencies: [
{
name: "duckduckgo-search",
version: "6.1.7",
version: "^6.3.5",
},
],
supportedFrameworks: ["fastapi", "nextjs", "express"],
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for DuckDuckGo search tool.",
value: `You are a DuckDuckGo search agent.
You can use the duckduckgo search tool to get information from the web to answer user questions.
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
},
],
@@ -83,18 +82,11 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "llama-index-tools-wikipedia",
version: "^0.2.0",
version: "^0.3.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LLAMAHUB,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for wiki tool.",
value: `You are a Wikipedia agent. You help users to get information from Wikipedia.`,
},
],
},
{
display: "Weather",
@@ -102,13 +94,6 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for weather tool.",
value: `You are a weather forecast agent. You help users to get the weather forecast for a given location.`,
},
],
},
{
display: "Document generator",
@@ -139,7 +124,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "e2b_code_interpreter",
version: "0.0.10",
version: "1.0.3",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
@@ -170,7 +155,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "e2b_code_interpreter",
version: "^0.0.11b38",
version: "1.0.3",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
@@ -211,14 +196,6 @@ For better results, you can specify the region parameter to get results from a s
},
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for openapi action tool.",
value:
"You are an OpenAPI action agent. You help users to make requests to the provided OpenAPI schema.",
},
],
},
{
display: "Image Generator",
@@ -231,11 +208,6 @@ For better results, you can specify the region parameter to get results from a s
description:
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for image generator tool.",
value: `You are an image generator agent. You help users to generate images using the Stability API.`,
},
],
},
{
@@ -267,6 +239,22 @@ For better results, you can specify the region parameter to get results from a s
},
],
},
{
display: "Form Filling",
name: "form_filling",
supportedFrameworks: ["fastapi"],
type: ToolType.LOCAL,
dependencies: [
{
name: "pandas",
version: "^2.2.3",
},
{
name: "tabulate",
version: "^0.9.0",
},
],
},
];
export const getTool = (toolName: string): Tool | undefined => {
+22 -8
View File
@@ -9,6 +9,7 @@ export type ModelProvider =
| "gemini"
| "mistral"
| "azure-openai"
| "huggingface"
| "t-systems";
export type ModelConfig = {
provider: ModelProvider;
@@ -19,11 +20,11 @@ export type ModelConfig = {
isConfigured(): boolean;
};
export type TemplateType =
| "extractor"
| "streaming"
| "community"
| "llamapack"
| "multiagent";
| "multiagent"
| "reflex";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
@@ -46,12 +47,25 @@ export type TemplateDataSource = {
type: TemplateDataSourceType;
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db" | "llamacloud";
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";
// Config for both file and folder
export type FileSourceConfig = {
path: string;
};
export type FileSourceConfig =
| {
path: string;
filename?: string;
}
| {
url: URL;
filename?: string;
};
export type WebSourceConfig = {
baseUrl?: string;
prefix?: string;
@@ -83,15 +97,15 @@ export interface InstallTemplateArgs {
framework: TemplateFramework;
ui: TemplateUI;
dataSources: TemplateDataSource[];
customApiPath?: string;
modelConfig: ModelConfig;
llamaCloudKey?: string;
useLlamaParse?: boolean;
communityProjectConfig?: CommunityProjectConfig;
llamapack?: string;
vectorDb?: TemplateVectorDB;
externalPort?: number;
port?: number;
postInstallAction?: TemplatePostInstallAction;
tools?: Tool[];
observability?: TemplateObservability;
useCase?: TemplateUseCase;
}
+38 -18
View File
@@ -1,7 +1,7 @@
import fs from "fs/promises";
import os from "os";
import path from "path";
import { bold, cyan, yellow } from "picocolors";
import { bold, cyan, red, yellow } from "picocolors";
import { assetRelocator, copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { templatesDir } from "./dir";
@@ -26,6 +26,7 @@ export const installTSTemplate = async ({
tools,
dataSources,
useLlamaParse,
useCase,
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
@@ -57,11 +58,9 @@ export const installTSTemplate = async ({
console.log("\nUsing static site generation\n");
} else {
if (vectorDb === "milvus") {
nextConfigJson.experimental.serverComponentsExternalPackages =
nextConfigJson.experimental.serverComponentsExternalPackages ?? [];
nextConfigJson.experimental.serverComponentsExternalPackages.push(
"@zilliz/milvus2-sdk-node",
);
nextConfigJson.serverExternalPackages =
nextConfigJson.serverExternalPackages ?? [];
nextConfigJson.serverExternalPackages.push("@zilliz/milvus2-sdk-node");
}
}
await fs.writeFile(
@@ -132,6 +131,34 @@ export const installTSTemplate = async ({
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), {
@@ -214,14 +241,12 @@ export const installTSTemplate = async ({
vectorDb,
});
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
if (
backend &&
(postInstallAction === "runApp" || postInstallAction === "dependencies")
) {
await installTSDependencies(packageJson, packageManager, isOnline);
}
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
};
async function updatePackageJson({
@@ -279,12 +304,7 @@ async function updatePackageJson({
"remark-gfm": undefined,
"remark-math": undefined,
"react-markdown": undefined,
"react-syntax-highlighter": undefined,
};
packageJson.devDependencies = {
...packageJson.devDependencies,
"@types/react-syntax-highlighter": undefined,
"highlight.js": undefined,
};
}
+29 -23
View File
@@ -1,40 +1,26 @@
import fs from "fs";
import path from "path";
import { assetRelocator, copy } from "./copy";
import { TemplateFramework } from "./types";
function renderDevcontainerContent(
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) {
const devcontainerJson: any = JSON.parse(
fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
);
// Modify postCreateCommand
if (frontend) {
devcontainerJson.postCreateCommand =
framework === "fastapi"
? "cd backend && poetry install && cd ../frontend && npm install"
: "cd backend && npm install && cd ../frontend && npm install";
} else {
devcontainerJson.postCreateCommand =
framework === "fastapi" ? "poetry install" : "npm install";
}
devcontainerJson.postCreateCommand =
framework === "fastapi" ? "poetry install" : "npm install";
// Modify containerEnv
if (framework === "fastapi") {
if (frontend) {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
};
} else {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
};
}
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
};
}
return JSON.stringify(devcontainerJson, null, 2);
@@ -44,7 +30,6 @@ export const writeDevcontainer = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) => {
const devcontainerDir = path.join(root, ".devcontainer");
if (fs.existsSync(devcontainerDir)) {
@@ -54,7 +39,6 @@ export const writeDevcontainer = async (
const devcontainerContent = renderDevcontainerContent(
templatesDir,
framework,
frontend,
);
fs.mkdirSync(devcontainerDir);
await fs.promises.writeFile(
@@ -62,3 +46,25 @@ export const writeDevcontainer = async (
devcontainerContent,
);
};
export const copyVSCodeSettings = async (
root: string,
templatesDir: string,
) => {
const vscodeDir = path.join(root, ".vscode");
await copy("vscode_settings.json", vscodeDir, {
cwd: templatesDir,
rename: assetRelocator,
});
};
export const configVSCode = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
) => {
await writeDevcontainer(root, templatesDir, framework);
if (framework === "fastapi") {
await copyVSCodeSettings(root, templatesDir);
}
};
+73 -115
View File
@@ -1,11 +1,10 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import Commander from "commander";
import Conf from "conf";
import { Command } from "commander";
import fs from "fs";
import inquirer from "inquirer";
import path from "path";
import { bold, cyan, green, red, yellow } from "picocolors";
import prompts from "prompts";
import terminalLink from "terminal-link";
import checkForUpdate from "update-check";
import { createApp } from "./create-app";
@@ -17,8 +16,8 @@ import { runApp } from "./helpers/run-app";
import { getTools } from "./helpers/tools";
import { validateNpmName } from "./helpers/validate-pkg";
import packageJson from "./package.json";
import { QuestionArgs, askQuestions, onPromptState } from "./questions";
import { askQuestions } from "./questions/index";
import { QuestionArgs } from "./questions/types";
// Run the initialization function
initializeGlobalAgent();
@@ -29,12 +28,14 @@ const handleSigTerm = () => process.exit(0);
process.on("SIGINT", handleSigTerm);
process.on("SIGTERM", handleSigTerm);
const program = new Commander.Command(packageJson.name)
const program = new Command(packageJson.name)
.version(packageJson.version)
.arguments("<project-directory>")
.usage(`${green("<project-directory>")} [options]`)
.arguments("[project-directory]")
.usage(`${green("[project-directory]")} [options]`)
.action((name) => {
projectPath = name;
if (name) {
projectPath = name;
}
})
.option(
"--use-npm",
@@ -55,13 +56,6 @@ const program = new Commander.Command(packageJson.name)
`
Explicitly tell the CLI to bootstrap the application using Yarn
`,
)
.option(
"--reset-preferences",
`
Explicitly tell the CLI to reset any stored preferences
`,
)
.option(
@@ -124,7 +118,14 @@ const program = new Commander.Command(packageJson.name)
"--frontend",
`
Whether to generate a frontend for your backend.
Generate a frontend for your backend.
`,
)
.option(
"--no-frontend",
`
Do not generate a frontend for your backend.
`,
)
.option(
@@ -132,13 +133,6 @@ const program = new Commander.Command(packageJson.name)
`
Select UI port.
`,
)
.option(
"--external-port <external>",
`
Select external port.
`,
)
.option(
@@ -161,6 +155,12 @@ const program = new Commander.Command(packageJson.name)
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 || tools === "none") {
return [];
}
return getTools(tools.split(","));
},
)
.option(
"--use-llama-parse",
@@ -189,109 +189,94 @@ const program = new Commander.Command(packageJson.name)
Allow interactive selection of LLM and embedding models of different model providers.
`,
false,
)
.option(
"--ask-examples",
"--pro",
`
Allow interactive selection of community templates and LlamaPacks.
Allow interactive selection of all features.
`,
false,
)
.option(
"--use-case <useCase>",
`
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
`,
)
.allowUnknownOption()
.parse(process.argv);
if (process.argv.includes("--no-frontend")) {
program.frontend = false;
}
if (process.argv.includes("--tools")) {
if (program.tools === "none") {
program.tools = [];
} else {
program.tools = getTools(program.tools.split(","));
}
}
const options = program.opts();
if (
process.argv.includes("--no-llama-parse") ||
program.template === "extractor"
options.template === "reflex"
) {
program.useLlamaParse = false;
options.useLlamaParse = false;
}
program.askModels = process.argv.includes("--ask-models");
program.askExamples = process.argv.includes("--ask-examples");
if (process.argv.includes("--no-files")) {
program.dataSources = [];
options.dataSources = [];
} else if (process.argv.includes("--example-file")) {
program.dataSources = getDataSources(program.files, program.exampleFile);
options.dataSources = getDataSources(options.files, options.exampleFile);
} else if (process.argv.includes("--llamacloud")) {
program.dataSources = [
{
type: "llamacloud",
config: {},
},
EXAMPLE_FILE,
];
options.dataSources = [EXAMPLE_FILE];
options.vectorDb = "llamacloud";
} else if (process.argv.includes("--web-source")) {
program.dataSources = [
options.dataSources = [
{
type: "web",
config: {
baseUrl: program.webSource,
prefix: program.webSource,
baseUrl: options.webSource,
prefix: options.webSource,
depth: 1,
},
},
];
} else if (process.argv.includes("--db-source")) {
program.dataSources = [
options.dataSources = [
{
type: "db",
config: {
uri: program.dbSource,
queries: program.dbQuery || "SELECT * FROM mytable",
uri: options.dbSource,
queries: options.dbQuery || "SELECT * FROM mytable",
},
},
];
}
const packageManager = !!program.useNpm
const packageManager = !!options.useNpm
? "npm"
: !!program.usePnpm
: !!options.usePnpm
? "pnpm"
: !!program.useYarn
: !!options.useYarn
? "yarn"
: getPkgManager();
async function run(): Promise<void> {
const conf = new Conf({ projectName: "create-llama" });
if (program.resetPreferences) {
conf.clear();
console.log(`Preferences reset successfully`);
return;
}
if (typeof projectPath === "string") {
projectPath = projectPath.trim();
}
if (!projectPath) {
const res = await prompts({
onState: onPromptState,
type: "text",
name: "path",
message: "What is your project named?",
initial: "my-app",
validate: (name) => {
const validation = validateNpmName(path.basename(path.resolve(name)));
if (validation.valid) {
return true;
}
return "Invalid project name: " + validation.problems![0];
const { path: projectPathAnswer } = await inquirer.prompt([
{
type: "input",
name: "path",
message: "What is your project named?",
default: "my-app",
validate(name) {
const validation = validateNpmName(path.basename(path.resolve(name)));
if (validation.valid) {
return true;
}
return "Invalid project name" + validation.problems![0];
},
},
});
if (typeof res.path === "string") {
projectPath = res.path.trim();
}
]);
projectPath = projectPathAnswer;
}
if (!projectPath) {
@@ -331,35 +316,15 @@ async function run(): Promise<void> {
process.exit(1);
}
const preferences = (conf.get("preferences") || {}) as QuestionArgs;
await askQuestions(
program as unknown as QuestionArgs,
preferences,
program.openAiKey,
);
const answers = await askQuestions(options as unknown as QuestionArgs);
await createApp({
template: program.template,
framework: program.framework,
ui: program.ui,
...answers,
appPath: resolvedProjectPath,
packageManager,
frontend: program.frontend,
modelConfig: program.modelConfig,
llamaCloudKey: program.llamaCloudKey,
communityProjectConfig: program.communityProjectConfig,
llamapack: program.llamapack,
vectorDb: program.vectorDb,
externalPort: program.externalPort,
postInstallAction: program.postInstallAction,
dataSources: program.dataSources,
tools: program.tools,
useLlamaParse: program.useLlamaParse,
observability: program.observability,
});
conf.set("preferences", preferences);
if (program.postInstallAction === "VSCode") {
if (answers.postInstallAction === "VSCode") {
console.log(`Starting VSCode in ${root}...`);
try {
execSync(`code . --new-window --goto README.md`, {
@@ -383,16 +348,9 @@ Please check ${cyan(
)} for more information.`,
);
}
} else if (program.postInstallAction === "runApp") {
} else if (answers.postInstallAction === "runApp") {
console.log(`Running app in ${root}...`);
await runApp(
root,
program.template,
program.frontend,
program.framework,
program.port,
program.externalPort,
);
await runApp(root, answers.template, answers.framework, options.port);
}
}
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.2.19",
"version": "0.3.27",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -43,14 +43,13 @@
"@types/cross-spawn": "6.0.0",
"@types/fs-extra": "11.0.4",
"@types/node": "^20.11.7",
"@types/prompts": "2.0.1",
"@types/inquirer": "^9.0.7",
"@types/tar": "6.1.5",
"@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": "2.20.0",
"conf": "10.2.0",
"commander": "12.1.0",
"cross-spawn": "7.0.3",
"fast-glob": "3.3.1",
"fs-extra": "11.2.0",
@@ -59,7 +58,7 @@
"ollama": "^0.5.0",
"ora": "^8.0.1",
"picocolors": "1.0.0",
"prompts": "2.1.0",
"inquirer": "^9.2.14",
"smol-toml": "^1.1.4",
"tar": "6.1.15",
"terminal-link": "^3.0.0",
+205 -173
View File
@@ -20,12 +20,12 @@ importers:
'@types/fs-extra':
specifier: 11.0.4
version: 11.0.4
'@types/inquirer':
specifier: ^9.0.7
version: 9.0.7
'@types/node':
specifier: ^20.11.7
version: 20.12.10
'@types/prompts':
specifier: 2.0.1
version: 2.0.1
'@types/tar':
specifier: 6.1.5
version: 6.1.5
@@ -42,11 +42,8 @@ importers:
specifier: github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540
version: https://codeload.github.com/watson/ci-info/tar.gz/f43f6a1cefff47fb361c88cf4b943fdbcaafe540
commander:
specifier: 2.20.0
version: 2.20.0
conf:
specifier: 10.2.0
version: 10.2.0
specifier: 12.1.0
version: 12.1.0
cross-spawn:
specifier: 7.0.3
version: 7.0.3
@@ -62,6 +59,9 @@ importers:
got:
specifier: 10.7.0
version: 10.7.0
inquirer:
specifier: ^9.2.14
version: 9.3.7
ollama:
specifier: ^0.5.0
version: 0.5.0
@@ -71,9 +71,6 @@ importers:
picocolors:
specifier: 1.0.0
version: 1.0.0
prompts:
specifier: 2.1.0
version: 2.1.0
smol-toml:
specifier: ^1.1.4
version: 1.1.4
@@ -226,6 +223,10 @@ packages:
'@humanwhocodes/object-schema@2.0.3':
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
'@inquirer/figures@1.0.9':
resolution: {integrity: sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==}
engines: {node: '>=18'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -283,6 +284,9 @@ packages:
'@types/http-cache-semantics@4.0.4':
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
'@types/inquirer@9.0.7':
resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==}
'@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
@@ -301,9 +305,6 @@ packages:
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
'@types/prompts@2.0.1':
resolution: {integrity: sha512-AhtMcmETelF8wFDV1ucbChKhLgsc+ytXZXkNz/nnTAMSDeqsjALknEFxi7ZtLgS/G8bV2rp90LhDW5SGACimIQ==}
'@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
@@ -316,6 +317,9 @@ packages:
'@types/tar@6.1.5':
resolution: {integrity: sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q==}
'@types/through@0.0.33':
resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==}
'@types/validate-npm-package-name@3.0.0':
resolution: {integrity: sha512-iFNNIrEaJH1lbPiyX+O/QyxSbKxrTjdNBVZGckt+iEL9So0hdZNBL68sOfHnt2txuUD8UJXvmKv/1DkgkebgUg==}
@@ -336,24 +340,17 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@8.13.0:
resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-escapes@5.0.0:
resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==}
engines: {node: '>=12'}
@@ -410,10 +407,6 @@ packages:
async-sema@3.0.1:
resolution: {integrity: sha512-fKT2riE8EHAvJEfLJXZiATQWqZttjx1+tfgnVshCDrH8vlw4YC8aECe0B8MU184g+aVRFVgmfxFlKZKaozSrNw==}
atomically@1.7.0:
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
engines: {node: '>=10.12.0'}
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -421,10 +414,16 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
boolean@3.2.0:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
@@ -441,6 +440,9 @@ packages:
breakword@1.0.6:
resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
builtins@1.0.3:
resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==}
@@ -495,6 +497,10 @@ packages:
resolution: {tarball: https://codeload.github.com/watson/ci-info/tar.gz/f43f6a1cefff47fb361c88cf4b943fdbcaafe540}
version: 2.0.0
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
cli-cursor@4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -503,6 +509,10 @@ packages:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
@@ -530,8 +540,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@2.20.0:
resolution: {integrity: sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
@@ -540,10 +551,6 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
conf@10.2.0:
resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==}
engines: {node: '>=12'}
cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
@@ -576,10 +583,6 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
debounce-fn@4.0.0:
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
engines: {node: '>=10'}
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -638,10 +641,6 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
dot-prop@6.0.1:
resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
engines: {node: '>=10'}
duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
@@ -664,10 +663,6 @@ packages:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -788,10 +783,6 @@ packages:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@@ -971,6 +962,9 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.1:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
engines: {node: '>= 4'}
@@ -996,6 +990,10 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
inquirer@9.3.7:
resolution: {integrity: sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==}
engines: {node: '>=18'}
internal-slot@1.0.7:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
@@ -1041,6 +1039,10 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
is-interactive@2.0.0:
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
engines: {node: '>=12'}
@@ -1057,10 +1059,6 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-obj@2.0.0:
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
engines: {node: '>=8'}
is-path-inside@3.0.3:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
@@ -1093,6 +1091,10 @@ packages:
resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
engines: {node: '>= 0.4'}
is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
is-unicode-supported@1.3.0:
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
engines: {node: '>=12'}
@@ -1138,12 +1140,6 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema-typed@7.0.3:
resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -1163,10 +1159,6 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
@@ -1182,10 +1174,6 @@ packages:
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
engines: {node: '>=6'}
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -1200,6 +1188,10 @@ packages:
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
log-symbols@6.0.0:
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
engines: {node: '>=18'}
@@ -1243,10 +1235,6 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mimic-fn@3.1.0:
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==}
engines: {node: '>=8'}
mimic-response@1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
@@ -1305,6 +1293,10 @@ packages:
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -1340,6 +1332,10 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
ora@8.0.1:
resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==}
engines: {node: '>=18'}
@@ -1375,10 +1371,6 @@ packages:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
@@ -1407,10 +1399,6 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1449,10 +1437,6 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
pkg-up@3.1.0:
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
engines: {node: '>=8'}
playwright-core@1.44.0:
resolution: {integrity: sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==}
engines: {node: '>=16'}
@@ -1498,10 +1482,6 @@ packages:
engines: {node: '>=14'}
hasBin: true
prompts@2.1.0:
resolution: {integrity: sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==}
engines: {node: '>= 6'}
pseudomap@1.0.2:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
@@ -1535,6 +1515,10 @@ packages:
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
engines: {node: '>=6'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@@ -1557,10 +1541,6 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
@@ -1579,6 +1559,10 @@ packages:
responselike@2.0.1:
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
restore-cursor@3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
restore-cursor@4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1604,9 +1588,16 @@ packages:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
run-async@3.0.0:
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
engines: {node: '>=0.12.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
safe-array-concat@1.1.2:
resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
engines: {node: '>=0.4'}
@@ -1675,9 +1666,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@@ -1742,6 +1730,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -1813,6 +1804,9 @@ packages:
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
engines: {node: '>=8'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tty-table@4.2.3:
resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==}
engines: {node: '>=8.0.0'}
@@ -1834,6 +1828,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
type-fest@0.6.0:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'}
@@ -1887,6 +1885,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@@ -1984,6 +1985,10 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
yoctocolors-cjs@2.1.2:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
snapshots:
'@babel/code-frame@7.24.2':
@@ -2187,6 +2192,8 @@ snapshots:
'@humanwhocodes/object-schema@2.0.3': {}
'@inquirer/figures@1.0.9': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -2261,6 +2268,11 @@ snapshots:
'@types/http-cache-semantics@4.0.4': {}
'@types/inquirer@9.0.7':
dependencies:
'@types/through': 0.0.33
rxjs: 7.8.1
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 20.12.10
@@ -2279,8 +2291,6 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
'@types/prompts@2.0.1': {}
'@types/responselike@1.0.3':
dependencies:
'@types/node': 20.12.10
@@ -2294,6 +2304,10 @@ snapshots:
'@types/node': 20.12.10
minipass: 4.2.8
'@types/through@0.0.33':
dependencies:
'@types/node': 20.12.10
'@types/validate-npm-package-name@3.0.0': {}
'@ungap/structured-clone@1.2.0': {}
@@ -2306,10 +2320,6 @@ snapshots:
acorn@8.11.3: {}
ajv-formats@2.1.1(ajv@8.13.0):
optionalDependencies:
ajv: 8.13.0
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -2317,15 +2327,12 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.13.0:
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
ansi-escapes@5.0.0:
dependencies:
type-fest: 1.4.0
@@ -2383,18 +2390,24 @@ snapshots:
async-sema@3.0.1: {}
atomically@1.7.0: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.0.0
balanced-match@1.0.2: {}
base64-js@1.5.1: {}
better-path-resolve@1.0.0:
dependencies:
is-windows: 1.0.2
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
boolean@3.2.0: {}
brace-expansion@1.1.11:
@@ -2414,6 +2427,11 @@ snapshots:
dependencies:
wcwidth: 1.0.1
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
builtins@1.0.3: {}
cacheable-lookup@2.0.1:
@@ -2470,12 +2488,18 @@ snapshots:
ci-info@https://codeload.github.com/watson/ci-info/tar.gz/f43f6a1cefff47fb361c88cf4b943fdbcaafe540: {}
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
cli-cursor@4.0.0:
dependencies:
restore-cursor: 4.0.0
cli-spinners@2.9.2: {}
cli-width@4.1.0: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
@@ -2506,25 +2530,12 @@ snapshots:
color-name@1.1.4: {}
commander@2.20.0: {}
commander@12.1.0: {}
commander@9.5.0: {}
concat-map@0.0.1: {}
conf@10.2.0:
dependencies:
ajv: 8.13.0
ajv-formats: 2.1.1(ajv@8.13.0)
atomically: 1.7.0
debounce-fn: 4.0.0
dot-prop: 6.0.1
env-paths: 2.2.1
json-schema-typed: 7.0.3
onetime: 5.1.2
pkg-up: 3.1.0
semver: 7.6.1
cross-spawn@5.1.0:
dependencies:
lru-cache: 4.1.5
@@ -2568,10 +2579,6 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
debounce-fn@4.0.0:
dependencies:
mimic-fn: 3.1.0
debug@4.3.4:
dependencies:
ms: 2.1.2
@@ -2621,10 +2628,6 @@ snapshots:
dependencies:
esutils: 2.0.3
dot-prop@6.0.1:
dependencies:
is-obj: 2.0.0
duplexer3@0.1.5: {}
eastasianwidth@0.2.0: {}
@@ -2644,8 +2647,6 @@ snapshots:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
env-paths@2.2.1: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@@ -2841,10 +2842,6 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
find-up@3.0.0:
dependencies:
locate-path: 3.0.0
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
@@ -3055,6 +3052,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.1: {}
import-fresh@3.3.0:
@@ -3075,6 +3074,21 @@ snapshots:
ini@1.3.8: {}
inquirer@9.3.7:
dependencies:
'@inquirer/figures': 1.0.9
ansi-escapes: 4.3.2
cli-width: 4.1.0
external-editor: 3.1.0
mute-stream: 1.0.0
ora: 5.4.1
run-async: 3.0.0
rxjs: 7.8.1
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.2
internal-slot@1.0.7:
dependencies:
es-errors: 1.3.0
@@ -3119,6 +3133,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-interactive@1.0.0: {}
is-interactive@2.0.0: {}
is-negative-zero@2.0.3: {}
@@ -3129,8 +3145,6 @@ snapshots:
is-number@7.0.0: {}
is-obj@2.0.0: {}
is-path-inside@3.0.3: {}
is-plain-obj@1.1.0: {}
@@ -3160,6 +3174,8 @@ snapshots:
dependencies:
which-typed-array: 1.1.15
is-unicode-supported@0.1.0: {}
is-unicode-supported@1.3.0: {}
is-unicode-supported@2.0.0: {}
@@ -3197,10 +3213,6 @@ snapshots:
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-schema-typed@7.0.3: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json-stringify-safe@5.0.1: {}
@@ -3221,8 +3233,6 @@ snapshots:
kind-of@6.0.3: {}
kleur@3.0.3: {}
kleur@4.1.5: {}
levn@0.4.1:
@@ -3239,11 +3249,6 @@ snapshots:
pify: 4.0.1
strip-bom: 3.0.0
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
path-exists: 3.0.0
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -3256,6 +3261,11 @@ snapshots:
lodash.startcase@4.4.0: {}
log-symbols@4.1.0:
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
log-symbols@6.0.0:
dependencies:
chalk: 5.3.0
@@ -3301,8 +3311,6 @@ snapshots:
mimic-fn@2.1.0: {}
mimic-fn@3.1.0: {}
mimic-response@1.0.1: {}
mimic-response@2.1.0: {}
@@ -3346,6 +3354,8 @@ snapshots:
ms@2.1.2: {}
mute-stream@1.0.0: {}
natural-compare@1.4.0: {}
normalize-package-data@2.5.0:
@@ -3389,6 +3399,18 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
ora@5.4.1:
dependencies:
bl: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.9.2
is-interactive: 1.0.0
is-unicode-supported: 0.1.0
log-symbols: 4.1.0
strip-ansi: 6.0.1
wcwidth: 1.0.1
ora@8.0.1:
dependencies:
chalk: 5.3.0
@@ -3425,10 +3447,6 @@ snapshots:
dependencies:
yocto-queue: 0.1.0
p-locate@3.0.0:
dependencies:
p-limit: 2.3.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
@@ -3456,8 +3474,6 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
path-exists@3.0.0: {}
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -3483,10 +3499,6 @@ snapshots:
dependencies:
find-up: 4.1.0
pkg-up@3.1.0:
dependencies:
find-up: 3.0.0
playwright-core@1.44.0: {}
playwright@1.44.0:
@@ -3515,11 +3527,6 @@ snapshots:
prettier@3.2.5: {}
prompts@2.1.0:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
pseudomap@1.0.2: {}
pump@3.0.0:
@@ -3560,6 +3567,12 @@ snapshots:
pify: 4.0.1
strip-bom: 3.0.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@@ -3585,8 +3598,6 @@ snapshots:
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
require-main-filename@2.0.0: {}
resolve-from@4.0.0: {}
@@ -3603,6 +3614,11 @@ snapshots:
dependencies:
lowercase-keys: 2.0.0
restore-cursor@3.1.0:
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
restore-cursor@4.0.0:
dependencies:
onetime: 5.1.2
@@ -3629,10 +3645,16 @@ snapshots:
semver-compare: 1.0.0
sprintf-js: 1.1.3
run-async@3.0.0: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
rxjs@7.8.1:
dependencies:
tslib: 2.8.1
safe-array-concat@1.1.2:
dependencies:
call-bind: 1.0.7
@@ -3701,8 +3723,6 @@ snapshots:
signal-exit@4.1.0: {}
sisteransi@1.0.5: {}
slash@3.0.0: {}
smartwrap@2.0.2:
@@ -3782,6 +3802,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -3845,6 +3869,8 @@ snapshots:
trim-newlines@3.0.1: {}
tslib@2.8.1: {}
tty-table@4.2.3:
dependencies:
chalk: 4.1.2
@@ -3865,6 +3891,8 @@ snapshots:
type-fest@0.20.2: {}
type-fest@0.21.3: {}
type-fest@0.6.0: {}
type-fest@0.8.1: {}
@@ -3927,6 +3955,8 @@ snapshots:
dependencies:
punycode: 2.3.1
util-deprecate@1.0.2: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
@@ -4045,3 +4075,5 @@ snapshots:
yargs-parser: 21.1.1
yocto-queue@0.1.0: {}
yoctocolors-cjs@2.1.2: {}
-769
View File
@@ -1,769 +0,0 @@
import { execSync } from "child_process";
import ciInfo from "ci-info";
import fs from "fs";
import path from "path";
import { blue, green, red } from "picocolors";
import prompts from "prompts";
import { InstallAppArgs } from "./create-app";
import {
TemplateDataSource,
TemplateDataSourceType,
TemplateFramework,
TemplateType,
} from "./helpers";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
import { EXAMPLE_FILE } from "./helpers/datasources";
import { templatesDir } from "./helpers/dir";
import { getAvailableLlamapackOptions } from "./helpers/llama-pack";
import { askModelConfig } from "./helpers/providers";
import { getProjectOptions } from "./helpers/repo";
import {
supportedTools,
toolRequiresConfig,
toolsRequireConfig,
} from "./helpers/tools";
export type QuestionArgs = Omit<
InstallAppArgs,
"appPath" | "packageManager"
> & {
askModels?: boolean;
askExamples?: boolean;
};
const supportedContextFileTypes = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
];
const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.windows.forms
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$dialogResult = $folderBrowser.ShowDialog()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
{
$folderBrowser.SelectedPath
}
`;
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: "",
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
tools: [],
};
export const questionHandlers = {
onCancel: () => {
console.error("Exiting.");
process.exit(1);
},
};
const getVectorDbChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No, just store the data in the file system",
value: "none",
},
{ title: "MongoDB", value: "mongo" },
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
{ title: "ChromaDB", value: "chroma" },
{ title: "Weaviate", value: "weaviate" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
const compPath = path.join(templatesDir, "components");
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
const availableChoices = fs
.readdirSync(vectordbPath)
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
const displayedChoices = choices.filter((choice) =>
availableChoices.includes(choice.value),
);
return displayedChoices;
};
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
template?: TemplateType,
) => {
// If LlamaCloud is already selected, don't show any other options
if (selectedDataSource.find((s) => s.type === "llamacloud")) {
return [];
}
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 !== "extractor") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
if (!selectedDataSource.length && template !== "extractor") {
choices.push({
title: "Use managed index from LlamaCloud",
value: "llamacloud",
});
}
return choices;
};
const selectLocalContextData = async (type: TemplateDataSourceType) => {
try {
let selectedPath: string = "";
let execScript: string;
let execOpts: any = {};
switch (process.platform) {
case "win32": // Windows
execScript =
type === "file"
? WINDOWS_FILE_SELECTION_SCRIPT
: WINDOWS_FOLDER_SELECTION_SCRIPT;
execOpts = { shell: "powershell.exe" };
break;
case "darwin": // MacOS
execScript =
type === "file"
? MACOS_FILE_SELECTION_SCRIPT
: MACOS_FOLDER_SELECTION_SCRIPT;
break;
default: // Unsupported OS
console.log(red("Unsupported OS error!"));
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
),
);
process.exit(1);
}
}
return paths;
} catch (error) {
console.log(
red(
"Got an error when trying to select local context data! Please try again or select another data source option.",
),
);
process.exit(1);
}
};
export const onPromptState = (state: any) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write("\x1B[?25h");
process.stdout.write("\n");
process.exit(1);
}
};
export const askQuestions = async (
program: QuestionArgs,
preferences: QuestionArgs,
openAiKey?: string,
) => {
const getPrefOrDefault = <K extends keyof Omit<QuestionArgs, "modelConfig">>(
field: K,
): Omit<QuestionArgs, "modelConfig">[K] =>
preferences[field] ?? defaults[field];
// Ask for next action after installation
async function askPostInstallAction() {
if (program.postInstallAction === undefined) {
if (ciInfo.isCI) {
program.postInstallAction = getPrefOrDefault("postInstallAction");
} else {
const actionChoices = [
{
title: "Just generate code (~1 sec)",
value: "none",
},
{
title: "Start in VSCode (~1 sec)",
value: "VSCode",
},
{
title: "Generate code and install dependencies (~2 min)",
value: "dependencies",
},
];
const modelConfigured =
!program.llamapack && program.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = program.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(program.tools)
) {
actionChoices.push({
title:
"Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await prompts(
{
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: actionChoices,
initial: 1,
},
questionHandlers,
);
program.postInstallAction = action;
}
}
}
if (!program.template) {
if (ciInfo.isCI) {
program.template = getPrefOrDefault("template");
} else {
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: "Structured Extractor", value: "extractor" },
...(program.askExamples
? [
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
]
: []),
],
initial: 0,
},
questionHandlers,
);
program.template = template;
preferences.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;
preferences.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;
preferences.llamapack = llamapack;
await askPostInstallAction();
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "extractor") {
// Extractor 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 = preferences.framework = "fastapi";
}
if (!program.framework) {
if (ciInfo.isCI) {
program.framework = getPrefOrDefault("framework");
} else {
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;
preferences.framework = framework;
}
}
if (
(program.framework === "express" || 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) {
if (ciInfo.isCI) {
program.frontend = getPrefOrDefault("frontend");
} else {
const styledNextJS = blue("NextJS");
const styledBackend = green(
program.framework === "express"
? "Express "
: program.framework === "fastapi"
? "FastAPI (Python) "
: "",
);
const { frontend } = await prompts({
onState: onPromptState,
type: "toggle",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
initial: getPrefOrDefault("frontend"),
active: "Yes",
inactive: "No",
});
program.frontend = Boolean(frontend);
preferences.frontend = Boolean(frontend);
}
}
} else {
program.frontend = false;
}
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = defaults.ui;
}
}
if (!program.observability && program.template === "streaming") {
if (ciInfo.isCI) {
program.observability = getPrefOrDefault("observability");
} else {
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;
preferences.observability = observability;
}
}
if (!program.modelConfig) {
const modelConfig = await askModelConfig({
openAiKey,
askModels: program.askModels ?? false,
framework: program.framework,
});
program.modelConfig = modelConfig;
preferences.modelConfig = modelConfig;
}
if (!program.dataSources) {
if (ciInfo.isCI) {
program.dataSources = getPrefOrDefault("dataSources");
} else {
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;
}
case "llamacloud": {
program.dataSources.push({
type: "llamacloud",
config: {},
});
program.dataSources.push(EXAMPLE_FILE);
break;
}
}
}
}
}
const isUsingLlamaCloud = program.dataSources.some(
(ds) => ds.type === "llamacloud",
);
// Asking for LlamaParse if user selected file data source
if (isUsingLlamaCloud) {
// default to use LlamaParse if using LlamaCloud
program.useLlamaParse = preferences.useLlamaParse = true;
} else {
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
if (
program.useLlamaParse === undefined &&
program.template !== "extractor"
) {
// if already set useLlamaParse, don't ask again
if (program.dataSources.some((ds) => ds.type === "file")) {
if (ciInfo.isCI) {
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
} else {
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;
preferences.useLlamaParse = useLlamaParse;
}
}
}
}
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
if (isUsingLlamaCloud || program.useLlamaParse) {
if (!program.llamaCloudKey) {
// if already set, don't ask again
if (ciInfo.isCI) {
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
} else {
// 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 = preferences.llamaCloudKey =
llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
}
}
if (isUsingLlamaCloud) {
// When using a LlamaCloud index, don't ask for vector database and use code in `llamacloud` folder for vector database
const vectorDb = "llamacloud";
program.vectorDb = vectorDb;
preferences.vectorDb = vectorDb;
} else if (program.dataSources.length > 0 && !program.vectorDb) {
if (ciInfo.isCI) {
program.vectorDb = getPrefOrDefault("vectorDb");
} else {
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;
preferences.vectorDb = vectorDb;
}
}
if (
!program.tools &&
(program.template === "streaming" || program.template === "multiagent")
) {
if (ciInfo.isCI) {
program.tools = getPrefOrDefault("tools");
} else {
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;
preferences.tools = tools;
}
}
await askPostInstallAction();
};
export const toChoice = (value: string) => {
return { title: value, value };
};
+30
View File
@@ -0,0 +1,30 @@
import { askModelConfig } from "../helpers/providers";
import { QuestionArgs, QuestionResults } from "./types";
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: "",
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,
}),
};
}
+64
View File
@@ -0,0 +1,64 @@
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;
};
+20
View File
@@ -0,0 +1,20 @@
import ciInfo from "ci-info";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
import { QuestionArgs, QuestionResults } from "./types";
export const isCI = ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1";
export const askQuestions = async (
args: QuestionArgs,
): Promise<QuestionResults> => {
if (isCI) {
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
await askProQuestions(args);
return args as unknown as QuestionResults;
}
return await askSimpleQuestions(args);
};
+431
View File
@@ -0,0 +1,431 @@
import inquirer, { QuestionCollection } from "inquirer";
import { blue } from "picocolors";
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, 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 inquirer.prompt([
{
type: "list",
name: "template",
message: "Which template would you like to use?",
choices: [
{ name: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
{
name: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ name: "Fullstack python template with Reflex", value: "reflex" },
{
name: `Community template from ${styledRepo}`,
value: "community",
},
{
name: "Example using a LlamaPack",
value: "llamapack",
},
],
},
]);
program.template = template;
}
if (program.template === "community") {
const projectOptions = await getProjectOptions(
COMMUNITY_OWNER,
COMMUNITY_REPO,
);
const { communityProjectConfig } = await inquirer.prompt([
{
type: "list",
name: "communityProjectConfig",
message: "Select community template",
choices: projectOptions.map(({ title, value }) => ({
name: title,
value: JSON.stringify(value), // serialize value to string in terminal
})),
},
]);
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 inquirer.prompt([
{
type: "list",
name: "llamapack",
message: "Select LlamaPack",
choices: availableLlamaPacks.map((pack) => ({
name: pack.name,
value: pack.folderPath,
})),
},
]);
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 inquirer.prompt([
{
type: "list",
name: "useCase",
message: "Which use case would you like to build?",
choices: [
{ name: "Structured Extractor", value: "extractor" },
{
name: "Contract review (using Workflow)",
value: "contract_review",
},
],
},
]);
program.useCase = useCase;
}
if (!program.framework) {
const choices = [
{ name: "NextJS", value: "nextjs" },
{ name: "Express", value: "express" },
{ name: "FastAPI (Python)", value: "fastapi" },
];
const { framework } = await inquirer.prompt([
{
type: "list",
name: "framework",
message: "Which framework would you like to use?",
choices,
},
]);
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 inquirer.prompt([
{
type: "confirm",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
default: false,
},
]);
program.frontend = 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 inquirer.prompt([
{
type: "list",
name: "observability",
message: "Would you like to set up observability?",
choices: [
{ name: "No", value: "none" },
...(program.framework === "fastapi"
? [{ name: "LlamaTrace", value: "llamatrace" }]
: []),
{ name: "Traceloop", value: "traceloop" },
],
},
]);
program.observability = observability;
}
if (
(program.template === "reflex" || program.template === "multiagent") &&
!program.useCase
) {
const choices =
program.template === "reflex"
? [
{ name: "Structured Extractor", value: "extractor" },
{
name: "Contract review (using Workflow)",
value: "contract_review",
},
]
: [
{
name: "Financial report (generate a financial report)",
value: "financial_report",
},
{
name: "Form filling (fill missing value in a CSV file)",
value: "form_filling",
},
{ name: "Blog writer (Write a blog post)", value: "blog" },
];
const { useCase } = await inquirer.prompt([
{
type: "list",
name: "useCase",
message: "Which use case would you like to use?",
choices,
},
]);
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 inquirer.prompt([
{
type: "list",
name: "vectorDb",
message: "Would you like to use a vector database?",
choices: getVectorDbChoices(program.framework),
},
]);
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 inquirer.prompt([
{
type: "list",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices,
},
]);
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 inquirer.prompt([
{
type: "input",
name: "baseUrl",
message: "Please provide base URL of the website: ",
default: "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;
},
},
]);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
break;
}
case "db": {
const dbPrompts: QuestionCollection[] = [
{
type: "input",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
default: "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;
},
},
{
type: "input",
name: "queries",
message: "Please enter the SQL query to fetch data:",
default: "SELECT * FROM mytable",
when: (answers: any) => !!answers.uri,
},
];
program.dataSources.push({
type: "db",
config: await inquirer.prompt(dbPrompts),
});
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 inquirer.prompt([
{
type: "confirm",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
default: false,
},
]);
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 inquirer.prompt([
{
type: "input",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
]);
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) => ({
name: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
value: tool.name,
}));
const { toolsName } = await inquirer.prompt([
{
type: "checkbox",
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);
}
};
+225
View File
@@ -0,0 +1,225 @@
import inquirer from "inquirer";
import {
AI_REPORTS,
EXAMPLE_10K_SEC_FILES,
EXAMPLE_FILE,
EXAMPLE_GDPR,
} from "../helpers/datasources";
import { askModelConfig } from "../helpers/providers";
import { getTools } from "../helpers/tools";
import { ModelConfig, TemplateFramework } from "../helpers/types";
import { PureQuestionArgs, QuestionResults } from "./types";
import { askPostInstallAction } from "./utils";
type AppType =
| "rag"
| "code_artifact"
| "financial_report_agent"
| "form_filling"
| "extractor"
| "contract_review"
| "data_scientist"
| "deep_research";
type SimpleAnswers = {
appType: AppType;
language: TemplateFramework;
useLlamaCloud: boolean;
llamaCloudKey?: string;
};
export const askSimpleQuestions = async (
args: PureQuestionArgs,
): Promise<QuestionResults> => {
const { appType } = await inquirer.prompt([
{
type: "list",
name: "appType",
message: "What app do you want to build?",
pageSize: Infinity,
choices: [
new inquirer.Separator("Agents"),
{ name: " Agentic RAG", value: "rag" },
{ name: " Data Scientist", value: "data_scientist" },
{ name: " Code Artifact Agent", value: "code_artifact" },
{ name: " Information Extractor", value: "extractor" },
new inquirer.Separator("Agentic Document Workflows"),
{
name: " Financial Report Generator",
value: "financial_report_agent",
},
{
name: " Financial 10k SEC Form Filler",
value: "form_filling",
},
{
name: " Contract Review",
value: "contract_review",
},
{
name: " Deep Researcher",
value: "deep_research",
},
],
},
]);
let language: TemplateFramework = "fastapi";
let llamaCloudKey = args.llamaCloudKey;
let useLlamaCloud = false;
if (
appType !== "extractor" &&
appType !== "contract_review" &&
appType !== "deep_research"
) {
const { language: newLanguage } = await inquirer.prompt([
{
type: "list",
name: "language",
message: "What language do you want to use?",
choices: [
{ name: "Python (FastAPI)", value: "fastapi" },
{ name: "Typescript (NextJS)", value: "nextjs" },
],
},
]);
language = newLanguage;
}
const { useLlamaCloud: newUseLlamaCloud } = await inquirer.prompt([
{
type: "confirm",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud services?",
suffix: " (see https://www.llamaindex.ai/enterprise for more info)",
default: false,
},
]);
useLlamaCloud = newUseLlamaCloud;
if (useLlamaCloud && !llamaCloudKey) {
// Ask for LlamaCloud API key, if not set
const { llamaCloudKey: newLlamaCloudKey } = await inquirer.prompt([
{
type: "input",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
]);
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_GPT4o: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4o",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
return !!args.openAiKey;
},
};
const lookup: Record<
AppType,
Pick<
QuestionResults,
"template" | "tools" | "frontend" | "dataSources" | "useCase"
> & {
modelConfig?: ModelConfig;
}
> = {
rag: {
template: "streaming",
tools: getTools(["weather"]),
frontend: true,
dataSources: [EXAMPLE_FILE],
},
data_scientist: {
template: "streaming",
tools: getTools(["interpreter", "document_generator"]),
frontend: true,
dataSources: [],
modelConfig: MODEL_GPT4o,
},
code_artifact: {
template: "streaming",
tools: getTools(["artifact"]),
frontend: true,
dataSources: [],
modelConfig: MODEL_GPT4o,
},
financial_report_agent: {
template: "multiagent",
useCase: "financial_report",
tools: getTools(["document_generator", "interpreter"]),
dataSources: EXAMPLE_10K_SEC_FILES,
frontend: true,
modelConfig: MODEL_GPT4o,
},
form_filling: {
template: "multiagent",
useCase: "form_filling",
tools: getTools(["form_filling"]),
dataSources: EXAMPLE_10K_SEC_FILES,
frontend: true,
modelConfig: MODEL_GPT4o,
},
extractor: {
template: "reflex",
useCase: "extractor",
tools: [],
frontend: false,
dataSources: [EXAMPLE_FILE],
},
contract_review: {
template: "reflex",
useCase: "contract_review",
tools: [],
frontend: false,
dataSources: [EXAMPLE_GDPR],
},
deep_research: {
template: "multiagent",
useCase: "deep_research",
tools: [],
frontend: true,
dataSources: [AI_REPORTS],
},
};
const results = lookup[answers.appType];
return {
framework: answers.language,
ui: "shadcn",
llamaCloudKey: answers.llamaCloudKey,
useLlamaParse: answers.useLlamaCloud,
llamapack: "",
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
observability: "none",
...results,
modelConfig:
results.modelConfig ??
(await askModelConfig({
openAiKey: args.openAiKey,
askModels: args.askModels ?? false,
framework: answers.language,
})),
frontend: answers.language === "nextjs" ? false : results.frontend,
};
};
+36
View File
@@ -0,0 +1,36 @@
import fs from "fs";
import path from "path";
import { TemplateFramework } from "../helpers";
import { templatesDir } from "../helpers/dir";
export const getVectorDbChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No, just store the data in the file system",
value: "none",
},
{ title: "MongoDB", value: "mongo" },
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
{ title: "ChromaDB", value: "chroma" },
{ title: "Weaviate", value: "weaviate" },
{ title: "LlamaCloud (use Managed Index)", value: "llamacloud" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
const compPath = path.join(templatesDir, "components");
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
const availableChoices = fs
.readdirSync(vectordbPath)
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
const displayedChoices = choices.filter((choice) =>
availableChoices.includes(choice.value),
);
return displayedChoices;
};
+15
View File
@@ -0,0 +1,15 @@
import { InstallAppArgs } from "../create-app";
export type QuestionResults = Omit<
InstallAppArgs,
"appPath" | "packageManager"
>;
export type PureQuestionArgs = {
askModels?: boolean;
pro?: boolean;
openAiKey?: string;
llamaCloudKey?: string;
};
export type QuestionArgs = QuestionResults & PureQuestionArgs;
+175
View File
@@ -0,0 +1,175 @@
import { execSync } from "child_process";
import fs from "fs";
import inquirer from "inquirer";
import path from "path";
import { red } from "picocolors";
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
import { toolsRequireConfig } from "../helpers/tools";
import { QuestionResults } from "./types";
export const supportedContextFileTypes = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
];
const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.windows.forms
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$dialogResult = $folderBrowser.ShowDialog()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
{
$folderBrowser.SelectedPath
}
`;
export const selectLocalContextData = async (type: TemplateDataSourceType) => {
try {
let selectedPath: string = "";
let execScript: string;
let execOpts: any = {};
switch (process.platform) {
case "win32": // Windows
execScript =
type === "file"
? WINDOWS_FILE_SELECTION_SCRIPT
: WINDOWS_FOLDER_SELECTION_SCRIPT;
execOpts = { shell: "powershell.exe" };
break;
case "darwin": // MacOS
execScript =
type === "file"
? MACOS_FILE_SELECTION_SCRIPT
: MACOS_FOLDER_SELECTION_SCRIPT;
break;
default: // Unsupported OS
console.log(red("Unsupported OS error!"));
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
),
);
process.exit(1);
}
}
return paths;
} catch (error) {
console.log(
red(
"Got an error when trying to select local context data! Please try again or select another data source option.",
),
);
process.exit(1);
}
};
export const onPromptState = (state: any) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write("\x1B[?25h");
process.stdout.write("\n");
process.exit(1);
}
};
export const toChoice = (value: string) => {
return { name: value, value };
};
export const questionHandlers = {
onCancel: () => {
console.error("Exiting.");
process.exit(1);
},
};
// Ask for next action after installation
export async function askPostInstallAction(
args: QuestionResults,
): Promise<TemplatePostInstallAction> {
const actionChoices = [
{
name: "Just generate code (~1 sec)",
value: "none",
},
{
name: "Start in VSCode (~1 sec)",
value: "VSCode",
},
{
name: "Generate code and install dependencies (~2 min)",
value: "dependencies",
},
];
const modelConfigured = !args.llamapack && 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)
) {
actionChoices.push({
name: "Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "How would you like to proceed?",
choices: actionChoices,
},
]);
return action;
}
-3
View File
@@ -1,3 +0,0 @@
__pycache__
poetry.lock
storage
-18
View File
@@ -1,18 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, startup the backend as described in the [backend README](./backend/README.md).
Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md).
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -1,5 +1,3 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Overview
This example is using three agents to generate a blog post:
@@ -10,9 +8,9 @@ This example is using three agents to generate a blog post:
There are three different methods how the agents can interact to reach their goal:
1. [Choreography](./app/examples/choreography.py) - the agents decide themselves to delegate a task to another agent
1. [Orchestrator](./app/examples/orchestrator.py) - a central orchestrator decides which agent should execute a task
1. [Explicit Workflow](./app/examples/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks
1. [Choreography](./app/agents/choreography.py) - the agents decide themselves to delegate a task to another agent
1. [Orchestrator](./app/agents/orchestrator.py) - a central orchestrator decides which agent should execute a task
1. [Explicit Workflow](./app/agents/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks
## Getting Started
@@ -25,7 +23,6 @@ poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
Second, generate the embeddings of the documents in the `./data` directory:
```shell
@@ -35,11 +32,10 @@ poetry run generate
Third, run the development server:
```shell
poetry run python main.py
poetry run dev
```
Per default, the example is using the explicit workflow. You can change the example by setting the `EXAMPLE_TYPE` environment variable to `choreography` or `orchestrator`.
The example provides one streaming API endpoint `/api/chat`.
You can test the endpoint with the following curl request:
@@ -51,19 +47,22 @@ curl --location 'localhost:8000/api/chat' \
You can start editing the API by modifying `app/api/routers/chat.py` or `app/examples/workflow.py`. The API auto-updates as you save the files.
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
To start the app optimized for **production**, run:
```
ENVIRONMENT=prod poetry run python main.py
poetry run prod
```
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,10 +1,10 @@
from textwrap import dedent
from typing import List, Optional
from app.agents.multi import AgentCallingAgent
from app.agents.single import FunctionCallingAgent
from app.examples.publisher import create_publisher
from app.examples.researcher import create_researcher
from app.agents.publisher import create_publisher
from app.agents.researcher import create_researcher
from app.workflows.multi import AgentCallingAgent
from app.workflows.single import FunctionCallingAgent
from llama_index.core.chat_engine.types import ChatMessage
@@ -1,10 +1,10 @@
from textwrap import dedent
from typing import List, Optional
from app.agents.multi import AgentOrchestrator
from app.agents.single import FunctionCallingAgent
from app.examples.publisher import create_publisher
from app.examples.researcher import create_researcher
from app.agents.publisher import create_publisher
from app.agents.researcher import create_researcher
from app.workflows.multi import AgentOrchestrator
from app.workflows.single import FunctionCallingAgent
from llama_index.core.chat_engine.types import ChatMessage
@@ -1,8 +1,8 @@
from textwrap import dedent
from typing import List, Tuple
from app.agents.single import FunctionCallingAgent
from app.engine.tools import ToolFactory
from app.workflows.single import FunctionCallingAgent
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.tools import FunctionTool
@@ -11,11 +11,11 @@ def get_publisher_tools() -> Tuple[List[FunctionTool], str, str]:
tools = []
# Get configured tools from the tools.yaml file
configured_tools = ToolFactory.from_env(map_result=True)
if "document_generator" in configured_tools.keys():
tools.extend(configured_tools["document_generator"])
if "generate_document" in configured_tools.keys():
tools.append(configured_tools["generate_document"])
prompt_instructions = dedent("""
Normally, reply the blog post content to the user directly.
But if user requested to generate a file, use the document_generator tool to generate the file and reply the link to the file.
But if user requested to generate a file, use the generate_document tool to generate the file and reply the link to the file.
""")
description = "Expert in publishing the blog post, able to publish the blog post in PDF or HTML format."
else:
@@ -1,52 +1,37 @@
import os
from textwrap import dedent
from typing import List
from app.agents.single import FunctionCallingAgent
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.workflows.single import FunctionCallingAgent
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from app.engine.tools.query_engine import get_query_engine_tool
def _create_query_engine_tool(params=None) -> QueryEngineTool:
"""
Provide an agent worker that can be used to query the index.
"""
# Add query tool if index exists
index_config = IndexConfig(**(params or {}))
index = get_index(index_config)
if index is None:
return None
top_k = int(os.getenv("TOP_K", 0))
query_engine = index.as_query_engine(
**({"similarity_top_k": top_k} if top_k != 0 else {})
)
return QueryEngineTool(
query_engine=query_engine,
metadata=ToolMetadata(
name="query_index",
description="""
Use this tool to retrieve information about the text corpus from the index.
""",
),
)
def _get_research_tools(**kwargs) -> QueryEngineTool:
def _get_research_tools(**kwargs):
"""
Researcher take responsibility for retrieving information.
Try init wikipedia or duckduckgo tool if available.
"""
tools = []
query_engine_tool = _create_query_engine_tool(**kwargs)
if query_engine_tool is not None:
tools.append(query_engine_tool)
researcher_tool_names = ["duckduckgo", "wikipedia.WikipediaToolSpec"]
# Create query engine tool
index_config = IndexConfig(**kwargs)
index = get_index(index_config)
if index is not None:
query_engine_tool = get_query_engine_tool(index=index)
if query_engine_tool is not None:
tools.append(query_engine_tool)
# Create duckduckgo tool
researcher_tool_names = [
"duckduckgo_search",
"duckduckgo_image_search",
"wikipedia.WikipediaToolSpec",
]
configured_tools = ToolFactory.from_env(map_result=True)
for tool_name, tool in configured_tools.items():
if tool_name in researcher_tool_names:
tools.extend(tool)
tools.append(tool)
return tools
@@ -1,9 +1,9 @@
from textwrap import dedent
from typing import AsyncGenerator, List, Optional
from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
from app.examples.publisher import create_publisher
from app.examples.researcher import create_researcher
from app.agents.publisher import create_publisher
from app.agents.researcher import create_researcher
from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.prompts import PromptTemplate
from llama_index.core.settings import Settings
@@ -238,7 +238,9 @@ class BlogPostWorkflow(Workflow):
publisher: FunctionCallingAgent,
) -> StopEvent:
try:
result: AgentRunResult = await self.run_agent(ctx, publisher, ev.input)
result: AgentRunResult = await self.run_agent(
ctx, publisher, ev.input, streaming=ctx.data["streaming"]
)
return StopEvent(result=result)
except Exception as e:
ctx.write_event_to_stream(
@@ -0,0 +1,3 @@
from .blog import create_workflow
__all__ = ["create_workflow"]
@@ -2,19 +2,20 @@ import logging
import os
from typing import List, Optional
from app.examples.choreography import create_choreography
from app.examples.orchestrator import create_orchestrator
from app.examples.workflow import create_workflow
from app.agents.choreography import create_choreography
from app.agents.orchestrator import create_orchestrator
from app.agents.workflow import create_workflow as create_blog_workflow
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.workflow import Workflow
logger = logging.getLogger("uvicorn")
def get_chat_engine(
def create_workflow(
chat_history: Optional[List[ChatMessage]] = None, **kwargs
) -> Workflow:
# TODO: the EXAMPLE_TYPE could be passed as a chat config parameter?
# Chat filters are not supported yet
kwargs.pop("filters", None)
agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
match agent_type:
case "choreography":
@@ -22,7 +23,7 @@ def get_chat_engine(
case "orchestrator":
agent = create_orchestrator(chat_history, **kwargs)
case _:
agent = create_workflow(chat_history, **kwargs)
agent = create_blog_workflow(chat_history, **kwargs)
logger.info(f"Using agent pattern: {agent_type}")
@@ -1,7 +1,7 @@
from typing import Any, List
from app.agents.planner import StructuredPlannerAgent
from app.agents.single import (
from app.workflows.planner import StructuredPlannerAgent
from app.workflows.single import (
AgentRunResult,
ContextAwareTool,
FunctionCallingAgent,
@@ -2,7 +2,7 @@ import uuid
from enum import Enum
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
from llama_index.core.agent.runner.planner import (
DEFAULT_INITIAL_PLAN_PROMPT,
DEFAULT_PLAN_REFINE_PROMPT,
@@ -317,7 +317,7 @@ class Planner:
# gather completed sub-tasks and response pairs
completed_outputs_str = ""
for sub_task_name, task_output in completed_sub_task.items():
task_str = f"{sub_task_name}:\n" f"\t{task_output!s}\n"
task_str = f"{sub_task_name}:\n\t{task_output!s}\n"
completed_outputs_str += task_str
# get a string for the remaining sub-tasks
@@ -1,4 +1,5 @@
from abc import abstractmethod
from enum import Enum
from typing import Any, AsyncGenerator, List, Optional
from llama_index.core.llms import ChatMessage, ChatResponse
@@ -15,7 +16,7 @@ from llama_index.core.workflow import (
Workflow,
step,
)
from pydantic import BaseModel
from pydantic import BaseModel, Field
class InputEvent(Event):
@@ -26,17 +27,27 @@ class ToolCallEvent(Event):
tool_calls: list[ToolSelection]
class AgentRunEventType(Enum):
TEXT = "text"
PROGRESS = "progress"
class AgentRunEvent(Event):
name: str
_msg: str
msg: str
event_type: AgentRunEventType = Field(default=AgentRunEventType.TEXT)
data: Optional[dict] = None
@property
def msg(self):
return self._msg
@msg.setter
def msg(self, value):
self._msg = value
def to_response(self) -> dict:
return {
"type": "agent",
"data": {
"agent": self.name,
"type": self.event_type.value,
"text": self.msg,
"data": self.data,
},
}
class AgentRunResult(BaseModel):
@@ -0,0 +1,47 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
Second, generate the embeddings of the documents in the `./data` directory:
```shell
poetry run generate
```
Third, run the development server:
```shell
poetry run dev
```
## Use Case: Deep Research over own documents
The workflow performs deep research by retrieving and analyzing documents from the [data](./data) directory from multiple perspectives. The project includes a sample PDF about AI investment in 2024 to help you get started. You can also add your own documents by placing them in the data directory and running the generate script again to index them.
After starting the server, go to [http://localhost:8000](http://localhost:8000) and send a message to the agent to write a blog post.
E.g: "AI investment in 2024"
To update the workflow, you can edit the [deep_research.py](./app/workflows/deep_research.py) file.
By default, the workflow retrieves 10 results from your documents. To customize the amount of information covered in the answer, you can adjust the `TOP_K` environment variable in the `.env` file. A higher value will retrieve more results from your documents, potentially providing more comprehensive answers.
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -0,0 +1,3 @@
from .deep_research import create_workflow
__all__ = ["create_workflow"]
@@ -0,0 +1,158 @@
from typing import List, Literal, Optional
from llama_index.core.base.llms.types import (
CompletionResponse,
CompletionResponseAsyncGen,
)
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
from llama_index.core.prompts import PromptTemplate
from llama_index.core.schema import MetadataMode, Node, NodeWithScore
from llama_index.core.settings import Settings
from pydantic import BaseModel, Field
class AnalysisDecision(BaseModel):
decision: Literal["research", "write", "cancel"] = Field(
description="Whether to continue research, write a report, or cancel the research after several retries"
)
research_questions: Optional[List[str]] = Field(
description="Questions to research if continuing research. Maximum 3 questions. Set to null or empty if writing a report.",
default_factory=list,
)
cancel_reason: Optional[str] = Field(
description="The reason for cancellation if the decision is to cancel research.",
default=None,
)
async def plan_research(
memory: SimpleComposableMemory,
context_nodes: List[Node],
user_request: str,
) -> AnalysisDecision:
analyze_prompt = PromptTemplate(
"""
You are a professor who is guiding a researcher to research a specific request/problem.
Your task is to decide on a research plan for the researcher.
The possible actions are:
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
The workflow should be:
+ Always begin by providing some initial questions for the researcher to investigate.
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
<User request>
{user_request}
</User request>
<Collected information>
{context_str}
</Collected information>
<Conversation context>
{conversation_context}
</Conversation context>
"""
)
conversation_context = "\n".join(
[f"{message.role}: {message.content}" for message in memory.get_all()]
)
context_str = "\n".join(
[node.get_content(metadata_mode=MetadataMode.LLM) for node in context_nodes]
)
res = await Settings.llm.astructured_predict(
output_cls=AnalysisDecision,
prompt=analyze_prompt,
user_request=user_request,
context_str=context_str,
conversation_context=conversation_context,
)
return res
async def research(
question: str,
context_nodes: List[NodeWithScore],
) -> str:
prompt = """
You are a researcher who is in the process of answering the question.
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
Always add citations to the sentence/point/paragraph using the id of the provided content.
The citation should follow this format: [citation:id]() where id is the id of the content.
E.g:
If we have a context like this:
<Citation id='abc-xyz'>
Baby llama is called cria
</Citation id='abc-xyz'>
And your answer uses the content, then the citation should be:
- Baby llama is called cria [citation:abc-xyz]()
Here is the provided context for the question:
<Collected information>
{context_str}
</Collected information>`
No prior knowledge, just use the provided context to answer the question: {question}
"""
context_str = "\n".join(
[_get_text_node_content_for_citation(node) for node in context_nodes]
)
res = await Settings.llm.acomplete(
prompt=prompt.format(question=question, context_str=context_str),
)
return res.text
async def write_report(
memory: SimpleComposableMemory,
user_request: str,
stream: bool = False,
) -> CompletionResponse | CompletionResponseAsyncGen:
report_prompt = """
You are a researcher writing a report based on a user request and the research context.
You have researched various perspectives related to the user request.
The report should provide a comprehensive outline covering all important points from the researched perspectives.
Create a well-structured outline for the research report that covers all the answers.
# IMPORTANT when writing in markdown format:
+ Use tables or figures where appropriate to enhance presentation.
+ Preserve all citation syntax (the `[citation:id]()` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
+ Do not add links, a table of contents, or a references section to the report.
<User request>
{user_request}
</User request>
<Research context>
{research_context}
</Research context>
Now, write a report addressing the user request based on the research provided following the format and guidelines above.
"""
research_context = "\n".join(
[f"{message.role}: {message.content}" for message in memory.get_all()]
)
llm_complete_func = (
Settings.llm.astream_complete if stream else Settings.llm.acomplete
)
res = await llm_complete_func(
prompt=report_prompt.format(
user_request=user_request,
research_context=research_context,
),
)
return res
def _get_text_node_content_for_citation(node: NodeWithScore) -> str:
"""
Construct node content for LLM with citation flag.
"""
node_id = node.node.node_id
content = f"<Citation id='{node_id}'>\n{node.get_content(metadata_mode=MetadataMode.LLM)}</Citation id='{node_id}'>"
return content
@@ -0,0 +1,309 @@
import logging
import os
import uuid
from typing import Any, Dict, List, Optional
from llama_index.core.indices.base import BaseIndex
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
from llama_index.core.schema import Node
from llama_index.core.types import ChatMessage, MessageRole
from llama_index.core.workflow import (
Context,
StartEvent,
StopEvent,
Workflow,
step,
)
from app.engine.index import IndexConfig, get_index
from app.workflows.agents import plan_research, research, write_report
from app.workflows.events import SourceNodesEvent
from app.workflows.models import (
CollectAnswersEvent,
DataEvent,
PlanResearchEvent,
ReportEvent,
ResearchEvent,
)
logger = logging.getLogger("uvicorn")
logger.setLevel(logging.INFO)
def create_workflow(
chat_history: Optional[List[ChatMessage]] = None,
params: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Workflow:
index_config = IndexConfig(**params)
index = get_index(index_config)
if index is None:
raise ValueError(
"Index is not found. Try run generation script to create the index first."
)
return DeepResearchWorkflow(
index=index,
chat_history=chat_history,
timeout=120.0,
)
class DeepResearchWorkflow(Workflow):
"""
A workflow to research and analyze documents from multiple perspectives and write a comprehensive report.
Requirements:
- An indexed documents containing the knowledge base related to the topic
Steps:
1. Retrieve information from the knowledge base
2. Analyze the retrieved information and provide questions for answering
3. Answer the questions
4. Write the report based on the research results
"""
memory: SimpleComposableMemory
context_nodes: List[Node]
index: BaseIndex
user_request: str
stream: bool = True
def __init__(
self,
index: BaseIndex,
chat_history: Optional[List[ChatMessage]] = None,
stream: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.index = index
self.context_nodes = []
self.stream = stream
self.chat_history = chat_history
self.memory = SimpleComposableMemory.from_defaults(
primary_memory=ChatMemoryBuffer.from_defaults(
chat_history=chat_history,
),
)
@step
def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent:
"""
Initiate the workflow: memory, tools, agent
"""
self.user_request = ev.get("input")
self.memory.put_messages(
messages=[
ChatMessage(
role=MessageRole.USER,
content=self.user_request,
)
]
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "inprogress",
},
)
)
retriever = self.index.as_retriever(
similarity_top_k=int(os.getenv("TOP_K", 10)),
)
nodes = retriever.retrieve(self.user_request)
self.context_nodes.extend(nodes)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "done",
},
)
)
# Send source nodes to the stream
# Use SourceNodesEvent to display source nodes in the UI.
ctx.write_event_to_stream(
SourceNodesEvent(
nodes=nodes,
)
)
return PlanResearchEvent(
context_nodes=self.context_nodes,
)
@step
async def analyze(
self, ctx: Context, ev: PlanResearchEvent
) -> ResearchEvent | ReportEvent | StopEvent:
"""
Analyze the retrieved information
"""
logger.info("Analyzing the retrieved information")
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "inprogress",
},
)
)
res = await plan_research(
memory=self.memory,
context_nodes=self.context_nodes,
user_request=self.user_request,
)
if res.decision == "cancel":
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return StopEvent(
result=res.cancel_reason,
)
elif res.decision == "write":
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="No more idea to analyze. We should report the answers.",
)
)
ctx.send_event(ReportEvent())
else:
await ctx.set("n_questions", len(res.research_questions))
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="We need to find answers to the following questions:\n"
+ "\n".join(res.research_questions),
)
)
for question in res.research_questions:
question_id = str(uuid.uuid4())
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "pending",
"id": question_id,
"question": question,
"answer": None,
},
)
)
ctx.send_event(
ResearchEvent(
question_id=question_id,
question=question,
context_nodes=self.context_nodes,
)
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return None
@step(num_workers=2)
async def answer(self, ctx: Context, ev: ResearchEvent) -> CollectAnswersEvent:
"""
Answer the question
"""
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "inprogress",
"id": ev.question_id,
"question": ev.question,
},
)
)
try:
answer = await research(
context_nodes=ev.context_nodes,
question=ev.question,
)
except Exception as e:
logger.error(f"Error answering question {ev.question}: {e}")
answer = f"Got error when answering the question: {ev.question}"
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "done",
"id": ev.question_id,
"question": ev.question,
"answer": answer,
},
)
)
return CollectAnswersEvent(
question_id=ev.question_id,
question=ev.question,
answer=answer,
)
@step
async def collect_answers(
self, ctx: Context, ev: CollectAnswersEvent
) -> PlanResearchEvent:
"""
Collect answers to all questions
"""
num_questions = await ctx.get("n_questions")
results = ctx.collect_events(
ev,
expected=[CollectAnswersEvent] * num_questions,
)
if results is None:
return None
for result in results:
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content=f"<Question>{result.question}</Question>\n<Answer>{result.answer}</Answer>",
)
)
await ctx.set("n_questions", 0)
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="Researched all the questions. Now, i need to analyze if it's ready to write a report or need to research more.",
)
)
return PlanResearchEvent()
@step
async def report(self, ctx: Context, ev: ReportEvent) -> StopEvent:
"""
Report the answers
"""
logger.info("Writing the report")
res = await write_report(
memory=self.memory,
user_request=self.user_request,
stream=self.stream,
)
return StopEvent(
result=res,
)
@@ -0,0 +1,43 @@
from typing import List, Literal, Optional
from llama_index.core.schema import NodeWithScore
from llama_index.core.workflow import Event
from pydantic import BaseModel
# Workflow events
class PlanResearchEvent(Event):
pass
class ResearchEvent(Event):
question_id: str
question: str
context_nodes: List[NodeWithScore]
class CollectAnswersEvent(Event):
question_id: str
question: str
answer: str
class ReportEvent(Event):
pass
# Events that are streamed to the frontend and rendered there
class DeepResearchEventData(BaseModel):
event: Literal["retrieve", "analyze", "answer"]
state: Literal["pending", "inprogress", "done", "error"]
id: Optional[str] = None
question: Optional[str] = None
answer: Optional[str] = None
class DataEvent(Event):
type: Literal["deep_research_event"]
data: DeepResearchEventData
def to_response(self):
return self.model_dump()
@@ -0,0 +1,57 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider and `E2B_API_KEY` for the [E2B's code interpreter tool](https://e2b.dev/docs)).
Second, generate the embeddings of the documents in the `./data` directory:
```shell
poetry run generate
```
Third, run the development server:
```shell
poetry run dev
```
The example provides one streaming API endpoint `/api/chat`.
You can test the endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
```
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/financial_report.py`. The API auto-updates as you save the files.
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
To start the app optimized for **production**, run:
```
poetry run prod
```
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -0,0 +1,3 @@
from .financial_report import create_workflow
__all__ = ["create_workflow"]
@@ -0,0 +1,297 @@
from typing import Any, Dict, List, Optional
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
from app.workflows.events import AgentRunEvent
from app.workflows.tools import (
call_tools,
chat_with_tools,
)
from llama_index.core import Settings
from llama_index.core.base.llms.types import ChatMessage, MessageRole
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
def create_workflow(
chat_history: Optional[List[ChatMessage]] = None,
params: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Workflow:
# Create query engine tool
index_config = IndexConfig(**params)
index = get_index(index_config)
if index is None:
raise ValueError(
"Index is not found. Try run generation script to create the index first."
)
query_engine_tool = get_query_engine_tool(index=index)
configured_tools: Dict[str, FunctionTool] = ToolFactory.from_env(map_result=True) # type: ignore
code_interpreter_tool = configured_tools.get("interpret")
document_generator_tool = configured_tools.get("generate_document")
return FinancialReportWorkflow(
query_engine_tool=query_engine_tool,
code_interpreter_tool=code_interpreter_tool,
document_generator_tool=document_generator_tool,
chat_history=chat_history,
)
class InputEvent(Event):
input: List[ChatMessage]
response: bool = False
class ResearchEvent(Event):
input: list[ToolSelection]
class AnalyzeEvent(Event):
input: list[ToolSelection] | ChatMessage
class ReportEvent(Event):
input: list[ToolSelection]
class FinancialReportWorkflow(Workflow):
"""
A workflow to generate a financial report using indexed documents.
Requirements:
- Indexed documents containing financial data and a query engine tool to search them
- A code interpreter tool to analyze data and generate reports
- A document generator tool to create report files
Steps:
1. LLM Input: The LLM determines the next step based on function calling.
For example, if the model requests the query engine tool, it returns a ResearchEvent;
if it requests document generation, it returns a ReportEvent.
2. Research: Uses the query engine to find relevant chunks from indexed documents.
After gathering information, it requests analysis (step 3).
3. Analyze: Uses a custom prompt to analyze research results and can call the code
interpreter tool for visualization or calculation. Returns results to the LLM.
4. Report: Uses the document generator tool to create a report. Returns results to the LLM.
"""
_default_system_prompt = """
You are a financial analyst who are given a set of tools to help you.
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
"""
def __init__(
self,
query_engine_tool: QueryEngineTool,
code_interpreter_tool: FunctionTool,
document_generator_tool: FunctionTool,
llm: Optional[FunctionCallingLLM] = None,
timeout: int = 360,
chat_history: Optional[List[ChatMessage]] = None,
system_prompt: Optional[str] = None,
):
super().__init__(timeout=timeout)
self.system_prompt = system_prompt or self._default_system_prompt
self.chat_history = chat_history or []
self.query_engine_tool = query_engine_tool
self.code_interpreter_tool = code_interpreter_tool
self.document_generator_tool = document_generator_tool
assert query_engine_tool is not None, (
"Query engine tool is not found. Try run generation script or upload a document file first."
)
assert code_interpreter_tool is not None, "Code interpreter tool is required"
assert document_generator_tool is not None, (
"Document generator tool is required"
)
self.tools = [
self.query_engine_tool,
self.code_interpreter_tool,
self.document_generator_tool,
]
self.llm: FunctionCallingLLM = llm or Settings.llm
assert isinstance(self.llm, FunctionCallingLLM)
self.memory = ChatMemoryBuffer.from_defaults(
llm=self.llm, chat_history=self.chat_history
)
@step()
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
ctx.data["input"] = ev.input
if self.system_prompt:
system_msg = ChatMessage(
role=MessageRole.SYSTEM, content=self.system_prompt
)
self.memory.put(system_msg)
# Add user input to memory
self.memory.put(ChatMessage(role=MessageRole.USER, content=ev.input))
return InputEvent(input=self.memory.get())
@step()
async def handle_llm_input( # type: ignore
self,
ctx: Context,
ev: InputEvent,
) -> ResearchEvent | AnalyzeEvent | ReportEvent | StopEvent:
"""
Handle an LLM input and decide the next step.
"""
# Always use the latest chat history from the input
chat_history: list[ChatMessage] = ev.input
# Get tool calls
response = await chat_with_tools(
self.llm,
self.tools, # type: ignore
chat_history,
)
if not response.has_tool_calls():
# If no tool call, return the response generator
return StopEvent(result=response.generator)
# calling different tools at the same time is not supported at the moment
# add an error message to tell the AI to process step by step
if response.is_calling_different_tools():
self.memory.put(
ChatMessage(
role=MessageRole.ASSISTANT,
content="Cannot call different tools at the same time. Try calling one tool at a time.",
)
)
return InputEvent(input=self.memory.get())
self.memory.put(response.tool_call_message)
match response.tool_name():
case self.code_interpreter_tool.metadata.name:
return AnalyzeEvent(input=response.tool_calls)
case self.document_generator_tool.metadata.name:
return ReportEvent(input=response.tool_calls)
case self.query_engine_tool.metadata.name:
return ResearchEvent(input=response.tool_calls)
case _:
raise ValueError(f"Unknown tool: {response.tool_name()}")
@step()
async def research(self, ctx: Context, ev: ResearchEvent) -> AnalyzeEvent:
"""
Do a research to gather information for the user's request.
A researcher should have these tools: query engine, search engine, etc.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Researcher",
msg="Starting research",
)
)
tool_calls = ev.input
tool_messages = await call_tools(
ctx=ctx,
agent_name="Researcher",
tools=[self.query_engine_tool],
tool_calls=tool_calls,
)
self.memory.put_messages(tool_messages)
return AnalyzeEvent(
input=ChatMessage(
role=MessageRole.ASSISTANT,
content="I've finished the research. Please analyze the result.",
),
)
@step()
async def analyze(self, ctx: Context, ev: AnalyzeEvent) -> InputEvent:
"""
Analyze the research result.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Analyst",
msg="Starting analysis",
)
)
event_requested_by_workflow_llm = isinstance(ev.input, list)
# Requested by the workflow LLM Input step, it's a tool call
if event_requested_by_workflow_llm:
# Set the tool calls
tool_calls = ev.input
else:
# Otherwise, it's triggered by the research step
# Use a custom prompt and independent memory for the analyst agent
analysis_prompt = """
You are a financial analyst, you are given a research result and a set of tools to help you.
Always use the given information, don't make up anything yourself. If there is not enough information, you can asking for more information.
If you have enough numerical information, it's good to include some charts/visualizations to the report so you can use the code interpreter tool to generate a report.
"""
# This is handled by analyst agent
# Clone the shared memory to avoid conflicting with the workflow.
chat_history = self.memory.get()
chat_history.append(
ChatMessage(role=MessageRole.SYSTEM, content=analysis_prompt)
)
chat_history.append(ev.input) # type: ignore
# Check if the analyst agent needs to call tools
response = await chat_with_tools(
self.llm,
[self.code_interpreter_tool],
chat_history,
)
if not response.has_tool_calls():
# If no tool call, fallback analyst message to the workflow
analyst_msg = ChatMessage(
role=MessageRole.ASSISTANT,
content=await response.full_response(),
)
self.memory.put(analyst_msg)
return InputEvent(input=self.memory.get())
else:
# Set the tool calls and the tool call message to the memory
tool_calls = response.tool_calls
self.memory.put(response.tool_call_message)
# Call tools
tool_messages = await call_tools(
ctx=ctx,
agent_name="Analyst",
tools=[self.code_interpreter_tool],
tool_calls=tool_calls, # type: ignore
)
self.memory.put_messages(tool_messages)
# Fallback to the input with the latest chat history
return InputEvent(input=self.memory.get())
@step()
async def report(self, ctx: Context, ev: ReportEvent) -> InputEvent:
"""
Generate a report based on the analysis result.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Reporter",
msg="Starting report generation",
)
)
tool_calls = ev.input
tool_messages = await call_tools(
ctx=ctx,
agent_name="Reporter",
tools=[self.document_generator_tool],
tool_calls=tool_calls,
)
self.memory.put_messages(tool_messages)
# After the tool calls, fallback to the input with the latest chat history
return InputEvent(input=self.memory.get())
@@ -0,0 +1,63 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
poetry install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have the `OPENAI_API_KEY` set.
Second, run the development server:
```shell
poetry run dev
```
## Use Case: Filling Financial CSV Template
To reproduce the use case, start the [frontend](../frontend/README.md) and follow these steps in the frontend:
1. Upload the Apple and Tesla financial reports from the [data](./data) directory. Just send an empty message.
2. Upload the CSV file [sec_10k_template.csv](./sec_10k_template.csv) and send the message "Fill the missing cells in the CSV file".
The agent will fill the missing cells by retrieving the information from the uploaded financial reports and return a new CSV file with the filled cells.
### API endpoints
The example provides one streaming API endpoint `/api/chat`.
You can test the endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "What can you do?" }] }'
```
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/form_filling.py`. The API auto-updates as you save the files.
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
To start the app optimized for **production**, run:
```
poetry run prod
```
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -0,0 +1,3 @@
from .form_filling import create_workflow
__all__ = ["create_workflow"]
@@ -0,0 +1,236 @@
from typing import Any, Dict, List, Optional
from llama_index.core import Settings
from llama_index.core.base.llms.types import ChatMessage, MessageRole
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
from app.workflows.events import AgentRunEvent
from app.workflows.tools import (
call_tools,
chat_with_tools,
)
def create_workflow(
chat_history: Optional[List[ChatMessage]] = None,
params: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Workflow:
# Create query engine tool
index_config = IndexConfig(**params)
index = get_index(index_config)
if index is None:
query_engine_tool = None
else:
query_engine_tool = get_query_engine_tool(index=index)
configured_tools = ToolFactory.from_env(map_result=True)
extractor_tool = configured_tools.get("extract_questions") # type: ignore
filling_tool = configured_tools.get("fill_form") # type: ignore
workflow = FormFillingWorkflow(
query_engine_tool=query_engine_tool,
extractor_tool=extractor_tool, # type: ignore
filling_tool=filling_tool, # type: ignore
chat_history=chat_history,
)
return workflow
class InputEvent(Event):
input: List[ChatMessage]
response: bool = False
class ExtractMissingCellsEvent(Event):
tool_calls: list[ToolSelection]
class FindAnswersEvent(Event):
tool_calls: list[ToolSelection]
class FillEvent(Event):
tool_calls: list[ToolSelection]
class FormFillingWorkflow(Workflow):
"""
A predefined workflow for filling missing cells in a CSV file.
Required tools:
- query_engine: A query engine to query for the answers to the questions.
- extract_question: Extract missing cells in a CSV file and generate questions to fill them.
- answer_question: Query for the answers to the questions.
Flow:
1. Extract missing cells in a CSV file and generate questions to fill them.
2. Query for the answers to the questions.
3. Fill the missing cells with the answers.
"""
_default_system_prompt = """
You are a helpful assistant who helps fill missing cells in a CSV file.
Only extract missing cells from CSV files.
Only use provided data - never make up any information yourself. Fill N/A if an answer is not found.
If there is no query engine tool or the gathered information has many N/A values indicating the questions don't match the data, respond with a warning and ask the user to upload a different file or connect to a knowledge base.
"""
def __init__(
self,
query_engine_tool: Optional[QueryEngineTool],
extractor_tool: FunctionTool,
filling_tool: FunctionTool,
llm: Optional[FunctionCallingLLM] = None,
timeout: int = 360,
chat_history: Optional[List[ChatMessage]] = None,
system_prompt: Optional[str] = None,
):
super().__init__(timeout=timeout)
self.system_prompt = system_prompt or self._default_system_prompt
self.chat_history = chat_history or []
self.query_engine_tool = query_engine_tool
self.extractor_tool = extractor_tool
self.filling_tool = filling_tool
if self.extractor_tool is None or self.filling_tool is None:
raise ValueError("Extractor and filling tools are required.")
self.tools = [self.extractor_tool, self.filling_tool]
if self.query_engine_tool is not None:
self.tools.append(self.query_engine_tool) # type: ignore
self.llm: FunctionCallingLLM = llm or Settings.llm
if not isinstance(self.llm, FunctionCallingLLM):
raise ValueError("FormFillingWorkflow only supports FunctionCallingLLM.")
self.memory = ChatMemoryBuffer.from_defaults(
llm=self.llm, chat_history=self.chat_history
)
@step()
async def start(self, ctx: Context, ev: StartEvent) -> InputEvent:
ctx.data["input"] = ev.input
if self.system_prompt:
system_msg = ChatMessage(
role=MessageRole.SYSTEM, content=self.system_prompt
)
self.memory.put(system_msg)
user_input = ev.input
user_msg = ChatMessage(role=MessageRole.USER, content=user_input)
self.memory.put(user_msg)
chat_history = self.memory.get()
return InputEvent(input=chat_history)
@step()
async def handle_llm_input( # type: ignore
self,
ctx: Context,
ev: InputEvent,
) -> ExtractMissingCellsEvent | FillEvent | StopEvent:
"""
Handle an LLM input and decide the next step.
"""
chat_history: list[ChatMessage] = ev.input
response = await chat_with_tools(
self.llm,
self.tools,
chat_history,
)
if not response.has_tool_calls():
return StopEvent(result=response.generator)
# calling different tools at the same time is not supported at the moment
# add an error message to tell the AI to process step by step
if response.is_calling_different_tools():
self.memory.put(
ChatMessage(
role=MessageRole.ASSISTANT,
content="Cannot call different tools at the same time. Try calling one tool at a time.",
)
)
return InputEvent(input=self.memory.get())
self.memory.put(response.tool_call_message)
match response.tool_name():
case self.extractor_tool.metadata.name:
return ExtractMissingCellsEvent(tool_calls=response.tool_calls)
case self.query_engine_tool.metadata.name:
return FindAnswersEvent(tool_calls=response.tool_calls)
case self.filling_tool.metadata.name:
return FillEvent(tool_calls=response.tool_calls)
case _:
raise ValueError(f"Unknown tool: {response.tool_name()}")
@step()
async def extract_missing_cells(
self, ctx: Context, ev: ExtractMissingCellsEvent
) -> InputEvent | FindAnswersEvent:
"""
Extract missing cells in a CSV file and generate questions to fill them.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Extractor",
msg="Extracting missing cells",
)
)
# Call the extract questions tool
tool_messages = await call_tools(
agent_name="Extractor",
tools=[self.extractor_tool],
ctx=ctx,
tool_calls=ev.tool_calls,
)
self.memory.put_messages(tool_messages)
return InputEvent(input=self.memory.get())
@step()
async def find_answers(self, ctx: Context, ev: FindAnswersEvent) -> InputEvent:
"""
Call answer questions tool to query for the answers to the questions.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Researcher",
msg="Finding answers for missing cells",
)
)
tool_messages = await call_tools(
ctx=ctx,
agent_name="Researcher",
tools=[self.query_engine_tool],
tool_calls=ev.tool_calls,
)
self.memory.put_messages(tool_messages)
return InputEvent(input=self.memory.get())
@step()
async def fill_cells(self, ctx: Context, ev: FillEvent) -> InputEvent:
"""
Call fill cells tool to fill the missing cells with the answers.
"""
ctx.write_event_to_stream(
AgentRunEvent(
name="Processor",
msg="Filling missing cells",
)
)
tool_messages = await call_tools(
agent_name="Processor",
tools=[self.filling_tool],
ctx=ctx,
tool_calls=ev.tool_calls,
)
self.memory.put_messages(tool_messages)
return InputEvent(input=self.memory.get())
@@ -0,0 +1,17 @@
Parameter,2023 Apple (AAPL),2023 Tesla (TSLA)
Revenue,,
Net Income,,
Earnings Per Share (EPS),,
Debt-to-Equity Ratio,,
Current Ratio,,
Gross Margin,,
Operating Margin,,
Net Profit Margin,,
Inventory Turnover,,
Accounts Receivable Turnover,,
Capital Expenditure,,
Research and Development Expense,,
Market Cap,,
Price to Earnings Ratio,,
Dividend Yield,,
Year-over-Year Growth Rate,,
1 Parameter 2023 Apple (AAPL) 2023 Tesla (TSLA)
2 Revenue
3 Net Income
4 Earnings Per Share (EPS)
5 Debt-to-Equity Ratio
6 Current Ratio
7 Gross Margin
8 Operating Margin
9 Net Profit Margin
10 Inventory Turnover
11 Accounts Receivable Turnover
12 Capital Expenditure
13 Research and Development Expense
14 Market Cap
15 Price to Earnings Ratio
16 Dividend Yield
17 Year-over-Year Growth Rate
@@ -1,19 +1,16 @@
import { ChatMessage } from "llamaindex";
import { getTool } from "../engine/tools";
import { FunctionCallingAgent } from "./single-agent";
import { getQueryEngineTool, lookupTools } from "./tools";
import { getQueryEngineTool } from "./tools";
export const createResearcher = async (
chatHistory: ChatMessage[],
params?: any,
) => {
const queryEngineTool = await getQueryEngineTool(params);
const tools = (
await lookupTools([
"wikipedia_tool",
"duckduckgo_search",
"image_generator",
])
).concat(queryEngineTool ? [queryEngineTool] : []);
export const createResearcher = async (chatHistory: ChatMessage[]) => {
const queryEngineTool = await getQueryEngineTool();
const tools = [
await getTool("wikipedia_tool"),
await getTool("duckduckgo_search"),
await getTool("image_generator"),
queryEngineTool,
].filter((tool) => tool !== undefined);
return new FunctionCallingAgent({
name: "researcher",
@@ -81,17 +78,17 @@ Example:
};
export const createPublisher = async (chatHistory: ChatMessage[]) => {
const tools = await lookupTools(["document_generator"]);
const tool = await getTool("document_generator");
let systemPrompt = `You are an expert in publishing blog posts. You are given a task to publish a blog post.
If the writer says that there was an error, you should reply with the error and not publish the post.`;
if (tools.length > 0) {
if (tool) {
systemPrompt = `${systemPrompt}.
If the user requests to generate a file, use the document_generator tool to generate the file and reply with the link to the file.
Otherwise, simply return the content of the post.`;
}
return new FunctionCallingAgent({
name: "publisher",
tools: tools,
tools: tool ? [tool] : [],
systemPrompt: systemPrompt,
chatHistory,
});
@@ -0,0 +1,291 @@
import {
HandlerContext,
StartEvent,
StopEvent,
Workflow,
WorkflowContext,
WorkflowEvent,
} from "@llamaindex/workflow";
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
import {
createPublisher,
createResearcher,
createReviewer,
createWriter,
} from "./agents";
import {
FunctionCallingAgent,
FunctionCallingAgentInput,
} from "./single-agent";
import { AgentInput, AgentRunEvent } from "./type";
const TIMEOUT = 360 * 1000;
const MAX_ATTEMPTS = 2;
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
class WriteEvent extends WorkflowEvent<{
input: string;
isGood: boolean;
}> {}
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
class PublishEvent extends WorkflowEvent<{ input: string }> {}
type BlogContext = {
task: string;
attempts: number;
result: string;
};
export const createWorkflow = ({
chatHistory,
params,
}: {
chatHistory: ChatMessage[];
params?: any;
}) => {
const runAgent = async (
context: HandlerContext<BlogContext>,
agent: FunctionCallingAgent,
input: FunctionCallingAgentInput,
) => {
const agentContext = agent.run(input, {
streaming: input.streaming ?? false,
});
for await (const event of agentContext) {
if (event instanceof AgentRunEvent) {
context.sendEvent(event);
}
if (event instanceof StopEvent) {
return event;
}
}
return null;
};
const start = async (
context: HandlerContext<BlogContext>,
ev: StartEvent<AgentInput>,
) => {
const chatHistoryStr = chatHistory
.map((msg) => `${msg.role}: ${msg.content}`)
.join("\n");
// Decision-making process
const decision = await decideWorkflow(
ev.data.message.toString(),
chatHistoryStr,
);
if (decision !== "publish") {
return new ResearchEvent({
input: `Research for this task: ${JSON.stringify(context.data.task)}`,
});
} else {
return new PublishEvent({
input: `Publish content based on the chat history\n${chatHistoryStr}\n\n and task: ${context.data.task}`,
});
}
};
const decideWorkflow = async (task: string, chatHistoryStr: string) => {
const llm = Settings.llm;
const prompt = `You are an expert in decision-making, helping people write and publish blog posts.
If the user is asking for a file or to publish content, respond with 'publish'.
If the user requests to write or update a blog post, respond with 'not_publish'.
Here is the chat history:
${chatHistoryStr}
The current user request is:
${task}
Given the chat history and the new user request, decide whether to publish based on existing information.
Decision (respond with either 'not_publish' or 'publish'):`;
const output = await llm.complete({ prompt: prompt });
const decision = output.text.trim().toLowerCase();
return decision === "publish" ? "publish" : "research";
};
const research = async (
context: HandlerContext<BlogContext>,
ev: ResearchEvent,
) => {
const researcher = await createResearcher(chatHistory);
const researchRes = await runAgent(context, researcher, {
displayName: "Researcher",
message: ev.data.input,
});
const researchResult = researchRes?.data;
return new WriteEvent({
input: `Write a blog post given this task: ${JSON.stringify(
context.data.task,
)} using this research content: ${researchResult}`,
isGood: false,
});
};
const write = async (
context: HandlerContext<BlogContext>,
ev: WriteEvent,
) => {
const writer = createWriter(chatHistory);
context.data.attempts = context.data.attempts + 1;
const tooManyAttempts = context.data.attempts > MAX_ATTEMPTS;
if (tooManyAttempts) {
context.sendEvent(
new AgentRunEvent({
agent: "writer",
text: `Too many attempts (${MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.`,
type: "text",
}),
);
}
if (ev.data.isGood || tooManyAttempts) {
// the blog post is good or too many attempts
// stream the final content
const result = await runAgent(context, writer, {
message: `Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: ${ev.data.input}`,
displayName: "Writer",
streaming: true,
});
return result as unknown as StopEvent<AsyncGenerator<ChatResponseChunk>>;
}
const writeRes = await runAgent(context, writer, {
message: ev.data.input,
displayName: "Writer",
streaming: false,
});
const writeResult = writeRes?.data;
context.data.result = writeResult; // store the last result
return new ReviewEvent({ input: writeResult });
};
const review = async (
context: HandlerContext<BlogContext>,
ev: ReviewEvent,
) => {
const reviewer = createReviewer(chatHistory);
const reviewResult = (await runAgent(context, reviewer, {
message: ev.data.input,
displayName: "Reviewer",
streaming: false,
})) as unknown as StopEvent<string>;
const reviewResultStr = reviewResult.data;
const oldContent = context.data.result;
const postIsGood = reviewResultStr.toLowerCase().includes("post is good");
context.sendEvent(
new AgentRunEvent({
agent: "reviewer",
text: `The post is ${postIsGood ? "" : "not "}good enough for publishing. Sending back to the writer${
postIsGood ? " for publication." : "."
}`,
type: "text",
}),
);
if (postIsGood) {
return new WriteEvent({
input: "",
isGood: true,
});
}
return new WriteEvent({
input: `Improve the writing of a given blog post by using a given review.
Blog post:
\`\`\`
${oldContent}
\`\`\`
Review:
\`\`\`
${reviewResult}
\`\`\``,
isGood: false,
});
};
const publish = async (
context: HandlerContext<BlogContext>,
ev: PublishEvent,
) => {
const publisher = await createPublisher(chatHistory);
const publishResult = await runAgent(context, publisher, {
message: `${ev.data.input}`,
displayName: "Publisher",
streaming: true,
});
return publishResult as unknown as StopEvent<
AsyncGenerator<ChatResponseChunk>
>;
};
const workflow: Workflow<
BlogContext,
AgentInput,
string | AsyncGenerator<boolean | ChatResponseChunk>
> = new Workflow();
workflow.addStep(
{
inputs: [StartEvent<AgentInput>],
outputs: [ResearchEvent, PublishEvent],
},
start,
);
workflow.addStep(
{
inputs: [ResearchEvent],
outputs: [WriteEvent],
},
research,
);
workflow.addStep(
{
inputs: [WriteEvent],
outputs: [ReviewEvent, StopEvent<AsyncGenerator<ChatResponseChunk>>],
},
write,
);
workflow.addStep(
{
inputs: [ReviewEvent],
outputs: [WriteEvent],
},
review,
);
workflow.addStep(
{
inputs: [PublishEvent],
outputs: [StopEvent],
},
publish,
);
// Overload run method to initialize the context
workflow.run = function (
input: AgentInput,
): WorkflowContext<
AgentInput,
string | AsyncGenerator<boolean | ChatResponseChunk>,
BlogContext
> {
return Workflow.prototype.run.call(workflow, new StartEvent(input), {
task: input.message.toString(),
attempts: 0,
result: "",
});
};
return workflow;
};
@@ -0,0 +1,47 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have the `OPENAI_API_KEY` set.
Second, generate the embeddings of the documents in the `./data` directory:
```
npm run generate
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Use Case: Filling Financial CSV Template
You can start by sending an request on the chat UI to create a report comparing the finances of Apple and Tesla.
Or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/guide/workflow) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -0,0 +1,28 @@
import { ChatMessage, ToolCallLLM } from "llamaindex";
import { getTool } from "../engine/tools";
import { FinancialReportWorkflow } from "./fin-report";
import { getQueryEngineTool } from "./tools";
const TIMEOUT = 360 * 1000;
export async function createWorkflow(options: {
chatHistory: ChatMessage[];
llm?: ToolCallLLM;
}) {
const queryEngineTool = await getQueryEngineTool();
const codeInterpreterTool = await getTool("interpreter");
const documentGeneratorTool = await getTool("document_generator");
if (!queryEngineTool || !codeInterpreterTool || !documentGeneratorTool) {
throw new Error("One or more required tools are not defined");
}
return new FinancialReportWorkflow({
chatHistory: options.chatHistory,
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
llm: options.llm,
timeout: TIMEOUT,
});
}
@@ -0,0 +1,320 @@
import {
HandlerContext,
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/workflow";
import {
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponseChunk,
Settings,
ToolCall,
ToolCallLLM,
} from "llamaindex";
import { callTools, chatWithTools } from "./tools";
import { AgentInput, AgentRunEvent } from "./type";
// Create a custom event type
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
class ResearchEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
class AnalyzeEvent extends WorkflowEvent<{
input: ChatMessage | ToolCall[];
}> {}
class ReportGenerationEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
const DEFAULT_SYSTEM_PROMPT = `
You are a financial analyst who are given a set of tools to help you.
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
`;
export class FinancialReportWorkflow extends Workflow<
null,
AgentInput,
ChatResponseChunk
> {
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
constructor(options: {
llm?: ToolCallLLM;
chatHistory: ChatMessage[];
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
if (!(this.llm instanceof ToolCallLLM)) {
throw new Error("LLM is not a ToolCallLLM");
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.queryEngineTool = options.queryEngineTool;
this.codeInterpreterTool = options.codeInterpreterTool;
this.documentGeneratorTool = options.documentGeneratorTool;
this.memory = new ChatMemoryBuffer({
llm: this.llm,
chatHistory: options.chatHistory,
});
// Add steps
this.addStep(
{
inputs: [StartEvent<AgentInput>],
outputs: [InputEvent],
},
this.prepareChatHistory,
);
this.addStep(
{
inputs: [InputEvent],
outputs: [
InputEvent,
ResearchEvent,
AnalyzeEvent,
ReportGenerationEvent,
StopEvent,
],
},
this.handleLLMInput,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [AnalyzeEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [AnalyzeEvent],
outputs: [InputEvent],
},
this.handleAnalyze,
);
this.addStep(
{
inputs: [ReportGenerationEvent],
outputs: [InputEvent],
},
this.handleReportGeneration,
);
}
prepareChatHistory = async (
ctx: HandlerContext<null>,
ev: StartEvent<AgentInput>,
): Promise<InputEvent> => {
const { message } = ev.data;
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
this.memory.put({ role: "user", content: message });
return new InputEvent({ input: this.memory.getMessages() });
};
handleLLMInput = async (
ctx: HandlerContext<null>,
ev: InputEvent,
): Promise<
| InputEvent
| ResearchEvent
| AnalyzeEvent
| ReportGenerationEvent
| StopEvent
> => {
const chatHistory = ev.data.input;
const tools = [
this.codeInterpreterTool,
this.documentGeneratorTool,
this.queryEngineTool,
];
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
return new StopEvent(toolCallResponse.responseGenerator);
}
if (toolCallResponse.hasMultipleTools()) {
this.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
return new InputEvent({ input: this.memory.getMessages() });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
this.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case this.codeInterpreterTool.metadata.name:
return new AnalyzeEvent({
input: toolCallResponse.toolCalls,
});
case this.documentGeneratorTool.metadata.name:
return new ReportGenerationEvent({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (this.queryEngineTool.metadata.name === toolName) {
return new ResearchEvent({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
};
handleResearch = async (
ctx: HandlerContext<null>,
ev: ResearchEvent,
): Promise<AnalyzeEvent> => {
ctx.sendEvent(
new AgentRunEvent({
agent: "Researcher",
text: "Researching data",
type: "text",
}),
);
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.queryEngineTool],
toolCalls,
ctx,
agentName: "Researcher",
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
return new AnalyzeEvent({
input: {
role: "assistant",
content:
"I have finished researching the data, please analyze the data.",
},
});
};
/**
* Analyze a research result or a tool call for code interpreter from the LLM
*/
handleAnalyze = async (
ctx: HandlerContext<null>,
ev: AnalyzeEvent,
): Promise<InputEvent> => {
ctx.sendEvent(
new AgentRunEvent({
agent: "Analyst",
text: `Starting analysis`,
type: "text",
}),
);
// Request by workflow LLM, input is a list of tool calls
let toolCalls: ToolCall[] = [];
if (Array.isArray(ev.data.input)) {
toolCalls = ev.data.input;
} else {
// Requested by Researcher, input is a ChatMessage
// We start new LLM chat specifically for analyzing the data
const analysisPrompt = `
You are an expert in analyzing financial data.
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
Construct the analysis in textual format; including tables would be great!
Don't need to synthesize the data, just analyze and provide your findings.
`;
// Clone the current chat history
// Add the analysis system prompt and the message from the researcher
const newChatHistory = [
...this.memory.getMessages(),
{ role: "system", content: analysisPrompt },
ev.data.input,
];
const toolCallResponse = await chatWithTools(
this.llm,
[this.codeInterpreterTool],
newChatHistory as ChatMessage[],
);
if (!toolCallResponse.hasToolCall()) {
this.memory.put(await toolCallResponse.asFullResponse());
return new InputEvent({
input: this.memory.getMessages(),
});
} else {
this.memory.put(toolCallResponse.toolCallMessage);
toolCalls = toolCallResponse.toolCalls;
}
}
// Call the tools
const toolMsgs = await callTools({
tools: [this.codeInterpreterTool],
toolCalls,
ctx,
agentName: "Analyst",
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
return new InputEvent({
input: this.memory.getMessages(),
});
};
handleReportGeneration = async (
ctx: HandlerContext<null>,
ev: ReportGenerationEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.documentGeneratorTool],
toolCalls,
ctx,
agentName: "Reporter",
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
return new InputEvent({ input: this.memory.getMessages() });
};
}
@@ -0,0 +1,37 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have the `OPENAI_API_KEY` set.
Second, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Use Case: Filling Financial CSV Template
1. Upload the Apple and Tesla financial reports from the [data](./data) directory. Just send an empty message.
2. Upload the CSV file [sec_10k_template.csv](./sec_10k_template.csv) and send the message "Fill the missing cells in the CSV file".
The agent will fill the missing cells by retrieving the information from the uploaded financial reports and return a new CSV file with the filled cells.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/guide/workflow) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -0,0 +1,17 @@
Parameter,2023 Apple (AAPL),2023 Tesla (TSLA)
Revenue,,
Net Income,,
Earnings Per Share (EPS),,
Debt-to-Equity Ratio,,
Current Ratio,,
Gross Margin,,
Operating Margin,,
Net Profit Margin,,
Inventory Turnover,,
Accounts Receivable Turnover,,
Capital Expenditure,,
Research and Development Expense,,
Market Cap,,
Price to Earnings Ratio,,
Dividend Yield,,
Year-over-Year Growth Rate,,
1 Parameter 2023 Apple (AAPL) 2023 Tesla (TSLA)
2 Revenue
3 Net Income
4 Earnings Per Share (EPS)
5 Debt-to-Equity Ratio
6 Current Ratio
7 Gross Margin
8 Operating Margin
9 Net Profit Margin
10 Inventory Turnover
11 Accounts Receivable Turnover
12 Capital Expenditure
13 Research and Development Expense
14 Market Cap
15 Price to Earnings Ratio
16 Dividend Yield
17 Year-over-Year Growth Rate
@@ -0,0 +1,27 @@
import { ChatMessage, ToolCallLLM } from "llamaindex";
import { getTool } from "../engine/tools";
import { FormFillingWorkflow } from "./form-filling";
import { getQueryEngineTool } from "./tools";
const TIMEOUT = 360 * 1000;
export async function createWorkflow(options: {
chatHistory: ChatMessage[];
llm?: ToolCallLLM;
}) {
const extractorTool = await getTool("extract_missing_cells");
const fillMissingCellsTool = await getTool("fill_missing_cells");
if (!extractorTool || !fillMissingCellsTool) {
throw new Error("One or more required tools are not defined");
}
return new FormFillingWorkflow({
chatHistory: options.chatHistory,
queryEngineTool: (await getQueryEngineTool()) || undefined,
extractorTool,
fillMissingCellsTool,
llm: options.llm,
timeout: TIMEOUT,
});
}
@@ -0,0 +1,275 @@
import {
HandlerContext,
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/workflow";
import {
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponseChunk,
Settings,
ToolCall,
ToolCallLLM,
} from "llamaindex";
import { callTools, chatWithTools } from "./tools";
import { AgentInput, AgentRunEvent } from "./type";
// Create a custom event type
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
class ExtractMissingCellsEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
class FindAnswersEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
class FillMissingCellsEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
const DEFAULT_SYSTEM_PROMPT = `
You are a helpful assistant who helps fill missing cells in a CSV file.
Only use the information from the retriever tool - don't make up any information yourself. Fill N/A if an answer is not found.
If there is no retriever tool or the gathered information has many N/A values indicating the questions don't match the data, respond with a warning and ask the user to upload a different file or connect to a knowledge base.
You can make multiple tool calls at once but only call with the same tool.
Only use the local file path for the tools.
`;
export class FormFillingWorkflow extends Workflow<
null,
AgentInput,
ChatResponseChunk
> {
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
extractorTool: BaseToolWithCall;
queryEngineTool?: BaseToolWithCall;
fillMissingCellsTool: BaseToolWithCall;
systemPrompt?: string;
constructor(options: {
llm?: ToolCallLLM;
chatHistory: ChatMessage[];
extractorTool: BaseToolWithCall;
queryEngineTool?: BaseToolWithCall;
fillMissingCellsTool: BaseToolWithCall;
systemPrompt?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
if (!(this.llm instanceof ToolCallLLM)) {
throw new Error("LLM is not a ToolCallLLM");
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.extractorTool = options.extractorTool;
this.queryEngineTool = options.queryEngineTool;
this.fillMissingCellsTool = options.fillMissingCellsTool;
this.memory = new ChatMemoryBuffer({
llm: this.llm,
chatHistory: options.chatHistory,
});
// Add steps
this.addStep(
{
inputs: [StartEvent<AgentInput>],
outputs: [InputEvent],
},
this.prepareChatHistory,
);
this.addStep(
{
inputs: [InputEvent],
outputs: [
InputEvent,
ExtractMissingCellsEvent,
FindAnswersEvent,
FillMissingCellsEvent,
StopEvent,
],
},
this.handleLLMInput,
);
this.addStep(
{
inputs: [ExtractMissingCellsEvent],
outputs: [InputEvent],
},
this.handleExtractMissingCells,
);
this.addStep(
{
inputs: [FindAnswersEvent],
outputs: [InputEvent],
},
this.handleFindAnswers,
);
this.addStep(
{
inputs: [FillMissingCellsEvent],
outputs: [InputEvent],
},
this.handleFillMissingCells,
);
}
prepareChatHistory = async (
ctx: HandlerContext<null>,
ev: StartEvent<AgentInput>,
): Promise<InputEvent> => {
const { message } = ev.data;
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
this.memory.put({ role: "user", content: message });
return new InputEvent({ input: this.memory.getMessages() });
};
handleLLMInput = async (
ctx: HandlerContext<null>,
ev: InputEvent,
): Promise<
| InputEvent
| ExtractMissingCellsEvent
| FindAnswersEvent
| FillMissingCellsEvent
| StopEvent
> => {
const chatHistory = ev.data.input;
const tools = [this.extractorTool, this.fillMissingCellsTool];
if (this.queryEngineTool) {
tools.push(this.queryEngineTool);
}
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
return new StopEvent(toolCallResponse.responseGenerator);
}
if (toolCallResponse.hasMultipleTools()) {
this.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
return new InputEvent({ input: this.memory.getMessages() });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
this.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case this.extractorTool.metadata.name:
return new ExtractMissingCellsEvent({
toolCalls: toolCallResponse.toolCalls,
});
case this.fillMissingCellsTool.metadata.name:
return new FillMissingCellsEvent({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (
this.queryEngineTool &&
this.queryEngineTool.metadata.name === toolName
) {
return new FindAnswersEvent({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
};
handleExtractMissingCells = async (
ctx: HandlerContext<null>,
ev: ExtractMissingCellsEvent,
): Promise<InputEvent> => {
ctx.sendEvent(
new AgentRunEvent({
agent: "CSVExtractor",
text: "Extracting missing cells",
type: "text",
}),
);
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.extractorTool],
toolCalls,
ctx,
agentName: "CSVExtractor",
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
return new InputEvent({ input: this.memory.getMessages() });
};
handleFindAnswers = async (
ctx: HandlerContext<null>,
ev: FindAnswersEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
if (!this.queryEngineTool) {
throw new Error("Query engine tool is not available");
}
ctx.sendEvent(
new AgentRunEvent({
agent: "Researcher",
text: "Finding answers",
type: "text",
}),
);
const toolMsgs = await callTools({
tools: [this.queryEngineTool],
toolCalls,
ctx,
agentName: "Researcher",
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
return new InputEvent({ input: this.memory.getMessages() });
};
handleFillMissingCells = async (
ctx: HandlerContext<null>,
ev: FillMissingCellsEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.fillMissingCellsTool],
toolCalls,
ctx,
agentName: "Processor",
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
return new InputEvent({ input: this.memory.getMessages() });
};
}
@@ -1,18 +1,18 @@
import os
from typing import List
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from llama_index.core.agent import AgentRunner
from llama_index.core.callbacks import CallbackManager
from llama_index.core.settings import Settings
from llama_index.core.tools import BaseTool
from llama_index.core.tools.query_engine import QueryEngineTool
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
def get_chat_engine(params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
top_k = int(os.getenv("TOP_K", 0))
tools: List[BaseTool] = []
callback_manager = CallbackManager(handlers=event_handlers or [])
@@ -20,10 +20,7 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
index = get_index(index_config)
if index is not None:
query_engine = index.as_query_engine(
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
)
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
query_engine_tool = get_query_engine_tool(index, **kwargs)
tools.append(query_engine_tool)
# Add additional tools
@@ -56,7 +56,7 @@ class ToolFactory:
A dictionary of tool names to lists of FunctionTools if map_result is True,
otherwise a list of FunctionTools.
"""
tools: Union[Dict[str, List[FunctionTool]], List[FunctionTool]] = (
tools: Union[Dict[str, FunctionTool], List[FunctionTool]] = (
{} if map_result else []
)
@@ -69,7 +69,9 @@ class ToolFactory:
tool_type, tool_name, config
)
if map_result:
tools[tool_name] = loaded_tools # type: ignore
tools.update( # type: ignore
{tool.metadata.name: tool for tool in loaded_tools}
)
else:
tools.extend(loaded_tools) # type: ignore
@@ -66,21 +66,29 @@ class CodeGeneratorTool:
def __init__(self):
pass
def artifact(self, query: str, old_code: Optional[str] = None) -> Dict:
"""Generate a code artifact based on the input.
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): The description of the application you want to build.
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 the generated artifact information.
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),
@@ -90,7 +98,10 @@ class CodeGeneratorTool:
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact) # type: ignore
response = sllm.chat(messages)
data: CodeArtifact = response.raw
return data.model_dump()
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
@@ -105,7 +105,7 @@ class DocumentGenerator:
Generate HTML content from the original markdown content.
"""
try:
import markdown
import markdown # type: ignore
except ImportError:
raise ImportError(
"Failed to import required modules. Please install markdown."
@@ -0,0 +1,224 @@
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),
]
@@ -3,7 +3,7 @@ import os
import uuid
from typing import Optional
import requests
import requests # type: ignore
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel, Field
@@ -2,14 +2,15 @@ import base64
import logging
import os
import uuid
from typing import Dict, List, Optional
from typing import List, Optional
from e2b_code_interpreter import CodeInterpreter
from app.services.file import DocumentFile, FileService
from e2b_code_interpreter import Sandbox
from e2b_code_interpreter.models import Logs
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel
logger = logging.getLogger(__name__)
logger = logging.getLogger("uvicorn")
class InterpreterExtraResult(BaseModel):
@@ -22,13 +23,16 @@ class InterpreterExtraResult(BaseModel):
class E2BToolOutput(BaseModel):
is_error: bool
logs: Logs
error_message: Optional[str] = None
results: List[InterpreterExtraResult] = []
retry_count: int = 0
class E2BCodeInterpreter:
output_dir = "output/tools"
uploaded_files_dir = "output/uploaded"
def __init__(self, api_key: str = None):
def __init__(self, api_key: Optional[str] = None):
if api_key is None:
api_key = os.getenv("E2B_API_KEY")
filesever_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
@@ -42,40 +46,45 @@ class E2BCodeInterpreter:
)
self.filesever_url_prefix = filesever_url_prefix
self.interpreter = CodeInterpreter(api_key=api_key)
self.interpreter = None
self.api_key = api_key
def __del__(self):
self.interpreter.close()
"""
Kill the interpreter when the tool is no longer in use
"""
if self.interpreter is not None:
self.interpreter.kill()
def get_output_path(self, filename: str) -> str:
# if output directory doesn't exist, create it
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir, exist_ok=True)
return os.path.join(self.output_dir, filename)
def _init_interpreter(self, sandbox_files: List[str] = []):
"""
Lazily initialize the interpreter.
"""
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
self.interpreter = Sandbox(api_key=self.api_key)
if len(sandbox_files) > 0:
for file_path in sandbox_files:
file_name = os.path.basename(file_path)
local_file_path = os.path.join(self.uploaded_files_dir, file_name)
with open(local_file_path, "rb") as f:
content = f.read()
if self.interpreter and self.interpreter.files:
self.interpreter.files.write(file_path, content)
logger.info(f"Uploaded {len(sandbox_files)} files to sandbox")
def save_to_disk(self, base64_data: str, ext: str) -> Dict:
filename = f"{uuid.uuid4()}.{ext}" # generate a unique filename
def _save_to_disk(self, base64_data: str, ext: str) -> DocumentFile:
buffer = base64.b64decode(base64_data)
output_path = self.get_output_path(filename)
try:
with open(output_path, "wb") as file:
file.write(buffer)
except IOError as e:
logger.error(f"Failed to write to file {output_path}: {str(e)}")
raise e
# Output from e2b doesn't have a name. Create a random name for it.
filename = f"e2b_file_{uuid.uuid4()}.{ext}"
logger.info(f"Saved file to {output_path}")
document_file = FileService.save_file(
buffer, file_name=filename, save_dir=self.output_dir
)
return {
"outputPath": output_path,
"filename": filename,
}
return document_file
def get_file_url(self, filename: str) -> str:
return f"{self.filesever_url_prefix}/{self.output_dir}/{filename}"
def parse_result(self, result) -> List[InterpreterExtraResult]:
def _parse_result(self, result) -> List[InterpreterExtraResult]:
"""
The result could include multiple formats (e.g. png, svg, etc.) but encoded in base64
We save each result to disk and return saved file metadata (extension, filename, url)
@@ -92,16 +101,20 @@ class E2BCodeInterpreter:
for ext, data in zip(formats, results):
match ext:
case "png" | "svg" | "jpeg" | "pdf":
result = self.save_to_disk(data, ext)
filename = result["filename"]
document_file = self._save_to_disk(data, ext)
output.append(
InterpreterExtraResult(
type=ext,
filename=filename,
url=self.get_file_url(filename),
filename=document_file.name,
url=document_file.url,
)
)
case _:
# Try serialize data to string
try:
data = str(data)
except Exception as e:
data = f"Error when serializing data: {e}"
output.append(
InterpreterExtraResult(
type=ext,
@@ -114,28 +127,75 @@ class E2BCodeInterpreter:
return output
def interpret(self, code: str) -> E2BToolOutput:
def interpret(
self,
code: str,
sandbox_files: List[str] = [],
retry_count: int = 0,
) -> E2BToolOutput:
"""
Execute python code in a Jupyter notebook cell, the toll will return result, stdout, stderr, display_data, and error.
Execute Python code in a Jupyter notebook cell. The tool will return the 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:
code (str): The python code to be executed in a single cell.
code (str): The Python code to be executed in a single cell.
sandbox_files (List[str]): List of local file paths to be used by the code. The tool will throw an error if a file is not found.
retry_count (int): Number of times the tool has been retried.
"""
logger.info(
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
)
exec = self.interpreter.notebook.exec_cell(code)
if retry_count > 2:
return E2BToolOutput(
is_error=True,
logs=Logs(
stdout="",
stderr="",
display_data="",
error="",
),
error_message="Failed to execute the code after 3 retries. Explain the error to the user and suggest a fix.",
retry_count=retry_count,
)
if exec.error:
logger.error("Error when executing code", exec.error)
output = E2BToolOutput(is_error=True, logs=exec.logs, results=[])
else:
if len(exec.results) == 0:
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
if self.interpreter is None:
self._init_interpreter(sandbox_files)
if self.interpreter:
logger.info(
f"\n{'=' * 50}\n> Running following AI-generated code:\n{code}\n{'=' * 50}"
)
exec = self.interpreter.run_code(code)
if exec.error:
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
logger.error(error_message)
# Calling the generated code caused an error. Kill the interpreter and return the error to the LLM so it can try to fix the error
try:
self.interpreter.kill() # type: ignore
except Exception:
pass
finally:
self.interpreter = None
output = E2BToolOutput(
is_error=True,
logs=exec.logs,
results=[],
error_message=error_message,
retry_count=retry_count + 1,
)
else:
results = self.parse_result(exec.results[0])
output = E2BToolOutput(is_error=False, logs=exec.logs, results=results)
return output
if len(exec.results) == 0:
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
else:
results = self._parse_result(exec.results[0])
output = E2BToolOutput(
is_error=False,
logs=exec.logs,
results=results,
retry_count=retry_count + 1,
)
return output
else:
raise ValueError("Interpreter is not initialized.")
def get_tools(**kwargs):
@@ -1,4 +1,5 @@
from typing import Dict, List, Tuple
from llama_index.tools.openapi import OpenAPIToolSpec
from llama_index.tools.requests import RequestsToolSpec
@@ -43,11 +44,12 @@ class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec):
Returns:
List[Document]: A list of Document objects.
"""
import yaml
from urllib.parse import urlparse
import yaml # type: ignore
if uri.startswith("http"):
import requests
import requests # type: ignore
response = requests.get(uri)
if response.status_code != 200:
@@ -0,0 +1,187 @@
import os
from typing import Any, Dict, List, Optional, Sequence
from llama_index.core import get_response_synthesizer
from llama_index.core.base.base_query_engine import BaseQueryEngine
from llama_index.core.base.response.schema import RESPONSE_TYPE, Response
from llama_index.core.multi_modal_llms import MultiModalLLM
from llama_index.core.prompts.base import BasePromptTemplate
from llama_index.core.prompts.default_prompt_selectors import (
DEFAULT_TEXT_QA_PROMPT_SEL,
)
from llama_index.core.query_engine.multi_modal import _get_image_and_text_nodes
from llama_index.core.response_synthesizers.base import BaseSynthesizer, QueryTextType
from llama_index.core.schema import (
ImageNode,
NodeWithScore,
)
from llama_index.core.tools.query_engine import QueryEngineTool
from llama_index.core.types import RESPONSE_TEXT_TYPE
from app.settings import get_multi_modal_llm
def create_query_engine(index, **kwargs) -> BaseQueryEngine:
"""
Create a query engine for the given index.
Args:
index: The index to create a query engine for.
params (optional): Additional parameters for the query engine, e.g: similarity_top_k
"""
top_k = int(os.getenv("TOP_K", 0))
if top_k != 0 and kwargs.get("filters") is None:
kwargs["similarity_top_k"] = top_k
multimodal_llm = get_multi_modal_llm()
if multimodal_llm:
kwargs["response_synthesizer"] = MultiModalSynthesizer(
multimodal_model=multimodal_llm,
)
# If index is index is LlamaCloudIndex
# use auto_routed mode for better query results
if index.__class__.__name__ == "LlamaCloudIndex":
if kwargs.get("retrieval_mode") is None:
kwargs["retrieval_mode"] = "auto_routed"
if multimodal_llm:
kwargs["retrieve_image_nodes"] = True
return index.as_query_engine(**kwargs)
def get_query_engine_tool(
index,
name: Optional[str] = None,
description: Optional[str] = None,
**kwargs,
) -> QueryEngineTool:
"""
Get a query engine tool for the given index.
Args:
index: The index to create a query engine for.
name (optional): The name of the tool.
description (optional): The description of the tool.
"""
if name is None:
name = "query_index"
if description is None:
description = (
"Use this tool to retrieve information about the text corpus from an index."
)
query_engine = create_query_engine(index, **kwargs)
return QueryEngineTool.from_defaults(
query_engine=query_engine,
name=name,
description=description,
)
class MultiModalSynthesizer(BaseSynthesizer):
"""
A synthesizer that summarizes text nodes and uses a multi-modal LLM to generate a response.
"""
def __init__(
self,
multimodal_model: MultiModalLLM,
response_synthesizer: Optional[BaseSynthesizer] = None,
text_qa_template: Optional[BasePromptTemplate] = None,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self._multi_modal_llm = multimodal_model
self._response_synthesizer = response_synthesizer or get_response_synthesizer()
self._text_qa_template = text_qa_template or DEFAULT_TEXT_QA_PROMPT_SEL
def _get_prompts(self, **kwargs) -> Dict[str, Any]:
return {
"text_qa_template": self._text_qa_template,
}
def _update_prompts(self, prompts: Dict[str, Any]) -> None:
if "text_qa_template" in prompts:
self._text_qa_template = prompts["text_qa_template"]
async def aget_response(
self,
*args,
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
return await self._response_synthesizer.aget_response(*args, **response_kwargs)
def get_response(self, *args, **kwargs) -> RESPONSE_TEXT_TYPE:
return self._response_synthesizer.get_response(*args, **kwargs)
async def asynthesize(
self,
query: QueryTextType,
nodes: List[NodeWithScore],
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
**response_kwargs: Any,
) -> RESPONSE_TYPE:
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
if len(image_nodes) == 0:
return await self._response_synthesizer.asynthesize(query, text_nodes)
# Summarize the text nodes to avoid exceeding the token limit
text_response = str(
await self._response_synthesizer.asynthesize(query, text_nodes)
)
fmt_prompt = self._text_qa_template.format(
context_str=text_response,
query_str=query.query_str, # type: ignore
)
llm_response = await self._multi_modal_llm.acomplete(
prompt=fmt_prompt,
image_documents=[
image_node.node
for image_node in image_nodes
if isinstance(image_node.node, ImageNode)
],
)
return Response(
response=str(llm_response),
source_nodes=nodes,
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
)
def synthesize(
self,
query: QueryTextType,
nodes: List[NodeWithScore],
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
**response_kwargs: Any,
) -> RESPONSE_TYPE:
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
if len(image_nodes) == 0:
return self._response_synthesizer.synthesize(query, text_nodes)
# Summarize the text nodes to avoid exceeding the token limit
text_response = str(self._response_synthesizer.synthesize(query, text_nodes))
fmt_prompt = self._text_qa_template.format(
context_str=text_response,
query_str=query.query_str, # type: ignore
)
llm_response = self._multi_modal_llm.complete(
prompt=fmt_prompt,
image_documents=[
image_node.node
for image_node in image_nodes
if isinstance(image_node.node, ImageNode)
],
)
return Response(
response=str(llm_response),
source_nodes=nodes,
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
)
@@ -1,8 +1,9 @@
"""Open Meteo weather map tool spec."""
import logging
import requests
import pytz
import pytz # type: ignore
import requests # type: ignore
from llama_index.core.tools import FunctionTool
logger = logging.getLogger(__name__)
@@ -9,7 +9,7 @@ from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.settings import Settings
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
def get_chat_engine(params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
top_k = int(os.getenv("TOP_K", 0))
@@ -33,10 +33,9 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
),
)
retriever = index.as_retriever(
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
)
if top_k != 0 and kwargs.get("similarity_top_k") is None:
kwargs["similarity_top_k"] = top_k
retriever = index.as_retriever(**kwargs)
return CondensePlusContextChatEngine(
llm=llm,
@@ -1,14 +1,9 @@
import {
BaseChatEngine,
BaseToolWithCall,
OpenAIAgent,
QueryEngineTool,
} from "llamaindex";
import { BaseChatEngine, BaseToolWithCall, LLMAgent } from "llamaindex";
import fs from "node:fs/promises";
import path from "node:path";
import { getDataSource } from "./index";
import { generateFilters } from "./queryFilter";
import { createTools } from "./tools";
import { createQueryEngineTool } from "./tools/query-engine";
export async function createChatEngine(documentIds?: string[], params?: any) {
const tools: BaseToolWithCall[] = [];
@@ -17,17 +12,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
// Delete this code if you don't have a data source
const index = await getDataSource(params);
if (index) {
tools.push(
new QueryEngineTool({
queryEngine: index.asQueryEngine({
preFilters: generateFilters(documentIds || []),
}),
metadata: {
name: "data_query_engine",
description: `A query engine for documents from your data source.`,
},
}),
);
tools.push(createQueryEngineTool(index, { documentIds }));
}
const configFile = path.join("config", "tools.json");
@@ -42,7 +27,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
tools.push(...(await createTools(toolConfig)));
}
const agent = new OpenAIAgent({
const agent = new LLMAgent({
tools,
systemPrompt: process.env.SYSTEM_PROMPT,
}) as unknown as BaseChatEngine;
@@ -48,11 +48,13 @@ export type CodeArtifact = {
port: number | null;
file_path: string;
code: string;
files?: string[];
};
export type CodeGeneratorParameter = {
requirement: string;
oldCode?: string;
sandboxFiles?: string[];
};
export type CodeGeneratorToolParams = {
@@ -75,6 +77,15 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
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"],
},
@@ -92,7 +103,11 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
const artifact = await this.generateArtifact(
input.requirement,
input.oldCode,
input.sandboxFiles, // help the generated code use exact files
);
if (input.sandboxFiles) {
artifact.files = input.sandboxFiles;
}
return artifact as JSONValue;
} catch (error) {
return { isError: true };
@@ -103,10 +118,12 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
async generateArtifact(
query: string,
oldCode?: string,
attachments?: string[],
): Promise<CodeArtifact> {
const userMessage = `
${query}
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
`;
const messages: ChatMessage[] = [
{ role: "system", content: CODE_GENERATION_PROMPT },
@@ -0,0 +1,296 @@
import { JSONSchemaType } from "ajv";
import fs from "fs";
import { BaseTool, Settings, ToolMetadata } from "llamaindex";
import Papa from "papaparse";
import path from "path";
import { saveDocument } from "../../llamaindex/documents/helper";
type ExtractMissingCellsParameter = {
filePath: string;
};
export type MissingCell = {
rowIndex: number;
columnIndex: number;
question: string;
};
const CSV_EXTRACTION_PROMPT = `You are a data analyst. You are given a table with missing cells.
Your task is to identify the missing cells and the questions needed to fill them.
IMPORTANT: Column indices should be 0-based
# Instructions:
- Understand the entire content of the table and the topics of the table.
- Identify the missing cells and the meaning of the data in the cells.
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
- Finally, only return the answer in JSON format with the following schema:
{
"missing_cells": [
{
"rowIndex": number,
"columnIndex": number,
"question": string
}
]
}
- If there are no missing cells, return an empty array.
- The answer is only the JSON object, nothing else and don't wrap it inside markdown code block.
# Example:
# | | Name | Age | City |
# |----|------|-----|------|
# | 0 | John | | Paris|
# | 1 | Mary | | |
# | 2 | | 30 | |
#
# Your thoughts:
# - The table is about people's names, ages, and cities.
# - Row: 1, Column: 2 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
# - Row: 1, Column: 3 (City column), Question: "In which city does Mary live? Please provide only the city name."
# Your answer:
# {
# "missing_cells": [
# {
# "rowIndex": 1,
# "columnIndex": 2,
# "question": "How old is Mary? Please provide only the numerical answer."
# },
# {
# "rowIndex": 1,
# "columnIndex": 3,
# "question": "In which city does Mary live? Please provide only the city name."
# }
# ]
# }
# Here is your task:
- Table content:
{table_content}
- Your answer:
`;
const DEFAULT_METADATA: ToolMetadata<
JSONSchemaType<ExtractMissingCellsParameter>
> = {
name: "extract_missing_cells",
description: `Use this tool to extract missing cells in a CSV file and generate questions to fill them. This tool only works with local file path.`,
parameters: {
type: "object",
properties: {
filePath: {
type: "string",
description: "The local file path to the CSV file.",
},
},
required: ["filePath"],
},
};
export interface ExtractMissingCellsParams {
metadata?: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
}
export class ExtractMissingCellsTool
implements BaseTool<ExtractMissingCellsParameter>
{
metadata: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
defaultExtractionPrompt: string;
constructor(params: ExtractMissingCellsParams) {
this.metadata = params.metadata ?? DEFAULT_METADATA;
this.defaultExtractionPrompt = CSV_EXTRACTION_PROMPT;
}
private readCsvFile(filePath: string): Promise<string[][]> {
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
reject(err);
return;
}
const parsedData = Papa.parse<string[]>(data, {
skipEmptyLines: false,
});
if (parsedData.errors.length) {
reject(parsedData.errors);
return;
}
// Ensure all rows have the same number of columns as the header
const maxColumns = parsedData.data[0].length;
const paddedRows = parsedData.data.map((row) => {
return [...row, ...Array(maxColumns - row.length).fill("")];
});
resolve(paddedRows);
});
});
}
private formatToMarkdownTable(data: string[][]): string {
if (data.length === 0) return "";
const maxColumns = data[0].length;
const headerRow = `| ${data[0].join(" | ")} |`;
const separatorRow = `| ${Array(maxColumns).fill("---").join(" | ")} |`;
const dataRows = data.slice(1).map((row) => {
return `| ${row.join(" | ")} |`;
});
return [headerRow, separatorRow, ...dataRows].join("\n");
}
async call(input: ExtractMissingCellsParameter): Promise<MissingCell[]> {
const { filePath } = input;
let tableContent: string[][];
try {
tableContent = await this.readCsvFile(filePath);
} catch (error) {
throw new Error(
`Failed to read CSV file. Make sure that you are reading a local file path (not a sandbox path).`,
);
}
const prompt = this.defaultExtractionPrompt.replace(
"{table_content}",
this.formatToMarkdownTable(tableContent),
);
const llm = Settings.llm;
const response = await llm.complete({
prompt,
});
const rawAnswer = response.text;
const parsedResponse = JSON.parse(rawAnswer) as {
missing_cells: MissingCell[];
};
if (!parsedResponse.missing_cells) {
throw new Error(
"The answer is not in the correct format. There should be a missing_cells array.",
);
}
const answer = parsedResponse.missing_cells;
return answer;
}
}
type FillMissingCellsParameter = {
filePath: string;
cells: {
rowIndex: number;
columnIndex: number;
answer: string;
}[];
};
const FILL_CELLS_METADATA: ToolMetadata<
JSONSchemaType<FillMissingCellsParameter>
> = {
name: "fill_missing_cells",
description: `Use this tool to fill missing cells in a CSV file with provided answers. This tool only works with local file path.`,
parameters: {
type: "object",
properties: {
filePath: {
type: "string",
description: "The local file path to the CSV file.",
},
cells: {
type: "array",
items: {
type: "object",
properties: {
rowIndex: { type: "number" },
columnIndex: { type: "number" },
answer: { type: "string" },
},
required: ["rowIndex", "columnIndex", "answer"],
},
description: "Array of cells to fill with their answers",
},
},
required: ["filePath", "cells"],
},
};
export interface FillMissingCellsParams {
metadata?: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
}
export class FillMissingCellsTool
implements BaseTool<FillMissingCellsParameter>
{
metadata: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
constructor(params: FillMissingCellsParams = {}) {
this.metadata = params.metadata ?? FILL_CELLS_METADATA;
}
async call(input: FillMissingCellsParameter): Promise<string> {
const { filePath, cells } = input;
// Read the CSV file
const fileContent = await new Promise<string>((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
// Parse CSV with PapaParse
const parseResult = Papa.parse<string[]>(fileContent, {
header: false, // Ensure the header is not treated as a separate object
skipEmptyLines: false, // Ensure empty lines are not skipped
});
if (parseResult.errors.length) {
throw new Error(
"Failed to parse CSV file: " + parseResult.errors[0].message,
);
}
const rows = parseResult.data;
// Fill the cells with answers
for (const cell of cells) {
// Adjust rowIndex to start from 1 for data rows
const adjustedRowIndex = cell.rowIndex + 1;
if (
adjustedRowIndex < rows.length &&
cell.columnIndex < rows[adjustedRowIndex].length
) {
rows[adjustedRowIndex][cell.columnIndex] = cell.answer;
}
}
// Convert back to CSV format
const updatedContent = Papa.unparse(rows, {
delimiter: parseResult.meta.delimiter,
});
// Use the helper function to write the file
const parsedPath = path.parse(filePath);
const newFileName = `${parsedPath.name}-filled${parsedPath.ext}`;
const newFilePath = path.join("output/tools", newFileName);
const newFileUrl = await saveDocument(newFilePath, updatedContent);
return (
"Successfully filled missing cells in the CSV file. File URL to show to the user: " +
newFileUrl
);
}
}
@@ -1,11 +1,19 @@
import { BaseToolWithCall } from "llamaindex";
import { ToolsFactory } from "llamaindex/tools/ToolsFactory";
import fs from "node:fs/promises";
import path from "node:path";
import { CodeGeneratorTool, CodeGeneratorToolParams } from "./code-generator";
import {
DocumentGenerator,
DocumentGeneratorParams,
} from "./document-generator";
import { DuckDuckGoSearchTool, DuckDuckGoToolParams } from "./duckduckgo";
import {
ExtractMissingCellsParams,
ExtractMissingCellsTool,
FillMissingCellsParams,
FillMissingCellsTool,
} from "./form-filling";
import { ImgGeneratorTool, ImgGeneratorToolParams } from "./img-gen";
import { InterpreterTool, InterpreterToolParams } from "./interpreter";
import { OpenAPIActionTool } from "./openapi-action";
@@ -54,6 +62,12 @@ const toolFactory: Record<string, ToolCreator> = {
document_generator: async (config: unknown) => {
return [new DocumentGenerator(config as DocumentGeneratorParams)];
},
form_filling: async (config: unknown) => {
return [
new ExtractMissingCellsTool(config as ExtractMissingCellsParams),
new FillMissingCellsTool(config as FillMissingCellsParams),
];
},
};
async function createLocalTools(
@@ -70,3 +84,19 @@ async function createLocalTools(
return tools;
}
export async function getConfiguredTools(
configPath?: string,
): Promise<BaseToolWithCall[]> {
const configFile = path.join(configPath ?? "config", "tools.json");
const toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
const tools = await createTools(toolConfig);
return tools;
}
export async function getTool(
toolName: string,
): Promise<BaseToolWithCall | undefined> {
const tools = await getConfiguredTools();
return tools.find((tool) => tool.metadata.name === toolName);
}
@@ -1,4 +1,4 @@
import { CodeInterpreter, Logs, Result } from "@e2b/code-interpreter";
import { Logs, Result, Sandbox } from "@e2b/code-interpreter";
import type { JSONSchemaType } from "ajv";
import fs from "fs";
import { BaseTool, ToolMetadata } from "llamaindex";
@@ -7,6 +7,8 @@ import path from "node:path";
export type InterpreterParameter = {
code: string;
sandboxFiles?: string[];
retryCount?: number;
};
export type InterpreterToolParams = {
@@ -18,7 +20,9 @@ export type InterpreterToolParams = {
export type InterpreterToolOutput = {
isError: boolean;
logs: Logs;
text?: string;
extraResult: InterpreterExtraResult[];
retryCount?: number;
};
type InterpreterExtraType =
@@ -41,8 +45,10 @@ export type InterpreterExtraResult = {
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.",
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: {
@@ -50,6 +56,21 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
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"],
},
@@ -57,10 +78,11 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
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?: CodeInterpreter;
codeInterpreter?: Sandbox;
constructor(params?: InterpreterToolParams) {
this.metadata = params?.metadata || DEFAULT_META_DATA;
@@ -80,38 +102,75 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
}
}
public async initInterpreter() {
public async initInterpreter(input: InterpreterParameter) {
if (!this.codeInterpreter) {
this.codeInterpreter = await CodeInterpreter.create({
this.codeInterpreter = await Sandbox.create({
apiKey: this.apiKey,
});
// upload files to sandbox 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(code: string): Promise<InterpreterToolOutput> {
public async codeInterpret(
input: InterpreterParameter,
): Promise<InterpreterToolOutput> {
console.log(
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
`Sandbox files: ${input.sandboxFiles}. Retry count: ${input.retryCount}`,
);
const interpreter = await this.initInterpreter();
const exec = await interpreter.notebook.execCell(code);
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.code);
const result = await this.codeInterpret(input);
return result;
}
async close() {
await this.codeInterpreter?.close();
await this.codeInterpreter?.kill();
}
private async getExtraResult(
@@ -0,0 +1,57 @@
import {
BaseQueryEngine,
CloudRetrieveParams,
LlamaCloudIndex,
MetadataFilters,
QueryEngineTool,
VectorStoreIndex,
} from "llamaindex";
import { generateFilters } from "../queryFilter";
interface QueryEngineParams {
documentIds?: string[];
topK?: number;
}
export function createQueryEngineTool(
index: VectorStoreIndex | LlamaCloudIndex,
params?: QueryEngineParams,
name?: string,
description?: string,
): QueryEngineTool {
return new QueryEngineTool({
queryEngine: createQueryEngine(index, params),
metadata: {
name: name || "query_engine",
description:
description ||
`Use this tool to retrieve information about the text corpus from an index.`,
},
});
}
function createQueryEngine(
index: VectorStoreIndex | LlamaCloudIndex,
params?: QueryEngineParams,
): BaseQueryEngine {
const baseQueryParams = {
similarityTopK:
params?.topK ??
(process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined),
};
if (index instanceof LlamaCloudIndex) {
return index.asQueryEngine({
...baseQueryParams,
retrieval_mode: "auto_routed",
preFilters: generateFilters(
params?.documentIds || [],
) as CloudRetrieveParams["filters"],
});
}
return index.asQueryEngine({
...baseQueryParams,
preFilters: generateFilters(params?.documentIds || []) as MetadataFilters,
});
}
@@ -1,27 +1,60 @@
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",
};
const UPLOADED_FOLDER = "output/uploaded";
export const UPLOADED_FOLDER = "output/uploaded";
export async function storeAndParseFile(
filename: string,
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);
const filepath = path.join(UPLOADED_FOLDER, filename);
await saveDocument(filepath, fileBuffer);
for (const document of documents) {
document.metadata = {
...document.metadata,
@@ -48,12 +81,6 @@ export async function saveDocument(filepath: string, content: string | Buffer) {
if (path.isAbsolute(filepath)) {
throw new Error("Absolute file paths are not allowed.");
}
const fileName = path.basename(filepath);
if (!/^[a-zA-Z0-9_.-]+$/.test(fileName)) {
throw new Error(
"File name is not allowed to contain any special characters.",
);
}
if (!process.env.FILESERVER_URL_PREFIX) {
throw new Error("FILESERVER_URL_PREFIX environment variable is not set.");
}
@@ -71,3 +98,8 @@ export async function saveDocument(filepath: string, content: string | Buffer) {
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, "_");
}
@@ -3,11 +3,12 @@ import {
IngestionPipeline,
Settings,
SimpleNodeParser,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
export async function runPipeline(
currentIndex: VectorStoreIndex,
currentIndex: VectorStoreIndex | null,
documents: Document[],
) {
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
@@ -21,8 +22,27 @@ export async function runPipeline(
],
});
const nodes = await pipeline.run({ documents });
await currentIndex.insertNodes(nodes);
currentIndex.storageContext.docStore.persist();
console.log("Added nodes to the vector store.");
return documents.map((document) => document.id_);
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,27 +1,36 @@
import { LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { storeAndParseFile } from "./helper";
import { DocumentFile } from "../streaming/annotations";
import { parseFile, storeFile } from "./helper";
import { runPipeline } from "./pipeline";
export async function uploadDocument(
index: VectorStoreIndex | LlamaCloudIndex,
filename: string,
index: VectorStoreIndex | LlamaCloudIndex | null,
name: string,
raw: string,
): Promise<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 {
return [
documentIds = [
await LLamaCloudFileService.addFileToPipeline(
projectId,
pipelineId,
new File([fileBuffer], filename, { type: mimeType }),
new File([fileBuffer], name, { type: mimeType }),
{ private: "true" },
),
];
@@ -36,9 +45,17 @@ export async function uploadDocument(
}
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);
}
// run the pipeline for other vector store indexes
const documents = await storeAndParseFile(filename, fileBuffer, mimeType);
return runPipeline(index, documents);
// Update file metadata with document IDs
fileMetadata.refs = documentIds;
return fileMetadata;
}
@@ -1,19 +1,21 @@
import { JSONValue, Message } from "ai";
import { MessageContent, MessageContentDetail } from "llamaindex";
import {
ChatMessage,
MessageContent,
MessageContentDetail,
MessageType,
} from "llamaindex";
import { UPLOADED_FOLDER } from "../documents/helper";
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
export type DocumentFileContent = {
type: "ref" | "text";
value: string[] | string;
};
export type DocumentFile = {
id: string;
filename: string;
filesize: number;
filetype: DocumentFileType;
content: DocumentFileContent;
name: string;
size: number;
type: string;
url: string;
refs?: string[];
};
type Annotation = {
@@ -29,28 +31,25 @@ export function isValidMessages(messages: Message[]): boolean {
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 ids: string[] = [];
const files: DocumentFile[] = [];
for (const { type, data } of annotations) {
if (
type === "document_file" &&
"files" in data &&
Array.isArray(data.files)
) {
const files = data.files as DocumentFile[];
for (const file of files) {
if (Array.isArray(file.content.value)) {
// it's an array, so it's an array of doc IDs
ids.push(...file.content.value);
}
}
files.push(...data.files);
}
}
return ids;
return files;
}
export function retrieveMessageContent(messages: Message[]): MessageContent {
@@ -65,6 +64,78 @@ export function retrieveMessageContent(messages: Message[]): MessageContent {
];
}
export function convertToChatHistory(messages: Message[]): ChatMessage[] {
if (!messages || !Array.isArray(messages)) {
return [];
}
const agentHistory = retrieveAgentHistoryMessage(messages);
if (agentHistory) {
const previousMessages = messages.slice(0, -1);
return [...previousMessages, agentHistory].map((msg) => ({
role: msg.role as MessageType,
content: msg.content,
}));
}
return messages.map((msg) => ({
role: msg.role as MessageType,
content: msg.content,
}));
}
function retrieveAgentHistoryMessage(
messages: Message[],
maxAgentMessages = 10,
): ChatMessage | null {
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
messages,
{ role: "assistant", type: "agent" },
).slice(-maxAgentMessages);
if (agentAnnotations.length > 0) {
const messageContent =
"Here is the previous conversation of agents:\n" +
agentAnnotations.map((annotation) => annotation.data.text).join("\n");
return {
role: "assistant",
content: messageContent,
};
}
return null;
}
function getFileContent(file: DocumentFile): string {
let defaultContent = `=====File: ${file.name}=====\n`;
// Include file URL if it's available
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) =>
@@ -105,13 +176,10 @@ function retrieveLatestArtifact(messages: Message[]): MessageContentDetail[] {
}
function convertAnnotations(messages: Message[]): MessageContentDetail[] {
// annotations from the last user message that has annotations
const annotations: Annotation[] =
messages
.slice()
.reverse()
.find((message) => message.role === "user" && message.annotations)
?.annotations?.map(getValidAnnotation) || [];
// get all annotations from user messages
const annotations: Annotation[] = messages
.filter((message) => message.role === "user" && message.annotations)
.flatMap((message) => message.annotations?.map(getValidAnnotation) || []);
if (annotations.length === 0) return [];
const content: MessageContentDetail[] = [];
@@ -131,25 +199,11 @@ function convertAnnotations(messages: Message[]): MessageContentDetail[] {
"files" in data &&
Array.isArray(data.files)
) {
// get all CSV files and convert their whole content to one text message
// currently CSV files are the only files where we send the whole content - we don't use an index
const csvFiles: DocumentFile[] = data.files.filter(
(file: DocumentFile) => file.filetype === "csv",
);
if (csvFiles && csvFiles.length > 0) {
const csvContents = csvFiles.map((file: DocumentFile) => {
const fileContent = Array.isArray(file.content.value)
? file.content.value.join("\n")
: file.content.value;
return "```csv\n" + fileContent + "\n```";
});
const text =
"Use the following CSV content:\n" + csvContents.join("\n\n");
content.push({
type: "text",
text,
});
}
const fileContent = data.files.map(getFileContent).join("\n");
content.push({
type: "text",
text: fileContent,
});
}
});

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