mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-02 19:14:28 -04:00
Compare commits
580 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b618e91e99 | |||
| 91ce4e1236 | |||
| 2b85420370 | |||
| 52cc37f206 | |||
| 952b5b4908 | |||
| e8004fd711 | |||
| 48f6d849e6 | |||
| 02a9db3d40 | |||
| 8fa8c3bad8 | |||
| a221bc60f7 | |||
| 6c0fb51557 | |||
| 3589f946a9 | |||
| e2486eb080 | |||
| 66b81e5323 | |||
| 924649c025 | |||
| 1b04db917b | |||
| af9ad3c42d | |||
| 1ff6eaf3e1 | |||
| a543a27faf | |||
| 63edd74ba1 | |||
| 13a967b2a2 | |||
| 2ac4d92493 | |||
| 7e47cba4ba | |||
| bc56fa3c5f | |||
| 087c96164d | |||
| 3ff0a18876 | |||
| df1047480a | |||
| 8d89223a08 | |||
| 49a944182f | |||
| 058b3762c1 | |||
| 4c8579b04f | |||
| bb1e82cdae | |||
| f682a1c36e | |||
| b8a1ff6412 | |||
| 5fe9e17d3f | |||
| 15619d81a6 | |||
| 76742da78a | |||
| 693d7a0ea5 | |||
| 8d59ef0a6b | |||
| c62f26e31c | |||
| d3f73679b4 | |||
| 91c35cff33 | |||
| 82ac925224 | |||
| f24ee8e6f9 | |||
| 3acec88fbc | |||
| eee3230e99 | |||
| d8425e5290 | |||
| 0bc5a0d882 | |||
| bbae802bed | |||
| 25fba4381b | |||
| d0618fa2fa | |||
| f3fe3ffc9b | |||
| 6f75d4ab6e | |||
| 3242738fe4 | |||
| 17538eb0dd | |||
| d3772cb4a2 | |||
| 527075c086 | |||
| fb7d4da149 | |||
| 5c35b194bb | |||
| 85e5e7e662 | |||
| 58362542c0 | |||
| 6f44185f68 | |||
| afe9e9fc16 | |||
| 1b5a519f13 | |||
| f072308d03 | |||
| 1df8cfbdc2 | |||
| 24515393a6 | |||
| b3eb0ba7d4 | |||
| 556f33c0ab | |||
| 7a70390b00 | |||
| ad5912b41f | |||
| 76502d28e7 | |||
| f4ca602da5 | |||
| d304554f33 | |||
| 8dce9f913d | |||
| c62096c516 | |||
| 0384268543 | |||
| d9f9e3c1c3 | |||
| 1357c423a3 | |||
| 8105aa70b6 | |||
| 23a90625d1 | |||
| ac789bcb8d | |||
| 241d82a87d | |||
| b16cfd873b | |||
| 3130cdf18d | |||
| 7711216134 | |||
| 93d601972e | |||
| 8fe5fc24c1 | |||
| 3960618454 | |||
| 53e1cd56e7 | |||
| 0a2e12a2bb | |||
| 2e536dca36 | |||
| 4bc53ac24e | |||
| 2deb63a6cc | |||
| 2ffa057f77 | |||
| 64f151dd66 | |||
| 765181adb0 | |||
| 95c35e8a5c | |||
| 598865768a | |||
| 05453d55bf | |||
| d363ced4d8 | |||
| 293c6f97c1 | |||
| 44b4d89ac1 | |||
| 60f10c5b5d | |||
| ee85320701 | |||
| b12dc6f1e8 | |||
| 7c3b279417 | |||
| 1514a555d5 | |||
| cddb4f6bcc | |||
| c82e4f5791 | |||
| 1f7e0e3c69 | |||
| 7997cdeb70 | |||
| 76ec3605e5 | |||
| 5cfdec7d75 | |||
| 3d1b15d515 | |||
| 392393af9e | |||
| 920beda8ad | |||
| e6f8add778 | |||
| c9f8f8d5f2 | |||
| 24eb7736ee | |||
| 5fb27220f7 | |||
| 5caa3813f8 | |||
| bc95789a8d | |||
| 08b3e079e4 | |||
| 1876950f89 | |||
| c7349b44c4 | |||
| 4068618b2d | |||
| 54c9e2f95e | |||
| aec1173b71 | |||
| 481663dd63 | |||
| 1ca7dd2e48 | |||
| 3d20990713 | |||
| 8fb69cf807 | |||
| 61af56dac6 | |||
| 4b66039a96 | |||
| ee88f681a6 | |||
| 992c3a95e9 | |||
| 2a4fb702d1 | |||
| 24b9337096 | |||
| fceec69a3a | |||
| 03e5e0a16e | |||
| fe3cd36d3a | |||
| d5d10e9ead | |||
| 5ed925d75f | |||
| ca5df14d41 | |||
| ee69ce7cc1 | |||
| 0e4ecfaf8b | |||
| 3658fec684 | |||
| c3d275abe1 | |||
| 61204a1381 | |||
| 9e723c3a15 | |||
| d5da55b993 | |||
| c1552ebb00 | |||
| 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 | |||
| 05748bdf10 | |||
| d60b3c5a96 | |||
| c3e9ed3df4 | |||
| 48188ca3f9 | |||
| 1fde1dc585 | |||
| cd50a33d43 | |||
| ed114856d9 | |||
| 69c2e16c82 | |||
| f5da6623cf | |||
| 0950cb90f2 | |||
| bb53425b4b | |||
| bbd5b8ddd6 | |||
| 260d37a3f1 | |||
| 7873bfb030 | |||
| 0c7c41ee3b | |||
| 56537a1473 | |||
| d8dfc29edd | |||
| 84db798353 | |||
| 67a062af14 | |||
| 0bc8e75c64 | |||
| 6bd5e7b77a | |||
| 38bc1d1350 | |||
| cb1001de95 | |||
| 78776ac51e | |||
| 416073db1d | |||
| 84929de8b2 | |||
| 6fe240b854 | |||
| 8bb1024d0f | |||
| 988bfc2a60 | |||
| 056e376ee0 | |||
| 819cccb11a | |||
| 8a5ece10c2 | |||
| 63bb0505d6 | |||
| 2e80ef47ee | |||
| a1feb524e9 | |||
| 06823da849 | |||
| 7bd3ed551f | |||
| c981eb1423 | |||
| c094b0c6bf | |||
| e2567ffc03 | |||
| 5d8d752b16 | |||
| a0b04be23c | |||
| 94a2809ecd | |||
| e29ef92564 | |||
| 6bdd4ac69d | |||
| 1ad25451a6 | |||
| cfb5257a1e | |||
| 046ff06157 | |||
| 8b81b17984 | |||
| f1c3e8df69 | |||
| 089916a148 | |||
| 3bb94da804 | |||
| 418bf9ba8a | |||
| e5d20b66f6 | |||
| ae7b30106d | |||
| 5fb64b74ca | |||
| e4665b6c0d | |||
| 5463d3bf4b | |||
| 7225e916fd | |||
| 897feb9914 | |||
| 66b5f38eda | |||
| 1c3d0b19ec | |||
| a0dec80d88 | |||
| 1be69a5aa4 | |||
| 6acccd2b04 | |||
| 753229dfae | |||
| 9efcffe112 | |||
| 0cf6386e17 | |||
| a6e76e1cf3 | |||
| 2aaf99e256 | |||
| 1d78202ef4 | |||
| ee2cc66c3b | |||
| 83e7b65e25 | |||
| 58388b06b4 | |||
| 8201e2f5c2 | |||
| 0742d3bc45 | |||
| 625ed4d6f5 | |||
| 5512a9e454 | |||
| 29b17ee328 | |||
| c06d4af7b6 | |||
| 27397143db | |||
| 665c26cc5d | |||
| 922e0ceb9b | |||
| ae1536768a | |||
| 78ded9e242 | |||
| 4f10840f04 | |||
| 9ca343c34e | |||
| ce2f24d73f | |||
| 65bc28d1f2 | |||
| 45e2eccb11 | |||
| b38adf894d | |||
| dd6f84fdd2 | |||
| 99e758fcbb | |||
| b0720ccd8c | |||
| 459824dbec | |||
| b08cad0123 | |||
| c7a978e0aa | |||
| 76aa33612c | |||
| e8db041d58 | |||
| b3f26856c4 | |||
| 26ab74bd9a | |||
| 17afc91850 | |||
| cbc996fb30 | |||
| bf1430d5b6 | |||
| e9a6cd049a | |||
| 60ed8fe080 | |||
| 56faee0b44 | |||
| abb7488895 | |||
| e851c0c834 | |||
| 58d9c0c400 | |||
| be1259a9f0 | |||
| 3af6328400 | |||
| 39aaafcb6c | |||
| 9ac6f27ece | |||
| 9f6a0efec9 | |||
| cdc10bc3ab | |||
| 9ab2bdd64f | |||
| 079a3f7f42 | |||
| cf7806b604 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": true,
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add fetching llm and embedding models from server
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add Milvus vector database
|
||||
@@ -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.
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"max-params": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"prefer-const": "error",
|
||||
},
|
||||
}
|
||||
+130
-28
@@ -1,65 +1,167 @@
|
||||
name: E2E Tests
|
||||
name: E2E Tests for create-llama package
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "python/llama-index-server/**"
|
||||
- ".github/workflows/*llama_index_server.yml"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.6.1"
|
||||
paths-ignore:
|
||||
- "python/llama-index-server/**"
|
||||
- ".github/workflows/*llama_index_server.yml"
|
||||
|
||||
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"]
|
||||
vectordbs: ["none", "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@v4
|
||||
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@v2
|
||||
|
||||
- name: Install uv
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
- name: Add uv to PATH # Ensure uv is available in subsequent steps
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
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: .
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Build create-llama
|
||||
run: pnpm run build
|
||||
working-directory: .
|
||||
- name: Pack
|
||||
run: pnpm pack --pack-destination ./output
|
||||
working-directory: .
|
||||
- name: Extract Pack
|
||||
run: tar -xvzf ./output/*.tgz -C ./output
|
||||
working-directory: .
|
||||
- name: Run Playwright tests
|
||||
run: pnpm exec playwright test
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Install
|
||||
run: pnpm run pack-install
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Build and store server package
|
||||
run: |
|
||||
pnpm run build
|
||||
wheel_file=$(ls dist/*.whl | head -n 1)
|
||||
mkdir -p "${{ runner.temp }}"
|
||||
cp "$wheel_file" "${{ runner.temp }}/"
|
||||
echo "SERVER_PACKAGE_PATH=${{ runner.temp }}/$(basename "$wheel_file")" >> $GITHUB_ENV
|
||||
working-directory: python/llama-index-server
|
||||
|
||||
- name: Run Playwright tests for Python
|
||||
run: pnpm run e2e:python
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: .
|
||||
- uses: actions/upload-artifact@v3
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
VECTORDB: ${{ matrix.vectordbs }}
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONLEGACYWINDOWSSTDIO: utf-8
|
||||
SERVER_PACKAGE_PATH: ${{ env.SERVER_PACKAGE_PATH }}
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: ./playwright-report/
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
e2e-typescript:
|
||||
name: typescript
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [22]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs"]
|
||||
vectordbs: ["none", "llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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: packages/create-llama
|
||||
|
||||
- name: Build create-llama
|
||||
run: pnpm run build
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Install
|
||||
run: pnpm run pack-install
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Build server
|
||||
run: pnpm run build
|
||||
working-directory: packages/server
|
||||
|
||||
- name: Pack @llamaindex/server package
|
||||
run: |
|
||||
pnpm pack --pack-destination "${{ runner.temp }}"
|
||||
if [ "${{ runner.os }}" == "Windows" ]; then
|
||||
file=$(find "${{ runner.temp }}" -name "llamaindex-server-*.tgz" | head -n 1)
|
||||
mv "$file" "${{ runner.temp }}/llamaindex-server.tgz"
|
||||
else
|
||||
mv ${{ runner.temp }}/llamaindex-server-*.tgz ${{ runner.temp }}/llamaindex-server.tgz
|
||||
fi
|
||||
working-directory: packages/server
|
||||
|
||||
- name: Run Playwright tests for TypeScript
|
||||
run: |
|
||||
pnpm run e2e:ts
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
VECTORDB: ${{ matrix.vectordbs }}
|
||||
SERVER_PACKAGE_PATH: ${{ runner.temp }}/llamaindex-server.tgz
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs}}-node${{ matrix.node-version }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
@@ -13,17 +13,49 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
version: latest
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Run Prettier
|
||||
run: pnpm run format
|
||||
|
||||
- name: Run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run Typecheck for examples
|
||||
run: pnpm run typecheck
|
||||
working-directory: packages/server/examples
|
||||
|
||||
- name: Run Python format check
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
args: "format --check"
|
||||
src: "python/llama-index-server"
|
||||
|
||||
- name: Run Python lint
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
args: "check"
|
||||
src: "python/llama-index-server"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish to GitHub Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build tarball
|
||||
run: |
|
||||
pnpm pack
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "create-llama-*.tgz"
|
||||
name: Release ${{ github.ref }}
|
||||
bodyFile: "CHANGELOG.md"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Add auth token to .npmrc file
|
||||
run: |
|
||||
cat << EOF >> ".npmrc"
|
||||
//registry.npmjs.org/:_authToken=$NPM_TOKEN
|
||||
EOF
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Get changeset status
|
||||
id: get-changeset-status
|
||||
run: |
|
||||
pnpm changeset status --output .changeset/status.json
|
||||
new_version=$(jq -r '.releases[0].newVersion' < .changeset/status.json)
|
||||
rm -v .changeset/status.json
|
||||
echo "new-version=${new_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: Release ${{ steps.get-changeset-status.outputs.new-version }}
|
||||
title: Release ${{ steps.get-changeset-status.outputs.new-version }}
|
||||
# bump versions
|
||||
version: pnpm new-version
|
||||
# build package and call changeset publish
|
||||
publish: pnpm release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
@@ -0,0 +1,136 @@
|
||||
name: Build Package
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.9"
|
||||
UI_TEST: "true"
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python/llama-index-server
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ["3.9"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install && pnpm build
|
||||
|
||||
- name: Run unit tests
|
||||
shell: bash
|
||||
run: uv run pytest tests
|
||||
|
||||
type-check:
|
||||
name: Type Check
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python/llama-index-server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run mypy
|
||||
shell: bash
|
||||
run: uv run mypy llama_index
|
||||
|
||||
build:
|
||||
needs: [unit-test, type-check]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python/llama-index-server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install && pnpm build
|
||||
|
||||
- name: Build package
|
||||
shell: bash
|
||||
run: uv build
|
||||
|
||||
- name: Get the absolute wheel file path and save it to the output
|
||||
shell: bash
|
||||
id: get_whl_path
|
||||
run: |
|
||||
WHL_FILE=$(readlink -f dist/*.whl)
|
||||
echo "whl_file=$WHL_FILE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Test import
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
|
||||
run: |
|
||||
uv run --with $WHL_FILE python -c "from llama_index.server import LlamaIndexServer"
|
||||
|
||||
- name: Check frontend resources is present
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
|
||||
run: |
|
||||
uv run --with $WHL_FILE python -c "from llama_index.server.chat_ui import check_ui_resources; check_ui_resources()"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: llama-index-server
|
||||
path: dist/
|
||||
+4
-12
@@ -6,9 +6,6 @@ node_modules
|
||||
.pnpm-store
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
@@ -34,14 +31,9 @@ yarn-error.log*
|
||||
dist/
|
||||
lib/
|
||||
|
||||
# e2e
|
||||
.cache
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
playwright/.cache/
|
||||
.tsbuildinfo
|
||||
e2e/cache
|
||||
|
||||
# intellij
|
||||
**/.idea
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
!.vscode/settings.json
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
pnpm format
|
||||
pnpm lint
|
||||
uvx ruff check .
|
||||
uvx ruff format . --check
|
||||
|
||||
+15
-3
@@ -1,6 +1,18 @@
|
||||
apps/docs/i18n
|
||||
apps/docs/docs/api
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
lib/
|
||||
dist/
|
||||
.docusaurus/
|
||||
cache/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
packages/server/server/
|
||||
packages/server/project/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
# Python
|
||||
python/
|
||||
**/*.mypy_cache/**
|
||||
**/*.venv/**
|
||||
**/*.ruff_cache/**
|
||||
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
# create-llama
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- edd24c2: Add observability with openllmetry
|
||||
- 403fc6f: Minor bug fixes to improve DX (missing .env value and updated error messages)
|
||||
- 0f79757: Ability to download community submodules
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 89a49f4: Add more config variables to .env file
|
||||
- fdf48dd: Add "Start in VSCode" option to postInstallAction
|
||||
- fdf48dd: Add devcontainers to generated code
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2d29350: Add LlamaParse option when selecting a pdf file or a folder (FastAPI only)
|
||||
- b354f23: Add embedding model option to create-llama (FastAPI only)
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 09d532e: feat: generate llama pack project from llama index
|
||||
- cfdd6db: feat: add pinecone support to create llama
|
||||
- ef25d69: upgrade llama-index package to version v0.10.7 for create-llama app
|
||||
- 50dfd7b: update fastapi for CVE-2024-24762
|
||||
|
||||
## 0.0.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d06a85b: Add option to create an agent by selecting tools (Google, Wikipedia)
|
||||
- 7b7329b: Added latest turbo models for GPT-3.5 and GPT 4
|
||||
|
||||
## 0.0.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ba95ca3: Use condense plus context chat engine for FastAPI as default
|
||||
|
||||
## 0.0.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c680af6: Fixed issues with locating templates path
|
||||
|
||||
## 0.0.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6dd401e: Add an option to provide an URL and chat with the website data (FastAPI only)
|
||||
- e9b87ef: Select a folder as data source and support more file types (.pdf, .doc, .docx, .xls, .xlsx, .csv)
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 27d55fd: Add an option to provide an URL and chat with the website data
|
||||
|
||||
## 0.0.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3a29a80: Add node_modules to gitignore in Express backends
|
||||
- fe03aaa: feat: generate llama pack example
|
||||
|
||||
## 0.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 88d3b41: fix packaging
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fa17f7e: Add an option that allows the user to run the generated app
|
||||
- 9e5d8e1: Add an option to select a local PDF file as data source
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a73942d: Fix: Bundle mongo dependency with NextJS
|
||||
- 9492cc6: Feat: Added option to automatically install dependencies (for Python and TS)
|
||||
- f74dea5: Feat: Show images in chat messages using GPT4 Vision (Express and NextJS only)
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8e124e5: feat: support showing image on chat message
|
||||
|
||||
## 0.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2e6b36e: fix: re-organize file structure
|
||||
- 2b356c8: fix: relative path incorrect
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Added PostgreSQL vector store (for Typescript and Python)
|
||||
- Improved async handling in FastAPI
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9c5e22a: Added cross-env so frontends with Express/FastAPI backends are working under Windows
|
||||
- 5ab65eb: Bring Python templates with TS templates to feature parity
|
||||
- 9c5e22a: Added vector DB selector to create-llama (starting with MongoDB support)
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2aeb341: - Added option to create a new project based on community templates
|
||||
- Added OpenAI model selector for NextJS projects
|
||||
- Added GPT4 Vision support (and file upload)
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Bugfixes (thanks @marcusschiesser)
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- acfe232: Deployment fixes (thanks @seldo)
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8cdb07f: Fix Next deployment (thanks @seldo and @marcusschiesser)
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9f9f293: Added more to README and made it easier to switch models (thanks @seldo)
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4431ec7: Label bug fix (thanks @marcusschiesser)
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25257f4: Fix issue where it doesn't find OpenAI Key when running npm run generate (#182) (thanks @RayFernando1337)
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 031e926: Update create-llama readme (thanks @logan-markewich)
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 91b42a3: change version (thanks @marcusschiesser)
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e2a6805: Hello Create Llama (thanks @marcusschiesser)
|
||||
@@ -0,0 +1,201 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Repository Overview
|
||||
|
||||
Create-llama is a monorepo containing CLI tools and server frameworks for building LlamaIndex-powered applications. The repository combines TypeScript/Node.js and Python components in a unified development environment.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
- **`packages/create-llama/`**: Main CLI tool for scaffolding LlamaIndex applications
|
||||
- **`packages/server/`**: TypeScript/Next.js server framework (`@llamaindex/server`)
|
||||
- **`python/llama-index-server/`**: Python/FastAPI server framework
|
||||
- **Root**: Workspace configuration and shared development tools
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **Package Manager**: pnpm with workspace configuration
|
||||
- **Build Tools**: bunchee (TypeScript), Next.js, hatchling (Python)
|
||||
- **Testing**: Playwright for e2e, pytest for Python
|
||||
- **Version Management**: changesets for TypeScript packages, manual for Python
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Root Level (Monorepo)
|
||||
|
||||
```bash
|
||||
pnpm dev # Start all packages in development mode
|
||||
pnpm build # Build all packages
|
||||
pnpm lint # ESLint across TypeScript packages
|
||||
pnpm format # Prettier formatting
|
||||
pnpm e2e # Run end-to-end tests
|
||||
```
|
||||
|
||||
### Create-llama Package
|
||||
|
||||
```bash
|
||||
cd packages/create-llama
|
||||
npm run build # Build CLI using bash script and ncc
|
||||
npm run dev # Watch mode development
|
||||
npm run e2e # Playwright tests for generated projects
|
||||
npm run clean # Clean build artifacts and template caches
|
||||
```
|
||||
|
||||
### TypeScript Server Package
|
||||
|
||||
```bash
|
||||
cd packages/server
|
||||
pnpm dev # Watch mode with bunchee
|
||||
pnpm build # Multi-step build: ESM/CJS + Next.js + static assets
|
||||
pnpm clean # Clean all build outputs
|
||||
```
|
||||
|
||||
### Python Server Package
|
||||
|
||||
```bash
|
||||
cd python/llama-index-server
|
||||
uv run generate # Index data files
|
||||
fastapi dev # Start development server with hot reload
|
||||
pytest # Run test suite
|
||||
```
|
||||
|
||||
## Template System
|
||||
|
||||
The CLI uses a sophisticated template system in `packages/create-llama/templates/`:
|
||||
|
||||
### Organization
|
||||
|
||||
- **`types/`**: Base project structures (streaming, reflex, llamaindexserver)
|
||||
- **`components/`**: Reusable components across frameworks
|
||||
- `engines/` - Chat and agent engines
|
||||
- `loaders/` - File, web, database loaders
|
||||
- `providers/` - AI model configurations
|
||||
- `vectordbs/` - Vector database integrations
|
||||
- `use-cases/` - Workflow implementations
|
||||
|
||||
### Development Workflow
|
||||
|
||||
- Templates support multiple frameworks (Next.js, Express, FastAPI)
|
||||
- Component system allows mix-and-match functionality
|
||||
- E2E tests validate generated projects work correctly
|
||||
|
||||
## Server Framework Architecture
|
||||
|
||||
### TypeScript Server (`@llamaindex/server`)
|
||||
|
||||
- **Core**: `LlamaIndexServer` class wrapping Next.js with workflow support
|
||||
- **Frontend**: React-based chat UI with shadcn/ui components
|
||||
- **API**: `/api/chat` endpoint with streaming responses
|
||||
- **Build Process**: Complex multi-step build including static assets for Python integration
|
||||
|
||||
### Python Server (`llama-index-server`)
|
||||
|
||||
- **Core**: `LlamaIndexServer` class extending FastAPI
|
||||
- **Architecture**: Workflow factory pattern for stateless request handling
|
||||
- **UI Generation**: AI-powered React component generation from Pydantic schemas
|
||||
- **Development**: Hot reloading support with dev mode
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Workflow Integration
|
||||
|
||||
Both server frameworks use factory patterns:
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
const server = new LlamaIndexServer({
|
||||
workflow: (context) => createWorkflow(context)
|
||||
});
|
||||
|
||||
// Python
|
||||
def create_workflow(chat_request: ChatRequest) -> Workflow:
|
||||
return MyWorkflow(chat_request.messages)
|
||||
```
|
||||
|
||||
### Event System
|
||||
|
||||
Structured events for UI communication:
|
||||
|
||||
- **UIEvent**: Custom components with Pydantic/Zod schemas
|
||||
- **ArtifactEvent**: Code/documents for Canvas panel
|
||||
- **SourceNodesEvent**: Document sources with metadata
|
||||
- **AgentRunEvent**: Tool usage and progress tracking
|
||||
|
||||
### File Handling
|
||||
|
||||
- Both servers auto-mount `data/` and `output/` directories
|
||||
- LlamaCloud integration for remote file access
|
||||
- Static file serving through framework-specific methods
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### E2E Testing
|
||||
|
||||
- Playwright tests in `packages/create-llama/e2e/`
|
||||
- Tests both Python and TypeScript generated projects
|
||||
- Validates CLI generation and application functionality
|
||||
|
||||
### Unit Testing
|
||||
|
||||
- Python: pytest with comprehensive API and service tests
|
||||
- TypeScript: Integrated testing through build process
|
||||
|
||||
## Build Process
|
||||
|
||||
### Create-llama CLI
|
||||
|
||||
1. TypeScript compilation with bash script
|
||||
2. ncc bundling for standalone executable
|
||||
3. Template validation and caching
|
||||
|
||||
### Server Package Build
|
||||
|
||||
1. **prebuild**: Clean directories
|
||||
2. **build**: bunchee compilation to ESM/CJS
|
||||
3. **postbuild**: Next.js preparation and static asset generation
|
||||
4. **prepare:py-static**: Python integration assets
|
||||
|
||||
### Release Process
|
||||
|
||||
```bash
|
||||
pnpm release # Build all + publish npm packages + Python release
|
||||
```
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >=16.14.0
|
||||
- Python with uv package manager
|
||||
- pnpm for package management
|
||||
|
||||
### Common Workflow
|
||||
|
||||
1. Clone repository and run `pnpm install`
|
||||
2. For CLI development: work in `packages/create-llama/`
|
||||
3. For server development: choose TypeScript or Python package
|
||||
4. Use `pnpm dev` for concurrent development across packages
|
||||
5. Run `pnpm e2e` to validate changes with generated projects
|
||||
|
||||
## Special Considerations
|
||||
|
||||
### Template Development
|
||||
|
||||
- Changes to templates require rebuilding CLI
|
||||
- E2E tests validate template functionality across frameworks
|
||||
- Template caching system speeds up repeated builds
|
||||
|
||||
### Cross-package Dependencies
|
||||
|
||||
- Server package builds static assets for Python integration
|
||||
- Version synchronization between TypeScript and Python packages
|
||||
- Shared UI components and styling across implementations
|
||||
|
||||
### Performance
|
||||
|
||||
- CLI uses caching for template operations
|
||||
- Server frameworks support streaming responses
|
||||
- Background processing for file operations and LlamaCloud integration
|
||||
@@ -0,0 +1,73 @@
|
||||
# Contributing
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install NodeJS. Preferably v18 using nvm or n.
|
||||
|
||||
Inside the `create-llama` directory:
|
||||
|
||||
```
|
||||
npm i -g pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Note: we use pnpm in this repo, which has a lot of the same functionality and CLI options as npm but it does do some things better, like caching.
|
||||
|
||||
### Building
|
||||
|
||||
When we publish to NPM we will have a [ncc](https://github.com/vercel/ncc) compiled version of the tool. To run the build command, run
|
||||
|
||||
```
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Test cases
|
||||
|
||||
We are using a set of e2e tests to ensure that the tool works as expected.
|
||||
|
||||
We're using [playwright](https://playwright.dev/) to run the tests.
|
||||
To install it, call:
|
||||
|
||||
```
|
||||
pnpm exec playwright install --with-deps
|
||||
```
|
||||
|
||||
Then you can create a global `create-llama` command (used by the e2e tests) that is linked to your local dev environment (if you update the build, you don't need to re-link):
|
||||
|
||||
```
|
||||
pnpm link --global
|
||||
```
|
||||
|
||||
And then finally run the tests:
|
||||
|
||||
```
|
||||
pnpm run e2e
|
||||
```
|
||||
|
||||
To write new test cases write them in [e2e](/e2e)
|
||||
|
||||
## Changeset
|
||||
|
||||
We use [changesets](https://github.com/changesets/changesets) for managing versions and changelogs. To create a new changeset, run:
|
||||
|
||||
```
|
||||
pnpm changeset
|
||||
```
|
||||
|
||||
Please send a descriptive changeset for each PR.
|
||||
|
||||
## Publishing (maintainers only)
|
||||
|
||||
To publish a new version of the library, first create a new version:
|
||||
|
||||
```shell
|
||||
pnpm new-version
|
||||
```
|
||||
|
||||
If everything looks good, commit the generated files and release the new version:
|
||||
|
||||
```shell
|
||||
pnpm release
|
||||
git push # push to the main branch
|
||||
git push --tags
|
||||
```
|
||||
@@ -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,49 +24,46 @@ to start the development server. You can then visit [http://localhost:3000](http
|
||||
|
||||
## What you'll get
|
||||
|
||||
- A Next.js-powered front-end. The app is set up as a chat interface that can answer questions about your data (see below)
|
||||
- You can style it with HTML and CSS, or you can optionally use components from [shadcn/ui](https://ui.shadcn.com/)
|
||||
- Your choice of 3 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 a single endpoint that allows you to send the state of your chat and receive additional responses
|
||||
- You can choose whether you want a streaming or non-streaming back-end (if you're not sure, we recommend streaming)
|
||||
- You can choose whether you want to use `ContextChatEngine` or `SimpleChatEngine`
|
||||
- `SimpleChatEngine` will just talk to the LLM directly without using your data
|
||||
- `ContextChatEngine` will use your data to answer questions (see below).
|
||||
- A set of pre-configured use cases to get you started, e.g. Agentic RAG, Data Analysis, Report Generation, etc.
|
||||
- A front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
|
||||
- Your choice of two frameworks:
|
||||
- **Next.js**: if you select this option, 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 with [LlamaIndex Server for TS](https://npmjs.com/package/@llamaindex/server).
|
||||
- **Python FastAPI**: if you select this option, you’ll get full-stack Python application powered by the [llama-index Python package](https://pypi.org/project/llama-index/) and [LlamaIndex Server for Python](https://pypi.org/project/llama-index-server/)
|
||||
- The app uses OpenAI by default, so you'll need an OpenAI API key, or you can customize it to use any of the dozens of LLMs we support.
|
||||
|
||||
Here's how it looks like:
|
||||
|
||||
https://github.com/user-attachments/assets/d57af1a1-d99b-4e9c-98d9-4cbd1327eff8
|
||||
|
||||
## Using your data
|
||||
|
||||
If you've enabled `ContextChatEngine`, you can supply your own data and the app will index it and answer questions. Your generated app will have a folder called `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`.
|
||||
|
||||
- With the Next.js backend this is `./data`
|
||||
- With the Express or Python backend this is in `./backend/data`
|
||||
The app will ingest any supported files you put in this directory. Your Next.js apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
|
||||
|
||||
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:
|
||||
Before you can use your data, you need to index it. If you're using the Next.js apps, run:
|
||||
|
||||
```bash
|
||||
npm run generate
|
||||
```
|
||||
|
||||
Then re-start your app. Remember you'll need to re-run `generate` if you add new files to your `data` folder. If you're using the Python backend, you can trigger indexing of your data by deleting the `./storage` folder and re-starting the app.
|
||||
Then re-start your app. Remember you'll need to re-run `generate` if you add new files to your `data` folder.
|
||||
|
||||
## Don't want a front-end?
|
||||
If you're using the Python backend, you can trigger indexing of your data by calling:
|
||||
|
||||
It's optional! If you've selected the Python or Express back-ends, just delete the `frontend` folder and you'll get an API without any front-end code.
|
||||
```bash
|
||||
uv run generate
|
||||
```
|
||||
|
||||
## Customizing the LLM
|
||||
## Customizing the AI models
|
||||
|
||||
By default the app will use OpenAI's gpt-3.5-turbo model. If you want to use GPT-4, you can modify this by editing a file:
|
||||
The app will default to OpenAI's `gpt-4.1` LLM and `text-embedding-3-large` embedding model.
|
||||
|
||||
- In the Next.js backend, edit `./app/api/chat/route.ts` and replace `gpt-3.5-turbo` with `gpt-4`
|
||||
- In the Express backend, edit `./backend/src/controllers/chat.controller.ts` and likewise replace `gpt-3.5-turbo` with `gpt-4`
|
||||
- In the Python backend, edit `./backend/app/utils/index.py` and once again replace `gpt-3.5-turbo` with `gpt-4`
|
||||
If you want to use different models, add the `--ask-models` CLI parameter.
|
||||
|
||||
You can also replace OpenAI with one of our [dozens of other supported LLMs](https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules.html).
|
||||
You can also replace one of the default models with one of our [dozens of other supported LLMs](https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules.html).
|
||||
|
||||
To do so, you have to manually change the generated code (edit the `settings.ts` file for Typescript projects or the `settings.py` file for Python projects)
|
||||
|
||||
## Example
|
||||
|
||||
@@ -84,43 +87,31 @@ 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 with streaming
|
||||
✔ Which framework would you like to use? › NextJS
|
||||
✔ Which UI would you like to use? › Just HTML
|
||||
✔ Which chat engine would you like to use? › ContextChatEngine
|
||||
✔ Please provide your OpenAI API key (leave blank to skip): …
|
||||
✔ Would you like to use ESLint? … No / Yes
|
||||
Creating a new LlamaIndex app in /home/my-app.
|
||||
✔ What use case do you want to build? › Agentic RAG
|
||||
✔ What language do you want to use? › Python (FastAPI)
|
||||
✔ Do you want to use LlamaCloud services? … No / Yes
|
||||
✔ Please provide your LlamaCloud API key (leave blank to skip): …
|
||||
? 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)
|
||||
```
|
||||
|
||||
### Running non-interactively
|
||||
|
||||
You can also pass command line arguments to set up a new project
|
||||
non-interactively. See `create-llama --help`:
|
||||
|
||||
```bash
|
||||
create-llama <project-directory> [options]
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
|
||||
--use-npm
|
||||
|
||||
Explicitly tell the CLI to bootstrap the app using npm
|
||||
|
||||
--use-pnpm
|
||||
|
||||
Explicitly tell the CLI to bootstrap the app using pnpm
|
||||
|
||||
--use-yarn
|
||||
|
||||
Explicitly tell the CLI to bootstrap the app using Yarn
|
||||
|
||||
```
|
||||
non-interactively. For a list of the latest options, call `create-llama --help`.
|
||||
|
||||
## LlamaIndex Documentation
|
||||
|
||||
- [TS/JS docs](https://ts.llamaindex.ai/)
|
||||
- [Python docs](https://docs.llamaindex.ai/en/stable/)
|
||||
|
||||
## LlamaIndex Server
|
||||
|
||||
The generated code is using the LlamaIndex Server, which serves LlamaIndex Workflows and Agent Workflows via an API server. See the following docs for more information:
|
||||
|
||||
- [LlamaIndex Server For TypeScript](./packages/server/README.md)
|
||||
- [LlamaIndex Server For Python](./python/llama-index-server/README.md)
|
||||
|
||||
Inspired by and adapted from [create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
|
||||
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import path from "path";
|
||||
import { green, yellow } from "picocolors";
|
||||
import { tryGitInit } from "./helpers/git";
|
||||
import { isFolderEmpty } from "./helpers/is-folder-empty";
|
||||
import { getOnline } from "./helpers/is-online";
|
||||
import { isWriteable } from "./helpers/is-writeable";
|
||||
import { makeDir } from "./helpers/make-dir";
|
||||
|
||||
import fs from "fs";
|
||||
import terminalLink from "terminal-link";
|
||||
import type { InstallTemplateArgs } from "./helpers";
|
||||
import { installTemplate } from "./helpers";
|
||||
import { writeDevcontainer } from "./helpers/devcontainer";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { toolsRequireConfig } from "./helpers/tools";
|
||||
|
||||
export type InstallAppArgs = Omit<
|
||||
InstallTemplateArgs,
|
||||
"appName" | "root" | "isOnline" | "customApiPath"
|
||||
> & {
|
||||
appPath: string;
|
||||
frontend: boolean;
|
||||
};
|
||||
|
||||
export async function createApp({
|
||||
template,
|
||||
framework,
|
||||
engine,
|
||||
ui,
|
||||
appPath,
|
||||
packageManager,
|
||||
eslint,
|
||||
frontend,
|
||||
openAiKey,
|
||||
llamaCloudKey,
|
||||
model,
|
||||
embeddingModel,
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
observability,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
|
||||
if (!(await isWriteable(path.dirname(root)))) {
|
||||
console.error(
|
||||
"The application path is not writable, please check folder permissions and try again.",
|
||||
);
|
||||
console.error(
|
||||
"It is likely you do not have write permissions for this folder.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const appName = path.basename(root);
|
||||
|
||||
await makeDir(root);
|
||||
if (!isFolderEmpty(root, appName)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const useYarn = packageManager === "yarn";
|
||||
const isOnline = !useYarn || (await getOnline());
|
||||
|
||||
console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
|
||||
console.log();
|
||||
|
||||
const args = {
|
||||
appName,
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
engine,
|
||||
ui,
|
||||
packageManager,
|
||||
isOnline,
|
||||
eslint,
|
||||
openAiKey,
|
||||
llamaCloudKey,
|
||||
model,
|
||||
embeddingModel,
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
observability,
|
||||
};
|
||||
|
||||
if (frontend) {
|
||||
// install backend
|
||||
const backendRoot = path.join(root, "backend");
|
||||
await makeDir(backendRoot);
|
||||
await installTemplate({ ...args, root: backendRoot, backend: true });
|
||||
// install 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);
|
||||
|
||||
process.chdir(root);
|
||||
if (tryGitInit(root)) {
|
||||
console.log("Initialized a git repository.");
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (toolsRequireConfig(tools)) {
|
||||
console.log(
|
||||
yellow(
|
||||
`You have selected tools that require configuration. Please configure them in the ${terminalLink(
|
||||
"tools_config.json",
|
||||
`file://${root}/tools_config.json`,
|
||||
)} file.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log("");
|
||||
console.log(`${green("Success!")} Created ${appName} at ${appPath}`);
|
||||
|
||||
console.log(
|
||||
`Now have a look at the ${terminalLink(
|
||||
"README.md",
|
||||
`file://${root}/README.md`,
|
||||
)} and learn how to get started.`,
|
||||
);
|
||||
|
||||
if (args.observability === "opentelemetry") {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
@@ -1,145 +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 {
|
||||
TemplateEngine,
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateType,
|
||||
TemplateUI,
|
||||
} from "../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "./utils";
|
||||
|
||||
const templateTypes: TemplateType[] = ["streaming", "simple"];
|
||||
const templateFrameworks: TemplateFramework[] = [
|
||||
"nextjs",
|
||||
"express",
|
||||
"fastapi",
|
||||
];
|
||||
const templateEngines: TemplateEngine[] = ["simple", "context"];
|
||||
const templateUIs: TemplateUI[] = ["shadcn", "html"];
|
||||
const templatePostInstallActions: TemplatePostInstallAction[] = [
|
||||
"none",
|
||||
"runApp",
|
||||
];
|
||||
|
||||
for (const templateType of templateTypes) {
|
||||
for (const templateFramework of templateFrameworks) {
|
||||
for (const templateEngine of templateEngines) {
|
||||
for (const templateUI of templateUIs) {
|
||||
for (const templatePostInstallAction of templatePostInstallActions) {
|
||||
if (templateFramework === "nextjs" && templateType === "simple") {
|
||||
// nextjs doesn't support simple templates - skip tests
|
||||
continue;
|
||||
}
|
||||
const appType: AppType =
|
||||
templateFramework === "express" || templateFramework === "fastapi"
|
||||
? templateType === "simple"
|
||||
? "--no-frontend" // simple templates don't have frontends
|
||||
: "--frontend"
|
||||
: "";
|
||||
if (appType === "--no-frontend" && templateUI !== "html") {
|
||||
// if there's no frontend, don't iterate over UIs
|
||||
continue;
|
||||
}
|
||||
test.describe(`try create-llama ${templateType} ${templateFramework} ${templateEngine} ${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,
|
||||
templateEngine,
|
||||
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");
|
||||
test.skip(appType === "--no-frontend");
|
||||
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");
|
||||
test.skip(appType === "--no-frontend");
|
||||
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 should response when calling API", async ({
|
||||
request,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(appType !== "--no-frontend");
|
||||
const backendPort = appType === "" ? port : externalPort;
|
||||
const response = await request.post(
|
||||
`http://localhost:${backendPort}/api/chat`,
|
||||
{
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
import { ChildProcess, exec } from "child_process";
|
||||
import crypto from "node:crypto";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import * as path from "path";
|
||||
import waitPort from "wait-port";
|
||||
import {
|
||||
TemplateEngine,
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateType,
|
||||
TemplateUI,
|
||||
TemplateVectorDB,
|
||||
} from "../helpers";
|
||||
|
||||
export type AppType = "--frontend" | "--no-frontend" | "";
|
||||
const MODEL = "gpt-3.5-turbo";
|
||||
const EMBEDDING_MODEL = "text-embedding-ada-002";
|
||||
export type CreateLlamaResult = {
|
||||
projectName: string;
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export async function runCreateLlama(
|
||||
cwd: string,
|
||||
templateType: TemplateType,
|
||||
templateFramework: TemplateFramework,
|
||||
templateEngine: TemplateEngine,
|
||||
templateUI: TemplateUI,
|
||||
vectorDb: TemplateVectorDB,
|
||||
appType: AppType,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
postInstallAction: TemplatePostInstallAction,
|
||||
): Promise<CreateLlamaResult> {
|
||||
const createLlama = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"output",
|
||||
"package",
|
||||
"dist",
|
||||
"index.js",
|
||||
);
|
||||
|
||||
const name = [
|
||||
templateType,
|
||||
templateFramework,
|
||||
templateEngine,
|
||||
templateUI,
|
||||
appType,
|
||||
].join("-");
|
||||
const command = [
|
||||
"node",
|
||||
createLlama,
|
||||
name,
|
||||
"--template",
|
||||
templateType,
|
||||
"--framework",
|
||||
templateFramework,
|
||||
"--engine",
|
||||
templateEngine,
|
||||
"--ui",
|
||||
templateUI,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--model",
|
||||
MODEL,
|
||||
"--embedding-model",
|
||||
EMBEDDING_MODEL,
|
||||
"--open-ai-key",
|
||||
process.env.OPENAI_API_KEY || "testKey",
|
||||
appType,
|
||||
"--eslint",
|
||||
"--use-npm",
|
||||
"--port",
|
||||
port,
|
||||
"--external-port",
|
||||
externalPort,
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--tools",
|
||||
"none",
|
||||
"--no-llama-parse",
|
||||
"--observability",
|
||||
"none",
|
||||
].join(" ");
|
||||
console.log(`running command '${command}' in ${cwd}`);
|
||||
const appProcess = exec(command, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
});
|
||||
appProcess.stderr?.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
appProcess.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
throw new Error(`create-llama command was failed!`);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for app to start
|
||||
if (postInstallAction === "runApp") {
|
||||
await checkAppHasStarted(
|
||||
appType === "--frontend",
|
||||
templateFramework,
|
||||
port,
|
||||
externalPort,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
projectName: name,
|
||||
appProcess,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTestDir() {
|
||||
const cwd = path.join(__dirname, "cache", crypto.randomUUID());
|
||||
await mkdir(cwd, { recursive: true });
|
||||
return cwd;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import eslint from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/create-llama/**"],
|
||||
rules: {
|
||||
"max-params": ["error", 4],
|
||||
"prefer-const": "error",
|
||||
"no-empty": "off",
|
||||
"no-extra-boolean-cast": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/no-wrapper-object-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/server/**"],
|
||||
rules: {
|
||||
"no-irregular-whitespace": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"error",
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
"python/**",
|
||||
"**/*.mypy_cache/**",
|
||||
"**/*.venv/**",
|
||||
"**/*.ruff_cache/**",
|
||||
"**/dist/**",
|
||||
"**/e2e/cache/**",
|
||||
"**/lib/*",
|
||||
"**/.next/**",
|
||||
"**/out/**",
|
||||
"**/node_modules/**",
|
||||
"**/build/**",
|
||||
"packages/server/server/**",
|
||||
"packages/server/project/**",
|
||||
"packages/server/bin/**",
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
export const COMMUNITY_OWNER = "run-llama";
|
||||
export const COMMUNITY_REPO = "create_llama_projects";
|
||||
export const LLAMA_PACK_OWNER = "run-llama";
|
||||
export const LLAMA_PACK_REPO = "llama_index";
|
||||
export const LLAMA_PACK_FOLDER = "llama-index-packs";
|
||||
export const LLAMA_PACK_FOLDER_PATH = `${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/main/${LLAMA_PACK_FOLDER}`;
|
||||
@@ -1,61 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
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";
|
||||
}
|
||||
|
||||
// Modify containerEnv
|
||||
if (framework === "fastapi") {
|
||||
if (frontend) {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
|
||||
};
|
||||
} else {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(devcontainerJson, null, 2);
|
||||
}
|
||||
|
||||
export const writeDevcontainer = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
frontend: boolean,
|
||||
) => {
|
||||
console.log("Adding .devcontainer");
|
||||
const devcontainerContent = renderDevcontainerContent(
|
||||
templatesDir,
|
||||
framework,
|
||||
frontend,
|
||||
);
|
||||
const devcontainerDir = path.join(root, ".devcontainer");
|
||||
fs.mkdirSync(devcontainerDir);
|
||||
await fs.promises.writeFile(
|
||||
path.join(devcontainerDir, "devcontainer.json"),
|
||||
devcontainerContent,
|
||||
);
|
||||
};
|
||||
@@ -1,274 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import {
|
||||
FileSourceConfig,
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateVectorDB,
|
||||
WebSourceConfig,
|
||||
} from "./types";
|
||||
|
||||
type EnvVar = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const renderEnvVar = (envVars: EnvVar[]): string => {
|
||||
return envVars.reduce(
|
||||
(prev, env) =>
|
||||
prev +
|
||||
(env.description
|
||||
? `# ${env.description.replaceAll("\n", "\n# ")}\n`
|
||||
: "") +
|
||||
(env.name
|
||||
? env.value
|
||||
? `${env.name}=${env.value}\n\n`
|
||||
: `# ${env.name}=\n\n`
|
||||
: ""),
|
||||
"",
|
||||
);
|
||||
};
|
||||
|
||||
const getVectorDBEnvs = (vectorDb: TemplateVectorDB) => {
|
||||
switch (vectorDb) {
|
||||
case "mongo":
|
||||
return [
|
||||
{
|
||||
name: "MONGO_URI",
|
||||
description:
|
||||
"For generating a connection URI, see https://docs.timescale.com/use-timescale/latest/services/create-a-service\nThe MongoDB connection URI.",
|
||||
},
|
||||
{
|
||||
name: "MONGODB_DATABASE",
|
||||
},
|
||||
{
|
||||
name: "MONGODB_VECTORS",
|
||||
},
|
||||
{
|
||||
name: "MONGODB_VECTOR_INDEX",
|
||||
},
|
||||
];
|
||||
case "pg":
|
||||
return [
|
||||
{
|
||||
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.",
|
||||
},
|
||||
];
|
||||
|
||||
case "pinecone":
|
||||
return [
|
||||
{
|
||||
name: "PINECONE_API_KEY",
|
||||
description:
|
||||
"Configuration for Pinecone vector store\nThe Pinecone API key.",
|
||||
},
|
||||
{
|
||||
name: "PINECONE_ENVIRONMENT",
|
||||
},
|
||||
{
|
||||
name: "PINECONE_INDEX_NAME",
|
||||
},
|
||||
];
|
||||
case "milvus":
|
||||
return [
|
||||
{
|
||||
name: "MILVUS_ADDRESS",
|
||||
description:
|
||||
"The address of the Milvus server. Eg: http://localhost:19530",
|
||||
value: "http://localhost:19530",
|
||||
},
|
||||
{
|
||||
name: "MILVUS_COLLECTION",
|
||||
description:
|
||||
"The name of the Milvus collection to store the vectors.",
|
||||
value: "llamacollection",
|
||||
},
|
||||
{
|
||||
name: "MILVUS_USERNAME",
|
||||
description: "The username to access the Milvus server.",
|
||||
},
|
||||
{
|
||||
name: "MILVUS_PASSWORD",
|
||||
description: "The password to access the Milvus server.",
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getDataSourceEnvs = (dataSources: TemplateDataSource[]) => {
|
||||
const envs = [];
|
||||
for (const source of dataSources) {
|
||||
switch (source.type) {
|
||||
case "web":
|
||||
const config = source.config as WebSourceConfig;
|
||||
envs.push(
|
||||
{
|
||||
name: "BASE_URL",
|
||||
description: "The base URL to start web scraping.",
|
||||
value: config.baseUrl,
|
||||
},
|
||||
{
|
||||
name: "URL_PREFIX",
|
||||
description: "The prefix of the URL to start web scraping.",
|
||||
value: config.baseUrl,
|
||||
},
|
||||
{
|
||||
name: "MAX_DEPTH",
|
||||
description: "The maximum depth to scrape.",
|
||||
value: config.depth?.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return envs;
|
||||
};
|
||||
|
||||
export const createBackendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
openAiKey?: string;
|
||||
llamaCloudKey?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
model?: string;
|
||||
embeddingModel?: string;
|
||||
framework?: TemplateFramework;
|
||||
dataSources?: TemplateDataSource[];
|
||||
port?: number;
|
||||
},
|
||||
) => {
|
||||
// Init env values
|
||||
const envFileName = ".env";
|
||||
const defaultEnvs = [
|
||||
{
|
||||
render: true,
|
||||
name: "MODEL",
|
||||
description: "The name of LLM model to use.",
|
||||
value: opts.model || "gpt-3.5-turbo",
|
||||
},
|
||||
{
|
||||
render: true,
|
||||
name: "OPENAI_API_KEY",
|
||||
description: "The OpenAI API key to use.",
|
||||
value: opts.openAiKey,
|
||||
},
|
||||
// Add vector database environment variables
|
||||
...(opts.vectorDb ? getVectorDBEnvs(opts.vectorDb) : []),
|
||||
// Add data source environment variables
|
||||
...(opts.dataSources ? getDataSourceEnvs(opts.dataSources) : []),
|
||||
];
|
||||
let envVars: EnvVar[] = [];
|
||||
if (opts.framework === "fastapi") {
|
||||
envVars = [
|
||||
...defaultEnvs,
|
||||
...[
|
||||
{
|
||||
name: "APP_HOST",
|
||||
description: "The address to start the backend app.",
|
||||
value: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
name: "APP_PORT",
|
||||
description: "The port to start the backend app.",
|
||||
value: opts.port?.toString() || "8000",
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_MODEL",
|
||||
description: "Name of the embedding model to use.",
|
||||
value: opts.embeddingModel,
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_DIM",
|
||||
description: "Dimension of the embedding model to use.",
|
||||
},
|
||||
{
|
||||
name: "LLM_TEMPERATURE",
|
||||
description: "Temperature for sampling from the model.",
|
||||
},
|
||||
{
|
||||
name: "LLM_MAX_TOKENS",
|
||||
description: "Maximum number of tokens to generate.",
|
||||
},
|
||||
{
|
||||
name: "TOP_K",
|
||||
description:
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
value: "3",
|
||||
},
|
||||
{
|
||||
name: "SYSTEM_PROMPT",
|
||||
description: `Custom system prompt.
|
||||
Example:
|
||||
SYSTEM_PROMPT="
|
||||
We have provided context information below.
|
||||
---------------------
|
||||
{context_str}
|
||||
---------------------
|
||||
Given this information, please answer the question: {query_str}
|
||||
"`,
|
||||
},
|
||||
opts?.dataSources?.some(
|
||||
(ds) => (ds.config as FileSourceConfig).useLlamaParse,
|
||||
)
|
||||
? {
|
||||
name: "LLAMA_CLOUD_API_KEY",
|
||||
description: `The Llama Cloud API key.`,
|
||||
value: opts.llamaCloudKey,
|
||||
}
|
||||
: {},
|
||||
],
|
||||
];
|
||||
} else {
|
||||
envVars = [
|
||||
...defaultEnvs,
|
||||
...[
|
||||
opts.framework === "nextjs"
|
||||
? {
|
||||
name: "NEXT_PUBLIC_MODEL",
|
||||
description:
|
||||
"The LLM model to use (hardcode to front-end artifact).",
|
||||
value: opts.model || "gpt-3.5-turbo",
|
||||
}
|
||||
: {},
|
||||
],
|
||||
];
|
||||
}
|
||||
// Render and write env file
|
||||
const content = renderEnvVar(envVars);
|
||||
await fs.writeFile(path.join(root, envFileName), content);
|
||||
console.log(`Created '${envFileName}' file. Please check the settings.`);
|
||||
};
|
||||
|
||||
export const createFrontendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
customApiPath?: string;
|
||||
model?: string;
|
||||
},
|
||||
) => {
|
||||
const defaultFrontendEnvs = [
|
||||
{
|
||||
name: "MODEL",
|
||||
description: "The OpenAI model to use.",
|
||||
value: opts.model,
|
||||
},
|
||||
{
|
||||
name: "NEXT_PUBLIC_MODEL",
|
||||
description: "The OpenAI model to use (hardcode to front-end artifact).",
|
||||
value: opts.model,
|
||||
},
|
||||
{
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description: "The backend API for chat endpoint.",
|
||||
value: opts.customApiPath
|
||||
? opts.customApiPath
|
||||
: "http://localhost:8000/api/chat",
|
||||
},
|
||||
];
|
||||
const content = renderEnvVar(defaultFrontendEnvs);
|
||||
await fs.writeFile(path.join(root, ".env"), content);
|
||||
};
|
||||
@@ -1,208 +0,0 @@
|
||||
import { copy } from "./copy";
|
||||
import { callPackageManager } from "./install";
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { cyan } from "picocolors";
|
||||
|
||||
import { templatesDir } from "./dir";
|
||||
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { installLlamapackProject } from "./llama-pack";
|
||||
import { isHavingPoetryLockFile, tryPoetryRun } from "./poetry";
|
||||
import { installPythonTemplate } from "./python";
|
||||
import { downloadAndExtractRepo } from "./repo";
|
||||
import {
|
||||
FileSourceConfig,
|
||||
InstallTemplateArgs,
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
import { installTSTemplate } from "./typescript";
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
async function generateContextData(
|
||||
framework: TemplateFramework,
|
||||
packageManager?: PackageManager,
|
||||
openAiKey?: string,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
llamaCloudKey?: string,
|
||||
useLlamaParse?: boolean,
|
||||
) {
|
||||
if (packageManager) {
|
||||
const runGenerate = `${cyan(
|
||||
framework === "fastapi"
|
||||
? "poetry run python app/engine/generate.py"
|
||||
: `${packageManager} run generate`,
|
||||
)}`;
|
||||
const openAiKeyConfigured = openAiKey || process.env["OPENAI_API_KEY"];
|
||||
const llamaCloudKeyConfigured = useLlamaParse
|
||||
? llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
: true;
|
||||
const hasVectorDb = vectorDb && vectorDb !== "none";
|
||||
if (framework === "fastapi") {
|
||||
if (
|
||||
openAiKeyConfigured &&
|
||||
llamaCloudKeyConfigured &&
|
||||
!hasVectorDb &&
|
||||
isHavingPoetryLockFile()
|
||||
) {
|
||||
console.log(`Running ${runGenerate} to generate the context data.`);
|
||||
const result = tryPoetryRun("python app/engine/generate.py");
|
||||
if (!result) {
|
||||
console.log(`Failed to run ${runGenerate}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Generated context data`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (openAiKeyConfigured && vectorDb === "none") {
|
||||
console.log(`Running ${runGenerate} to generate the context data.`);
|
||||
await callPackageManager(packageManager, true, ["run", "generate"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = [];
|
||||
if (!openAiKeyConfigured) settings.push("your OpenAI 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 copyContextData = async (
|
||||
root: string,
|
||||
dataSource?: TemplateDataSource,
|
||||
) => {
|
||||
const destPath = path.join(root, "data");
|
||||
const dataSourceConfig = dataSource?.config as FileSourceConfig;
|
||||
|
||||
// Copy file
|
||||
if (dataSource?.type === "file") {
|
||||
if (dataSourceConfig.paths) {
|
||||
await fs.mkdir(destPath, { recursive: true });
|
||||
console.log(
|
||||
"Copying data from files:",
|
||||
dataSourceConfig.paths.toString(),
|
||||
);
|
||||
for (const p of dataSourceConfig.paths) {
|
||||
await fs.copyFile(p, path.join(destPath, path.basename(p)));
|
||||
}
|
||||
} else {
|
||||
console.log("Missing file path in config");
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy folder
|
||||
if (dataSource?.type === "folder") {
|
||||
// Example data does not have path config, set the default path
|
||||
const srcPaths = dataSourceConfig.paths ?? [
|
||||
path.join(templatesDir, "components", "data"),
|
||||
];
|
||||
console.log("Copying data from folders: ", srcPaths);
|
||||
for (const p of srcPaths) {
|
||||
const folderName = path.basename(p);
|
||||
const destFolderPath = path.join(destPath, folderName);
|
||||
await fs.mkdir(destFolderPath, { recursive: true });
|
||||
await copy("**", destFolderPath, {
|
||||
parents: true,
|
||||
cwd: p,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const installCommunityProject = async ({
|
||||
root,
|
||||
communityProjectConfig,
|
||||
}: Pick<InstallTemplateArgs, "root" | "communityProjectConfig">) => {
|
||||
const { owner, repo, branch, filePath } = communityProjectConfig!;
|
||||
console.log("\nInstalling community project:", filePath || repo);
|
||||
await downloadAndExtractRepo(root, {
|
||||
username: owner,
|
||||
name: repo,
|
||||
branch,
|
||||
filePath: filePath || "",
|
||||
});
|
||||
};
|
||||
|
||||
export const installTemplate = async (
|
||||
props: InstallTemplateArgs & { backend: boolean },
|
||||
) => {
|
||||
process.chdir(props.root);
|
||||
|
||||
if (props.template === "community" && props.communityProjectConfig) {
|
||||
await installCommunityProject(props);
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.template === "llamapack" && props.llamapack) {
|
||||
await installLlamapackProject(props);
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.framework === "fastapi") {
|
||||
await installPythonTemplate(props);
|
||||
} else {
|
||||
await installTSTemplate(props);
|
||||
}
|
||||
|
||||
if (props.backend) {
|
||||
// This is a backend, so we need to copy the test data and create the env file.
|
||||
|
||||
// Copy the environment file to the target directory.
|
||||
await createBackendEnvFile(props.root, {
|
||||
openAiKey: props.openAiKey,
|
||||
llamaCloudKey: props.llamaCloudKey,
|
||||
vectorDb: props.vectorDb,
|
||||
model: props.model,
|
||||
embeddingModel: props.embeddingModel,
|
||||
framework: props.framework,
|
||||
dataSources: props.dataSources,
|
||||
port: props.externalPort,
|
||||
});
|
||||
|
||||
if (props.engine === "context") {
|
||||
console.log("\nGenerating context data...\n");
|
||||
props.dataSources.forEach(async (ds) => {
|
||||
if (ds.type === "file" || ds.type === "folder") {
|
||||
await copyContextData(props.root, ds);
|
||||
}
|
||||
});
|
||||
if (
|
||||
props.postInstallAction === "runApp" ||
|
||||
props.postInstallAction === "dependencies"
|
||||
) {
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.packageManager,
|
||||
props.openAiKey,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.dataSources.some(
|
||||
(ds) =>
|
||||
(ds.type === "file" || ds.type === "folder") &&
|
||||
(ds.config as FileSourceConfig).useLlamaParse,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// this is a frontend for a full-stack app, create .env file with model information
|
||||
createFrontendEnvFile(props.root, {
|
||||
model: props.model,
|
||||
customApiPath: props.customApiPath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export * from "./types";
|
||||
@@ -1,148 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import got from "got";
|
||||
import path from "path";
|
||||
import { parse } from "smol-toml";
|
||||
import {
|
||||
LLAMA_PACK_FOLDER,
|
||||
LLAMA_PACK_FOLDER_PATH,
|
||||
LLAMA_PACK_OWNER,
|
||||
LLAMA_PACK_REPO,
|
||||
} from "./constant";
|
||||
import { copy } from "./copy";
|
||||
import { templatesDir } from "./dir";
|
||||
import { addDependencies, installPythonDependencies } from "./python";
|
||||
import { getRepoRawContent } from "./repo";
|
||||
import { InstallTemplateArgs } from "./types";
|
||||
|
||||
const getLlamaPackFolderSHA = async () => {
|
||||
const url = `https://api.github.com/repos/${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/contents`;
|
||||
const response = await got(url, {
|
||||
responseType: "json",
|
||||
});
|
||||
const data = response.body as any[];
|
||||
const llamaPackFolder = data.find((item) => item.name === LLAMA_PACK_FOLDER);
|
||||
return llamaPackFolder.sha;
|
||||
};
|
||||
|
||||
const getLLamaPackFolderTree = async (
|
||||
sha: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
path: string;
|
||||
}>
|
||||
> => {
|
||||
const url = `https://api.github.com/repos/${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/git/trees/${sha}?recursive=1`;
|
||||
const response = await got(url, {
|
||||
responseType: "json",
|
||||
});
|
||||
return (response.body as any).tree;
|
||||
};
|
||||
|
||||
export async function getAvailableLlamapackOptions(): Promise<
|
||||
{
|
||||
name: string;
|
||||
folderPath: string;
|
||||
}[]
|
||||
> {
|
||||
const EXAMPLE_RELATIVE_PATH = "/examples/example.py";
|
||||
const PACK_FOLDER_SUBFIX = "llama-index-packs";
|
||||
|
||||
const llamaPackFolderSHA = await getLlamaPackFolderSHA();
|
||||
const llamaPackTree = await getLLamaPackFolderTree(llamaPackFolderSHA);
|
||||
|
||||
// Return options that have example files
|
||||
const exampleFiles = llamaPackTree.filter((item) =>
|
||||
item.path.endsWith(EXAMPLE_RELATIVE_PATH),
|
||||
);
|
||||
const options = exampleFiles.map((file) => {
|
||||
const packFolder = file.path.substring(
|
||||
0,
|
||||
file.path.indexOf(EXAMPLE_RELATIVE_PATH),
|
||||
);
|
||||
const packName = packFolder.substring(PACK_FOLDER_SUBFIX.length + 1);
|
||||
return {
|
||||
name: packName,
|
||||
folderPath: packFolder,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
const copyLlamapackEmptyProject = async ({
|
||||
root,
|
||||
}: Pick<InstallTemplateArgs, "root">) => {
|
||||
const templatePath = path.join(
|
||||
templatesDir,
|
||||
"components/sample-projects/llamapack",
|
||||
);
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
});
|
||||
};
|
||||
|
||||
const copyData = async ({
|
||||
root,
|
||||
}: Pick<InstallTemplateArgs, "root" | "llamapack">) => {
|
||||
const dataPath = path.join(templatesDir, "components/data");
|
||||
await copy("**", path.join(root, "data"), {
|
||||
parents: true,
|
||||
cwd: dataPath,
|
||||
});
|
||||
};
|
||||
|
||||
const installLlamapackExample = async ({
|
||||
root,
|
||||
llamapack,
|
||||
}: Pick<InstallTemplateArgs, "root" | "llamapack">) => {
|
||||
const exampleFileName = "example.py";
|
||||
const readmeFileName = "README.md";
|
||||
const projectTomlFileName = "pyproject.toml";
|
||||
const exampleFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/examples/${exampleFileName}`;
|
||||
const readmeFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/${readmeFileName}`;
|
||||
const projectTomlFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/${projectTomlFileName}`;
|
||||
|
||||
// Download example.py from llamapack and save to root
|
||||
const exampleContent = await getRepoRawContent(exampleFilePath);
|
||||
await fs.writeFile(path.join(root, exampleFileName), exampleContent);
|
||||
|
||||
// Download README.md from llamapack and combine with README-template.md,
|
||||
// save to root and then delete template file
|
||||
const readmeContent = await getRepoRawContent(readmeFilePath);
|
||||
const readmeTemplateContent = await fs.readFile(
|
||||
path.join(root, "README-template.md"),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, readmeFileName),
|
||||
`${readmeContent}\n${readmeTemplateContent}`,
|
||||
);
|
||||
await fs.unlink(path.join(root, "README-template.md"));
|
||||
|
||||
// Download pyproject.toml from llamapack, parse it to get package name and version,
|
||||
// then add it as a dependency to current toml file in the project
|
||||
const projectTomlContent = await getRepoRawContent(projectTomlFilePath);
|
||||
const fileParsed = parse(projectTomlContent) as any;
|
||||
const packageName = fileParsed.tool.poetry.name;
|
||||
const packageVersion = fileParsed.tool.poetry.version;
|
||||
await addDependencies(root, [
|
||||
{
|
||||
name: packageName,
|
||||
version: packageVersion,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export const installLlamapackProject = async ({
|
||||
root,
|
||||
llamapack,
|
||||
postInstallAction,
|
||||
}: Pick<InstallTemplateArgs, "root" | "llamapack" | "postInstallAction">) => {
|
||||
console.log("\nInstalling Llamapack project:", llamapack!);
|
||||
await copyLlamapackEmptyProject({ root });
|
||||
await copyData({ root });
|
||||
await installLlamapackExample({ root, llamapack });
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
installPythonDependencies({ noRoot: true });
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
|
||||
export function isPoetryAvailable(): boolean {
|
||||
try {
|
||||
execSync("poetry --version", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tryPoetryInstall(noRoot: boolean): boolean {
|
||||
try {
|
||||
execSync(`poetry install${noRoot ? " --no-root" : ""}`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tryPoetryRun(command: string): boolean {
|
||||
try {
|
||||
execSync(`poetry run ${command}`, { stdio: "inherit" });
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isHavingPoetryLockFile(): boolean {
|
||||
try {
|
||||
return fs.existsSync("poetry.lock");
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { cyan, red } from "picocolors";
|
||||
import { parse, stringify } from "smol-toml";
|
||||
import terminalLink from "terminal-link";
|
||||
import { copy } from "./copy";
|
||||
import { templatesDir } from "./dir";
|
||||
import { isPoetryAvailable, tryPoetryInstall } from "./poetry";
|
||||
import { Tool } from "./tools";
|
||||
import {
|
||||
FileSourceConfig,
|
||||
InstallTemplateArgs,
|
||||
TemplateDataSource,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
|
||||
interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
vectorDb?: TemplateVectorDB,
|
||||
dataSource?: TemplateDataSource,
|
||||
tools?: Tool[],
|
||||
) => {
|
||||
const dependencies: Dependency[] = [];
|
||||
|
||||
// Add vector db dependencies
|
||||
switch (vectorDb) {
|
||||
case "mongo": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-mongodb",
|
||||
version: "^0.1.3",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pg": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-postgres",
|
||||
version: "^0.1.1",
|
||||
});
|
||||
}
|
||||
case "pinecone": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: "^0.1.3",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "milvus": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-milvus",
|
||||
version: "^0.1.6",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add data source dependencies
|
||||
const dataSourceType = dataSource?.type;
|
||||
if (dataSourceType === "file" || dataSourceType === "folder") {
|
||||
// llama-index-readers-file (pdf, excel, csv) is already included in llama_index package
|
||||
dependencies.push({
|
||||
name: "docx2txt",
|
||||
version: "^0.8",
|
||||
});
|
||||
} else if (dataSourceType === "web") {
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-web",
|
||||
version: "^0.1.6",
|
||||
});
|
||||
}
|
||||
|
||||
// Add tools dependencies
|
||||
tools?.forEach((tool) => {
|
||||
tool.dependencies?.forEach((dep) => {
|
||||
dependencies.push(dep);
|
||||
});
|
||||
});
|
||||
|
||||
return dependencies;
|
||||
};
|
||||
|
||||
const mergePoetryDependencies = (
|
||||
dependencies: Dependency[],
|
||||
existingDependencies: Record<string, Omit<Dependency, "name">>,
|
||||
) => {
|
||||
for (const dependency of dependencies) {
|
||||
let value = existingDependencies[dependency.name] ?? {};
|
||||
|
||||
// default string value is equal to attribute "version"
|
||||
if (typeof value === "string") {
|
||||
value = { version: value };
|
||||
}
|
||||
|
||||
value.version = dependency.version ?? value.version;
|
||||
value.extras = dependency.extras ?? value.extras;
|
||||
|
||||
if (value.version === undefined) {
|
||||
throw new Error(
|
||||
`Dependency "${dependency.name}" is missing attribute "version"!`,
|
||||
);
|
||||
}
|
||||
|
||||
existingDependencies[dependency.name] = value;
|
||||
}
|
||||
};
|
||||
|
||||
export const addDependencies = async (
|
||||
projectDir: string,
|
||||
dependencies: Dependency[],
|
||||
) => {
|
||||
if (dependencies.length === 0) return;
|
||||
|
||||
const FILENAME = "pyproject.toml";
|
||||
try {
|
||||
// Parse toml file
|
||||
const file = path.join(projectDir, FILENAME);
|
||||
const fileContent = await fs.readFile(file, "utf8");
|
||||
const fileParsed = parse(fileContent);
|
||||
|
||||
// Modify toml dependencies
|
||||
const tool = fileParsed.tool as any;
|
||||
const existingDependencies = tool.poetry.dependencies;
|
||||
mergePoetryDependencies(dependencies, existingDependencies);
|
||||
|
||||
// Write toml file
|
||||
const newFileContent = stringify(fileParsed);
|
||||
await fs.writeFile(file, newFileContent);
|
||||
|
||||
const dependenciesString = dependencies.map((d) => d.name).join(", ");
|
||||
console.log(`\nAdded ${dependenciesString} to ${cyan(FILENAME)}\n`);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Error while updating dependencies for Poetry project file ${FILENAME}\n`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const installPythonDependencies = (
|
||||
{ noRoot }: { noRoot: boolean } = { noRoot: false },
|
||||
) => {
|
||||
if (isPoetryAvailable()) {
|
||||
console.log(
|
||||
`Installing python dependencies using poetry. This may take a while...`,
|
||||
);
|
||||
const installSuccessful = tryPoetryInstall(noRoot);
|
||||
if (!installSuccessful) {
|
||||
console.error(
|
||||
red(
|
||||
"Installing dependencies using poetry failed. Please check error log above and try running create-llama again.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
red(
|
||||
`Poetry is not available in the current environment. Please check ${terminalLink(
|
||||
"Poetry Installation",
|
||||
`https://python-poetry.org/docs/#installation`,
|
||||
)} to install poetry first, then run create-llama again.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const installPythonTemplate = async ({
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
engine,
|
||||
vectorDb,
|
||||
dataSources,
|
||||
tools,
|
||||
postInstallAction,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "root"
|
||||
| "framework"
|
||||
| "template"
|
||||
| "engine"
|
||||
| "vectorDb"
|
||||
| "dataSources"
|
||||
| "tools"
|
||||
| "postInstallAction"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
rename(name) {
|
||||
switch (name) {
|
||||
case "gitignore": {
|
||||
return `.${name}`;
|
||||
}
|
||||
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
|
||||
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (engine === "context") {
|
||||
const enginePath = path.join(root, "app", "engine");
|
||||
const compPath = path.join(templatesDir, "components");
|
||||
|
||||
const vectorDbDirName = vectorDb ?? "none";
|
||||
const VectorDBPath = path.join(
|
||||
compPath,
|
||||
"vectordbs",
|
||||
"python",
|
||||
vectorDbDirName,
|
||||
);
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: VectorDBPath,
|
||||
});
|
||||
|
||||
// Copy engine code
|
||||
if (tools !== undefined && tools.length > 0) {
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "engines", "python", "agent"),
|
||||
});
|
||||
// Write tools_config.json
|
||||
const configContent: Record<string, any> = {};
|
||||
tools.forEach((tool) => {
|
||||
configContent[tool.name] = tool.config ?? {};
|
||||
});
|
||||
const configFilePath = path.join(root, "tools_config.json");
|
||||
await fs.writeFile(
|
||||
configFilePath,
|
||||
JSON.stringify(configContent, null, 2),
|
||||
);
|
||||
} else {
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "engines", "python", "chat"),
|
||||
});
|
||||
}
|
||||
|
||||
if (dataSources.length > 0 || dataSources[0].type !== "none") {
|
||||
// Copy loader.py file to enginePath
|
||||
await copy("loader.py", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "loaders", "python"),
|
||||
});
|
||||
|
||||
// Copy data source loaders
|
||||
const loaderPath = path.join(enginePath, "loaders");
|
||||
for (const source of dataSources) {
|
||||
const sourceType = source.type;
|
||||
if (sourceType === "file" || sourceType === "folder") {
|
||||
const sourceConfig = source.config as FileSourceConfig;
|
||||
const loaderFolder = sourceConfig.useLlamaParse
|
||||
? "llama_parse"
|
||||
: "file";
|
||||
await copy("**", loaderPath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "loaders", "python", loaderFolder),
|
||||
});
|
||||
} else {
|
||||
await copy("**", loaderPath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "loaders", "python", sourceType),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const dataSourceType = dataSource?.type;
|
||||
// if (dataSourceType !== undefined && dataSourceType !== "none") {
|
||||
// let loaderFolder: string;
|
||||
// if (dataSourceType === "file" || dataSourceType === "folder") {
|
||||
// const dataSourceConfig = dataSource?.config as FileSourceConfig;
|
||||
// loaderFolder = dataSourceConfig.useLlamaParse ? "llama_parse" : "file";
|
||||
// } else {
|
||||
// loaderFolder = dataSourceType;
|
||||
// }
|
||||
// await copy("**", enginePath, {
|
||||
// parents: true,
|
||||
// cwd: path.join(compPath, "loaders", "python", loaderFolder),
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
const addOnDependencies = dataSources
|
||||
.map((ds) => getAdditionalDependencies(vectorDb, ds, tools))
|
||||
.flat();
|
||||
await addDependencies(root, addOnDependencies);
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
installPythonDependencies();
|
||||
}
|
||||
};
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
import { createWriteStream, promises } from "fs";
|
||||
import got from "got";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { Stream } from "stream";
|
||||
import tar from "tar";
|
||||
import { promisify } from "util";
|
||||
import { makeDir } from "./make-dir";
|
||||
import { CommunityProjectConfig } from "./types";
|
||||
|
||||
export type RepoInfo = {
|
||||
username: string;
|
||||
name: string;
|
||||
branch: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
const pipeline = promisify(Stream.pipeline);
|
||||
|
||||
async function downloadTar(url: string) {
|
||||
const tempFile = join(tmpdir(), `next.js-cna-example.temp-${Date.now()}`);
|
||||
await pipeline(got.stream(url), createWriteStream(tempFile));
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
export async function downloadAndExtractRepo(
|
||||
root: string,
|
||||
{ username, name, branch, filePath }: RepoInfo,
|
||||
) {
|
||||
await makeDir(root);
|
||||
|
||||
const tempFile = await downloadTar(
|
||||
`https://codeload.github.com/${username}/${name}/tar.gz/${branch}`,
|
||||
);
|
||||
|
||||
await tar.x({
|
||||
file: tempFile,
|
||||
cwd: root,
|
||||
strip: filePath ? filePath.split("/").length + 1 : 1,
|
||||
filter: (p) =>
|
||||
p.startsWith(
|
||||
`${name}-${branch.replace(/\//g, "-")}${
|
||||
filePath ? `/${filePath}/` : "/"
|
||||
}`,
|
||||
),
|
||||
});
|
||||
|
||||
await promises.unlink(tempFile);
|
||||
}
|
||||
|
||||
const getRepoInfo = async (owner: string, repo: string) => {
|
||||
const repoInfoRes = await got(
|
||||
`https://api.github.com/repos/${owner}/${repo}`,
|
||||
{
|
||||
responseType: "json",
|
||||
},
|
||||
);
|
||||
const data = repoInfoRes.body as any;
|
||||
return data;
|
||||
};
|
||||
|
||||
export async function getProjectOptions(
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<
|
||||
{
|
||||
value: CommunityProjectConfig;
|
||||
title: string;
|
||||
}[]
|
||||
> {
|
||||
// TODO: consider using octokit (https://github.com/octokit) if more changes are needed in the future
|
||||
const getCommunityProjectConfig = async (
|
||||
item: any,
|
||||
): Promise<CommunityProjectConfig | null> => {
|
||||
// if item is a folder, return the path with default owner, repo, and main branch
|
||||
if (item.type === "dir")
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
branch: "main",
|
||||
filePath: item.path,
|
||||
};
|
||||
|
||||
// check if it's a submodule (has size = 0 and different owner & repo)
|
||||
if (item.type === "file") {
|
||||
if (item.size !== 0) return null; // submodules have size = 0
|
||||
|
||||
// get owner and repo from git_url
|
||||
const { git_url } = item;
|
||||
const startIndex = git_url.indexOf("repos/") + 6;
|
||||
const endIndex = git_url.indexOf("/git");
|
||||
const ownerRepoStr = git_url.substring(startIndex, endIndex);
|
||||
const [owner, repo] = ownerRepoStr.split("/");
|
||||
|
||||
// quick fetch repo info to get the default branch
|
||||
const { default_branch } = await getRepoInfo(owner, repo);
|
||||
|
||||
// return the path with default owner, repo, and main branch (path is empty for submodules)
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
branch: default_branch,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const url = `https://api.github.com/repos/${owner}/${repo}/contents`;
|
||||
const response = await got(url, {
|
||||
responseType: "json",
|
||||
});
|
||||
const data = response.body as any[];
|
||||
|
||||
const projectConfigs: CommunityProjectConfig[] = [];
|
||||
for (const item of data) {
|
||||
const communityProjectConfig = await getCommunityProjectConfig(item);
|
||||
if (communityProjectConfig) projectConfigs.push(communityProjectConfig);
|
||||
}
|
||||
return projectConfigs.map((config) => {
|
||||
return {
|
||||
value: config,
|
||||
title: config.filePath || config.repo, // for submodules, use repo name as title
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRepoRawContent(repoFilePath: string) {
|
||||
const url = `https://raw.githubusercontent.com/${repoFilePath}`;
|
||||
const response = await got(url, {
|
||||
responseType: "text",
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { ChildProcess, SpawnOptions, spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
const createProcess = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: SpawnOptions,
|
||||
) => {
|
||||
return spawn(command, args, {
|
||||
...options,
|
||||
shell: true,
|
||||
})
|
||||
.on("exit", function (code) {
|
||||
if (code !== 0) {
|
||||
console.log(`Child process exited with code=${code}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.log("Error when running chill process: ", err);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
externalPort?: number,
|
||||
): Promise<any> {
|
||||
let backendAppProcess: ChildProcess;
|
||||
let frontendAppProcess: ChildProcess | undefined;
|
||||
const frontendPort = port || 3000;
|
||||
let backendPort = externalPort || 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}` },
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { red } from "picocolors";
|
||||
|
||||
export type Tool = {
|
||||
display: string;
|
||||
name: string;
|
||||
config?: Record<string, any>;
|
||||
dependencies?: ToolDependencies[];
|
||||
};
|
||||
export type ToolDependencies = {
|
||||
name: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export const supportedTools: Tool[] = [
|
||||
{
|
||||
display: "Google Search (configuration required after installation)",
|
||||
name: "google.GoogleSearchToolSpec",
|
||||
config: {
|
||||
engine:
|
||||
"Your search engine id, see https://developers.google.com/custom-search/v1/overview#prerequisites",
|
||||
key: "Your search api key",
|
||||
num: 2,
|
||||
},
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-google",
|
||||
version: "0.1.2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Wikipedia",
|
||||
name: "wikipedia.WikipediaToolSpec",
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-wikipedia",
|
||||
version: "0.1.2",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getTool = (toolName: string): Tool | undefined => {
|
||||
return supportedTools.find((tool) => tool.name === toolName);
|
||||
};
|
||||
|
||||
export const getTools = (toolsName: string[]): Tool[] => {
|
||||
const tools: Tool[] = [];
|
||||
for (const toolName of toolsName) {
|
||||
const tool = getTool(toolName);
|
||||
if (!tool) {
|
||||
console.log(
|
||||
red(
|
||||
`Error: Tool '${toolName}' is not supported. Supported tools are: ${supportedTools
|
||||
.map((t) => t.name)
|
||||
.join(", ")}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
tools.push(tool);
|
||||
}
|
||||
return tools;
|
||||
};
|
||||
|
||||
export const toolsRequireConfig = (tools?: Tool[]): boolean => {
|
||||
if (tools) {
|
||||
return tools?.some((tool) => Object.keys(tool.config || {}).length > 0);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
import { Tool } from "./tools";
|
||||
|
||||
export type TemplateType = "simple" | "streaming" | "community" | "llamapack";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateEngine = "simple" | "context";
|
||||
export type TemplateUI = "html" | "shadcn";
|
||||
export type TemplateVectorDB = "none" | "mongo" | "pg" | "pinecone" | "milvus";
|
||||
export type TemplatePostInstallAction =
|
||||
| "none"
|
||||
| "VSCode"
|
||||
| "dependencies"
|
||||
| "runApp";
|
||||
export type TemplateDataSource = {
|
||||
type: TemplateDataSourceType;
|
||||
config: TemplateDataSourceConfig;
|
||||
};
|
||||
export type TemplateDataSourceType = "none" | "file" | "folder" | "web";
|
||||
export type TemplateObservability = "none" | "opentelemetry";
|
||||
// Config for both file and folder
|
||||
export type FileSourceConfig = {
|
||||
paths?: string[];
|
||||
useLlamaParse?: boolean;
|
||||
};
|
||||
export type WebSourceConfig = {
|
||||
baseUrl?: string;
|
||||
depth?: number;
|
||||
};
|
||||
export type TemplateDataSourceConfig = FileSourceConfig | WebSourceConfig;
|
||||
|
||||
export type CommunityProjectConfig = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch: string;
|
||||
filePath?: string;
|
||||
};
|
||||
|
||||
export interface InstallTemplateArgs {
|
||||
appName: string;
|
||||
root: string;
|
||||
packageManager: PackageManager;
|
||||
isOnline: boolean;
|
||||
template: TemplateType;
|
||||
framework: TemplateFramework;
|
||||
engine: TemplateEngine;
|
||||
ui: TemplateUI;
|
||||
dataSources: TemplateDataSource[];
|
||||
eslint: boolean;
|
||||
customApiPath?: string;
|
||||
openAiKey?: string;
|
||||
llamaCloudKey?: string;
|
||||
model: string;
|
||||
embeddingModel: string;
|
||||
communityProjectConfig?: CommunityProjectConfig;
|
||||
llamapack?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
externalPort?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bold, cyan } from "picocolors";
|
||||
import { copy } from "../helpers/copy";
|
||||
import { callPackageManager } from "../helpers/install";
|
||||
import { templatesDir } from "./dir";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { InstallTemplateArgs } from "./types";
|
||||
|
||||
const rename = (name: string) => {
|
||||
switch (name) {
|
||||
case "gitignore":
|
||||
case "eslintrc.json": {
|
||||
return `.${name}`;
|
||||
}
|
||||
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
|
||||
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const installTSDependencies = async (
|
||||
packageJson: any,
|
||||
packageManager: PackageManager,
|
||||
isOnline: boolean,
|
||||
): Promise<void> => {
|
||||
console.log("\nInstalling dependencies:");
|
||||
for (const dependency in packageJson.dependencies)
|
||||
console.log(`- ${cyan(dependency)}`);
|
||||
|
||||
console.log("\nInstalling devDependencies:");
|
||||
for (const dependency in packageJson.devDependencies)
|
||||
console.log(`- ${cyan(dependency)}`);
|
||||
|
||||
console.log();
|
||||
|
||||
await callPackageManager(packageManager, isOnline).catch((error) => {
|
||||
console.error("Failed to install TS dependencies. Exiting...");
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Install a LlamaIndex internal template to a given `root` directory.
|
||||
*/
|
||||
export const installTSTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
packageManager,
|
||||
isOnline,
|
||||
template,
|
||||
framework,
|
||||
engine,
|
||||
ui,
|
||||
eslint,
|
||||
customApiPath,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
backend,
|
||||
observability,
|
||||
}: InstallTemplateArgs & { backend: boolean }) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
/**
|
||||
* 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 copySource = ["**"];
|
||||
if (!eslint) copySource.push("!eslintrc.json");
|
||||
|
||||
await copy(copySource, root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
rename,
|
||||
});
|
||||
|
||||
/**
|
||||
* If next.js is used, update its configuration if necessary
|
||||
*/
|
||||
if (framework === "nextjs") {
|
||||
const nextConfigJsonFile = path.join(root, "next.config.json");
|
||||
const nextConfigJson: any = JSON.parse(
|
||||
await fs.readFile(nextConfigJsonFile, "utf8"),
|
||||
);
|
||||
if (!backend) {
|
||||
// update next.config.json for static site generation
|
||||
nextConfigJson.output = "export";
|
||||
nextConfigJson.images = { unoptimized: true };
|
||||
console.log("\nUsing static site generation\n");
|
||||
} else {
|
||||
if (vectorDb === "milvus") {
|
||||
nextConfigJson.experimental.serverComponentsExternalPackages =
|
||||
nextConfigJson.experimental.serverComponentsExternalPackages ?? [];
|
||||
nextConfigJson.experimental.serverComponentsExternalPackages.push(
|
||||
"@zilliz/milvus2-sdk-node",
|
||||
);
|
||||
}
|
||||
}
|
||||
await fs.writeFile(
|
||||
nextConfigJsonFile,
|
||||
JSON.stringify(nextConfigJson, null, 2) + os.EOL,
|
||||
);
|
||||
|
||||
const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
|
||||
if (observability === "opentelemetry") {
|
||||
const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
|
||||
await fs.rm(webpackConfigDefaultFile);
|
||||
await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
|
||||
} else {
|
||||
await fs.rm(webpackConfigOtelFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (observability && observability !== "none") {
|
||||
const chosenObservabilityPath = path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"observability",
|
||||
"typescript",
|
||||
observability,
|
||||
);
|
||||
const relativeObservabilityPath = framework === "nextjs" ? "app" : "src";
|
||||
|
||||
await copy(
|
||||
"**",
|
||||
path.join(root, relativeObservabilityPath, "observability"),
|
||||
{ cwd: chosenObservabilityPath },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the selected chat engine files to the target directory and reference it.
|
||||
*/
|
||||
let relativeEngineDestPath;
|
||||
const compPath = path.join(templatesDir, "components");
|
||||
if (engine && (framework === "express" || framework === "nextjs")) {
|
||||
console.log("\nUsing chat engine:", engine, "\n");
|
||||
|
||||
let vectorDBFolder: string = engine;
|
||||
|
||||
if (engine !== "simple" && vectorDb) {
|
||||
console.log("\nUsing vector DB:", vectorDb, "\n");
|
||||
vectorDBFolder = vectorDb;
|
||||
}
|
||||
|
||||
const VectorDBPath = path.join(
|
||||
compPath,
|
||||
"vectordbs",
|
||||
"typescript",
|
||||
vectorDBFolder,
|
||||
);
|
||||
relativeEngineDestPath =
|
||||
framework === "nextjs"
|
||||
? path.join("app", "api", "chat")
|
||||
: path.join("src", "controllers");
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "engine"), {
|
||||
parents: true,
|
||||
cwd: VectorDBPath,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the selected UI files to the target directory and reference it.
|
||||
*/
|
||||
if (framework === "nextjs" && ui !== "shadcn") {
|
||||
console.log("\nUsing UI:", ui, "\n");
|
||||
const uiPath = path.join(compPath, "ui", ui);
|
||||
const destUiPath = path.join(root, "app", "components", "ui");
|
||||
// remove the default ui folder
|
||||
await fs.rm(destUiPath, { recursive: true });
|
||||
// copy the selected ui folder
|
||||
await copy("**", destUiPath, {
|
||||
parents: true,
|
||||
cwd: uiPath,
|
||||
rename,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the package.json scripts.
|
||||
*/
|
||||
const packageJsonFile = path.join(root, "package.json");
|
||||
const packageJson: any = JSON.parse(
|
||||
await fs.readFile(packageJsonFile, "utf8"),
|
||||
);
|
||||
packageJson.name = appName;
|
||||
packageJson.version = "0.1.0";
|
||||
|
||||
if (framework === "nextjs" && customApiPath) {
|
||||
console.log(
|
||||
"\nUsing external API with custom API path:",
|
||||
customApiPath,
|
||||
"\n",
|
||||
);
|
||||
// remove the default api folder
|
||||
const apiPath = path.join(root, "app", "api");
|
||||
await fs.rm(apiPath, { recursive: true });
|
||||
// modify the dev script to use the custom api path
|
||||
}
|
||||
|
||||
if (engine === "context" && relativeEngineDestPath) {
|
||||
// add generate script if using context engine
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
generate: `node ${path.join(
|
||||
relativeEngineDestPath,
|
||||
"engine",
|
||||
"generate.mjs",
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (framework === "nextjs" && ui === "html") {
|
||||
// remove shadcn dependencies if html ui is selected
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"tailwind-merge": undefined,
|
||||
"@radix-ui/react-slot": undefined,
|
||||
"class-variance-authority": undefined,
|
||||
clsx: undefined,
|
||||
"lucide-react": undefined,
|
||||
remark: undefined,
|
||||
"remark-code-import": undefined,
|
||||
"remark-gfm": undefined,
|
||||
"remark-math": undefined,
|
||||
"react-markdown": undefined,
|
||||
"react-syntax-highlighter": undefined,
|
||||
};
|
||||
|
||||
packageJson.devDependencies = {
|
||||
...packageJson.devDependencies,
|
||||
"@types/react-syntax-highlighter": undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (observability === "opentelemetry") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@traceloop/node-server-sdk": "^0.5.19",
|
||||
};
|
||||
|
||||
packageJson.devDependencies = {
|
||||
...packageJson.devDependencies,
|
||||
"node-loader": "^2.0.0",
|
||||
};
|
||||
}
|
||||
|
||||
if (!eslint) {
|
||||
// Remove packages starting with "eslint" from devDependencies
|
||||
packageJson.devDependencies = Object.fromEntries(
|
||||
Object.entries(packageJson.devDependencies).filter(
|
||||
([key]) => !key.startsWith("eslint"),
|
||||
),
|
||||
);
|
||||
}
|
||||
await fs.writeFile(
|
||||
packageJsonFile,
|
||||
JSON.stringify(packageJson, null, 2) + os.EOL,
|
||||
);
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
await installTSDependencies(packageJson, packageManager, isOnline);
|
||||
}
|
||||
};
|
||||
+37
-53
@@ -1,74 +1,58 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.0.29",
|
||||
"name": "create-llama-monorepo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Monorepo for create-llama",
|
||||
"keywords": [
|
||||
"rag",
|
||||
"llamaindex",
|
||||
"next.js"
|
||||
"llamaindex"
|
||||
],
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS",
|
||||
"directory": "packages/create-llama"
|
||||
"url": "https://github.com/run-llama/create-llama"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"create-llama": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"python/*"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
|
||||
"dev": "pnpm -r dev",
|
||||
"build": "pnpm -r build",
|
||||
"e2e": "pnpm -r e2e",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"dev": "ncc build ./index.ts -w -o dist/",
|
||||
"build": "npm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
|
||||
"e2e": "playwright test",
|
||||
"prepare": "husky",
|
||||
"release": "pnpm run build && changeset publish",
|
||||
"new-version": "pnpm run build && changeset version"
|
||||
"new-snapshot": "pnpm -r build && changeset version --snapshot",
|
||||
"new-version-python": "pnpm --filter @create-llama/llama-index-server new-version",
|
||||
"new-version": "pnpm -r build && changeset version && pnpm new-version-python",
|
||||
"release-python": "pnpm --filter @create-llama/llama-index-server release",
|
||||
"release": "pnpm -r build && changeset publish && pnpm release-python",
|
||||
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@types/async-retry": "1.4.2",
|
||||
"@types/ci-info": "2.0.0",
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/node": "^20.11.7",
|
||||
"@types/prompts": "2.0.1",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/validate-npm-package-name": "3.0.0",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"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",
|
||||
"cross-spawn": "7.0.3",
|
||||
"fast-glob": "3.3.1",
|
||||
"got": "10.7.0",
|
||||
"picocolors": "1.0.0",
|
||||
"prompts": "2.1.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"smol-toml": "^1.1.4",
|
||||
"tar": "6.1.15",
|
||||
"terminal-link": "^3.0.0",
|
||||
"update-check": "1.5.4",
|
||||
"validate-npm-package-name": "3.0.0",
|
||||
"wait-port": "^1.1.0",
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"eslint": "^8.56.0",
|
||||
"bunchee": "6.4.0",
|
||||
"husky": "^9.0.10",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"typescript": "^5.3.3",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"ora": "^8.0.1"
|
||||
"lint-staged": "^15.2.11",
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"globals": "^15.12.0",
|
||||
"eslint": "9.22.0",
|
||||
"@eslint/js": "^9.25.0",
|
||||
"eslint-config-next": "^15.1.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "7.37.2",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"typescript": "^5.7.3",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.5",
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnpm-store
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
.coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# build
|
||||
dist/
|
||||
lib/
|
||||
|
||||
# e2e
|
||||
.cache
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
playwright/.cache/
|
||||
.tsbuildinfo
|
||||
e2e/cache
|
||||
|
||||
# intellij
|
||||
**/.idea
|
||||
|
||||
# Python
|
||||
.mypy_cache/
|
||||
venv/
|
||||
.venv/
|
||||
dist/
|
||||
.__pycache__
|
||||
__pycache__
|
||||
.python-version
|
||||
.ui
|
||||
|
||||
# build artifacts
|
||||
create-llama-*.tgz
|
||||
|
||||
# copied from root
|
||||
README.md
|
||||
LICENSE.md
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
# create-llama Package
|
||||
|
||||
## Overview
|
||||
|
||||
The `create-llama` package is a CLI tool for creating LlamaIndex-powered applications with one command. It's designed as a project generator that scaffolds various types of RAG (Retrieval-Augmented Generation) applications using different frameworks, databases, and AI model providers.
|
||||
|
||||
## Package Structure
|
||||
|
||||
### Core Files
|
||||
|
||||
- **`index.ts`**: Main CLI entry point using Commander.js for argument parsing
|
||||
- **`create-app.ts`**: Core application creation logic and orchestration
|
||||
- **`package.json`**: Package configuration with binary entry point at `./dist/index.js`
|
||||
|
||||
### Key Directories
|
||||
|
||||
- **`helpers/`**: Utility functions for package management, file operations, and configuration
|
||||
- **`questions/`**: Interactive prompts for user configuration
|
||||
- **`templates/`**: Project templates for different frameworks and use cases
|
||||
- **`e2e/`**: End-to-end tests using Playwright
|
||||
|
||||
## Core Functionality
|
||||
|
||||
### CLI Interface
|
||||
|
||||
The tool accepts numerous command-line options including:
|
||||
|
||||
- Framework selection (`--framework`: nextjs, express, fastapi)
|
||||
- Template type (`--template`: streaming, multiagent, reflex, llamaindexserver)
|
||||
- Model providers (OpenAI, Anthropic, Groq, Ollama, etc.)
|
||||
- Vector databases (none, mongo, pg, pinecone, milvus, etc.)
|
||||
- Data sources (files, web URLs, databases)
|
||||
- Tools and observability options
|
||||
|
||||
### Application Generation Flow
|
||||
|
||||
1. **Project validation**: Checks project name validity and directory permissions
|
||||
2. **Interactive questioning**: Prompts user for configuration if not provided via CLI
|
||||
3. **Template installation**: Copies and configures appropriate templates
|
||||
4. **Environment setup**: Creates `.env` files with API keys and configuration
|
||||
5. **Dependencies**: Installs packages using detected/specified package manager
|
||||
6. **Post-install actions**: Can run the app, open VSCode, or install dependencies
|
||||
|
||||
### Template System
|
||||
|
||||
Templates are organized by:
|
||||
|
||||
- **Framework**: NextJS (frontend), Express (Node backend), FastAPI (Python backend)
|
||||
- **Type**: Streaming chat, multiagent workflows, Reflex UI, LlamaIndex server
|
||||
- **Components**: Engines, loaders, providers, UI components, observability
|
||||
|
||||
### Helper Functions
|
||||
|
||||
Key helper modules include:
|
||||
|
||||
- **Installation**: Package manager detection and dependency installation
|
||||
- **Data sources**: File copying, web scraping, database connection setup
|
||||
- **Providers**: Model provider configuration (OpenAI, Anthropic, etc.)
|
||||
- **Tools**: Integration with external tools (Wikipedia, weather, code generation)
|
||||
- **Environment**: `.env` file generation with API keys and settings
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Build & Development
|
||||
|
||||
- `npm run build`: Build the CLI using bash script
|
||||
- `npm run dev`: Watch mode development build
|
||||
- `npm run clean`: Clean build artifacts and temporary files
|
||||
|
||||
### Testing
|
||||
|
||||
- `npm run e2e`: Run all end-to-end tests
|
||||
- `npm run e2e:python`: Test Python-specific templates
|
||||
- `npm run e2e:typescript`: Test TypeScript-specific templates
|
||||
|
||||
### Package Management
|
||||
|
||||
- `npm run pack-install`: Create and install local package for testing
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Model Configuration
|
||||
|
||||
The tool supports multiple AI providers with a unified `ModelConfig` interface that includes:
|
||||
|
||||
- Provider selection and API key management
|
||||
- Model and embedding model specification
|
||||
- Dimension configuration for embeddings
|
||||
|
||||
### Data Source Handling
|
||||
|
||||
Flexible data source configuration supporting:
|
||||
|
||||
- Local files and directories
|
||||
- Web URLs with configurable crawling depth
|
||||
- Database connections with custom queries
|
||||
- Automatic file downloading and copying
|
||||
|
||||
### Template Flexibility
|
||||
|
||||
Templates use a component-based system allowing mix-and-match of:
|
||||
|
||||
- Different frameworks (NextJS, Express, FastAPI)
|
||||
- Various vector databases
|
||||
- Multiple observability tools
|
||||
- Configurable tools and integrations
|
||||
|
||||
This package serves as the foundation for rapidly prototyping and deploying LlamaIndex applications across different technology stacks and use cases.
|
||||
@@ -0,0 +1,107 @@
|
||||
import path from "path";
|
||||
import { green, yellow } from "picocolors";
|
||||
import { tryGitInit } from "./helpers/git";
|
||||
import { isFolderEmpty } from "./helpers/is-folder-empty";
|
||||
import { isWriteable } from "./helpers/is-writeable";
|
||||
import { makeDir } from "./helpers/make-dir";
|
||||
|
||||
import terminalLink from "terminal-link";
|
||||
import type { InstallTemplateArgs } from "./helpers";
|
||||
import { installTemplate } from "./helpers";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { configVSCode } from "./helpers/vscode";
|
||||
|
||||
export type InstallAppArgs = Omit<
|
||||
InstallTemplateArgs,
|
||||
"appName" | "root" | "port"
|
||||
> & {
|
||||
appPath: string;
|
||||
};
|
||||
|
||||
export async function createApp({
|
||||
template,
|
||||
framework,
|
||||
appPath,
|
||||
packageManager,
|
||||
modelConfig,
|
||||
llamaCloudKey,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
|
||||
if (!(await isWriteable(path.dirname(root)))) {
|
||||
console.error(
|
||||
"The application path is not writable, please check folder permissions and try again.",
|
||||
);
|
||||
console.error(
|
||||
"It is likely you do not have write permissions for this folder.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const appName = path.basename(root);
|
||||
|
||||
await makeDir(root);
|
||||
if (!isFolderEmpty(root, appName)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
|
||||
console.log();
|
||||
|
||||
const args = {
|
||||
appName,
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
packageManager,
|
||||
modelConfig,
|
||||
llamaCloudKey,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
};
|
||||
|
||||
// Install backend
|
||||
await installTemplate(args);
|
||||
|
||||
await configVSCode(root, templatesDir, framework);
|
||||
|
||||
process.chdir(root);
|
||||
if (tryGitInit(root)) {
|
||||
console.log("Initialized a git repository.");
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log(`${green("Success!")} Created ${appName} at ${appPath}`);
|
||||
|
||||
console.log(
|
||||
`Now have a look at the ${terminalLink(
|
||||
"README.md",
|
||||
`file://${root}/README.md`,
|
||||
)} and learn how to get started.`,
|
||||
);
|
||||
|
||||
if (
|
||||
dataSources.some((dataSource) => dataSource.type === "file") &&
|
||||
process.platform === "linux"
|
||||
) {
|
||||
console.log(
|
||||
yellow(
|
||||
`You can add your own data files to ${terminalLink(
|
||||
"data",
|
||||
`file://${root}/data`,
|
||||
)} folder manually.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import {
|
||||
ALL_USE_CASES,
|
||||
TemplateFramework,
|
||||
TemplateVectorDB,
|
||||
} from "../../helpers/types";
|
||||
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const templateFramework: TemplateFramework = "fastapi";
|
||||
const vectorDb: TemplateVectorDB = process.env.VECTORDB
|
||||
? (process.env.VECTORDB as TemplateVectorDB)
|
||||
: "none";
|
||||
|
||||
test.describe("Mypy check", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
test.describe("LlamaIndexServer", async () => {
|
||||
for (const useCase of ALL_USE_CASES) {
|
||||
test(`should pass mypy for use case: ${useCase}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateFramework,
|
||||
vectorDb,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
useCase,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function createAndCheckLlamaProject({
|
||||
options,
|
||||
}: {
|
||||
options: RunCreateLlamaOptions;
|
||||
}): Promise<{ pyprojectPath: string; projectPath: string }> {
|
||||
const result = await runCreateLlama(options);
|
||||
const name = result.projectName;
|
||||
const projectPath = path.join(options.cwd, name);
|
||||
|
||||
// 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();
|
||||
|
||||
// Modify environment for the command
|
||||
const commandEnv = {
|
||||
...process.env,
|
||||
};
|
||||
|
||||
console.log("Running uv venv...");
|
||||
try {
|
||||
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
|
||||
"uv venv",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv venv stdout:", venvStdout);
|
||||
console.error("uv venv stderr:", venvStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running uv venv:", error);
|
||||
throw error; // Re-throw error to fail the test
|
||||
}
|
||||
|
||||
console.log("Running uv sync...");
|
||||
try {
|
||||
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
|
||||
"uv sync --all-extras",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv sync stdout:", syncStdout);
|
||||
console.error("uv sync stderr:", syncStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running uv sync:", error);
|
||||
throw error; // Re-throw error to fail the test
|
||||
}
|
||||
|
||||
console.log("Running uv run mypy ....");
|
||||
try {
|
||||
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
|
||||
"uv run mypy .",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv run mypy stdout:", mypyStdout);
|
||||
console.error("uv run mypy stderr:", mypyStderr);
|
||||
// Assuming mypy success means no output or specific success message
|
||||
// Adjust checks based on actual expected mypy output
|
||||
} catch (error) {
|
||||
console.error("Error running mypy:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If we reach this point without throwing an error, the test passes
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
return { pyprojectPath, projectPath };
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
ALL_USE_CASES,
|
||||
type TemplateFramework,
|
||||
type TemplateVectorDB,
|
||||
} from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const vectorDb: TemplateVectorDB = process.env.VECTORDB
|
||||
? (process.env.VECTORDB as TemplateVectorDB)
|
||||
: "none";
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
|
||||
for (const useCase of ALL_USE_CASES) {
|
||||
test.describe(`Test use case ${useCase} ${templateFramework} ${vectorDb}`, async () => {
|
||||
let port: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateFramework,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction: "runApp",
|
||||
useCase,
|
||||
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 }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 5 * 60 * 1000,
|
||||
});
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive the start of a streamed response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
useCase === "financial_report" || useCase === "deep_research",
|
||||
"Skip chat tests for financial report and deep research.",
|
||||
);
|
||||
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;
|
||||
console.log(`Response status: ${response.status()}`);
|
||||
const responseBody = await response
|
||||
.text()
|
||||
.catch((e) => `Error reading body: ${e}`);
|
||||
console.log(`Response body: ${responseBody}`);
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess, execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { type TemplateFramework, type TemplateVectorDB } from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = "nextjs";
|
||||
const useCase = "code_generator";
|
||||
const vectorDb: TemplateVectorDB = process.env.VECTORDB
|
||||
? (process.env.VECTORDB as TemplateVectorDB)
|
||||
: "none";
|
||||
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const ejectDir = "next";
|
||||
|
||||
test.describe.skip(
|
||||
`Test eject command for ${useCase} ${templateFramework} ${vectorDb}`,
|
||||
async () => {
|
||||
let port: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateFramework,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction: "dependencies",
|
||||
useCase,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("Should successfully eject, install dependencies and build without errors", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
vectorDb === "llamacloud",
|
||||
"Eject test only works with non-llamacloud",
|
||||
);
|
||||
// Run eject command
|
||||
execSync("npm run eject", { cwd: path.join(cwd, name) });
|
||||
|
||||
// Verify next directory exists
|
||||
const nextDirExists = fs.existsSync(path.join(cwd, name, ejectDir));
|
||||
expect(nextDirExists).toBeTruthy();
|
||||
|
||||
// Install dependencies in next directory
|
||||
execSync("npm install", { cwd: path.join(cwd, name, ejectDir) });
|
||||
|
||||
// Run build
|
||||
execSync("npm run build", { cwd: path.join(cwd, name, ejectDir) });
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,90 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import {
|
||||
ALL_USE_CASES,
|
||||
TemplateFramework,
|
||||
TemplateUseCase,
|
||||
TemplateVectorDB,
|
||||
} from "../../helpers/types";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const templateFramework: TemplateFramework = "nextjs";
|
||||
const vectorDb: TemplateVectorDB = process.env.VECTORDB
|
||||
? (process.env.VECTORDB as TemplateVectorDB)
|
||||
: "none";
|
||||
|
||||
test.describe("Test resolve TS dependencies", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
for (const useCase of ALL_USE_CASES) {
|
||||
const optionDescription = `useCase: ${useCase}, vectorDb: ${vectorDb}`;
|
||||
test.describe(`${optionDescription}`, () => {
|
||||
test(`${optionDescription}`, async () => {
|
||||
await runTest({
|
||||
useCase: useCase,
|
||||
vectorDb: vectorDb,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function runTest(options: {
|
||||
useCase: TemplateUseCase;
|
||||
vectorDb: TemplateVectorDB;
|
||||
}) {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const result = await runCreateLlama({
|
||||
cwd: cwd,
|
||||
templateFramework: templateFramework,
|
||||
vectorDb: options.vectorDb,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
useCase: options.useCase,
|
||||
});
|
||||
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 --ignore-workspace",
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { ChildProcess, exec } from "child_process";
|
||||
import crypto from "node:crypto";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import * as path from "path";
|
||||
import waitPort from "wait-port";
|
||||
import {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateVectorDB,
|
||||
} from "../helpers";
|
||||
|
||||
export type CreateLlamaResult = {
|
||||
projectName: string;
|
||||
appProcess: ChildProcess;
|
||||
};
|
||||
|
||||
export type RunCreateLlamaOptions = {
|
||||
cwd: string;
|
||||
templateFramework: TemplateFramework;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
useCase: string;
|
||||
llamaCloudProjectName?: string;
|
||||
llamaCloudIndexName?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
cwd,
|
||||
templateFramework,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction,
|
||||
useCase,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
}: 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 = [templateFramework, useCase, vectorDb].join("-");
|
||||
const commandArgs = [
|
||||
"create-llama",
|
||||
name,
|
||||
"--framework",
|
||||
templateFramework,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--use-npm",
|
||||
"--port",
|
||||
port,
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--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.error(data.toString());
|
||||
});
|
||||
appProcess.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
throw new Error(`create-llama command failed with exit code ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for app to start
|
||||
if (postInstallAction === "runApp") {
|
||||
await waitPorts([port]);
|
||||
} else if (postInstallAction === "dependencies") {
|
||||
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
|
||||
} else {
|
||||
// wait 10 seconds for create-llama to exit
|
||||
await waitForProcess(appProcess, 1000 * 10);
|
||||
}
|
||||
|
||||
return {
|
||||
projectName: name,
|
||||
appProcess,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTestDir() {
|
||||
const cwd = path.join(__dirname, "cache", crypto.randomUUID());
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { async as glob } from "fast-glob";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -48,3 +47,24 @@ export const copy = async (
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const assetRelocator = (name: string) => {
|
||||
switch (name) {
|
||||
case "gitignore":
|
||||
case "npmrc":
|
||||
case "eslintrc.json": {
|
||||
return `.${name}`;
|
||||
}
|
||||
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
|
||||
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
case "vscode_settings.json": {
|
||||
return "settings.json";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import path from "path";
|
||||
import { templatesDir } from "./dir";
|
||||
import { TemplateDataSource } from "./types";
|
||||
|
||||
export const EXAMPLE_FILE: TemplateDataSource = {
|
||||
type: "file",
|
||||
config: {
|
||||
path: path.join(templatesDir, "components", "data", "101.pdf"),
|
||||
},
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,428 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import {
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateFramework,
|
||||
TemplateType,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
|
||||
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
|
||||
|
||||
export type EnvVar = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const renderEnvVar = (envVars: EnvVar[]): string => {
|
||||
return envVars.reduce(
|
||||
(prev, env) =>
|
||||
prev +
|
||||
(env.description
|
||||
? `# ${env.description.replaceAll("\n", "\n# ")}\n`
|
||||
: "") +
|
||||
(env.name
|
||||
? env.value
|
||||
? `${env.name}=${env.value}\n\n`
|
||||
: `# ${env.name}=\n\n`
|
||||
: ""),
|
||||
"",
|
||||
);
|
||||
};
|
||||
|
||||
const getVectorDBEnvs = (
|
||||
vectorDb?: TemplateVectorDB,
|
||||
framework?: TemplateFramework,
|
||||
template?: TemplateType,
|
||||
): EnvVar[] => {
|
||||
if (!vectorDb || !framework) {
|
||||
return [];
|
||||
}
|
||||
switch (vectorDb) {
|
||||
case "mongo":
|
||||
return [
|
||||
{
|
||||
name: "MONGODB_URI",
|
||||
description:
|
||||
"For generating a connection URI, see https://www.mongodb.com/docs/manual/reference/connection-string/ \nThe MongoDB connection URI.",
|
||||
},
|
||||
{
|
||||
name: "MONGODB_DATABASE",
|
||||
},
|
||||
{
|
||||
name: "MONGODB_VECTORS",
|
||||
},
|
||||
{
|
||||
name: "MONGODB_VECTOR_INDEX",
|
||||
},
|
||||
];
|
||||
case "pg":
|
||||
return [
|
||||
{
|
||||
name: "PG_CONNECTION_STRING",
|
||||
description:
|
||||
"For generating a connection URI, see https://supabase.com/vector\nThe PostgreSQL connection string.",
|
||||
},
|
||||
];
|
||||
|
||||
case "pinecone":
|
||||
return [
|
||||
{
|
||||
name: "PINECONE_API_KEY",
|
||||
description:
|
||||
"Configuration for Pinecone vector store\nThe Pinecone API key.",
|
||||
},
|
||||
{
|
||||
name: "PINECONE_ENVIRONMENT",
|
||||
},
|
||||
{
|
||||
name: "PINECONE_INDEX_NAME",
|
||||
},
|
||||
];
|
||||
case "milvus":
|
||||
return [
|
||||
{
|
||||
name: "MILVUS_ADDRESS",
|
||||
description:
|
||||
"The address of the Milvus server. Eg: http://localhost:19530",
|
||||
value: "http://localhost:19530",
|
||||
},
|
||||
{
|
||||
name: "MILVUS_COLLECTION",
|
||||
description:
|
||||
"The name of the Milvus collection to store the vectors.",
|
||||
value: "llamacollection",
|
||||
},
|
||||
{
|
||||
name: "MILVUS_USERNAME",
|
||||
description: "The username to access the Milvus server.",
|
||||
},
|
||||
{
|
||||
name: "MILVUS_PASSWORD",
|
||||
description: "The password to access the Milvus server.",
|
||||
},
|
||||
];
|
||||
case "astra":
|
||||
return [
|
||||
{
|
||||
name: "ASTRA_DB_APPLICATION_TOKEN",
|
||||
description: "The generated app token for your Astra database",
|
||||
},
|
||||
{
|
||||
name: "ASTRA_DB_ENDPOINT",
|
||||
description: "The API endpoint for your Astra database",
|
||||
},
|
||||
{
|
||||
name: "ASTRA_DB_COLLECTION",
|
||||
description: "The name of the collection in your Astra database",
|
||||
},
|
||||
];
|
||||
case "qdrant":
|
||||
return [
|
||||
{
|
||||
name: "QDRANT_URL",
|
||||
description:
|
||||
"The qualified REST URL of the Qdrant server. Eg: http://localhost:6333",
|
||||
},
|
||||
{
|
||||
name: "QDRANT_COLLECTION",
|
||||
description: "The name of Qdrant collection to use.",
|
||||
},
|
||||
{
|
||||
name: "QDRANT_API_KEY",
|
||||
description:
|
||||
"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" && template !== "llamaindexserver"
|
||||
? // 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 = [
|
||||
{
|
||||
name: "CHROMA_COLLECTION",
|
||||
description: "The name of the collection in your Chroma database",
|
||||
},
|
||||
{
|
||||
name: "CHROMA_HOST",
|
||||
description: "The hostname for your Chroma database. Eg: localhost",
|
||||
},
|
||||
{
|
||||
name: "CHROMA_PORT",
|
||||
description: "The port for your Chroma database. Eg: 8000",
|
||||
},
|
||||
];
|
||||
// TS Version doesn't support config local storage path
|
||||
if (framework === "fastapi") {
|
||||
envs.push({
|
||||
name: "CHROMA_PATH",
|
||||
description: `The local path to the Chroma database.
|
||||
Specify this if you are using a local Chroma database.
|
||||
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 template !== "llamaindexserver"
|
||||
? [
|
||||
{
|
||||
name: "STORAGE_CACHE_DIR",
|
||||
description: "The directory to store the local storage cache.",
|
||||
value: ".cache",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
};
|
||||
|
||||
const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
return [
|
||||
{
|
||||
name: "MODEL",
|
||||
description: "The name of LLM model to use.",
|
||||
value: modelConfig.model,
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_MODEL",
|
||||
description: "Name of the embedding model to use.",
|
||||
value: modelConfig.embeddingModel,
|
||||
},
|
||||
{
|
||||
name: "CONVERSATION_STARTERS",
|
||||
description: "The questions to help users get started (multi-line).",
|
||||
},
|
||||
...(modelConfig.provider === "openai"
|
||||
? [
|
||||
{
|
||||
name: "OPENAI_API_KEY",
|
||||
description: "The OpenAI API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
{
|
||||
name: "LLM_TEMPERATURE",
|
||||
description: "Temperature for sampling from the model.",
|
||||
},
|
||||
{
|
||||
name: "LLM_MAX_TOKENS",
|
||||
description: "Maximum number of tokens to generate.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "anthropic"
|
||||
? [
|
||||
{
|
||||
name: "ANTHROPIC_API_KEY",
|
||||
description: "The Anthropic API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "groq"
|
||||
? [
|
||||
{
|
||||
name: "GROQ_API_KEY",
|
||||
description: "The Groq API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "gemini"
|
||||
? [
|
||||
{
|
||||
name: "GOOGLE_API_KEY",
|
||||
description: "The Google API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "ollama"
|
||||
? [
|
||||
{
|
||||
name: "OLLAMA_BASE_URL",
|
||||
description:
|
||||
"The base URL for the Ollama API. Eg: http://127.0.0.1:11434",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(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,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
const getFrameworkEnvs = (
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
): EnvVar[] => {
|
||||
const sPort = port?.toString() || "8000";
|
||||
const result: EnvVar[] = [];
|
||||
if (framework === "fastapi") {
|
||||
result.push(
|
||||
...[
|
||||
{
|
||||
name: "APP_HOST",
|
||||
description: "The address to start the FastAPI app.",
|
||||
value: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
name: "APP_PORT",
|
||||
description: "The port to start the FastAPI app.",
|
||||
value: sPort,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const createBackendEnvFile = async (
|
||||
root: string,
|
||||
opts: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "llamaCloudKey"
|
||||
| "vectorDb"
|
||||
| "modelConfig"
|
||||
| "framework"
|
||||
| "dataSources"
|
||||
| "template"
|
||||
| "port"
|
||||
| "useLlamaParse"
|
||||
>,
|
||||
) => {
|
||||
// Init env values
|
||||
const envFileName = ".env";
|
||||
const envVars: EnvVar[] = [
|
||||
...(opts.useLlamaParse
|
||||
? [
|
||||
{
|
||||
name: "LLAMA_CLOUD_API_KEY",
|
||||
description: `The Llama Cloud API key.`,
|
||||
value: opts.llamaCloudKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...getVectorDBEnvs(opts.vectorDb, opts.framework, opts.template),
|
||||
...getFrameworkEnvs(opts.framework, opts.port),
|
||||
...getModelEnvs(opts.modelConfig),
|
||||
];
|
||||
// Render and write env file
|
||||
const content = renderEnvVar(envVars);
|
||||
await fs.writeFile(path.join(root, envFileName), content);
|
||||
console.log(`Created '${envFileName}' file. Please check the settings.`);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -0,0 +1,192 @@
|
||||
import { callPackageManager } from "./install";
|
||||
|
||||
import path from "path";
|
||||
import picocolors, { cyan } from "picocolors";
|
||||
|
||||
import fsExtra from "fs-extra";
|
||||
import { createBackendEnvFile } from "./env-variables";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { makeDir } from "./make-dir";
|
||||
import { installPythonTemplate } from "./python";
|
||||
import {
|
||||
FileSourceConfig,
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
import { installTSTemplate } from "./typescript";
|
||||
import { isHavingUvLockFile, tryUvRun } from "./uv";
|
||||
|
||||
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 !== undefined &&
|
||||
vectorDb !== "none" &&
|
||||
vectorDb !== "llamacloud"
|
||||
) {
|
||||
missingSettings.push("your Vector DB environment variables");
|
||||
}
|
||||
|
||||
return missingSettings;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
async function generateContextData(
|
||||
framework: TemplateFramework,
|
||||
modelConfig: ModelConfig,
|
||||
dataSources: TemplateDataSource[],
|
||||
packageManager?: PackageManager,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
llamaCloudKey?: string,
|
||||
useLlamaParse?: boolean,
|
||||
) {
|
||||
if (packageManager) {
|
||||
const runGenerate = `${cyan(
|
||||
framework === "fastapi"
|
||||
? "uv run generate"
|
||||
: `${packageManager} run generate`,
|
||||
)}`;
|
||||
|
||||
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 (isHavingUvLockFile()) {
|
||||
console.log(`Running ${runGenerate} to generate the context data.`);
|
||||
const result = tryUvRun("generate");
|
||||
if (!result) {
|
||||
console.log(`Failed to run ${runGenerate}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Generated context data`);
|
||||
return;
|
||||
} else {
|
||||
console.log(
|
||||
picocolors.yellow(
|
||||
`\nWarning: uv.lock not found. Dependency installation might be incomplete. Skipping context generation.\nIf dependencies were installed, try running '${runGenerate}' manually.\n`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(`Running ${runGenerate} to generate the context data.`);
|
||||
|
||||
const shouldRunGenerate = dataSources.length > 0;
|
||||
|
||||
if (shouldRunGenerate) {
|
||||
await callPackageManager(packageManager, true, ["run", "generate"]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const settingsMessage = `After setting ${missingSettings.join(" and ")}, run ${runGenerate} to generate the context data.`;
|
||||
console.log(picocolors.yellow(`\n${settingsMessage}\n\n`));
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (url: string, destPath: string) => {
|
||||
const response = await fetch(url);
|
||||
const fileBuffer = await response.arrayBuffer();
|
||||
await fsExtra.writeFile(destPath, new Uint8Array(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;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const installTemplate = async (props: InstallTemplateArgs) => {
|
||||
process.chdir(props.root);
|
||||
|
||||
if (props.framework === "fastapi") {
|
||||
await installPythonTemplate(props);
|
||||
} else {
|
||||
await installTSTemplate(props);
|
||||
}
|
||||
|
||||
// This is a backend, so we need to copy the test data and create the env file.
|
||||
|
||||
// Copy the environment file to the target directory.
|
||||
await createBackendEnvFile(props.root, props);
|
||||
|
||||
await prepareContextData(
|
||||
props.root,
|
||||
props.dataSources.filter((ds) => ds.type === "file"),
|
||||
);
|
||||
|
||||
if (
|
||||
props.dataSources.length > 0 &&
|
||||
(props.postInstallAction === "runApp" ||
|
||||
props.postInstallAction === "dependencies")
|
||||
) {
|
||||
console.log("\nGenerating context data...\n");
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.modelConfig,
|
||||
props.dataSources,
|
||||
props.packageManager,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
}
|
||||
|
||||
// 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"));
|
||||
};
|
||||
|
||||
export * from "./types";
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import spawn from "cross-spawn";
|
||||
import { yellow } from "picocolors";
|
||||
import type { PackageManager } from "./get-pkg-manager";
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { blue, green } from "picocolors";
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ModelConfig } from "./types";
|
||||
|
||||
export const getGpt41ModelConfig = (): ModelConfig => ({
|
||||
provider: "openai",
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
model: "gpt-4.1",
|
||||
embeddingModel: "text-embedding-3-large",
|
||||
dimensions: 1536,
|
||||
isConfigured(): boolean {
|
||||
return !!process.env.OPENAI_API_KEY;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = [
|
||||
"claude-3-opus",
|
||||
"claude-3-sonnet",
|
||||
"claude-3-haiku",
|
||||
"claude-2.1",
|
||||
"claude-instant-1.2",
|
||||
];
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
|
||||
// TODO: get embedding vector dimensions from the anthropic sdk (currently not supported)
|
||||
// Use huggingface embedding models for now
|
||||
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;
|
||||
|
||||
export async function askAnthropicQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["ANTHROPIC_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message:
|
||||
"Please provide your Anthropic API key (or leave blank to use ANTHROPIC_API_KEY env variable):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions =
|
||||
EMBEDDING_MODELS[
|
||||
embeddingModel as HuggingFaceEmbeddingModelType
|
||||
].dimensions;
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } 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(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: 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;
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = [
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-pro",
|
||||
"gemini-pro-vision",
|
||||
];
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
"embedding-001": { dimensions: 768 },
|
||||
"text-embedding-004": { 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;
|
||||
|
||||
export async function askGeminiQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: process.env.GOOGLE_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["GOOGLE_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message:
|
||||
"Please provide your Google API key (or leave blank to use GOOGLE_API_KEY env variable):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.GOOGLE_API_KEY;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
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;
|
||||
|
||||
export async function askGroqQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
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;
|
||||
}
|
||||
|
||||
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,60 @@
|
||||
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;
|
||||
|
||||
export async function askHuggingfaceQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import prompts from "prompts";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
import { ModelConfig, 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";
|
||||
|
||||
export type ModelConfigQuestionsParams = {
|
||||
framework?: TemplateFramework;
|
||||
};
|
||||
|
||||
export type ModelConfigParams = Omit<ModelConfig, "provider">;
|
||||
|
||||
export async function askModelConfig({
|
||||
framework,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
|
||||
const choices = [
|
||||
{ title: "OpenAI", value: "openai" },
|
||||
{ title: "Groq", value: "groq" },
|
||||
{ title: "Ollama", value: "ollama" },
|
||||
{ title: "Anthropic", value: "anthropic" },
|
||||
{ title: "Gemini", value: "gemini" },
|
||||
{ title: "Mistral", value: "mistral" },
|
||||
{ title: "AzureOpenAI", value: "azure-openai" },
|
||||
];
|
||||
|
||||
if (framework === "fastapi") {
|
||||
choices.push({ title: "T-Systems", value: "t-systems" });
|
||||
choices.push({ title: "Huggingface", value: "huggingface" });
|
||||
}
|
||||
const { provider: modelProvider } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "provider",
|
||||
message: "Which model provider would you like to use",
|
||||
choices: choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
let modelConfig: ModelConfigParams;
|
||||
switch (modelProvider) {
|
||||
case "ollama":
|
||||
modelConfig = await askOllamaQuestions();
|
||||
break;
|
||||
case "groq":
|
||||
modelConfig = await askGroqQuestions();
|
||||
break;
|
||||
case "anthropic":
|
||||
modelConfig = await askAnthropicQuestions();
|
||||
break;
|
||||
case "gemini":
|
||||
modelConfig = await askGeminiQuestions();
|
||||
break;
|
||||
case "mistral":
|
||||
modelConfig = await askMistralQuestions();
|
||||
break;
|
||||
case "azure-openai":
|
||||
modelConfig = await askAzureQuestions();
|
||||
break;
|
||||
case "t-systems":
|
||||
modelConfig = await askLLMHubQuestions();
|
||||
break;
|
||||
case "huggingface":
|
||||
modelConfig = await askHuggingfaceQuestions();
|
||||
break;
|
||||
default:
|
||||
modelConfig = await askOpenAIQuestions();
|
||||
}
|
||||
return {
|
||||
...modelConfig,
|
||||
provider: modelProvider,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
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",
|
||||
];
|
||||
|
||||
export async function askLLMHubQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: process.env.T_SYSTEMS_LLMHUB_API_KEY,
|
||||
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:
|
||||
"Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):",
|
||||
validate: (value: string) => {
|
||||
if (!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;
|
||||
}
|
||||
|
||||
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,73 @@
|
||||
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;
|
||||
|
||||
export async function askMistralQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: process.env.MISTRAL_API_KEY,
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import ollama, { type ModelResponse } from "ollama";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const MODELS = ["llama3:8b", "wizardlm2:7b", "gemma:7b", "phi3"];
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
// TODO: get embedding vector dimensions from the ollama sdk (currently not supported)
|
||||
const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
"nomic-embed-text": { dimensions: 768 },
|
||||
"mxbai-embed-large": { dimensions: 1024 },
|
||||
"all-minilm": { dimensions: 384 },
|
||||
};
|
||||
const DEFAULT_EMBEDDING_MODEL: string = Object.keys(EMBEDDING_MODELS)[0];
|
||||
|
||||
export async function askOllamaQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: EMBEDDING_MODELS[DEFAULT_EMBEDDING_MODEL].dimensions,
|
||||
isConfigured(): boolean {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
await ensureModel(model);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
await ensureModel(embeddingModel);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function ensureModel(modelName: string) {
|
||||
try {
|
||||
if (modelName.split(":").length === 1) {
|
||||
// model doesn't have a version suffix, use latest
|
||||
modelName = modelName + ":latest";
|
||||
}
|
||||
const { models } = await ollama.list();
|
||||
const found =
|
||||
models.find((model: ModelResponse) => model.name === modelName) !==
|
||||
undefined;
|
||||
if (!found) {
|
||||
console.log(
|
||||
red(
|
||||
`Model ${modelName} was not pulled yet. Call 'ollama pull ${modelName}' and try again.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
red("Listing Ollama models failed. Is 'ollama' running? " + error),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
|
||||
const DEFAULT_MODEL = "gpt-4o-mini";
|
||||
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
|
||||
|
||||
export async function askOpenAIQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["OPENAI_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message:
|
||||
"Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):",
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
return "OPENAI_API_KEY env variable is not set - key is required";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
|
||||
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 OpenAI key to retrieve model choices");
|
||||
}
|
||||
const isLLMModel = (modelId: string) => {
|
||||
return modelId.startsWith("gpt");
|
||||
};
|
||||
|
||||
const isEmbeddingModel = (modelId: string) => {
|
||||
return modelId.includes("embedding");
|
||||
};
|
||||
|
||||
const spinner = ora("Fetching available models").start();
|
||||
try {
|
||||
const response = await got(`${OPENAI_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 OpenAI 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) {
|
||||
// at 2024-04-24 all OpenAI embedding models support 1536 dimensions except
|
||||
// "text-embedding-3-large", see https://openai.com/blog/new-embedding-models-and-api-updates
|
||||
return modelName === "text-embedding-3-large" ? 1024 : 1536;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Function to conditionally load the global-agent/bootstrap module */
|
||||
export async function initializeGlobalAgent() {
|
||||
if (process.env.GLOBAL_AGENT_HTTP_PROXY) {
|
||||
/* Dynamically import global-agent/bootstrap */
|
||||
await import("global-agent/bootstrap");
|
||||
console.log("Proxy enabled via global-agent.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { cyan, red } from "picocolors";
|
||||
import { parse, stringify } from "smol-toml";
|
||||
import terminalLink from "terminal-link";
|
||||
import { isUvAvailable, tryUvSync } from "./uv";
|
||||
|
||||
import { assetRelocator, copy } from "./copy";
|
||||
import { templatesDir } from "./dir";
|
||||
import {
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
|
||||
interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
constraints?: Record<string, string>;
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
modelConfig: ModelConfig,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
dataSources?: TemplateDataSource[],
|
||||
) => {
|
||||
const dependencies: Dependency[] = [];
|
||||
|
||||
// Add vector db dependencies
|
||||
switch (vectorDb) {
|
||||
case "mongo": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-mongodb",
|
||||
version: ">=0.3.2,<0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pg": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-postgres",
|
||||
version: ">=0.3.2,<0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pinecone": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: ">=0.4.1,<0.5.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "milvus": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-milvus",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymilvus",
|
||||
version: ">=2.4.4,<3.0.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "astra": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-astra-db",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "qdrant": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-qdrant",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
constraints: {
|
||||
python: ">=3.11,<3.13",
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "chroma": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-chroma",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "onnxruntime",
|
||||
version: "<1.22.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-weaviate",
|
||||
version: ">=1.2.3,<2.0.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "llamacloud":
|
||||
dependencies.push({
|
||||
name: "llama-index-indices-managed-llama-cloud",
|
||||
version: ">=0.6.3,<0.7.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Add data source dependencies
|
||||
if (dataSources) {
|
||||
for (const ds of dataSources) {
|
||||
const dsType = ds?.type;
|
||||
switch (dsType) {
|
||||
case "file":
|
||||
dependencies.push({
|
||||
name: "docx2txt",
|
||||
version: ">=0.8,<0.9",
|
||||
});
|
||||
break;
|
||||
case "web":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-web",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
break;
|
||||
case "db":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-database",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymysql",
|
||||
version: ">=1.1.0,<2.0.0",
|
||||
extras: ["rsa"],
|
||||
});
|
||||
dependencies.push({
|
||||
name: "psycopg2-binary",
|
||||
version: ">=2.9.9,<3.0.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (modelConfig.provider) {
|
||||
case "ollama":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-ollama",
|
||||
version: ">=0.5.0,<0.6.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-ollama",
|
||||
version: ">=0.6.0,<0.7.0",
|
||||
});
|
||||
break;
|
||||
case "openai":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-openai",
|
||||
version: ">=0.3.2,<0.4.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-openai",
|
||||
version: ">=0.3.1,<0.4.0",
|
||||
});
|
||||
break;
|
||||
case "groq":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-groq",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-fastembed",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
break;
|
||||
case "anthropic":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-anthropic",
|
||||
version: ">=0.6.0,<0.7.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-fastembed",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
break;
|
||||
case "gemini":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-google-genai",
|
||||
version: ">=0.2.0,<0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-google-genai",
|
||||
version: ">=0.2.0,<0.3.0",
|
||||
});
|
||||
break;
|
||||
case "mistral":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-mistralai",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-mistralai",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
break;
|
||||
case "azure-openai":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-azure-openai",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-azure-openai",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
break;
|
||||
case "huggingface":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-huggingface",
|
||||
version: ">=0.5.0,<0.6.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-huggingface",
|
||||
version: ">=0.5.0,<0.6.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "optimum",
|
||||
version: ">=1.23.3,<2.0.0",
|
||||
extras: ["onnxruntime"],
|
||||
});
|
||||
break;
|
||||
case "t-systems":
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-openai-like",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// If app template is llama-index-server and CI and SERVER_PACKAGE_PATH is set,
|
||||
// add @llamaindex/server to dependencies
|
||||
if (process.env.SERVER_PACKAGE_PATH) {
|
||||
dependencies.push({
|
||||
name: "llama-index-server",
|
||||
version: `@file://${process.env.SERVER_PACKAGE_PATH}`,
|
||||
});
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
};
|
||||
|
||||
export const addDependencies = async (
|
||||
projectDir: string,
|
||||
dependencies: Dependency[],
|
||||
) => {
|
||||
if (dependencies.length === 0) return;
|
||||
|
||||
const FILENAME = "pyproject.toml";
|
||||
try {
|
||||
// Parse toml file
|
||||
const file = path.join(projectDir, FILENAME);
|
||||
const fileContent = await fs.readFile(file, "utf8");
|
||||
let fileParsed: any;
|
||||
try {
|
||||
fileParsed = parse(fileContent);
|
||||
} catch (parseError) {
|
||||
console.error(`Error parsing ${FILENAME}:`, parseError);
|
||||
throw new Error(
|
||||
`Failed to parse ${FILENAME}. Please ensure it's valid TOML.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure [project] and [project.dependencies] exist
|
||||
if (!fileParsed.project) {
|
||||
fileParsed.project = {};
|
||||
}
|
||||
if (
|
||||
!fileParsed.project.dependencies ||
|
||||
!Array.isArray(fileParsed.project.dependencies)
|
||||
) {
|
||||
// If dependencies exist but aren't an array, log a warning or error.
|
||||
// For now, we'll overwrite it, assuming the intent is to use the standard array format.
|
||||
console.warn(
|
||||
`[project.dependencies] in ${FILENAME} is not an array. It will be overwritten.`,
|
||||
);
|
||||
fileParsed.project.dependencies = [];
|
||||
}
|
||||
|
||||
const existingDependencies: string[] = fileParsed.project.dependencies;
|
||||
const addedDeps: string[] = [];
|
||||
const updatedDeps: string[] = [];
|
||||
|
||||
// Add or update dependencies
|
||||
for (const newDep of dependencies) {
|
||||
let depString = newDep.name;
|
||||
if (newDep.extras && newDep.extras.length > 0) {
|
||||
depString += `[${newDep.extras.join(",")}]`;
|
||||
}
|
||||
if (newDep.version) {
|
||||
depString += newDep.version;
|
||||
}
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < existingDependencies.length; i++) {
|
||||
const existingDepNameMatch =
|
||||
existingDependencies[i].match(/^([a-zA-Z0-9._-]+)/);
|
||||
if (
|
||||
existingDepNameMatch &&
|
||||
existingDepNameMatch[1].toLowerCase() === depString.toLowerCase()
|
||||
) {
|
||||
// Found existing dependency, update it
|
||||
if (existingDependencies[i] !== depString) {
|
||||
updatedDeps.push(`${existingDependencies[i]} -> ${depString}`);
|
||||
existingDependencies[i] = depString;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// Add new dependency
|
||||
existingDependencies.push(depString);
|
||||
addedDeps.push(depString);
|
||||
}
|
||||
// Handle python version constraints separately (if any)
|
||||
if (newDep.constraints?.python) {
|
||||
if (
|
||||
!fileParsed.project["requires-python"] ||
|
||||
fileParsed.project["requires-python"] !== newDep.constraints.python
|
||||
) {
|
||||
// This simple overwrite might not be ideal; merging constraints is complex.
|
||||
// For now, let's just set it if the new dependency has one.
|
||||
console.log(
|
||||
`Setting requires-python = "${newDep.constraints.python}" from dependency ${newDep.name}`,
|
||||
);
|
||||
fileParsed.project["requires-python"] = newDep.constraints.python;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write toml file
|
||||
const newFileContent = stringify(fileParsed);
|
||||
await fs.writeFile(file, newFileContent);
|
||||
|
||||
if (addedDeps.length > 0) {
|
||||
console.log(`\nAdded dependencies to ${cyan(FILENAME)}:`);
|
||||
addedDeps.forEach((dep) => console.log(` ${dep}`));
|
||||
}
|
||||
if (updatedDeps.length > 0) {
|
||||
console.log(`\nUpdated dependencies in ${cyan(FILENAME)}:`);
|
||||
updatedDeps.forEach((dep) => console.log(` ${dep}`));
|
||||
}
|
||||
if (addedDeps.length > 0 || updatedDeps.length > 0) {
|
||||
console.log(""); // Newline for spacing
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Error while updating dependencies for Poetry project file ${FILENAME}\n`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const installPythonDependencies = () => {
|
||||
if (isUvAvailable()) {
|
||||
console.log(
|
||||
`Installing Python dependencies using uv. This may take a while...`,
|
||||
);
|
||||
const installSuccessful = tryUvSync();
|
||||
if (!installSuccessful) {
|
||||
console.error(
|
||||
red(
|
||||
"Installing dependencies using uv failed. Please check the error log above and ensure uv is installed correctly.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
red(
|
||||
`uv is not available in the current environment. Please check ${terminalLink(
|
||||
"uv Installation",
|
||||
`https://github.com/astral-sh/uv#installation`,
|
||||
)} to install uv first, then run create-llama again.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const installLlamaIndexServerTemplate = async ({
|
||||
root,
|
||||
useCase,
|
||||
useLlamaParse,
|
||||
modelConfig,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "useCase" | "useLlamaParse" | "modelConfig"
|
||||
>) => {
|
||||
if (!useCase) {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await copy("*.py", path.join(root, "app"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
|
||||
});
|
||||
|
||||
// copy model provider settings to app folder
|
||||
await copy("**", path.join(root, "app"), {
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"providers",
|
||||
"python",
|
||||
modelConfig.provider,
|
||||
),
|
||||
});
|
||||
|
||||
// Copy custom UI component code
|
||||
await copy(`*`, path.join(root, "components"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
|
||||
});
|
||||
|
||||
// Copy layout components to layout folder in root
|
||||
await copy("*", path.join(root, "layout"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "layout"),
|
||||
});
|
||||
|
||||
if (useLlamaParse) {
|
||||
await copy("index.py", path.join(root, "app"), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"python",
|
||||
),
|
||||
});
|
||||
// TODO: Consider moving generate.py to app folder.
|
||||
await copy("generate.py", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"python",
|
||||
),
|
||||
});
|
||||
}
|
||||
// Copy README.md
|
||||
await copy("README-template.md", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
};
|
||||
|
||||
export const installPythonTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
modelConfig,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "appName"
|
||||
| "root"
|
||||
| "template"
|
||||
| "framework"
|
||||
| "vectorDb"
|
||||
| "postInstallAction"
|
||||
| "modelConfig"
|
||||
| "dataSources"
|
||||
| "useLlamaParse"
|
||||
| "useCase"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
if (template === "llamaindexserver") {
|
||||
await installLlamaIndexServerTemplate({
|
||||
root,
|
||||
useCase,
|
||||
useLlamaParse,
|
||||
modelConfig,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Template ${template} not supported`);
|
||||
}
|
||||
|
||||
console.log("Adding additional dependencies");
|
||||
const addOnDependencies = getAdditionalDependencies(
|
||||
modelConfig,
|
||||
vectorDb,
|
||||
dataSources,
|
||||
);
|
||||
|
||||
await addDependencies(root, addOnDependencies);
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
installPythonDependencies();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { SpawnOptions, spawn } from "child_process";
|
||||
import { TemplateFramework, TemplateType } from "./types";
|
||||
|
||||
const createProcess = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: SpawnOptions,
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
spawn(command, args, {
|
||||
...options,
|
||||
shell: true,
|
||||
})
|
||||
.on("exit", function (code) {
|
||||
if (code !== 0) {
|
||||
console.log(`Child process exited with code=${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.log("Error when running child process: ", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function runFastAPIApp(
|
||||
appPath: string,
|
||||
port: number,
|
||||
template: TemplateType,
|
||||
) {
|
||||
const commandArgs = ["run", "fastapi", "dev", "--port", `${port}`];
|
||||
return createProcess("uv", commandArgs, {
|
||||
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,
|
||||
template: TemplateType,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort = framework === "nextjs" ? 3000 : 8000;
|
||||
|
||||
const appRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
|
||||
await appRunner(appPath, port || defaultPort, template);
|
||||
} catch (error) {
|
||||
console.error("Failed to run app:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
|
||||
export type ModelProvider =
|
||||
| "openai"
|
||||
| "groq"
|
||||
| "ollama"
|
||||
| "anthropic"
|
||||
| "gemini"
|
||||
| "mistral"
|
||||
| "azure-openai"
|
||||
| "huggingface"
|
||||
| "t-systems";
|
||||
export type ModelConfig = {
|
||||
provider: ModelProvider;
|
||||
apiKey?: string;
|
||||
model: string;
|
||||
embeddingModel: string;
|
||||
dimensions: number;
|
||||
isConfigured(): boolean;
|
||||
};
|
||||
export type TemplateType = "llamaindexserver";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateVectorDB =
|
||||
| "none"
|
||||
| "mongo"
|
||||
| "pg"
|
||||
| "pinecone"
|
||||
| "milvus"
|
||||
| "astra"
|
||||
| "qdrant"
|
||||
| "chroma"
|
||||
| "llamacloud"
|
||||
| "weaviate";
|
||||
export type TemplatePostInstallAction =
|
||||
| "none"
|
||||
| "VSCode"
|
||||
| "dependencies"
|
||||
| "runApp";
|
||||
export type TemplateDataSource = {
|
||||
type: TemplateDataSourceType;
|
||||
config: TemplateDataSourceConfig;
|
||||
};
|
||||
export type TemplateDataSourceType = "file" | "web" | "db";
|
||||
export type TemplateUseCase =
|
||||
| "financial_report"
|
||||
| "deep_research"
|
||||
| "agentic_rag"
|
||||
| "code_generator"
|
||||
| "document_generator"
|
||||
| "hitl";
|
||||
|
||||
export const ALL_USE_CASES: TemplateUseCase[] = [
|
||||
"agentic_rag",
|
||||
"deep_research",
|
||||
"financial_report",
|
||||
"code_generator",
|
||||
"document_generator",
|
||||
"hitl",
|
||||
];
|
||||
// Config for both file and folder
|
||||
export type FileSourceConfig =
|
||||
| {
|
||||
path: string;
|
||||
filename?: string;
|
||||
}
|
||||
| {
|
||||
url: URL;
|
||||
filename?: string;
|
||||
};
|
||||
export type WebSourceConfig = {
|
||||
baseUrl?: string;
|
||||
prefix?: string;
|
||||
depth?: number;
|
||||
};
|
||||
export type DbSourceConfig = {
|
||||
uri?: string;
|
||||
queries?: string;
|
||||
};
|
||||
|
||||
export type TemplateDataSourceConfig =
|
||||
| FileSourceConfig
|
||||
| WebSourceConfig
|
||||
| DbSourceConfig;
|
||||
|
||||
export interface InstallTemplateArgs {
|
||||
appName: string;
|
||||
root: string;
|
||||
packageManager: PackageManager;
|
||||
template: TemplateType;
|
||||
framework: TemplateFramework;
|
||||
dataSources: TemplateDataSource[];
|
||||
modelConfig: ModelConfig;
|
||||
llamaCloudKey?: string;
|
||||
useLlamaParse: boolean;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port?: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
useCase: TemplateUseCase;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bold, cyan, red } from "picocolors";
|
||||
import { assetRelocator, copy } from "../helpers/copy";
|
||||
import { callPackageManager } from "../helpers/install";
|
||||
import { templatesDir } from "./dir";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { InstallTemplateArgs, ModelProvider, TemplateVectorDB } from "./types";
|
||||
|
||||
const installLlamaIndexServerTemplate = async ({
|
||||
root,
|
||||
useCase,
|
||||
vectorDb,
|
||||
modelConfig,
|
||||
dataSources,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "useCase" | "vectorDb" | "modelConfig" | "dataSources"
|
||||
>) => {
|
||||
if (!useCase) {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!vectorDb) {
|
||||
console.log(
|
||||
red(
|
||||
`There is no vector db selected. Please pick a vector db to use via --vector-db flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// copy model provider settings to app folder
|
||||
await copy("**", path.join(root, "src", "app"), {
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"providers",
|
||||
"typescript",
|
||||
modelConfig.provider,
|
||||
),
|
||||
});
|
||||
|
||||
await copy("**", path.join(root), {
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"use-cases",
|
||||
"typescript",
|
||||
useCase,
|
||||
),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// copy workflow UI components to components folder in root
|
||||
await copy("*", path.join(root, "components"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
|
||||
});
|
||||
|
||||
// copy layout components to layout folder in root
|
||||
await copy("*", path.join(root, "layout"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "layout"),
|
||||
});
|
||||
|
||||
// Override generate.ts if workflow use case doesn't use custom UI
|
||||
if (vectorDb === "llamacloud") {
|
||||
await copy("**", path.join(root, "src"), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"typescript",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Simplify use case code
|
||||
if (vectorDb === "none" && dataSources.length === 0) {
|
||||
// use case without data sources doesn't use index.
|
||||
// We don't need data.ts, generate.ts
|
||||
await fs.rm(path.join(root, "src", "app", "data.ts"));
|
||||
// TODO: split generate.ts into generate for index and generate for ui and remove generate for index here too
|
||||
// then we can also remove it for llamacloud
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Install a LlamaIndex internal template to a given `root` directory.
|
||||
*/
|
||||
export const installTSTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
packageManager,
|
||||
template,
|
||||
framework,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
useCase,
|
||||
modelConfig,
|
||||
}: InstallTemplateArgs) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
/**
|
||||
* 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 copySource = ["**"];
|
||||
|
||||
await copy(copySource, root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
if (template === "llamaindexserver") {
|
||||
await installLlamaIndexServerTemplate({
|
||||
root,
|
||||
useCase,
|
||||
vectorDb,
|
||||
modelConfig,
|
||||
dataSources,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Template ${template} not supported`);
|
||||
}
|
||||
|
||||
const packageJson = await updatePackageJson({
|
||||
root,
|
||||
appName,
|
||||
vectorDb,
|
||||
modelConfig,
|
||||
});
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
await installTSDependencies(packageJson, packageManager, true);
|
||||
}
|
||||
};
|
||||
|
||||
const providerDependencies: {
|
||||
[key in ModelProvider]?: Record<string, string>;
|
||||
} = {
|
||||
openai: {
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
},
|
||||
gemini: {
|
||||
"@llamaindex/google": "^0.2.0",
|
||||
},
|
||||
ollama: {
|
||||
"@llamaindex/ollama": "^0.1.0",
|
||||
},
|
||||
mistral: {
|
||||
"@llamaindex/mistral": "^0.2.0",
|
||||
},
|
||||
"azure-openai": {
|
||||
"@llamaindex/openai": "^0.2.0",
|
||||
},
|
||||
groq: {
|
||||
"@llamaindex/groq": "^0.0.61",
|
||||
"@llamaindex/huggingface": "^0.1.0", // groq uses huggingface as default embedding model
|
||||
},
|
||||
anthropic: {
|
||||
"@llamaindex/anthropic": "^0.3.0",
|
||||
"@llamaindex/huggingface": "^0.1.0", // anthropic uses huggingface as default embedding model
|
||||
},
|
||||
};
|
||||
|
||||
const vectorDbDependencies: Record<TemplateVectorDB, Record<string, string>> = {
|
||||
astra: {
|
||||
"@llamaindex/astra": "^0.0.5",
|
||||
},
|
||||
chroma: {
|
||||
"@llamaindex/chroma": "^0.0.5",
|
||||
},
|
||||
llamacloud: {},
|
||||
milvus: {
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.6",
|
||||
"@llamaindex/milvus": "^0.1.0",
|
||||
},
|
||||
mongo: {
|
||||
mongodb: "6.7.0",
|
||||
"@llamaindex/mongodb": "^0.0.5",
|
||||
},
|
||||
none: {},
|
||||
pg: {
|
||||
pg: "^8.12.0",
|
||||
pgvector: "^0.2.0",
|
||||
"@llamaindex/postgres": "^0.0.33",
|
||||
},
|
||||
pinecone: {
|
||||
"@llamaindex/pinecone": "^0.0.5",
|
||||
},
|
||||
qdrant: {
|
||||
"@qdrant/js-client-rest": "^1.11.0",
|
||||
"@llamaindex/qdrant": "^0.1.0",
|
||||
},
|
||||
weaviate: {
|
||||
"@llamaindex/weaviate": "^0.0.5",
|
||||
},
|
||||
};
|
||||
|
||||
async function updatePackageJson({
|
||||
root,
|
||||
appName,
|
||||
vectorDb,
|
||||
modelConfig,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "appName" | "vectorDb" | "modelConfig"
|
||||
>): Promise<any> {
|
||||
const packageJsonFile = path.join(root, "package.json");
|
||||
const packageJson: any = JSON.parse(
|
||||
await fs.readFile(packageJsonFile, "utf8"),
|
||||
);
|
||||
packageJson.name = appName;
|
||||
packageJson.version = "0.1.0";
|
||||
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/readers": "~3.1.4",
|
||||
};
|
||||
|
||||
if (vectorDb && vectorDb in vectorDbDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...vectorDbDependencies[vectorDb],
|
||||
};
|
||||
}
|
||||
|
||||
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...providerDependencies[modelConfig.provider],
|
||||
};
|
||||
}
|
||||
|
||||
// if having custom server package tgz file, use it for testing @llamaindex/server
|
||||
const serverPackagePath = process.env.SERVER_PACKAGE_PATH;
|
||||
if (serverPackagePath) {
|
||||
const relativePath = path.relative(process.cwd(), serverPackagePath);
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/server": `file:${relativePath}`,
|
||||
};
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
packageJsonFile,
|
||||
JSON.stringify(packageJson, null, 2) + os.EOL,
|
||||
);
|
||||
|
||||
return packageJson;
|
||||
}
|
||||
|
||||
async function installTSDependencies(
|
||||
packageJson: any,
|
||||
packageManager: PackageManager,
|
||||
isOnline: boolean,
|
||||
): Promise<void> {
|
||||
console.log("\nInstalling dependencies:");
|
||||
for (const dependency in packageJson.dependencies)
|
||||
console.log(`- ${cyan(dependency)}`);
|
||||
|
||||
console.log("\nInstalling devDependencies:");
|
||||
for (const dependency in packageJson.devDependencies)
|
||||
console.log(`- ${cyan(dependency)}`);
|
||||
|
||||
console.log();
|
||||
|
||||
await callPackageManager(packageManager, isOnline).catch((error) => {
|
||||
console.error("Failed to install TS dependencies. Exiting...");
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Migrate poetry to uv
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import { red } from "picocolors";
|
||||
|
||||
export function isUvAvailable(): boolean {
|
||||
try {
|
||||
execSync("uv --version", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tryUvSync(): boolean {
|
||||
try {
|
||||
console.log("Syncing environment with pyproject.toml...");
|
||||
execSync(`uv sync`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tryUvRun(command: string): boolean {
|
||||
try {
|
||||
// Use uv run <command>
|
||||
execSync(`uv run ${command}`, { stdio: "inherit" });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(red(`Failed to run ${command}. Error: ${error}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isHavingUvLockFile(): boolean {
|
||||
try {
|
||||
// Check if uv.lock exists in the current directory
|
||||
return fs.existsSync("uv.lock");
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import validateProjectName from "validate-npm-package-name";
|
||||
|
||||
export function validateNpmName(name: string): {
|
||||
@@ -0,0 +1,70 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { assetRelocator, copy } from "./copy";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
function renderDevcontainerContent(
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
) {
|
||||
const devcontainerJson: any = JSON.parse(
|
||||
fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
|
||||
);
|
||||
|
||||
// Modify postCreateCommand
|
||||
devcontainerJson.postCreateCommand =
|
||||
framework === "fastapi" ? "poetry install" : "npm install";
|
||||
|
||||
// Modify containerEnv
|
||||
if (framework === "fastapi") {
|
||||
devcontainerJson.containerEnv = {
|
||||
...devcontainerJson.containerEnv,
|
||||
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(devcontainerJson, null, 2);
|
||||
}
|
||||
|
||||
export const writeDevcontainer = async (
|
||||
root: string,
|
||||
templatesDir: string,
|
||||
framework: TemplateFramework,
|
||||
) => {
|
||||
const devcontainerDir = path.join(root, ".devcontainer");
|
||||
if (fs.existsSync(devcontainerDir)) {
|
||||
console.log("Template already has a .devcontainer. Using it.");
|
||||
return;
|
||||
}
|
||||
const devcontainerContent = renderDevcontainerContent(
|
||||
templatesDir,
|
||||
framework,
|
||||
);
|
||||
fs.mkdirSync(devcontainerDir);
|
||||
await fs.promises.writeFile(
|
||||
path.join(devcontainerDir, "devcontainer.json"),
|
||||
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,8 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
/* 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";
|
||||
@@ -12,11 +9,15 @@ import checkForUpdate from "update-check";
|
||||
import { createApp } from "./create-app";
|
||||
import { getPkgManager } from "./helpers/get-pkg-manager";
|
||||
import { isFolderEmpty } from "./helpers/is-folder-empty";
|
||||
import { initializeGlobalAgent } from "./helpers/proxy";
|
||||
import { runApp } from "./helpers/run-app";
|
||||
import { getTools } from "./helpers/tools";
|
||||
import { validateNpmName } from "./helpers/validate-pkg";
|
||||
import packageJson from "./package.json";
|
||||
import { 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();
|
||||
|
||||
let projectPath: string = "";
|
||||
|
||||
@@ -25,20 +26,15 @@ 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(
|
||||
"--eslint",
|
||||
`
|
||||
|
||||
Initialize with eslint config.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--use-npm",
|
||||
`
|
||||
@@ -58,27 +54,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(
|
||||
"--template <template>",
|
||||
`
|
||||
|
||||
Select a template to bootstrap the application with.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--engine <engine>",
|
||||
`
|
||||
|
||||
Select a chat engine to bootstrap the application with.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -86,47 +61,6 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select a framework to bootstrap the application with.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--files <path>",
|
||||
`
|
||||
|
||||
Specify the path to a local file or folder for chatting.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--open-ai-key <key>",
|
||||
`
|
||||
|
||||
Provide an OpenAI API key.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--ui <ui>",
|
||||
`
|
||||
|
||||
Select a UI to bootstrap the application with.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--frontend",
|
||||
`
|
||||
|
||||
Whether to generate a frontend for your backend.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--model <model>",
|
||||
`
|
||||
|
||||
Select OpenAI model to use. E.g. gpt-3.5-turbo.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--embedding-model <embeddingModel>",
|
||||
`
|
||||
Select OpenAI embedding model to use. E.g. text-embedding-ada-002.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -134,13 +68,6 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select UI port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--external-port <external>",
|
||||
`
|
||||
|
||||
Select external port.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -155,71 +82,48 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select which vector database you would like to use, such as 'none', 'pg' or 'mongo'. The default option is not to use a vector database and use the local filesystem instead ('none').
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--tools <tools>",
|
||||
`
|
||||
|
||||
Specify the tools you want to use by providing a comma-separated list. For example, 'wikipedia.WikipediaToolSpec,google.GoogleSearchToolSpec'. Use 'none' to not using any tools.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--llama-parse",
|
||||
`
|
||||
Enable LlamaParse.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--llama-cloud-key <key>",
|
||||
`
|
||||
|
||||
Provide a LlamaCloud API key.
|
||||
`,
|
||||
)
|
||||
|
||||
.option(
|
||||
"--list-server-models",
|
||||
"Fetch available LLM and embedding models from OpenAI API.",
|
||||
"--ask-models",
|
||||
`
|
||||
|
||||
Allow interactive selection of LLM and embedding models of different model providers.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--observability <observability>",
|
||||
"Specify observability tools to use. Eg: none, opentelemetry",
|
||||
"--use-case <useCase>",
|
||||
`
|
||||
|
||||
Select which use case to use for the template (e.g: financial_report, blog).
|
||||
`,
|
||||
)
|
||||
.allowUnknownOption()
|
||||
.parse(process.argv);
|
||||
if (process.argv.includes("--no-frontend")) {
|
||||
program.frontend = false;
|
||||
}
|
||||
if (process.argv.includes("--no-eslint")) {
|
||||
program.eslint = 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.llamaParse = false;
|
||||
}
|
||||
|
||||
const packageManager = !!program.useNpm
|
||||
const options = program.opts();
|
||||
|
||||
const packageManager = !!options.useNpm
|
||||
? "npm"
|
||||
: !!program.usePnpm
|
||||
: !!options.usePnpm
|
||||
? "pnpm"
|
||||
: !!program.useYarn
|
||||
: !!options.useYarn
|
||||
? "yarn"
|
||||
: getPkgManager();
|
||||
|
||||
// options above must use all the properties of QuestionArgs
|
||||
const cliArgs = options as unknown as QuestionArgs;
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -282,34 +186,15 @@ async function run(): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const preferences = (conf.get("preferences") || {}) as QuestionArgs;
|
||||
await askQuestions(program as unknown as QuestionArgs, preferences);
|
||||
const answers = await askQuestions(cliArgs);
|
||||
|
||||
await createApp({
|
||||
template: program.template,
|
||||
framework: program.framework,
|
||||
engine: program.engine,
|
||||
ui: program.ui,
|
||||
...answers,
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
eslint: program.eslint,
|
||||
frontend: program.frontend,
|
||||
openAiKey: program.openAiKey,
|
||||
llamaCloudKey: program.llamaCloudKey,
|
||||
model: program.model,
|
||||
embeddingModel: program.embeddingModel,
|
||||
communityProjectConfig: program.communityProjectConfig,
|
||||
llamapack: program.llamapack,
|
||||
vectorDb: program.vectorDb,
|
||||
externalPort: program.externalPort,
|
||||
postInstallAction: program.postInstallAction,
|
||||
dataSources: program.dataSources,
|
||||
tools: program.tools,
|
||||
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`, {
|
||||
@@ -333,15 +218,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.6.1",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
"llamaindex",
|
||||
"next.js"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/create-llama",
|
||||
"directory": "packages/create-llama"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"create-llama": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE.md"
|
||||
],
|
||||
"scripts": {
|
||||
"copy": "cp -r ../../README.md ../../LICENSE.md .",
|
||||
"build": "bash ./scripts/build.sh",
|
||||
"build:ncc": "pnpm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
|
||||
"postbuild": "pnpm run copy",
|
||||
"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:ts": "playwright test e2e/shared e2e/typescript",
|
||||
"pack-install": "bash ./scripts/pack.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/async-retry": "1.4.2",
|
||||
"@types/ci-info": "2.0.0",
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "^20.11.7",
|
||||
"@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",
|
||||
"commander": "12.1.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"fast-glob": "3.3.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "10.7.0",
|
||||
"ollama": "^0.5.0",
|
||||
"ora": "^8.0.1",
|
||||
"picocolors": "1.0.0",
|
||||
"prompts": "2.4.2",
|
||||
"smol-toml": "^1.1.4",
|
||||
"tar": "6.1.15",
|
||||
"terminal-link": "^3.0.0",
|
||||
"update-check": "1.5.4",
|
||||
"validate-npm-package-name": "3.0.0",
|
||||
"yaml": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"wait-port": "^1.1.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.5",
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
@@ -0,0 +1,149 @@
|
||||
import prompts from "prompts";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import {
|
||||
TemplateFramework,
|
||||
TemplateUseCase,
|
||||
TemplateVectorDB,
|
||||
} from "../helpers/types";
|
||||
import { QuestionArgs, QuestionResults } from "./types";
|
||||
import { useCaseConfiguration } from "./usecases";
|
||||
import { askPostInstallAction, questionHandlers } from "./utils";
|
||||
|
||||
export const askQuestions = async (
|
||||
args: QuestionArgs,
|
||||
): Promise<QuestionResults> => {
|
||||
const {
|
||||
useCase: useCaseFromArgs,
|
||||
framework: frameworkFromArgs,
|
||||
llamaCloudKey: llamaCloudKeyFromArgs,
|
||||
vectorDb: vectorDbFromArgs,
|
||||
postInstallAction: postInstallActionFromArgs,
|
||||
askModels: askModelsFromArgs,
|
||||
} = args;
|
||||
|
||||
const { useCase, framework } = await prompts(
|
||||
[
|
||||
{
|
||||
type: useCaseFromArgs ? null : "select",
|
||||
name: "useCase",
|
||||
message: "What use case do you want to build?",
|
||||
choices: [
|
||||
{
|
||||
title: "Agentic RAG",
|
||||
value: "agentic_rag",
|
||||
description:
|
||||
"Chatbot that answers questions based on provided documents.",
|
||||
},
|
||||
{
|
||||
title: "Financial Report",
|
||||
value: "financial_report",
|
||||
description:
|
||||
"Agent that analyzes data and generates visualizations by using a code interpreter.",
|
||||
},
|
||||
{
|
||||
title: "Deep Research",
|
||||
value: "deep_research",
|
||||
description:
|
||||
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
|
||||
},
|
||||
{
|
||||
title: "Code Generator",
|
||||
value: "code_generator",
|
||||
description: "Build a Vercel v0 styled code generator.",
|
||||
},
|
||||
{
|
||||
title: "Document Generator",
|
||||
value: "document_generator",
|
||||
description: "Build a OpenAI canvas-styled document generator.",
|
||||
},
|
||||
{
|
||||
title: "Human in the Loop",
|
||||
value: "hitl",
|
||||
description:
|
||||
"Build a CLI command workflow that is reviewed by a human before execution",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
{
|
||||
type: frameworkFromArgs ? null : "select",
|
||||
name: "framework",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
],
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
const finalUseCase = (useCaseFromArgs ?? useCase) as TemplateUseCase;
|
||||
const finalFramework = (frameworkFromArgs ?? framework) as TemplateFramework;
|
||||
if (!finalUseCase) {
|
||||
throw new Error("Use case is required");
|
||||
}
|
||||
if (!finalFramework) {
|
||||
throw new Error("Framework is required");
|
||||
}
|
||||
|
||||
// lookup configuration for the use case
|
||||
const useCaseConfig = useCaseConfiguration[finalUseCase];
|
||||
|
||||
// Ask for model provider
|
||||
let modelConfig = useCaseConfig.modelConfig;
|
||||
if (askModelsFromArgs) {
|
||||
modelConfig = await askModelConfig({
|
||||
framework: finalFramework,
|
||||
});
|
||||
}
|
||||
|
||||
// Ask for LlamaCloud
|
||||
let llamaCloudKey = llamaCloudKeyFromArgs ?? process.env.LLAMA_CLOUD_API_KEY;
|
||||
let vectorDb: TemplateVectorDB = vectorDbFromArgs ?? "none";
|
||||
if (!vectorDbFromArgs && useCaseConfig.dataSources) {
|
||||
const { useLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud?",
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
initial: false,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
const { llamaCloudKey: llamaCloudKeyFromPrompt } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = llamaCloudKeyFromPrompt;
|
||||
}
|
||||
vectorDb = useLlamaCloud ? "llamacloud" : "none";
|
||||
}
|
||||
|
||||
const result = {
|
||||
...useCaseConfig,
|
||||
framework: finalFramework,
|
||||
useCase: finalUseCase,
|
||||
modelConfig,
|
||||
llamaCloudKey,
|
||||
useLlamaParse: vectorDb === "llamacloud",
|
||||
vectorDb,
|
||||
};
|
||||
|
||||
const postInstallAction =
|
||||
postInstallActionFromArgs ?? (await askPostInstallAction(result));
|
||||
|
||||
return {
|
||||
...result,
|
||||
postInstallAction,
|
||||
};
|
||||
};
|
||||
@@ -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,22 @@
|
||||
import { InstallAppArgs } from "../create-app";
|
||||
import {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUseCase,
|
||||
TemplateVectorDB,
|
||||
} from "../helpers";
|
||||
|
||||
export type QuestionResults = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager"
|
||||
>;
|
||||
|
||||
export type QuestionArgs = {
|
||||
useCase?: TemplateUseCase;
|
||||
framework?: TemplateFramework;
|
||||
askModels?: boolean;
|
||||
llamaCloudKey?: string;
|
||||
port?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import { getGpt41ModelConfig } from "../helpers/models";
|
||||
import { ModelConfig, TemplateUseCase } from "../helpers/types";
|
||||
import { QuestionResults } from "./types";
|
||||
|
||||
export const useCaseConfiguration: Record<
|
||||
TemplateUseCase,
|
||||
Pick<QuestionResults, "template" | "dataSources"> & {
|
||||
modelConfig: ModelConfig;
|
||||
}
|
||||
> = {
|
||||
agentic_rag: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
modelConfig: getGpt41ModelConfig(),
|
||||
},
|
||||
financial_report: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
modelConfig: getGpt41ModelConfig(),
|
||||
},
|
||||
deep_research: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
modelConfig: getGpt41ModelConfig(),
|
||||
},
|
||||
code_generator: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [],
|
||||
modelConfig: getGpt41ModelConfig(),
|
||||
},
|
||||
document_generator: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [],
|
||||
modelConfig: getGpt41ModelConfig(),
|
||||
},
|
||||
hitl: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [],
|
||||
modelConfig: getGpt41ModelConfig(),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
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 { 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: Omit<QuestionResults, "postInstallAction">,
|
||||
): 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.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) {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# build dist/index.js file
|
||||
pnpm run build:ncc
|
||||
|
||||
# add shebang to the top of dist/index.js
|
||||
# XXX: Windows needs a space after `node` to work correctly
|
||||
# Note: ncc can handle shebang but it didn't work with Windows in our tests
|
||||
echo '#!/usr/bin/env node ' | cat - dist/index.js >temp && mv temp dist/index.js
|
||||
|
||||
# make dist/index.js executable
|
||||
chmod +x dist/index.js
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
pnpm pack && npm install -g $(pwd)/$(ls ./*.tgz | head -1)
|
||||
@@ -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
|
||||
uv sync
|
||||
```
|
||||
|
||||
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
|
||||
uv run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
uv 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:
|
||||
|
||||
```
|
||||
uv 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!
|
||||
+34
@@ -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,
|
||||
)
|
||||
+44
@@ -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,
|
||||
)
|
||||
+71
@@ -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
|
||||
+3
@@ -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})
|
||||
+347
@@ -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(),
|
||||
}
|
||||
+254
@@ -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)
|
||||
+47
@@ -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
|
||||
uv sync
|
||||
```
|
||||
|
||||
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
|
||||
uv run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
uv 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!
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
from .deep_research import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
+183
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user