mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-07 00:31:11 -04:00
Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d18748aba4 | |||
| 27c4ef3410 | |||
| a7ee392d3e | |||
| 4415a6fdef | |||
| 1e1e6e96a1 | |||
| 461d1dfbcc | |||
| 5975fafefb | |||
| 71169fd545 | |||
| be895d564d | |||
| f36a27c218 | |||
| 8cdb07f151 | |||
| ea403a0ffe | |||
| 7f0b4e66ae | |||
| 3b226965ba | |||
| 63daf77412 | |||
| 079a1d5cc3 | |||
| 2377d1a466 | |||
| 9f9f29391e | |||
| b64716d3f7 | |||
| d7a47abe38 | |||
| 58b314a61e | |||
| 4431ec7a5e | |||
| 9542026d70 | |||
| cc4c5b64c0 | |||
| 82c2aac4a0 | |||
| a143e0f0f1 | |||
| db9775dc32 | |||
| 538c0b0740 | |||
| 21cd88caf6 | |||
| 0660d9e2a5 | |||
| 25257f49d7 | |||
| dd615f106d | |||
| 5db64d61e0 | |||
| ee5e1f94e4 | |||
| 031e926414 | |||
| 88b4b3143d | |||
| c1ce84ecec | |||
| d670011363 | |||
| c88332366b | |||
| cfee282c28 | |||
| 91b42a3539 | |||
| 02b1d176c5 | |||
| 63d072b8cc | |||
| 256d44f255 | |||
| e2a6805a31 | |||
| d46fc12079 | |||
| 5ce88f107c | |||
| 683c4addd9 | |||
| db58cf2e68 | |||
| 1cf535865a | |||
| 6042d2a3c7 | |||
| df03819e12 | |||
| 072354afb7 | |||
| 57c7369aea | |||
| f92cdf335f | |||
| 16d7dd426a | |||
| 787b6928d9 | |||
| ddbdbc5fb5 | |||
| d0edf9fb48 | |||
| 28d4446aa7 | |||
| ab3419ab09 | |||
| 457fe1535f | |||
| 6e90b02052 | |||
| fdc2680ae8 | |||
| 35a398443a | |||
| b55ce8aa93 | |||
| 74e67ef702 | |||
| e689248919 | |||
| 5a527b3fc9 | |||
| 565cc37912 | |||
| 50e1864a85 | |||
| 37ac88fc1b | |||
| 8ed98bcb07 | |||
| b8609ec149 | |||
| 96eb603bca | |||
| 20aaf35fc4 | |||
| 151a63a118 | |||
| 9db2267445 | |||
| 69a7ef063d | |||
| 8527875f0a | |||
| 2244da07e6 | |||
| 18bf710549 | |||
| 9e2e5a3f7f | |||
| 3df7fd6dd1 | |||
| 4371c46c4c | |||
| fcf7c1275b | |||
| e6e62fa767 | |||
| 8e1cb8fb70 | |||
| 00674686cb | |||
| b350bb2e7a | |||
| e17c704a4b | |||
| 3259245780 | |||
| 63f21084b6 | |||
| ced3555248 | |||
| 27eef24611 | |||
| 1dabdbf7d8 | |||
| d65397a0ba | |||
| 8c72500070 | |||
| 2a27e21e00 | |||
| 3bc52a1f2c | |||
| 9806b5a0a9 | |||
| 201cd0f5fc | |||
| 5e2e92c11a | |||
| d57657599b | |||
| 995db834b2 | |||
| dfd22aac46 | |||
| 72f62718f1 | |||
| e938a4d154 | |||
| 641019262e | |||
| fe9056f081 | |||
| fba49b8088 | |||
| a5ae1eea30 | |||
| 6e0ee9ec32 | |||
| a5e3e10e84 | |||
| 99afbdd606 | |||
| 90c0b83c34 | |||
| 68f9dd1ce1 | |||
| 51e4b1de99 | |||
| 08f091a889 | |||
| 692e3cc56e | |||
| bcfbccc381 | |||
| 8aa8c65d0e | |||
| 635d485b69 | |||
| c0630eeebb | |||
| 8932be2d49 | |||
| 3905486240 | |||
| eedc14b13c | |||
| 44bb615eee | |||
| 541d387143 | |||
| a8ad9c10bd | |||
| f1669224da | |||
| 2a27061891 | |||
| 6c55b2de58 | |||
| 9b99855c43 | |||
| 0269e88575 | |||
| 7fbd43283d | |||
| 226c123b77 | |||
| ac271d1006 | |||
| af84425689 | |||
| 512e9c947c | |||
| e7319376a5 | |||
| 2a7b493769 | |||
| f516a0d2e4 | |||
| 62f872122c | |||
| 89737d6e00 | |||
| 6a81d54e53 | |||
| c0062746eb | |||
| 809a904bc8 | |||
| 602d27c7b0 | |||
| aad61e876f | |||
| eb0e9947f2 | |||
| 23a09cff1b | |||
| ebe9041fdc | |||
| f93ef09b58 | |||
| e74cfb93b5 | |||
| 4a44621f87 | |||
| c7acaa2f5e | |||
| 139abad1f4 | |||
| a3a5306f11 | |||
| fb1c3bc446 | |||
| aaf344a4dd | |||
| 62ca9c0ed2 | |||
| dc8be8740d | |||
| d9bcf4df92 | |||
| 7ceb94f9c2 | |||
| 2e5becb4fb | |||
| 5e12f568bd | |||
| 80382c0bf9 | |||
| 91150d4150 | |||
| 6bfc38db53 | |||
| 95b99db199 | |||
| 1b13395e65 | |||
| fe21904b53 | |||
| ab0d666f03 | |||
| 30add7a765 | |||
| 83971a1913 | |||
| 2f62081683 | |||
| c7eb81dfa4 | |||
| 9f35f526e0 | |||
| e755a63250 | |||
| 29c6b62ba1 | |||
| 9d69903c36 | |||
| 51475a9290 | |||
| a9e794bde9 | |||
| 5114a7aa27 | |||
| d14042e536 | |||
| 7819fca349 | |||
| 68d9cfb550 | |||
| 1b7fd95214 | |||
| 0a1e6ccf9a | |||
| 0db3f415a8 | |||
| 8a1385b9d0 | |||
| a52143b0ef | |||
| 75ec41c85a | |||
| 827c8b3c48 | |||
| 194b35d889 | |||
| 1b33523537 | |||
| 807b95597a | |||
| 14b1ffa413 | |||
| d1db4d5534 | |||
| a45c0e537f | |||
| 4dab9b8fa3 | |||
| a84f8ba5d6 | |||
| f6f5cab661 | |||
| 618f563ce9 | |||
| 5b49c90538 | |||
| 41be0003f1 | |||
| 8f8ee28ba0 | |||
| b3ae7fbb49 | |||
| 837854de1e | |||
| 8cc1f0726f | |||
| e1d617ef70 | |||
| 5f199d68f9 | |||
| b8cca2db97 | |||
| 35e959219d | |||
| 08d466faee | |||
| 0b5823f451 | |||
| c77b150c28 | |||
| 3cf27bb838 | |||
| 26a90435c7 | |||
| f6efaba906 | |||
| ff0e831da9 | |||
| 96bb65723a | |||
| 33ac4bc424 | |||
| 698503b467 | |||
| 0657525d40 | |||
| 064d0de531 | |||
| 471bf36a7b | |||
| cb7d2b4040 | |||
| 6032cbcf45 | |||
| 73785d7552 | |||
| 431b5ffa59 | |||
| c0500a0d4d | |||
| 5300534188 | |||
| 02192a5f53 | |||
| 2a98d5b8ee | |||
| 4f495b5fc6 | |||
| b75e2d23a2 | |||
| 5261cdc794 | |||
| b179f61c6f | |||
| 71b245ad6f | |||
| 5b070cf87a | |||
| b8afe0b364 | |||
| 92b4ec48f7 | |||
| 6a69ac997d | |||
| 8c542c30a9 | |||
| a8388c841f |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"llamaindex": patch
|
||||
---
|
||||
|
||||
Chat History summarization (thanks @marcusschlesser)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"llamaindex": patch
|
||||
---
|
||||
|
||||
Notion database support (thanks @TomPenguin)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"llamaindex": patch
|
||||
---
|
||||
|
||||
KeywordIndex (thanks @swk777)
|
||||
@@ -3,6 +3,7 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnpm-store
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
@@ -36,3 +37,6 @@ yarn-error.log*
|
||||
.vercel
|
||||
|
||||
dist/
|
||||
|
||||
# vs code
|
||||
.vscode/launch.json
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm lint
|
||||
npx lint-staged
|
||||
|
||||
@@ -84,6 +84,26 @@ Check out our NextJS playground at https://llama-playground.vercel.app/. The sou
|
||||
|
||||
- [SimplePrompt](/packages/core/src/Prompt.ts): A simple standardized function call definition that takes in inputs and formats them in a template literal. SimplePrompts can be specialized using currying and combined using other SimplePrompt functions.
|
||||
|
||||
## Note: NextJS:
|
||||
|
||||
If you're using NextJS App Router, you'll need to use the NodeJS runtime (default) and add the follow config to your next.config.js to have it use imports/exports in the same way Node does.
|
||||
|
||||
```js
|
||||
export const runtime = "nodejs"; // default
|
||||
```
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["pdf-parse"], // Puts pdf-parse in actual NodeJS mode with NextJS App Router
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
```
|
||||
|
||||
## Supported LLMs:
|
||||
|
||||
- OpenAI GPT-3.5-turbo and GPT-4
|
||||
|
||||
@@ -6,6 +6,8 @@ sidebar_position: 4
|
||||
|
||||
We include several end-to-end examples using LlamaIndex.TS in the repository
|
||||
|
||||
Check out the examples below or try them out and complete them in minutes with interactive Github Codespace tutorials provided by Dev-Docs [here](https://codespaces.new/team-dev-docs/lits-dev-docs-playground?devcontainer_path=.devcontainer%2Fjavascript_ltsquickstart%2Fdevcontainer.json):
|
||||
|
||||
## [Chat Engine](https://github.com/run-llama/LlamaIndexTS/blob/main/apps/simple/chatEngine.ts)
|
||||
|
||||
Read a file and chat about it with the LLM.
|
||||
@@ -14,7 +16,7 @@ Read a file and chat about it with the LLM.
|
||||
|
||||
Create a vector index and query it. The vector index will use embeddings to fetch the top k most relevant nodes. By default, the top k is 2.
|
||||
|
||||
## [Summary Index](https://github.com/run-llama/LlamaIndexTS/blob/main/apps/simple/summarIndex.ts)
|
||||
## [Summary Index](https://github.com/run-llama/LlamaIndexTS/blob/main/apps/simple/summaryIndex.ts)
|
||||
|
||||
Create a list index and query it. This example also use the `LLMRetriever`, which will use the LLM to select the best nodes to use when generating answer.
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Environments
|
||||
|
||||
LlamaIndex currently officially supports NodeJS 18 and NodeJS 20.
|
||||
|
||||
## NextJS App Router
|
||||
|
||||
If you're using NextJS App Router route handlers/serverless functions, you'll need to use the NodeJS mode:
|
||||
|
||||
```js
|
||||
export const runtime = "nodejs"; // default
|
||||
```
|
||||
|
||||
and you'll need to add an exception for pdf-parse in your next.config.js
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["pdf-parse"], // Puts pdf-parse in actual NodeJS mode with NextJS App Router
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
```
|
||||
@@ -19,7 +19,7 @@ That's where **LlamaIndex.TS** comes in.
|
||||
|
||||
LlamaIndex.TS provides the following tools:
|
||||
|
||||
- **Data loading** ingest your existing `txt` and `pdf` data directly
|
||||
- **Data loading** ingest your existing `.txt`, `.pdf`, `.csv`, `.md` and `.docx` data directly
|
||||
- **Data indexes** structure your data in intermediate representations that are easy and performant for LLMs to consume.
|
||||
- **Engines** provide natural language access to your data. For example:
|
||||
- Query engines are powerful retrieval interfaces for knowledge-augmented output.
|
||||
|
||||
@@ -4,7 +4,7 @@ sidebar_position: 1
|
||||
|
||||
# Reader / Loader
|
||||
|
||||
LlamaIndex.TS supports easy loading of files from folders using the `SimpleDirectoryReader` class. Currently, `.txt` and `.pdf` files are supported, with more planned in the future!
|
||||
LlamaIndex.TS supports easy loading of files from folders using the `SimpleDirectoryReader` class. Currently, `.txt`, `.pdf`, `.csv`, `.md` and `.docx` files are supported, with more planned in the future!
|
||||
|
||||
```typescript
|
||||
import { SimpleDirectoryReader } from "llamaindex";
|
||||
|
||||
@@ -15,24 +15,24 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.4.1",
|
||||
"@docusaurus/preset-classic": "2.4.1",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "^2.4.1",
|
||||
"@docusaurus/core": "2.4.3",
|
||||
"@docusaurus/preset-classic": "2.4.3",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "^2.4.3",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"postcss": "^8.4.28",
|
||||
"postcss": "^8.4.31",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.4.1",
|
||||
"@docusaurus/types": "^2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.7",
|
||||
"@docusaurus/module-type-aliases": "2.4.3",
|
||||
"@docusaurus/types": "^2.4.3",
|
||||
"@tsconfig/docusaurus": "^2.0.1",
|
||||
"docusaurus-plugin-typedoc": "^0.19.2",
|
||||
"typedoc": "^0.24.8",
|
||||
"typedoc-plugin-markdown": "^3.15.4",
|
||||
"typedoc-plugin-markdown": "^3.16.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -1,5 +1,81 @@
|
||||
# simple
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [63f2108]
|
||||
- llamaindex@0.0.35
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2a27e21]
|
||||
- llamaindex@0.0.34
|
||||
|
||||
## 0.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5e2e92c]
|
||||
- llamaindex@0.0.33
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [90c0b83]
|
||||
- Updated dependencies [dfd22aa]
|
||||
- llamaindex@0.0.32
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6c55b2d]
|
||||
- Updated dependencies [8aa8c65]
|
||||
- Updated dependencies [6c55b2d]
|
||||
- llamaindex@0.0.31
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [139abad]
|
||||
- Updated dependencies [139abad]
|
||||
- Updated dependencies [eb0e994]
|
||||
- Updated dependencies [eb0e994]
|
||||
- Updated dependencies [139abad]
|
||||
- llamaindex@0.0.30
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [a52143b]
|
||||
- Updated dependencies [1b7fd95]
|
||||
- Updated dependencies [0db3f41]
|
||||
- llamaindex@0.0.29
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [96bb657]
|
||||
- Updated dependencies [96bb657]
|
||||
- Updated dependencies [837854d]
|
||||
- llamaindex@0.0.28
|
||||
|
||||
## 0.0.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [4a5591b]
|
||||
- Updated dependencies [4a5591b]
|
||||
- Updated dependencies [4a5591b]
|
||||
- llamaindex@0.0.27
|
||||
|
||||
## 0.0.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
import { SimpleDirectoryReader } from "llamaindex";
|
||||
|
||||
function callback(
|
||||
category: string,
|
||||
name: string,
|
||||
status: any,
|
||||
message?: string,
|
||||
): boolean {
|
||||
console.log(category, name, status, message);
|
||||
if (name.endsWith(".pdf")) {
|
||||
console.log("I DON'T WANT PDF FILES!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Load page
|
||||
const reader = new SimpleDirectoryReader(callback);
|
||||
const params = { directoryPath: "./data" };
|
||||
await reader.loadData(params);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { HTMLReader, VectorStoreIndex } from "llamaindex";
|
||||
|
||||
async function main() {
|
||||
// Load page
|
||||
const reader = new HTMLReader();
|
||||
const documents = await reader.loadData("data/18-1_Changelog.html");
|
||||
|
||||
// Split text and create embeddings. Store them in a VectorStoreIndex
|
||||
const index = await VectorStoreIndex.fromDocuments(documents);
|
||||
|
||||
// Query the index
|
||||
const queryEngine = index.asQueryEngine();
|
||||
const response = await queryEngine.query(
|
||||
"What were the notable changes in 18.1?",
|
||||
);
|
||||
|
||||
// Output response
|
||||
console.log(response.toString());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ChatMessage, SimpleChatEngine } from "llamaindex";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import readline from "node:readline/promises";
|
||||
import { Anthropic } from "../../packages/core/src/llm/LLM";
|
||||
|
||||
async function main() {
|
||||
const query: string = `
|
||||
Where is Istanbul?
|
||||
`;
|
||||
|
||||
// const llm = new OpenAI({ model: "gpt-3.5-turbo", temperature: 0.1 });
|
||||
const llm = new Anthropic();
|
||||
const message: ChatMessage = { content: query, role: "user" };
|
||||
|
||||
//TODO: Add callbacks later
|
||||
|
||||
//Stream Complete
|
||||
//Note: Setting streaming flag to true or false will auto-set your return type to
|
||||
//either an AsyncGenerator or a Response.
|
||||
// Omitting the streaming flag automatically sets streaming to false
|
||||
|
||||
const chatEngine: SimpleChatEngine = new SimpleChatEngine({
|
||||
chatHistory: undefined,
|
||||
llm: llm,
|
||||
});
|
||||
|
||||
const rl = readline.createInterface({ input, output });
|
||||
while (true) {
|
||||
const query = await rl.question("Query: ");
|
||||
|
||||
if (!query) {
|
||||
break;
|
||||
}
|
||||
|
||||
//Case 1: .chat(query, undefined, true) => Stream
|
||||
//Case 2: .chat(query, undefined, false) => Response object
|
||||
//Case 3: .chat(query, undefined) => Response object
|
||||
const chatStream = await chatEngine.chat(query, undefined, true);
|
||||
var accumulated_result = "";
|
||||
for await (const part of chatStream) {
|
||||
accumulated_result += part;
|
||||
process.stdout.write(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { Document } from "../../packages/core/src/Node";
|
||||
import { VectorStoreIndex } from "../../packages/core/src/indices";
|
||||
import { SimpleMongoReader } from "../../packages/core/src/readers/SimpleMongoReader";
|
||||
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import readline from "node:readline/promises";
|
||||
|
||||
async function main() {
|
||||
//Dummy test code
|
||||
const query: object = { _id: "waldo" };
|
||||
const options: object = {};
|
||||
const projections: object = { embedding: 0 };
|
||||
const limit: number = Infinity;
|
||||
const uri: string = process.env.MONGODB_URI ?? "fake_uri";
|
||||
const client: MongoClient = new MongoClient(uri);
|
||||
|
||||
//Where the real code starts
|
||||
const MR = new SimpleMongoReader(client);
|
||||
const documents: Document[] = await MR.loadData(
|
||||
"data",
|
||||
"posts",
|
||||
1,
|
||||
{},
|
||||
options,
|
||||
projections,
|
||||
);
|
||||
|
||||
//
|
||||
//If you need to look at low-level details of
|
||||
// a queryEngine (for example, needing to check each individual node)
|
||||
//
|
||||
|
||||
// Split text and create embeddings. Store them in a VectorStoreIndex
|
||||
// var storageContext = await storageContextFromDefaults({});
|
||||
// var serviceContext = serviceContextFromDefaults({});
|
||||
// const docStore = storageContext.docStore;
|
||||
|
||||
// for (const doc of documents) {
|
||||
// docStore.setDocumentHash(doc.id_, doc.hash);
|
||||
// }
|
||||
// const nodes = serviceContext.nodeParser.getNodesFromDocuments(documents);
|
||||
// console.log(nodes);
|
||||
|
||||
//
|
||||
//Making Vector Store from documents
|
||||
//
|
||||
|
||||
const index = await VectorStoreIndex.fromDocuments(documents);
|
||||
// Create query engine
|
||||
const queryEngine = index.asQueryEngine();
|
||||
|
||||
const rl = readline.createInterface({ input, output });
|
||||
while (true) {
|
||||
const query = await rl.question("Query: ");
|
||||
|
||||
if (!query) {
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await queryEngine.query(query);
|
||||
|
||||
// Output response
|
||||
console.log(response.toString());
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OpenAI } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llm = new OpenAI({ model: "gpt-3.5-turbo", temperature: 0.0 });
|
||||
const llm = new OpenAI({ model: "gpt-4-1106-preview", temperature: 0.1 });
|
||||
|
||||
// complete api
|
||||
const response1 = await llm.complete("How are you?");
|
||||
@@ -9,7 +9,7 @@ import { OpenAI } from "llamaindex";
|
||||
|
||||
// chat api
|
||||
const response2 = await llm.chat([
|
||||
{ content: "Tell me a joke!", role: "user" },
|
||||
{ content: "Tell me a joke.", role: "user" },
|
||||
]);
|
||||
console.log(response2.message.content);
|
||||
})();
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.33",
|
||||
"private": true,
|
||||
"name": "simple",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^2.2.12",
|
||||
"commander": "^11.0.0",
|
||||
"@notionhq/client": "^2.2.13",
|
||||
"@pinecone-database/pinecone": "^1.1.2",
|
||||
"commander": "^11.1.0",
|
||||
"llamaindex": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.17.12"
|
||||
"@types/node": "^18.18.6",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint ."
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Portkey } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llms = [{}];
|
||||
const portkey = new Portkey({
|
||||
mode: "single",
|
||||
llms: [
|
||||
{
|
||||
provider: "anyscale",
|
||||
virtual_key: "anyscale-3b3c04",
|
||||
model: "meta-llama/Llama-2-13b-chat-hf",
|
||||
max_tokens: 2000,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = portkey.stream_chat([
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Tell me a joke." },
|
||||
]);
|
||||
for await (const res of result) {
|
||||
process.stdout.write(res);
|
||||
}
|
||||
})();
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
OpenAI,
|
||||
RetrieverQueryEngine,
|
||||
serviceContextFromDefaults,
|
||||
SimilarityPostprocessor,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import essay from "./essay";
|
||||
@@ -21,8 +22,16 @@ async function main() {
|
||||
|
||||
const retriever = index.asRetriever();
|
||||
retriever.similarityTopK = 5;
|
||||
const nodePostprocessor = new SimilarityPostprocessor({
|
||||
similarityCutoff: 0.7,
|
||||
});
|
||||
// TODO: cannot pass responseSynthesizer into retriever query engine
|
||||
const queryEngine = new RetrieverQueryEngine(retriever);
|
||||
const queryEngine = new RetrieverQueryEngine(
|
||||
retriever,
|
||||
undefined,
|
||||
undefined,
|
||||
[nodePostprocessor],
|
||||
);
|
||||
|
||||
const response = await queryEngine.query(
|
||||
"What did the author do growing up?",
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
OpenAI,
|
||||
ResponseSynthesizer,
|
||||
RetrieverQueryEngine,
|
||||
serviceContextFromDefaults,
|
||||
TextNode,
|
||||
TreeSummarize,
|
||||
VectorIndexRetriever,
|
||||
VectorStore,
|
||||
VectorStoreIndex,
|
||||
VectorStoreQuery,
|
||||
VectorStoreQueryResult,
|
||||
} from "llamaindex";
|
||||
|
||||
import { Index, Pinecone, RecordMetadata } from "@pinecone-database/pinecone";
|
||||
|
||||
/**
|
||||
* Please do not use this class in production; it's only for demonstration purposes.
|
||||
*/
|
||||
class PineconeVectorStore<T extends RecordMetadata = RecordMetadata>
|
||||
implements VectorStore
|
||||
{
|
||||
storesText = true;
|
||||
isEmbeddingQuery = false;
|
||||
|
||||
indexName!: string;
|
||||
pineconeClient!: Pinecone;
|
||||
index!: Index<T>;
|
||||
|
||||
constructor({ indexName, client }: { indexName: string; client: Pinecone }) {
|
||||
this.indexName = indexName;
|
||||
this.pineconeClient = client;
|
||||
this.index = client.index<T>(indexName);
|
||||
}
|
||||
|
||||
client() {
|
||||
return this.pineconeClient;
|
||||
}
|
||||
|
||||
async query(
|
||||
query: VectorStoreQuery,
|
||||
kwargs?: any,
|
||||
): Promise<VectorStoreQueryResult> {
|
||||
let queryEmbedding: number[] = [];
|
||||
if (query.queryEmbedding) {
|
||||
if (typeof query.alpha === "number") {
|
||||
const alpha = query.alpha;
|
||||
queryEmbedding = query.queryEmbedding.map((v) => v * alpha);
|
||||
} else {
|
||||
queryEmbedding = query.queryEmbedding;
|
||||
}
|
||||
}
|
||||
|
||||
// Current LlamaIndexTS implementation only support exact match filter, so we use kwargs instead.
|
||||
const filter = kwargs?.filter || {};
|
||||
|
||||
const response = await this.index.query({
|
||||
filter,
|
||||
vector: queryEmbedding,
|
||||
topK: query.similarityTopK,
|
||||
includeValues: true,
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Numbers of vectors returned by Pinecone after preFilters are applied: ${
|
||||
response?.matches?.length || 0
|
||||
}.`,
|
||||
);
|
||||
|
||||
const topKIds: string[] = [];
|
||||
const topKNodes: TextNode[] = [];
|
||||
const topKScores: number[] = [];
|
||||
|
||||
const metadataToNode = (metadata?: T): Partial<TextNode> => {
|
||||
if (!metadata) {
|
||||
throw new Error("metadata is undefined.");
|
||||
}
|
||||
|
||||
const nodeContent = metadata["_node_content"];
|
||||
if (!nodeContent) {
|
||||
throw new Error("nodeContent is undefined.");
|
||||
}
|
||||
|
||||
if (typeof nodeContent !== "string") {
|
||||
throw new Error("nodeContent is not a string.");
|
||||
}
|
||||
|
||||
return JSON.parse(nodeContent);
|
||||
};
|
||||
|
||||
if (response.matches) {
|
||||
for (const match of response.matches) {
|
||||
const node = new TextNode({
|
||||
...metadataToNode(match.metadata),
|
||||
embedding: match.values,
|
||||
});
|
||||
|
||||
topKIds.push(match.id);
|
||||
topKNodes.push(node);
|
||||
topKScores.push(match.score ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
ids: topKIds,
|
||||
nodes: topKNodes,
|
||||
similarities: topKScores,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
add(): Promise<string[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
delete(): Promise<void> {
|
||||
throw new Error("Method `delete` not implemented.");
|
||||
}
|
||||
|
||||
persist(): Promise<void> {
|
||||
throw new Error("Method `persist` not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The goal of this example is to show how to use Pinecone as a vector store
|
||||
* for LlamaIndexTS with(out) preFilters.
|
||||
*
|
||||
* It should not be used in production like that,
|
||||
* as you might want to find a proper PineconeVectorStore implementation.
|
||||
*/
|
||||
async function main() {
|
||||
process.env.PINECONE_API_KEY = "Your Pinecone API Key.";
|
||||
process.env.PINECONE_ENVIRONMENT = "Your Pinecone Environment.";
|
||||
process.env.PINECONE_PROJECT_ID = "Your Pinecone Project ID.";
|
||||
process.env.PINECONE_INDEX_NAME = "Your Pinecone Index Name.";
|
||||
process.env.OPENAI_API_KEY = "Your OpenAI API Key.";
|
||||
process.env.OPENAI_API_ORGANIZATION = "Your OpenAI API Organization.";
|
||||
|
||||
const getPineconeVectorStore = async () => {
|
||||
return new PineconeVectorStore({
|
||||
indexName: process.env.PINECONE_INDEX_NAME || "index-name",
|
||||
client: new Pinecone(),
|
||||
});
|
||||
};
|
||||
|
||||
const getServiceContext = () => {
|
||||
const openAI = new OpenAI({
|
||||
model: "gpt-4",
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
return serviceContextFromDefaults({
|
||||
llm: openAI,
|
||||
});
|
||||
};
|
||||
|
||||
const getQueryEngine = async (filter: unknown) => {
|
||||
const vectorStore = await getPineconeVectorStore();
|
||||
const serviceContext = getServiceContext();
|
||||
|
||||
const vectorStoreIndex = await VectorStoreIndex.fromVectorStore(
|
||||
vectorStore,
|
||||
serviceContext,
|
||||
);
|
||||
|
||||
const retriever = new VectorIndexRetriever({
|
||||
index: vectorStoreIndex,
|
||||
similarityTopK: 500,
|
||||
});
|
||||
|
||||
const responseSynthesizer = new ResponseSynthesizer({
|
||||
serviceContext,
|
||||
responseBuilder: new TreeSummarize(serviceContext),
|
||||
});
|
||||
|
||||
return new RetrieverQueryEngine(retriever, responseSynthesizer, {
|
||||
filter,
|
||||
});
|
||||
};
|
||||
|
||||
// whatever is a key from your metadata
|
||||
const queryEngine = await getQueryEngine({
|
||||
whatever: {
|
||||
$gte: 1,
|
||||
$lte: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await queryEngine.query("How many results do you have?");
|
||||
|
||||
console.log(response.toString());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { OpenAI } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llm = new OpenAI({ model: "gpt-4-vision-preview", temperature: 0.1 });
|
||||
|
||||
// complete api
|
||||
const response1 = await llm.complete("How are you?");
|
||||
console.log(response1.message.content);
|
||||
|
||||
// chat api
|
||||
const response2 = await llm.chat([
|
||||
{ content: "Tell me a joke!", role: "user" },
|
||||
]);
|
||||
console.log(response2.message.content);
|
||||
})();
|
||||
@@ -0,0 +1,24 @@
|
||||
import { SimpleDirectoryReader } from "llamaindex";
|
||||
|
||||
function callback(
|
||||
category: string,
|
||||
name: string,
|
||||
status: any,
|
||||
message?: string,
|
||||
): boolean {
|
||||
console.log(category, name, status, message);
|
||||
if (name.endsWith(".pdf")) {
|
||||
console.log("I DON'T WANT PDF FILES!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Load page
|
||||
const reader = new SimpleDirectoryReader(callback);
|
||||
const params = { directoryPath: "./data" };
|
||||
await reader.loadData(params);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { HTMLReader, VectorStoreIndex } from "llamaindex";
|
||||
|
||||
async function main() {
|
||||
// Load page
|
||||
const reader = new HTMLReader();
|
||||
const documents = await reader.loadData("data/18-1_Changelog.html");
|
||||
|
||||
// Split text and create embeddings. Store them in a VectorStoreIndex
|
||||
const index = await VectorStoreIndex.fromDocuments(documents);
|
||||
|
||||
// Query the index
|
||||
const queryEngine = index.asQueryEngine();
|
||||
const response = await queryEngine.query(
|
||||
"What were the notable changes in 18.1?",
|
||||
);
|
||||
|
||||
// Output response
|
||||
console.log(response.toString());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ChatMessage, SimpleChatEngine } from "llamaindex";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import readline from "node:readline/promises";
|
||||
import { Anthropic } from "../../packages/core/src/llm/LLM";
|
||||
|
||||
async function main() {
|
||||
const query: string = `
|
||||
Where is Istanbul?
|
||||
`;
|
||||
|
||||
// const llm = new OpenAI({ model: "gpt-3.5-turbo", temperature: 0.1 });
|
||||
const llm = new Anthropic();
|
||||
const message: ChatMessage = { content: query, role: "user" };
|
||||
|
||||
//TODO: Add callbacks later
|
||||
|
||||
//Stream Complete
|
||||
//Note: Setting streaming flag to true or false will auto-set your return type to
|
||||
//either an AsyncGenerator or a Response.
|
||||
// Omitting the streaming flag automatically sets streaming to false
|
||||
|
||||
const chatEngine: SimpleChatEngine = new SimpleChatEngine({
|
||||
chatHistory: undefined,
|
||||
llm: llm,
|
||||
});
|
||||
|
||||
const rl = readline.createInterface({ input, output });
|
||||
while (true) {
|
||||
const query = await rl.question("Query: ");
|
||||
|
||||
if (!query) {
|
||||
break;
|
||||
}
|
||||
|
||||
//Case 1: .chat(query, undefined, true) => Stream
|
||||
//Case 2: .chat(query, undefined, false) => Response object
|
||||
//Case 3: .chat(query, undefined) => Response object
|
||||
const chatStream = await chatEngine.chat(query, undefined, true);
|
||||
var accumulated_result = "";
|
||||
for await (const part of chatStream) {
|
||||
accumulated_result += part;
|
||||
process.stdout.write(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { Document } from "../../packages/core/src/Node";
|
||||
import { VectorStoreIndex } from "../../packages/core/src/indices";
|
||||
import { SimpleMongoReader } from "../../packages/core/src/readers/SimpleMongoReader";
|
||||
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import readline from "node:readline/promises";
|
||||
|
||||
async function main() {
|
||||
//Dummy test code
|
||||
const query: object = { _id: "waldo" };
|
||||
const options: object = {};
|
||||
const projections: object = { embedding: 0 };
|
||||
const limit: number = Infinity;
|
||||
const uri: string = process.env.MONGODB_URI ?? "fake_uri";
|
||||
const client: MongoClient = new MongoClient(uri);
|
||||
|
||||
//Where the real code starts
|
||||
const MR = new SimpleMongoReader(client);
|
||||
const documents: Document[] = await MR.loadData(
|
||||
"data",
|
||||
"posts",
|
||||
1,
|
||||
{},
|
||||
options,
|
||||
projections,
|
||||
);
|
||||
|
||||
//
|
||||
//If you need to look at low-level details of
|
||||
// a queryEngine (for example, needing to check each individual node)
|
||||
//
|
||||
|
||||
// Split text and create embeddings. Store them in a VectorStoreIndex
|
||||
// var storageContext = await storageContextFromDefaults({});
|
||||
// var serviceContext = serviceContextFromDefaults({});
|
||||
// const docStore = storageContext.docStore;
|
||||
|
||||
// for (const doc of documents) {
|
||||
// docStore.setDocumentHash(doc.id_, doc.hash);
|
||||
// }
|
||||
// const nodes = serviceContext.nodeParser.getNodesFromDocuments(documents);
|
||||
// console.log(nodes);
|
||||
|
||||
//
|
||||
//Making Vector Store from documents
|
||||
//
|
||||
|
||||
const index = await VectorStoreIndex.fromDocuments(documents);
|
||||
// Create query engine
|
||||
const queryEngine = index.asQueryEngine();
|
||||
|
||||
const rl = readline.createInterface({ input, output });
|
||||
while (true) {
|
||||
const query = await rl.question("Query: ");
|
||||
|
||||
if (!query) {
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await queryEngine.query(query);
|
||||
|
||||
// Output response
|
||||
console.log(response.toString());
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { OpenAI } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llm = new OpenAI({ model: "gpt-3.5-turbo", temperature: 0.0 });
|
||||
const llm = new OpenAI({ model: "gpt-4-1106-preview", temperature: 0.1 });
|
||||
|
||||
// complete api
|
||||
const response1 = await llm.complete("How are you?");
|
||||
@@ -9,7 +9,7 @@ import { OpenAI } from "llamaindex";
|
||||
|
||||
// chat api
|
||||
const response2 = await llm.chat([
|
||||
{ content: "Tell me a joke!", role: "user" },
|
||||
{ content: "Tell me a joke.", role: "user" },
|
||||
]);
|
||||
console.log(response2.message.content);
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Portkey } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llms = [{}];
|
||||
const portkey = new Portkey({
|
||||
mode: "single",
|
||||
llms: [
|
||||
{
|
||||
provider: "anyscale",
|
||||
virtual_key: "anyscale-3b3c04",
|
||||
model: "meta-llama/Llama-2-13b-chat-hf",
|
||||
max_tokens: 2000,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = portkey.stream_chat([
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Tell me a joke." },
|
||||
]);
|
||||
for await (const res of result) {
|
||||
process.stdout.write(res);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
import { execSync } from "child_process";
|
||||
import {
|
||||
PDFReader,
|
||||
serviceContextFromDefaults,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
|
||||
const STORAGE_DIR = "./cache";
|
||||
|
||||
async function main() {
|
||||
// write the index to disk
|
||||
const serviceContext = serviceContextFromDefaults({});
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: `${STORAGE_DIR}`,
|
||||
});
|
||||
const reader = new PDFReader();
|
||||
const documents = await reader.loadData("data/brk-2022.pdf");
|
||||
await VectorStoreIndex.fromDocuments(documents, {
|
||||
storageContext,
|
||||
serviceContext,
|
||||
});
|
||||
console.log("wrote index to disk - now trying to read it");
|
||||
// make index dir read only
|
||||
execSync(`chmod -R 555 ${STORAGE_DIR}`);
|
||||
// reopen index
|
||||
const readOnlyStorageContext = await storageContextFromDefaults({
|
||||
persistDir: `${STORAGE_DIR}`,
|
||||
});
|
||||
await VectorStoreIndex.init({
|
||||
storageContext: readOnlyStorageContext,
|
||||
serviceContext,
|
||||
});
|
||||
console.log("read only index successfully opened");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
OpenAI,
|
||||
RetrieverQueryEngine,
|
||||
serviceContextFromDefaults,
|
||||
SimilarityPostprocessor,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import essay from "./essay";
|
||||
@@ -21,8 +22,16 @@ async function main() {
|
||||
|
||||
const retriever = index.asRetriever();
|
||||
retriever.similarityTopK = 5;
|
||||
const nodePostprocessor = new SimilarityPostprocessor({
|
||||
similarityCutoff: 0.7,
|
||||
});
|
||||
// TODO: cannot pass responseSynthesizer into retriever query engine
|
||||
const queryEngine = new RetrieverQueryEngine(retriever);
|
||||
const queryEngine = new RetrieverQueryEngine(
|
||||
retriever,
|
||||
undefined,
|
||||
undefined,
|
||||
[nodePostprocessor],
|
||||
);
|
||||
|
||||
const response = await queryEngine.query(
|
||||
"What did the author do growing up?",
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
OpenAI,
|
||||
ResponseSynthesizer,
|
||||
RetrieverQueryEngine,
|
||||
serviceContextFromDefaults,
|
||||
TextNode,
|
||||
TreeSummarize,
|
||||
VectorIndexRetriever,
|
||||
VectorStore,
|
||||
VectorStoreIndex,
|
||||
VectorStoreQuery,
|
||||
VectorStoreQueryResult,
|
||||
} from "llamaindex";
|
||||
|
||||
import { Index, Pinecone, RecordMetadata } from "@pinecone-database/pinecone";
|
||||
|
||||
/**
|
||||
* Please do not use this class in production; it's only for demonstration purposes.
|
||||
*/
|
||||
class PineconeVectorStore<T extends RecordMetadata = RecordMetadata>
|
||||
implements VectorStore
|
||||
{
|
||||
storesText = true;
|
||||
isEmbeddingQuery = false;
|
||||
|
||||
indexName!: string;
|
||||
pineconeClient!: Pinecone;
|
||||
index!: Index<T>;
|
||||
|
||||
constructor({ indexName, client }: { indexName: string; client: Pinecone }) {
|
||||
this.indexName = indexName;
|
||||
this.pineconeClient = client;
|
||||
this.index = client.index<T>(indexName);
|
||||
}
|
||||
|
||||
client() {
|
||||
return this.pineconeClient;
|
||||
}
|
||||
|
||||
async query(
|
||||
query: VectorStoreQuery,
|
||||
kwargs?: any,
|
||||
): Promise<VectorStoreQueryResult> {
|
||||
let queryEmbedding: number[] = [];
|
||||
if (query.queryEmbedding) {
|
||||
if (typeof query.alpha === "number") {
|
||||
const alpha = query.alpha;
|
||||
queryEmbedding = query.queryEmbedding.map((v) => v * alpha);
|
||||
} else {
|
||||
queryEmbedding = query.queryEmbedding;
|
||||
}
|
||||
}
|
||||
|
||||
// Current LlamaIndexTS implementation only support exact match filter, so we use kwargs instead.
|
||||
const filter = kwargs?.filter || {};
|
||||
|
||||
const response = await this.index.query({
|
||||
filter,
|
||||
vector: queryEmbedding,
|
||||
topK: query.similarityTopK,
|
||||
includeValues: true,
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Numbers of vectors returned by Pinecone after preFilters are applied: ${
|
||||
response?.matches?.length || 0
|
||||
}.`,
|
||||
);
|
||||
|
||||
const topKIds: string[] = [];
|
||||
const topKNodes: TextNode[] = [];
|
||||
const topKScores: number[] = [];
|
||||
|
||||
const metadataToNode = (metadata?: T): Partial<TextNode> => {
|
||||
if (!metadata) {
|
||||
throw new Error("metadata is undefined.");
|
||||
}
|
||||
|
||||
const nodeContent = metadata["_node_content"];
|
||||
if (!nodeContent) {
|
||||
throw new Error("nodeContent is undefined.");
|
||||
}
|
||||
|
||||
if (typeof nodeContent !== "string") {
|
||||
throw new Error("nodeContent is not a string.");
|
||||
}
|
||||
|
||||
return JSON.parse(nodeContent);
|
||||
};
|
||||
|
||||
if (response.matches) {
|
||||
for (const match of response.matches) {
|
||||
const node = new TextNode({
|
||||
...metadataToNode(match.metadata),
|
||||
embedding: match.values,
|
||||
});
|
||||
|
||||
topKIds.push(match.id);
|
||||
topKNodes.push(node);
|
||||
topKScores.push(match.score ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
ids: topKIds,
|
||||
nodes: topKNodes,
|
||||
similarities: topKScores,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
add(): Promise<string[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
delete(): Promise<void> {
|
||||
throw new Error("Method `delete` not implemented.");
|
||||
}
|
||||
|
||||
persist(): Promise<void> {
|
||||
throw new Error("Method `persist` not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The goal of this example is to show how to use Pinecone as a vector store
|
||||
* for LlamaIndexTS with(out) preFilters.
|
||||
*
|
||||
* It should not be used in production like that,
|
||||
* as you might want to find a proper PineconeVectorStore implementation.
|
||||
*/
|
||||
async function main() {
|
||||
process.env.PINECONE_API_KEY = "Your Pinecone API Key.";
|
||||
process.env.PINECONE_ENVIRONMENT = "Your Pinecone Environment.";
|
||||
process.env.PINECONE_PROJECT_ID = "Your Pinecone Project ID.";
|
||||
process.env.PINECONE_INDEX_NAME = "Your Pinecone Index Name.";
|
||||
process.env.OPENAI_API_KEY = "Your OpenAI API Key.";
|
||||
process.env.OPENAI_API_ORGANIZATION = "Your OpenAI API Organization.";
|
||||
|
||||
const getPineconeVectorStore = async () => {
|
||||
return new PineconeVectorStore({
|
||||
indexName: process.env.PINECONE_INDEX_NAME || "index-name",
|
||||
client: new Pinecone(),
|
||||
});
|
||||
};
|
||||
|
||||
const getServiceContext = () => {
|
||||
const openAI = new OpenAI({
|
||||
model: "gpt-4",
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
return serviceContextFromDefaults({
|
||||
llm: openAI,
|
||||
});
|
||||
};
|
||||
|
||||
const getQueryEngine = async (filter: unknown) => {
|
||||
const vectorStore = await getPineconeVectorStore();
|
||||
const serviceContext = getServiceContext();
|
||||
|
||||
const vectorStoreIndex = await VectorStoreIndex.fromVectorStore(
|
||||
vectorStore,
|
||||
serviceContext,
|
||||
);
|
||||
|
||||
const retriever = new VectorIndexRetriever({
|
||||
index: vectorStoreIndex,
|
||||
similarityTopK: 500,
|
||||
});
|
||||
|
||||
const responseSynthesizer = new ResponseSynthesizer({
|
||||
serviceContext,
|
||||
responseBuilder: new TreeSummarize(serviceContext),
|
||||
});
|
||||
|
||||
return new RetrieverQueryEngine(retriever, responseSynthesizer, {
|
||||
filter,
|
||||
});
|
||||
};
|
||||
|
||||
// whatever is a key from your metadata
|
||||
const queryEngine = await getQueryEngine({
|
||||
whatever: {
|
||||
$gte: 1,
|
||||
$lte: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await queryEngine.query("How many results do you have?");
|
||||
|
||||
console.log(response.toString());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { OpenAI } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llm = new OpenAI({ model: "gpt-4-vision-preview", temperature: 0.1 });
|
||||
|
||||
// complete api
|
||||
const response1 = await llm.complete("How are you?");
|
||||
console.log(response1.message.content);
|
||||
|
||||
// chat api
|
||||
const response2 = await llm.chat([
|
||||
{ content: "Tell me a joke!", role: "user" },
|
||||
]);
|
||||
console.log(response2.message.content);
|
||||
})();
|
||||
+16
-13
@@ -3,7 +3,7 @@
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md}\"",
|
||||
"lint": "turbo run lint",
|
||||
"prepare": "husky install",
|
||||
"test": "turbo run test",
|
||||
@@ -11,24 +11,27 @@
|
||||
"publish-snapshot": "turbo run build lint test && changeset version --snapshot && changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbo/gen": "^1.10.13",
|
||||
"@types/jest": "^29.5.4",
|
||||
"eslint": "^7.32.0",
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@turbo/gen": "^1.10.16",
|
||||
"@types/jest": "^29.5.8",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.6.4",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.1.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"turbo": "^1.10.13"
|
||||
},
|
||||
"packageManager": "pnpm@7.15.0",
|
||||
"dependencies": {
|
||||
"@changesets/cli": "^2.26.2"
|
||||
"turbo": "^1.10.16"
|
||||
},
|
||||
"packageManager": "pnpm@8.10.5+sha256.a4bd9bb7b48214bbfcd95f264bd75bb70d100e5d4b58808f5cd6ab40c6ac21c5",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"trim": "1.0.1"
|
||||
"trim": "1.0.1",
|
||||
"@babel/traverse": "7.23.2"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md}": "prettier --write"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,72 @@
|
||||
# llamaindex
|
||||
|
||||
## 0.0.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 63f2108: Add multimodal support (thanks @marcusschiesser)
|
||||
|
||||
## 0.0.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2a27e21: Add support for gpt-3.5-turbo-1106
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5e2e92c: gpt-4-1106-preview and gpt-4-vision-preview from OpenAI dev day
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 90c0b83: Add HTMLReader (thanks @mtutty)
|
||||
- dfd22aa: Add observer/filter to the SimpleDirectoryReader (thanks @mtutty)
|
||||
|
||||
## 0.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6c55b2d: Give HistoryChatEngine pluggable options (thanks @marcusschiesser)
|
||||
- 8aa8c65: Add SimilarityPostProcessor (thanks @TomPenguin)
|
||||
- 6c55b2d: Added LLMMetadata (thanks @marcusschiesser)
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 139abad: Streaming improvements including Anthropic (thanks @kkang2097)
|
||||
- 139abad: Portkey integration (Thank you @noble-varghese)
|
||||
- eb0e994: Add export for PromptHelper (thanks @zigamall)
|
||||
- eb0e994: Publish ESM module again
|
||||
- 139abad: Pinecone demo (thanks @Einsenhorn)
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a52143b: Added DocxReader for Word documents (thanks @jayantasamaddar)
|
||||
- 1b7fd95: Updated OpenAI streaming (thanks @kkang2097)
|
||||
- 0db3f41: Migrated to Tiktoken lite, which hopefully fixes the Windows issue
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 96bb657: Typesafe metadata (thanks @TomPenguin)
|
||||
- 96bb657: MongoReader (thanks @kkang2097)
|
||||
- 837854d: Make OutputParser less strict and add tests (Thanks @kkang2097)
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a5591b: Chat History summarization (thanks @marcusschiesser)
|
||||
- 4a5591b: Notion database support (thanks @TomPenguin)
|
||||
- 4a5591b: KeywordIndex (thanks @swk777)
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+24
-14
@@ -1,37 +1,47 @@
|
||||
{
|
||||
"name": "llamaindex",
|
||||
"version": "0.0.26",
|
||||
"version": "0.0.35",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.6.2",
|
||||
"@notionhq/client": "^2.2.12",
|
||||
"@anthropic-ai/sdk": "^0.9.0",
|
||||
"@notionhq/client": "^2.2.13",
|
||||
"js-tiktoken": "^1.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.6.0",
|
||||
"md-utils-ts": "^2.0.0",
|
||||
"mongodb": "^6.2.0",
|
||||
"notion-md-crawler": "^0.0.2",
|
||||
"openai": "^4.3.1",
|
||||
"openai": "^4.16.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"portkey-ai": "^0.1.16",
|
||||
"rake-modified": "^1.0.8",
|
||||
"replicate": "^0.16.1",
|
||||
"tiktoken-node": "^0.0.6",
|
||||
"uuid": "^9.0.0",
|
||||
"replicate": "^0.21.1",
|
||||
"string-strip-html": "^13.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"wink-nlp": "^1.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/papaparse": "^5.3.8",
|
||||
"@types/pdf-parse": "^1.1.1",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "^18.18.8",
|
||||
"@types/papaparse": "^5.3.10",
|
||||
"@types/pdf-parse": "^1.1.3",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"node-stdlib-browser": "^1.2.0",
|
||||
"tsup": "^7.2.0"
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"repository": "run-llama/LlamaIndexTS",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "jest",
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts"
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts",
|
||||
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
||||
}
|
||||
}
|
||||
|
||||
+261
-46
@@ -1,8 +1,6 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { ChatHistory, SimpleChatHistory } from "./ChatHistory";
|
||||
import { ChatMessage, LLM, OpenAI } from "./llm/LLM";
|
||||
import { TextNode } from "./Node";
|
||||
import { ChatHistory } from "./ChatHistory";
|
||||
import { NodeWithScore, TextNode } from "./Node";
|
||||
import {
|
||||
CondenseQuestionPrompt,
|
||||
ContextSystemPrompt,
|
||||
@@ -14,6 +12,9 @@ import { BaseQueryEngine } from "./QueryEngine";
|
||||
import { Response } from "./Response";
|
||||
import { BaseRetriever } from "./Retriever";
|
||||
import { ServiceContext, serviceContextFromDefaults } from "./ServiceContext";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { BaseNodePostprocessor } from "./indices/BaseNodePostprocessor";
|
||||
import { ChatMessage, LLM, OpenAI } from "./llm/LLM";
|
||||
|
||||
/**
|
||||
* A ChatEngine is used to handle back and forth chats between the application and the LLM.
|
||||
@@ -23,8 +24,16 @@ export interface ChatEngine {
|
||||
* Send message along with the class's current chat history to the LLM.
|
||||
* @param message
|
||||
* @param chatHistory optional chat history if you want to customize the chat history
|
||||
* @param streaming optional streaming flag, which auto-sets the return value if True.
|
||||
*/
|
||||
chat(message: string, chatHistory?: ChatMessage[]): Promise<Response>;
|
||||
chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
|
||||
>(
|
||||
message: string,
|
||||
chatHistory?: ChatMessage[],
|
||||
streaming?: T,
|
||||
): Promise<R>;
|
||||
|
||||
/**
|
||||
* Resets the chat history so that it's empty.
|
||||
@@ -44,13 +53,45 @@ export class SimpleChatEngine implements ChatEngine {
|
||||
this.llm = init?.llm ?? new OpenAI();
|
||||
}
|
||||
|
||||
async chat(message: string, chatHistory?: ChatMessage[]): Promise<Response> {
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
|
||||
>(message: string, chatHistory?: ChatMessage[], streaming?: T): Promise<R> {
|
||||
//Streaming option
|
||||
if (streaming) {
|
||||
return this.streamChat(message, chatHistory) as R;
|
||||
}
|
||||
|
||||
//Non-streaming option
|
||||
chatHistory = chatHistory ?? this.chatHistory;
|
||||
chatHistory.push({ content: message, role: "user" });
|
||||
const response = await this.llm.chat(chatHistory);
|
||||
const response = await this.llm.chat(chatHistory, undefined);
|
||||
chatHistory.push(response.message);
|
||||
this.chatHistory = chatHistory;
|
||||
return new Response(response.message.content);
|
||||
return new Response(response.message.content) as R;
|
||||
}
|
||||
|
||||
protected async *streamChat(
|
||||
message: string,
|
||||
chatHistory?: ChatMessage[],
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
chatHistory = chatHistory ?? this.chatHistory;
|
||||
chatHistory.push({ content: message, role: "user" });
|
||||
const response_generator = await this.llm.chat(
|
||||
chatHistory,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
var accumulator: string = "";
|
||||
for await (const part of response_generator) {
|
||||
accumulator += part;
|
||||
yield part;
|
||||
}
|
||||
|
||||
chatHistory.push({ content: accumulator, role: "assistant" });
|
||||
this.chatHistory = chatHistory;
|
||||
return;
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -99,10 +140,14 @@ export class CondenseQuestionChatEngine implements ChatEngine {
|
||||
);
|
||||
}
|
||||
|
||||
async chat(
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
|
||||
>(
|
||||
message: string,
|
||||
chatHistory?: ChatMessage[] | undefined,
|
||||
): Promise<Response> {
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
chatHistory = chatHistory ?? this.chatHistory;
|
||||
|
||||
const condensedQuestion = (
|
||||
@@ -114,7 +159,7 @@ export class CondenseQuestionChatEngine implements ChatEngine {
|
||||
chatHistory.push({ content: message, role: "user" });
|
||||
chatHistory.push({ content: response.response, role: "assistant" });
|
||||
|
||||
return response;
|
||||
return response as R;
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -122,57 +167,117 @@ export class CondenseQuestionChatEngine implements ChatEngine {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
message: ChatMessage;
|
||||
nodes: NodeWithScore[];
|
||||
}
|
||||
|
||||
export interface ContextGenerator {
|
||||
generate(message: string, parentEvent?: Event): Promise<Context>;
|
||||
}
|
||||
|
||||
export class DefaultContextGenerator implements ContextGenerator {
|
||||
retriever: BaseRetriever;
|
||||
contextSystemPrompt: ContextSystemPrompt;
|
||||
nodePostprocessors: BaseNodePostprocessor[];
|
||||
|
||||
constructor(init: {
|
||||
retriever: BaseRetriever;
|
||||
contextSystemPrompt?: ContextSystemPrompt;
|
||||
nodePostprocessors?: BaseNodePostprocessor[];
|
||||
}) {
|
||||
this.retriever = init.retriever;
|
||||
this.contextSystemPrompt =
|
||||
init?.contextSystemPrompt ?? defaultContextSystemPrompt;
|
||||
this.nodePostprocessors = init.nodePostprocessors || [];
|
||||
}
|
||||
|
||||
private applyNodePostprocessors(nodes: NodeWithScore[]) {
|
||||
return this.nodePostprocessors.reduce(
|
||||
(nodes, nodePostprocessor) => nodePostprocessor.postprocessNodes(nodes),
|
||||
nodes,
|
||||
);
|
||||
}
|
||||
|
||||
async generate(message: string, parentEvent?: Event): Promise<Context> {
|
||||
if (!parentEvent) {
|
||||
parentEvent = {
|
||||
id: uuidv4(),
|
||||
type: "wrapper",
|
||||
tags: ["final"],
|
||||
};
|
||||
}
|
||||
const sourceNodesWithScore = await this.retriever.retrieve(
|
||||
message,
|
||||
parentEvent,
|
||||
);
|
||||
|
||||
const nodes = this.applyNodePostprocessors(sourceNodesWithScore);
|
||||
|
||||
return {
|
||||
message: {
|
||||
content: this.contextSystemPrompt({
|
||||
context: nodes.map((r) => (r.node as TextNode).text).join("\n\n"),
|
||||
}),
|
||||
role: "system",
|
||||
},
|
||||
nodes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextChatEngine uses the Index to get the appropriate context for each query.
|
||||
* The context is stored in the system prompt, and the chat history is preserved,
|
||||
* ideally allowing the appropriate context to be surfaced for each query.
|
||||
*/
|
||||
export class ContextChatEngine implements ChatEngine {
|
||||
retriever: BaseRetriever;
|
||||
chatModel: OpenAI;
|
||||
chatModel: LLM;
|
||||
chatHistory: ChatMessage[];
|
||||
contextSystemPrompt: ContextSystemPrompt;
|
||||
contextGenerator: ContextGenerator;
|
||||
|
||||
constructor(init: {
|
||||
retriever: BaseRetriever;
|
||||
chatModel?: OpenAI;
|
||||
chatModel?: LLM;
|
||||
chatHistory?: ChatMessage[];
|
||||
contextSystemPrompt?: ContextSystemPrompt;
|
||||
nodePostprocessors?: BaseNodePostprocessor[];
|
||||
}) {
|
||||
this.retriever = init.retriever;
|
||||
this.chatModel =
|
||||
init.chatModel ?? new OpenAI({ model: "gpt-3.5-turbo-16k" });
|
||||
this.chatHistory = init?.chatHistory ?? [];
|
||||
this.contextSystemPrompt =
|
||||
init?.contextSystemPrompt ?? defaultContextSystemPrompt;
|
||||
this.contextGenerator = new DefaultContextGenerator({
|
||||
retriever: init.retriever,
|
||||
contextSystemPrompt: init?.contextSystemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
async chat(message: string, chatHistory?: ChatMessage[] | undefined) {
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
|
||||
>(
|
||||
message: string,
|
||||
chatHistory?: ChatMessage[] | undefined,
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
chatHistory = chatHistory ?? this.chatHistory;
|
||||
|
||||
//Streaming option
|
||||
if (streaming) {
|
||||
return this.streamChat(message, chatHistory) as R;
|
||||
}
|
||||
|
||||
const parentEvent: Event = {
|
||||
id: uuidv4(),
|
||||
type: "wrapper",
|
||||
tags: ["final"],
|
||||
};
|
||||
const sourceNodesWithScore = await this.retriever.retrieve(
|
||||
message,
|
||||
parentEvent,
|
||||
);
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
content: this.contextSystemPrompt({
|
||||
context: sourceNodesWithScore
|
||||
.map((r) => (r.node as TextNode).text)
|
||||
.join("\n\n"),
|
||||
}),
|
||||
role: "system",
|
||||
};
|
||||
const context = await this.contextGenerator.generate(message, parentEvent);
|
||||
|
||||
chatHistory.push({ content: message, role: "user" });
|
||||
|
||||
const response = await this.chatModel.chat(
|
||||
[systemMessage, ...chatHistory],
|
||||
[context.message, ...chatHistory],
|
||||
parentEvent,
|
||||
);
|
||||
chatHistory.push(response.message);
|
||||
@@ -181,8 +286,41 @@ export class ContextChatEngine implements ChatEngine {
|
||||
|
||||
return new Response(
|
||||
response.message.content,
|
||||
sourceNodesWithScore.map((r) => r.node),
|
||||
context.nodes.map((r) => r.node),
|
||||
) as R;
|
||||
}
|
||||
|
||||
protected async *streamChat(
|
||||
message: string,
|
||||
chatHistory?: ChatMessage[] | undefined,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
chatHistory = chatHistory ?? this.chatHistory;
|
||||
|
||||
const parentEvent: Event = {
|
||||
id: uuidv4(),
|
||||
type: "wrapper",
|
||||
tags: ["final"],
|
||||
};
|
||||
const context = await this.contextGenerator.generate(message, parentEvent);
|
||||
|
||||
chatHistory.push({ content: message, role: "user" });
|
||||
|
||||
const response_stream = await this.chatModel.chat(
|
||||
[context.message, ...chatHistory],
|
||||
parentEvent,
|
||||
true,
|
||||
);
|
||||
var accumulator: string = "";
|
||||
for await (const part of response_stream) {
|
||||
accumulator += part;
|
||||
yield part;
|
||||
}
|
||||
|
||||
chatHistory.push({ content: accumulator, role: "assistant" });
|
||||
|
||||
this.chatHistory = chatHistory;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -190,27 +328,104 @@ export class ContextChatEngine implements ChatEngine {
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageContentDetail {
|
||||
type: "text" | "image_url";
|
||||
text: string;
|
||||
image_url: { url: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* HistoryChatEngine is a ChatEngine that uses a ChatHistory to keep track of the chat history. This is an example with the same behavior as SimpleChatEngine
|
||||
* TODO: generally use the ChatHistory instead of ChatMessage[] - breaking change
|
||||
* Extended type for the content of a message that allows for multi-modal messages.
|
||||
*/
|
||||
export class HistoryChatEngine implements ChatEngine {
|
||||
chatHistory: ChatHistory;
|
||||
export type MessageContent = string | MessageContentDetail[];
|
||||
|
||||
/**
|
||||
* HistoryChatEngine is a ChatEngine that uses a `ChatHistory` object
|
||||
* to keeps track of chat's message history.
|
||||
* A `ChatHistory` object is passed as a parameter for each call to the `chat` method,
|
||||
* so the state of the chat engine is preserved between calls.
|
||||
* Optionally, a `ContextGenerator` can be used to generate an additional context for each call to `chat`.
|
||||
*/
|
||||
export class HistoryChatEngine {
|
||||
llm: LLM;
|
||||
contextGenerator?: ContextGenerator;
|
||||
|
||||
constructor(init?: Partial<HistoryChatEngine>) {
|
||||
this.chatHistory = init?.chatHistory ?? new SimpleChatHistory();
|
||||
this.llm = init?.llm ?? new OpenAI();
|
||||
this.contextGenerator = init?.contextGenerator;
|
||||
}
|
||||
|
||||
async chat(message: string): Promise<Response> {
|
||||
this.chatHistory.addMessage({ content: message, role: "user" });
|
||||
const response = await this.llm.chat(this.chatHistory.messages);
|
||||
this.chatHistory.addMessage(response.message);
|
||||
return new Response(response.message.content);
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
|
||||
>(
|
||||
message: MessageContent,
|
||||
chatHistory: ChatHistory,
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
//Streaming option
|
||||
if (streaming) {
|
||||
return this.streamChat(message, chatHistory) as R;
|
||||
}
|
||||
const requestMessages = await this.prepareRequestMessages(
|
||||
message,
|
||||
chatHistory,
|
||||
);
|
||||
const response = await this.llm.chat(requestMessages);
|
||||
chatHistory.addMessage(response.message);
|
||||
return new Response(response.message.content) as R;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.chatHistory.reset();
|
||||
protected async *streamChat(
|
||||
message: MessageContent,
|
||||
chatHistory: ChatHistory,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
const requestMessages = await this.prepareRequestMessages(
|
||||
message,
|
||||
chatHistory,
|
||||
);
|
||||
const response_stream = await this.llm.chat(
|
||||
requestMessages,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
var accumulator = "";
|
||||
for await (const part of response_stream) {
|
||||
accumulator += part;
|
||||
yield part;
|
||||
}
|
||||
chatHistory.addMessage({
|
||||
content: accumulator,
|
||||
role: "assistant",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
private async prepareRequestMessages(
|
||||
message: MessageContent,
|
||||
chatHistory: ChatHistory,
|
||||
) {
|
||||
chatHistory.addMessage({
|
||||
content: message,
|
||||
role: "user",
|
||||
});
|
||||
let requestMessages;
|
||||
let context;
|
||||
if (this.contextGenerator) {
|
||||
if (Array.isArray(message)) {
|
||||
// message is of type MessageContentDetail[] - retrieve just the text parts and concatenate them
|
||||
// so we can pass them to the context generator
|
||||
message = (message as MessageContentDetail[])
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("\n\n");
|
||||
}
|
||||
context = await this.contextGenerator.generate(message);
|
||||
}
|
||||
requestMessages = await chatHistory.requestMessages(
|
||||
context ? [context.message] : undefined,
|
||||
);
|
||||
return requestMessages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatMessage, LLM, OpenAI } from "./llm/LLM";
|
||||
import { ChatMessage, LLM, MessageType, OpenAI } from "./llm/LLM";
|
||||
import {
|
||||
defaultSummaryPrompt,
|
||||
messagesToHistoryStr,
|
||||
@@ -14,60 +14,187 @@ export interface ChatHistory {
|
||||
* Adds a message to the chat history.
|
||||
* @param message
|
||||
*/
|
||||
addMessage(message: ChatMessage): Promise<void>;
|
||||
addMessage(message: ChatMessage): void;
|
||||
|
||||
/**
|
||||
* Returns the messages that should be used as input to the LLM.
|
||||
*/
|
||||
requestMessages(transientMessages?: ChatMessage[]): Promise<ChatMessage[]>;
|
||||
|
||||
/**
|
||||
* Resets the chat history so that it's empty.
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Returns the new messages since the last call to this function (or since calling the constructor)
|
||||
*/
|
||||
newMessages(): ChatMessage[];
|
||||
}
|
||||
|
||||
export class SimpleChatHistory implements ChatHistory {
|
||||
messages: ChatMessage[];
|
||||
private messagesBefore: number;
|
||||
|
||||
constructor(init?: Partial<SimpleChatHistory>) {
|
||||
this.messages = init?.messages ?? [];
|
||||
this.messagesBefore = this.messages.length;
|
||||
}
|
||||
|
||||
async addMessage(message: ChatMessage) {
|
||||
addMessage(message: ChatMessage) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
|
||||
async requestMessages(transientMessages?: ChatMessage[]) {
|
||||
return [...(transientMessages ?? []), ...this.messages];
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
newMessages() {
|
||||
const newMessages = this.messages.slice(this.messagesBefore);
|
||||
this.messagesBefore = this.messages.length;
|
||||
return newMessages;
|
||||
}
|
||||
}
|
||||
|
||||
export class SummaryChatHistory implements ChatHistory {
|
||||
tokensToSummarize: number;
|
||||
messages: ChatMessage[];
|
||||
summaryPrompt: SummaryPrompt;
|
||||
llm: LLM;
|
||||
private messagesBefore: number;
|
||||
|
||||
constructor(init?: Partial<SummaryChatHistory>) {
|
||||
this.messages = init?.messages ?? [];
|
||||
this.messagesBefore = this.messages.length;
|
||||
this.summaryPrompt = init?.summaryPrompt ?? defaultSummaryPrompt;
|
||||
this.llm = init?.llm ?? new OpenAI();
|
||||
if (!this.llm.metadata.maxTokens) {
|
||||
throw new Error(
|
||||
"LLM maxTokens is not set. Needed so the summarizer ensures the context window size of the LLM.",
|
||||
);
|
||||
}
|
||||
this.tokensToSummarize =
|
||||
this.llm.metadata.contextWindow - this.llm.metadata.maxTokens;
|
||||
}
|
||||
|
||||
private async summarize() {
|
||||
const chatHistoryStr = messagesToHistoryStr(this.messages);
|
||||
private async summarize(): Promise<ChatMessage> {
|
||||
// get the conversation messages to create summary
|
||||
const messagesToSummarize = this.calcConversationMessages();
|
||||
|
||||
const response = await this.llm.complete(
|
||||
this.summaryPrompt({ context: chatHistoryStr }),
|
||||
);
|
||||
let promptMessages;
|
||||
do {
|
||||
promptMessages = [
|
||||
{
|
||||
content: this.summaryPrompt({
|
||||
context: messagesToHistoryStr(messagesToSummarize),
|
||||
}),
|
||||
role: "user" as MessageType,
|
||||
},
|
||||
];
|
||||
// remove oldest message until the chat history is short enough for the context window
|
||||
messagesToSummarize.shift();
|
||||
} while (this.llm.tokens(promptMessages) > this.tokensToSummarize);
|
||||
|
||||
this.messages = [{ content: response.message.content, role: "system" }];
|
||||
const response = await this.llm.chat(promptMessages);
|
||||
return { content: response.message.content, role: "memory" };
|
||||
}
|
||||
|
||||
async addMessage(message: ChatMessage) {
|
||||
// TODO: check if summarization is necessary
|
||||
// TBD what are good conditions, e.g. depending on the context length of the LLM?
|
||||
// for now we just have a dummy implementation at always summarizes the messages
|
||||
await this.summarize();
|
||||
addMessage(message: ChatMessage) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
|
||||
// Find last summary message
|
||||
private getLastSummaryIndex(): number | null {
|
||||
const reversedMessages = this.messages.slice().reverse();
|
||||
const index = reversedMessages.findIndex(
|
||||
(message) => message.role === "memory",
|
||||
);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
return this.messages.length - 1 - index;
|
||||
}
|
||||
|
||||
private get systemMessages() {
|
||||
// get array of all system messages
|
||||
return this.messages.filter((message) => message.role === "system");
|
||||
}
|
||||
|
||||
private get nonSystemMessages() {
|
||||
// get array of all non-system messages
|
||||
return this.messages.filter((message) => message.role !== "system");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the messages that describe the conversation so far.
|
||||
* If there's no memory, all non-system messages are used.
|
||||
* If there's a memory, uses all messages after the last summary message.
|
||||
*/
|
||||
private calcConversationMessages(transformSummary?: boolean): ChatMessage[] {
|
||||
const lastSummaryIndex = this.getLastSummaryIndex();
|
||||
if (!lastSummaryIndex) {
|
||||
// there's no memory, so just use all non-system messages
|
||||
return this.nonSystemMessages;
|
||||
} else {
|
||||
// there's a memory, so use all messages after the last summary message
|
||||
// and convert summary message so it can be send to the LLM
|
||||
const summaryMessage: ChatMessage = transformSummary
|
||||
? {
|
||||
content: `Summary of the conversation so far: ${this.messages[lastSummaryIndex].content}`,
|
||||
role: "system",
|
||||
}
|
||||
: this.messages[lastSummaryIndex];
|
||||
return [summaryMessage, ...this.messages.slice(lastSummaryIndex + 1)];
|
||||
}
|
||||
}
|
||||
|
||||
private calcCurrentRequestMessages(transientMessages?: ChatMessage[]) {
|
||||
// TODO: check order: currently, we're sending:
|
||||
// system messages first, then transient messages and then the messages that describe the conversation so far
|
||||
return [
|
||||
...this.systemMessages,
|
||||
...(transientMessages ? transientMessages : []),
|
||||
...this.calcConversationMessages(true),
|
||||
];
|
||||
}
|
||||
|
||||
async requestMessages(transientMessages?: ChatMessage[]) {
|
||||
const requestMessages = this.calcCurrentRequestMessages(transientMessages);
|
||||
|
||||
// get tokens of current request messages and the transient messages
|
||||
const tokens = this.llm.tokens(requestMessages);
|
||||
if (tokens > this.tokensToSummarize) {
|
||||
// if there are too many tokens for the next request, call summarize
|
||||
const memoryMessage = await this.summarize();
|
||||
const lastMessage = this.messages.at(-1);
|
||||
if (lastMessage && lastMessage.role === "user") {
|
||||
// if last message is a user message, ensure that it's sent after the new memory message
|
||||
this.messages.pop();
|
||||
this.messages.push(memoryMessage);
|
||||
this.messages.push(lastMessage);
|
||||
} else {
|
||||
// otherwise just add the memory message
|
||||
this.messages.push(memoryMessage);
|
||||
}
|
||||
// TODO: we still might have too many tokens
|
||||
// e.g. too large system messages or transient messages
|
||||
// how should we deal with that?
|
||||
return this.calcCurrentRequestMessages(transientMessages);
|
||||
}
|
||||
return requestMessages;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
newMessages() {
|
||||
const newMessages = this.messages.slice(this.messagesBefore);
|
||||
this.messagesBefore = this.messages.length;
|
||||
return newMessages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,54 @@
|
||||
import { encodingForModel } from "js-tiktoken";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Event, EventTag, EventType } from "./callbacks/CallbackManager";
|
||||
|
||||
export enum Tokenizers {
|
||||
CL100K_BASE = "cl100k_base",
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class singleton
|
||||
*/
|
||||
class GlobalsHelper {
|
||||
defaultTokenizer: {
|
||||
encode: (text: string) => number[];
|
||||
decode: (tokens: number[]) => string;
|
||||
encode: (text: string) => Uint32Array;
|
||||
decode: (tokens: Uint32Array) => string;
|
||||
} | null = null;
|
||||
|
||||
tokenizer() {
|
||||
private initDefaultTokenizer() {
|
||||
const encoding = encodingForModel("text-embedding-ada-002"); // cl100k_base
|
||||
|
||||
this.defaultTokenizer = {
|
||||
encode: (text: string) => {
|
||||
return new Uint32Array(encoding.encode(text));
|
||||
},
|
||||
decode: (tokens: Uint32Array) => {
|
||||
const numberArray = Array.from(tokens);
|
||||
const text = encoding.decode(numberArray);
|
||||
const uint8Array = new TextEncoder().encode(text);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
tokenizer(encoding?: string) {
|
||||
if (encoding && encoding !== Tokenizers.CL100K_BASE) {
|
||||
throw new Error(`Tokenizer encoding ${encoding} not yet supported`);
|
||||
}
|
||||
if (!this.defaultTokenizer) {
|
||||
const tiktoken = require("tiktoken-node");
|
||||
this.defaultTokenizer = tiktoken.getEncoding("gpt2");
|
||||
this.initDefaultTokenizer();
|
||||
}
|
||||
|
||||
return this.defaultTokenizer!.encode.bind(this.defaultTokenizer);
|
||||
}
|
||||
|
||||
tokenizerDecoder() {
|
||||
tokenizerDecoder(encoding?: string) {
|
||||
if (encoding && encoding !== Tokenizers.CL100K_BASE) {
|
||||
throw new Error(`Tokenizer encoding ${encoding} not yet supported`);
|
||||
}
|
||||
if (!this.defaultTokenizer) {
|
||||
const tiktoken = require("tiktoken-node");
|
||||
this.defaultTokenizer = tiktoken.getEncoding("gpt2");
|
||||
this.initDefaultTokenizer();
|
||||
}
|
||||
|
||||
return this.defaultTokenizer!.decode.bind(this.defaultTokenizer);
|
||||
|
||||
+25
-21
@@ -23,19 +23,23 @@ export enum MetadataMode {
|
||||
NONE = "NONE",
|
||||
}
|
||||
|
||||
export interface RelatedNodeInfo {
|
||||
export type Metadata = Record<string, any>;
|
||||
|
||||
export interface RelatedNodeInfo<T extends Metadata = Metadata> {
|
||||
nodeId: string;
|
||||
nodeType?: ObjectType;
|
||||
metadata: Record<string, any>;
|
||||
metadata: T;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
export type RelatedNodeType = RelatedNodeInfo | RelatedNodeInfo[];
|
||||
export type RelatedNodeType<T extends Metadata = Metadata> =
|
||||
| RelatedNodeInfo<T>
|
||||
| RelatedNodeInfo<T>[];
|
||||
|
||||
/**
|
||||
* Generic abstract class for retrievable nodes
|
||||
*/
|
||||
export abstract class BaseNode {
|
||||
export abstract class BaseNode<T extends Metadata = Metadata> {
|
||||
/**
|
||||
* The unique ID of the Node/Document. The trailing underscore is here
|
||||
* to avoid collisions with the id keyword in Python.
|
||||
@@ -46,13 +50,13 @@ export abstract class BaseNode {
|
||||
embedding?: number[];
|
||||
|
||||
// Metadata fields
|
||||
metadata: Record<string, any> = {};
|
||||
metadata: T = {} as T;
|
||||
excludedEmbedMetadataKeys: string[] = [];
|
||||
excludedLlmMetadataKeys: string[] = [];
|
||||
relationships: Partial<Record<NodeRelationship, RelatedNodeType>> = {};
|
||||
relationships: Partial<Record<NodeRelationship, RelatedNodeType<T>>> = {};
|
||||
hash: string = "";
|
||||
|
||||
constructor(init?: Partial<BaseNode>) {
|
||||
constructor(init?: Partial<BaseNode<T>>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@@ -62,7 +66,7 @@ export abstract class BaseNode {
|
||||
abstract getMetadataStr(metadataMode: MetadataMode): string;
|
||||
abstract setContent(value: any): void;
|
||||
|
||||
get sourceNode(): RelatedNodeInfo | undefined {
|
||||
get sourceNode(): RelatedNodeInfo<T> | undefined {
|
||||
const relationship = this.relationships[NodeRelationship.SOURCE];
|
||||
|
||||
if (Array.isArray(relationship)) {
|
||||
@@ -72,7 +76,7 @@ export abstract class BaseNode {
|
||||
return relationship;
|
||||
}
|
||||
|
||||
get prevNode(): RelatedNodeInfo | undefined {
|
||||
get prevNode(): RelatedNodeInfo<T> | undefined {
|
||||
const relationship = this.relationships[NodeRelationship.PREVIOUS];
|
||||
|
||||
if (Array.isArray(relationship)) {
|
||||
@@ -84,7 +88,7 @@ export abstract class BaseNode {
|
||||
return relationship;
|
||||
}
|
||||
|
||||
get nextNode(): RelatedNodeInfo | undefined {
|
||||
get nextNode(): RelatedNodeInfo<T> | undefined {
|
||||
const relationship = this.relationships[NodeRelationship.NEXT];
|
||||
|
||||
if (Array.isArray(relationship)) {
|
||||
@@ -94,7 +98,7 @@ export abstract class BaseNode {
|
||||
return relationship;
|
||||
}
|
||||
|
||||
get parentNode(): RelatedNodeInfo | undefined {
|
||||
get parentNode(): RelatedNodeInfo<T> | undefined {
|
||||
const relationship = this.relationships[NodeRelationship.PARENT];
|
||||
|
||||
if (Array.isArray(relationship)) {
|
||||
@@ -104,7 +108,7 @@ export abstract class BaseNode {
|
||||
return relationship;
|
||||
}
|
||||
|
||||
get childNodes(): RelatedNodeInfo[] | undefined {
|
||||
get childNodes(): RelatedNodeInfo<T>[] | undefined {
|
||||
const relationship = this.relationships[NodeRelationship.CHILD];
|
||||
|
||||
if (!Array.isArray(relationship)) {
|
||||
@@ -126,7 +130,7 @@ export abstract class BaseNode {
|
||||
return this.embedding;
|
||||
}
|
||||
|
||||
asRelatedNodeInfo(): RelatedNodeInfo {
|
||||
asRelatedNodeInfo(): RelatedNodeInfo<T> {
|
||||
return {
|
||||
nodeId: this.id_,
|
||||
metadata: this.metadata,
|
||||
@@ -146,7 +150,7 @@ export abstract class BaseNode {
|
||||
/**
|
||||
* TextNode is the default node type for text. Most common node type in LlamaIndex.TS
|
||||
*/
|
||||
export class TextNode extends BaseNode {
|
||||
export class TextNode<T extends Metadata = Metadata> extends BaseNode<T> {
|
||||
text: string = "";
|
||||
startCharIdx?: number;
|
||||
endCharIdx?: number;
|
||||
@@ -154,7 +158,7 @@ export class TextNode extends BaseNode {
|
||||
// metadataTemplate: NOTE write your own formatter if needed
|
||||
metadataSeparator: string = "\n";
|
||||
|
||||
constructor(init?: Partial<TextNode>) {
|
||||
constructor(init?: Partial<TextNode<T>>) {
|
||||
super(init);
|
||||
Object.assign(this, init);
|
||||
|
||||
@@ -233,10 +237,10 @@ export class TextNode extends BaseNode {
|
||||
// }
|
||||
// }
|
||||
|
||||
export class IndexNode extends TextNode {
|
||||
export class IndexNode<T extends Metadata = Metadata> extends TextNode<T> {
|
||||
indexId: string = "";
|
||||
|
||||
constructor(init?: Partial<IndexNode>) {
|
||||
constructor(init?: Partial<IndexNode<T>>) {
|
||||
super(init);
|
||||
Object.assign(this, init);
|
||||
|
||||
@@ -253,8 +257,8 @@ export class IndexNode extends TextNode {
|
||||
/**
|
||||
* A document is just a special text node with a docId.
|
||||
*/
|
||||
export class Document extends TextNode {
|
||||
constructor(init?: Partial<Document>) {
|
||||
export class Document<T extends Metadata = Metadata> extends TextNode<T> {
|
||||
constructor(init?: Partial<Document<T>>) {
|
||||
super(init);
|
||||
Object.assign(this, init);
|
||||
|
||||
@@ -292,7 +296,7 @@ export function jsonToNode(json: any) {
|
||||
/**
|
||||
* A node with a similarity score
|
||||
*/
|
||||
export interface NodeWithScore {
|
||||
node: BaseNode;
|
||||
export interface NodeWithScore<T extends Metadata = Metadata> {
|
||||
node: BaseNode<T>;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
@@ -53,30 +53,31 @@ class OutputParserError extends Error {
|
||||
* @param text A markdown block with JSON
|
||||
* @returns parsed JSON object
|
||||
*/
|
||||
function parseJsonMarkdown(text: string) {
|
||||
export function parseJsonMarkdown(text: string) {
|
||||
text = text.trim();
|
||||
|
||||
const beginDelimiter = "```json";
|
||||
const endDelimiter = "```";
|
||||
const left_square = text.indexOf("[");
|
||||
const left_brace = text.indexOf("{");
|
||||
|
||||
const beginIndex = text.indexOf(beginDelimiter);
|
||||
const endIndex = text.indexOf(
|
||||
endDelimiter,
|
||||
beginIndex + beginDelimiter.length,
|
||||
);
|
||||
if (beginIndex === -1 || endIndex === -1) {
|
||||
throw new OutputParserError("Not a json markdown", { output: text });
|
||||
var left: number;
|
||||
var right: number;
|
||||
if (left_square < left_brace && left_square != -1) {
|
||||
left = left_square;
|
||||
right = text.lastIndexOf("]");
|
||||
} else {
|
||||
left = left_brace;
|
||||
right = text.lastIndexOf("}");
|
||||
}
|
||||
|
||||
const jsonText = text.substring(beginIndex + beginDelimiter.length, endIndex);
|
||||
|
||||
const jsonText = text.substring(left, right + 1);
|
||||
try {
|
||||
//Single JSON object case
|
||||
if (left_square === -1) {
|
||||
return [JSON.parse(jsonText)];
|
||||
}
|
||||
//Multiple JSON object case.
|
||||
return JSON.parse(jsonText);
|
||||
} catch (e) {
|
||||
throw new OutputParserError("Not a valid json", {
|
||||
cause: e as Error,
|
||||
output: text,
|
||||
});
|
||||
throw new OutputParserError("Not a json markdown", { output: text });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class PromptHelper {
|
||||
numOutput = DEFAULT_NUM_OUTPUTS;
|
||||
chunkOverlapRatio = DEFAULT_CHUNK_OVERLAP_RATIO;
|
||||
chunkSizeLimit?: number;
|
||||
tokenizer: (text: string) => number[];
|
||||
tokenizer: (text: string) => Uint32Array;
|
||||
separator = " ";
|
||||
|
||||
constructor(
|
||||
@@ -42,7 +42,7 @@ export class PromptHelper {
|
||||
numOutput = DEFAULT_NUM_OUTPUTS,
|
||||
chunkOverlapRatio = DEFAULT_CHUNK_OVERLAP_RATIO,
|
||||
chunkSizeLimit?: number,
|
||||
tokenizer?: (text: string) => number[],
|
||||
tokenizer?: (text: string) => Uint32Array,
|
||||
separator = " ",
|
||||
) {
|
||||
this.contextWindow = contextWindow;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BaseRetriever } from "./Retriever";
|
||||
import { ServiceContext, serviceContextFromDefaults } from "./ServiceContext";
|
||||
import { QueryEngineTool, ToolMetadata } from "./Tool";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { BaseNodePostprocessor } from "./indices/BaseNodePostprocessor";
|
||||
|
||||
/**
|
||||
* A query engine is a question answerer that can use one or more steps.
|
||||
@@ -30,16 +31,39 @@ export interface BaseQueryEngine {
|
||||
export class RetrieverQueryEngine implements BaseQueryEngine {
|
||||
retriever: BaseRetriever;
|
||||
responseSynthesizer: ResponseSynthesizer;
|
||||
nodePostprocessors: BaseNodePostprocessor[];
|
||||
preFilters?: unknown;
|
||||
|
||||
constructor(
|
||||
retriever: BaseRetriever,
|
||||
responseSynthesizer?: ResponseSynthesizer,
|
||||
preFilters?: unknown,
|
||||
nodePostprocessors?: BaseNodePostprocessor[],
|
||||
) {
|
||||
this.retriever = retriever;
|
||||
const serviceContext: ServiceContext | undefined =
|
||||
this.retriever.getServiceContext();
|
||||
this.responseSynthesizer =
|
||||
responseSynthesizer || new ResponseSynthesizer({ serviceContext });
|
||||
this.preFilters = preFilters;
|
||||
this.nodePostprocessors = nodePostprocessors || [];
|
||||
}
|
||||
|
||||
private applyNodePostprocessors(nodes: NodeWithScore[]) {
|
||||
return this.nodePostprocessors.reduce(
|
||||
(nodes, nodePostprocessor) => nodePostprocessor.postprocessNodes(nodes),
|
||||
nodes,
|
||||
);
|
||||
}
|
||||
|
||||
private async retrieve(query: string, parentEvent: Event) {
|
||||
const nodes = await this.retriever.retrieve(
|
||||
query,
|
||||
parentEvent,
|
||||
this.preFilters,
|
||||
);
|
||||
|
||||
return this.applyNodePostprocessors(nodes);
|
||||
}
|
||||
|
||||
async query(query: string, parentEvent?: Event) {
|
||||
@@ -48,7 +72,7 @@ export class RetrieverQueryEngine implements BaseQueryEngine {
|
||||
type: "wrapper",
|
||||
tags: ["final"],
|
||||
};
|
||||
const nodes = await this.retriever.retrieve(query, _parentEvent);
|
||||
const nodes = await this.retrieve(query, _parentEvent);
|
||||
return this.responseSynthesizer.synthesize(query, nodes, _parentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { LLM } from "./llm/LLM";
|
||||
import { MetadataMode, NodeWithScore } from "./Node";
|
||||
import {
|
||||
defaultRefinePrompt,
|
||||
defaultTextQaPrompt,
|
||||
defaultTreeSummarizePrompt,
|
||||
RefinePrompt,
|
||||
SimplePrompt,
|
||||
TextQaPrompt,
|
||||
TreeSummarizePrompt,
|
||||
defaultRefinePrompt,
|
||||
defaultTextQaPrompt,
|
||||
defaultTreeSummarizePrompt,
|
||||
} from "./Prompt";
|
||||
import { getBiggestPrompt } from "./PromptHelper";
|
||||
import { Response } from "./Response";
|
||||
import { ServiceContext, serviceContextFromDefaults } from "./ServiceContext";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { LLM } from "./llm/LLM";
|
||||
|
||||
/**
|
||||
* Response modes of the response synthesizer
|
||||
@@ -231,6 +231,7 @@ export class TreeSummarize implements BaseResponseBuilder {
|
||||
throw new Error("Must have at least one text chunk");
|
||||
}
|
||||
|
||||
// Should we send the query here too?
|
||||
const packedTextChunks = this.serviceContext.promptHelper.repack(
|
||||
this.summaryTemplate,
|
||||
textChunks,
|
||||
@@ -241,6 +242,7 @@ export class TreeSummarize implements BaseResponseBuilder {
|
||||
await this.serviceContext.llm.complete(
|
||||
this.summaryTemplate({
|
||||
context: packedTextChunks[0],
|
||||
query,
|
||||
}),
|
||||
parentEvent,
|
||||
)
|
||||
@@ -251,6 +253,7 @@ export class TreeSummarize implements BaseResponseBuilder {
|
||||
this.serviceContext.llm.complete(
|
||||
this.summaryTemplate({
|
||||
context: chunk,
|
||||
query,
|
||||
}),
|
||||
parentEvent,
|
||||
),
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { NodeWithScore } from "./Node";
|
||||
import { ServiceContext } from "./ServiceContext";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
|
||||
/**
|
||||
* Retrievers retrieve the nodes that most closely match our query in similarity.
|
||||
*/
|
||||
export interface BaseRetriever {
|
||||
retrieve(query: string, parentEvent?: Event): Promise<NodeWithScore[]>;
|
||||
retrieve(
|
||||
query: string,
|
||||
parentEvent?: Event,
|
||||
preFilters?: unknown,
|
||||
): Promise<NodeWithScore[]>;
|
||||
getServiceContext(): ServiceContext;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ interface BaseCallbackResponse {
|
||||
event: Event;
|
||||
}
|
||||
|
||||
export interface StreamToken {
|
||||
//Specify StreamToken per mainstream LLM
|
||||
export interface DefaultStreamToken {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
@@ -29,16 +30,34 @@ export interface StreamToken {
|
||||
index: number;
|
||||
delta: {
|
||||
content?: string | null;
|
||||
role?: "user" | "assistant" | "system" | "function";
|
||||
role?: "user" | "assistant" | "system" | "function" | "tool";
|
||||
};
|
||||
finish_reason: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
//OpenAI stream token schema is the default.
|
||||
//Note: Anthropic and Replicate also use similar token schemas.
|
||||
export type OpenAIStreamToken = DefaultStreamToken;
|
||||
export type AnthropicStreamToken = {
|
||||
completion: string;
|
||||
model: string;
|
||||
stop_reason: string | undefined;
|
||||
stop?: boolean | undefined;
|
||||
log_id?: string;
|
||||
};
|
||||
|
||||
//
|
||||
//Callback Responses
|
||||
//
|
||||
//TODO: Write Embedding Callbacks
|
||||
|
||||
//StreamCallbackResponse should let practitioners implement callbacks out of the box...
|
||||
//When custom streaming LLMs are involved, people are expected to write their own StreamCallbackResponses
|
||||
export interface StreamCallbackResponse extends BaseCallbackResponse {
|
||||
index: number;
|
||||
isDone?: boolean;
|
||||
token?: StreamToken;
|
||||
token?: DefaultStreamToken;
|
||||
}
|
||||
|
||||
export interface RetrievalCallbackResponse extends BaseCallbackResponse {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ChatCompletionChunk } from "openai/resources/chat";
|
||||
import { Stream } from "openai/streaming";
|
||||
import { globalsHelper } from "../../GlobalsHelper";
|
||||
import { MessageType } from "../../llm/LLM";
|
||||
import { Event, StreamCallbackResponse } from "../CallbackManager";
|
||||
|
||||
/**
|
||||
* Handles the OpenAI streaming interface and pipes it to the callback function
|
||||
* @param response - The response from the OpenAI API.
|
||||
* @param onLLMStream - A callback function to handle the LLM stream.
|
||||
* @param parentEvent - An optional parent event.
|
||||
* @returns A promise that resolves to an object with a message and a role.
|
||||
*/
|
||||
export async function handleOpenAIStream({
|
||||
response,
|
||||
onLLMStream,
|
||||
parentEvent,
|
||||
}: {
|
||||
response: Stream<ChatCompletionChunk>;
|
||||
onLLMStream: (data: StreamCallbackResponse) => void;
|
||||
parentEvent?: Event;
|
||||
}): Promise<{ message: string; role: MessageType }> {
|
||||
const event = globalsHelper.createEvent({
|
||||
parentEvent,
|
||||
type: "llmPredict",
|
||||
});
|
||||
let index = 0;
|
||||
let cumulativeText = "";
|
||||
let messageRole: MessageType = "assistant";
|
||||
for await (const part of response) {
|
||||
const { content = "", role = "assistant" } = part.choices[0].delta;
|
||||
|
||||
// ignore the first token
|
||||
if (!content && role === "assistant" && index === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cumulativeText += content;
|
||||
messageRole = role;
|
||||
onLLMStream?.({ event, index, token: part });
|
||||
index++;
|
||||
}
|
||||
onLLMStream?.({ event, index, isDone: true });
|
||||
return { message: cumulativeText, role: messageRole };
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
export * from "./ChatEngine";
|
||||
export * from "./ChatHistory";
|
||||
export * from "./Embedding";
|
||||
export * from "./GlobalsHelper";
|
||||
export * from "./Node";
|
||||
export * from "./NodeParser";
|
||||
export * from "./OutputParser";
|
||||
export * from "./Prompt";
|
||||
export * from "./PromptHelper";
|
||||
export * from "./QueryEngine";
|
||||
export * from "./QuestionGenerator";
|
||||
export * from "./Response";
|
||||
@@ -13,18 +15,15 @@ export * from "./Retriever";
|
||||
export * from "./ServiceContext";
|
||||
export * from "./TextSplitter";
|
||||
export * from "./Tool";
|
||||
export * from "./constants";
|
||||
export * from "./llm/LLM";
|
||||
|
||||
export * from "./indices";
|
||||
|
||||
export * from "./callbacks/CallbackManager";
|
||||
|
||||
export * from "./constants";
|
||||
export * from "./indices";
|
||||
export * from "./llm/LLM";
|
||||
export * from "./readers/CSVReader";
|
||||
export * from "./readers/HTMLReader";
|
||||
export * from "./readers/MarkdownReader";
|
||||
export * from "./readers/NotionReader";
|
||||
export * from "./readers/PDFReader";
|
||||
export * from "./readers/SimpleDirectoryReader";
|
||||
export * from "./readers/base";
|
||||
|
||||
export * from "./storage";
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NodeWithScore } from "../Node";
|
||||
|
||||
export interface BaseNodePostprocessor {
|
||||
postprocessNodes: (nodes: NodeWithScore[]) => NodeWithScore[];
|
||||
}
|
||||
|
||||
export class SimilarityPostprocessor implements BaseNodePostprocessor {
|
||||
similarityCutoff?: number;
|
||||
|
||||
constructor(options?: { similarityCutoff?: number }) {
|
||||
this.similarityCutoff = options?.similarityCutoff;
|
||||
}
|
||||
|
||||
postprocessNodes(nodes: NodeWithScore[]) {
|
||||
if (this.similarityCutoff === undefined) return nodes;
|
||||
|
||||
const cutoff = this.similarityCutoff || 0;
|
||||
return nodes.filter((node) => node.score && node.score >= cutoff);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./BaseIndex";
|
||||
export * from "./BaseNodePostprocessor";
|
||||
export * from "./keyword";
|
||||
export * from "./summary";
|
||||
export * from "./vectorStore";
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
IndexStructType,
|
||||
KeywordTable,
|
||||
} from "../BaseIndex";
|
||||
import { BaseNodePostprocessor } from "../BaseNodePostprocessor";
|
||||
import {
|
||||
KeywordTableLLMRetriever,
|
||||
KeywordTableRAKERetriever,
|
||||
@@ -129,11 +130,15 @@ export class KeywordTableIndex extends BaseIndex<KeywordTable> {
|
||||
asQueryEngine(options?: {
|
||||
retriever?: BaseRetriever;
|
||||
responseSynthesizer?: ResponseSynthesizer;
|
||||
preFilters?: unknown;
|
||||
nodePostprocessors?: BaseNodePostprocessor[];
|
||||
}): BaseQueryEngine {
|
||||
const { retriever, responseSynthesizer } = options ?? {};
|
||||
return new RetrieverQueryEngine(
|
||||
retriever ?? this.asRetriever(),
|
||||
responseSynthesizer,
|
||||
options?.preFilters,
|
||||
options?.nodePostprocessors,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
IndexList,
|
||||
IndexStructType,
|
||||
} from "../BaseIndex";
|
||||
import { BaseNodePostprocessor } from "../BaseNodePostprocessor";
|
||||
import {
|
||||
SummaryIndexLLMRetriever,
|
||||
SummaryIndexRetriever,
|
||||
@@ -155,6 +156,8 @@ export class SummaryIndex extends BaseIndex<IndexList> {
|
||||
asQueryEngine(options?: {
|
||||
retriever?: BaseRetriever;
|
||||
responseSynthesizer?: ResponseSynthesizer;
|
||||
preFilters?: unknown;
|
||||
nodePostprocessors?: BaseNodePostprocessor[];
|
||||
}): BaseQueryEngine {
|
||||
let { retriever, responseSynthesizer } = options ?? {};
|
||||
|
||||
@@ -170,7 +173,12 @@ export class SummaryIndex extends BaseIndex<IndexList> {
|
||||
});
|
||||
}
|
||||
|
||||
return new RetrieverQueryEngine(retriever, responseSynthesizer);
|
||||
return new RetrieverQueryEngine(
|
||||
retriever,
|
||||
responseSynthesizer,
|
||||
options?.preFilters,
|
||||
options?.nodePostprocessors,
|
||||
);
|
||||
}
|
||||
|
||||
static async buildIndexFromNodes(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Event } from "../../callbacks/CallbackManager";
|
||||
import { DEFAULT_SIMILARITY_TOP_K } from "../../constants";
|
||||
import { globalsHelper } from "../../GlobalsHelper";
|
||||
import { NodeWithScore } from "../../Node";
|
||||
import { BaseRetriever } from "../../Retriever";
|
||||
import { ServiceContext } from "../../ServiceContext";
|
||||
import { Event } from "../../callbacks/CallbackManager";
|
||||
import { DEFAULT_SIMILARITY_TOP_K } from "../../constants";
|
||||
import {
|
||||
VectorStoreQuery,
|
||||
VectorStoreQueryMode,
|
||||
@@ -32,7 +32,11 @@ export class VectorIndexRetriever implements BaseRetriever {
|
||||
this.similarityTopK = similarityTopK ?? DEFAULT_SIMILARITY_TOP_K;
|
||||
}
|
||||
|
||||
async retrieve(query: string, parentEvent?: Event): Promise<NodeWithScore[]> {
|
||||
async retrieve(
|
||||
query: string,
|
||||
parentEvent?: Event,
|
||||
preFilters?: unknown,
|
||||
): Promise<NodeWithScore[]> {
|
||||
const queryEmbedding =
|
||||
await this.serviceContext.embedModel.getQueryEmbedding(query);
|
||||
|
||||
@@ -41,10 +45,15 @@ export class VectorIndexRetriever implements BaseRetriever {
|
||||
mode: VectorStoreQueryMode.DEFAULT,
|
||||
similarityTopK: this.similarityTopK,
|
||||
};
|
||||
const result = await this.index.vectorStore.query(q);
|
||||
const result = await this.index.vectorStore.query(q, preFilters);
|
||||
|
||||
let nodesWithScores: NodeWithScore[] = [];
|
||||
for (let i = 0; i < result.ids.length; i++) {
|
||||
const nodeFromResult = result.nodes?.[i];
|
||||
if (!this.index.indexStruct.nodesDict[result.ids[i]] && nodeFromResult) {
|
||||
this.index.indexStruct.nodesDict[result.ids[i]] = nodeFromResult;
|
||||
}
|
||||
|
||||
const node = this.index.indexStruct.nodesDict[result.ids[i]];
|
||||
nodesWithScores.push({
|
||||
node: node,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IndexDict,
|
||||
IndexStructType,
|
||||
} from "../BaseIndex";
|
||||
import { BaseNodePostprocessor } from "../BaseNodePostprocessor";
|
||||
import { VectorIndexRetriever } from "./VectorIndexRetriever";
|
||||
|
||||
export interface VectorIndexOptions {
|
||||
@@ -87,24 +88,23 @@ export class VectorStoreIndex extends BaseIndex<IndexDict> {
|
||||
);
|
||||
}
|
||||
|
||||
if (!indexStruct && !options.nodes) {
|
||||
if (options.nodes) {
|
||||
// If nodes are passed in, then we need to update the index
|
||||
indexStruct = await VectorStoreIndex.buildIndexFromNodes(
|
||||
options.nodes,
|
||||
serviceContext,
|
||||
vectorStore,
|
||||
docStore,
|
||||
indexStruct,
|
||||
);
|
||||
|
||||
await indexStore.addIndexStruct(indexStruct);
|
||||
} else if (!indexStruct) {
|
||||
throw new Error(
|
||||
"Cannot initialize VectorStoreIndex without nodes or indexStruct",
|
||||
);
|
||||
}
|
||||
|
||||
const nodes = options.nodes ?? [];
|
||||
|
||||
indexStruct = await VectorStoreIndex.buildIndexFromNodes(
|
||||
nodes,
|
||||
serviceContext,
|
||||
vectorStore,
|
||||
docStore,
|
||||
indexStruct,
|
||||
);
|
||||
|
||||
await indexStore.addIndexStruct(indexStruct);
|
||||
|
||||
return new VectorStoreIndex({
|
||||
storageContext,
|
||||
serviceContext,
|
||||
@@ -219,6 +219,27 @@ export class VectorStoreIndex extends BaseIndex<IndexDict> {
|
||||
return index;
|
||||
}
|
||||
|
||||
static async fromVectorStore(
|
||||
vectorStore: VectorStore,
|
||||
serviceContext: ServiceContext,
|
||||
) {
|
||||
if (!vectorStore.storesText) {
|
||||
throw new Error(
|
||||
"Cannot initialize from a vector store that does not store text",
|
||||
);
|
||||
}
|
||||
|
||||
const storageContext = await storageContextFromDefaults({ vectorStore });
|
||||
|
||||
const index = await VectorStoreIndex.init({
|
||||
nodes: [],
|
||||
storageContext,
|
||||
serviceContext,
|
||||
});
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
asRetriever(options?: any): VectorIndexRetriever {
|
||||
return new VectorIndexRetriever({ index: this, ...options });
|
||||
}
|
||||
@@ -226,11 +247,15 @@ export class VectorStoreIndex extends BaseIndex<IndexDict> {
|
||||
asQueryEngine(options?: {
|
||||
retriever?: BaseRetriever;
|
||||
responseSynthesizer?: ResponseSynthesizer;
|
||||
preFilters?: unknown;
|
||||
nodePostprocessors?: BaseNodePostprocessor[];
|
||||
}): BaseQueryEngine {
|
||||
const { retriever, responseSynthesizer } = options ?? {};
|
||||
return new RetrieverQueryEngine(
|
||||
retriever ?? this.asRetriever(),
|
||||
responseSynthesizer,
|
||||
options?.preFilters,
|
||||
options?.nodePostprocessors,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+467
-69
@@ -1,6 +1,16 @@
|
||||
import OpenAILLM, { ClientOptions as OpenAIClientOptions } from "openai";
|
||||
import { CallbackManager, Event } from "../callbacks/CallbackManager";
|
||||
import { handleOpenAIStream } from "../callbacks/utility/handleOpenAIStream";
|
||||
import {
|
||||
AnthropicStreamToken,
|
||||
CallbackManager,
|
||||
Event,
|
||||
EventType,
|
||||
OpenAIStreamToken,
|
||||
StreamCallbackResponse,
|
||||
} from "../callbacks/CallbackManager";
|
||||
|
||||
import { ChatCompletionMessageParam } from "openai/resources";
|
||||
import { LLMOptions } from "portkey-ai";
|
||||
import { globalsHelper, Tokenizers } from "../GlobalsHelper";
|
||||
import {
|
||||
ANTHROPIC_AI_PROMPT,
|
||||
ANTHROPIC_HUMAN_PROMPT,
|
||||
@@ -14,7 +24,8 @@ import {
|
||||
getAzureModel,
|
||||
shouldUseAzure,
|
||||
} from "./azure";
|
||||
import { OpenAISession, getOpenAISession } from "./openai";
|
||||
import { getOpenAISession, OpenAISession } from "./openai";
|
||||
import { getPortkeySession, PortkeySession } from "./portkey";
|
||||
import { ReplicateSession } from "./replicate";
|
||||
|
||||
export type MessageType =
|
||||
@@ -22,10 +33,11 @@ export type MessageType =
|
||||
| "assistant"
|
||||
| "system"
|
||||
| "generic"
|
||||
| "function";
|
||||
| "function"
|
||||
| "memory";
|
||||
|
||||
export interface ChatMessage {
|
||||
content: string;
|
||||
content: any;
|
||||
role: MessageType;
|
||||
}
|
||||
|
||||
@@ -38,31 +50,67 @@ export interface ChatResponse {
|
||||
// NOTE in case we need CompletionResponse to diverge from ChatResponse in the future
|
||||
export type CompletionResponse = ChatResponse;
|
||||
|
||||
export interface LLMMetadata {
|
||||
model: string;
|
||||
temperature: number;
|
||||
topP: number;
|
||||
maxTokens?: number;
|
||||
contextWindow: number;
|
||||
tokenizer: Tokenizers | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified language model interface
|
||||
*/
|
||||
export interface LLM {
|
||||
metadata: LLMMetadata;
|
||||
// Whether a LLM has streaming support
|
||||
hasStreaming: boolean;
|
||||
/**
|
||||
* Get a chat response from the LLM
|
||||
* @param messages
|
||||
*
|
||||
* The return type of chat() and complete() are set by the "streaming" parameter being set to True.
|
||||
*/
|
||||
chat(messages: ChatMessage[], parentEvent?: Event): Promise<ChatResponse>;
|
||||
chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event,
|
||||
streaming?: T,
|
||||
): Promise<R>;
|
||||
|
||||
/**
|
||||
* Get a prompt completion from the LLM
|
||||
* @param prompt the prompt to complete
|
||||
*/
|
||||
complete(prompt: string, parentEvent?: Event): Promise<CompletionResponse>;
|
||||
complete<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(
|
||||
prompt: string,
|
||||
parentEvent?: Event,
|
||||
streaming?: T,
|
||||
): Promise<R>;
|
||||
|
||||
/**
|
||||
* Calculates the number of tokens needed for the given chat messages
|
||||
*/
|
||||
tokens(messages: ChatMessage[]): number;
|
||||
}
|
||||
|
||||
export const GPT4_MODELS = {
|
||||
"gpt-4": { contextWindow: 8192 },
|
||||
"gpt-4-32k": { contextWindow: 32768 },
|
||||
"gpt-4-1106-preview": { contextWindow: 128000 },
|
||||
"gpt-4-vision-preview": { contextWindow: 8192 },
|
||||
};
|
||||
|
||||
export const TURBO_MODELS = {
|
||||
export const GPT35_MODELS = {
|
||||
"gpt-3.5-turbo": { contextWindow: 4096 },
|
||||
"gpt-3.5-turbo-16k": { contextWindow: 16384 },
|
||||
"gpt-3.5-turbo-1106": { contextWindow: 16384 },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -70,20 +118,22 @@ export const TURBO_MODELS = {
|
||||
*/
|
||||
export const ALL_AVAILABLE_OPENAI_MODELS = {
|
||||
...GPT4_MODELS,
|
||||
...TURBO_MODELS,
|
||||
...GPT35_MODELS,
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAI LLM implementation
|
||||
*/
|
||||
export class OpenAI implements LLM {
|
||||
hasStreaming: boolean = true;
|
||||
|
||||
// Per completion OpenAI params
|
||||
model: keyof typeof ALL_AVAILABLE_OPENAI_MODELS;
|
||||
temperature: number;
|
||||
topP: number;
|
||||
maxTokens?: number;
|
||||
additionalChatOptions?: Omit<
|
||||
Partial<OpenAILLM.Chat.CompletionCreateParams>,
|
||||
Partial<OpenAILLM.Chat.ChatCompletionCreateParams>,
|
||||
"max_tokens" | "messages" | "model" | "temperature" | "top_p" | "streaming"
|
||||
>;
|
||||
|
||||
@@ -153,6 +203,32 @@ export class OpenAI implements LLM {
|
||||
this.callbackManager = init?.callbackManager;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return {
|
||||
model: this.model,
|
||||
temperature: this.temperature,
|
||||
topP: this.topP,
|
||||
maxTokens: this.maxTokens,
|
||||
contextWindow: ALL_AVAILABLE_OPENAI_MODELS[this.model].contextWindow,
|
||||
tokenizer: Tokenizers.CL100K_BASE,
|
||||
};
|
||||
}
|
||||
|
||||
tokens(messages: ChatMessage[]): number {
|
||||
// for latest OpenAI models, see https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
const tokenizer = globalsHelper.tokenizer(this.metadata.tokenizer);
|
||||
const tokensPerMessage = 3;
|
||||
let numTokens = 0;
|
||||
for (const message of messages) {
|
||||
numTokens += tokensPerMessage;
|
||||
for (const value of Object.values(message)) {
|
||||
numTokens += tokenizer(value).length;
|
||||
}
|
||||
}
|
||||
numTokens += 3; // every reply is primed with <|im_start|>assistant<|im_sep|>
|
||||
return numTokens;
|
||||
}
|
||||
|
||||
mapMessageType(
|
||||
messageType: MessageType,
|
||||
): "user" | "assistant" | "system" | "function" {
|
||||
@@ -170,52 +246,124 @@ export class OpenAI implements LLM {
|
||||
}
|
||||
}
|
||||
|
||||
async chat(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event,
|
||||
): Promise<ChatResponse> {
|
||||
const baseRequestParams: OpenAILLM.Chat.CompletionCreateParams = {
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(messages: ChatMessage[], parentEvent?: Event, streaming?: T): Promise<R> {
|
||||
const baseRequestParams: OpenAILLM.Chat.ChatCompletionCreateParams = {
|
||||
model: this.model,
|
||||
temperature: this.temperature,
|
||||
max_tokens: this.maxTokens,
|
||||
messages: messages.map((message) => ({
|
||||
role: this.mapMessageType(message.role),
|
||||
content: message.content,
|
||||
})),
|
||||
messages: messages.map(
|
||||
(message) =>
|
||||
({
|
||||
role: this.mapMessageType(message.role),
|
||||
content: message.content,
|
||||
}) as ChatCompletionMessageParam,
|
||||
),
|
||||
top_p: this.topP,
|
||||
...this.additionalChatOptions,
|
||||
};
|
||||
// Streaming
|
||||
if (streaming) {
|
||||
if (!this.hasStreaming) {
|
||||
throw Error("No streaming support for this LLM.");
|
||||
}
|
||||
return this.streamChat(messages, parentEvent) as R;
|
||||
}
|
||||
// Non-streaming
|
||||
const response = await this.session.openai.chat.completions.create({
|
||||
...baseRequestParams,
|
||||
stream: false,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message?.content ?? "";
|
||||
return {
|
||||
message: { content, role: response.choices[0].message.role },
|
||||
} as R;
|
||||
}
|
||||
|
||||
async complete<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(prompt: string, parentEvent?: Event, streaming?: T): Promise<R> {
|
||||
return this.chat(
|
||||
[{ content: prompt, role: "user" }],
|
||||
parentEvent,
|
||||
streaming,
|
||||
);
|
||||
}
|
||||
|
||||
//We can wrap a stream in a generator to add some additional logging behavior
|
||||
//For future edits: syntax for generator type is <typeof Yield, typeof Return, typeof Accept>
|
||||
//"typeof Accept" refers to what types you'll accept when you manually call generator.next(<AcceptType>)
|
||||
protected async *streamChat(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
const baseRequestParams: OpenAILLM.Chat.ChatCompletionCreateParams = {
|
||||
model: this.model,
|
||||
temperature: this.temperature,
|
||||
max_tokens: this.maxTokens,
|
||||
messages: messages.map(
|
||||
(message) =>
|
||||
({
|
||||
role: this.mapMessageType(message.role),
|
||||
content: message.content,
|
||||
}) as ChatCompletionMessageParam,
|
||||
),
|
||||
top_p: this.topP,
|
||||
...this.additionalChatOptions,
|
||||
};
|
||||
|
||||
if (this.callbackManager?.onLLMStream) {
|
||||
// Streaming
|
||||
const response = await this.session.openai.chat.completions.create({
|
||||
//Now let's wrap our stream in a callback
|
||||
const onLLMStream = this.callbackManager?.onLLMStream
|
||||
? this.callbackManager.onLLMStream
|
||||
: () => {};
|
||||
|
||||
const chunk_stream: AsyncIterable<OpenAIStreamToken> =
|
||||
await this.session.openai.chat.completions.create({
|
||||
...baseRequestParams,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
const { message, role } = await handleOpenAIStream({
|
||||
response,
|
||||
onLLMStream: this.callbackManager.onLLMStream,
|
||||
parentEvent,
|
||||
});
|
||||
return { message: { content: message, role } };
|
||||
} else {
|
||||
// Non-streaming
|
||||
const response = await this.session.openai.chat.completions.create({
|
||||
...baseRequestParams,
|
||||
stream: false,
|
||||
});
|
||||
const event: Event = parentEvent
|
||||
? parentEvent
|
||||
: {
|
||||
id: "unspecified",
|
||||
type: "llmPredict" as EventType,
|
||||
};
|
||||
|
||||
const content = response.choices[0].message?.content ?? "";
|
||||
return { message: { content, role: response.choices[0].message.role } };
|
||||
//Indices
|
||||
var idx_counter: number = 0;
|
||||
for await (const part of chunk_stream) {
|
||||
//Increment
|
||||
part.choices[0].index = idx_counter;
|
||||
const is_done: boolean =
|
||||
part.choices[0].finish_reason === "stop" ? true : false;
|
||||
//onLLMStream Callback
|
||||
|
||||
const stream_callback: StreamCallbackResponse = {
|
||||
event: event,
|
||||
index: idx_counter,
|
||||
isDone: is_done,
|
||||
token: part,
|
||||
};
|
||||
onLLMStream(stream_callback);
|
||||
|
||||
idx_counter++;
|
||||
|
||||
yield part.choices[0].delta.content ? part.choices[0].delta.content : "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async complete(
|
||||
prompt: string,
|
||||
//streamComplete doesn't need to be async because it's child function is already async
|
||||
protected streamComplete(
|
||||
query: string,
|
||||
parentEvent?: Event,
|
||||
): Promise<CompletionResponse> {
|
||||
return this.chat([{ content: prompt, role: "user" }], parentEvent);
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
return this.streamChat([{ content: query, role: "user" }], parentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,10 +377,10 @@ export const ALL_AVAILABLE_LLAMADEUCE_MODELS = {
|
||||
"Llama-2-70b-chat-4bit": {
|
||||
contextWindow: 4096,
|
||||
replicateApi:
|
||||
"replicate/llama70b-v2-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1",
|
||||
"meta/llama-2-70b-chat:02e509c789964a7ea8736978a43525956ef40397be9033abf9fd2badfe68c9e3",
|
||||
//^ Model is based off of exllama 4bit.
|
||||
},
|
||||
"Llama-2-13b-chat": {
|
||||
"Llama-2-13b-chat-old": {
|
||||
contextWindow: 4096,
|
||||
replicateApi:
|
||||
"a16z-infra/llama13b-v2-chat:df7690f1994d94e96ad9d568eac121aecf50684a0b0963b25a41cc40061269e5",
|
||||
@@ -241,9 +389,9 @@ export const ALL_AVAILABLE_LLAMADEUCE_MODELS = {
|
||||
"Llama-2-13b-chat-4bit": {
|
||||
contextWindow: 4096,
|
||||
replicateApi:
|
||||
"a16z-infra/llama13b-v2-chat:2a7f981751ec7fdf87b5b91ad4db53683a98082e9ff7bfd12c8cd5ea85980a52",
|
||||
"meta/llama-2-13b-chat:f4e2de70d66816a838a89eeeb621910adffb0dd0baba3976c96980970978018d",
|
||||
},
|
||||
"Llama-2-7b-chat": {
|
||||
"Llama-2-7b-chat-old": {
|
||||
contextWindow: 4096,
|
||||
replicateApi:
|
||||
"a16z-infra/llama7b-v2-chat:4f0a4744c7295c024a1de15e1a63c880d3da035fa1f49bfd344fe076074c8eea",
|
||||
@@ -255,7 +403,7 @@ export const ALL_AVAILABLE_LLAMADEUCE_MODELS = {
|
||||
"Llama-2-7b-chat-4bit": {
|
||||
contextWindow: 4096,
|
||||
replicateApi:
|
||||
"a16z-infra/llama7b-v2-chat:4f0b260b6a13eb53a6b1891f089d57c08f41003ae79458be5011303d81a394dc",
|
||||
"meta/llama-2-7b-chat:13c3cdee13ee059ab779f0291d29054dab00a47dad8261375654de5540165fb0",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -267,6 +415,8 @@ export enum DeuceChatStrategy {
|
||||
// Unfortunately any string only API won't support these properly.
|
||||
REPLICATE4BIT = "replicate4bit",
|
||||
//^ To satisfy Replicate's 4 bit models' requirements where they also insert some INST tags
|
||||
REPLICATE4BITWNEWLINES = "replicate4bitwnewlines",
|
||||
//^ Replicate's documentation recommends using newlines: https://replicate.com/blog/how-to-prompt-llama
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,13 +429,14 @@ export class LlamaDeuce implements LLM {
|
||||
topP: number;
|
||||
maxTokens?: number;
|
||||
replicateSession: ReplicateSession;
|
||||
hasStreaming: boolean;
|
||||
|
||||
constructor(init?: Partial<LlamaDeuce>) {
|
||||
this.model = init?.model ?? "Llama-2-70b-chat-4bit";
|
||||
this.chatStrategy =
|
||||
init?.chatStrategy ??
|
||||
(this.model.endsWith("4bit")
|
||||
? DeuceChatStrategy.REPLICATE4BIT // With the newer A16Z/Replicate models they do the system message themselves.
|
||||
? DeuceChatStrategy.REPLICATE4BITWNEWLINES // With the newer Replicate models they do the system message themselves.
|
||||
: DeuceChatStrategy.METAWBOS); // With BOS and EOS seems to work best, although they all have problems past a certain point
|
||||
this.temperature = init?.temperature ?? 0.1; // minimum temperature is 0.01 for Replicate endpoint
|
||||
this.topP = init?.topP ?? 1;
|
||||
@@ -293,6 +444,22 @@ export class LlamaDeuce implements LLM {
|
||||
init?.maxTokens ??
|
||||
ALL_AVAILABLE_LLAMADEUCE_MODELS[this.model].contextWindow; // For Replicate, the default is 500 tokens which is too low.
|
||||
this.replicateSession = init?.replicateSession ?? new ReplicateSession();
|
||||
this.hasStreaming = init?.hasStreaming ?? false;
|
||||
}
|
||||
|
||||
tokens(messages: ChatMessage[]): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return {
|
||||
model: this.model,
|
||||
temperature: this.temperature,
|
||||
topP: this.topP,
|
||||
maxTokens: this.maxTokens,
|
||||
contextWindow: ALL_AVAILABLE_LLAMADEUCE_MODELS[this.model].contextWindow,
|
||||
tokenizer: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
mapMessagesToPrompt(messages: ChatMessage[]) {
|
||||
@@ -303,7 +470,15 @@ export class LlamaDeuce implements LLM {
|
||||
} else if (this.chatStrategy === DeuceChatStrategy.METAWBOS) {
|
||||
return this.mapMessagesToPromptMeta(messages, { withBos: true });
|
||||
} else if (this.chatStrategy === DeuceChatStrategy.REPLICATE4BIT) {
|
||||
return this.mapMessagesToPromptMeta(messages, { replicate4Bit: true });
|
||||
return this.mapMessagesToPromptMeta(messages, {
|
||||
replicate4Bit: true,
|
||||
withNewlines: true,
|
||||
});
|
||||
} else if (this.chatStrategy === DeuceChatStrategy.REPLICATE4BITWNEWLINES) {
|
||||
return this.mapMessagesToPromptMeta(messages, {
|
||||
replicate4Bit: true,
|
||||
withNewlines: true,
|
||||
});
|
||||
} else {
|
||||
return this.mapMessagesToPromptMeta(messages);
|
||||
}
|
||||
@@ -338,9 +513,17 @@ export class LlamaDeuce implements LLM {
|
||||
|
||||
mapMessagesToPromptMeta(
|
||||
messages: ChatMessage[],
|
||||
opts?: { withBos?: boolean; replicate4Bit?: boolean },
|
||||
opts?: {
|
||||
withBos?: boolean;
|
||||
replicate4Bit?: boolean;
|
||||
withNewlines?: boolean;
|
||||
},
|
||||
) {
|
||||
const { withBos = false, replicate4Bit = false } = opts ?? {};
|
||||
const {
|
||||
withBos = false,
|
||||
replicate4Bit = false,
|
||||
withNewlines = false,
|
||||
} = opts ?? {};
|
||||
const DEFAULT_SYSTEM_PROMPT = `You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.
|
||||
|
||||
If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.`;
|
||||
@@ -388,21 +571,28 @@ If a question does not make any sense, or is not factually coherent, explain why
|
||||
return {
|
||||
prompt: messages.reduce((acc, message, index) => {
|
||||
if (index % 2 === 0) {
|
||||
return `${acc}${
|
||||
withBos ? BOS : ""
|
||||
}${B_INST} ${message.content.trim()} ${E_INST}`;
|
||||
return (
|
||||
`${acc}${
|
||||
withBos ? BOS : ""
|
||||
}${B_INST} ${message.content.trim()} ${E_INST}` +
|
||||
(withNewlines ? "\n" : "")
|
||||
);
|
||||
} else {
|
||||
return `${acc} ${message.content.trim()} ` + (withBos ? EOS : ""); // Yes, the EOS comes after the space. This is not a mistake.
|
||||
return (
|
||||
`${acc} ${message.content.trim()}` +
|
||||
(withNewlines ? "\n" : " ") +
|
||||
(withBos ? EOS : "")
|
||||
); // Yes, the EOS comes after the space. This is not a mistake.
|
||||
}
|
||||
}, ""),
|
||||
systemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
async chat(
|
||||
messages: ChatMessage[],
|
||||
_parentEvent?: Event,
|
||||
): Promise<ChatResponse> {
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(messages: ChatMessage[], _parentEvent?: Event, streaming?: T): Promise<R> {
|
||||
const api = ALL_AVAILABLE_LLAMADEUCE_MODELS[this.model]
|
||||
.replicateApi as `${string}/${string}:${string}`;
|
||||
|
||||
@@ -423,6 +613,9 @@ If a question does not make any sense, or is not factually coherent, explain why
|
||||
replicateOptions.input.max_length = this.maxTokens;
|
||||
}
|
||||
|
||||
//TODO: Add streaming for this
|
||||
|
||||
//Non-streaming
|
||||
const response = await this.replicateSession.replicate.run(
|
||||
api,
|
||||
replicateOptions,
|
||||
@@ -433,24 +626,32 @@ If a question does not make any sense, or is not factually coherent, explain why
|
||||
//^ We need to do this because Replicate returns a list of strings (for streaming functionality which is not exposed by the run function)
|
||||
role: "assistant",
|
||||
},
|
||||
};
|
||||
} as R;
|
||||
}
|
||||
|
||||
async complete(
|
||||
prompt: string,
|
||||
parentEvent?: Event,
|
||||
): Promise<CompletionResponse> {
|
||||
async complete<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(prompt: string, parentEvent?: Event, streaming?: T): Promise<R> {
|
||||
return this.chat([{ content: prompt, role: "user" }], parentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export const ALL_AVAILABLE_ANTHROPIC_MODELS = {
|
||||
// both models have 100k context window, see https://docs.anthropic.com/claude/reference/selecting-a-model
|
||||
"claude-2": { contextWindow: 100000 },
|
||||
"claude-instant-1": { contextWindow: 100000 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Anthropic LLM implementation
|
||||
*/
|
||||
|
||||
export class Anthropic implements LLM {
|
||||
hasStreaming: boolean = true;
|
||||
|
||||
// Per completion Anthropic params
|
||||
model: string;
|
||||
model: keyof typeof ALL_AVAILABLE_ANTHROPIC_MODELS;
|
||||
temperature: number;
|
||||
topP: number;
|
||||
maxTokens?: number;
|
||||
@@ -483,6 +684,21 @@ export class Anthropic implements LLM {
|
||||
this.callbackManager = init?.callbackManager;
|
||||
}
|
||||
|
||||
tokens(messages: ChatMessage[]): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return {
|
||||
model: this.model,
|
||||
temperature: this.temperature,
|
||||
topP: this.topP,
|
||||
maxTokens: this.maxTokens,
|
||||
contextWindow: ALL_AVAILABLE_ANTHROPIC_MODELS[this.model].contextWindow,
|
||||
tokenizer: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
mapMessagesToPrompt(messages: ChatMessage[]) {
|
||||
return (
|
||||
messages.reduce((acc, message) => {
|
||||
@@ -498,10 +714,22 @@ export class Anthropic implements LLM {
|
||||
);
|
||||
}
|
||||
|
||||
async chat(
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event | undefined,
|
||||
): Promise<ChatResponse> {
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
//Streaming
|
||||
if (streaming) {
|
||||
if (!this.hasStreaming) {
|
||||
throw Error("No streaming support for this LLM.");
|
||||
}
|
||||
return this.streamChat(messages, parentEvent) as R;
|
||||
}
|
||||
//Non-streaming
|
||||
const response = await this.session.anthropic.completions.create({
|
||||
model: this.model,
|
||||
prompt: this.mapMessagesToPrompt(messages),
|
||||
@@ -514,12 +742,182 @@ export class Anthropic implements LLM {
|
||||
message: { content: response.completion.trimStart(), role: "assistant" },
|
||||
//^ We're trimming the start because Anthropic often starts with a space in the response
|
||||
// That space will be re-added when we generate the next prompt.
|
||||
};
|
||||
} as R;
|
||||
}
|
||||
async complete(
|
||||
|
||||
protected async *streamChat(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event | undefined,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
// AsyncIterable<AnthropicStreamToken>
|
||||
const stream: AsyncIterable<AnthropicStreamToken> =
|
||||
await this.session.anthropic.completions.create({
|
||||
model: this.model,
|
||||
prompt: this.mapMessagesToPrompt(messages),
|
||||
max_tokens_to_sample: this.maxTokens ?? 100000,
|
||||
temperature: this.temperature,
|
||||
top_p: this.topP,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
var idx_counter: number = 0;
|
||||
for await (const part of stream) {
|
||||
//TODO: LLM Stream Callback, pending re-work.
|
||||
|
||||
idx_counter++;
|
||||
yield part.completion;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async complete<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(
|
||||
prompt: string,
|
||||
parentEvent?: Event | undefined,
|
||||
): Promise<CompletionResponse> {
|
||||
return this.chat([{ content: prompt, role: "user" }], parentEvent);
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
if (streaming) {
|
||||
return this.streamComplete(prompt, parentEvent) as R;
|
||||
}
|
||||
return this.chat(
|
||||
[{ content: prompt, role: "user" }],
|
||||
parentEvent,
|
||||
streaming,
|
||||
) as R;
|
||||
}
|
||||
|
||||
protected streamComplete(
|
||||
prompt: string,
|
||||
parentEvent?: Event | undefined,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
return this.streamChat([{ content: prompt, role: "user" }], parentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export class Portkey implements LLM {
|
||||
hasStreaming: boolean = true;
|
||||
|
||||
apiKey?: string = undefined;
|
||||
baseURL?: string = undefined;
|
||||
mode?: string = undefined;
|
||||
llms?: [LLMOptions] | null = undefined;
|
||||
session: PortkeySession;
|
||||
callbackManager?: CallbackManager;
|
||||
|
||||
constructor(init?: Partial<Portkey>) {
|
||||
this.apiKey = init?.apiKey;
|
||||
this.baseURL = init?.baseURL;
|
||||
this.mode = init?.mode;
|
||||
this.llms = init?.llms;
|
||||
this.session = getPortkeySession({
|
||||
apiKey: this.apiKey,
|
||||
baseURL: this.baseURL,
|
||||
llms: this.llms,
|
||||
mode: this.mode,
|
||||
});
|
||||
this.callbackManager = init?.callbackManager;
|
||||
}
|
||||
|
||||
tokens(messages: ChatMessage[]): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
get metadata(): LLMMetadata {
|
||||
throw new Error("metadata not implemented for Portkey");
|
||||
}
|
||||
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event | undefined,
|
||||
streaming?: T,
|
||||
params?: Record<string, any>,
|
||||
): Promise<R> {
|
||||
if (streaming) {
|
||||
return this.streamChat(messages, parentEvent, params) as R;
|
||||
} else {
|
||||
const resolvedParams = params || {};
|
||||
const response = await this.session.portkey.chatCompletions.create({
|
||||
messages,
|
||||
...resolvedParams,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message?.content ?? "";
|
||||
const role = response.choices[0].message?.role || "assistant";
|
||||
return { message: { content, role: role as MessageType } } as R;
|
||||
}
|
||||
}
|
||||
|
||||
async complete<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : ChatResponse,
|
||||
>(
|
||||
prompt: string,
|
||||
parentEvent?: Event | undefined,
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
return this.chat(
|
||||
[{ content: prompt, role: "user" }],
|
||||
parentEvent,
|
||||
streaming,
|
||||
);
|
||||
}
|
||||
|
||||
async *streamChat(
|
||||
messages: ChatMessage[],
|
||||
parentEvent?: Event,
|
||||
params?: Record<string, any>,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
// Wrapping the stream in a callback.
|
||||
const onLLMStream = this.callbackManager?.onLLMStream
|
||||
? this.callbackManager.onLLMStream
|
||||
: () => {};
|
||||
|
||||
const chunkStream = await this.session.portkey.chatCompletions.create({
|
||||
messages,
|
||||
...params,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
const event: Event = parentEvent
|
||||
? parentEvent
|
||||
: {
|
||||
id: "unspecified",
|
||||
type: "llmPredict" as EventType,
|
||||
};
|
||||
|
||||
//Indices
|
||||
var idx_counter: number = 0;
|
||||
for await (const part of chunkStream) {
|
||||
//Increment
|
||||
part.choices[0].index = idx_counter;
|
||||
const is_done: boolean =
|
||||
part.choices[0].finish_reason === "stop" ? true : false;
|
||||
//onLLMStream Callback
|
||||
|
||||
const stream_callback: StreamCallbackResponse = {
|
||||
event: event,
|
||||
index: idx_counter,
|
||||
isDone: is_done,
|
||||
// token: part,
|
||||
};
|
||||
onLLMStream(stream_callback);
|
||||
|
||||
idx_counter++;
|
||||
|
||||
yield part.choices[0].delta?.content ?? "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
streamComplete(
|
||||
query: string,
|
||||
parentEvent?: Event,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
return this.streamChat([{ content: query, role: "user" }], parentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ export class OpenAISession {
|
||||
if (options.azure) {
|
||||
this.openai = new AzureOpenAI(options);
|
||||
} else {
|
||||
this.openai = new OpenAI(options);
|
||||
this.openai = new OpenAI({
|
||||
...options,
|
||||
// defaultHeaders: { "OpenAI-Beta": "assistants=v1" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import _ from "lodash";
|
||||
import { LLMOptions, Portkey } from "portkey-ai";
|
||||
|
||||
export const readEnv = (
|
||||
env: string,
|
||||
default_val?: string,
|
||||
): string | undefined => {
|
||||
if (typeof process !== "undefined") {
|
||||
return process.env?.[env] ?? default_val;
|
||||
}
|
||||
return default_val;
|
||||
};
|
||||
|
||||
interface PortkeyOptions {
|
||||
apiKey?: string;
|
||||
baseURL?: string;
|
||||
mode?: string;
|
||||
llms?: [LLMOptions] | null;
|
||||
}
|
||||
|
||||
export class PortkeySession {
|
||||
portkey: Portkey;
|
||||
|
||||
constructor(options: PortkeyOptions = {}) {
|
||||
if (!options.apiKey) {
|
||||
options.apiKey = readEnv("PORTKEY_API_KEY");
|
||||
}
|
||||
|
||||
if (!options.baseURL) {
|
||||
options.baseURL = readEnv("PORTKEY_BASE_URL", "https://api.portkey.ai");
|
||||
}
|
||||
|
||||
this.portkey = new Portkey({});
|
||||
this.portkey.llms = [{}];
|
||||
if (!options.apiKey) {
|
||||
throw new Error("Set Portkey ApiKey in PORTKEY_API_KEY env variable");
|
||||
}
|
||||
|
||||
this.portkey = new Portkey(options);
|
||||
}
|
||||
}
|
||||
|
||||
let defaultPortkeySession: {
|
||||
session: PortkeySession;
|
||||
options: PortkeyOptions;
|
||||
}[] = [];
|
||||
|
||||
/**
|
||||
* Get a session for the Portkey API. If one already exists with the same options,
|
||||
* it will be returned. Otherwise, a new session will be created.
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function getPortkeySession(options: PortkeyOptions = {}) {
|
||||
let session = defaultPortkeySession.find((session) => {
|
||||
return _.isEqual(session.options, options);
|
||||
})?.session;
|
||||
|
||||
if (!session) {
|
||||
session = new PortkeySession(options);
|
||||
defaultPortkeySession.push({ session, options });
|
||||
}
|
||||
return session;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import mammoth from "mammoth";
|
||||
import { Document } from "../Node";
|
||||
import { GenericFileSystem } from "../storage/FileSystem";
|
||||
import { DEFAULT_FS } from "../storage/constants";
|
||||
import { BaseReader } from "./base";
|
||||
|
||||
export class DocxReader implements BaseReader {
|
||||
/** DocxParser */
|
||||
async loadData(
|
||||
file: string,
|
||||
fs: GenericFileSystem = DEFAULT_FS,
|
||||
): Promise<Document[]> {
|
||||
const dataBuffer = (await fs.readFile(file)) as any;
|
||||
const { value } = await mammoth.extractRawText({ buffer: dataBuffer });
|
||||
return [new Document({ text: value, id_: file })];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Document } from "../Node";
|
||||
import { DEFAULT_FS } from "../storage/constants";
|
||||
import { GenericFileSystem } from "../storage/FileSystem";
|
||||
import { BaseReader } from "./base";
|
||||
|
||||
/**
|
||||
* Extract the significant text from an arbitrary HTML document.
|
||||
* The contents of any head, script, style, and xml tags are removed completely.
|
||||
* The URLs for a[href] tags are extracted, along with the inner text of the tag.
|
||||
* All other tags are removed, and the inner text is kept intact.
|
||||
* Html entities (e.g., &) are not decoded.
|
||||
*/
|
||||
export class HTMLReader implements BaseReader {
|
||||
/**
|
||||
* Public method for this reader.
|
||||
* Required by BaseReader interface.
|
||||
* @param file Path/name of the file to be loaded.
|
||||
* @param fs fs wrapper interface for getting the file content.
|
||||
* @returns Promise<Document[]> A Promise object, eventually yielding zero or one Document parsed from the HTML content of the specified file.
|
||||
*/
|
||||
async loadData(
|
||||
file: string,
|
||||
fs: GenericFileSystem = DEFAULT_FS,
|
||||
): Promise<Document[]> {
|
||||
const dataBuffer = await fs.readFile(file, "utf-8");
|
||||
const htmlOptions = this.getOptions();
|
||||
const content = await this.parseContent(dataBuffer, htmlOptions);
|
||||
return [new Document({ text: content, id_: file })];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for string-strip-html usage.
|
||||
* @param html Raw HTML content to be parsed.
|
||||
* @param options An object of options for the underlying library
|
||||
* @see getOptions
|
||||
* @returns The HTML content, stripped of unwanted tags and attributes
|
||||
*/
|
||||
async parseContent(html: string, options: any = {}): Promise<string> {
|
||||
const { stripHtml } = await import("string-strip-html"); // ESM only
|
||||
return stripHtml(html).result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for our configuration options passed to string-strip-html library
|
||||
* @see https://codsen.com/os/string-strip-html/examples
|
||||
* @returns An object of options for the underlying library
|
||||
*/
|
||||
getOptions() {
|
||||
return {
|
||||
skipHtmlDecoding: true,
|
||||
stripTogetherWithTheirContents: [
|
||||
"script", // default
|
||||
"style", // default
|
||||
"xml", // default
|
||||
"head", // <-- custom-added
|
||||
],
|
||||
// Keep the URLs for embedded links
|
||||
// cb: (tag: any, deleteFrom: number, deleteTo: number, insert: string, rangesArr: any, proposedReturn: string) => {
|
||||
// let temp;
|
||||
// if (
|
||||
// tag.name === "a" &&
|
||||
// tag.attributes &&
|
||||
// tag.attributes.some((attr: any) => {
|
||||
// if (attr.name === "href") {
|
||||
// temp = attr.value;
|
||||
// return true;
|
||||
// }
|
||||
// })
|
||||
// ) {
|
||||
// rangesArr.push([deleteFrom, deleteTo, `${temp} ${insert || ""}`]);
|
||||
// } else {
|
||||
// rangesArr.push(proposedReturn);
|
||||
// }
|
||||
// },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,24 @@ import { Document } from "../Node";
|
||||
import { CompleteFileSystem, walk } from "../storage/FileSystem";
|
||||
import { DEFAULT_FS } from "../storage/constants";
|
||||
import { PapaCSVReader } from "./CSVReader";
|
||||
import { DocxReader } from "./DocxReader";
|
||||
import { HTMLReader } from "./HTMLReader";
|
||||
import { MarkdownReader } from "./MarkdownReader";
|
||||
import { PDFReader } from "./PDFReader";
|
||||
import { BaseReader } from "./base";
|
||||
|
||||
type ReaderCallback = (
|
||||
category: "file" | "directory",
|
||||
name: string,
|
||||
status: ReaderStatus,
|
||||
message?: string,
|
||||
) => boolean;
|
||||
enum ReaderStatus {
|
||||
STARTED = 0,
|
||||
COMPLETE,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a .txt file
|
||||
*/
|
||||
@@ -20,11 +34,14 @@ export class TextFileReader implements BaseReader {
|
||||
}
|
||||
}
|
||||
|
||||
const FILE_EXT_TO_READER: Record<string, BaseReader> = {
|
||||
export const FILE_EXT_TO_READER: Record<string, BaseReader> = {
|
||||
txt: new TextFileReader(),
|
||||
pdf: new PDFReader(),
|
||||
csv: new PapaCSVReader(),
|
||||
md: new MarkdownReader(),
|
||||
docx: new DocxReader(),
|
||||
htm: new HTMLReader(),
|
||||
html: new HTMLReader(),
|
||||
};
|
||||
|
||||
export type SimpleDirectoryReaderLoadDataProps = {
|
||||
@@ -35,20 +52,37 @@ export type SimpleDirectoryReaderLoadDataProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Read all of the documents in a directory. Currently supports PDF and TXT files.
|
||||
* Read all of the documents in a directory.
|
||||
* By default, supports the list of file types
|
||||
* in the FILE_EXIT_TO_READER map.
|
||||
*/
|
||||
export class SimpleDirectoryReader implements BaseReader {
|
||||
constructor(private observer?: ReaderCallback) {}
|
||||
|
||||
async loadData({
|
||||
directoryPath,
|
||||
fs = DEFAULT_FS as CompleteFileSystem,
|
||||
defaultReader = new TextFileReader(),
|
||||
fileExtToReader = FILE_EXT_TO_READER,
|
||||
}: SimpleDirectoryReaderLoadDataProps): Promise<Document[]> {
|
||||
// Observer can decide to skip the directory
|
||||
if (
|
||||
!this.doObserverCheck("directory", directoryPath, ReaderStatus.STARTED)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let docs: Document[] = [];
|
||||
for await (const filePath of walk(fs, directoryPath)) {
|
||||
try {
|
||||
const fileExt = _.last(filePath.split(".")) || "";
|
||||
|
||||
// Observer can decide to skip each file
|
||||
if (!this.doObserverCheck("file", filePath, ReaderStatus.STARTED)) {
|
||||
// Skip this file
|
||||
continue;
|
||||
}
|
||||
|
||||
let reader = null;
|
||||
|
||||
if (fileExt in fileExtToReader) {
|
||||
@@ -56,16 +90,52 @@ export class SimpleDirectoryReader implements BaseReader {
|
||||
} else if (!_.isNil(defaultReader)) {
|
||||
reader = defaultReader;
|
||||
} else {
|
||||
console.warn(`No reader for file extension of ${filePath}`);
|
||||
const msg = `No reader for file extension of ${filePath}`;
|
||||
console.warn(msg);
|
||||
|
||||
// In an error condition, observer's false cancels the whole process.
|
||||
if (
|
||||
!this.doObserverCheck("file", filePath, ReaderStatus.ERROR, msg)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileDocs = await reader.loadData(filePath, fs);
|
||||
docs.push(...fileDocs);
|
||||
|
||||
// Observer can still cancel addition of the resulting docs from this file
|
||||
if (this.doObserverCheck("file", filePath, ReaderStatus.COMPLETE)) {
|
||||
docs.push(...fileDocs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error reading file ${filePath}: ${e}`);
|
||||
const msg = `Error reading file ${filePath}: ${e}`;
|
||||
console.error(msg);
|
||||
|
||||
// In an error condition, observer's false cancels the whole process.
|
||||
if (!this.doObserverCheck("file", filePath, ReaderStatus.ERROR, msg)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After successful import of all files, directory completion
|
||||
// is only a notification for observer, cannot be cancelled.
|
||||
this.doObserverCheck("directory", directoryPath, ReaderStatus.COMPLETE);
|
||||
|
||||
return docs;
|
||||
}
|
||||
|
||||
private doObserverCheck(
|
||||
category: "file" | "directory",
|
||||
name: string,
|
||||
status: ReaderStatus,
|
||||
message?: string,
|
||||
): boolean {
|
||||
if (this.observer) {
|
||||
return this.observer(category, name, status, message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { Document } from "../Node";
|
||||
import { BaseReader } from "./base";
|
||||
|
||||
/**
|
||||
* Read in from MongoDB
|
||||
*/
|
||||
export class SimpleMongoReader implements BaseReader {
|
||||
private client: MongoClient;
|
||||
|
||||
constructor(client: MongoClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from MongoDB collection
|
||||
* @param {string} db_name - The name of the database to load.
|
||||
* @param {string} collection_name - The name of the collection to load.
|
||||
* @param {Number} [max_docs = 0] - Maximum number of documents to return. 0 means no limit.
|
||||
* @param {Record<string, any>} [query_dict={}] - Specific query, as specified by MongoDB NodeJS documentation.
|
||||
* @param {Record<string, any>} [query_options={}] - Specific query options, as specified by MongoDB NodeJS documentation.
|
||||
* @param {Record<string, any>} [projection = {}] - Projection options, as specified by MongoDB NodeJS documentation.
|
||||
* @returns {Promise<Document[]>}
|
||||
*/
|
||||
async loadData(
|
||||
db_name: string,
|
||||
collection_name: string,
|
||||
max_docs = 0,
|
||||
//For later: Think about whether we want to pass generic objects in...
|
||||
query_dict: Record<string, any> = {},
|
||||
query_options: Record<string, any> = {},
|
||||
projection: Record<string, any> = {},
|
||||
): Promise<Document[]> {
|
||||
//Get items from collection using built-in functions
|
||||
const cursor: Partial<Document>[] = await this.client
|
||||
.db(db_name)
|
||||
.collection(collection_name)
|
||||
.find(query_dict, query_options)
|
||||
.limit(max_docs)
|
||||
.project(projection)
|
||||
.toArray();
|
||||
|
||||
//Aggregate results and return
|
||||
const documents: Document[] = [];
|
||||
cursor.forEach((element: Partial<Document>) => {
|
||||
//For later: Metadata filtering
|
||||
documents.push(new Document({ text: JSON.stringify(element) }));
|
||||
});
|
||||
return documents;
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,9 @@ export interface VectorStore {
|
||||
client(): any;
|
||||
add(embeddingResults: BaseNode[]): Promise<string[]>;
|
||||
delete(refDocId: string, deleteKwargs?: any): Promise<void>;
|
||||
query(query: VectorStoreQuery, kwargs?: any): Promise<VectorStoreQueryResult>;
|
||||
query(
|
||||
query: VectorStoreQuery,
|
||||
options?: any,
|
||||
): Promise<VectorStoreQueryResult>;
|
||||
persist(persistPath: string, fs?: GenericFileSystem): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { SubQuestionOutputParser } from "../OutputParser";
|
||||
|
||||
//This parser is really important, so make sure to add tests
|
||||
// as the parser sees through more iterations.
|
||||
describe("SubQuestionOutputParser", () => {
|
||||
test("parses expected", () => {
|
||||
const parser = new SubQuestionOutputParser();
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "uber_10k",
|
||||
description: "Provides information about Uber financials for year 2021",
|
||||
},
|
||||
{
|
||||
name: "lyft_10k",
|
||||
description: "Provides information about Lyft financials for year 2021",
|
||||
},
|
||||
];
|
||||
|
||||
const data_str: string = JSON.stringify(data);
|
||||
const full_string = `\`\`\`json
|
||||
${data_str}
|
||||
\`\`\``;
|
||||
|
||||
const real_answer = { parsedOutput: data, rawOutput: full_string };
|
||||
|
||||
expect(parser.parse(full_string)).toEqual(real_answer);
|
||||
});
|
||||
|
||||
//This is in case our LLM outputs a list response, but without ```json.
|
||||
test("parses without ```json", () => {
|
||||
const parser = new SubQuestionOutputParser();
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "uber_10k",
|
||||
description: "Provides information about Uber financials for year 2021",
|
||||
},
|
||||
{
|
||||
name: "lyft_10k",
|
||||
description: "Provides information about Lyft financials for year 2021",
|
||||
},
|
||||
];
|
||||
|
||||
const data_str: string = JSON.stringify(data);
|
||||
const full_string = `${data_str}`;
|
||||
|
||||
const real_answer = { parsedOutput: data, rawOutput: full_string };
|
||||
|
||||
expect(parser.parse(JSON.stringify(data))).toEqual(real_answer);
|
||||
});
|
||||
|
||||
test("parses null single response", () => {
|
||||
const parser = new SubQuestionOutputParser();
|
||||
const data_str =
|
||||
"[\n" +
|
||||
" {\n" +
|
||||
` "subQuestion": "Sorry, I don't have any relevant information to answer your question",\n` +
|
||||
' "toolName": ""\n' +
|
||||
" }\n" +
|
||||
"]";
|
||||
const data = [
|
||||
{
|
||||
subQuestion:
|
||||
"Sorry, I don't have any relevant information to answer your question",
|
||||
toolName: "",
|
||||
},
|
||||
];
|
||||
const real_answer = { parsedOutput: data, rawOutput: data_str };
|
||||
expect(parser.parse(data_str)).toEqual(real_answer);
|
||||
});
|
||||
|
||||
test("Single JSON object case", () => {
|
||||
const parser = new SubQuestionOutputParser();
|
||||
const data_str =
|
||||
" {\n" +
|
||||
` "subQuestion": "Sorry, I don't have any relevant information to answer your question",\n` +
|
||||
' "toolName": ""\n' +
|
||||
" }\n";
|
||||
const data = [
|
||||
{
|
||||
subQuestion:
|
||||
"Sorry, I don't have any relevant information to answer your question",
|
||||
toolName: "",
|
||||
},
|
||||
];
|
||||
const real_answer = { parsedOutput: data, rawOutput: data_str };
|
||||
expect(parser.parse(data_str)).toEqual(real_answer);
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,7 @@ describe("SentenceSplitter", () => {
|
||||
let splits = sentenceSplitter.splitText(
|
||||
"This is a sentence. This is another sentence. 1.0",
|
||||
);
|
||||
|
||||
expect(splits).toEqual([
|
||||
"This is a sentence.",
|
||||
"This is another sentence.",
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"target": "ES2015"
|
||||
"target": "ES2015",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# create-llama
|
||||
|
||||
## 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,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 LlamaIndex, Vercel, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,126 @@
|
||||
# Create LlamaIndex App
|
||||
|
||||
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.
|
||||
|
||||
Just run
|
||||
|
||||
```bash
|
||||
npx create-llama@latest
|
||||
```
|
||||
|
||||
to get started, or see below for more options. Once your app is generated, run
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
to start the development server. You can then visit [http://localhost:3000](http://localhost:3000) to see your app.
|
||||
|
||||
## 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).
|
||||
- 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.
|
||||
|
||||
## 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`:
|
||||
|
||||
- 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 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:
|
||||
|
||||
```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.
|
||||
|
||||
## Don't want a front-end?
|
||||
|
||||
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.
|
||||
|
||||
## Customizing the LLM
|
||||
|
||||
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:
|
||||
|
||||
- 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`
|
||||
|
||||
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).
|
||||
|
||||
## Example
|
||||
|
||||
The simplest thing to do is run `create-llama` in interactive mode:
|
||||
|
||||
```bash
|
||||
npx create-llama@latest
|
||||
# or
|
||||
npm create llama@latest
|
||||
# or
|
||||
yarn create llama
|
||||
# or
|
||||
pnpm create llama@latest
|
||||
```
|
||||
|
||||
You will be asked for the name of your project, along with other configuration options, something like this:
|
||||
|
||||
```bash
|
||||
>> npm create llama@latest
|
||||
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.
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
|
||||
## LlamaIndex Documentation
|
||||
|
||||
- [TS/JS docs](https://ts.llamaindex.ai/)
|
||||
- [Python docs](https://docs.llamaindex.ai/en/stable/)
|
||||
|
||||
Inspired by and adapted from [create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
|
||||
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import path from "path";
|
||||
import { green } 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 "./templates";
|
||||
import { installTemplate } from "./templates";
|
||||
|
||||
export async function createApp({
|
||||
template,
|
||||
framework,
|
||||
engine,
|
||||
ui,
|
||||
appPath,
|
||||
packageManager,
|
||||
eslint,
|
||||
frontend,
|
||||
openAIKey,
|
||||
}: Omit<
|
||||
InstallTemplateArgs,
|
||||
"appName" | "root" | "isOnline" | "customApiPath"
|
||||
> & {
|
||||
appPath: string;
|
||||
frontend: boolean;
|
||||
}): 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,
|
||||
};
|
||||
|
||||
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:8000/api/chat",
|
||||
backend: false,
|
||||
});
|
||||
// copy readme for fullstack
|
||||
await fs.promises.copyFile(
|
||||
path.join(__dirname, "templates", "README-fullstack.md"),
|
||||
path.join(root, "README.md"),
|
||||
);
|
||||
} else {
|
||||
await installTemplate({ ...args, backend: true, forBackend: framework });
|
||||
}
|
||||
|
||||
process.chdir(root);
|
||||
if (tryGitInit(root)) {
|
||||
console.log("Initialized a git repository.");
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(`${green("Success!")} Created ${appName} at ${appPath}`);
|
||||
|
||||
console.log(
|
||||
`Now have a look at the ${terminalLink(
|
||||
"README.md",
|
||||
`file://${appName}/README.md`,
|
||||
)} and learn how to get started.`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { async as glob } from "fast-glob";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
interface CopyOption {
|
||||
cwd?: string;
|
||||
rename?: (basename: string) => string;
|
||||
parents?: boolean;
|
||||
}
|
||||
|
||||
const identity = (x: string) => x;
|
||||
|
||||
export const copy = async (
|
||||
src: string | string[],
|
||||
dest: string,
|
||||
{ cwd, rename = identity, parents = true }: CopyOption = {},
|
||||
) => {
|
||||
const source = typeof src === "string" ? [src] : src;
|
||||
|
||||
if (source.length === 0 || !dest) {
|
||||
throw new TypeError("`src` and `dest` are required");
|
||||
}
|
||||
|
||||
const sourceFiles = await glob(source, {
|
||||
cwd,
|
||||
dot: true,
|
||||
absolute: false,
|
||||
stats: false,
|
||||
});
|
||||
|
||||
const destRelativeToCwd = cwd ? path.resolve(cwd, dest) : dest;
|
||||
|
||||
return Promise.all(
|
||||
sourceFiles.map(async (p) => {
|
||||
const dirname = path.dirname(p);
|
||||
const basename = rename(path.basename(p));
|
||||
|
||||
const from = cwd ? path.resolve(cwd, p) : p;
|
||||
const to = parents
|
||||
? path.join(destRelativeToCwd, dirname, basename)
|
||||
: path.join(destRelativeToCwd, basename);
|
||||
|
||||
// Ensure the destination directory exists
|
||||
await fs.promises.mkdir(path.dirname(to), { recursive: true });
|
||||
|
||||
return fs.promises.copyFile(from, to);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export type PackageManager = "npm" | "pnpm" | "yarn";
|
||||
|
||||
export function getPkgManager(): PackageManager {
|
||||
const userAgent = process.env.npm_config_user_agent || "";
|
||||
|
||||
if (userAgent.startsWith("yarn")) {
|
||||
return "yarn";
|
||||
}
|
||||
|
||||
if (userAgent.startsWith("pnpm")) {
|
||||
return "pnpm";
|
||||
}
|
||||
|
||||
return "npm";
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
function isInGitRepository(): boolean {
|
||||
try {
|
||||
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isInMercurialRepository(): boolean {
|
||||
try {
|
||||
execSync("hg --cwd . root", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDefaultBranchSet(): boolean {
|
||||
try {
|
||||
execSync("git config init.defaultBranch", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tryGitInit(root: string): boolean {
|
||||
let didInit = false;
|
||||
try {
|
||||
execSync("git --version", { stdio: "ignore" });
|
||||
if (isInGitRepository() || isInMercurialRepository()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
execSync("git init", { stdio: "ignore" });
|
||||
didInit = true;
|
||||
|
||||
if (!isDefaultBranchSet()) {
|
||||
execSync("git checkout -b main", { stdio: "ignore" });
|
||||
}
|
||||
|
||||
execSync("git add -A", { stdio: "ignore" });
|
||||
execSync('git commit -m "Initial commit from Create Llama"', {
|
||||
stdio: "ignore",
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (didInit) {
|
||||
try {
|
||||
fs.rmSync(path.join(root, ".git"), { recursive: true, force: true });
|
||||
} catch (_) {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import spawn from "cross-spawn";
|
||||
import { yellow } from "picocolors";
|
||||
import type { PackageManager } from "./get-pkg-manager";
|
||||
|
||||
/**
|
||||
* Spawn a package manager installation based on user preference.
|
||||
*
|
||||
* @returns A Promise that resolves once the installation is finished.
|
||||
*/
|
||||
export async function callPackageManager(
|
||||
/** Indicate which package manager to use. */
|
||||
packageManager: PackageManager,
|
||||
/** Indicate whether there is an active Internet connection.*/
|
||||
isOnline: boolean,
|
||||
args: string[] = ["install"],
|
||||
): Promise<void> {
|
||||
if (!isOnline) {
|
||||
console.log(
|
||||
yellow("You appear to be offline.\nFalling back to the local cache."),
|
||||
);
|
||||
args.push("--offline");
|
||||
}
|
||||
/**
|
||||
* Return a Promise that resolves once the installation is finished.
|
||||
*/
|
||||
return new Promise((resolve, reject) => {
|
||||
/**
|
||||
* Spawn the installation process.
|
||||
*/
|
||||
const child = spawn(packageManager, args, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
ADBLOCK: "1",
|
||||
// we set NODE_ENV to development as pnpm skips dev
|
||||
// dependencies when production
|
||||
NODE_ENV: "development",
|
||||
DISABLE_OPENCOLLECTIVE: "1",
|
||||
},
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject({ command: `${packageManager} ${args.join(" ")}` });
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { blue, green } from "picocolors";
|
||||
|
||||
export function isFolderEmpty(root: string, name: string): boolean {
|
||||
const validFiles = [
|
||||
".DS_Store",
|
||||
".git",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
".gitlab-ci.yml",
|
||||
".hg",
|
||||
".hgcheck",
|
||||
".hgignore",
|
||||
".idea",
|
||||
".npmignore",
|
||||
".travis.yml",
|
||||
"LICENSE",
|
||||
"Thumbs.db",
|
||||
"docs",
|
||||
"mkdocs.yml",
|
||||
"npm-debug.log",
|
||||
"yarn-debug.log",
|
||||
"yarn-error.log",
|
||||
"yarnrc.yml",
|
||||
".yarn",
|
||||
];
|
||||
|
||||
const conflicts = fs
|
||||
.readdirSync(root)
|
||||
.filter((file) => !validFiles.includes(file))
|
||||
// Support IntelliJ IDEA-based editors
|
||||
.filter((file) => !/\.iml$/.test(file));
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
console.log(
|
||||
`The directory ${green(name)} contains files that could conflict:`,
|
||||
);
|
||||
console.log();
|
||||
for (const file of conflicts) {
|
||||
try {
|
||||
const stats = fs.lstatSync(path.join(root, file));
|
||||
if (stats.isDirectory()) {
|
||||
console.log(` ${blue(file)}/`);
|
||||
} else {
|
||||
console.log(` ${file}`);
|
||||
}
|
||||
} catch {
|
||||
console.log(` ${file}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
console.log(
|
||||
"Either try using a new directory name, or remove the files listed above.",
|
||||
);
|
||||
console.log();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { execSync } from "child_process";
|
||||
import dns from "dns";
|
||||
import url from "url";
|
||||
|
||||
function getProxy(): string | undefined {
|
||||
if (process.env.https_proxy) {
|
||||
return process.env.https_proxy;
|
||||
}
|
||||
|
||||
try {
|
||||
const httpsProxy = execSync("npm config get https-proxy").toString().trim();
|
||||
return httpsProxy !== "null" ? httpsProxy : undefined;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function getOnline(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
dns.lookup("registry.yarnpkg.com", (registryErr) => {
|
||||
if (!registryErr) {
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
const proxy = getProxy();
|
||||
if (!proxy) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
const { hostname } = url.parse(proxy);
|
||||
if (!hostname) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
dns.lookup(hostname, (proxyErr) => {
|
||||
resolve(proxyErr == null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function isUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import fs from "fs";
|
||||
|
||||
export async function isWriteable(directory: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(directory, (fs.constants || fs).W_OK);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import fs from "fs";
|
||||
|
||||
export function makeDir(
|
||||
root: string,
|
||||
options = { recursive: true },
|
||||
): Promise<string | undefined> {
|
||||
return fs.promises.mkdir(root, options);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import validateProjectName from "validate-npm-package-name";
|
||||
|
||||
export function validateNpmName(name: string): {
|
||||
valid: boolean;
|
||||
problems?: string[];
|
||||
} {
|
||||
const nameValidation = validateProjectName(name);
|
||||
if (nameValidation.validForNewPackages) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
problems: [
|
||||
...(nameValidation.errors || []),
|
||||
...(nameValidation.warnings || []),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import ciInfo from "ci-info";
|
||||
import Commander from "commander";
|
||||
import Conf from "conf";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { blue, bold, cyan, green, red, yellow } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
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 { validateNpmName } from "./helpers/validate-pkg";
|
||||
import packageJson from "./package.json";
|
||||
|
||||
let projectPath: string = "";
|
||||
|
||||
const handleSigTerm = () => process.exit(0);
|
||||
|
||||
process.on("SIGINT", handleSigTerm);
|
||||
process.on("SIGTERM", handleSigTerm);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const program = new Commander.Command(packageJson.name)
|
||||
.version(packageJson.version)
|
||||
.arguments("<project-directory>")
|
||||
.usage(`${green("<project-directory>")} [options]`)
|
||||
.action((name) => {
|
||||
projectPath = name;
|
||||
})
|
||||
.option(
|
||||
"--eslint",
|
||||
`
|
||||
|
||||
Initialize with eslint config.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--use-npm",
|
||||
`
|
||||
|
||||
Explicitly tell the CLI to bootstrap the application using npm
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--use-pnpm",
|
||||
`
|
||||
|
||||
Explicitly tell the CLI to bootstrap the application using pnpm
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--use-yarn",
|
||||
`
|
||||
|
||||
Explicitly tell the CLI to bootstrap the application using Yarn
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--reset-preferences",
|
||||
`
|
||||
|
||||
Explicitly tell the CLI to reset any stored preferences
|
||||
`,
|
||||
)
|
||||
.allowUnknownOption()
|
||||
.parse(process.argv);
|
||||
|
||||
const packageManager = !!program.useNpm
|
||||
? "npm"
|
||||
: !!program.usePnpm
|
||||
? "pnpm"
|
||||
: !!program.useYarn
|
||||
? "yarn"
|
||||
: getPkgManager();
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const conf = new Conf({ projectName: "create-llama" });
|
||||
|
||||
if (program.resetPreferences) {
|
||||
conf.clear();
|
||||
console.log(`Preferences reset successfully`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof projectPath === "string") {
|
||||
projectPath = projectPath.trim();
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
const res = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "text",
|
||||
name: "path",
|
||||
message: "What is your project named?",
|
||||
initial: "my-app",
|
||||
validate: (name) => {
|
||||
const validation = validateNpmName(path.basename(path.resolve(name)));
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
return "Invalid project name: " + validation.problems![0];
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof res.path === "string") {
|
||||
projectPath = res.path.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.log(
|
||||
"\nPlease specify the project directory:\n" +
|
||||
` ${cyan(program.name())} ${green("<project-directory>")}\n` +
|
||||
"For example:\n" +
|
||||
` ${cyan(program.name())} ${green("my-next-app")}\n\n` +
|
||||
`Run ${cyan(`${program.name()} --help`)} to see all options.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const resolvedProjectPath = path.resolve(projectPath);
|
||||
const projectName = path.basename(resolvedProjectPath);
|
||||
|
||||
const { valid, problems } = validateNpmName(projectName);
|
||||
if (!valid) {
|
||||
console.error(
|
||||
`Could not create a project called ${red(
|
||||
`"${projectName}"`,
|
||||
)} because of npm naming restrictions:`,
|
||||
);
|
||||
|
||||
problems!.forEach((p) => console.error(` ${red(bold("*"))} ${p}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the project dir is empty or doesn't exist
|
||||
*/
|
||||
const root = path.resolve(resolvedProjectPath);
|
||||
const appName = path.basename(root);
|
||||
const folderExists = fs.existsSync(root);
|
||||
|
||||
if (folderExists && !isFolderEmpty(root, appName)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const preferences = (conf.get("preferences") || {}) as Record<
|
||||
string,
|
||||
boolean | string
|
||||
>;
|
||||
|
||||
const defaults: typeof preferences = {
|
||||
template: "simple",
|
||||
framework: "nextjs",
|
||||
engine: "simple",
|
||||
ui: "html",
|
||||
eslint: true,
|
||||
frontend: false,
|
||||
openAIKey: "",
|
||||
};
|
||||
const getPrefOrDefault = (field: string) =>
|
||||
preferences[field] ?? defaults[field];
|
||||
|
||||
const handlers = {
|
||||
onCancel: () => {
|
||||
console.error("Exiting.");
|
||||
process.exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
if (!program.template) {
|
||||
if (ciInfo.isCI) {
|
||||
program.template = getPrefOrDefault("template");
|
||||
} else {
|
||||
const { template } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "template",
|
||||
message: "Which template would you like to use?",
|
||||
choices: [
|
||||
{ title: "Chat without streaming", value: "simple" },
|
||||
{ title: "Chat with streaming", value: "streaming" },
|
||||
],
|
||||
initial: 1,
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.template = template;
|
||||
preferences.template = template;
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.framework) {
|
||||
if (ciInfo.isCI) {
|
||||
program.framework = getPrefOrDefault("framework");
|
||||
} else {
|
||||
const { framework } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "framework",
|
||||
message: "Which framework would you like to use?",
|
||||
choices: [
|
||||
{ title: "NextJS", value: "nextjs" },
|
||||
{ title: "Express", value: "express" },
|
||||
{ title: "FastAPI (Python)", value: "fastapi" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.framework = framework;
|
||||
preferences.framework = framework;
|
||||
}
|
||||
}
|
||||
|
||||
if (program.framework === "express" || program.framework === "fastapi") {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
if (!program.frontend) {
|
||||
if (ciInfo.isCI) {
|
||||
program.frontend = getPrefOrDefault("frontend");
|
||||
} else {
|
||||
const styledNextJS = blue("NextJS");
|
||||
const styledBackend = green(
|
||||
program.framework === "express"
|
||||
? "Express "
|
||||
: program.framework === "fastapi"
|
||||
? "FastAPI (Python) "
|
||||
: "",
|
||||
);
|
||||
const { frontend } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "frontend",
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
|
||||
initial: getPrefOrDefault("frontend"),
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
});
|
||||
program.frontend = Boolean(frontend);
|
||||
preferences.frontend = Boolean(frontend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (program.framework === "nextjs" || program.frontend) {
|
||||
if (!program.ui) {
|
||||
if (ciInfo.isCI) {
|
||||
program.ui = getPrefOrDefault("ui");
|
||||
} else {
|
||||
const { ui } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "ui",
|
||||
message: "Which UI would you like to use?",
|
||||
choices: [
|
||||
{ title: "Just HTML", value: "html" },
|
||||
{ title: "Shadcn", value: "shadcn" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.ui = ui;
|
||||
preferences.ui = ui;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (program.framework === "express" || program.framework === "nextjs") {
|
||||
if (!program.engine) {
|
||||
if (ciInfo.isCI) {
|
||||
program.engine = getPrefOrDefault("engine");
|
||||
} else {
|
||||
const { engine } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "engine",
|
||||
message: "Which chat engine would you like to use?",
|
||||
choices: [
|
||||
{ title: "ContextChatEngine", value: "context" },
|
||||
{
|
||||
title: "SimpleChatEngine (no data, just chat)",
|
||||
value: "simple",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.engine = engine;
|
||||
preferences.engine = engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.openAIKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message: "Please provide your OpenAI API key (leave blank to skip):",
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.openAIKey = key;
|
||||
preferences.openAIKey = key;
|
||||
}
|
||||
|
||||
if (
|
||||
program.framework !== "fastapi" &&
|
||||
!process.argv.includes("--eslint") &&
|
||||
!process.argv.includes("--no-eslint")
|
||||
) {
|
||||
if (ciInfo.isCI) {
|
||||
program.eslint = getPrefOrDefault("eslint");
|
||||
} else {
|
||||
const styledEslint = blue("ESLint");
|
||||
const { eslint } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "eslint",
|
||||
message: `Would you like to use ${styledEslint}?`,
|
||||
initial: getPrefOrDefault("eslint"),
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
});
|
||||
program.eslint = Boolean(eslint);
|
||||
preferences.eslint = Boolean(eslint);
|
||||
}
|
||||
}
|
||||
|
||||
await createApp({
|
||||
template: program.template,
|
||||
framework: program.framework,
|
||||
engine: program.engine,
|
||||
ui: program.ui,
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
eslint: program.eslint,
|
||||
frontend: program.frontend,
|
||||
openAIKey: program.openAIKey,
|
||||
});
|
||||
conf.set("preferences", preferences);
|
||||
}
|
||||
|
||||
const update = checkForUpdate(packageJson).catch(() => null);
|
||||
|
||||
async function notifyUpdate(): Promise<void> {
|
||||
try {
|
||||
const res = await update;
|
||||
if (res?.latest) {
|
||||
const updateMessage =
|
||||
packageManager === "yarn"
|
||||
? "yarn global add create-llama@latest"
|
||||
: packageManager === "pnpm"
|
||||
? "pnpm add -g create-llama@latest"
|
||||
: "npm i -g create-llama@latest";
|
||||
|
||||
console.log(
|
||||
yellow(bold("A new version of `create-llama` is available!")) +
|
||||
"\n" +
|
||||
"You can update by running: " +
|
||||
cyan(updateMessage) +
|
||||
"\n",
|
||||
);
|
||||
}
|
||||
process.exit();
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
.then(notifyUpdate)
|
||||
.catch(async (reason) => {
|
||||
console.log();
|
||||
console.log("Aborting installation.");
|
||||
if (reason.command) {
|
||||
console.log(` ${cyan(reason.command)} has failed.`);
|
||||
} else {
|
||||
console.log(
|
||||
red("Unexpected error. Please report it as a bug:") + "\n",
|
||||
reason,
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
|
||||
await notifyUpdate();
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.0.8",
|
||||
"keywords": [
|
||||
"rag",
|
||||
"llamaindex",
|
||||
"next.js"
|
||||
],
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS",
|
||||
"directory": "packages/create-llama"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"create-llama": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "ncc build ./index.ts -w -o dist/",
|
||||
"build": "ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
|
||||
"lint": "eslint . --ignore-pattern dist",
|
||||
"prepublishOnly": "cd ../../ && turbo run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/async-retry": "1.4.2",
|
||||
"@types/ci-info": "2.0.0",
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/prompts": "2.0.1",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/validate-npm-package-name": "3.0.0",
|
||||
"@vercel/ncc": "0.34.0",
|
||||
"async-retry": "1.3.1",
|
||||
"async-sema": "3.0.1",
|
||||
"ci-info": "github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
|
||||
"commander": "2.20.0",
|
||||
"conf": "10.2.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"fast-glob": "3.3.1",
|
||||
"got": "10.7.0",
|
||||
"picocolors": "1.0.0",
|
||||
"prompts": "2.1.0",
|
||||
"tar": "6.1.15",
|
||||
"terminal-link": "^3.0.0",
|
||||
"update-check": "1.5.4",
|
||||
"validate-npm-package-name": "3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
poetry.lock
|
||||
storage
|
||||
@@ -0,0 +1,18 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, startup the backend as described in the [backend README](./backend/README.md).
|
||||
|
||||
Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md).
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
export const STORAGE_DIR = "./data";
|
||||
export const STORAGE_CACHE_DIR = "./cache";
|
||||
export const CHUNK_SIZE = 512;
|
||||
export const CHUNK_OVERLAP = 20;
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
serviceContextFromDefaults,
|
||||
SimpleDirectoryReader,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
|
||||
import {
|
||||
CHUNK_OVERLAP,
|
||||
CHUNK_SIZE,
|
||||
STORAGE_CACHE_DIR,
|
||||
STORAGE_DIR,
|
||||
} from "./constants.mjs";
|
||||
|
||||
async function getRuntime(func) {
|
||||
const start = Date.now();
|
||||
await func();
|
||||
const end = Date.now();
|
||||
return end - start;
|
||||
}
|
||||
|
||||
async function generateDatasource(serviceContext) {
|
||||
console.log(`Generating storage context...`);
|
||||
// Split documents, create embeddings and store them in the storage context
|
||||
const ms = await getRuntime(async () => {
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: STORAGE_CACHE_DIR,
|
||||
});
|
||||
const documents = await new SimpleDirectoryReader().loadData({
|
||||
directoryPath: STORAGE_DIR,
|
||||
});
|
||||
await VectorStoreIndex.fromDocuments(documents, {
|
||||
storageContext,
|
||||
serviceContext,
|
||||
});
|
||||
});
|
||||
console.log(`Storage context successfully generated in ${ms / 1000}s.`);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const serviceContext = serviceContextFromDefaults({
|
||||
chunkSize: CHUNK_SIZE,
|
||||
chunkOverlap: CHUNK_OVERLAP,
|
||||
});
|
||||
|
||||
await generateDatasource(serviceContext);
|
||||
console.log("Finished generating storage.");
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ContextChatEngine,
|
||||
LLM,
|
||||
serviceContextFromDefaults,
|
||||
SimpleDocumentStore,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
|
||||
|
||||
async function getDataSource(llm: LLM) {
|
||||
const serviceContext = serviceContextFromDefaults({
|
||||
llm,
|
||||
chunkSize: CHUNK_SIZE,
|
||||
chunkOverlap: CHUNK_OVERLAP,
|
||||
});
|
||||
let storageContext = await storageContextFromDefaults({
|
||||
persistDir: `${STORAGE_CACHE_DIR}`,
|
||||
});
|
||||
|
||||
const numberOfDocs = Object.keys(
|
||||
(storageContext.docStore as SimpleDocumentStore).toDict(),
|
||||
).length;
|
||||
if (numberOfDocs === 0) {
|
||||
throw new Error(
|
||||
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
|
||||
);
|
||||
}
|
||||
return await VectorStoreIndex.init({
|
||||
storageContext,
|
||||
serviceContext,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createChatEngine(llm: LLM) {
|
||||
const index = await getDataSource(llm);
|
||||
const retriever = index.asRetriever();
|
||||
retriever.similarityTopK = 5;
|
||||
|
||||
return new ContextChatEngine({
|
||||
chatModel: llm,
|
||||
retriever,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Using the chat component from https://github.com/marcusschiesser/ui (based on https://ui.shadcn.com/)
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PauseCircle, RefreshCw } from "lucide-react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
|
||||
export default function ChatActions(
|
||||
props: Pick<ChatHandler, "stop" | "reload"> & {
|
||||
showReload?: boolean;
|
||||
showStop?: boolean;
|
||||
},
|
||||
) {
|
||||
return (
|
||||
<div className="space-x-4">
|
||||
{props.showStop && (
|
||||
<Button variant="outline" size="sm" onClick={props.stop}>
|
||||
<PauseCircle className="mr-2 h-4 w-4" />
|
||||
Stop generating
|
||||
</Button>
|
||||
)}
|
||||
{props.showReload && (
|
||||
<Button variant="outline" size="sm" onClick={props.reload}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Regenerate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { User2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function ChatAvatar({ role }: { role: string }) {
|
||||
if (role === "user") {
|
||||
return (
|
||||
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow">
|
||||
<User2 className="h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white shadow">
|
||||
<Image
|
||||
className="rounded-md"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Button } from "../button";
|
||||
import { Input } from "../input";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
|
||||
export default function ChatInput(
|
||||
props: Pick<
|
||||
ChatHandler,
|
||||
"isLoading" | "handleSubmit" | "handleInputChange" | "input"
|
||||
>,
|
||||
) {
|
||||
return (
|
||||
<form
|
||||
onSubmit={props.handleSubmit}
|
||||
className="flex w-full items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl"
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
name="message"
|
||||
placeholder="Type a message"
|
||||
className="flex-1"
|
||||
value={props.input}
|
||||
onChange={props.handleInputChange}
|
||||
/>
|
||||
<Button type="submit" disabled={props.isLoading}>
|
||||
Send message
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import ChatAvatar from "./chat-avatar";
|
||||
import { Message } from "./chat.interface";
|
||||
import Markdown from "./markdown";
|
||||
import { useCopyToClipboard } from "./use-copy-to-clipboard";
|
||||
|
||||
export default function ChatMessage(chatMessage: Message) {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
|
||||
return (
|
||||
<div className="flex items-start gap-4 pr-5 pt-5">
|
||||
<ChatAvatar role={chatMessage.role} />
|
||||
<div className="group flex flex-1 justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<Markdown content={chatMessage.content} />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(chatMessage.content)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import ChatActions from "./chat-actions";
|
||||
import ChatMessage from "./chat-message";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
|
||||
export default function ChatMessages(
|
||||
props: Pick<ChatHandler, "messages" | "isLoading" | "reload" | "stop">,
|
||||
) {
|
||||
const scrollableChatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messageLength = props.messages.length;
|
||||
const lastMessage = props.messages[messageLength - 1];
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollableChatContainerRef.current) {
|
||||
scrollableChatContainerRef.current.scrollTop =
|
||||
scrollableChatContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const isLastMessageFromAssistant =
|
||||
messageLength > 0 && lastMessage?.role !== "user";
|
||||
const showReload =
|
||||
props.reload && !props.isLoading && isLastMessageFromAssistant;
|
||||
const showStop = props.stop && props.isLoading;
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messageLength, lastMessage]);
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-xl bg-white p-4 shadow-xl pb-0">
|
||||
<div
|
||||
className="flex h-[50vh] flex-col gap-5 divide-y overflow-y-auto pb-4"
|
||||
ref={scrollableChatContainerRef}
|
||||
>
|
||||
{props.messages.map((m) => (
|
||||
<ChatMessage key={m.id} {...m} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end py-4">
|
||||
<ChatActions
|
||||
reload={props.reload}
|
||||
stop={props.stop}
|
||||
showReload={showReload}
|
||||
showStop={showStop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface ChatHandler {
|
||||
messages: Message[];
|
||||
input: string;
|
||||
isLoading: boolean;
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
reload?: () => void;
|
||||
stop?: () => void;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Copy, Download } from "lucide-react";
|
||||
import { FC, memo } from "react";
|
||||
import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter";
|
||||
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { useCopyToClipboard } from "./use-copy-to-clipboard";
|
||||
|
||||
// TODO: Remove this when @type/react-syntax-highlighter is updated
|
||||
const SyntaxHighlighter = Prism as unknown as FC<SyntaxHighlighterProps>;
|
||||
|
||||
interface Props {
|
||||
language: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface languageMap {
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
export const programmingLanguages: languageMap = {
|
||||
javascript: ".js",
|
||||
python: ".py",
|
||||
java: ".java",
|
||||
c: ".c",
|
||||
cpp: ".cpp",
|
||||
"c++": ".cpp",
|
||||
"c#": ".cs",
|
||||
ruby: ".rb",
|
||||
php: ".php",
|
||||
swift: ".swift",
|
||||
"objective-c": ".m",
|
||||
kotlin: ".kt",
|
||||
typescript: ".ts",
|
||||
go: ".go",
|
||||
perl: ".pl",
|
||||
rust: ".rs",
|
||||
scala: ".scala",
|
||||
haskell: ".hs",
|
||||
lua: ".lua",
|
||||
shell: ".sh",
|
||||
sql: ".sql",
|
||||
html: ".html",
|
||||
css: ".css",
|
||||
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
||||
};
|
||||
|
||||
export const generateRandomString = (length: number, lowercase = false) => {
|
||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return lowercase ? result.toLowerCase() : result;
|
||||
};
|
||||
|
||||
const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
|
||||
|
||||
const downloadAsFile = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
const fileExtension = programmingLanguages[language] || ".file";
|
||||
const suggestedFileName = `file-${generateRandomString(
|
||||
3,
|
||||
true,
|
||||
)}${fileExtension}`;
|
||||
const fileName = window.prompt("Enter file name" || "", suggestedFileName);
|
||||
|
||||
if (!fileName) {
|
||||
// User pressed cancel on prompt.
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([value], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.download = fileName;
|
||||
link.href = url;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const onCopy = () => {
|
||||
if (isCopied) return;
|
||||
copyToClipboard(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="codeblock relative w-full bg-zinc-950 font-sans">
|
||||
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
|
||||
<span className="text-xs lowercase">{language}</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button variant="ghost" onClick={downloadAsFile} size="icon">
|
||||
<Download />
|
||||
<span className="sr-only">Download</span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={onCopy}>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={coldarkDark}
|
||||
PreTag="div"
|
||||
showLineNumbers
|
||||
customStyle={{
|
||||
width: "100%",
|
||||
background: "transparent",
|
||||
padding: "1.5rem 1rem",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontSize: "0.9rem",
|
||||
fontFamily: "var(--font-mono)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
|
||||
export { CodeBlock };
|
||||
@@ -0,0 +1,5 @@
|
||||
import ChatInput from "./chat-input";
|
||||
import ChatMessages from "./chat-messages";
|
||||
|
||||
export { type ChatHandler, type Message } from "./chat.interface";
|
||||
export { ChatInput, ChatMessages };
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FC, memo } from "react";
|
||||
import ReactMarkdown, { Options } from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
|
||||
import { CodeBlock } from "./codeblock";
|
||||
|
||||
const MemoizedReactMarkdown: FC<Options> = memo(
|
||||
ReactMarkdown,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.children === nextProps.children &&
|
||||
prevProps.className === nextProps.className,
|
||||
);
|
||||
|
||||
export default function Markdown({ content }: { content: string }) {
|
||||
return (
|
||||
<MemoizedReactMarkdown
|
||||
className="prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
components={{
|
||||
p({ children }) {
|
||||
return <p className="mb-2 last:mb-0">{children}</p>;
|
||||
},
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
if (children.length) {
|
||||
if (children[0] == "▍") {
|
||||
return (
|
||||
<span className="mt-1 animate-pulse cursor-default">▍</span>
|
||||
);
|
||||
}
|
||||
|
||||
children[0] = (children[0] as string).replace("`▍`", "▍");
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
|
||||
if (inline) {
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
language={(match && match[1]) || ""}
|
||||
value={String(children).replace(/\n$/, "")}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</MemoizedReactMarkdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
export interface useCopyToClipboardProps {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export function useCopyToClipboard({
|
||||
timeout = 2000,
|
||||
}: useCopyToClipboardProps) {
|
||||
const [isCopied, setIsCopied] = React.useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = (value: string) => {
|
||||
if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
return { isCopied, copyToClipboard };
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user