mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-02 19:14:28 -04:00
Compare commits
309 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 131e63ae4a | |||
| 4e06714cdd | |||
| 18c8d2540c | |||
| d4b4338f54 | |||
| b4e41aa526 | |||
| 860b9d46d4 | |||
| f73d46bf10 | |||
| eec237c5fe | |||
| 5450096e96 | |||
| 163492f189 | |||
| a84743c576 | |||
| fc5e56efa5 | |||
| a7a6592441 | |||
| af21426952 | |||
| 9077cae2f5 | |||
| 765d2c4fff | |||
| 25667d45e9 | |||
| d31910a303 | |||
| 9852e7399c | |||
| 95227a7539 | |||
| 71f29ea85d | |||
| 27d2499aff | |||
| a07f320e6d | |||
| f9a057ddde | |||
| aedd73d8c0 | |||
| da4505aff7 | |||
| 63e961e635 | |||
| fe90a7e7ee | |||
| 02b2473103 | |||
| f17449b90a | |||
| 28c8808ce3 | |||
| 0a7dfcf84b | |||
| 6e70e327d3 | |||
| 8b371d8347 | |||
| 30fe269575 | |||
| 49c35b834b | |||
| 82c2580ee5 | |||
| fc5b266a40 | |||
| f8f97d2c00 | |||
| 9c2e094883 | |||
| 00f0b3ae03 | |||
| 4663dec81d | |||
| 7f14e47f56 | |||
| 6925676013 | |||
| 44b34fb464 | |||
| a108911fc1 | |||
| 282eaa07fc | |||
| 80db5f7c46 | |||
| 7a22c9f56d | |||
| 8431b788ad | |||
| 2b712cebec | |||
| 6edea6af5c | |||
| d79d1652d1 | |||
| 8ebd8d7039 | |||
| 2b8aaa835d | |||
| 1fe21f85bd | |||
| b9570b2eb9 | |||
| 00009ae53e | |||
| 63558c11fa | |||
| 9172fed2e8 | |||
| 78ccde78fc | |||
| 02510703d8 | |||
| ed59927bd0 | |||
| 9f866aa981 | |||
| b8f78612b8 | |||
| 4a8346900d | |||
| 42e63842d0 | |||
| fa803787e3 | |||
| c5559d8e59 | |||
| 0182368744 | |||
| ff46bd6153 | |||
| 2209409cdb | |||
| 623f8b811b | |||
| 384a1368dd | |||
| 189c0e3f6c | |||
| 99b8247bc9 | |||
| 74c5a15450 | |||
| 9293e330ac | |||
| 6d1b6b9372 | |||
| a8162a9269 | |||
| f3577c50d6 | |||
| a5f5c9dc9c | |||
| 2be68d1c7f | |||
| 8c80cc05ce | |||
| dfd4fd58ab | |||
| 0a69fe09fa | |||
| de88b32208 | |||
| ef88bff211 | |||
| 7562cb48d6 | |||
| 9dde6d0288 | |||
| 98a82b0b25 | |||
| 7db72b6f2e | |||
| 3d41488301 | |||
| 1ee05eaf4b | |||
| 75e1f6104c | |||
| 88220f1dd2 | |||
| 6304114ef5 | |||
| 6335de1174 | |||
| b9184ff59a | |||
| cd3fcd0512 | |||
| a47d778602 | |||
| 7f4ac228ee | |||
| 5263bde8e7 | |||
| 4dee65b93d | |||
| c60182a925 | |||
| 0e78ba4603 | |||
| 7652b2b388 | |||
| d18f0399e5 | |||
| 3790ca0250 | |||
| 16e6124db2 | |||
| 51dc0e4334 | |||
| 5a7216e36d | |||
| 27a1b9fdf2 | |||
| 04ddebcd64 | |||
| 3e8057a83a | |||
| 12ed570a53 | |||
| bde3daae08 | |||
| 727eb105ea | |||
| ef070c0b4b | |||
| 70f7dcacc8 | |||
| cf65162bef | |||
| 7c2a3f69a7 | |||
| c7b7672062 | |||
| cb8d535d9b | |||
| 811cb13cba | |||
| 0213fe07dd | |||
| b31fa80326 | |||
| 40c5c8412c | |||
| 0031e674c9 | |||
| 6e9184dd37 | |||
| fa28cb5d0d | |||
| 8c1087f5f1 | |||
| 27333973f1 | |||
| cf3ec97a4c | |||
| 505b8e944a | |||
| 578f7f9e50 | |||
| adc40cf770 | |||
| 7bce7386d5 | |||
| c011455dc4 | |||
| 38a8be8d12 | |||
| 6e70eb4d11 | |||
| 917e862202 | |||
| e363bfeecc | |||
| b6da3c2419 | |||
| 71fbe1b18f | |||
| 8105c5cf06 | |||
| c16deed864 | |||
| 6a409cbbc6 | |||
| a1892bef26 | |||
| 2f7e0220b5 | |||
| 435109fef0 | |||
| b1f3d5222f | |||
| e2c61884ef | |||
| fd4abb3bdd | |||
| bedde2bf20 | |||
| 5cd12fa90d | |||
| 72b71952aa | |||
| 2f8feabcba | |||
| a8a8c247e2 | |||
| 4fa2b76f3d | |||
| 4ead8e14c2 | |||
| 90398400c6 | |||
| 8f670a935c | |||
| f04f60d555 | |||
| 1ffd3c915b | |||
| 57e7638083 | |||
| 22ac2cae61 | |||
| 8077195601 | |||
| 8ce4a8513d | |||
| 1d93775f04 | |||
| 3fb93c7939 | |||
| e248dc56bc | |||
| bd5e39a390 | |||
| de2c7523dd | |||
| 9fd832c8b0 | |||
| b2c76dc7b6 | |||
| 2b7a5d8797 | |||
| d93ec803f5 | |||
| a6023b695b | |||
| 81ef7f0f93 | |||
| 8faf9170cf | |||
| c49a5e1620 | |||
| 8b2de431f2 | |||
| d746c75e49 | |||
| c87978ab96 | |||
| 26359a0ac9 | |||
| 4039d3d1ea | |||
| 3ec5163304 | |||
| 878cfc2ca1 | |||
| 9b5835b71c | |||
| 04a9c71759 | |||
| 0bfdbc1dfe | |||
| fbcaebcbcf | |||
| b6dd7a9acb | |||
| 09e3022ad6 | |||
| 9f739b9834 | |||
| c06ec4f14c | |||
| e7d30b1c69 | |||
| e974c8ef11 | |||
| 8890e27a14 | |||
| 072e69b465 | |||
| 83a648df0a | |||
| dcf52abdba | |||
| 9a09e8c7e2 | |||
| a4a55239e9 | |||
| c5c7eee04d | |||
| 8b89ac547f | |||
| f43399cc18 | |||
| df51361ca1 | |||
| c67daeb2be | |||
| af6ac9a444 | |||
| 22245ca9fd | |||
| 81b67794ef | |||
| 5c13646e55 | |||
| 43474a51ff | |||
| cf11b233c6 | |||
| fd9fb42ace | |||
| 92798f73dd | |||
| e71d8bd6e2 | |||
| e25e112873 | |||
| 048187cce3 | |||
| 6bd76fbfb1 | |||
| a553d5051e | |||
| b0becaa8dc | |||
| 6a42542642 | |||
| f936a470f3 | |||
| df9cca5a52 | |||
| dc9ee895a7 | |||
| 98ff3c2e77 | |||
| 0900413689 | |||
| 8dc6a2bf5a | |||
| 23b735717d | |||
| bd4714ca8d | |||
| 455ab6862e | |||
| 58e6c150c0 | |||
| e57e9813dd | |||
| 7302880c5f | |||
| 624c721ac4 | |||
| d2c66cf550 | |||
| df96159e88 | |||
| 32fb32ab18 | |||
| 3b57bdcf12 | |||
| a221cfc11f | |||
| d3f92f8a69 | |||
| d1026ea784 | |||
| 791ca7c945 | |||
| 07fcefde5d | |||
| 9ecd061262 | |||
| 344d832d3d | |||
| a0aab03226 | |||
| a8073063c5 | |||
| aeb6fef4da | |||
| 64732f05aa | |||
| 588e0d607b | |||
| f2c3389168 | |||
| 5093b37c05 | |||
| f383f0cbe9 | |||
| b3c969dae5 | |||
| 628e16df7c | |||
| aa69014d04 | |||
| 293557cbb4 | |||
| b46d050fc3 | |||
| 02ed277dd0 | |||
| 48b96ff188 | |||
| 9c9decbb88 | |||
| 0748f2e8d7 | |||
| 3079162806 | |||
| 48c19c6e62 | |||
| d75c08e7d8 | |||
| 8f03f8d4bc | |||
| 19c57d945a | |||
| 9112d0801e | |||
| 93b797c162 | |||
| d53b760fd0 | |||
| a880c7c016 | |||
| 7b116ce7f7 | |||
| d1232fb1d5 | |||
| bedf199236 | |||
| c1510bd3fa | |||
| 69b9ce76bf | |||
| 9ced116e1a | |||
| fae9bcd65a | |||
| 2091fea2b4 | |||
| 563b51d76d | |||
| 88c88bf16d | |||
| cd6ebf7295 | |||
| 50b2ddbbf5 | |||
| 5fe2d519d2 | |||
| 09f1db3b5e | |||
| cb3be7d1d4 | |||
| 5474a1f182 | |||
| 1148ddba53 | |||
| 9e945ed355 | |||
| 6342163df2 | |||
| a42fa53a6b | |||
| 099f626586 | |||
| 956538eeb0 | |||
| 555f6b2905 | |||
| d8bc271a21 | |||
| f29561cde2 | |||
| 442abae8ac | |||
| 0ad2207684 | |||
| bfde30deed | |||
| 96fdb83abf | |||
| b7e0072c9c | |||
| 81bc340dda | |||
| ddf3aef7dc | |||
| 1f5a26f3a8 | |||
| 48188ca3f9 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add support E2B code interpreter tool for FastAPI
|
||||
@@ -0,0 +1,6 @@
|
||||
# coderabbit.yml
|
||||
reviews:
|
||||
path_instructions:
|
||||
- path: "templates/**"
|
||||
instructions: |
|
||||
For files under the `templates` folder, do not report 'Missing Dependencies Detected' errors.
|
||||
@@ -9,15 +9,17 @@ env:
|
||||
POETRY_VERSION: "1.6.1"
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: create-llama
|
||||
e2e-python:
|
||||
name: python
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [18, 20]
|
||||
node-version: [20]
|
||||
python-version: ["3.11"]
|
||||
os: [macos-latest, windows-latest]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["fastapi"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -58,15 +60,85 @@ jobs:
|
||||
run: pnpm run pack-install
|
||||
working-directory: .
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm run e2e
|
||||
- name: Run Playwright tests for Python
|
||||
run: pnpm run e2e:python
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
working-directory: .
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
name: playwright-report-python
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
e2e-typescript:
|
||||
name: typescript
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [18, 20]
|
||||
python-version: ["3.11"]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs", "express"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: ${{ env.POETRY_VERSION }}
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
working-directory: .
|
||||
|
||||
- name: Build create-llama
|
||||
run: pnpm run build
|
||||
working-directory: .
|
||||
|
||||
- name: Install
|
||||
run: pnpm run pack-install
|
||||
working-directory: .
|
||||
|
||||
- name: Run Playwright tests for TypeScript
|
||||
run: pnpm run e2e:typescript
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
working-directory: .
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-typescript
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -30,3 +30,13 @@ jobs:
|
||||
|
||||
- name: Run Prettier
|
||||
run: pnpm run format
|
||||
|
||||
- name: Run Python format check
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
args: "format --check"
|
||||
|
||||
- name: Run Python lint
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
args: "check"
|
||||
|
||||
@@ -17,6 +17,9 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -46,5 +46,12 @@ e2e/cache
|
||||
# intellij
|
||||
**/.idea
|
||||
|
||||
# Python
|
||||
.mypy_cache/
|
||||
|
||||
# build artifacts
|
||||
create-llama-*.tgz
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
!.vscode/settings.json
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pnpm format
|
||||
pnpm lint
|
||||
uvx ruff format --check templates/
|
||||
|
||||
+575
@@ -1,5 +1,580 @@
|
||||
# create-llama
|
||||
|
||||
## 0.3.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4e06714: Fix the error: Unable to view file sources due to CORS.
|
||||
|
||||
## 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
|
||||
|
||||
- 3d41488: feat: use selected llamacloud for multiagent
|
||||
|
||||
## 0.2.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 75e1f61: Fix cannot query public document from llamacloud
|
||||
- 88220f1: fix workflow doesn't stop when user presses stop generation button
|
||||
- 75e1f61: Fix typescript templates cannot upload file to llamacloud
|
||||
- 88220f1: Bump llama_index@0.11.17
|
||||
|
||||
## 0.2.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cd3fcd0: bump: use LlamaIndexTS 0.6.18
|
||||
- 6335de1: Fix using LlamaCloud selector does not use the configured values in the environment (Python)
|
||||
|
||||
## 0.2.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0e78ba4: Fix: programmatically ensure index for LlamaCloud
|
||||
- 0e78ba4: Fix .env not loaded on poetry run generate
|
||||
- 7f4ac22: Don't need to run generate script for LlamaCloud
|
||||
- 5263bde: Use selected LlamaCloud index in multi-agent template
|
||||
|
||||
## 0.2.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 16e6124: Bump package for llamatrace observability
|
||||
- 3790ca0: Add multi-agent task selector for TS template
|
||||
- d18f039: Add e2b code artifact tool for the FastAPI template
|
||||
|
||||
## 0.2.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5a7216e: feat: implement artifact tool in TS
|
||||
|
||||
## 0.2.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 04ddebc: Add publisher agent to multi-agents for generating documents (PDF and HTML)
|
||||
- 04ddebc: Allow tool selection for multi-agents (Python and TS)
|
||||
|
||||
## 0.2.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 70f7dca: feat: add test deps for llamaparse
|
||||
- ef070c0: Add multi agents template for Typescript
|
||||
|
||||
## 0.2.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c2a3f6: fix: postgres import
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cb8d535: Fix only produces one agent event
|
||||
|
||||
## 0.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0213fe0: Update dependencies for vector stores and add e2e test to ensure that they work as expected.
|
||||
|
||||
## 0.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0031e67: Bump llama-index to 0.11.11 for the multi-agent template
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 505b8e9: bump: use latest ai package version
|
||||
- cf3ec97: Dynamically select model for Groq
|
||||
- 8c1087f: feat: enhance style for markdown
|
||||
|
||||
## 0.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- adc40cf: fix: vercel ai update crash sending annotations
|
||||
|
||||
## 0.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 38a8be8: fix: filter in mongo vector store
|
||||
|
||||
## 0.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 917e862: Fix errors in building the frontend
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b6da3c2: Ensure the generation script always works
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8105c5c: Add env config for next questions feature
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a409cb: Bump web and database reader packages
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 435109f: Add multi-agents template based on workflows
|
||||
|
||||
## 0.1.44
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bedde2b: Change metadata filters to use already existing documents in LlamaCloud Index
|
||||
- 5cd12fa: Use one callback manager per request
|
||||
- 5cd12fa: Bump llama_index version to 0.11.1
|
||||
- fd4abb3: Fix to use filename for uploaded documents in NextJS
|
||||
- 2f8feab: Simplify CLI interface
|
||||
|
||||
## 0.1.43
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4fa2b76: feat: implement citation for TS
|
||||
|
||||
## 0.1.42
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8f670a9: Allow relative URL in documents
|
||||
|
||||
## 0.1.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 57e7638: Use the retrieval defaults from LlamaCloud
|
||||
|
||||
## 0.1.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ce4a85: Add UI for extractor template
|
||||
|
||||
## 0.1.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3fb93c7: Use LlamaCloud pipeline for data ingestion in TS (private file uploads and generate script)
|
||||
|
||||
## 0.1.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd5e39a: Fix error that files in sub folders of 'data' are not displayed
|
||||
|
||||
## 0.1.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9fd832c: Add in-text citation references
|
||||
|
||||
## 0.1.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2b7a5d8: Fix: private file upload not working in Python without LlamaCloud
|
||||
|
||||
## 0.1.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 81ef7f0: Use LlamaCloud pipeline for data ingestion (private file uploads and generate script)
|
||||
|
||||
## 0.1.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c49a5e1: Add error handling for generating the next question
|
||||
- c49a5e1: Fix wrong api key variable in Azure OpenAI provider
|
||||
|
||||
## 0.1.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d746c75: Add Weaviate vector store (Typescript)
|
||||
|
||||
## 0.1.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3ec5163: Add Weaviate vector database support (Python)
|
||||
|
||||
## 0.1.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 04a9c71: Cluster nodes by document
|
||||
|
||||
## 0.1.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 09e3022: Add support for LlamaTrace (Python)
|
||||
- c06ec4f: Fix imports for MongoDB
|
||||
- b6dd7a9: Always send chat data when submit message
|
||||
|
||||
## 0.1.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8890e27: Let user change indexes in LlamaCloud projects
|
||||
|
||||
## 0.1.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9a09e8c: Fix Vercel deployment
|
||||
|
||||
## 0.1.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c5c7eee: Make components reusable for chat-llamaindex
|
||||
|
||||
## 0.1.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f43399c: Add metadatafilters to context chat engine (Typescript)
|
||||
|
||||
## 0.1.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c67daeb: fix: missing set private to false for default generate.py
|
||||
|
||||
## 0.1.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 43474a5: Configure LlamaCloud organization ID for Python
|
||||
- cf11b23: Add Azure code interpreter for Python and TS
|
||||
- fd9fb42: Add Azure OpenAI as model provider
|
||||
- 5c13646: Fix starter questions not working in python backend
|
||||
|
||||
## 0.1.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6bd76fb: Add template for structured extraction
|
||||
|
||||
## 0.1.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b0becaa: Add e2e testing for llamacloud datasource
|
||||
- df9cca5: Upgrade pdf viewer
|
||||
|
||||
## 0.1.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd4714c: Filter private documents for Typescript (Using MetadataFilters) and update to LlamaIndexTS 0.5.7
|
||||
- 58e6c15: Add using LlamaParse for private file uploader
|
||||
- 455ab68: Display files in sources using LlamaCloud indexes.
|
||||
- 23b7357: Use gpt-4o-mini as default model
|
||||
- 0900413: Add suggestions for next questions.
|
||||
|
||||
## 0.1.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 624c721: Update to LlamaIndex 0.10.55
|
||||
|
||||
## 0.1.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- df96159: Use Qdrant FastEmbed as local embedding provider
|
||||
- 32fb32a: Support upload document files: pdf, docx, txt
|
||||
|
||||
## 0.1.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d1026ea: support Mistral as llm and embedding
|
||||
- a221cfc: Use LlamaParse for all the file types that it supports (if activated)
|
||||
|
||||
## 0.1.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ecd061: Add new template for a multi-agents app
|
||||
|
||||
## 0.1.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a0aab03: Add T-System's LLMHUB as a model provider
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 64732f0: Fix the issue of images not showing with the sandbox URL from OpenAI's models
|
||||
- aeb6fef: use llamacloud for chat
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f2c3389: chore: update to llamaindex 0.4.3
|
||||
- 5093b37: Remove non-working file selectors for Linux
|
||||
|
||||
## 0.1.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b3c969d: Add image generator tool
|
||||
|
||||
## 0.1.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- aa69014: Fix NextJS for TS 5.2
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 48b96ff: Add DuckDuckGo search tool
|
||||
- 9c9decb: Reuse function tool instances and improve e2b interpreter tool for Python
|
||||
- 02ed277: Add Groq as a model provider
|
||||
- 0748f2e: Remove hard-coded Gemini supported models
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9112d08: Add OpenAPI tool for Typescript
|
||||
- 8f03f8d: Add OLLAMA_REQUEST_TIMEOUT variable to config Ollama timeout (Python)
|
||||
- 8f03f8d: Apply nest_asyncio for llama parse
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a42fa53: Add CSV upload
|
||||
- 563b51d: Fix Vercel streaming (python) to stream data events instantly
|
||||
- d60b3c5: Add E2B code interpreter tool for FastAPI
|
||||
- 956538e: Add OpenAPI action tool for FastAPI
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
# Create LlamaIndex App
|
||||
# Create Llama
|
||||
|
||||
The easiest way to get started with [LlamaIndex](https://www.llamaindex.ai/) is by using `create-llama`. This CLI tool enables you to quickly start building a new LlamaIndex application, with everything set up for you.
|
||||
|
||||
## Get started
|
||||
|
||||
Just run
|
||||
|
||||
```bash
|
||||
npx create-llama@latest
|
||||
```
|
||||
|
||||
to get started, or see below for more options. Once your app is generated, run
|
||||
to get started, or watch this video for a demo session:
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/c4a7fe18-8e30-498a-96f8-78127dd706b9" width="100%">
|
||||
|
||||
Once your app is generated, run
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -18,21 +24,25 @@ to start the development server. You can then visit [http://localhost:3000](http
|
||||
|
||||
## What you'll get
|
||||
|
||||
- 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 (see below)
|
||||
- Your choice of 3 back-ends:
|
||||
- A set of pre-configured use cases to get you started, e.g. Agentic RAG, Data Analysis, Report Generation, etc.
|
||||
- A Next.js-powered front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
|
||||
- Your choice of two back-ends:
|
||||
- **Next.js**: if you select this option, you’ll 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, you’ll 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, you’ll 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:
|
||||
|
||||
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:
|
||||
|
||||
@@ -48,13 +58,9 @@ 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-4-turbo` LLM and `text-embedding-3-large` embedding model.
|
||||
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
|
||||
|
||||
If you want to use different OpenAI models, add the `--ask-models` CLI parameter.
|
||||
|
||||
@@ -84,45 +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? › Chat
|
||||
✔ 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
|
||||
? 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
|
||||
|
||||
|
||||
+44
-26
@@ -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 } from "./helpers";
|
||||
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)) {
|
||||
@@ -142,14 +132,42 @@ export async function createApp({
|
||||
)} and learn how to get started.`,
|
||||
);
|
||||
|
||||
if (args.observability === "opentelemetry") {
|
||||
outputObservability(args.observability);
|
||||
|
||||
if (
|
||||
dataSources.some((dataSource) => dataSource.type === "file") &&
|
||||
process.platform === "linux"
|
||||
) {
|
||||
console.log(
|
||||
`\n${yellow("Observability")}: Visit the ${terminalLink(
|
||||
"documentation",
|
||||
"https://traceloop.com/docs/openllmetry/integrations",
|
||||
)} to set up the environment variables and start seeing execution traces.`,
|
||||
yellow(
|
||||
`You can add your own data files to ${terminalLink(
|
||||
"data",
|
||||
`file://${root}/data`,
|
||||
)} folder manually.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
function outputObservability(observability?: TemplateObservability) {
|
||||
switch (observability) {
|
||||
case "traceloop":
|
||||
console.log(
|
||||
`\n${yellow("Observability")}: Visit the ${terminalLink(
|
||||
"documentation",
|
||||
"https://traceloop.com/docs/openllmetry/integrations",
|
||||
)} to set up the environment variables and start seeing execution traces.`,
|
||||
);
|
||||
break;
|
||||
case "llamatrace":
|
||||
console.log(
|
||||
`\n${yellow("Observability")}: LlamaTrace has been configured for your project. Visit the ${terminalLink(
|
||||
"LlamaTrace dashboard",
|
||||
"https://llamatrace.com/login",
|
||||
)} to view your traces and monitor your application.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +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 type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateType,
|
||||
TemplateUI,
|
||||
} from "../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "./utils";
|
||||
|
||||
const templateTypes: TemplateType[] = ["streaming"];
|
||||
const templateFrameworks: TemplateFramework[] = [
|
||||
"nextjs",
|
||||
"express",
|
||||
"fastapi",
|
||||
];
|
||||
const dataSources: string[] = ["--no-files", "--example-file"];
|
||||
const templateUIs: TemplateUI[] = ["shadcn", "html"];
|
||||
const templatePostInstallActions: TemplatePostInstallAction[] = [
|
||||
"none",
|
||||
"runApp",
|
||||
];
|
||||
|
||||
for (const templateType of templateTypes) {
|
||||
for (const templateFramework of templateFrameworks) {
|
||||
for (const dataSource of dataSources) {
|
||||
for (const templateUI of templateUIs) {
|
||||
for (const templatePostInstallAction of templatePostInstallActions) {
|
||||
const appType: AppType =
|
||||
templateFramework === "nextjs" ? "" : "--frontend";
|
||||
test.describe(`try create-llama ${templateType} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
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,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
templateUI,
|
||||
vectorDb,
|
||||
appType,
|
||||
port,
|
||||
externalPort,
|
||||
templatePostInstallAction,
|
||||
);
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form input", "hello");
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(res) => {
|
||||
return (
|
||||
res.url().includes("/api/chat") && res.status() === 200
|
||||
);
|
||||
},
|
||||
{
|
||||
timeout: 1000 * 60,
|
||||
},
|
||||
),
|
||||
page.click("form button[type=submit]"),
|
||||
]);
|
||||
const text = await response.text();
|
||||
console.log("AI response when submitting message: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Backend frameworks should response when calling non-streaming chat API", async ({
|
||||
request,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${externalPort}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
console.log("AI response when calling API: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
|
||||
// TODO: add support for other templates
|
||||
|
||||
if (
|
||||
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
|
||||
) {
|
||||
// vectorDBs, tools, and data source combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"qdrant",
|
||||
"chroma",
|
||||
"weaviate",
|
||||
];
|
||||
|
||||
const toolOptions = [
|
||||
"wikipedia.WikipediaToolSpec",
|
||||
"google.GoogleSearchToolSpec",
|
||||
"document_generator",
|
||||
"artifact",
|
||||
];
|
||||
|
||||
const dataSources = [
|
||||
"--example-file",
|
||||
"--web-source https://www.example.com",
|
||||
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
];
|
||||
|
||||
const observabilityOptions = ["llamatrace", "traceloop"];
|
||||
|
||||
test.describe("Mypy check", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
// Test vector databases
|
||||
for (const vectorDb of vectorDbs) {
|
||||
test(`Mypy check for vectorDB: ${vectorDb}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (vectorDb !== "none") {
|
||||
if (vectorDb === "pg") {
|
||||
expect(pyprojectContent).toContain(
|
||||
"llama-index-vector-stores-postgres",
|
||||
);
|
||||
} else {
|
||||
expect(pyprojectContent).toContain(
|
||||
`llama-index-vector-stores-${vectorDb}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test tools
|
||||
for (const tool of toolOptions) {
|
||||
test(`Mypy check for tool: ${tool}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: tool,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (tool === "wikipedia.WikipediaToolSpec") {
|
||||
expect(pyprojectContent).toContain("wikipedia");
|
||||
}
|
||||
if (tool === "google.GoogleSearchToolSpec") {
|
||||
expect(pyprojectContent).toContain("google");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test data sources
|
||||
for (const dataSource of dataSources) {
|
||||
const dataSourceType = dataSource.split(" ")[0];
|
||||
test(`Mypy check for data source: ${dataSourceType}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (dataSource.includes("--web-source")) {
|
||||
expect(pyprojectContent).toContain("llama-index-readers-web");
|
||||
}
|
||||
if (dataSource.includes("--db-source")) {
|
||||
expect(pyprojectContent).toContain("llama-index-readers-database");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test observability options
|
||||
for (const observability of observabilityOptions) {
|
||||
test(`Mypy check for observability: ${observability}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createAndCheckLlamaProject({
|
||||
options,
|
||||
}: {
|
||||
options: RunCreateLlamaOptions;
|
||||
}): Promise<{ pyprojectPath: string; projectPath: string }> {
|
||||
const result = await runCreateLlama(options);
|
||||
const name = result.projectName;
|
||||
const projectPath = path.join(options.cwd, name);
|
||||
|
||||
// Check if the app folder exists
|
||||
expect(fs.existsSync(projectPath)).toBeTruthy();
|
||||
|
||||
// Check if pyproject.toml exists
|
||||
const pyprojectPath = path.join(projectPath, "pyproject.toml");
|
||||
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
POETRY_VIRTUALENVS_IN_PROJECT: "true",
|
||||
};
|
||||
|
||||
// Run poetry install
|
||||
try {
|
||||
const { stdout: installStdout, stderr: installStderr } = await execAsync(
|
||||
"poetry install",
|
||||
{ cwd: projectPath, env },
|
||||
);
|
||||
console.log("poetry install stdout:", installStdout);
|
||||
console.error("poetry install stderr:", installStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running poetry install:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run poetry run mypy
|
||||
try {
|
||||
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
|
||||
"poetry run mypy .",
|
||||
{ cwd: projectPath, env },
|
||||
);
|
||||
console.log("poetry run mypy stdout:", mypyStdout);
|
||||
console.error("poetry run mypy stderr:", mypyStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running mypy:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If we reach this point without throwing an error, the test passes
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
return { pyprojectPath, projectPath };
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* 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 type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUI,
|
||||
} from "../../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateUseCases = ["financial_report", "blog", "form_filling"];
|
||||
|
||||
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";
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/* 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 type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUI,
|
||||
} from "../../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
|
||||
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
const isNode18 = process.version.startsWith("v18");
|
||||
const isLlamaCloud = dataSource === "--llamacloud";
|
||||
// llamacloud is using File API which is not supported on node 18
|
||||
if (isNode18 && isLlamaCloud) {
|
||||
test.skip(true, "Skipping tests for Node 18 and LlamaCloud data source");
|
||||
}
|
||||
|
||||
let port: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(res) => {
|
||||
return res.url().includes("/api/chat") && res.status() === 200;
|
||||
},
|
||||
{
|
||||
timeout: 1000 * 60,
|
||||
},
|
||||
),
|
||||
page.click("form button[type=submit]"),
|
||||
]);
|
||||
const text = await response.text();
|
||||
console.log("AI response when submitting message: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Backend frameworks should response when calling non-streaming chat API", async ({
|
||||
request,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${port}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
console.log("AI response when calling API: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "nextjs";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
|
||||
// vectorDBs combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"qdrant",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"chroma",
|
||||
"llamacloud",
|
||||
"weaviate",
|
||||
];
|
||||
|
||||
test.describe("Test resolve TS dependencies", () => {
|
||||
// Test vector DBs without LlamaParse
|
||||
for (const vectorDb of vectorDbs) {
|
||||
const optionDescription = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
|
||||
|
||||
test(`Vector DB test - ${optionDescription}`, async () => {
|
||||
await runTest(vectorDb, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Test LlamaParse with vectorDB 'none'
|
||||
test(`LlamaParse test - vectorDb: none, dataSource: ${dataSource}, llamaParse: true`, async () => {
|
||||
await runTest("none", true);
|
||||
});
|
||||
|
||||
async function runTest(
|
||||
vectorDb: TemplateVectorDB | "none",
|
||||
useLlamaParse: boolean,
|
||||
) {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const result = await runCreateLlama({
|
||||
cwd: cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework: templateFramework,
|
||||
dataSource: dataSource,
|
||||
vectorDb: vectorDb,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
tools: undefined,
|
||||
useLlamaParse: useLlamaParse,
|
||||
});
|
||||
const name = result.projectName;
|
||||
|
||||
// Check if the app folder exists
|
||||
const appDir = path.join(cwd, name);
|
||||
const dirExists = fs.existsSync(appDir);
|
||||
expect(dirExists).toBeTruthy();
|
||||
|
||||
// Install dependencies using pnpm
|
||||
try {
|
||||
const { stderr: installStderr } = await execAsync(
|
||||
"pnpm install --prefer-offline",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error installing dependencies:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run tsc type check and capture the output
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"pnpm exec tsc -b --diagnostics",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
// Check if there's any error output
|
||||
expect(stderr).toBeFalsy();
|
||||
|
||||
// Log the stdout for debugging purposes
|
||||
console.log("TypeScript type-check output:", stdout);
|
||||
} catch (error) {
|
||||
console.error("Error running tsc:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
+123
-95
@@ -18,141 +18,132 @@ export type CreateLlamaResult = {
|
||||
appProcess: ChildProcess;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export async function checkAppHasStarted(
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
timeout: number,
|
||||
) {
|
||||
if (frontend) {
|
||||
await Promise.all([
|
||||
waitPort({
|
||||
host: "localhost",
|
||||
port: port,
|
||||
timeout,
|
||||
}),
|
||||
waitPort({
|
||||
host: "localhost",
|
||||
port: externalPort,
|
||||
timeout,
|
||||
}),
|
||||
]).catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
} else {
|
||||
let wPort: number;
|
||||
if (framework === "nextjs") {
|
||||
wPort = port;
|
||||
} else {
|
||||
wPort = externalPort;
|
||||
}
|
||||
await waitPort({
|
||||
host: "localhost",
|
||||
port: wPort,
|
||||
timeout,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
export type RunCreateLlamaOptions = {
|
||||
cwd: string;
|
||||
templateType: TemplateType;
|
||||
templateFramework: TemplateFramework;
|
||||
dataSource: string;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
templateUI?: TemplateUI;
|
||||
appType?: AppType;
|
||||
llamaCloudProjectName?: string;
|
||||
llamaCloudIndexName?: string;
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
useCase?: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export async function runCreateLlama(
|
||||
cwd: string,
|
||||
templateType: TemplateType,
|
||||
templateFramework: TemplateFramework,
|
||||
dataSource: string,
|
||||
templateUI: TemplateUI,
|
||||
vectorDb: TemplateVectorDB,
|
||||
appType: AppType,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
postInstallAction: TemplatePostInstallAction,
|
||||
): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
throw new Error("Setting OPENAI_API_KEY is mandatory to run tests");
|
||||
export async function runCreateLlama({
|
||||
cwd,
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
useCase,
|
||||
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
|
||||
throw new Error(
|
||||
"Setting the OPENAI_API_KEY and LLAMA_CLOUD_API_KEY is mandatory to run tests",
|
||||
);
|
||||
}
|
||||
const name = [
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
dataSource.split(" ")[0],
|
||||
templateUI,
|
||||
appType,
|
||||
].join("-");
|
||||
const command = [
|
||||
|
||||
// Handle different data source types
|
||||
let dataSourceArgs = [];
|
||||
if (dataSource.includes("--web-source" || "--db-source")) {
|
||||
const webSource = dataSource.split(" ")[1];
|
||||
dataSourceArgs.push("--web-source", webSource);
|
||||
} else if (dataSource.includes("--db-source")) {
|
||||
const dbSource = dataSource.split(" ")[1];
|
||||
dataSourceArgs.push("--db-source", dbSource);
|
||||
} else {
|
||||
dataSourceArgs.push(dataSource);
|
||||
}
|
||||
|
||||
const commandArgs = [
|
||||
"create-llama",
|
||||
name,
|
||||
"--template",
|
||||
templateType,
|
||||
"--framework",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
"--ui",
|
||||
templateUI,
|
||||
...dataSourceArgs,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--open-ai-key",
|
||||
process.env.OPENAI_API_KEY,
|
||||
appType,
|
||||
"--use-pnpm",
|
||||
"--use-npm",
|
||||
"--port",
|
||||
port,
|
||||
"--external-port",
|
||||
externalPort,
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--tools",
|
||||
"none",
|
||||
"--no-llama-parse",
|
||||
tools ?? "none",
|
||||
"--observability",
|
||||
"none",
|
||||
].join(" ");
|
||||
];
|
||||
|
||||
if (templateUI) {
|
||||
commandArgs.push("--ui", templateUI);
|
||||
}
|
||||
if (appType) {
|
||||
commandArgs.push(appType);
|
||||
}
|
||||
if (useLlamaParse) {
|
||||
commandArgs.push("--use-llama-parse");
|
||||
} else {
|
||||
commandArgs.push("--no-llama-parse");
|
||||
}
|
||||
if (observability) {
|
||||
commandArgs.push("--observability", observability);
|
||||
}
|
||||
if ((templateType === "multiagent" || templateType === "reflex") && useCase) {
|
||||
commandArgs.push("--use-case", useCase);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
console.log(`running command '${command}' in ${cwd}`);
|
||||
const appProcess = exec(command, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
LLAMA_CLOUD_PROJECT_NAME: llamaCloudProjectName,
|
||||
LLAMA_CLOUD_INDEX_NAME: llamaCloudIndexName,
|
||||
},
|
||||
});
|
||||
appProcess.stderr?.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
console.error(data.toString());
|
||||
});
|
||||
appProcess.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
throw new Error(`create-llama command was failed!`);
|
||||
throw new Error(`create-llama command failed with exit code ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for app to start
|
||||
if (postInstallAction === "runApp") {
|
||||
await checkAppHasStarted(
|
||||
appType === "--frontend",
|
||||
templateFramework,
|
||||
port,
|
||||
externalPort,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
await waitPorts([port]);
|
||||
} else if (postInstallAction === "dependencies") {
|
||||
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
|
||||
} else {
|
||||
// wait create-llama to exit
|
||||
// we don't test install dependencies for now, so just set timeout for 10 seconds
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("create-llama timeout error"));
|
||||
}, 1000 * 10);
|
||||
appProcess.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error("create-llama command was failed!"));
|
||||
} else {
|
||||
clearTimeout(timeout);
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
// wait 10 seconds for create-llama to exit
|
||||
await waitForProcess(appProcess, 1000 * 10);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -166,3 +157,40 @@ export async function createTestDir() {
|
||||
await mkdir(cwd, { recursive: true });
|
||||
return cwd;
|
||||
}
|
||||
|
||||
async function waitPorts(ports: number[]): Promise<void> {
|
||||
const waitForPort = async (port: number): Promise<void> => {
|
||||
await waitPort({
|
||||
host: "localhost",
|
||||
port: port,
|
||||
// wait max. 5 mins for start up of app
|
||||
timeout: 1000 * 60 * 5,
|
||||
});
|
||||
};
|
||||
try {
|
||||
await Promise.all(ports.map(waitForPort));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForProcess(
|
||||
process: ChildProcess,
|
||||
timeoutMs: number,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Process timeout error"));
|
||||
}, timeoutMs);
|
||||
|
||||
process.on("exit", (code) => {
|
||||
clearTimeout(timeout);
|
||||
if (code !== 0 && code !== null) {
|
||||
reject(new Error("Process exited with non-zero code"));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+93
-60
@@ -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,
|
||||
@@ -36,74 +77,66 @@ export async function writeLoadersConfig(
|
||||
dataSources: TemplateDataSource[],
|
||||
useLlamaParse?: boolean,
|
||||
) {
|
||||
if (dataSources.length === 0) return; // no datasources, no config needed
|
||||
const loaderConfig = new Document({});
|
||||
// Web loader config
|
||||
const loaderConfig: Record<string, any> = {};
|
||||
|
||||
// Always set file loader config
|
||||
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
|
||||
|
||||
if (dataSources.some((ds) => ds.type === "web")) {
|
||||
const webLoaderConfig = new Document({});
|
||||
|
||||
// Create config for browser driver arguments
|
||||
const driverArgNodeValue = webLoaderConfig.createNode([
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
]);
|
||||
driverArgNodeValue.commentBefore =
|
||||
" The arguments to pass to the webdriver. E.g.: add --headless to run in headless mode";
|
||||
webLoaderConfig.set("driver_arguments", driverArgNodeValue);
|
||||
|
||||
// Create config for urls
|
||||
const urlConfigs = dataSources
|
||||
.filter((ds) => ds.type === "web")
|
||||
.map((ds) => {
|
||||
const dsConfig = ds.config as WebSourceConfig;
|
||||
return {
|
||||
base_url: dsConfig.baseUrl,
|
||||
prefix: dsConfig.prefix,
|
||||
depth: dsConfig.depth,
|
||||
};
|
||||
});
|
||||
const urlConfigNode = webLoaderConfig.createNode(urlConfigs);
|
||||
urlConfigNode.commentBefore = ` base_url: The URL to start crawling with
|
||||
prefix: Only crawl URLs matching the specified prefix
|
||||
depth: The maximum depth for BFS traversal
|
||||
You can add more websites by adding more entries (don't forget the - prefix from YAML)`;
|
||||
webLoaderConfig.set("urls", urlConfigNode);
|
||||
|
||||
// Add web config to the loaders config
|
||||
loaderConfig.set("web", webLoaderConfig);
|
||||
loaderConfig.web = createWebLoaderConfig(dataSources);
|
||||
}
|
||||
|
||||
// File loader config
|
||||
if (dataSources.some((ds) => ds.type === "file")) {
|
||||
// Add documentation to web loader config
|
||||
const node = loaderConfig.createNode({
|
||||
use_llama_parse: useLlamaParse,
|
||||
});
|
||||
node.commentBefore = ` use_llama_parse: Use LlamaParse if \`true\`. Needs a \`LLAMA_CLOUD_API_KEY\` from https://cloud.llamaindex.ai set as environment variable`;
|
||||
loaderConfig.set("file", node);
|
||||
}
|
||||
|
||||
// DB loader config
|
||||
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
|
||||
if (dbLoaders.length > 0) {
|
||||
const dbLoaderConfig = new Document({});
|
||||
const configEntries = dbLoaders.map((ds) => {
|
||||
const dsConfig = ds.config as DbSourceConfig;
|
||||
return {
|
||||
uri: dsConfig.uri,
|
||||
queries: [dsConfig.queries],
|
||||
};
|
||||
});
|
||||
|
||||
const node = dbLoaderConfig.createNode(configEntries);
|
||||
node.commentBefore = ` The configuration for the database loader, only supports MySQL and PostgreSQL databases for now.
|
||||
uri: The URI for the database. E.g.: mysql+pymysql://user:password@localhost:3306/db or postgresql+psycopg2://user:password@localhost:5432/db
|
||||
query: The query to fetch data from the database. E.g.: SELECT * FROM table`;
|
||||
loaderConfig.set("db", node);
|
||||
loaderConfig.db = createDbLoaderConfig(dbLoaders);
|
||||
}
|
||||
|
||||
// Create a new Document with the loaderConfig
|
||||
const yamlDoc = new Document(loaderConfig);
|
||||
|
||||
// Write loaders config
|
||||
const loaderConfigPath = path.join(root, "config", "loaders.yaml");
|
||||
await fs.mkdir(path.join(root, "config"), { recursive: true });
|
||||
await fs.writeFile(loaderConfigPath, yaml.stringify(loaderConfig));
|
||||
await fs.writeFile(loaderConfigPath, yaml.stringify(yamlDoc));
|
||||
}
|
||||
|
||||
function createWebLoaderConfig(dataSources: TemplateDataSource[]): any {
|
||||
const webLoaderConfig: Record<string, any> = {};
|
||||
|
||||
// Create config for browser driver arguments
|
||||
webLoaderConfig.driver_arguments = [
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
];
|
||||
|
||||
// Create config for urls
|
||||
const urlConfigs = dataSources
|
||||
.filter((ds) => ds.type === "web")
|
||||
.map((ds) => {
|
||||
const dsConfig = ds.config as WebSourceConfig;
|
||||
return {
|
||||
base_url: dsConfig.baseUrl,
|
||||
prefix: dsConfig.prefix,
|
||||
depth: dsConfig.depth,
|
||||
};
|
||||
});
|
||||
webLoaderConfig.urls = urlConfigs;
|
||||
|
||||
return webLoaderConfig;
|
||||
}
|
||||
|
||||
function createFileLoaderConfig(useLlamaParse?: boolean): any {
|
||||
return {
|
||||
use_llama_parse: useLlamaParse,
|
||||
};
|
||||
}
|
||||
|
||||
function createDbLoaderConfig(dbLoaders: TemplateDataSource[]): any {
|
||||
return dbLoaders.map((ds) => {
|
||||
const dsConfig = ds.config as DbSourceConfig;
|
||||
return {
|
||||
uri: dsConfig.uri,
|
||||
queries: [dsConfig.queries],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
+280
-45
@@ -2,12 +2,23 @@ import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { TOOL_SYSTEM_PROMPT_ENV_VAR, Tool } from "./tools";
|
||||
import {
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateObservability,
|
||||
TemplateType,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
|
||||
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT =
|
||||
"You are a helpful assistant who helps users with their questions.";
|
||||
|
||||
const DATA_SOURCES_PROMPT =
|
||||
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
|
||||
|
||||
export type EnvVar = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -60,7 +71,7 @@ const getVectorDBEnvs = (
|
||||
{
|
||||
name: "PG_CONNECTION_STRING",
|
||||
description:
|
||||
"For generating a connection URI, see https://docs.timescale.com/use-timescale/latest/services/create-a-service\nThe PostgreSQL connection string.",
|
||||
"For generating a connection URI, see https://supabase.com/vector\nThe PostgreSQL connection string.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -133,6 +144,42 @@ const getVectorDBEnvs = (
|
||||
"Optional API key for authenticating requests to Qdrant.",
|
||||
},
|
||||
];
|
||||
case "llamacloud":
|
||||
return [
|
||||
{
|
||||
name: "LLAMA_CLOUD_INDEX_NAME",
|
||||
description:
|
||||
"The name of the LlamaCloud index to use (part of the LlamaCloud project).",
|
||||
value: "test",
|
||||
},
|
||||
{
|
||||
name: "LLAMA_CLOUD_PROJECT_NAME",
|
||||
description: "The name of the LlamaCloud project.",
|
||||
value: "Default",
|
||||
},
|
||||
{
|
||||
name: "LLAMA_CLOUD_BASE_URL",
|
||||
description:
|
||||
"The base URL for the LlamaCloud API. Only change this for non-production environments",
|
||||
value: "https://api.cloud.llamaindex.ai",
|
||||
},
|
||||
{
|
||||
name: "LLAMA_CLOUD_ORGANIZATION_ID",
|
||||
description:
|
||||
"The organization ID for the LlamaCloud project (uses default organization if not specified)",
|
||||
},
|
||||
...(framework === "nextjs"
|
||||
? // activate index selector per default (not needed for non-NextJS backends as it's handled by createFrontendEnvFile)
|
||||
[
|
||||
{
|
||||
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
|
||||
description:
|
||||
"Let's the user change indexes in LlamaCloud projects",
|
||||
value: "true",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
case "chroma":
|
||||
const envs = [
|
||||
{
|
||||
@@ -141,11 +188,11 @@ const getVectorDBEnvs = (
|
||||
},
|
||||
{
|
||||
name: "CHROMA_HOST",
|
||||
description: "The API endpoint for your Chroma database",
|
||||
description: "The hostname for your Chroma database. Eg: localhost",
|
||||
},
|
||||
{
|
||||
name: "CHROMA_PORT",
|
||||
description: "The port for your Chroma database",
|
||||
description: "The port for your Chroma database. Eg: 8000",
|
||||
},
|
||||
];
|
||||
// TS Version doesn't support config local storage path
|
||||
@@ -158,8 +205,31 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
|
||||
});
|
||||
}
|
||||
return envs;
|
||||
case "weaviate":
|
||||
return [
|
||||
{
|
||||
name: "WEAVIATE_CLUSTER_URL",
|
||||
description:
|
||||
"The URL of the Weaviate cloud cluster, see: https://weaviate.io/developers/wcs/connect",
|
||||
},
|
||||
{
|
||||
name: "WEAVIATE_API_KEY",
|
||||
description: "The API key for the Weaviate cloud cluster",
|
||||
},
|
||||
{
|
||||
name: "WEAVIATE_INDEX_NAME",
|
||||
description:
|
||||
"(Optional) The collection name to use, default is LlamaIndex if not specified",
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
name: "STORAGE_CACHE_DIR",
|
||||
description: "The directory to store the local storage cache.",
|
||||
value: ".cache",
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -185,6 +255,10 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
description: "Dimension of the embedding model to use.",
|
||||
value: modelConfig.dimensions.toString(),
|
||||
},
|
||||
{
|
||||
name: "CONVERSATION_STARTERS",
|
||||
description: "The questions to help users get started (multi-line).",
|
||||
},
|
||||
...(modelConfig.provider === "openai"
|
||||
? [
|
||||
{
|
||||
@@ -211,6 +285,15 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "groq"
|
||||
? [
|
||||
{
|
||||
name: "GROQ_API_KEY",
|
||||
description: "The Groq API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "gemini"
|
||||
? [
|
||||
{
|
||||
@@ -229,6 +312,71 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "mistral"
|
||||
? [
|
||||
{
|
||||
name: "MISTRAL_API_KEY",
|
||||
description: "The Mistral API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "azure-openai"
|
||||
? [
|
||||
{
|
||||
name: "AZURE_OPENAI_API_KEY",
|
||||
description: "The Azure OpenAI key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
{
|
||||
name: "AZURE_OPENAI_ENDPOINT",
|
||||
description: "The Azure OpenAI endpoint to use.",
|
||||
},
|
||||
{
|
||||
name: "AZURE_OPENAI_API_VERSION",
|
||||
description: "The Azure OpenAI API version to use.",
|
||||
},
|
||||
{
|
||||
name: "AZURE_OPENAI_LLM_DEPLOYMENT",
|
||||
description:
|
||||
"The Azure OpenAI deployment to use for LLM deployment.",
|
||||
},
|
||||
{
|
||||
name: "AZURE_OPENAI_EMBEDDING_DEPLOYMENT",
|
||||
description:
|
||||
"The Azure OpenAI deployment to use for embedding deployment.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(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"
|
||||
? [
|
||||
{
|
||||
name: "T_SYSTEMS_LLMHUB_BASE_URL",
|
||||
description:
|
||||
"The base URL for the T-Systems AI Foundation Model API. Eg: http://localhost:11434",
|
||||
value: TSYSTEMS_LLMHUB_API_URL,
|
||||
},
|
||||
{
|
||||
name: "T_SYSTEMS_LLMHUB_API_KEY",
|
||||
description: "API Key for T-System's AI Foundation Model.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -265,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;
|
||||
};
|
||||
|
||||
@@ -274,7 +429,6 @@ const getEngineEnvs = (): EnvVar[] => {
|
||||
name: "TOP_K",
|
||||
description:
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
value: "3",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -296,43 +450,126 @@ const getToolEnvs = (tools?: Tool[]): EnvVar[] => {
|
||||
return toolEnvs;
|
||||
};
|
||||
|
||||
const getSystemPromptEnv = (tools?: Tool[]): EnvVar => {
|
||||
const defaultSystemPrompt =
|
||||
"You are a helpful assistant who helps users with their questions.";
|
||||
|
||||
const getSystemPromptEnv = (
|
||||
tools?: Tool[],
|
||||
dataSources?: TemplateDataSource[],
|
||||
template?: TemplateType,
|
||||
): EnvVar[] => {
|
||||
const systemPromptEnv: EnvVar[] = [];
|
||||
// build tool system prompt by merging all tool system prompts
|
||||
let toolSystemPrompt = "";
|
||||
tools?.forEach((tool) => {
|
||||
const toolSystemPromptEnv = tool.envVars?.find(
|
||||
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
);
|
||||
if (toolSystemPromptEnv) {
|
||||
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
|
||||
}
|
||||
});
|
||||
// multiagent template doesn't need system prompt
|
||||
if (template !== "multiagent") {
|
||||
let toolSystemPrompt = "";
|
||||
tools?.forEach((tool) => {
|
||||
const toolSystemPromptEnv = tool.envVars?.find(
|
||||
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
);
|
||||
if (toolSystemPromptEnv) {
|
||||
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
const systemPrompt = toolSystemPrompt
|
||||
? `\"${toolSystemPrompt}\"`
|
||||
: defaultSystemPrompt;
|
||||
const systemPrompt =
|
||||
'"' +
|
||||
DEFAULT_SYSTEM_PROMPT +
|
||||
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
|
||||
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
|
||||
'"';
|
||||
|
||||
return {
|
||||
name: "SYSTEM_PROMPT",
|
||||
description: "The system prompt for the AI model.",
|
||||
value: systemPrompt,
|
||||
};
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_PROMPT",
|
||||
description: "The system prompt for the AI model.",
|
||||
value: systemPrompt,
|
||||
});
|
||||
}
|
||||
if (tools?.length == 0 && (dataSources?.length ?? 0 > 0)) {
|
||||
const citationPrompt = `'You have provided information from a knowledge base that has been passed to you in nodes of information.
|
||||
Each node has useful metadata such as node ID, file name, page, etc.
|
||||
Please add the citation to the data node for each sentence or paragraph that you reference in the provided information.
|
||||
The citation format is: . [citation:<node_id>]()
|
||||
Where the <node_id> is the unique identifier of the data node.
|
||||
|
||||
Example:
|
||||
We have two nodes:
|
||||
node_id: xyz
|
||||
file_name: llama.pdf
|
||||
|
||||
node_id: abc
|
||||
file_name: animal.pdf
|
||||
|
||||
User question: Tell me a fun fact about Llama.
|
||||
Your answer:
|
||||
A baby llama is called "Cria" [citation:xyz]().
|
||||
It often live in desert [citation:abc]().
|
||||
It\\'s cute animal.
|
||||
'`;
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_CITATION_PROMPT",
|
||||
description:
|
||||
"An additional system prompt to add citation when responding to user questions.",
|
||||
value: citationPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
return systemPromptEnv;
|
||||
};
|
||||
|
||||
const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
|
||||
const nextQuestionEnvs: EnvVar[] = [
|
||||
{
|
||||
name: "NEXT_QUESTION_PROMPT",
|
||||
description: `Customize prompt to generate the next question suggestions based on the conversation history.
|
||||
Disable this prompt to disable the next question suggestions feature.`,
|
||||
value: `"You're a helpful assistant! Your task is to suggest the next question that user might ask.
|
||||
Here is the conversation history
|
||||
---------------------
|
||||
{conversation}
|
||||
---------------------
|
||||
Given the conversation history, please give me 3 questions that user might ask next!
|
||||
Your answer should be wrapped in three sticks which follows the following format:
|
||||
\`\`\`
|
||||
<question 1>
|
||||
<question 2>
|
||||
<question 3>
|
||||
\`\`\`"`,
|
||||
},
|
||||
];
|
||||
|
||||
if (template === "multiagent" || template === "streaming") {
|
||||
return nextQuestionEnvs;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getObservabilityEnvs = (
|
||||
observability?: TemplateObservability,
|
||||
): EnvVar[] => {
|
||||
if (observability === "llamatrace") {
|
||||
return [
|
||||
{
|
||||
name: "PHOENIX_API_KEY",
|
||||
description:
|
||||
"API key for LlamaTrace observability. Retrieve from https://llamatrace.com/login",
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const createBackendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
llamaCloudKey?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
modelConfig: ModelConfig;
|
||||
framework: TemplateFramework;
|
||||
dataSources?: TemplateDataSource[];
|
||||
port?: number;
|
||||
tools?: Tool[];
|
||||
},
|
||||
opts: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "llamaCloudKey"
|
||||
| "vectorDb"
|
||||
| "modelConfig"
|
||||
| "framework"
|
||||
| "dataSources"
|
||||
| "template"
|
||||
| "port"
|
||||
| "tools"
|
||||
| "observability"
|
||||
>,
|
||||
) => {
|
||||
// Init env values
|
||||
const envFileName = ".env";
|
||||
@@ -342,15 +579,15 @@ export const createBackendEnvFile = async (
|
||||
description: `The Llama Cloud API key.`,
|
||||
value: opts.llamaCloudKey,
|
||||
},
|
||||
// Add model environment variables
|
||||
// Add environment variables of each component
|
||||
...getModelEnvs(opts.modelConfig),
|
||||
// Add engine environment variables
|
||||
...getEngineEnvs(),
|
||||
// Add vector database environment variables
|
||||
...getVectorDBEnvs(opts.vectorDb, opts.framework),
|
||||
...getFrameworkEnvs(opts.framework, opts.port),
|
||||
...getToolEnvs(opts.tools),
|
||||
getSystemPromptEnv(opts.tools),
|
||||
...getTemplateEnvs(opts.template),
|
||||
...getObservabilityEnvs(opts.observability),
|
||||
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
|
||||
];
|
||||
// Render and write env file
|
||||
const content = renderEnvVar(envVars);
|
||||
@@ -361,16 +598,14 @@ 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",
|
||||
value: opts.vectorDb === "llamacloud" ? "true" : "false",
|
||||
},
|
||||
];
|
||||
const content = renderEnvVar(defaultFrontendEnvs);
|
||||
|
||||
+106
-59
@@ -8,6 +8,7 @@ import { writeLoadersConfig } from "./datasources";
|
||||
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { installLlamapackProject } from "./llama-pack";
|
||||
import { makeDir } from "./make-dir";
|
||||
import { isHavingPoetryLockFile, tryPoetryRun } from "./poetry";
|
||||
import { installPythonTemplate } from "./python";
|
||||
import { downloadAndExtractRepo } from "./repo";
|
||||
@@ -22,6 +23,31 @@ import {
|
||||
} from "./types";
|
||||
import { installTSTemplate } from "./typescript";
|
||||
|
||||
const checkForGenerateScript = (
|
||||
modelConfig: ModelConfig,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
llamaCloudKey?: string,
|
||||
useLlamaParse?: boolean,
|
||||
) => {
|
||||
const missingSettings = [];
|
||||
|
||||
if (!modelConfig.isConfigured()) {
|
||||
missingSettings.push("your model provider API key");
|
||||
}
|
||||
|
||||
const llamaCloudApiKey = llamaCloudKey ?? process.env["LLAMA_CLOUD_API_KEY"];
|
||||
const isRequiredLlamaCloudKey = useLlamaParse || vectorDb === "llamacloud";
|
||||
if (isRequiredLlamaCloudKey && !llamaCloudApiKey) {
|
||||
missingSettings.push("your LLAMA_CLOUD_API_KEY");
|
||||
}
|
||||
|
||||
if (vectorDb !== "none" && vectorDb !== "llamacloud") {
|
||||
missingSettings.push("your Vector DB environment variables");
|
||||
}
|
||||
|
||||
return missingSettings;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
async function generateContextData(
|
||||
framework: TemplateFramework,
|
||||
@@ -37,12 +63,15 @@ async function generateContextData(
|
||||
? "poetry run generate"
|
||||
: `${packageManager} run generate`,
|
||||
)}`;
|
||||
const modelConfigured = modelConfig.isConfigured();
|
||||
const llamaCloudKeyConfigured = useLlamaParse
|
||||
? llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
: true;
|
||||
const hasVectorDb = vectorDb && vectorDb !== "none";
|
||||
if (modelConfigured && llamaCloudKeyConfigured && !hasVectorDb) {
|
||||
|
||||
const missingSettings = checkForGenerateScript(
|
||||
modelConfig,
|
||||
vectorDb,
|
||||
llamaCloudKey,
|
||||
useLlamaParse,
|
||||
);
|
||||
|
||||
if (!missingSettings.length) {
|
||||
// If all the required environment variables are set, run the generate script
|
||||
if (framework === "fastapi") {
|
||||
if (isHavingPoetryLockFile()) {
|
||||
@@ -62,30 +91,47 @@ async function generateContextData(
|
||||
}
|
||||
}
|
||||
|
||||
// generate the message of what to do to run the generate script manually
|
||||
const settings = [];
|
||||
if (!modelConfigured) settings.push("your model provider API key");
|
||||
if (!llamaCloudKeyConfigured) settings.push("your Llama Cloud key");
|
||||
if (hasVectorDb) settings.push("your Vector DB environment variables");
|
||||
const settingsMessage =
|
||||
settings.length > 0 ? `After setting ${settings.join(" and ")}, ` : "";
|
||||
const generateMessage = `run ${runGenerate} to generate the context data.`;
|
||||
console.log(`\n${settingsMessage}${generateMessage}\n\n`);
|
||||
const settingsMessage = `After setting ${missingSettings.join(" and ")}, run ${runGenerate} to generate the context data.`;
|
||||
console.log(`\n${settingsMessage}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
const copyContextData = async (
|
||||
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[],
|
||||
) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,12 +166,15 @@ export const installTemplate = async (
|
||||
|
||||
if (props.framework === "fastapi") {
|
||||
await installPythonTemplate(props);
|
||||
// write loaders configuration (currently Python only)
|
||||
await writeLoadersConfig(
|
||||
props.root,
|
||||
props.dataSources,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
if (props.vectorDb !== "llamacloud") {
|
||||
// write loaders configuration (currently Python only)
|
||||
// not needed for LlamaCloud as it has its own loaders
|
||||
await writeLoadersConfig(
|
||||
props.root,
|
||||
props.dataSources,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await installTSTemplate(props);
|
||||
}
|
||||
@@ -141,45 +190,43 @@ export const installTemplate = async (
|
||||
// This is a backend, so we need to copy the test data and create the env file.
|
||||
|
||||
// Copy the environment file to the target directory.
|
||||
await createBackendEnvFile(props.root, {
|
||||
modelConfig: props.modelConfig,
|
||||
llamaCloudKey: props.llamaCloudKey,
|
||||
vectorDb: props.vectorDb,
|
||||
framework: props.framework,
|
||||
dataSources: props.dataSources,
|
||||
port: props.externalPort,
|
||||
tools: props.tools,
|
||||
});
|
||||
if (
|
||||
props.template === "streaming" ||
|
||||
props.template === "multiagent" ||
|
||||
props.template === "reflex"
|
||||
) {
|
||||
await createBackendEnvFile(props.root, props);
|
||||
}
|
||||
|
||||
if (props.dataSources.length > 0) {
|
||||
await prepareContextData(
|
||||
props.root,
|
||||
props.dataSources.filter((ds) => ds.type === "file"),
|
||||
);
|
||||
|
||||
if (
|
||||
props.dataSources.length > 0 &&
|
||||
(props.postInstallAction === "runApp" ||
|
||||
props.postInstallAction === "dependencies")
|
||||
) {
|
||||
console.log("\nGenerating context data...\n");
|
||||
await copyContextData(
|
||||
props.root,
|
||||
props.dataSources.filter((ds) => ds.type === "file"),
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.modelConfig,
|
||||
props.packageManager,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
if (
|
||||
props.postInstallAction === "runApp" ||
|
||||
props.postInstallAction === "dependencies"
|
||||
) {
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.modelConfig,
|
||||
props.packageManager,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create tool-output directory
|
||||
if (props.tools && props.tools.length > 0) {
|
||||
await fsExtra.mkdir(path.join(props.root, "tool-output"));
|
||||
}
|
||||
// Create outputs directory
|
||||
await makeDir(path.join(props.root, "output/tools"));
|
||||
await makeDir(path.join(props.root, "output/uploaded"));
|
||||
await makeDir(path.join(props.root, "output/llamacloud"));
|
||||
} else {
|
||||
// this is a frontend for a full-stack app, create .env file with model information
|
||||
await createFrontendEnvFile(props.root, {
|
||||
customApiPath: props.customApiPath,
|
||||
vectorDb: props.vectorDb,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = [
|
||||
"claude-3-opus",
|
||||
@@ -70,9 +69,7 @@ export async function askAnthropicQuestions({
|
||||
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) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
|
||||
"gpt-35-turbo": { openAIModel: "gpt-3.5-turbo" },
|
||||
"gpt-35-turbo-16k": {
|
||||
openAIModel: "gpt-3.5-turbo-16k",
|
||||
},
|
||||
"gpt-4o": { openAIModel: "gpt-4o" },
|
||||
"gpt-4o-mini": { openAIModel: "gpt-4o-mini" },
|
||||
"gpt-4": { openAIModel: "gpt-4" },
|
||||
"gpt-4-32k": { openAIModel: "gpt-4-32k" },
|
||||
"gpt-4-turbo": {
|
||||
openAIModel: "gpt-4-turbo",
|
||||
},
|
||||
"gpt-4-turbo-2024-04-09": {
|
||||
openAIModel: "gpt-4-turbo",
|
||||
},
|
||||
"gpt-4-vision-preview": {
|
||||
openAIModel: "gpt-4-vision-preview",
|
||||
},
|
||||
"gpt-4-1106-preview": {
|
||||
openAIModel: "gpt-4-1106-preview",
|
||||
},
|
||||
"gpt-4o-2024-05-13": {
|
||||
openAIModel: "gpt-4o-2024-05-13",
|
||||
},
|
||||
"gpt-4o-mini-2024-07-18": {
|
||||
openAIModel: "gpt-4o-mini-2024-07-18",
|
||||
},
|
||||
};
|
||||
|
||||
const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
|
||||
string,
|
||||
{
|
||||
dimensions: number;
|
||||
openAIModel: string;
|
||||
}
|
||||
> = {
|
||||
"text-embedding-3-small": {
|
||||
dimensions: 1536,
|
||||
openAIModel: "text-embedding-3-small",
|
||||
},
|
||||
"text-embedding-3-large": {
|
||||
dimensions: 3072,
|
||||
openAIModel: "text-embedding-3-large",
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL = "gpt-4o";
|
||||
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
|
||||
|
||||
export async function askAzureQuestions({
|
||||
openAiKey,
|
||||
askModels,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: openAiKey || process.env.AZURE_OPENAI_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
isConfigured(): boolean {
|
||||
// the Azure model provider can't be fully configured as endpoint and deployment names have to be configured with env variables
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: getAvailableModelChoices(),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: getAvailableEmbeddingModelChoices(),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = getDimensions(embeddingModel);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function getAvailableModelChoices() {
|
||||
return Object.keys(ALL_AZURE_OPENAI_CHAT_MODELS).map((key) => ({
|
||||
title: key,
|
||||
value: key,
|
||||
}));
|
||||
}
|
||||
|
||||
function getAvailableEmbeddingModelChoices() {
|
||||
return Object.keys(ALL_AZURE_OPENAI_EMBEDDING_MODELS).map((key) => ({
|
||||
title: key,
|
||||
value: key,
|
||||
}));
|
||||
}
|
||||
|
||||
function getDimensions(modelName: string) {
|
||||
return ALL_AZURE_OPENAI_EMBEDDING_MODELS[modelName].dimensions;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
|
||||
type ModelData = {
|
||||
@@ -54,9 +53,7 @@ export async function askGeminiQuestions({
|
||||
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) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
|
||||
const GROQ_API_URL = "https://api.groq.com/openai/v1";
|
||||
|
||||
async function getAvailableModelChoicesGroq(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error("Need Groq API key to retrieve model choices");
|
||||
}
|
||||
|
||||
const spinner = ora("Fetching available models from Groq").start();
|
||||
try {
|
||||
const response = await got(`${GROQ_API_URL}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
timeout: 5000,
|
||||
responseType: "json",
|
||||
});
|
||||
const data: any = await response.body;
|
||||
spinner.stop();
|
||||
|
||||
// Filter out the Whisper models
|
||||
return data.data
|
||||
.filter((model: any) => !model.id.toLowerCase().includes("whisper"))
|
||||
.map((el: any) => {
|
||||
return {
|
||||
title: el.id,
|
||||
value: el.id,
|
||||
};
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
spinner.stop();
|
||||
console.log(error);
|
||||
if ((error as any).response?.statusCode === 401) {
|
||||
console.log(
|
||||
red(
|
||||
"Invalid Groq API key provided! Please provide a valid key and try again!",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(red("Request failed: " + error));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL = "llama3-70b-8192";
|
||||
|
||||
// Use huggingface embedding models for now as Groq doesn't support embedding models
|
||||
enum HuggingFaceEmbeddingModelType {
|
||||
XENOVA_ALL_MINILM_L6_V2 = "all-MiniLM-L6-v2",
|
||||
XENOVA_ALL_MPNET_BASE_V2 = "all-mpnet-base-v2",
|
||||
}
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
|
||||
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MINILM_L6_V2]: {
|
||||
dimensions: 384,
|
||||
},
|
||||
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MPNET_BASE_V2]: {
|
||||
dimensions: 768,
|
||||
},
|
||||
};
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type GroqQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askGroqQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: GroqQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["GROQ_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
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;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
|
||||
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: modelChoices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions =
|
||||
EMBEDDING_MODELS[
|
||||
embeddingModel as HuggingFaceEmbeddingModelType
|
||||
].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, 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 prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which Hugging Face model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
+40
-13
@@ -1,9 +1,13 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { ModelConfig, ModelProvider } from "../types";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
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";
|
||||
import { askOpenAIQuestions } from "./openai";
|
||||
|
||||
@@ -12,6 +16,7 @@ const DEFAULT_MODEL_PROVIDER = "openai";
|
||||
export type ModelConfigQuestionsParams = {
|
||||
openAiKey?: string;
|
||||
askModels: boolean;
|
||||
framework?: TemplateFramework;
|
||||
};
|
||||
|
||||
export type ModelConfigParams = Omit<ModelConfig, "provider">;
|
||||
@@ -19,23 +24,30 @@ export type ModelConfigParams = Omit<ModelConfig, "provider">;
|
||||
export async function askModelConfig({
|
||||
askModels,
|
||||
openAiKey,
|
||||
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" },
|
||||
];
|
||||
|
||||
if (framework === "fastapi") {
|
||||
choices.push({ title: "T-Systems", value: "t-systems" });
|
||||
choices.push({ title: "Huggingface", value: "huggingface" });
|
||||
}
|
||||
const { provider } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "provider",
|
||||
message: "Which model provider would you like to use",
|
||||
choices: [
|
||||
{
|
||||
title: "OpenAI",
|
||||
value: "openai",
|
||||
},
|
||||
{ title: "Ollama", value: "ollama" },
|
||||
{ title: "Anthropic", value: "anthropic" },
|
||||
{ title: "Gemini", value: "gemini" },
|
||||
],
|
||||
choices: choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
@@ -48,12 +60,27 @@ export async function askModelConfig({
|
||||
case "ollama":
|
||||
modelConfig = await askOllamaQuestions({ askModels });
|
||||
break;
|
||||
case "groq":
|
||||
modelConfig = await askGroqQuestions({ askModels });
|
||||
break;
|
||||
case "anthropic":
|
||||
modelConfig = await askAnthropicQuestions({ askModels });
|
||||
break;
|
||||
case "gemini":
|
||||
modelConfig = await askGeminiQuestions({ askModels });
|
||||
break;
|
||||
case "mistral":
|
||||
modelConfig = await askMistralQuestions({ askModels });
|
||||
break;
|
||||
case "azure-openai":
|
||||
modelConfig = await askAzureQuestions({ askModels });
|
||||
break;
|
||||
case "t-systems":
|
||||
modelConfig = await askLLMHubQuestions({ askModels });
|
||||
break;
|
||||
case "huggingface":
|
||||
modelConfig = await askHuggingfaceQuestions({ askModels });
|
||||
break;
|
||||
default:
|
||||
modelConfig = await askOpenAIQuestions({
|
||||
openAiKey,
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
export const TSYSTEMS_LLMHUB_API_URL =
|
||||
"https://llm-server.llmhub.t-systems.net/v2";
|
||||
|
||||
const DEFAULT_MODEL = "gpt-3.5-turbo";
|
||||
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
|
||||
|
||||
const LLMHUB_MODELS = [
|
||||
"gpt-35-turbo",
|
||||
"gpt-4-32k-1",
|
||||
"gpt-4-32k-canada",
|
||||
"gpt-4-32k-france",
|
||||
"gpt-4-turbo-128k-france",
|
||||
"Llama2-70b-Instruct",
|
||||
"Llama-3-70B-Instruct",
|
||||
"Mixtral-8x7B-Instruct-v0.1",
|
||||
"mistral-large-32k-france",
|
||||
"CodeLlama-2",
|
||||
];
|
||||
const LLMHUB_EMBEDDING_MODELS = [
|
||||
"text-embedding-ada-002",
|
||||
"text-embedding-ada-002-france",
|
||||
"jina-embeddings-v2-base-de",
|
||||
"jina-embeddings-v2-base-code",
|
||||
"text-embedding-bge-m3",
|
||||
];
|
||||
|
||||
type LLMHubQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askLLMHubQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: LLMHubQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["T_SYSTEMS_LLMHUB_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message: askModels
|
||||
? "Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):"
|
||||
: "Please provide your LLMHub API key (leave blank to skip):",
|
||||
validate: (value: string) => {
|
||||
if (askModels && !value) {
|
||||
if (process.env.T_SYSTEMS_LLMHUB_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
return "T_SYSTEMS_LLMHUB_API_KEY env variable is not set - key is required";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: await getAvailableModelChoices(false, config.apiKey),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: await getAvailableModelChoices(true, config.apiKey),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = getDimensions(embeddingModel);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function getAvailableModelChoices(
|
||||
selectEmbedding: boolean,
|
||||
apiKey?: string,
|
||||
) {
|
||||
if (!apiKey) {
|
||||
throw new Error("Need LLMHub key to retrieve model choices");
|
||||
}
|
||||
const isLLMModel = (modelId: string) => {
|
||||
return LLMHUB_MODELS.includes(modelId);
|
||||
};
|
||||
|
||||
const isEmbeddingModel = (modelId: string) => {
|
||||
return LLMHUB_EMBEDDING_MODELS.includes(modelId);
|
||||
};
|
||||
|
||||
const spinner = ora("Fetching available models").start();
|
||||
try {
|
||||
const response = await got(`${TSYSTEMS_LLMHUB_API_URL}/models`, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + apiKey,
|
||||
},
|
||||
timeout: 5000,
|
||||
responseType: "json",
|
||||
});
|
||||
const data: any = await response.body;
|
||||
spinner.stop();
|
||||
return data.data
|
||||
.filter((model: any) =>
|
||||
selectEmbedding ? isEmbeddingModel(model.id) : isLLMModel(model.id),
|
||||
)
|
||||
.map((el: any) => {
|
||||
return {
|
||||
title: el.id,
|
||||
value: el.id,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
if ((error as any).response?.statusCode === 401) {
|
||||
console.log(
|
||||
red(
|
||||
"Invalid LLMHub API key provided! Please provide a valid key and try again!",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(red("Request failed: " + error));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getDimensions(modelName: string) {
|
||||
// Assuming dimensions similar to OpenAI for simplicity. Update if different.
|
||||
return modelName === "text-embedding-004" ? 768 : 1536;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = ["mistral-tiny", "mistral-small", "mistral-medium"];
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
"mistral-embed": { dimensions: 1024 },
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type MistralQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askMistralQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: MistralQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["MISTRAL_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
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;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import ciInfo from "ci-info";
|
||||
import ollama, { type ModelResponse } from "ollama";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
@@ -34,9 +33,7 @@ export async function askOllamaQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import ciInfo from "ci-info";
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { isCI } from "../../questions";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
|
||||
const DEFAULT_MODEL = "gpt-3.5-turbo";
|
||||
const DEFAULT_MODEL = "gpt-4o-mini";
|
||||
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
|
||||
|
||||
export async function askOpenAIQuestions({
|
||||
@@ -31,7 +31,7 @@ export async function askOpenAIQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
if (!config.apiKey && !isCI) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
@@ -54,9 +54,7 @@ export async function askOpenAIQuestions({
|
||||
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) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
+256
-57
@@ -12,6 +12,7 @@ import {
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateType,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
|
||||
@@ -19,6 +20,7 @@ interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
constraints?: Record<string, string>;
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
@@ -26,6 +28,7 @@ const getAdditionalDependencies = (
|
||||
vectorDb?: TemplateVectorDB,
|
||||
dataSources?: TemplateDataSource[],
|
||||
tools?: Tool[],
|
||||
templateType?: TemplateType,
|
||||
) => {
|
||||
const dependencies: Dependency[] = [];
|
||||
|
||||
@@ -34,56 +37,75 @@ const getAdditionalDependencies = (
|
||||
case "mongo": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-mongodb",
|
||||
version: "^0.1.3",
|
||||
version: "^0.6.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pg": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-postgres",
|
||||
version: "^0.1.1",
|
||||
version: "^0.3.2",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pinecone": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: "^0.1.3",
|
||||
version: "^0.4.1",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "milvus": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-milvus",
|
||||
version: "^0.1.6",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymilvus",
|
||||
version: "2.3.7",
|
||||
version: "2.4.4",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "astra": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-astra-db",
|
||||
version: "^0.1.5",
|
||||
version: "^0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "qdrant": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-qdrant",
|
||||
version: "^0.2.8",
|
||||
version: "^0.4.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "chroma": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-chroma",
|
||||
version: "^0.1.8",
|
||||
version: "^0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-weaviate",
|
||||
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
|
||||
@@ -100,13 +122,13 @@ const getAdditionalDependencies = (
|
||||
case "web":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-web",
|
||||
version: "^0.1.6",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
break;
|
||||
case "db":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-database",
|
||||
version: "^0.1.3",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymysql",
|
||||
@@ -114,7 +136,7 @@ const getAdditionalDependencies = (
|
||||
extras: ["rsa"],
|
||||
});
|
||||
dependencies.push({
|
||||
name: "psycopg2",
|
||||
name: "psycopg2-binary",
|
||||
version: "^2.9.9",
|
||||
});
|
||||
break;
|
||||
@@ -134,37 +156,114 @@ const getAdditionalDependencies = (
|
||||
case "ollama":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-ollama",
|
||||
version: "0.1.2",
|
||||
version: "0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-ollama",
|
||||
version: "0.1.2",
|
||||
version: "0.3.0",
|
||||
});
|
||||
break;
|
||||
case "openai":
|
||||
if (templateType !== "multiagent") {
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-openai",
|
||||
version: "^0.3.2",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-openai",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: "^0.4.0",
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "groq":
|
||||
// Fastembed==0.2.0 does not support python3.13 at the moment
|
||||
// Fixed the python version less than 3.13
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: "0.2.2",
|
||||
name: "python",
|
||||
version: "^3.11,<3.13",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-groq",
|
||||
version: "0.2.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-fastembed",
|
||||
version: "^0.2.0",
|
||||
});
|
||||
break;
|
||||
case "anthropic":
|
||||
// Fastembed==0.2.0 does not support python3.13 at the moment
|
||||
// Fixed the python version less than 3.13
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-anthropic",
|
||||
version: "0.1.10",
|
||||
name: "python",
|
||||
version: "^3.11,<3.13",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-huggingface",
|
||||
version: "0.2.0",
|
||||
name: "llama-index-llms-anthropic",
|
||||
version: "0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-fastembed",
|
||||
version: "^0.2.0",
|
||||
});
|
||||
break;
|
||||
case "gemini":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-gemini",
|
||||
version: "0.1.7",
|
||||
version: "0.3.4",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-gemini",
|
||||
version: "0.1.6",
|
||||
version: "^0.2.0",
|
||||
});
|
||||
break;
|
||||
case "mistral":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-mistralai",
|
||||
version: "0.2.1",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-mistralai",
|
||||
version: "0.2.0",
|
||||
});
|
||||
break;
|
||||
case "azure-openai":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-azure-openai",
|
||||
version: "0.2.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-azure-openai",
|
||||
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",
|
||||
version: "0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-openai-like",
|
||||
version: "0.2.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -174,7 +273,7 @@ const getAdditionalDependencies = (
|
||||
|
||||
const mergePoetryDependencies = (
|
||||
dependencies: Dependency[],
|
||||
existingDependencies: Record<string, Omit<Dependency, "name">>,
|
||||
existingDependencies: Record<string, Omit<Dependency, "name"> | string>,
|
||||
) => {
|
||||
for (const dependency of dependencies) {
|
||||
let value = existingDependencies[dependency.name] ?? {};
|
||||
@@ -187,13 +286,35 @@ 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"!`,
|
||||
);
|
||||
}
|
||||
|
||||
existingDependencies[dependency.name] = value;
|
||||
// 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
|
||||
existingDependencies[dependency.name] = value.version;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyRouterCode = async (root: string, tools: Tool[]) => {
|
||||
// Copy sandbox router if the artifact tool is selected
|
||||
if (tools?.some((t) => t.name === "artifact")) {
|
||||
await copy("sandbox.py", path.join(root, "app", "api", "routers"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "routers", "python"),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,29 +380,40 @@ 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");
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
let templatePath;
|
||||
if (template === "reflex") {
|
||||
templatePath = path.join(templatesDir, "types", "reflex");
|
||||
} else {
|
||||
templatePath = path.join(templatesDir, "types", "streaming", framework);
|
||||
}
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
@@ -297,26 +429,85 @@ export const installPythonTemplate = async ({
|
||||
cwd: path.join(compPath, "vectordbs", "python", vectorDb ?? "none"),
|
||||
});
|
||||
|
||||
// Copy all loaders to enginePath
|
||||
const loaderPath = path.join(enginePath, "loaders");
|
||||
await copy("**", loaderPath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "loaders", "python"),
|
||||
if (vectorDb !== "llamacloud") {
|
||||
// Copy all loaders to enginePath
|
||||
// Not needed for LlamaCloud as it has its own loaders
|
||||
const loaderPath = path.join(enginePath, "loaders");
|
||||
await copy("**", loaderPath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "loaders", "python"),
|
||||
});
|
||||
}
|
||||
|
||||
// Copy settings.py to app
|
||||
await copy("**", path.join(root, "app"), {
|
||||
cwd: path.join(compPath, "settings", "python"),
|
||||
});
|
||||
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
tools = tools ?? [];
|
||||
if (dataSources.length > 0 && tools.length === 0) {
|
||||
console.log("\nNo tools selected - use optimized context chat engine\n");
|
||||
engine = "chat";
|
||||
} else {
|
||||
engine = "agent";
|
||||
// Copy services
|
||||
if (template == "streaming" || template == "multiagent") {
|
||||
await copy("**", path.join(root, "app", "api", "services"), {
|
||||
cwd: path.join(compPath, "services", "python"),
|
||||
});
|
||||
}
|
||||
// Copy engine code
|
||||
if (template === "streaming" || template === "multiagent") {
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
// Multiagent always uses agent engine
|
||||
if (template === "multiagent") {
|
||||
engine = "agent";
|
||||
} else {
|
||||
// For streaming, use chat engine by default
|
||||
// Unless tools are selected, in which case use agent engine
|
||||
if (dataSources.length > 0 && (!tools || tools.length === 0)) {
|
||||
console.log(
|
||||
"\nNo tools selected - use optimized context chat engine\n",
|
||||
);
|
||||
engine = "chat";
|
||||
} else {
|
||||
engine = "agent";
|
||||
}
|
||||
}
|
||||
|
||||
// Copy engine code
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "engines", "python", engine),
|
||||
});
|
||||
|
||||
// Copy router code
|
||||
await copyRouterCode(root, tools ?? []);
|
||||
}
|
||||
|
||||
// Copy multiagents overrides
|
||||
if (template === "multiagent") {
|
||||
await copy("**", path.join(root), {
|
||||
cwd: path.join(compPath, "multiagent", "python"),
|
||||
});
|
||||
}
|
||||
|
||||
if (template === "multiagent" || template === "reflex") {
|
||||
if (useCase) {
|
||||
const sourcePath =
|
||||
template === "multiagent"
|
||||
? path.join(compPath, "agents", "python", useCase)
|
||||
: path.join(compPath, "reflex", useCase);
|
||||
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: sourcePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "engines", "python", engine),
|
||||
});
|
||||
|
||||
console.log("Adding additional dependencies");
|
||||
|
||||
@@ -325,20 +516,33 @@ export const installPythonTemplate = async ({
|
||||
vectorDb,
|
||||
dataSources,
|
||||
tools,
|
||||
template,
|
||||
);
|
||||
|
||||
if (observability === "opentelemetry") {
|
||||
addOnDependencies.push({
|
||||
name: "traceloop-sdk",
|
||||
version: "^0.15.11",
|
||||
});
|
||||
if (observability && observability !== "none") {
|
||||
if (observability === "traceloop") {
|
||||
addOnDependencies.push({
|
||||
name: "traceloop-sdk",
|
||||
version: "^0.15.11",
|
||||
});
|
||||
}
|
||||
|
||||
if (observability === "llamatrace") {
|
||||
addOnDependencies.push({
|
||||
name: "llama-index-callbacks-arize-phoenix",
|
||||
version: "^0.3.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const templateObservabilityPath = path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"observability",
|
||||
"python",
|
||||
"opentelemetry",
|
||||
observability,
|
||||
);
|
||||
await copy("**", path.join(root, "app"), {
|
||||
cwd: templateObservabilityPath,
|
||||
@@ -350,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"),
|
||||
});
|
||||
};
|
||||
|
||||
+66
-73
@@ -1,88 +1,81 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export function runReflexApp(appPath: string, port: number) {
|
||||
const commandArgs = [
|
||||
"run",
|
||||
"reflex",
|
||||
"run",
|
||||
"--frontend-port",
|
||||
port.toString(),
|
||||
];
|
||||
return createProcess("poetry", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function runFastAPIApp(appPath: string, port: number) {
|
||||
return createProcess("poetry", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
env: { ...process.env, APP_PORT: `${port}` },
|
||||
});
|
||||
}
|
||||
|
||||
export function runTSApp(appPath: string, port: number) {
|
||||
return createProcess("npm", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
env: { ...process.env, PORT: `${port}` },
|
||||
});
|
||||
}
|
||||
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
frontend: boolean,
|
||||
template: TemplateType,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
externalPort?: number,
|
||||
): Promise<any> {
|
||||
let backendAppProcess: ChildProcess;
|
||||
let frontendAppProcess: ChildProcess | undefined;
|
||||
const frontendPort = port || 3000;
|
||||
let backendPort = externalPort || 8000;
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort =
|
||||
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
|
||||
|
||||
// Callback to kill app processes
|
||||
process.on("exit", () => {
|
||||
console.log("Killing app processes...");
|
||||
backendAppProcess.kill();
|
||||
frontendAppProcess?.kill();
|
||||
});
|
||||
|
||||
let backendCommand = "";
|
||||
let backendArgs: string[];
|
||||
if (framework === "fastapi") {
|
||||
backendCommand = "poetry";
|
||||
backendArgs = [
|
||||
"run",
|
||||
"uvicorn",
|
||||
"main:app",
|
||||
"--host=0.0.0.0",
|
||||
"--port=" + backendPort,
|
||||
];
|
||||
} else if (framework === "nextjs") {
|
||||
backendCommand = "npm";
|
||||
backendArgs = ["run", "dev"];
|
||||
backendPort = frontendPort;
|
||||
} else {
|
||||
backendCommand = "npm";
|
||||
backendArgs = ["run", "dev"];
|
||||
}
|
||||
|
||||
if (frontend) {
|
||||
return new Promise((resolve, reject) => {
|
||||
backendAppProcess = createProcess(backendCommand, backendArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: path.join(appPath, "backend"),
|
||||
env: { ...process.env, PORT: `${backendPort}` },
|
||||
});
|
||||
frontendAppProcess = createProcess("npm", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: path.join(appPath, "frontend"),
|
||||
env: { ...process.env, PORT: `${frontendPort}` },
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
backendAppProcess = createProcess(backendCommand, backendArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: path.join(appPath),
|
||||
env: { ...process.env, PORT: `${backendPort}` },
|
||||
});
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+165
-22
@@ -30,7 +30,7 @@ export type ToolDependencies = {
|
||||
|
||||
export const supportedTools: Tool[] = [
|
||||
{
|
||||
display: "Google Search (configuration required after installation)",
|
||||
display: "Google Search",
|
||||
name: "google.GoogleSearchToolSpec",
|
||||
config: {
|
||||
engine:
|
||||
@@ -41,7 +41,7 @@ export const supportedTools: Tool[] = [
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-google",
|
||||
version: "0.1.2",
|
||||
version: "^0.3.0",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi"],
|
||||
@@ -54,24 +54,39 @@ export const supportedTools: Tool[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// For python app, we will use a local DuckDuckGo search tool (instead of DuckDuckGo search tool in LlamaHub)
|
||||
// to get the same results as the TS app.
|
||||
display: "DuckDuckGo Search",
|
||||
name: "duckduckgo",
|
||||
dependencies: [
|
||||
{
|
||||
name: "duckduckgo-search",
|
||||
version: "^6.3.5",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for DuckDuckGo search tool.",
|
||||
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
|
||||
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Wikipedia",
|
||||
name: "wikipedia.WikipediaToolSpec",
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-wikipedia",
|
||||
version: "0.1.2",
|
||||
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",
|
||||
@@ -79,11 +94,27 @@ export const supportedTools: Tool[] = [
|
||||
dependencies: [],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
},
|
||||
{
|
||||
display: "Document generator",
|
||||
name: "document_generator",
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
dependencies: [
|
||||
{
|
||||
name: "xhtml2pdf",
|
||||
version: "^0.2.14",
|
||||
},
|
||||
{
|
||||
name: "markdown",
|
||||
version: "^3.7",
|
||||
},
|
||||
],
|
||||
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.`,
|
||||
description: "System prompt for document generator tool.",
|
||||
value: `If user request for a report or a post, use document generator tool to create a file and reply with the link to the file.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -93,7 +124,7 @@ export const supportedTools: Tool[] = [
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.7",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -107,13 +138,120 @@ export const supportedTools: Tool[] = [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for code interpreter tool.",
|
||||
value: `You are a Python interpreter.
|
||||
- You are given tasks to complete and you run python code to solve them.
|
||||
- The python code runs in a Jupyter notebook. Every time you call \`interpreter\` tool, the python code is executed in a separate cell. It's okay to make multiple calls to \`interpreter\`.
|
||||
- Display visualizations using matplotlib or any other visualization library directly in the notebook. Shouldn't save the visualizations to a file, just return the base64 encoded data.
|
||||
- You can install any pip package (if it exists) if you need to but the usual packages for data analysis are already preinstalled.
|
||||
- You can run any python code you want in a secure environment.
|
||||
- Use absolute url from result to display images or any other media.`,
|
||||
value: `-You are a Python interpreter that can run any python code in a secure environment.
|
||||
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
|
||||
- You are given tasks to complete and you run python code to solve them.
|
||||
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
|
||||
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
|
||||
- You can install any pip package (if it exists) by running a cell with pip install.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Artifact Code Generator",
|
||||
name: "artifact",
|
||||
// Using pre-release version of e2b_code_interpreter
|
||||
// TODO: Update to stable version when 0.0.11 is released
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "1.0.3",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: "E2B_API_KEY",
|
||||
description:
|
||||
"E2B_API_KEY key is required to run artifact code generator tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
|
||||
},
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for artifact code generator tool.",
|
||||
value:
|
||||
"You are a code assistant that can generate and execute code using its tools. Don't generate code yourself, use the provided tools instead. Do not show the code or sandbox url in chat, just describe the steps to build the application based on the code that is generated by your tools. Do not describe how to run the code, just the steps to build the application.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "OpenAPI action",
|
||||
name: "openapi_action.OpenAPIActionToolSpec",
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-openapi",
|
||||
version: "0.2.0",
|
||||
},
|
||||
{
|
||||
name: "jsonschema",
|
||||
version: "^4.22.0",
|
||||
},
|
||||
{
|
||||
name: "llama-index-tools-requests",
|
||||
version: "0.2.0",
|
||||
},
|
||||
],
|
||||
config: {
|
||||
openapi_uri: "The URL or file path of the OpenAPI schema",
|
||||
},
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
},
|
||||
{
|
||||
display: "Image Generator",
|
||||
name: "img_gen",
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: "STABILITY_API_KEY",
|
||||
description:
|
||||
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Azure Code Interpreter",
|
||||
name: "azure_code_interpreter.AzureCodeInterpreterToolSpec",
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
type: ToolType.LLAMAHUB,
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-azure-code-interpreter",
|
||||
version: "0.2.0",
|
||||
},
|
||||
],
|
||||
envVars: [
|
||||
{
|
||||
name: "AZURE_POOL_MANAGEMENT_ENDPOINT",
|
||||
description:
|
||||
"Please follow this guideline to create and get the pool management endpoint: https://learn.microsoft.com/azure/container-apps/sessions?tabs=azure-cli",
|
||||
},
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for Azure code interpreter tool.",
|
||||
value: `-You are a Python interpreter that can run any python code in a secure environment.
|
||||
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
|
||||
- You are given tasks to complete and you run python code to solve them.
|
||||
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
|
||||
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
|
||||
- You can install any pip package (if it exists) by running a cell with pip install.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Form Filling",
|
||||
name: "form_filling",
|
||||
supportedFrameworks: ["fastapi"],
|
||||
type: ToolType.LOCAL,
|
||||
dependencies: [
|
||||
{
|
||||
name: "pandas",
|
||||
version: "^2.2.3",
|
||||
},
|
||||
{
|
||||
name: "tabulate",
|
||||
version: "^0.9.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -142,9 +280,15 @@ export const getTools = (toolsName: string[]): Tool[] => {
|
||||
return tools;
|
||||
};
|
||||
|
||||
export const toolRequiresConfig = (tool: Tool): boolean => {
|
||||
const hasConfig = Object.keys(tool.config || {}).length > 0;
|
||||
const hasEmptyEnvVar = tool.envVars?.some((envVar) => !envVar.value) ?? false;
|
||||
return hasConfig || hasEmptyEnvVar;
|
||||
};
|
||||
|
||||
export const toolsRequireConfig = (tools?: Tool[]): boolean => {
|
||||
if (tools) {
|
||||
return tools?.some((tool) => Object.keys(tool.config || {}).length > 0);
|
||||
return tools?.some(toolRequiresConfig);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -159,7 +303,6 @@ export const writeToolsConfig = async (
|
||||
tools: Tool[] = [],
|
||||
type: ConfigFileType = ConfigFileType.YAML,
|
||||
) => {
|
||||
if (tools.length === 0) return; // no tools selected, no config need
|
||||
const configContent: {
|
||||
[key in ToolType]: Record<string, any>;
|
||||
} = {
|
||||
|
||||
+38
-9
@@ -1,7 +1,16 @@
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
import { Tool } from "./tools";
|
||||
|
||||
export type ModelProvider = "openai" | "ollama" | "anthropic" | "gemini";
|
||||
export type ModelProvider =
|
||||
| "openai"
|
||||
| "groq"
|
||||
| "ollama"
|
||||
| "anthropic"
|
||||
| "gemini"
|
||||
| "mistral"
|
||||
| "azure-openai"
|
||||
| "huggingface"
|
||||
| "t-systems";
|
||||
export type ModelConfig = {
|
||||
provider: ModelProvider;
|
||||
apiKey?: string;
|
||||
@@ -10,7 +19,12 @@ export type ModelConfig = {
|
||||
dimensions: number;
|
||||
isConfigured(): boolean;
|
||||
};
|
||||
export type TemplateType = "streaming" | "community" | "llamapack";
|
||||
export type TemplateType =
|
||||
| "streaming"
|
||||
| "community"
|
||||
| "llamapack"
|
||||
| "multiagent"
|
||||
| "reflex";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateUI = "html" | "shadcn";
|
||||
export type TemplateVectorDB =
|
||||
@@ -21,7 +35,9 @@ export type TemplateVectorDB =
|
||||
| "milvus"
|
||||
| "astra"
|
||||
| "qdrant"
|
||||
| "chroma";
|
||||
| "chroma"
|
||||
| "llamacloud"
|
||||
| "weaviate";
|
||||
export type TemplatePostInstallAction =
|
||||
| "none"
|
||||
| "VSCode"
|
||||
@@ -32,11 +48,24 @@ export type TemplateDataSource = {
|
||||
config: TemplateDataSourceConfig;
|
||||
};
|
||||
export type TemplateDataSourceType = "file" | "web" | "db";
|
||||
export type TemplateObservability = "none" | "opentelemetry";
|
||||
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;
|
||||
@@ -68,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;
|
||||
}
|
||||
|
||||
+124
-24
@@ -1,7 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bold, cyan } 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}.`));
|
||||
|
||||
@@ -33,7 +34,7 @@ export const installTSTemplate = async ({
|
||||
* Copy the template files to the target directory.
|
||||
*/
|
||||
console.log("\nInitializing project with template:", template, "\n");
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
const templatePath = path.join(templatesDir, "types", "streaming", framework);
|
||||
const copySource = ["**"];
|
||||
|
||||
await copy(copySource, root, {
|
||||
@@ -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(
|
||||
@@ -70,7 +69,7 @@ export const installTSTemplate = async ({
|
||||
);
|
||||
|
||||
const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
|
||||
if (observability === "opentelemetry") {
|
||||
if (observability === "traceloop") {
|
||||
const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
|
||||
await fs.rm(webpackConfigDefaultFile);
|
||||
await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
|
||||
@@ -104,13 +103,77 @@ export const installTSTemplate = async ({
|
||||
: path.join("src", "controllers");
|
||||
const enginePath = path.join(root, relativeEngineDestPath, "engine");
|
||||
|
||||
// copy llamaindex code for TS templates
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "llamaindex"), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "llamaindex", "typescript"),
|
||||
});
|
||||
|
||||
// copy vector db component
|
||||
console.log("\nUsing vector DB:", vectorDb ?? "none", "\n");
|
||||
if (vectorDb === "llamacloud") {
|
||||
console.log(
|
||||
`\nUsing managed index from LlamaCloud. Ensure the ${yellow("LLAMA_CLOUD_* environment variables are set correctly.")}`,
|
||||
);
|
||||
} else {
|
||||
console.log("\nUsing vector DB:", vectorDb ?? "none");
|
||||
}
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
|
||||
});
|
||||
|
||||
if (template === "multiagent") {
|
||||
const multiagentPath = path.join(compPath, "multiagent", "typescript");
|
||||
|
||||
// copy workflow code for multiagent template
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "workflow"),
|
||||
});
|
||||
|
||||
// Copy use case code for multiagent template
|
||||
if (useCase) {
|
||||
console.log("\nCopying use case:", useCase, "\n");
|
||||
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
|
||||
const useCaseCodePath = path.join(useCasePath, "workflow");
|
||||
|
||||
// Copy use case codes
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: useCaseCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// Copy use case files to project root
|
||||
await copy("*.*", path.join(root), {
|
||||
parents: true,
|
||||
cwd: useCasePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (framework === "nextjs") {
|
||||
// patch route.ts file
|
||||
await copy("**", path.join(root, relativeEngineDestPath), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "nextjs"),
|
||||
});
|
||||
} else if (framework === "express") {
|
||||
// patch chat.controller.ts file
|
||||
await copy("**", path.join(root, relativeEngineDestPath), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "express"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// copy loader component (TS only supports llama_parse and file for now)
|
||||
const loaderFolder = useLlamaParse ? "llama_parse" : "file";
|
||||
await copy("**", enginePath, {
|
||||
@@ -121,7 +184,10 @@ export const installTSTemplate = async ({
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
tools = tools ?? [];
|
||||
if (dataSources.length > 0 && tools.length === 0) {
|
||||
// multiagent template always uses agent engine
|
||||
if (template === "multiagent") {
|
||||
engine = "agent";
|
||||
} else if (dataSources.length > 0 && tools.length === 0) {
|
||||
console.log("\nNo tools selected - use optimized context chat engine\n");
|
||||
engine = "chat";
|
||||
} else {
|
||||
@@ -132,6 +198,11 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(compPath, "engines", "typescript", engine),
|
||||
});
|
||||
|
||||
// copy settings to engine folder
|
||||
await copy("**", enginePath, {
|
||||
cwd: path.join(compPath, "settings", "typescript"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy the selected UI files to the target directory and reference it.
|
||||
*/
|
||||
@@ -167,16 +238,15 @@ export const installTSTemplate = async ({
|
||||
framework,
|
||||
ui,
|
||||
observability,
|
||||
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({
|
||||
@@ -187,9 +257,16 @@ async function updatePackageJson({
|
||||
framework,
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "appName" | "dataSources" | "framework" | "ui" | "observability"
|
||||
| "root"
|
||||
| "appName"
|
||||
| "dataSources"
|
||||
| "framework"
|
||||
| "ui"
|
||||
| "observability"
|
||||
| "vectorDb"
|
||||
> & {
|
||||
relativeEngineDestPath: string;
|
||||
}): Promise<any> {
|
||||
@@ -227,16 +304,39 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
if (observability === "opentelemetry") {
|
||||
if (vectorDb === "pg") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
pg: "^8.12.0",
|
||||
pgvector: "^0.2.0",
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "qdrant") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@qdrant/js-client-rest": "^1.11.0",
|
||||
};
|
||||
}
|
||||
if (vectorDb === "mongo") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
mongodb: "^6.7.0",
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "milvus") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.6",
|
||||
};
|
||||
}
|
||||
|
||||
if (observability === "traceloop") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@traceloop/node-server-sdk": "^0.5.19",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
/* 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 path from "path";
|
||||
import { bold, cyan, green, red, yellow } from "picocolors";
|
||||
@@ -9,7 +8,7 @@ import prompts from "prompts";
|
||||
import terminalLink from "terminal-link";
|
||||
import checkForUpdate from "update-check";
|
||||
import { createApp } from "./create-app";
|
||||
import { getDataSources } from "./helpers/datasources";
|
||||
import { EXAMPLE_FILE, getDataSources } from "./helpers/datasources";
|
||||
import { getPkgManager } from "./helpers/get-pkg-manager";
|
||||
import { isFolderEmpty } from "./helpers/is-folder-empty";
|
||||
import { initializeGlobalAgent } from "./helpers/proxy";
|
||||
@@ -17,8 +16,9 @@ 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";
|
||||
import { onPromptState } from "./questions/utils";
|
||||
// Run the initialization function
|
||||
initializeGlobalAgent();
|
||||
|
||||
@@ -29,12 +29,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 +57,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(
|
||||
@@ -90,6 +85,20 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select to use an example PDF as data source.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--web-source <url>",
|
||||
`
|
||||
|
||||
Specify a website URL to use as a data source.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--db-source <connection-string>",
|
||||
`
|
||||
|
||||
Specify a database connection string to use as a data source.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -110,7 +119,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(
|
||||
@@ -118,13 +134,6 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select UI port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--external-port <external>",
|
||||
`
|
||||
|
||||
Select external port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -147,6 +156,13 @@ 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 === "none") {
|
||||
return [];
|
||||
} else {
|
||||
return getTools(tools.split(","));
|
||||
}
|
||||
},
|
||||
)
|
||||
.option(
|
||||
"--use-llama-parse",
|
||||
@@ -173,48 +189,75 @@ const program = new Commander.Command(packageJson.name)
|
||||
"--ask-models",
|
||||
`
|
||||
|
||||
Select LLM and embedding models.
|
||||
Allow interactive selection of LLM and embedding models of different model providers.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--pro",
|
||||
`
|
||||
|
||||
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;
|
||||
|
||||
const options = program.opts();
|
||||
|
||||
if (
|
||||
process.argv.includes("--no-llama-parse") ||
|
||||
options.template === "reflex"
|
||||
) {
|
||||
options.useLlamaParse = false;
|
||||
}
|
||||
if (process.argv.includes("--tools")) {
|
||||
if (program.tools === "none") {
|
||||
program.tools = [];
|
||||
} else {
|
||||
program.tools = getTools(program.tools.split(","));
|
||||
}
|
||||
}
|
||||
if (process.argv.includes("--no-llama-parse")) {
|
||||
program.useLlamaParse = false;
|
||||
}
|
||||
program.askModels = process.argv.includes("--ask-models");
|
||||
if (process.argv.includes("--no-files")) {
|
||||
program.dataSources = [];
|
||||
} else {
|
||||
program.dataSources = getDataSources(program.files, program.exampleFile);
|
||||
options.dataSources = [];
|
||||
} else if (process.argv.includes("--example-file")) {
|
||||
options.dataSources = getDataSources(options.files, options.exampleFile);
|
||||
} else if (process.argv.includes("--llamacloud")) {
|
||||
options.dataSources = [EXAMPLE_FILE];
|
||||
options.vectorDb = "llamacloud";
|
||||
} else if (process.argv.includes("--web-source")) {
|
||||
options.dataSources = [
|
||||
{
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl: options.webSource,
|
||||
prefix: options.webSource,
|
||||
depth: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (process.argv.includes("--db-source")) {
|
||||
options.dataSources = [
|
||||
{
|
||||
type: "db",
|
||||
config: {
|
||||
uri: options.dbSource,
|
||||
queries: options.dbQuery || "SELECT * FROM mytable",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const packageManager = !!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();
|
||||
}
|
||||
@@ -277,35 +320,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`, {
|
||||
@@ -329,15 +352,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.frontend,
|
||||
program.framework,
|
||||
program.port,
|
||||
program.externalPort,
|
||||
);
|
||||
await runApp(root, answers.template, answers.framework, options.port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.1.8",
|
||||
"version": "0.3.28",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS",
|
||||
"url": "https://github.com/run-llama/create-llama",
|
||||
"directory": "packages/create-llama"
|
||||
},
|
||||
"license": "MIT",
|
||||
@@ -25,6 +25,8 @@
|
||||
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
|
||||
"dev": "ncc build ./index.ts -w -o dist/",
|
||||
"e2e": "playwright test",
|
||||
"e2e:python": "playwright test e2e/shared e2e/python",
|
||||
"e2e:typescript": "playwright test e2e/shared e2e/typescript",
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
|
||||
@@ -41,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/prompts": "2.4.2",
|
||||
"@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",
|
||||
@@ -57,7 +58,7 @@
|
||||
"ollama": "^0.5.0",
|
||||
"ora": "^8.0.1",
|
||||
"picocolors": "1.0.0",
|
||||
"prompts": "2.1.0",
|
||||
"prompts": "2.4.2",
|
||||
"smol-toml": "^1.1.4",
|
||||
"tar": "6.1.15",
|
||||
"terminal-link": "^3.0.0",
|
||||
|
||||
Generated
+19
-152
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^20.11.7
|
||||
version: 20.12.10
|
||||
'@types/prompts':
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
'@types/tar':
|
||||
specifier: 6.1.5
|
||||
version: 6.1.5
|
||||
@@ -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
|
||||
@@ -72,8 +69,8 @@ importers:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
prompts:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
smol-toml:
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4
|
||||
@@ -301,8 +298,8 @@ packages:
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
'@types/prompts@2.0.1':
|
||||
resolution: {integrity: sha512-AhtMcmETelF8wFDV1ucbChKhLgsc+ytXZXkNz/nnTAMSDeqsjALknEFxi7ZtLgS/G8bV2rp90LhDW5SGACimIQ==}
|
||||
'@types/prompts@2.4.2':
|
||||
resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==}
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||
@@ -336,20 +333,9 @@ 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'}
|
||||
@@ -410,10 +396,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'}
|
||||
@@ -530,8 +512,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 +523,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 +555,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 +613,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 +635,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 +755,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'}
|
||||
@@ -1057,10 +1020,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'}
|
||||
@@ -1138,12 +1097,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==}
|
||||
|
||||
@@ -1182,10 +1135,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'}
|
||||
@@ -1243,10 +1192,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'}
|
||||
@@ -1375,10 +1320,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 +1348,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 +1386,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,8 +1431,8 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
prompts@2.1.0:
|
||||
resolution: {integrity: sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==}
|
||||
prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pseudomap@1.0.2:
|
||||
@@ -1557,10 +1490,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==}
|
||||
|
||||
@@ -2279,7 +2208,10 @@ snapshots:
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/prompts@2.0.1': {}
|
||||
'@types/prompts@2.4.2':
|
||||
dependencies:
|
||||
'@types/node': 20.12.10
|
||||
kleur: 3.0.3
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
dependencies:
|
||||
@@ -2306,10 +2238,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,13 +2245,6 @@ 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@5.0.0:
|
||||
@@ -2383,8 +2304,6 @@ snapshots:
|
||||
|
||||
async-sema@3.0.1: {}
|
||||
|
||||
atomically@1.7.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
@@ -2506,25 +2425,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 +2474,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 +2523,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 +2542,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 +2737,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
|
||||
@@ -3129,8 +3021,6 @@ snapshots:
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-obj@2.0.0: {}
|
||||
|
||||
is-path-inside@3.0.3: {}
|
||||
|
||||
is-plain-obj@1.1.0: {}
|
||||
@@ -3197,10 +3087,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: {}
|
||||
@@ -3239,11 +3125,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
|
||||
@@ -3301,8 +3182,6 @@ snapshots:
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-fn@3.1.0: {}
|
||||
|
||||
mimic-response@1.0.1: {}
|
||||
|
||||
mimic-response@2.1.0: {}
|
||||
@@ -3425,10 +3304,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 +3331,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 +3356,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,7 +3384,7 @@ snapshots:
|
||||
|
||||
prettier@3.2.5: {}
|
||||
|
||||
prompts@2.1.0:
|
||||
prompts@2.4.2:
|
||||
dependencies:
|
||||
kleur: 3.0.3
|
||||
sisteransi: 1.0.5
|
||||
@@ -3585,8 +3454,6 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
-678
@@ -1,678 +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,
|
||||
} 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, toolsRequireConfig } from "./helpers/tools";
|
||||
|
||||
export type QuestionArgs = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager"
|
||||
> & {
|
||||
askModels?: 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" },
|
||||
];
|
||||
|
||||
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[],
|
||||
) => {
|
||||
const choices = [];
|
||||
if (selectedDataSource.length > 0) {
|
||||
choices.push({
|
||||
title: "No",
|
||||
value: "no",
|
||||
});
|
||||
}
|
||||
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
|
||||
choices.push({
|
||||
title: "No data, just a simple chat or agent",
|
||||
value: "none",
|
||||
});
|
||||
choices.push({
|
||||
title: "Use an example PDF",
|
||||
value: "exampleFile",
|
||||
});
|
||||
}
|
||||
|
||||
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") {
|
||||
choices.push({
|
||||
title: "Use website content (requires Chrome)",
|
||||
value: "web",
|
||||
});
|
||||
choices.push({
|
||||
title: "Use data from a database (Mysql, PostgreSQL)",
|
||||
value: "db",
|
||||
});
|
||||
}
|
||||
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: "Chat", value: "streaming" },
|
||||
{
|
||||
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.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") {
|
||||
// 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) {
|
||||
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" },
|
||||
{ title: "OpenTelemetry", value: "opentelemetry" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.observability = observability;
|
||||
preferences.observability = observability;
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
const modelConfig = await askModelConfig({
|
||||
openAiKey,
|
||||
askModels: program.askModels ?? false,
|
||||
});
|
||||
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 { 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: getDataSourceChoices(
|
||||
program.framework,
|
||||
program.dataSources,
|
||||
),
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Asking for LlamaParse if user selected file or folder data source
|
||||
if (
|
||||
program.dataSources.some((ds) => ds.type === "file") &&
|
||||
program.useLlamaParse === undefined
|
||||
) {
|
||||
if (ciInfo.isCI) {
|
||||
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
|
||||
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
|
||||
} 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;
|
||||
|
||||
// Ask for LlamaCloud API key
|
||||
if (useLlamaParse && program.llamaCloudKey === undefined) {
|
||||
const { llamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaIndex Cloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamaCloudKey = llamaCloudKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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,
|
||||
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 };
|
||||
};
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,459 @@
|
||||
import { blue } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { isCI } from ".";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
|
||||
import { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
|
||||
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getProjectOptions } from "../helpers/repo";
|
||||
import { supportedTools, toolRequiresConfig } from "../helpers/tools";
|
||||
import { getDataSourceChoices } from "./datasources";
|
||||
import { getVectorDbChoices } from "./stores";
|
||||
import { QuestionArgs } from "./types";
|
||||
import {
|
||||
askPostInstallAction,
|
||||
onPromptState,
|
||||
questionHandlers,
|
||||
selectLocalContextData,
|
||||
} from "./utils";
|
||||
|
||||
export const askProQuestions = async (program: QuestionArgs) => {
|
||||
if (!program.template) {
|
||||
const styledRepo = blue(
|
||||
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
|
||||
);
|
||||
const { template } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "template",
|
||||
message: "Which template would you like to use?",
|
||||
choices: [
|
||||
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
|
||||
{
|
||||
title: "Multi-agent app (using workflows)",
|
||||
value: "multiagent",
|
||||
},
|
||||
{ title: "Fullstack python template with Reflex", value: "reflex" },
|
||||
{
|
||||
title: `Community template from ${styledRepo}`,
|
||||
value: "community",
|
||||
},
|
||||
{
|
||||
title: "Example using a LlamaPack",
|
||||
value: "llamapack",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.template = template;
|
||||
}
|
||||
|
||||
if (program.template === "community") {
|
||||
const projectOptions = await getProjectOptions(
|
||||
COMMUNITY_OWNER,
|
||||
COMMUNITY_REPO,
|
||||
);
|
||||
const { communityProjectConfig } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "communityProjectConfig",
|
||||
message: "Select community template",
|
||||
choices: projectOptions.map(({ title, value }) => ({
|
||||
title,
|
||||
value: JSON.stringify(value), // serialize value to string in terminal
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
const projectConfig = JSON.parse(communityProjectConfig);
|
||||
program.communityProjectConfig = projectConfig;
|
||||
return; // early return - no further questions needed for community projects
|
||||
}
|
||||
|
||||
if (program.template === "llamapack") {
|
||||
const availableLlamaPacks = await getAvailableLlamapackOptions();
|
||||
const { llamapack } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "llamapack",
|
||||
message: "Select LlamaPack",
|
||||
choices: availableLlamaPacks.map((pack) => ({
|
||||
title: pack.name,
|
||||
value: pack.folderPath,
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamapack = llamapack;
|
||||
if (!program.postInstallAction) {
|
||||
program.postInstallAction = await askPostInstallAction(program);
|
||||
}
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.template === "reflex") {
|
||||
// Reflex template only supports FastAPI, empty data sources, and llamacloud
|
||||
// So we just use example file for extractor template, this allows user to choose vector database later
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
program.framework = "fastapi";
|
||||
// Ask for which Reflex use case to use
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to build?",
|
||||
choices: [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
if (!program.framework) {
|
||||
const choices = [
|
||||
{ title: "NextJS", value: "nextjs" },
|
||||
{ title: "Express", value: "express" },
|
||||
{ title: "FastAPI (Python)", value: "fastapi" },
|
||||
];
|
||||
|
||||
const { framework } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "framework",
|
||||
message: "Which framework would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.framework = framework;
|
||||
}
|
||||
|
||||
if (
|
||||
program.framework === "fastapi" &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
if (program.frontend === undefined) {
|
||||
const styledNextJS = blue("NextJS");
|
||||
const { frontend } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "frontend",
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
});
|
||||
program.frontend = Boolean(frontend);
|
||||
}
|
||||
} else {
|
||||
program.frontend = false;
|
||||
}
|
||||
|
||||
if (program.framework === "nextjs" || program.frontend) {
|
||||
if (!program.ui) {
|
||||
program.ui = "shadcn";
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.observability && program.template === "streaming") {
|
||||
const { observability } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "observability",
|
||||
message: "Would you like to set up observability?",
|
||||
choices: [
|
||||
{ title: "No", value: "none" },
|
||||
...(program.framework === "fastapi"
|
||||
? [{ title: "LlamaTrace", value: "llamatrace" }]
|
||||
: []),
|
||||
{ title: "Traceloop", value: "traceloop" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.observability = observability;
|
||||
}
|
||||
|
||||
if (
|
||||
(program.template === "reflex" || program.template === "multiagent") &&
|
||||
!program.useCase
|
||||
) {
|
||||
const choices =
|
||||
program.template === "reflex"
|
||||
? [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{ title: "Blog writer (Write a blog post)", value: "blog" },
|
||||
];
|
||||
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
// Configure framework and data sources for Reflex template
|
||||
if (program.template === "reflex") {
|
||||
program.framework = "fastapi";
|
||||
|
||||
program.dataSources =
|
||||
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
const modelConfig = await askModelConfig({
|
||||
openAiKey: program.openAiKey,
|
||||
askModels: program.askModels ?? false,
|
||||
framework: program.framework,
|
||||
});
|
||||
program.modelConfig = modelConfig;
|
||||
}
|
||||
|
||||
if (!program.vectorDb) {
|
||||
const { vectorDb } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "vectorDb",
|
||||
message: "Would you like to use a vector database?",
|
||||
choices: getVectorDbChoices(program.framework),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.vectorDb = vectorDb;
|
||||
}
|
||||
|
||||
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
|
||||
// When using a LlamaCloud index and no data sources are provided, just copy an example file
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
}
|
||||
|
||||
if (!program.dataSources) {
|
||||
program.dataSources = [];
|
||||
// continue asking user for data sources if none are initially provided
|
||||
while (true) {
|
||||
const firstQuestion = program.dataSources.length === 0;
|
||||
const choices = getDataSourceChoices(
|
||||
program.framework,
|
||||
program.dataSources,
|
||||
program.template,
|
||||
);
|
||||
if (choices.length === 0) break;
|
||||
const { selectedSource } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "selectedSource",
|
||||
message: firstQuestion
|
||||
? "Which data source would you like to use?"
|
||||
: "Would you like to add another data source?",
|
||||
choices,
|
||||
initial: firstQuestion ? 1 : 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
if (selectedSource === "no" || selectedSource === "none") {
|
||||
// user doesn't want another data source or any data source
|
||||
break;
|
||||
}
|
||||
switch (selectedSource) {
|
||||
case "exampleFile": {
|
||||
program.dataSources.push(EXAMPLE_FILE);
|
||||
break;
|
||||
}
|
||||
case "file":
|
||||
case "folder": {
|
||||
const selectedPaths = await selectLocalContextData(selectedSource);
|
||||
for (const p of selectedPaths) {
|
||||
program.dataSources.push({
|
||||
type: "file",
|
||||
config: {
|
||||
path: p,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "web": {
|
||||
const { baseUrl } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "baseUrl",
|
||||
message: "Please provide base URL of the website: ",
|
||||
initial: "https://www.llamaindex.ai",
|
||||
validate: (value: string) => {
|
||||
if (!value.includes("://")) {
|
||||
value = `https://${value}`;
|
||||
}
|
||||
const urlObj = new URL(value);
|
||||
if (
|
||||
urlObj.protocol !== "https:" &&
|
||||
urlObj.protocol !== "http:"
|
||||
) {
|
||||
return `URL=${value} has invalid protocol, only allow http or https`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.dataSources.push({
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl,
|
||||
prefix: baseUrl,
|
||||
depth: 1,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "db": {
|
||||
const dbPrompts: prompts.PromptObject<string>[] = [
|
||||
{
|
||||
type: "text",
|
||||
name: "uri",
|
||||
message:
|
||||
"Please enter the connection string (URI) for the database.",
|
||||
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return "Please provide a valid connection string";
|
||||
} else if (
|
||||
!(
|
||||
value.startsWith("mysql+pymysql://") ||
|
||||
value.startsWith("postgresql+psycopg://")
|
||||
)
|
||||
) {
|
||||
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// Only ask for a query, user can provide more complex queries in the config file later
|
||||
{
|
||||
type: (prev) => (prev ? "text" : null),
|
||||
name: "queries",
|
||||
message: "Please enter the SQL query to fetch data:",
|
||||
initial: "SELECT * FROM mytable",
|
||||
},
|
||||
];
|
||||
program.dataSources.push({
|
||||
type: "db",
|
||||
config: await prompts(dbPrompts, questionHandlers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUsingLlamaCloud = program.vectorDb === "llamacloud";
|
||||
|
||||
// Asking for LlamaParse if user selected file data source
|
||||
if (isUsingLlamaCloud) {
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = true;
|
||||
} else {
|
||||
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
|
||||
if (program.useLlamaParse === undefined && program.template !== "reflex") {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
const { useLlamaParse } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaParse",
|
||||
message:
|
||||
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useLlamaParse = useLlamaParse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
|
||||
if (isUsingLlamaCloud || program.useLlamaParse) {
|
||||
if (!program.llamaCloudKey && !isCI) {
|
||||
// if already set, don't ask again
|
||||
// Ask for LlamaCloud API key
|
||||
const { llamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamaCloudKey = llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!program.tools &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
const options = supportedTools.filter((t) =>
|
||||
t.supportedFrameworks?.includes(program.framework),
|
||||
);
|
||||
const toolChoices = options.map((tool) => ({
|
||||
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
|
||||
value: tool.name,
|
||||
}));
|
||||
const { toolsName } = await prompts({
|
||||
type: "multiselect",
|
||||
name: "toolsName",
|
||||
message:
|
||||
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
|
||||
choices: toolChoices,
|
||||
});
|
||||
const tools = toolsName?.map((tool: string) =>
|
||||
supportedTools.find((t) => t.name === tool),
|
||||
);
|
||||
program.tools = tools;
|
||||
}
|
||||
|
||||
if (!program.postInstallAction) {
|
||||
program.postInstallAction = await askPostInstallAction(program);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
import prompts from "prompts";
|
||||
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, questionHandlers } 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 prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "appType",
|
||||
message: "What app do you want to build?",
|
||||
hint: "🤖: Agent, 🔀: Workflow",
|
||||
choices: [
|
||||
{
|
||||
title: "🤖 Agentic RAG",
|
||||
value: "rag",
|
||||
description:
|
||||
"Chatbot that answers questions based on provided documents.",
|
||||
},
|
||||
{
|
||||
title: "🤖 Data Scientist",
|
||||
value: "data_scientist",
|
||||
description:
|
||||
"Agent that analyzes data and generates visualizations by using a code interpreter.",
|
||||
},
|
||||
{
|
||||
title: "🤖 Code Artifact Agent",
|
||||
value: "code_artifact",
|
||||
description:
|
||||
"Agent that writes code, runs it in a sandbox, and shows the output in the chat UI.",
|
||||
},
|
||||
{
|
||||
title: "🤖 Information Extractor",
|
||||
value: "extractor",
|
||||
description:
|
||||
"Extracts information from documents and returns it as a structured JSON object.",
|
||||
},
|
||||
{
|
||||
title: "🔀 Financial Report Generator",
|
||||
value: "financial_report_agent",
|
||||
description:
|
||||
"Generates a financial report by analyzing the provided 10-K SEC data. Uses a code interpreter to create charts or to conduct further analysis.",
|
||||
},
|
||||
{
|
||||
title: "🔀 Financial 10k SEC Form Filler",
|
||||
value: "form_filling",
|
||||
description:
|
||||
"Extracts information from 10k SEC data and uses it to fill out a CSV form.",
|
||||
},
|
||||
{
|
||||
title: "🔀 Contract Reviewer",
|
||||
value: "contract_review",
|
||||
description:
|
||||
"Extracts and reviews contracts to ensure compliance with GDPR regulations",
|
||||
},
|
||||
{
|
||||
title: "🔀 Deep Researcher",
|
||||
value: "deep_research",
|
||||
description:
|
||||
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
|
||||
},
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
let language: TemplateFramework = "fastapi";
|
||||
let llamaCloudKey = args.llamaCloudKey;
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (
|
||||
appType !== "extractor" &&
|
||||
appType !== "contract_review" &&
|
||||
appType !== "deep_research"
|
||||
) {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
|
||||
const results = await convertAnswers(args, {
|
||||
appType,
|
||||
language,
|
||||
useLlamaCloud,
|
||||
llamaCloudKey,
|
||||
});
|
||||
|
||||
results.postInstallAction = await askPostInstallAction(results);
|
||||
return results;
|
||||
};
|
||||
|
||||
const convertAnswers = async (
|
||||
args: PureQuestionArgs,
|
||||
answers: SimpleAnswers,
|
||||
): Promise<QuestionResults> => {
|
||||
const MODEL_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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,178 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
|
||||
import { toolsRequireConfig } from "../helpers/tools";
|
||||
import { QuestionResults } from "./types";
|
||||
|
||||
export const supportedContextFileTypes = [
|
||||
".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 { title: 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 = [
|
||||
{
|
||||
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 = !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({
|
||||
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,
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
@@ -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!
|
||||
@@ -0,0 +1,68 @@
|
||||
## Overview
|
||||
|
||||
This example is using three agents to generate a blog post:
|
||||
|
||||
- a researcher that retrieves content via a RAG pipeline,
|
||||
- a writer that specializes in writing blog posts and
|
||||
- a reviewer that is reviewing the blog post.
|
||||
|
||||
There are three different methods how the agents can interact to reach their goal:
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
curl --location 'localhost:8000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Write a blog post about physical standards for letters" }] }'
|
||||
```
|
||||
|
||||
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](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,34 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional
|
||||
|
||||
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
|
||||
|
||||
|
||||
def create_choreography(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(chat_history, **kwargs)
|
||||
publisher = create_publisher(chat_history)
|
||||
reviewer = FunctionCallingAgent(
|
||||
name="reviewer",
|
||||
description="expert in reviewing blog posts, needs a written post to review",
|
||||
system_prompt="You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. If the post is good, you can say 'The post is good.'",
|
||||
chat_history=chat_history,
|
||||
)
|
||||
return AgentCallingAgent(
|
||||
name="writer",
|
||||
agents=[researcher, reviewer, publisher],
|
||||
description="expert in writing blog posts, needs researched information and images to write a blog post",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in writing blog posts. You are given a task to write a blog post. Before starting to write the post, consult the researcher agent to get the information you need. Don't make up any information yourself.
|
||||
After creating a draft for the post, send it to the reviewer agent to receive feedback and make sure to incorporate the feedback from the reviewer.
|
||||
You can consult the reviewer and researcher a maximum of two times. Your output should contain only the blog post.
|
||||
Finally, always request the publisher to create a document (PDF, HTML) and publish the blog post.
|
||||
"""
|
||||
),
|
||||
# TODO: add chat_history support to AgentCallingAgent
|
||||
# chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional
|
||||
|
||||
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
|
||||
|
||||
|
||||
def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(chat_history, **kwargs)
|
||||
writer = FunctionCallingAgent(
|
||||
name="writer",
|
||||
description="expert in writing blog posts, need information and images to write a post",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in writing blog posts.
|
||||
You are given a task to write a blog post. Do not make up any information yourself.
|
||||
If you don't have the necessary information to write a blog post, reply "I need information about the topic to write the blog post".
|
||||
If you need to use images, reply "I need images about the topic to write the blog post". Do not use any dummy images made up by you.
|
||||
If you have all the information needed, write the blog post.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
reviewer = FunctionCallingAgent(
|
||||
name="reviewer",
|
||||
description="expert in reviewing blog posts, needs a written blog post to review",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post and fix any issues found yourself. You must output a final blog post.
|
||||
A post must include at least one valid image. If not, reply "I need images about the topic to write the blog post". An image URL starting with "example" or "your website" is not valid.
|
||||
Especially check for logical inconsistencies and proofread the post for grammar and spelling errors.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
publisher = create_publisher(chat_history)
|
||||
return AgentOrchestrator(
|
||||
agents=[writer, reviewer, researcher, publisher],
|
||||
refine_plan=False,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Tuple
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 "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 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:
|
||||
prompt_instructions = "You don't have a tool to generate document. Please reply the content directly."
|
||||
description = "Expert in publishing the blog post"
|
||||
return tools, prompt_instructions, description
|
||||
|
||||
|
||||
def create_publisher(chat_history: List[ChatMessage]):
|
||||
tools, prompt_instructions, description = get_publisher_tools()
|
||||
return FunctionCallingAgent(
|
||||
name="publisher",
|
||||
tools=tools,
|
||||
description=description,
|
||||
system_prompt=prompt_instructions,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
|
||||
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 app.engine.tools.query_engine import get_query_engine_tool
|
||||
|
||||
|
||||
def _get_research_tools(**kwargs):
|
||||
"""
|
||||
Researcher take responsibility for retrieving information.
|
||||
Try init wikipedia or duckduckgo tool if available.
|
||||
"""
|
||||
tools = []
|
||||
# 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.append(tool)
|
||||
return tools
|
||||
|
||||
|
||||
def create_researcher(chat_history: List[ChatMessage], **kwargs):
|
||||
"""
|
||||
Researcher is an agent that take responsibility for using tools to complete a given task.
|
||||
"""
|
||||
tools = _get_research_tools(**kwargs)
|
||||
return FunctionCallingAgent(
|
||||
name="researcher",
|
||||
tools=tools,
|
||||
description="expert in retrieving any unknown content or searching for images from the internet",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are a researcher agent. You are given a research task.
|
||||
|
||||
If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
|
||||
Otherwise, you must use tools to retrieve information or images needed for the task.
|
||||
|
||||
It's normal for the task to include some ambiguity. You must always think carefully about the context of the user's request to understand what are the main content needs to be retrieved.
|
||||
Example:
|
||||
Request: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
->Though: The main content is "history of the internet", while "write in English and publish in PDF format" is a requirement for other agents.
|
||||
Your task: Look for information in English about the history of the Internet.
|
||||
This is not your task: Create a blog post or look for how to create a PDF.
|
||||
|
||||
Next request: "Publish the blog post in HTML format."
|
||||
->Though: User just asking for a format change, the previous content is still valid.
|
||||
Your task: Return the previous content of the post to the writer. No need to do any research.
|
||||
This is not your task: Look for how to create an HTML file.
|
||||
|
||||
If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
|
||||
If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history."
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,267 @@
|
||||
from textwrap import dedent
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
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
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(
|
||||
chat_history=chat_history,
|
||||
**kwargs,
|
||||
)
|
||||
publisher = create_publisher(
|
||||
chat_history=chat_history,
|
||||
)
|
||||
writer = FunctionCallingAgent(
|
||||
name="writer",
|
||||
description="expert in writing blog posts, need information and images to write a post.",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in writing blog posts.
|
||||
You are given the task of writing a blog post based on research content provided by the researcher agent. Do not invent any information yourself.
|
||||
It's important to read the entire conversation history to write the blog post accurately.
|
||||
If you receive a review from the reviewer, update the post according to the feedback and return the new post content.
|
||||
If the content is not valid (e.g., broken link, broken image, etc.), do not use it.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to write the post correctly.
|
||||
If you update the post based on the reviewer's feedback, first explain what changes you made to the post, then provide the new post content. Do not include the reviewer's comments.
|
||||
Example:
|
||||
Task: "Here is the information I found about the history of the internet:
|
||||
Create a blog post about the history of the internet, write in English, and publish in PDF format."
|
||||
-> Your task: Use the research content {...} to write a blog post in English.
|
||||
-> This is not your task: Create a PDF
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
reviewer = FunctionCallingAgent(
|
||||
name="reviewer",
|
||||
description="expert in reviewing blog posts, needs a written blog post to review.",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in reviewing blog posts.
|
||||
You are given a task to review a blog post. As a reviewer, it's important that your review aligns with the user's request. Please focus on the user's request when reviewing the post.
|
||||
Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement.
|
||||
Furthermore, proofread the post for grammar and spelling errors.
|
||||
Only if the post is good enough for publishing should you return 'The post is good.' In all other cases, return your review.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to review the post correctly.
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
|
||||
Example:
|
||||
Task: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
-> Your task: Review whether the main content of the post is about the history of the internet and if it is written in English.
|
||||
-> This is not your task: Create blog post, create PDF, write in English.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
workflow = BlogPostWorkflow(
|
||||
timeout=360, chat_history=chat_history
|
||||
) # Pass chat_history here
|
||||
workflow.add_workflows(
|
||||
researcher=researcher,
|
||||
writer=writer,
|
||||
reviewer=reviewer,
|
||||
publisher=publisher,
|
||||
)
|
||||
return workflow
|
||||
|
||||
|
||||
class ResearchEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class WriteEvent(Event):
|
||||
input: str
|
||||
is_good: bool = False
|
||||
|
||||
|
||||
class ReviewEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class PublishEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class BlogPostWorkflow(Workflow):
|
||||
def __init__(
|
||||
self, timeout: int = 360, chat_history: Optional[List[ChatMessage]] = None
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.chat_history = chat_history or []
|
||||
|
||||
@step()
|
||||
async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent | PublishEvent:
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
# start the workflow with researching about a topic
|
||||
ctx.data["task"] = ev.input
|
||||
ctx.data["user_input"] = ev.input
|
||||
|
||||
# Decision-making process
|
||||
decision = await self._decide_workflow(ev.input, self.chat_history)
|
||||
|
||||
if decision != "publish":
|
||||
return ResearchEvent(input=f"Research for this task: {ev.input}")
|
||||
else:
|
||||
chat_history_str = "\n".join(
|
||||
[f"{msg.role}: {msg.content}" for msg in self.chat_history]
|
||||
)
|
||||
return PublishEvent(
|
||||
input=f"Please publish content based on the chat history\n{chat_history_str}\n\n and task: {ev.input}"
|
||||
)
|
||||
|
||||
async def _decide_workflow(
|
||||
self, input: str, chat_history: List[ChatMessage]
|
||||
) -> str:
|
||||
prompt_template = PromptTemplate(
|
||||
dedent(
|
||||
"""
|
||||
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:
|
||||
{chat_history}
|
||||
|
||||
The current user request is:
|
||||
{input}
|
||||
|
||||
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'):
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
chat_history_str = "\n".join(
|
||||
[f"{msg.role}: {msg.content}" for msg in chat_history]
|
||||
)
|
||||
prompt = prompt_template.format(chat_history=chat_history_str, input=input)
|
||||
|
||||
output = await Settings.llm.acomplete(prompt)
|
||||
decision = output.text.strip().lower()
|
||||
|
||||
return "publish" if decision == "publish" else "research"
|
||||
|
||||
@step()
|
||||
async def research(
|
||||
self, ctx: Context, ev: ResearchEvent, researcher: FunctionCallingAgent
|
||||
) -> WriteEvent:
|
||||
result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input)
|
||||
content = result.response.message.content
|
||||
return WriteEvent(
|
||||
input=f"Write a blog post given this task: {ctx.data['task']} using this research content: {content}"
|
||||
)
|
||||
|
||||
@step()
|
||||
async def write(
|
||||
self, ctx: Context, ev: WriteEvent, writer: FunctionCallingAgent
|
||||
) -> ReviewEvent | StopEvent:
|
||||
MAX_ATTEMPTS = 2
|
||||
ctx.data["attempts"] = ctx.data.get("attempts", 0) + 1
|
||||
too_many_attempts = ctx.data["attempts"] > MAX_ATTEMPTS
|
||||
if too_many_attempts:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=writer.name,
|
||||
msg=f"Too many attempts ({MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.",
|
||||
)
|
||||
)
|
||||
if ev.is_good or too_many_attempts:
|
||||
# too many attempts or the blog post is good - stream final response if requested
|
||||
result = await self.run_agent(
|
||||
ctx,
|
||||
writer,
|
||||
f"Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: {ev.input}",
|
||||
streaming=ctx.data["streaming"],
|
||||
)
|
||||
return StopEvent(result=result)
|
||||
result: AgentRunResult = await self.run_agent(ctx, writer, ev.input)
|
||||
ctx.data["result"] = result
|
||||
return ReviewEvent(input=result.response.message.content)
|
||||
|
||||
@step()
|
||||
async def review(
|
||||
self, ctx: Context, ev: ReviewEvent, reviewer: FunctionCallingAgent
|
||||
) -> WriteEvent:
|
||||
result: AgentRunResult = await self.run_agent(ctx, reviewer, ev.input)
|
||||
review = result.response.message.content
|
||||
old_content = ctx.data["result"].response.message.content
|
||||
post_is_good = "post is good" in review.lower()
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=reviewer.name,
|
||||
msg=f"The post is {'not ' if not post_is_good else ''}good enough for publishing. Sending back to the writer{' for publication.' if post_is_good else '.'}",
|
||||
)
|
||||
)
|
||||
if post_is_good:
|
||||
return WriteEvent(
|
||||
input=f"You're blog post is ready for publication. Please respond with just the blog post. Blog post: ```{old_content}```",
|
||||
is_good=True,
|
||||
)
|
||||
else:
|
||||
return WriteEvent(
|
||||
input=dedent(
|
||||
f"""
|
||||
Improve the writing of a given blog post by using a given review.
|
||||
Blog post:
|
||||
```
|
||||
{old_content}
|
||||
```
|
||||
|
||||
Review:
|
||||
```
|
||||
{review}
|
||||
```
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
@step()
|
||||
async def publish(
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: PublishEvent,
|
||||
publisher: FunctionCallingAgent,
|
||||
) -> StopEvent:
|
||||
try:
|
||||
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(
|
||||
AgentRunEvent(
|
||||
name=publisher.name,
|
||||
msg=f"Error publishing: {e}",
|
||||
)
|
||||
)
|
||||
return StopEvent(result=None)
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
ctx: Context,
|
||||
agent: FunctionCallingAgent,
|
||||
input: str,
|
||||
streaming: bool = False,
|
||||
) -> AgentRunResult | AsyncGenerator:
|
||||
handler = agent.run(input=input, streaming=streaming)
|
||||
# bubble all events while running the executor to the planner
|
||||
async for event in handler.stream_events():
|
||||
# Don't write the StopEvent from sub task to the stream
|
||||
if type(event) is not StopEvent:
|
||||
ctx.write_event_to_stream(event)
|
||||
return await handler
|
||||
@@ -0,0 +1,3 @@
|
||||
from .blog import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
@@ -0,0 +1,30 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
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 create_workflow(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
# Chat filters are not supported yet
|
||||
kwargs.pop("filters", None)
|
||||
agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
|
||||
match agent_type:
|
||||
case "choreography":
|
||||
agent = create_choreography(chat_history, **kwargs)
|
||||
case "orchestrator":
|
||||
agent = create_orchestrator(chat_history, **kwargs)
|
||||
case _:
|
||||
agent = create_blog_workflow(chat_history, **kwargs)
|
||||
|
||||
logger.info(f"Using agent pattern: {agent_type}")
|
||||
|
||||
return agent
|
||||
@@ -0,0 +1,86 @@
|
||||
from typing import Any, List
|
||||
|
||||
from app.workflows.planner import StructuredPlannerAgent
|
||||
from app.workflows.single import (
|
||||
AgentRunResult,
|
||||
ContextAwareTool,
|
||||
FunctionCallingAgent,
|
||||
)
|
||||
from llama_index.core.tools.types import ToolMetadata, ToolOutput
|
||||
from llama_index.core.tools.utils import create_schema_from_function
|
||||
from llama_index.core.workflow import Context, StopEvent, Workflow
|
||||
|
||||
|
||||
class AgentCallTool(ContextAwareTool):
|
||||
def __init__(self, agent: Workflow) -> None:
|
||||
self.agent = agent
|
||||
name = f"call_{agent.name}"
|
||||
|
||||
async def schema_call(input: str) -> str:
|
||||
pass
|
||||
|
||||
# create the schema without the Context
|
||||
fn_schema = create_schema_from_function(name, schema_call)
|
||||
self._metadata = ToolMetadata(
|
||||
name=name,
|
||||
description=(
|
||||
f"Use this tool to delegate a sub task to the {agent.name} agent."
|
||||
+ (
|
||||
f" The agent is an {agent.description}."
|
||||
if agent.description
|
||||
else ""
|
||||
)
|
||||
),
|
||||
fn_schema=fn_schema,
|
||||
)
|
||||
|
||||
# overload the acall function with the ctx argument as it's needed for bubbling the events
|
||||
async def acall(self, ctx: Context, input: str) -> ToolOutput:
|
||||
handler = self.agent.run(input=input)
|
||||
# bubble all events while running the agent to the calling agent
|
||||
async for ev in handler.stream_events():
|
||||
if type(ev) is not StopEvent:
|
||||
ctx.write_event_to_stream(ev)
|
||||
ret: AgentRunResult = await handler
|
||||
response = ret.response.message.content
|
||||
return ToolOutput(
|
||||
content=str(response),
|
||||
tool_name=self.metadata.name,
|
||||
raw_input={"args": input, "kwargs": {}},
|
||||
raw_output=response,
|
||||
)
|
||||
|
||||
|
||||
class AgentCallingAgent(FunctionCallingAgent):
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
name: str,
|
||||
agents: List[FunctionCallingAgent] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
agents = agents or []
|
||||
tools = [AgentCallTool(agent=agent) for agent in agents]
|
||||
super().__init__(*args, name=name, tools=tools, **kwargs)
|
||||
# call add_workflows so agents will get detected by llama agents automatically
|
||||
self.add_workflows(**{agent.name: agent for agent in agents})
|
||||
|
||||
|
||||
class AgentOrchestrator(StructuredPlannerAgent):
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
name: str = "orchestrator",
|
||||
agents: List[FunctionCallingAgent] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
agents = agents or []
|
||||
tools = [AgentCallTool(agent=agent) for agent in agents]
|
||||
super().__init__(
|
||||
*args,
|
||||
name=name,
|
||||
tools=tools,
|
||||
**kwargs,
|
||||
)
|
||||
# call add_workflows so agents will get detected by llama agents automatically
|
||||
self.add_workflows(**{agent.name: agent for agent in agents})
|
||||
@@ -0,0 +1,347 @@
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
from llama_index.core.agent.runner.planner import (
|
||||
DEFAULT_INITIAL_PLAN_PROMPT,
|
||||
DEFAULT_PLAN_REFINE_PROMPT,
|
||||
Plan,
|
||||
PlannerAgentState,
|
||||
SubTask,
|
||||
)
|
||||
from llama_index.core.bridge.pydantic import ValidationError
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import BaseTool
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
INITIAL_PLANNER_PROMPT = """\
|
||||
Think step-by-step. Given a conversation, set of tools and a user request. Your responsibility is to create a plan to complete the task.
|
||||
The plan must adapt with the user request and the conversation.
|
||||
|
||||
The tools available are:
|
||||
{tools_str}
|
||||
|
||||
Conversation: {chat_history}
|
||||
|
||||
Overall Task: {task}
|
||||
"""
|
||||
|
||||
|
||||
class ExecutePlanEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
class SubTaskEvent(Event):
|
||||
sub_task: SubTask
|
||||
|
||||
|
||||
class SubTaskResultEvent(Event):
|
||||
sub_task: SubTask
|
||||
result: AgentRunResult | AsyncGenerator
|
||||
|
||||
|
||||
class PlanEventType(Enum):
|
||||
CREATED = "created"
|
||||
REFINED = "refined"
|
||||
|
||||
|
||||
class PlanEvent(AgentRunEvent):
|
||||
event_type: PlanEventType
|
||||
plan: Plan
|
||||
|
||||
@property
|
||||
def msg(self) -> str:
|
||||
sub_task_names = ", ".join(task.name for task in self.plan.sub_tasks)
|
||||
return f"Plan {self.event_type.value}: Let's do: {sub_task_names}"
|
||||
|
||||
|
||||
class StructuredPlannerAgent(Workflow):
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
name: str,
|
||||
llm: FunctionCallingLLM | None = None,
|
||||
tools: List[BaseTool] | None = None,
|
||||
timeout: float = 360.0,
|
||||
refine_plan: bool = False,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, timeout=timeout, **kwargs)
|
||||
self.name = name
|
||||
self.refine_plan = refine_plan
|
||||
self.chat_history = chat_history
|
||||
|
||||
self.tools = tools or []
|
||||
self.planner = Planner(
|
||||
llm=llm,
|
||||
tools=self.tools,
|
||||
initial_plan_prompt=INITIAL_PLANNER_PROMPT,
|
||||
verbose=self._verbose,
|
||||
)
|
||||
# The executor is keeping the memory of all tool calls and decides to call the right tool for the task
|
||||
self.executor = FunctionCallingAgent(
|
||||
name="executor",
|
||||
llm=llm,
|
||||
tools=self.tools,
|
||||
write_events=False,
|
||||
# it's important to instruct to just return the tool call, otherwise the executor will interpret and change the result
|
||||
system_prompt="You are an expert in completing given tasks by calling the right tool for the task. Just return the result of the tool call. Don't add any information yourself",
|
||||
)
|
||||
self.add_workflows(executor=self.executor)
|
||||
|
||||
@step()
|
||||
async def create_plan(
|
||||
self, ctx: Context, ev: StartEvent
|
||||
) -> ExecutePlanEvent | StopEvent:
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
ctx.data["task"] = ev.input
|
||||
|
||||
plan_id, plan = await self.planner.create_plan(
|
||||
input=ev.input, chat_history=self.chat_history
|
||||
)
|
||||
ctx.data["act_plan_id"] = plan_id
|
||||
|
||||
# inform about the new plan
|
||||
ctx.write_event_to_stream(
|
||||
PlanEvent(name=self.name, event_type=PlanEventType.CREATED, plan=plan)
|
||||
)
|
||||
if self._verbose:
|
||||
print("=== Executing plan ===\n")
|
||||
return ExecutePlanEvent()
|
||||
|
||||
@step()
|
||||
async def execute_plan(self, ctx: Context, ev: ExecutePlanEvent) -> SubTaskEvent:
|
||||
upcoming_sub_tasks = self.planner.state.get_next_sub_tasks(
|
||||
ctx.data["act_plan_id"]
|
||||
)
|
||||
|
||||
if upcoming_sub_tasks:
|
||||
# Execute only the first sub-task
|
||||
# otherwise the executor will get over-lapping messages
|
||||
# alternatively, we could use one executor for all sub tasks
|
||||
next_sub_task = upcoming_sub_tasks[0]
|
||||
return SubTaskEvent(sub_task=next_sub_task)
|
||||
|
||||
return None
|
||||
|
||||
@step()
|
||||
async def execute_sub_task(
|
||||
self, ctx: Context, ev: SubTaskEvent
|
||||
) -> SubTaskResultEvent:
|
||||
if self._verbose:
|
||||
print(f"=== Executing sub task: {ev.sub_task.name} ===")
|
||||
is_last_tasks = self.get_remaining_subtasks(ctx) == 1
|
||||
# TODO: streaming only works without plan refining
|
||||
streaming = is_last_tasks and ctx.data["streaming"] and not self.refine_plan
|
||||
handler = self.executor.run(
|
||||
input=ev.sub_task.input,
|
||||
streaming=streaming,
|
||||
)
|
||||
# bubble all events while running the executor to the planner
|
||||
async for event in handler.stream_events():
|
||||
# Don't write the StopEvent from sub task to the stream
|
||||
if type(event) is not StopEvent:
|
||||
ctx.write_event_to_stream(event)
|
||||
result: AgentRunResult = await handler
|
||||
if self._verbose:
|
||||
print("=== Done executing sub task ===\n")
|
||||
self.planner.state.add_completed_sub_task(ctx.data["act_plan_id"], ev.sub_task)
|
||||
return SubTaskResultEvent(sub_task=ev.sub_task, result=result)
|
||||
|
||||
@step()
|
||||
async def gather_results(
|
||||
self, ctx: Context, ev: SubTaskResultEvent
|
||||
) -> ExecutePlanEvent | StopEvent:
|
||||
result = ev
|
||||
|
||||
upcoming_sub_tasks = self.get_upcoming_sub_tasks(ctx)
|
||||
# if no more tasks to do, stop workflow and send result of last step
|
||||
if upcoming_sub_tasks == 0:
|
||||
return StopEvent(result=result.result)
|
||||
|
||||
if self.refine_plan:
|
||||
# store the result for refining the plan
|
||||
ctx.data["results"] = ctx.data.get("results", {})
|
||||
ctx.data["results"][result.sub_task.name] = result.result
|
||||
|
||||
new_plan = await self.planner.refine_plan(
|
||||
ctx.data["task"], ctx.data["act_plan_id"], ctx.data["results"]
|
||||
)
|
||||
# inform about the new plan
|
||||
if new_plan is not None:
|
||||
ctx.write_event_to_stream(
|
||||
PlanEvent(
|
||||
name=self.name, event_type=PlanEventType.REFINED, plan=new_plan
|
||||
)
|
||||
)
|
||||
|
||||
# continue executing plan
|
||||
return ExecutePlanEvent()
|
||||
|
||||
def get_upcoming_sub_tasks(self, ctx: Context):
|
||||
upcoming_sub_tasks = self.planner.state.get_next_sub_tasks(
|
||||
ctx.data["act_plan_id"]
|
||||
)
|
||||
return len(upcoming_sub_tasks)
|
||||
|
||||
def get_remaining_subtasks(self, ctx: Context):
|
||||
remaining_subtasks = self.planner.state.get_remaining_subtasks(
|
||||
ctx.data["act_plan_id"]
|
||||
)
|
||||
return len(remaining_subtasks)
|
||||
|
||||
|
||||
# Concern dealing with creating and refining a plan, extracted from https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/runner/planner.py#L138
|
||||
class Planner:
|
||||
def __init__(
|
||||
self,
|
||||
llm: FunctionCallingLLM | None = None,
|
||||
tools: List[BaseTool] | None = None,
|
||||
initial_plan_prompt: Union[str, PromptTemplate] = DEFAULT_INITIAL_PLAN_PROMPT,
|
||||
plan_refine_prompt: Union[str, PromptTemplate] = DEFAULT_PLAN_REFINE_PROMPT,
|
||||
verbose: bool = True,
|
||||
) -> None:
|
||||
if llm is None:
|
||||
llm = Settings.llm
|
||||
self.llm = llm
|
||||
assert self.llm.metadata.is_function_calling_model
|
||||
|
||||
self.tools = tools or []
|
||||
self.state = PlannerAgentState()
|
||||
self.verbose = verbose
|
||||
|
||||
if isinstance(initial_plan_prompt, str):
|
||||
initial_plan_prompt = PromptTemplate(initial_plan_prompt)
|
||||
self.initial_plan_prompt = initial_plan_prompt
|
||||
|
||||
if isinstance(plan_refine_prompt, str):
|
||||
plan_refine_prompt = PromptTemplate(plan_refine_prompt)
|
||||
self.plan_refine_prompt = plan_refine_prompt
|
||||
|
||||
async def create_plan(
|
||||
self, input: str, chat_history: Optional[List[ChatMessage]] = None
|
||||
) -> Tuple[str, Plan]:
|
||||
tools = self.tools
|
||||
tools_str = ""
|
||||
for tool in tools:
|
||||
tools_str += tool.metadata.name + ": " + tool.metadata.description + "\n"
|
||||
|
||||
try:
|
||||
plan = await self.llm.astructured_predict(
|
||||
Plan,
|
||||
self.initial_plan_prompt,
|
||||
tools_str=tools_str,
|
||||
task=input,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
except (ValueError, ValidationError):
|
||||
if self.verbose:
|
||||
print("No complex plan predicted. Defaulting to a single task plan.")
|
||||
plan = Plan(
|
||||
sub_tasks=[
|
||||
SubTask(
|
||||
name="default", input=input, expected_output="", dependencies=[]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
print("=== Initial plan ===")
|
||||
for sub_task in plan.sub_tasks:
|
||||
print(
|
||||
f"{sub_task.name}:\n{sub_task.input} -> {sub_task.expected_output}\ndeps: {sub_task.dependencies}\n\n"
|
||||
)
|
||||
|
||||
plan_id = str(uuid.uuid4())
|
||||
self.state.plan_dict[plan_id] = plan
|
||||
|
||||
return plan_id, plan
|
||||
|
||||
async def refine_plan(
|
||||
self,
|
||||
input: str,
|
||||
plan_id: str,
|
||||
completed_sub_tasks: Dict[str, str],
|
||||
) -> Optional[Plan]:
|
||||
"""Refine a plan."""
|
||||
prompt_args = self.get_refine_plan_prompt_kwargs(
|
||||
plan_id, input, completed_sub_tasks
|
||||
)
|
||||
|
||||
try:
|
||||
new_plan = await self.llm.astructured_predict(
|
||||
Plan, self.plan_refine_prompt, **prompt_args
|
||||
)
|
||||
|
||||
self._update_plan(plan_id, new_plan)
|
||||
|
||||
return new_plan
|
||||
except (ValueError, ValidationError) as e:
|
||||
# likely no new plan predicted
|
||||
if self.verbose:
|
||||
print(f"No new plan predicted: {e}")
|
||||
return None
|
||||
|
||||
def _update_plan(self, plan_id: str, new_plan: Plan) -> None:
|
||||
"""Update the plan."""
|
||||
# update state with new plan
|
||||
self.state.plan_dict[plan_id] = new_plan
|
||||
|
||||
if self.verbose:
|
||||
print("=== Refined plan ===")
|
||||
for sub_task in new_plan.sub_tasks:
|
||||
print(
|
||||
f"{sub_task.name}:\n{sub_task.input} -> {sub_task.expected_output}\ndeps: {sub_task.dependencies}\n\n"
|
||||
)
|
||||
|
||||
def get_refine_plan_prompt_kwargs(
|
||||
self,
|
||||
plan_id: str,
|
||||
task: str,
|
||||
completed_sub_task: Dict[str, str],
|
||||
) -> dict:
|
||||
"""Get the refine plan prompt."""
|
||||
# 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\t{task_output!s}\n"
|
||||
completed_outputs_str += task_str
|
||||
|
||||
# get a string for the remaining sub-tasks
|
||||
remaining_sub_tasks = self.state.get_remaining_subtasks(plan_id)
|
||||
remaining_sub_tasks_str = "" if len(remaining_sub_tasks) != 0 else "None"
|
||||
for sub_task in remaining_sub_tasks:
|
||||
task_str = (
|
||||
f"SubTask(name='{sub_task.name}', "
|
||||
f"input='{sub_task.input}', "
|
||||
f"expected_output='{sub_task.expected_output}', "
|
||||
f"dependencies='{sub_task.dependencies}')\n"
|
||||
)
|
||||
remaining_sub_tasks_str += task_str
|
||||
|
||||
# get the tools string
|
||||
tools = self.tools
|
||||
tools_str = ""
|
||||
for tool in tools:
|
||||
tools_str += tool.metadata.name + ": " + tool.metadata.description + "\n"
|
||||
|
||||
# return the kwargs
|
||||
return {
|
||||
"tools_str": tools_str.strip(),
|
||||
"task": task.strip(),
|
||||
"completed_outputs": completed_outputs_str.strip(),
|
||||
"remaining_sub_tasks": remaining_sub_tasks_str.strip(),
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Any, AsyncGenerator, List, Optional
|
||||
|
||||
from llama_index.core.llms import ChatMessage, ChatResponse
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import FunctionTool, ToolOutput, ToolSelection
|
||||
from llama_index.core.tools.types import BaseTool
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: list[ChatMessage]
|
||||
|
||||
|
||||
class ToolCallEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class AgentRunEventType(Enum):
|
||||
TEXT = "text"
|
||||
PROGRESS = "progress"
|
||||
|
||||
|
||||
class AgentRunEvent(Event):
|
||||
name: str
|
||||
msg: str
|
||||
event_type: AgentRunEventType = Field(default=AgentRunEventType.TEXT)
|
||||
data: Optional[dict] = None
|
||||
|
||||
def to_response(self) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"agent": self.name,
|
||||
"type": self.event_type.value,
|
||||
"text": self.msg,
|
||||
"data": self.data,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AgentRunResult(BaseModel):
|
||||
response: ChatResponse
|
||||
sources: list[ToolOutput]
|
||||
|
||||
|
||||
class ContextAwareTool(FunctionTool):
|
||||
@abstractmethod
|
||||
async def acall(self, ctx: Context, input: Any) -> ToolOutput:
|
||||
pass
|
||||
|
||||
|
||||
class FunctionCallingAgent(Workflow):
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
llm: FunctionCallingLLM | None = None,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
tools: List[BaseTool] | None = None,
|
||||
system_prompt: str | None = None,
|
||||
verbose: bool = False,
|
||||
timeout: float = 360.0,
|
||||
name: str,
|
||||
write_events: bool = True,
|
||||
description: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, verbose=verbose, timeout=timeout, **kwargs)
|
||||
self.tools = tools or []
|
||||
self.name = name
|
||||
self.write_events = write_events
|
||||
self.description = description
|
||||
|
||||
if llm is None:
|
||||
llm = Settings.llm
|
||||
self.llm = llm
|
||||
assert self.llm.metadata.is_function_calling_model
|
||||
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
self.memory = ChatMemoryBuffer.from_defaults(
|
||||
llm=self.llm, chat_history=chat_history
|
||||
)
|
||||
self.sources = []
|
||||
|
||||
@step()
|
||||
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
# clear sources
|
||||
self.sources = []
|
||||
|
||||
# set system prompt
|
||||
if self.system_prompt is not None:
|
||||
system_msg = ChatMessage(role="system", content=self.system_prompt)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
|
||||
# get user input
|
||||
user_input = ev.input
|
||||
user_msg = ChatMessage(role="user", content=user_input)
|
||||
self.memory.put(user_msg)
|
||||
if self.write_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(name=self.name, msg=f"Start to work on: {user_input}")
|
||||
)
|
||||
|
||||
# get chat history
|
||||
chat_history = self.memory.get()
|
||||
return InputEvent(input=chat_history)
|
||||
|
||||
@step()
|
||||
async def handle_llm_input(
|
||||
self, ctx: Context, ev: InputEvent
|
||||
) -> ToolCallEvent | StopEvent:
|
||||
if ctx.data["streaming"]:
|
||||
return await self.handle_llm_input_stream(ctx, ev)
|
||||
|
||||
chat_history = ev.input
|
||||
|
||||
response = await self.llm.achat_with_tools(
|
||||
self.tools, chat_history=chat_history
|
||||
)
|
||||
self.memory.put(response.message)
|
||||
|
||||
tool_calls = self.llm.get_tool_calls_from_response(
|
||||
response, error_on_no_tool_call=False
|
||||
)
|
||||
|
||||
if not tool_calls:
|
||||
if self.write_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(name=self.name, msg="Finished task")
|
||||
)
|
||||
return StopEvent(
|
||||
result=AgentRunResult(response=response, sources=[*self.sources])
|
||||
)
|
||||
else:
|
||||
return ToolCallEvent(tool_calls=tool_calls)
|
||||
|
||||
async def handle_llm_input_stream(
|
||||
self, ctx: Context, ev: InputEvent
|
||||
) -> ToolCallEvent | StopEvent:
|
||||
chat_history = ev.input
|
||||
|
||||
async def response_generator() -> AsyncGenerator:
|
||||
response_stream = await self.llm.astream_chat_with_tools(
|
||||
self.tools, chat_history=chat_history
|
||||
)
|
||||
|
||||
full_response = None
|
||||
yielded_indicator = False
|
||||
async for chunk in response_stream:
|
||||
if "tool_calls" not in chunk.message.additional_kwargs:
|
||||
# Yield a boolean to indicate whether the response is a tool call
|
||||
if not yielded_indicator:
|
||||
yield False
|
||||
yielded_indicator = True
|
||||
|
||||
# if not a tool call, yield the chunks!
|
||||
yield chunk
|
||||
elif not yielded_indicator:
|
||||
# Yield the indicator for a tool call
|
||||
yield True
|
||||
yielded_indicator = True
|
||||
|
||||
full_response = chunk
|
||||
|
||||
# Write the full response to memory
|
||||
self.memory.put(full_response.message)
|
||||
|
||||
# Yield the final response
|
||||
yield full_response
|
||||
|
||||
# Start the generator
|
||||
generator = response_generator()
|
||||
|
||||
# Check for immediate tool call
|
||||
is_tool_call = await generator.__anext__()
|
||||
if is_tool_call:
|
||||
full_response = await generator.__anext__()
|
||||
tool_calls = self.llm.get_tool_calls_from_response(full_response)
|
||||
return ToolCallEvent(tool_calls=tool_calls)
|
||||
|
||||
# If we've reached here, it's not an immediate tool call, so we return the generator
|
||||
if self.write_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(name=self.name, msg="Finished task")
|
||||
)
|
||||
return StopEvent(result=generator)
|
||||
|
||||
@step()
|
||||
async def handle_tool_calls(self, ctx: Context, ev: ToolCallEvent) -> InputEvent:
|
||||
tool_calls = ev.tool_calls
|
||||
tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}
|
||||
|
||||
tool_msgs = []
|
||||
|
||||
# call tools -- safely!
|
||||
for tool_call in tool_calls:
|
||||
tool = tools_by_name.get(tool_call.tool_name)
|
||||
additional_kwargs = {
|
||||
"tool_call_id": tool_call.tool_id,
|
||||
"name": tool.metadata.get_name(),
|
||||
}
|
||||
if not tool:
|
||||
tool_msgs.append(
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
content=f"Tool {tool_call.tool_name} does not exist",
|
||||
additional_kwargs=additional_kwargs,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
if isinstance(tool, ContextAwareTool):
|
||||
# inject context for calling an context aware tool
|
||||
tool_output = await tool.acall(ctx=ctx, **tool_call.tool_kwargs)
|
||||
else:
|
||||
tool_output = await tool.acall(**tool_call.tool_kwargs)
|
||||
self.sources.append(tool_output)
|
||||
tool_msgs.append(
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
content=tool_output.content,
|
||||
additional_kwargs=additional_kwargs,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
tool_msgs.append(
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
content=f"Encountered error in tool call: {e}",
|
||||
additional_kwargs=additional_kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
for msg in tool_msgs:
|
||||
self.memory.put(msg)
|
||||
|
||||
chat_history = self.memory.get()
|
||||
return InputEvent(input=chat_history)
|
||||
@@ -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,183 @@
|
||||
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="""
|
||||
If the decision is to research, provide a list of questions to research that related to the user request.
|
||||
Maximum 3 questions. Set to null or empty if writing a report or cancel the research.
|
||||
""",
|
||||
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,
|
||||
total_questions: int,
|
||||
) -> AnalysisDecision:
|
||||
analyze_prompt = """
|
||||
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.
|
||||
|
||||
Here are the context:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
<Conversation context>
|
||||
{conversation_context}
|
||||
</Conversation context>
|
||||
|
||||
{enhanced_prompt}
|
||||
|
||||
Now, provide your decision in the required format for this user request:
|
||||
<User request>
|
||||
{user_request}
|
||||
</User request>
|
||||
"""
|
||||
# Manually craft the prompt to avoid LLM hallucination
|
||||
enhanced_prompt = ""
|
||||
if total_questions == 0:
|
||||
# Avoid writing a report without any research context
|
||||
enhanced_prompt = """
|
||||
|
||||
The student has no questions to research. Let start by asking some questions.
|
||||
"""
|
||||
elif total_questions > 6:
|
||||
# Avoid asking too many questions (when the data is not ready for writing a report)
|
||||
enhanced_prompt = f"""
|
||||
|
||||
The student has researched {total_questions} questions. Should cancel the research if the context is not enough to write a report.
|
||||
"""
|
||||
|
||||
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=PromptTemplate(template=analyze_prompt),
|
||||
user_request=user_request,
|
||||
context_str=context_str,
|
||||
conversation_context=conversation_context,
|
||||
enhanced_prompt=enhanced_prompt,
|
||||
)
|
||||
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,330 @@
|
||||
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
|
||||
async def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent:
|
||||
"""
|
||||
Initiate the workflow: memory, tools, agent
|
||||
"""
|
||||
await ctx.set("total_questions", 0)
|
||||
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()
|
||||
|
||||
@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",
|
||||
},
|
||||
)
|
||||
)
|
||||
total_questions = await ctx.get("total_questions")
|
||||
res = await plan_research(
|
||||
memory=self.memory,
|
||||
context_nodes=self.context_nodes,
|
||||
user_request=self.user_request,
|
||||
total_questions=total_questions,
|
||||
)
|
||||
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":
|
||||
# Writing a report without any research context is not allowed.
|
||||
# It's a LLM hallucination.
|
||||
if total_questions == 0:
|
||||
ctx.write_event_to_stream(
|
||||
DataEvent(
|
||||
type="deep_research_event",
|
||||
data={
|
||||
"event": "analyze",
|
||||
"state": "done",
|
||||
},
|
||||
)
|
||||
)
|
||||
return StopEvent(
|
||||
result="Sorry, I have a problem when analyzing the retrieved information. Please try again.",
|
||||
)
|
||||
|
||||
self.memory.put(
|
||||
message=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="No more idea to analyze. We should report the answers.",
|
||||
)
|
||||
)
|
||||
ctx.send_event(ReportEvent())
|
||||
else:
|
||||
total_questions += len(res.research_questions)
|
||||
await ctx.set("total_questions", total_questions) # For tracking
|
||||
await ctx.set(
|
||||
"waiting_questions", len(res.research_questions)
|
||||
) # For waiting questions to be answered
|
||||
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("waiting_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("waiting_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"]
|
||||
+297
@@ -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,,
|
||||
|
@@ -0,0 +1,95 @@
|
||||
import { ChatMessage } from "llamaindex";
|
||||
import { getTool } from "../engine/tools";
|
||||
import { FunctionCallingAgent } from "./single-agent";
|
||||
import { getQueryEngineTool } from "./tools";
|
||||
|
||||
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",
|
||||
tools: tools,
|
||||
systemPrompt: `You are a researcher agent. You are given a research task.
|
||||
|
||||
If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
|
||||
Otherwise, you must use tools to retrieve information or images needed for the task.
|
||||
|
||||
It's normal for the task to include some ambiguity. You must always think carefully about the context of the user's request to understand what are the main content needs to be retrieved.
|
||||
Example:
|
||||
Request: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
->Though: The main content is "history of the internet", while "write in English and publish in PDF format" is a requirement for other agents.
|
||||
Your task: Look for information in English about the history of the Internet.
|
||||
This is not your task: Create a blog post or look for how to create a PDF.
|
||||
|
||||
Next request: "Publish the blog post in HTML format."
|
||||
->Though: User just asking for a format change, the previous content is still valid.
|
||||
Your task: Return the previous content of the post to the writer. No need to do any research.
|
||||
This is not your task: Look for how to create an HTML file.
|
||||
|
||||
If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
|
||||
If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history.
|
||||
`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createWriter = (chatHistory: ChatMessage[]) => {
|
||||
return new FunctionCallingAgent({
|
||||
name: "writer",
|
||||
systemPrompt: `You are an expert in writing blog posts.
|
||||
You are given the task of writing a blog post based on research content provided by the researcher agent. Do not invent any information yourself.
|
||||
It's important to read the entire conversation history to write the blog post accurately.
|
||||
If you receive a review from the reviewer, update the post according to the feedback and return the new post content.
|
||||
If the content is not valid (e.g., broken link, broken image, etc.), do not use it.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to write the post correctly.
|
||||
If you update the post based on the reviewer's feedback, first explain what changes you made to the post, then provide the new post content. Do not include the reviewer's comments.
|
||||
Example:
|
||||
Task: "Here is the information I found about the history of the internet:
|
||||
Create a blog post about the history of the internet, write in English, and publish in PDF format."
|
||||
-> Your task: Use the research content {...} to write a blog post in English.
|
||||
-> This is not your task: Create a PDF
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createReviewer = (chatHistory: ChatMessage[]) => {
|
||||
return new FunctionCallingAgent({
|
||||
name: "reviewer",
|
||||
systemPrompt: `You are an expert in reviewing blog posts.
|
||||
You are given a task to review a blog post. As a reviewer, it's important that your review aligns with the user's request. Please focus on the user's request when reviewing the post.
|
||||
Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement.
|
||||
Furthermore, proofread the post for grammar and spelling errors.
|
||||
Only if the post is good enough for publishing should you return 'The post is good.' In all other cases, return your review.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to review the post correctly.
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
|
||||
Example:
|
||||
Task: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
-> Your task: Review whether the main content of the post is about the history of the internet and if it is written in English.
|
||||
-> This is not your task: Create blog post, create PDF, write in English.`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createPublisher = async (chatHistory: ChatMessage[]) => {
|
||||
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 (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: 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,,
|
||||
|
@@ -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,29 +0,0 @@
|
||||
import os
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.agent import AgentRunner
|
||||
from llama_index.core.tools.query_engine import QueryEngineTool
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.index import get_index
|
||||
|
||||
|
||||
def get_chat_engine():
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
top_k = os.getenv("TOP_K", "3")
|
||||
tools = []
|
||||
|
||||
# Add query tool if index exists
|
||||
index = get_index()
|
||||
if index is not None:
|
||||
query_engine = index.as_query_engine(similarity_top_k=int(top_k))
|
||||
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
|
||||
tools.append(query_engine_tool)
|
||||
|
||||
# Add additional tools
|
||||
tools += ToolFactory.from_env()
|
||||
|
||||
return AgentRunner.from_llm(
|
||||
llm=Settings.llm,
|
||||
tools=tools,
|
||||
system_prompt=system_prompt,
|
||||
verbose=True,
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from llama_index.core.agent import AgentRunner
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import BaseTool
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.tools.query_engine import get_query_engine_tool
|
||||
|
||||
|
||||
def get_chat_engine(params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
tools: List[BaseTool] = []
|
||||
callback_manager = CallbackManager(handlers=event_handlers or [])
|
||||
|
||||
# Add query tool if index exists
|
||||
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is not None:
|
||||
query_engine_tool = get_query_engine_tool(index, **kwargs)
|
||||
tools.append(query_engine_tool)
|
||||
|
||||
# Add additional tools
|
||||
configured_tools: List[BaseTool] = ToolFactory.from_env()
|
||||
tools.extend(configured_tools)
|
||||
|
||||
return AgentRunner.from_llm(
|
||||
llm=Settings.llm,
|
||||
tools=tools,
|
||||
system_prompt=system_prompt,
|
||||
callback_manager=callback_manager,
|
||||
verbose=True,
|
||||
)
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import yaml
|
||||
import importlib
|
||||
import os
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
import yaml # type: ignore
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
|
||||
|
||||
class ToolType:
|
||||
@@ -12,14 +13,13 @@ class ToolType:
|
||||
|
||||
|
||||
class ToolFactory:
|
||||
|
||||
TOOL_SOURCE_PACKAGE_MAP = {
|
||||
ToolType.LLAMAHUB: "llama_index.tools",
|
||||
ToolType.LOCAL: "app.engine.tools",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def load_tools(tool_type: str, tool_name: str, config: dict) -> list[FunctionTool]:
|
||||
def load_tools(tool_type: str, tool_name: str, config: dict) -> List[FunctionTool]:
|
||||
source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type]
|
||||
try:
|
||||
if "ToolSpec" in tool_name:
|
||||
@@ -31,7 +31,7 @@ class ToolFactory:
|
||||
return tool_spec.to_tool_list()
|
||||
else:
|
||||
module = importlib.import_module(f"{source_package}.{tool_name}")
|
||||
tools = getattr(module, "tools")
|
||||
tools = module.get_tools(**config)
|
||||
if not all(isinstance(tool, FunctionTool) for tool in tools):
|
||||
raise ValueError(
|
||||
f"The module {module} does not contain valid tools"
|
||||
@@ -43,14 +43,36 @@ class ToolFactory:
|
||||
raise ValueError(f"Failed to load tool {tool_name}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> list[FunctionTool]:
|
||||
tools = []
|
||||
def from_env(
|
||||
map_result: bool = False,
|
||||
) -> Union[Dict[str, List[FunctionTool]], List[FunctionTool]]:
|
||||
"""
|
||||
Load tools from the configured file.
|
||||
|
||||
Args:
|
||||
map_result: If True, return a map of tool names to their corresponding tools.
|
||||
|
||||
Returns:
|
||||
A dictionary of tool names to lists of FunctionTools if map_result is True,
|
||||
otherwise a list of FunctionTools.
|
||||
"""
|
||||
tools: Union[Dict[str, FunctionTool], List[FunctionTool]] = (
|
||||
{} if map_result else []
|
||||
)
|
||||
|
||||
if os.path.exists("config/tools.yaml"):
|
||||
with open("config/tools.yaml", "r") as f:
|
||||
tool_configs = yaml.safe_load(f)
|
||||
for tool_type, config_entries in tool_configs.items():
|
||||
for tool_name, config in config_entries.items():
|
||||
tools.extend(
|
||||
ToolFactory.load_tools(tool_type, tool_name, config)
|
||||
loaded_tools = ToolFactory.load_tools(
|
||||
tool_type, tool_name, config
|
||||
)
|
||||
if map_result:
|
||||
tools.update( # type: ignore
|
||||
{tool.metadata.name: tool for tool in loaded_tools}
|
||||
)
|
||||
else:
|
||||
tools.extend(loaded_tools) # type: ignore
|
||||
|
||||
return tools
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from llama_index.core.base.llms.types import ChatMessage
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Prompt based on https://github.com/e2b-dev/ai-artifacts
|
||||
CODE_GENERATION_PROMPT = """You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:
|
||||
|
||||
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
|
||||
|
||||
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: nextjs@14.2.5, typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
|
||||
|
||||
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, nuxt@3.13.0, tailwindcss. Port: 3000.
|
||||
|
||||
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
|
||||
|
||||
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
|
||||
|
||||
Make sure to use the correct syntax for the programming language you're using.
|
||||
"""
|
||||
|
||||
|
||||
class CodeArtifact(BaseModel):
|
||||
commentary: str = Field(
|
||||
...,
|
||||
description="Describe what you're about to do and the steps you want to take for generating the artifact in great detail.",
|
||||
)
|
||||
template: str = Field(
|
||||
..., description="Name of the template used to generate the artifact."
|
||||
)
|
||||
title: str = Field(..., description="Short title of the artifact. Max 3 words.")
|
||||
description: str = Field(
|
||||
..., description="Short description of the artifact. Max 1 sentence."
|
||||
)
|
||||
additional_dependencies: List[str] = Field(
|
||||
...,
|
||||
description="Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.",
|
||||
)
|
||||
has_additional_dependencies: bool = Field(
|
||||
...,
|
||||
description="Detect if additional dependencies that are not included in the template are required by the artifact.",
|
||||
)
|
||||
install_dependencies_command: str = Field(
|
||||
...,
|
||||
description="Command to install additional dependencies required by the artifact.",
|
||||
)
|
||||
port: Optional[int] = Field(
|
||||
...,
|
||||
description="Port number used by the resulted artifact. Null when no ports are exposed.",
|
||||
)
|
||||
file_path: str = Field(
|
||||
..., description="Relative path to the file, including the file name."
|
||||
)
|
||||
code: str = Field(
|
||||
...,
|
||||
description="Code generated by the artifact. Only runnable code is allowed.",
|
||||
)
|
||||
|
||||
|
||||
class CodeGeneratorTool:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def artifact(
|
||||
self,
|
||||
query: str,
|
||||
sandbox_files: Optional[List[str]] = None,
|
||||
old_code: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""Generate a code artifact based on the provided input.
|
||||
|
||||
Args:
|
||||
query (str): A description of the application you want to build.
|
||||
sandbox_files (Optional[List[str]], optional): A list of sandbox file paths. Defaults to None. Include these files if the code requires them.
|
||||
old_code (Optional[str], optional): The existing code to be modified. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Dict: A dictionary containing information about the generated artifact.
|
||||
"""
|
||||
|
||||
if old_code:
|
||||
user_message = f"{query}\n\nThe existing code is: \n```\n{old_code}\n```"
|
||||
else:
|
||||
user_message = query
|
||||
if sandbox_files:
|
||||
user_message += f"\n\nThe provided files are: \n{str(sandbox_files)}"
|
||||
|
||||
messages: List[ChatMessage] = [
|
||||
ChatMessage(role="system", content=CODE_GENERATION_PROMPT),
|
||||
ChatMessage(role="user", content=user_message),
|
||||
]
|
||||
try:
|
||||
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact) # type: ignore
|
||||
response = sllm.chat(messages)
|
||||
data: CodeArtifact = response.raw
|
||||
data_dict = data.model_dump()
|
||||
if sandbox_files:
|
||||
data_dict["files"] = sandbox_files
|
||||
return data_dict
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate artifact: {str(e)}")
|
||||
raise e
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(fn=CodeGeneratorTool().artifact)]
|
||||
@@ -0,0 +1,229 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
OUTPUT_DIR = "output/tools"
|
||||
|
||||
|
||||
class DocumentType(Enum):
|
||||
PDF = "pdf"
|
||||
HTML = "html"
|
||||
|
||||
|
||||
COMMON_STYLES = """
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
pre {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
HTML_SPECIFIC_STYLES = """
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
"""
|
||||
|
||||
PDF_SPECIFIC_STYLES = """
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 2cm;
|
||||
}
|
||||
body {
|
||||
font-size: 11pt;
|
||||
}
|
||||
h1 { font-size: 18pt; }
|
||||
h2 { font-size: 16pt; }
|
||||
h3 { font-size: 14pt; }
|
||||
h4, h5, h6 { font-size: 12pt; }
|
||||
pre, code {
|
||||
font-family: Courier, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
"""
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
{common_styles}
|
||||
{specific_styles}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class DocumentGenerator:
|
||||
@classmethod
|
||||
def _generate_html_content(cls, original_content: str) -> str:
|
||||
"""
|
||||
Generate HTML content from the original markdown content.
|
||||
"""
|
||||
try:
|
||||
import markdown # type: ignore
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Failed to import required modules. Please install markdown."
|
||||
)
|
||||
|
||||
# Convert markdown to HTML with fenced code and table extensions
|
||||
html_content = markdown.markdown(
|
||||
original_content, extensions=["fenced_code", "tables"]
|
||||
)
|
||||
return html_content
|
||||
|
||||
@classmethod
|
||||
def _generate_pdf(cls, html_content: str) -> BytesIO:
|
||||
"""
|
||||
Generate a PDF from the HTML content.
|
||||
"""
|
||||
try:
|
||||
from xhtml2pdf import pisa
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Failed to import required modules. Please install xhtml2pdf."
|
||||
)
|
||||
|
||||
pdf_html = HTML_TEMPLATE.format(
|
||||
common_styles=COMMON_STYLES,
|
||||
specific_styles=PDF_SPECIFIC_STYLES,
|
||||
content=html_content,
|
||||
)
|
||||
|
||||
buffer = BytesIO()
|
||||
pdf = pisa.pisaDocument(
|
||||
BytesIO(pdf_html.encode("UTF-8")), buffer, encoding="UTF-8"
|
||||
)
|
||||
|
||||
if pdf.err:
|
||||
logging.error(f"PDF generation failed: {pdf.err}")
|
||||
raise ValueError("PDF generation failed")
|
||||
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
@classmethod
|
||||
def _generate_html(cls, html_content: str) -> str:
|
||||
"""
|
||||
Generate a complete HTML document with the given HTML content.
|
||||
"""
|
||||
return HTML_TEMPLATE.format(
|
||||
common_styles=COMMON_STYLES,
|
||||
specific_styles=HTML_SPECIFIC_STYLES,
|
||||
content=html_content,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_document(
|
||||
cls, original_content: str, document_type: str, file_name: str
|
||||
) -> str:
|
||||
"""
|
||||
To generate document as PDF or HTML file.
|
||||
Parameters:
|
||||
original_content: str (markdown style)
|
||||
document_type: str (pdf or html) specify the type of the file format based on the use case
|
||||
file_name: str (name of the document file) must be a valid file name, no extensions needed
|
||||
Returns:
|
||||
str (URL to the document file): A file URL ready to serve.
|
||||
"""
|
||||
try:
|
||||
document_type = DocumentType(document_type.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid document type: {document_type}. Must be 'pdf' or 'html'."
|
||||
)
|
||||
# Always generate html content first
|
||||
html_content = cls._generate_html_content(original_content)
|
||||
|
||||
# Based on the type of document, generate the corresponding file
|
||||
if document_type == DocumentType.PDF:
|
||||
content = cls._generate_pdf(html_content)
|
||||
file_extension = "pdf"
|
||||
elif document_type == DocumentType.HTML:
|
||||
content = BytesIO(cls._generate_html(html_content).encode("utf-8"))
|
||||
file_extension = "html"
|
||||
else:
|
||||
raise ValueError(f"Unexpected document type: {document_type}")
|
||||
|
||||
file_name = cls._validate_file_name(file_name)
|
||||
file_path = os.path.join(OUTPUT_DIR, f"{file_name}.{file_extension}")
|
||||
|
||||
cls._write_to_file(content, file_path)
|
||||
|
||||
file_url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}"
|
||||
return file_url
|
||||
|
||||
@staticmethod
|
||||
def _write_to_file(content: BytesIO, file_path: str):
|
||||
"""
|
||||
Write the content to a file.
|
||||
"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(content.getvalue())
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def _validate_file_name(file_name: str) -> str:
|
||||
"""
|
||||
Validate the file name.
|
||||
"""
|
||||
# Don't allow directory traversal
|
||||
if os.path.isabs(file_name):
|
||||
raise ValueError("File name is not allowed.")
|
||||
# Don't allow special characters
|
||||
if re.match(r"^[a-zA-Z0-9_.-]+$", file_name):
|
||||
return file_name
|
||||
else:
|
||||
raise ValueError("File name is not allowed to contain special characters.")
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(DocumentGenerator.generate_document)]
|
||||
@@ -0,0 +1,70 @@
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
|
||||
def duckduckgo_search(
|
||||
query: str,
|
||||
region: str = "wt-wt",
|
||||
max_results: int = 10,
|
||||
):
|
||||
"""
|
||||
Use this function to search for any query in DuckDuckGo.
|
||||
Args:
|
||||
query (str): The query to search in DuckDuckGo.
|
||||
region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
|
||||
max_results Optional(int): The maximum number of results to be returned. Default is 10.
|
||||
"""
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"duckduckgo_search package is required to use this function."
|
||||
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
|
||||
)
|
||||
|
||||
results = []
|
||||
with DDGS() as ddg:
|
||||
results = list(
|
||||
ddg.text(
|
||||
keywords=query,
|
||||
region=region,
|
||||
max_results=max_results,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def duckduckgo_image_search(
|
||||
query: str,
|
||||
region: str = "wt-wt",
|
||||
max_results: int = 10,
|
||||
):
|
||||
"""
|
||||
Use this function to search for images in DuckDuckGo.
|
||||
Args:
|
||||
query (str): The query to search in DuckDuckGo.
|
||||
region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
|
||||
max_results Optional(int): The maximum number of results to be returned. Default is 10.
|
||||
"""
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"duckduckgo_search package is required to use this function."
|
||||
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
|
||||
)
|
||||
with DDGS() as ddg:
|
||||
results = list(
|
||||
ddg.images(
|
||||
keywords=query,
|
||||
region=region,
|
||||
max_results=max_results,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [
|
||||
FunctionTool.from_defaults(duckduckgo_search),
|
||||
FunctionTool.from_defaults(duckduckgo_image_search),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
@@ -0,0 +1,109 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import requests # type: ignore
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageGeneratorToolOutput(BaseModel):
|
||||
is_success: bool = Field(
|
||||
...,
|
||||
description="Whether the image generation was successful.",
|
||||
)
|
||||
image_url: Optional[str] = Field(
|
||||
None,
|
||||
description="The URL of the generated image.",
|
||||
)
|
||||
error_message: Optional[str] = Field(
|
||||
None,
|
||||
description="The error message if the image generation failed.",
|
||||
)
|
||||
|
||||
|
||||
class ImageGeneratorTool:
|
||||
_IMG_OUTPUT_FORMAT = "webp"
|
||||
_IMG_OUTPUT_DIR = "output/tools"
|
||||
_IMG_GEN_API = "https://api.stability.ai/v2beta/stable-image/generate/core"
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
if not api_key:
|
||||
api_key = os.getenv("STABILITY_API_KEY")
|
||||
self._api_key = api_key
|
||||
self.fileserver_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if self._api_key is None:
|
||||
raise ValueError(
|
||||
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys"
|
||||
)
|
||||
if self.fileserver_url_prefix is None:
|
||||
raise ValueError("FILESERVER_URL_PREFIX is required.")
|
||||
|
||||
def _prepare_output_dir(self):
|
||||
"""
|
||||
Create the output directory if it doesn't exist
|
||||
"""
|
||||
if not os.path.exists(self._IMG_OUTPUT_DIR):
|
||||
os.makedirs(self._IMG_OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
def _save_image(self, image_data: bytes):
|
||||
self._prepare_output_dir()
|
||||
filename = f"{uuid.uuid4()}.{self._IMG_OUTPUT_FORMAT}"
|
||||
output_path = os.path.join(self._IMG_OUTPUT_DIR, filename)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{self._IMG_OUTPUT_DIR}/{filename}"
|
||||
logger.info(f"Saved image to {output_path}.\nURL: {url}")
|
||||
return url
|
||||
|
||||
def _call_stability_api(self, prompt: str):
|
||||
headers = {
|
||||
"authorization": f"Bearer {self._api_key}",
|
||||
"accept": "image/*",
|
||||
}
|
||||
data = {
|
||||
"prompt": prompt,
|
||||
"output_format": self._IMG_OUTPUT_FORMAT,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
self._IMG_GEN_API,
|
||||
headers=headers,
|
||||
files={"none": ""},
|
||||
data=data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def generate_image(self, prompt: str) -> ImageGeneratorToolOutput:
|
||||
"""
|
||||
Use this tool to generate an image based on the prompt.
|
||||
Args:
|
||||
prompt (str): The prompt to generate the image from.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Call the Stability API
|
||||
response = self._call_stability_api(prompt)
|
||||
|
||||
# Save the image and get the URL
|
||||
image_url = self._save_image(response.content)
|
||||
|
||||
return ImageGeneratorToolOutput(
|
||||
is_success=True,
|
||||
image_url=image_url,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e, exc_info=True)
|
||||
return ImageGeneratorToolOutput(
|
||||
is_success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(ImageGeneratorTool(**kwargs).generate_image)]
|
||||
@@ -1,66 +1,90 @@
|
||||
import os
|
||||
import logging
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Tuple, Dict
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from e2b_code_interpreter import CodeInterpreter
|
||||
from typing import List, Optional
|
||||
|
||||
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):
|
||||
type: str
|
||||
filename: str
|
||||
url: str
|
||||
content: Optional[str] = None
|
||||
filename: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
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"
|
||||
|
||||
output_dir = "tool-output"
|
||||
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")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"E2B_API_KEY key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key"
|
||||
)
|
||||
if not filesever_url_prefix:
|
||||
raise ValueError(
|
||||
"FILESERVER_URL_PREFIX is required to display file output from sandbox"
|
||||
)
|
||||
|
||||
def __init__(self, api_key: str, filesever_url_prefix: str):
|
||||
self.api_key = api_key
|
||||
self.filesever_url_prefix = filesever_url_prefix
|
||||
self.interpreter = None
|
||||
self.api_key = api_key
|
||||
|
||||
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 __del__(self):
|
||||
"""
|
||||
Kill the interpreter when the tool is no longer in use
|
||||
"""
|
||||
if self.interpreter is not None:
|
||||
self.interpreter.kill()
|
||||
|
||||
def save_to_disk(self, base64_data: str, ext: str) -> Dict:
|
||||
filename = f"{uuid.uuid4()}.{ext}" # generate a unique 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) -> 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)
|
||||
@@ -72,63 +96,107 @@ class E2BCodeInterpreter:
|
||||
|
||||
try:
|
||||
formats = result.formats()
|
||||
base64_data_arr = [result[format] for format in formats]
|
||||
results = [result[format] for format in formats]
|
||||
|
||||
for ext, base64_data in zip(formats, base64_data_arr):
|
||||
if ext and base64_data:
|
||||
result = self.save_to_disk(base64_data, ext)
|
||||
filename = result["filename"]
|
||||
output.append(
|
||||
InterpreterExtraResult(
|
||||
type=ext, filename=filename, url=self.get_file_url(filename)
|
||||
for ext, data in zip(formats, results):
|
||||
match ext:
|
||||
case "png" | "svg" | "jpeg" | "pdf":
|
||||
document_file = self._save_to_disk(data, ext)
|
||||
output.append(
|
||||
InterpreterExtraResult(
|
||||
type=ext,
|
||||
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,
|
||||
content=data,
|
||||
)
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("Error when saving data to disk", error)
|
||||
logger.exception(error, exc_info=True)
|
||||
logger.error("Error when parsing output from E2b interpreter tool", error)
|
||||
|
||||
return output
|
||||
|
||||
def interpret(self, code: str) -> E2BToolOutput:
|
||||
with CodeInterpreter(api_key=self.api_key) as interpreter:
|
||||
logger.info(
|
||||
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
|
||||
def interpret(
|
||||
self,
|
||||
code: str,
|
||||
sandbox_files: List[str] = [],
|
||||
retry_count: int = 0,
|
||||
) -> E2BToolOutput:
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
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,
|
||||
)
|
||||
exec = interpreter.notebook.exec_cell(code)
|
||||
|
||||
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:
|
||||
output = E2BToolOutput(is_error=True, logs=[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:
|
||||
if len(exec.results) == 0:
|
||||
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
|
||||
else:
|
||||
results = self.parse_result(exec.results[0])
|
||||
results = self._parse_result(exec.results[0])
|
||||
output = E2BToolOutput(
|
||||
is_error=False, logs=exec.logs, results=results
|
||||
is_error=False,
|
||||
logs=exec.logs,
|
||||
results=results,
|
||||
retry_count=retry_count + 1,
|
||||
)
|
||||
return output
|
||||
else:
|
||||
raise ValueError("Interpreter is not initialized.")
|
||||
|
||||
|
||||
def code_interpret(code: str) -> Dict:
|
||||
"""
|
||||
Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.
|
||||
"""
|
||||
api_key = os.getenv("E2B_API_KEY")
|
||||
filesever_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"E2B_API_KEY key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key"
|
||||
)
|
||||
if not filesever_url_prefix:
|
||||
raise ValueError(
|
||||
"FILESERVER_URL_PREFIX is required to display file output from sandbox"
|
||||
)
|
||||
|
||||
interpreter = E2BCodeInterpreter(
|
||||
api_key=api_key, filesever_url_prefix=filesever_url_prefix
|
||||
)
|
||||
output = interpreter.interpret(code)
|
||||
return output.dict()
|
||||
|
||||
|
||||
# Specify as functions tools to be loaded by the ToolFactory
|
||||
tools = [FunctionTool.from_defaults(code_interpret)]
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(E2BCodeInterpreter(**kwargs).interpret)]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from llama_index.tools.openapi import OpenAPIToolSpec
|
||||
from llama_index.tools.requests import RequestsToolSpec
|
||||
|
||||
|
||||
class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec):
|
||||
"""
|
||||
A combination of OpenAPI and Requests tool specs that can parse OpenAPI specs and make requests.
|
||||
|
||||
openapi_uri: str: The file path or URL to the OpenAPI spec.
|
||||
domain_headers: dict: Whitelist domains and the headers to use.
|
||||
"""
|
||||
|
||||
spec_functions = OpenAPIToolSpec.spec_functions + RequestsToolSpec.spec_functions
|
||||
# Cached parsed specs by URI
|
||||
_specs: Dict[str, Tuple[Dict, List[str]]] = {}
|
||||
|
||||
def __init__(self, openapi_uri: str, domain_headers: dict = None, **kwargs):
|
||||
if domain_headers is None:
|
||||
domain_headers = {}
|
||||
if openapi_uri not in self._specs:
|
||||
openapi_spec, servers = self._load_openapi_spec(openapi_uri)
|
||||
self._specs[openapi_uri] = (openapi_spec, servers)
|
||||
else:
|
||||
openapi_spec, servers = self._specs[openapi_uri]
|
||||
|
||||
# Add the servers to the domain headers if they are not already present
|
||||
for server in servers:
|
||||
if server not in domain_headers:
|
||||
domain_headers[server] = {}
|
||||
|
||||
OpenAPIToolSpec.__init__(self, spec=openapi_spec)
|
||||
RequestsToolSpec.__init__(self, domain_headers)
|
||||
|
||||
@staticmethod
|
||||
def _load_openapi_spec(uri: str) -> Tuple[Dict, List[str]]:
|
||||
"""
|
||||
Load an OpenAPI spec from a URI.
|
||||
|
||||
Args:
|
||||
uri (str): A file path or URL to the OpenAPI spec.
|
||||
|
||||
Returns:
|
||||
List[Document]: A list of Document objects.
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml # type: ignore
|
||||
|
||||
if uri.startswith("http"):
|
||||
import requests # type: ignore
|
||||
|
||||
response = requests.get(uri)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: "
|
||||
f"Failed to load OpenAPI spec from {uri}, status code: {response.status_code}"
|
||||
)
|
||||
spec = yaml.safe_load(response.text)
|
||||
elif uri.startswith("file"):
|
||||
filepath = urlparse(uri).path
|
||||
with open(filepath, "r") as file:
|
||||
spec = yaml.safe_load(file)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI URI provided. "
|
||||
"Only HTTP and file path are supported."
|
||||
)
|
||||
# Add the servers to the whitelist
|
||||
try:
|
||||
servers = [
|
||||
urlparse(server["url"]).netloc for server in spec.get("servers", [])
|
||||
]
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI spec provided. "
|
||||
"Could not get `servers` from the spec."
|
||||
) from e
|
||||
return spec, servers
|
||||
@@ -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__)
|
||||
@@ -69,4 +70,5 @@ class OpenMeteoWeather:
|
||||
return response.json()
|
||||
|
||||
|
||||
tools = [FunctionTool.from_defaults(OpenMeteoWeather.get_weather_information)]
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(OpenMeteoWeather.get_weather_information)]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.node_postprocessors import NodeCitationProcessor
|
||||
from fastapi import HTTPException
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from llama_index.core.chat_engine import CondensePlusContextChatEngine
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
|
||||
def get_chat_engine(params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
llm = Settings.llm
|
||||
memory = ChatMemoryBuffer.from_defaults(
|
||||
token_limit=llm.metadata.context_window - 256
|
||||
)
|
||||
callback_manager = CallbackManager(handlers=event_handlers or [])
|
||||
|
||||
node_postprocessors = []
|
||||
if citation_prompt:
|
||||
node_postprocessors = [NodeCitationProcessor()]
|
||||
system_prompt = f"{system_prompt}\n{citation_prompt}"
|
||||
|
||||
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(
|
||||
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
|
||||
),
|
||||
)
|
||||
if top_k != 0 and kwargs.get("similarity_top_k") is None:
|
||||
kwargs["similarity_top_k"] = top_k
|
||||
retriever = index.as_retriever(**kwargs)
|
||||
|
||||
return CondensePlusContextChatEngine(
|
||||
llm=llm,
|
||||
memory=memory,
|
||||
system_prompt=system_prompt,
|
||||
retriever=retriever,
|
||||
node_postprocessors=node_postprocessors, # type: ignore
|
||||
callback_manager=callback_manager,
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from llama_index.core import QueryBundle
|
||||
from llama_index.core.postprocessor.types import BaseNodePostprocessor
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
|
||||
|
||||
class NodeCitationProcessor(BaseNodePostprocessor):
|
||||
"""
|
||||
Append node_id into metadata for citation purpose.
|
||||
Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
|
||||
"""
|
||||
|
||||
def _postprocess_nodes(
|
||||
self,
|
||||
nodes: List[NodeWithScore],
|
||||
query_bundle: Optional[QueryBundle] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
for node_score in nodes:
|
||||
node_score.node.metadata["node_id"] = node_score.node.node_id
|
||||
return nodes
|
||||
@@ -1,26 +1,18 @@
|
||||
import { 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 { STORAGE_CACHE_DIR } from "./shared";
|
||||
import { createTools } from "./tools";
|
||||
import { createQueryEngineTool } from "./tools/query-engine";
|
||||
|
||||
export async function createChatEngine() {
|
||||
export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
|
||||
// Add a query engine tool if we have a data source
|
||||
// Delete this code if you don't have a data source
|
||||
const index = await getDataSource();
|
||||
const index = await getDataSource(params);
|
||||
if (index) {
|
||||
tools.push(
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine(),
|
||||
metadata: {
|
||||
name: "data_query_engine",
|
||||
description: `A query engine for documents in storage folder: ${STORAGE_CACHE_DIR}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
tools.push(createQueryEngineTool(index, { documentIds }));
|
||||
}
|
||||
|
||||
const configFile = path.join("config", "tools.json");
|
||||
@@ -35,8 +27,10 @@ export async function createChatEngine() {
|
||||
tools.push(...(await createTools(toolConfig)));
|
||||
}
|
||||
|
||||
return new OpenAIAgent({
|
||||
const agent = new LLMAgent({
|
||||
tools,
|
||||
systemPrompt: process.env.SYSTEM_PROMPT,
|
||||
});
|
||||
}) as unknown as BaseChatEngine;
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import {
|
||||
BaseTool,
|
||||
ChatMessage,
|
||||
JSONValue,
|
||||
Settings,
|
||||
ToolMetadata,
|
||||
} from "llamaindex";
|
||||
|
||||
// prompt based on https://github.com/e2b-dev/ai-artifacts
|
||||
const CODE_GENERATION_PROMPT = `You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:\n
|
||||
|
||||
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
|
||||
|
||||
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: nextjs@14.2.5, typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
|
||||
|
||||
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, nuxt@3.13.0, tailwindcss. Port: 3000.
|
||||
|
||||
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
|
||||
|
||||
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
|
||||
|
||||
Provide detail information about the artifact you're about to generate in the following JSON format with the following keys:
|
||||
|
||||
commentary: Describe what you're about to do and the steps you want to take for generating the artifact in great detail.
|
||||
template: Name of the template used to generate the artifact.
|
||||
title: Short title of the artifact. Max 3 words.
|
||||
description: Short description of the artifact. Max 1 sentence.
|
||||
additional_dependencies: Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.
|
||||
has_additional_dependencies: Detect if additional dependencies that are not included in the template are required by the artifact.
|
||||
install_dependencies_command: Command to install additional dependencies required by the artifact.
|
||||
port: Port number used by the resulted artifact. Null when no ports are exposed.
|
||||
file_path: Relative path to the file, including the file name.
|
||||
code: Code generated by the artifact. Only runnable code is allowed.
|
||||
|
||||
Make sure to use the correct syntax for the programming language you're using. Make sure to generate only one code file. If you need to use CSS, make sure to include the CSS in the code file using Tailwind CSS syntax.
|
||||
`;
|
||||
|
||||
// detail information to execute code
|
||||
export type CodeArtifact = {
|
||||
commentary: string;
|
||||
template: string;
|
||||
title: string;
|
||||
description: string;
|
||||
additional_dependencies: string[];
|
||||
has_additional_dependencies: boolean;
|
||||
install_dependencies_command: string;
|
||||
port: number | null;
|
||||
file_path: string;
|
||||
code: string;
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
export type CodeGeneratorParameter = {
|
||||
requirement: string;
|
||||
oldCode?: string;
|
||||
sandboxFiles?: string[];
|
||||
};
|
||||
|
||||
export type CodeGeneratorToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
|
||||
{
|
||||
name: "artifact",
|
||||
description: `Generate a code artifact based on the input. Don't call this tool if the user has not asked for code generation. E.g. if the user asks to write a description or specification, don't call this tool.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
requirement: {
|
||||
type: "string",
|
||||
description: "The description of the application you want to build.",
|
||||
},
|
||||
oldCode: {
|
||||
type: "string",
|
||||
description: "The existing code to be modified",
|
||||
nullable: true,
|
||||
},
|
||||
sandboxFiles: {
|
||||
type: "array",
|
||||
description:
|
||||
"A list of sandbox file paths. Include these files if the code requires them.",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["requirement"],
|
||||
},
|
||||
};
|
||||
|
||||
export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
|
||||
|
||||
constructor(params?: CodeGeneratorToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async call(input: CodeGeneratorParameter) {
|
||||
try {
|
||||
const artifact = await this.generateArtifact(
|
||||
input.requirement,
|
||||
input.oldCode,
|
||||
input.sandboxFiles, // help the generated code use exact files
|
||||
);
|
||||
if (input.sandboxFiles) {
|
||||
artifact.files = input.sandboxFiles;
|
||||
}
|
||||
return artifact as JSONValue;
|
||||
} catch (error) {
|
||||
return { isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Generate artifact (code, environment, dependencies, etc.)
|
||||
async generateArtifact(
|
||||
query: string,
|
||||
oldCode?: string,
|
||||
attachments?: string[],
|
||||
): Promise<CodeArtifact> {
|
||||
const userMessage = `
|
||||
${query}
|
||||
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
|
||||
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
|
||||
`;
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: CODE_GENERATION_PROMPT },
|
||||
{ role: "user", content: userMessage },
|
||||
];
|
||||
try {
|
||||
const response = await Settings.llm.chat({ messages });
|
||||
const content = response.message.content.toString();
|
||||
const jsonContent = content
|
||||
.replace(/^```json\s*|\s*```$/g, "")
|
||||
.replace(/^`+|`+$/g, "")
|
||||
.trim();
|
||||
const artifact = JSON.parse(jsonContent) as CodeArtifact;
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
console.log("Failed to generate artifact", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
import { marked } from "marked";
|
||||
import path from "node:path";
|
||||
import { saveDocument } from "../../llamaindex/documents/helper";
|
||||
|
||||
const OUTPUT_DIR = "output/tools";
|
||||
|
||||
type DocumentParameter = {
|
||||
originalContent: string;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
const DEFAULT_METADATA: ToolMetadata<JSONSchemaType<DocumentParameter>> = {
|
||||
name: "document_generator",
|
||||
description:
|
||||
"Generate HTML document from markdown content. Return a file url to the document",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
originalContent: {
|
||||
type: "string",
|
||||
description: "The original markdown content to convert.",
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description: "The name of the document file (without extension).",
|
||||
},
|
||||
},
|
||||
required: ["originalContent", "fileName"],
|
||||
},
|
||||
};
|
||||
|
||||
const COMMON_STYLES = `
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
pre {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML_SPECIFIC_STYLES = `
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML_TEMPLATE = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
${COMMON_STYLES}
|
||||
${HTML_SPECIFIC_STYLES}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export interface DocumentGeneratorParams {
|
||||
metadata?: ToolMetadata<JSONSchemaType<DocumentParameter>>;
|
||||
}
|
||||
|
||||
export class DocumentGenerator implements BaseTool<DocumentParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<DocumentParameter>>;
|
||||
|
||||
constructor(params: DocumentGeneratorParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_METADATA;
|
||||
}
|
||||
|
||||
private static async generateHtmlContent(
|
||||
originalContent: string,
|
||||
): Promise<string> {
|
||||
return await marked(originalContent);
|
||||
}
|
||||
|
||||
private static generateHtmlDocument(htmlContent: string): string {
|
||||
return HTML_TEMPLATE.replace("{{content}}", htmlContent);
|
||||
}
|
||||
|
||||
async call(input: DocumentParameter): Promise<string> {
|
||||
const { originalContent, fileName } = input;
|
||||
|
||||
const htmlContent =
|
||||
await DocumentGenerator.generateHtmlContent(originalContent);
|
||||
const fileContent = DocumentGenerator.generateHtmlDocument(htmlContent);
|
||||
|
||||
const filePath = path.join(OUTPUT_DIR, `${fileName}.html`);
|
||||
|
||||
return `URL: ${await saveDocument(filePath, fileContent)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTools(): BaseTool[] {
|
||||
return [new DocumentGenerator({})];
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import { search } from "duck-duck-scrape";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
|
||||
export type DuckDuckGoParameter = {
|
||||
query: string;
|
||||
region?: string;
|
||||
maxResults?: number;
|
||||
};
|
||||
|
||||
export type DuckDuckGoToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_SEARCH_METADATA: ToolMetadata<
|
||||
JSONSchemaType<DuckDuckGoParameter>
|
||||
> = {
|
||||
name: "duckduckgo_search",
|
||||
description:
|
||||
"Use this function to search for information (only text) in the internet using DuckDuckGo.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "The query to search in DuckDuckGo.",
|
||||
},
|
||||
region: {
|
||||
type: "string",
|
||||
description:
|
||||
"Optional, The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...",
|
||||
nullable: true,
|
||||
},
|
||||
maxResults: {
|
||||
type: "number",
|
||||
description:
|
||||
"Optional, The maximum number of results to be returned. Default is 10.",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
type DuckDuckGoSearchResult = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
|
||||
|
||||
constructor(params: DuckDuckGoToolParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_SEARCH_METADATA;
|
||||
}
|
||||
|
||||
async call(input: DuckDuckGoParameter) {
|
||||
const { query, region, maxResults = 10 } = input;
|
||||
const options = region ? { region } : {};
|
||||
// Temporarily sleep to reduce overloading the DuckDuckGo
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const searchResults = await search(query, options);
|
||||
|
||||
return searchResults.results.slice(0, maxResults).map((result) => {
|
||||
return {
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
url: result.url,
|
||||
} as DuckDuckGoSearchResult;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getTools() {
|
||||
return [new DuckDuckGoSearchTool({})];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user