mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-13 22:17:48 -04:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Fix issue where it doesn't find OpenAI Key when running npm run generate (#182) (thanks @RayFernando1337)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"llamaindex": patch
|
||||
---
|
||||
|
||||
Add HTMLReader (thanks @mtutty)
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ import readline from "node:readline/promises";
|
||||
import { ChatMessage, LlamaDeuce, OpenAI } from "llamaindex";
|
||||
|
||||
(async () => {
|
||||
const gpt4 = new OpenAI({ model: "gpt-4", temperature: 0.9 });
|
||||
const gpt4 = new OpenAI({ model: "gpt-4-vision-preview", temperature: 0.9 });
|
||||
const l2 = new LlamaDeuce({
|
||||
model: "Llama-2-70b-chat-4bit",
|
||||
temperature: 0.9,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
+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);
|
||||
})();
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"ts-jest": "^29.1.1",
|
||||
"turbo": "^1.10.16"
|
||||
},
|
||||
"packageManager": "pnpm@7.15.0",
|
||||
"packageManager": "pnpm@8.10.4+sha256.df3202c6c8fd345be5ba6a4199297582d5bebf8963822aa3344f4cd2b8be8d43",
|
||||
"dependencies": {
|
||||
"@changesets/cli": "^2.26.2"
|
||||
},
|
||||
|
||||
@@ -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,10 +11,10 @@
|
||||
"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",
|
||||
"string-strip-html": "^13.4.3",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}[];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -648,7 +658,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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# create-llama
|
||||
|
||||
## 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
|
||||
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",
|
||||
value: "simple (no data, just chat)",
|
||||
},
|
||||
],
|
||||
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.4",
|
||||
"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 React, { FC, memo } from "react"
|
||||
import { Check, Copy, Download } from "lucide-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 { ChatMessages, ChatInput };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">app/page.tsx</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||
<a
|
||||
href="https://www.llamaindex.ai/"
|
||||
className="flex items-center justify-center font-nunito text-lg font-bold gap-2"
|
||||
>
|
||||
<span>Built by LlamaIndex</span>
|
||||
<Image
|
||||
className="rounded-xl"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Message } from "./chat-messages";
|
||||
|
||||
export default function ChatAvatar(message: Message) {
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white">
|
||||
<Image
|
||||
className="rounded-md"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
export interface ChatInputProps {
|
||||
/** The current value of the input */
|
||||
input?: string;
|
||||
/** An input/textarea-ready onChange handler to control the value of the input */
|
||||
handleInputChange?: (
|
||||
e:
|
||||
| React.ChangeEvent<HTMLInputElement>
|
||||
| React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) => void;
|
||||
/** Form submission handler to automatically reset input and append a user message */
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function ChatInput(props: ChatInputProps) {
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={props.handleSubmit}
|
||||
className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4"
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
name="message"
|
||||
placeholder="Type a message"
|
||||
className="w-full p-4 rounded-xl shadow-inner flex-1"
|
||||
value={props.input}
|
||||
onChange={props.handleInputChange}
|
||||
/>
|
||||
<button
|
||||
disabled={props.isLoading}
|
||||
type="submit"
|
||||
className="p-4 text-white rounded-xl shadow-xl bg-gradient-to-r from-cyan-500 to-sky-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send message
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import ChatAvatar from "./chat-avatar";
|
||||
import { Message } from "./chat-messages";
|
||||
|
||||
export default function ChatItem(message: Message) {
|
||||
return (
|
||||
<div className="flex items-start gap-4 pt-5">
|
||||
<ChatAvatar {...message} />
|
||||
<p className="break-words">{message.content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import ChatItem from "./chat-item";
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export default function ChatMessages({ messages }: { messages: Message[] }) {
|
||||
const scrollableChatContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollableChatContainerRef.current) {
|
||||
scrollableChatContainerRef.current.scrollTop =
|
||||
scrollableChatContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages.length]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl">
|
||||
<div
|
||||
className="flex flex-col gap-5 divide-y h-[50vh] overflow-auto"
|
||||
ref={scrollableChatContainerRef}
|
||||
>
|
||||
{messages.map((m: Message) => (
|
||||
<ChatItem key={m.id} {...m} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import ChatInput from "./chat-input";
|
||||
import ChatMessages from "./chat-messages";
|
||||
|
||||
export type { ChatInputProps } from "./chat-input";
|
||||
export type { Message } from "./chat-messages";
|
||||
export { ChatMessages, ChatInput };
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,94 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 215 20.2% 65.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 216 34% 17%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings:
|
||||
"rlig" 1,
|
||||
"calt" 1;
|
||||
}
|
||||
.background-gradient {
|
||||
background-color: #fff;
|
||||
background-image: radial-gradient(
|
||||
at 21% 11%,
|
||||
rgba(186, 186, 233, 0.53) 0,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%),
|
||||
radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%),
|
||||
radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Llama App",
|
||||
description: "Generated by create-llama",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import ChatSection from "@/app/components/chat-section";
|
||||
import Header from "@/app/components/header";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center gap-10 p-24 background-gradient">
|
||||
<Header />
|
||||
<ChatSection />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["llamaindex"],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "llama-index-nextjs",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"llamaindex": "0.0.31",
|
||||
"nanoid": "^5",
|
||||
"next": "^13",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^13",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,78 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ["class"],
|
||||
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
xl: `calc(var(--radius) + 4px)`,
|
||||
lg: `var(--radius)`,
|
||||
md: `calc(var(--radius) - 2px)`,
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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!
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user