mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-13 22:17:48 -04:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| a5ae1eea30 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"llamaindex": patch
|
||||
---
|
||||
|
||||
Add HTMLReader (thanks @mtutty)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Label bug fix
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"llamaindex": patch
|
||||
---
|
||||
|
||||
Add observer/filter to the SimpleDirectoryReader (thanks @mtutty)
|
||||
@@ -37,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
|
||||
|
||||
@@ -89,7 +89,7 @@ Check out our NextJS playground at https://llama-playground.vercel.app/. The sou
|
||||
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
|
||||
export const runtime = "nodejs"; // default
|
||||
```
|
||||
|
||||
```js
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -11,7 +11,7 @@ LlamaIndex currently officially supports NodeJS 18 and NodeJS 20.
|
||||
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
|
||||
export const runtime = "nodejs"; // default
|
||||
```
|
||||
|
||||
and you'll need to add an exception for pdf-parse in your next.config.js
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChatMessage, OpenAI, SimpleChatEngine } from "llamaindex";
|
||||
import {Anthropic} from "../../packages/core/src/llm/LLM";
|
||||
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 = `
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { VectorStoreIndex } from "../../packages/core/src/indices";
|
||||
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";
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.33",
|
||||
"private": true,
|
||||
"name": "simple",
|
||||
"dependencies": {
|
||||
|
||||
+11
-11
@@ -1,23 +1,23 @@
|
||||
import { Portkey } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llms = [{
|
||||
|
||||
}]
|
||||
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
|
||||
}]
|
||||
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." }
|
||||
{ role: "user", content: "Tell me a joke." },
|
||||
]);
|
||||
for await (const res of result) {
|
||||
process.stdout.write(res)
|
||||
process.stdout.write(res);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
+11
-11
@@ -1,23 +1,23 @@
|
||||
import { Portkey } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const llms = [{
|
||||
|
||||
}]
|
||||
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
|
||||
}]
|
||||
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." }
|
||||
{ role: "user", content: "Tell me a joke." },
|
||||
]);
|
||||
for await (const res of result) {
|
||||
process.stdout.write(res)
|
||||
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);
|
||||
})();
|
||||
+11
-9
@@ -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,25 +11,27 @@
|
||||
"publish-snapshot": "turbo run build lint test && changeset version --snapshot && changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@turbo/gen": "^1.10.16",
|
||||
"@types/jest": "^29.5.6",
|
||||
"eslint": "^8.52.0",
|
||||
"@types/jest": "^29.5.8",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"lint-staged": "^15.1.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"turbo": "^1.10.16"
|
||||
},
|
||||
"packageManager": "pnpm@7.15.0",
|
||||
"dependencies": {
|
||||
"@changesets/cli": "^2.26.2"
|
||||
},
|
||||
"packageManager": "pnpm@8.10.5+sha256.a4bd9bb7b48214bbfcd95f264bd75bb70d100e5d4b58808f5cd6ab40c6ac21c5",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"trim": "1.0.1",
|
||||
"@babel/traverse": "7.23.2"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md}": "prettier --write"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# llamaindex
|
||||
|
||||
## 0.0.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 63f2108: Add multimodal support (thanks Marcus)
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "llamaindex",
|
||||
"version": "0.0.31",
|
||||
"version": "0.0.35",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.8.1",
|
||||
"@anthropic-ai/sdk": "^0.9.0",
|
||||
"@notionhq/client": "^2.2.13",
|
||||
"js-tiktoken": "^1.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -11,19 +11,19 @@
|
||||
"md-utils-ts": "^2.0.0",
|
||||
"mongodb": "^6.2.0",
|
||||
"notion-md-crawler": "^0.0.2",
|
||||
"openai": "^4.14.0",
|
||||
"openai": "^4.16.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"portkey-ai": "^0.1.13",
|
||||
"portkey-ai": "^0.1.16",
|
||||
"rake-modified": "^1.0.8",
|
||||
"replicate": "^0.20.1",
|
||||
"replicate": "^0.21.1",
|
||||
"string-strip-html": "^13.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"wink-nlp": "^1.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "^18.18.7",
|
||||
"@types/node": "^18.18.8",
|
||||
"@types/papaparse": "^5.3.10",
|
||||
"@types/pdf-parse": "^1.1.3",
|
||||
"@types/uuid": "^9.0.6",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { ChatHistory } from "./ChatHistory";
|
||||
import { BaseNodePostprocessor } from "./indices/BaseNodePostprocessor";
|
||||
import { ChatMessage, LLM, OpenAI } from "./llm/LLM";
|
||||
import { NodeWithScore, TextNode } from "./Node";
|
||||
import {
|
||||
CondenseQuestionPrompt,
|
||||
@@ -15,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.
|
||||
@@ -328,6 +328,17 @@ export class ContextChatEngine implements ChatEngine {
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageContentDetail {
|
||||
type: "text" | "image_url";
|
||||
text: string;
|
||||
image_url: { url: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended type for the content of a message that allows for multi-modal messages.
|
||||
*/
|
||||
export type MessageContent = string | MessageContentDetail[];
|
||||
|
||||
/**
|
||||
* HistoryChatEngine is a ChatEngine that uses a `ChatHistory` object
|
||||
* to keeps track of chat's message history.
|
||||
@@ -347,38 +358,34 @@ export class HistoryChatEngine {
|
||||
async chat<
|
||||
T extends boolean | undefined = undefined,
|
||||
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
|
||||
>(message: string, chatHistory: ChatHistory, streaming?: T): Promise<R> {
|
||||
>(
|
||||
message: MessageContent,
|
||||
chatHistory: ChatHistory,
|
||||
streaming?: T,
|
||||
): Promise<R> {
|
||||
//Streaming option
|
||||
if (streaming) {
|
||||
return this.streamChat(message, chatHistory) as R;
|
||||
}
|
||||
const context = await this.contextGenerator?.generate(message);
|
||||
chatHistory.addMessage({
|
||||
content: message,
|
||||
role: "user",
|
||||
});
|
||||
const response = await this.llm.chat(
|
||||
await chatHistory.requestMessages(
|
||||
context ? [context.message] : undefined,
|
||||
),
|
||||
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;
|
||||
}
|
||||
|
||||
protected async *streamChat(
|
||||
message: string,
|
||||
message: MessageContent,
|
||||
chatHistory: ChatHistory,
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
const context = await this.contextGenerator?.generate(message);
|
||||
chatHistory.addMessage({
|
||||
content: message,
|
||||
role: "user",
|
||||
});
|
||||
const requestMessages = await this.prepareRequestMessages(
|
||||
message,
|
||||
chatHistory,
|
||||
);
|
||||
const response_stream = await this.llm.chat(
|
||||
await chatHistory.requestMessages(
|
||||
context ? [context.message] : undefined,
|
||||
),
|
||||
requestMessages,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
@@ -394,4 +401,31 @@ export class HistoryChatEngine {
|
||||
});
|
||||
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 { encodingForModel, TiktokenModel } from "js-tiktoken";
|
||||
import { encodingForModel } from "js-tiktoken";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Event, EventTag, EventType } from "./callbacks/CallbackManager";
|
||||
@@ -27,7 +27,7 @@ class GlobalsHelper {
|
||||
const numberArray = Array.from(tokens);
|
||||
const text = encoding.decode(numberArray);
|
||||
const uint8Array = new TextEncoder().encode(text);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -39,10 +39,10 @@ class GlobalsHelper {
|
||||
if (!this.defaultTokenizer) {
|
||||
this.initDefaultTokenizer();
|
||||
}
|
||||
|
||||
|
||||
return this.defaultTokenizer!.encode.bind(this.defaultTokenizer);
|
||||
}
|
||||
|
||||
|
||||
tokenizerDecoder(encoding?: string) {
|
||||
if (encoding && encoding !== Tokenizers.CL100K_BASE) {
|
||||
throw new Error(`Tokenizer encoding ${encoding} not yet supported`);
|
||||
@@ -50,7 +50,7 @@ class GlobalsHelper {
|
||||
if (!this.defaultTokenizer) {
|
||||
this.initDefaultTokenizer();
|
||||
}
|
||||
|
||||
|
||||
return this.defaultTokenizer!.decode.bind(this.defaultTokenizer);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Event } from "./callbacks/CallbackManager";
|
||||
import { BaseNodePostprocessor } from "./indices/BaseNodePostprocessor";
|
||||
import { NodeWithScore, TextNode } from "./Node";
|
||||
import {
|
||||
BaseQuestionGenerator,
|
||||
@@ -12,6 +10,8 @@ import { CompactAndRefine, ResponseSynthesizer } from "./ResponseSynthesizer";
|
||||
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,7 +30,7 @@ export interface DefaultStreamToken {
|
||||
index: number;
|
||||
delta: {
|
||||
content?: string | null;
|
||||
role?: "user" | "assistant" | "system" | "function";
|
||||
role?: "user" | "assistant" | "system" | "function" | "tool";
|
||||
};
|
||||
finish_reason: string | null;
|
||||
}[];
|
||||
|
||||
+12
-12
@@ -1,11 +1,7 @@
|
||||
export * from "./callbacks/CallbackManager";
|
||||
export * from "./ChatEngine";
|
||||
export * from "./ChatHistory";
|
||||
export * from "./constants";
|
||||
export * from "./Embedding";
|
||||
export * from "./GlobalsHelper";
|
||||
export * from "./indices";
|
||||
export * from "./llm/LLM";
|
||||
export * from "./Node";
|
||||
export * from "./NodeParser";
|
||||
export * from "./OutputParser";
|
||||
@@ -13,17 +9,21 @@ export * from "./Prompt";
|
||||
export * from "./PromptHelper";
|
||||
export * from "./QueryEngine";
|
||||
export * from "./QuestionGenerator";
|
||||
export * from "./readers/base";
|
||||
export * from "./readers/CSVReader";
|
||||
export * from "./readers/MarkdownReader";
|
||||
export * from "./readers/NotionReader";
|
||||
export * from "./readers/PDFReader";
|
||||
export * from "./readers/HTMLReader";
|
||||
export * from "./readers/SimpleDirectoryReader";
|
||||
export * from "./Response";
|
||||
export * from "./ResponseSynthesizer";
|
||||
export * from "./Retriever";
|
||||
export * from "./ServiceContext";
|
||||
export * from "./storage";
|
||||
export * from "./TextSplitter";
|
||||
export * from "./Tool";
|
||||
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";
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
ServiceContext,
|
||||
serviceContextFromDefaults,
|
||||
} from "../../ServiceContext";
|
||||
import { BaseDocumentStore, RefDocInfo } from "../../storage/docStore/types";
|
||||
import {
|
||||
StorageContext,
|
||||
storageContextFromDefaults,
|
||||
} from "../../storage/StorageContext";
|
||||
import { BaseDocumentStore, RefDocInfo } from "../../storage/docStore/types";
|
||||
import {
|
||||
BaseIndex,
|
||||
BaseIndexInit,
|
||||
|
||||
@@ -32,7 +32,11 @@ export class VectorIndexRetriever implements BaseRetriever {
|
||||
this.similarityTopK = similarityTopK ?? DEFAULT_SIMILARITY_TOP_K;
|
||||
}
|
||||
|
||||
async retrieve(query: string, parentEvent?: Event, preFilters?: unknown): Promise<NodeWithScore[]> {
|
||||
async retrieve(
|
||||
query: string,
|
||||
parentEvent?: Event,
|
||||
preFilters?: unknown,
|
||||
): Promise<NodeWithScore[]> {
|
||||
const queryEmbedding =
|
||||
await this.serviceContext.embedModel.getQueryEmbedding(query);
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import {
|
||||
StreamCallbackResponse,
|
||||
} from "../callbacks/CallbackManager";
|
||||
|
||||
import { ChatCompletionMessageParam } from "openai/resources";
|
||||
import { LLMOptions } from "portkey-ai";
|
||||
import { globalsHelper, Tokenizers } from "../GlobalsHelper";
|
||||
import {
|
||||
AnthropicSession,
|
||||
ANTHROPIC_AI_PROMPT,
|
||||
ANTHROPIC_HUMAN_PROMPT,
|
||||
AnthropicSession,
|
||||
getAnthropicSession,
|
||||
} from "./anthropic";
|
||||
import {
|
||||
@@ -36,7 +37,7 @@ export type MessageType =
|
||||
| "memory";
|
||||
|
||||
export interface ChatMessage {
|
||||
content: string;
|
||||
content: any;
|
||||
role: MessageType;
|
||||
}
|
||||
|
||||
@@ -102,11 +103,14 @@ export interface LLM {
|
||||
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 },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -114,7 +118,7 @@ export const TURBO_MODELS = {
|
||||
*/
|
||||
export const ALL_AVAILABLE_OPENAI_MODELS = {
|
||||
...GPT4_MODELS,
|
||||
...TURBO_MODELS,
|
||||
...GPT35_MODELS,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -250,10 +254,13 @@ export class OpenAI implements LLM {
|
||||
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,
|
||||
};
|
||||
@@ -298,10 +305,13 @@ export class OpenAI implements LLM {
|
||||
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,
|
||||
};
|
||||
@@ -367,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",
|
||||
@@ -379,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",
|
||||
@@ -393,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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -405,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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,7 +436,7 @@ export class LlamaDeuce implements LLM {
|
||||
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;
|
||||
@@ -458,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);
|
||||
}
|
||||
@@ -493,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.`;
|
||||
@@ -543,11 +571,18 @@ 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,
|
||||
@@ -648,7 +683,7 @@ export class Anthropic implements LLM {
|
||||
|
||||
this.callbackManager = init?.callbackManager;
|
||||
}
|
||||
|
||||
|
||||
tokens(messages: ChatMessage[]): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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;
|
||||
export const readEnv = (
|
||||
env: string,
|
||||
default_val?: string,
|
||||
): string | undefined => {
|
||||
if (typeof process !== "undefined") {
|
||||
return process.env?.[env] ?? default_val;
|
||||
}
|
||||
return default_val;
|
||||
};
|
||||
@@ -12,23 +15,23 @@ interface PortkeyOptions {
|
||||
apiKey?: string;
|
||||
baseURL?: string;
|
||||
mode?: string;
|
||||
llms?: [LLMOptions] | null
|
||||
llms?: [LLMOptions] | null;
|
||||
}
|
||||
|
||||
export class PortkeySession {
|
||||
portkey: Portkey;
|
||||
|
||||
constructor(options:PortkeyOptions = {}) {
|
||||
constructor(options: PortkeyOptions = {}) {
|
||||
if (!options.apiKey) {
|
||||
options.apiKey = readEnv('PORTKEY_API_KEY')
|
||||
options.apiKey = readEnv("PORTKEY_API_KEY");
|
||||
}
|
||||
|
||||
if (!options.baseURL) {
|
||||
options.baseURL = readEnv('PORTKEY_BASE_URL', "https://api.portkey.ai")
|
||||
options.baseURL = readEnv("PORTKEY_BASE_URL", "https://api.portkey.ai");
|
||||
}
|
||||
|
||||
this.portkey = new Portkey({});
|
||||
this.portkey.llms = [{}]
|
||||
this.portkey.llms = [{}];
|
||||
if (!options.apiKey) {
|
||||
throw new Error("Set Portkey ApiKey in PORTKEY_API_KEY env variable");
|
||||
}
|
||||
@@ -59,4 +62,3 @@ export function getPortkeySession(options: PortkeyOptions = {}) {
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import mammoth from "mammoth";
|
||||
import { Document } from "../Node";
|
||||
import { DEFAULT_FS } from "../storage/constants";
|
||||
import { GenericFileSystem } from "../storage/FileSystem";
|
||||
import { DEFAULT_FS } from "../storage/constants";
|
||||
import { BaseReader } from "./base";
|
||||
|
||||
export class DocxReader implements BaseReader {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# create-llama
|
||||
|
||||
## 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)
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 91b42a3: change version (thanks Marcus)
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e2a6805: Hello Create Llama (thanks Marcus!)
|
||||
@@ -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,78 @@
|
||||
# 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.
|
||||
|
||||
## Features
|
||||
|
||||
- NextJS, ExpressJS, or FastAPI (python) stateless backend generation 💻
|
||||
- Streaming or non-streaming backend ⚡
|
||||
- Optional `shadcn` frontend generation 🎨
|
||||
|
||||
## Get Started
|
||||
|
||||
You can run `create-llama` in interactive or non-interactive mode.
|
||||
|
||||
### Interactive
|
||||
|
||||
You can create a new project interactively by running:
|
||||
|
||||
```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.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
### Non-interactive
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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.5",
|
||||
"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 };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import { copy } from "../helpers/copy";
|
||||
import { callPackageManager } from "../helpers/install";
|
||||
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bold, cyan } from "picocolors";
|
||||
import { version } from "../../core/package.json";
|
||||
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
import {
|
||||
InstallTemplateArgs,
|
||||
TemplateEngine,
|
||||
TemplateFramework,
|
||||
} from "./types";
|
||||
|
||||
const envFileNameMap: Record<TemplateFramework, string> = {
|
||||
nextjs: ".env.local",
|
||||
express: ".env",
|
||||
fastapi: ".env",
|
||||
};
|
||||
|
||||
const createEnvLocalFile = async (
|
||||
root: string,
|
||||
framework: TemplateFramework,
|
||||
openAIKey?: string,
|
||||
) => {
|
||||
if (openAIKey) {
|
||||
const envFileName = envFileNameMap[framework];
|
||||
if (!envFileName) return;
|
||||
await fs.writeFile(
|
||||
path.join(root, envFileName),
|
||||
`OPENAI_API_KEY=${openAIKey}\n`,
|
||||
);
|
||||
console.log(`Created '${envFileName}' file containing OPENAI_API_KEY`);
|
||||
process.env["OPENAI_API_KEY"] = openAIKey;
|
||||
}
|
||||
};
|
||||
|
||||
const copyTestData = async (
|
||||
root: string,
|
||||
framework: TemplateFramework,
|
||||
packageManager?: PackageManager,
|
||||
engine?: TemplateEngine,
|
||||
) => {
|
||||
if (engine === "context" || framework === "fastapi") {
|
||||
const srcPath = path.join(__dirname, "components", "data");
|
||||
const destPath = path.join(root, "data");
|
||||
console.log(`\nCopying test data to ${cyan(destPath)}\n`);
|
||||
await copy("**", destPath, {
|
||||
parents: true,
|
||||
cwd: srcPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (packageManager && engine === "context") {
|
||||
if (process.env["OPENAI_API_KEY"]) {
|
||||
console.log(
|
||||
`\nRunning ${cyan(
|
||||
`${packageManager} run generate`,
|
||||
)} to generate the context data.\n`,
|
||||
);
|
||||
await callPackageManager(packageManager, true, ["run", "generate"]);
|
||||
console.log();
|
||||
} else {
|
||||
console.log(
|
||||
`\nAfter setting your OpenAI key, run ${cyan(
|
||||
`${packageManager} run generate`,
|
||||
)} to generate the context data.\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rename = (name: string) => {
|
||||
switch (name) {
|
||||
case "gitignore":
|
||||
case "eslintrc.json": {
|
||||
return `.${name}`;
|
||||
}
|
||||
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
|
||||
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Install a LlamaIndex internal template to a given `root` directory.
|
||||
*/
|
||||
const installTSTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
packageManager,
|
||||
isOnline,
|
||||
template,
|
||||
framework,
|
||||
engine,
|
||||
ui,
|
||||
eslint,
|
||||
customApiPath,
|
||||
}: InstallTemplateArgs) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
/**
|
||||
* Copy the template files to the target directory.
|
||||
*/
|
||||
console.log("\nInitializing project with template:", template, "\n");
|
||||
const templatePath = path.join(__dirname, "types", template, framework);
|
||||
const copySource = ["**"];
|
||||
if (!eslint) copySource.push("!eslintrc.json");
|
||||
|
||||
await copy(copySource, root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
rename,
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy the selected chat engine files to the target directory and reference it.
|
||||
*/
|
||||
let relativeEngineDestPath;
|
||||
const compPath = path.join(__dirname, "components");
|
||||
if (engine && (framework === "express" || framework === "nextjs")) {
|
||||
console.log("\nUsing chat engine:", engine, "\n");
|
||||
const enginePath = path.join(compPath, "engines", engine);
|
||||
relativeEngineDestPath =
|
||||
framework === "nextjs"
|
||||
? path.join("app", "api", "chat")
|
||||
: path.join("src", "controllers");
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "engine"), {
|
||||
parents: true,
|
||||
cwd: enginePath,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the selected UI files to the target directory and reference it.
|
||||
*/
|
||||
if (framework === "nextjs" && ui !== "html") {
|
||||
console.log("\nUsing UI:", ui, "\n");
|
||||
const uiPath = path.join(compPath, "ui", ui);
|
||||
const destUiPath = path.join(root, "app", "components", "ui");
|
||||
// remove the default ui folder
|
||||
await fs.rm(destUiPath, { recursive: true });
|
||||
// copy the selected ui folder
|
||||
await copy("**", destUiPath, {
|
||||
parents: true,
|
||||
cwd: uiPath,
|
||||
rename,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the package.json scripts.
|
||||
*/
|
||||
const packageJsonFile = path.join(root, "package.json");
|
||||
const packageJson: any = JSON.parse(
|
||||
await fs.readFile(packageJsonFile, "utf8"),
|
||||
);
|
||||
packageJson.name = appName;
|
||||
packageJson.version = "0.1.0";
|
||||
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
llamaindex: version,
|
||||
};
|
||||
|
||||
if (framework === "nextjs" && customApiPath) {
|
||||
console.log(
|
||||
"\nUsing external API with custom API path:",
|
||||
customApiPath,
|
||||
"\n",
|
||||
);
|
||||
// remove the default api folder
|
||||
const apiPath = path.join(root, "app", "api");
|
||||
await fs.rm(apiPath, { recursive: true });
|
||||
// modify the dev script to use the custom api path
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
dev: `NEXT_PUBLIC_CHAT_API=${customApiPath} next dev`,
|
||||
};
|
||||
}
|
||||
|
||||
if (engine === "context" && relativeEngineDestPath) {
|
||||
// add generate script if using context engine
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
generate: `node ${path.join(
|
||||
relativeEngineDestPath,
|
||||
"engine",
|
||||
"generate.mjs",
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (framework === "nextjs" && ui === "shadcn") {
|
||||
// add shadcn dependencies to package.json
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"tailwind-merge": "^2",
|
||||
"@radix-ui/react-slot": "^1",
|
||||
"class-variance-authority": "^0.7",
|
||||
"lucide-react": "^0.291",
|
||||
remark: "^14.0.3",
|
||||
"remark-code-import": "^1.2.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
};
|
||||
|
||||
packageJson.devDependencies = {
|
||||
...packageJson.devDependencies,
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
};
|
||||
}
|
||||
|
||||
if (!eslint) {
|
||||
// Remove packages starting with "eslint" from devDependencies
|
||||
packageJson.devDependencies = Object.fromEntries(
|
||||
Object.entries(packageJson.devDependencies).filter(
|
||||
([key]) => !key.startsWith("eslint"),
|
||||
),
|
||||
);
|
||||
}
|
||||
await fs.writeFile(
|
||||
packageJsonFile,
|
||||
JSON.stringify(packageJson, null, 2) + os.EOL,
|
||||
);
|
||||
|
||||
console.log("\nInstalling dependencies:");
|
||||
for (const dependency in packageJson.dependencies)
|
||||
console.log(`- ${cyan(dependency)}`);
|
||||
|
||||
console.log("\nInstalling devDependencies:");
|
||||
for (const dependency in packageJson.devDependencies)
|
||||
console.log(`- ${cyan(dependency)}`);
|
||||
|
||||
console.log();
|
||||
|
||||
await callPackageManager(packageManager, isOnline);
|
||||
};
|
||||
|
||||
const installPythonTemplate = async ({
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
}: Pick<InstallTemplateArgs, "root" | "framework" | "template">) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
const templatePath = path.join(__dirname, "types", template, framework);
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
rename(name) {
|
||||
switch (name) {
|
||||
case "gitignore": {
|
||||
return `.${name}`;
|
||||
}
|
||||
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
|
||||
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
|
||||
case "README-template.md": {
|
||||
return "README.md";
|
||||
}
|
||||
default: {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
"\nPython project, dependencies won't be installed automatically.\n",
|
||||
);
|
||||
};
|
||||
|
||||
export const installTemplate = async (
|
||||
props: InstallTemplateArgs & { backend: boolean },
|
||||
) => {
|
||||
process.chdir(props.root);
|
||||
if (props.framework === "fastapi") {
|
||||
await installPythonTemplate(props);
|
||||
} else {
|
||||
await installTSTemplate(props);
|
||||
}
|
||||
|
||||
if (props.backend) {
|
||||
// This is a backend, so we need to copy the test data and create the env file.
|
||||
|
||||
// Copy the environment file to the target directory.
|
||||
await createEnvLocalFile(props.root, props.framework, props.openAIKey);
|
||||
|
||||
// Copy test pdf file
|
||||
await copyTestData(
|
||||
props.root,
|
||||
props.framework,
|
||||
props.packageManager,
|
||||
props.engine,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
|
||||
export type TemplateType = "simple" | "streaming";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateEngine = "simple" | "context";
|
||||
export type TemplateUI = "html" | "shadcn";
|
||||
|
||||
export interface InstallTemplateArgs {
|
||||
appName: string;
|
||||
root: string;
|
||||
packageManager: PackageManager;
|
||||
isOnline: boolean;
|
||||
template: TemplateType;
|
||||
framework: TemplateFramework;
|
||||
engine?: TemplateEngine;
|
||||
ui: TemplateUI;
|
||||
eslint: boolean;
|
||||
customApiPath?: string;
|
||||
openAIKey?: string;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Express](https://expressjs.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then call the express API endpoint `/api/chat` to see the result:
|
||||
|
||||
```
|
||||
curl --location 'localhost:8000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
|
||||
```
|
||||
|
||||
You can start editing the API by modifying `src/controllers/chat.controller.ts`. The endpoint auto-updates as you save the file.
|
||||
|
||||
## Production
|
||||
|
||||
First, build the project:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can then run the production server:
|
||||
|
||||
```
|
||||
NODE_ENV=production npm run start
|
||||
```
|
||||
|
||||
> Note that the `NODE_ENV` environment variable is set to `production`. This disables CORS for all origins.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "eslint:recommended"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import cors from "cors";
|
||||
import "dotenv/config";
|
||||
import express, { Express, Request, Response } from "express";
|
||||
import chatRouter from "./src/routes/chat.route";
|
||||
|
||||
const app: Express = express();
|
||||
const port = 8000;
|
||||
|
||||
const env = process.env["NODE_ENV"];
|
||||
const isDevelopment = !env || env === "development";
|
||||
if (isDevelopment) {
|
||||
console.warn("Running in development mode - allowing CORS for all origins");
|
||||
app.use(cors());
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.send("LlamaIndex Express Server");
|
||||
});
|
||||
|
||||
app.use("/api/chat", chatRouter);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "llama-index-express",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsup index.ts --format esm --dts",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4",
|
||||
"llamaindex": "0.0.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.16",
|
||||
"@types/express": "^4",
|
||||
"@types/node": "^20",
|
||||
"concurrently": "^8",
|
||||
"eslint": "^8",
|
||||
"nodemon": "^3",
|
||||
"tsup": "^7",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { ChatMessage, OpenAI } from "llamaindex";
|
||||
import { createChatEngine } from "./engine";
|
||||
|
||||
export const chat = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { messages }: { messages: ChatMessage[] } = req.body;
|
||||
const lastMessage = messages.pop();
|
||||
if (!messages || !lastMessage || lastMessage.role !== "user") {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
"messages are required in the request body and the last message must be from the user",
|
||||
});
|
||||
}
|
||||
|
||||
const llm = new OpenAI({
|
||||
model: "gpt-3.5-turbo",
|
||||
});
|
||||
|
||||
const chatEngine = await createChatEngine(llm);
|
||||
|
||||
const response = await chatEngine.chat(lastMessage.content, messages);
|
||||
const result: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: response.response,
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return res.status(500).json({
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { LLM, SimpleChatEngine } from "llamaindex";
|
||||
|
||||
export async function createChatEngine(llm: LLM) {
|
||||
return new SimpleChatEngine({
|
||||
llm,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import express from "express";
|
||||
import { chat } from "../controllers/chat.controller";
|
||||
|
||||
const llmRouter = express.Router();
|
||||
|
||||
llmRouter.route("/").post(chat);
|
||||
|
||||
export default llmRouter;
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, setup the environment:
|
||||
|
||||
```
|
||||
poetry install
|
||||
poetry shell
|
||||
```
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```
|
||||
python main.py
|
||||
```
|
||||
|
||||
Then call the API endpoint `/api/chat` to see the result:
|
||||
|
||||
```
|
||||
curl --location 'localhost:8000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
|
||||
```
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py`. The endpoint auto-updates as you save the file.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod uvicorn main:app
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,56 @@
|
||||
from typing import List
|
||||
|
||||
from app.utils.index import get_index
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from llama_index import VectorStoreIndex
|
||||
from llama_index.llms.base import MessageRole, ChatMessage
|
||||
from pydantic import BaseModel
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
|
||||
|
||||
class _Message(BaseModel):
|
||||
role: MessageRole
|
||||
content: str
|
||||
|
||||
|
||||
class _ChatData(BaseModel):
|
||||
messages: List[_Message]
|
||||
|
||||
|
||||
class _Result(BaseModel):
|
||||
result: _Message
|
||||
|
||||
|
||||
@r.post("")
|
||||
async def chat(
|
||||
data: _ChatData,
|
||||
index: VectorStoreIndex = Depends(get_index),
|
||||
) -> _Result:
|
||||
# check preconditions and get last message
|
||||
if len(data.messages) == 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No messages provided",
|
||||
)
|
||||
lastMessage = data.messages.pop()
|
||||
if lastMessage.role != MessageRole.USER:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Last message must be from user",
|
||||
)
|
||||
# convert messages coming from the request to type ChatMessage
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role=m.role,
|
||||
content=m.content,
|
||||
)
|
||||
for m in data.messages
|
||||
]
|
||||
|
||||
# query chat engine
|
||||
chat_engine = index.as_chat_engine()
|
||||
response = chat_engine.chat(lastMessage.content, messages)
|
||||
return _Result(
|
||||
result=_Message(role=MessageRole.ASSISTANT, content=response.response)
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from llama_index import (
|
||||
SimpleDirectoryReader,
|
||||
StorageContext,
|
||||
VectorStoreIndex,
|
||||
load_index_from_storage,
|
||||
)
|
||||
|
||||
|
||||
STORAGE_DIR = "./storage" # directory to cache the generated index
|
||||
DATA_DIR = "./data" # directory containing the documents to index
|
||||
|
||||
|
||||
def get_index():
|
||||
logger = logging.getLogger("uvicorn")
|
||||
# check if storage already exists
|
||||
if not os.path.exists(STORAGE_DIR):
|
||||
logger.info("Creating new index")
|
||||
# load the documents and create the index
|
||||
documents = SimpleDirectoryReader(DATA_DIR).load_data()
|
||||
index = VectorStoreIndex.from_documents(documents)
|
||||
# store it for later
|
||||
index.storage_context.persist(STORAGE_DIR)
|
||||
logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}")
|
||||
else:
|
||||
# load the existing index
|
||||
logger.info(f"Loading index from {STORAGE_DIR}...")
|
||||
storage_context = StorageContext.from_defaults(persist_dir=STORAGE_DIR)
|
||||
index = load_index_from_storage(storage_context)
|
||||
logger.info(f"Finished loading index from {STORAGE_DIR}")
|
||||
return index
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
storage
|
||||
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
import os
|
||||
import uvicorn
|
||||
from app.api.routers.chat import chat_router
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
|
||||
|
||||
|
||||
if environment == "dev":
|
||||
logger = logging.getLogger("uvicorn")
|
||||
logger.warning("Running in development mode - allowing CORS for all origins")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(chat_router, prefix="/api/chat")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app="main:app", host="0.0.0.0", reload=True)
|
||||
@@ -0,0 +1,19 @@
|
||||
[tool.poetry]
|
||||
name = "llamaindex-fastapi"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Marcus Schiesser <mail@marcusschiesser.de>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11,<3.12"
|
||||
fastapi = "^0.104.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
llama-index = "^0.8.56"
|
||||
pypdf = "^3.17.0"
|
||||
python-dotenv = "^1.0.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -0,0 +1,3 @@
|
||||
# Rename this file to `.env.local` to use environment variables locally with `next dev`
|
||||
# https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
|
||||
MY_HOST="example.com"
|
||||
@@ -0,0 +1,30 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,7 @@
|
||||
import { LLM, SimpleChatEngine } from "llamaindex";
|
||||
|
||||
export async function createChatEngine(llm: LLM) {
|
||||
return new SimpleChatEngine({
|
||||
llm,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ChatMessage, OpenAI } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createChatEngine } from "./engine";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages }: { messages: ChatMessage[] } = body;
|
||||
const lastMessage = messages.pop();
|
||||
if (!messages || !lastMessage || lastMessage.role !== "user") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"messages are required in the request body and the last message must be from the user",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const llm = new OpenAI({
|
||||
model: "gpt-3.5-turbo",
|
||||
});
|
||||
|
||||
const chatEngine = await createChatEngine(llm);
|
||||
|
||||
const response = await chatEngine.chat(lastMessage.content, messages);
|
||||
const result: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: response.response,
|
||||
};
|
||||
|
||||
return NextResponse.json({ result });
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: (error as Error).message,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
import { useState } from "react";
|
||||
import { ChatInput, ChatInputProps, ChatMessages, Message } from "./ui/chat";
|
||||
|
||||
function useChat(): ChatInputProps & { messages: Message[] } {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
const getAssistantMessage = async (messages: Message[]) => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_CHAT_API ?? "/api/chat",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = await response.json();
|
||||
const assistantMessage = data.result as Message;
|
||||
return assistantMessage;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!input) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const newMessages = [
|
||||
...messages,
|
||||
{ id: nanoid(), content: input, role: "user" },
|
||||
];
|
||||
setMessages(newMessages);
|
||||
setInput("");
|
||||
const assistantMessage = await getAssistantMessage(newMessages);
|
||||
setMessages([...newMessages, { ...assistantMessage, id: nanoid() }]);
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: any): void => {
|
||||
setInput(e.target.value);
|
||||
};
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
input,
|
||||
handleSubmit,
|
||||
handleInputChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ChatSection() {
|
||||
const { messages, isLoading, input, handleSubmit, handleInputChange } =
|
||||
useChat();
|
||||
return (
|
||||
<div className="space-y-4 max-w-5xl w-full">
|
||||
<ChatMessages messages={messages} />
|
||||
<ChatInput
|
||||
handleSubmit={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
input={input}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user