mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-18 13:05:55 -04:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78776ac51e | |||
| 416073db1d | |||
| 84929de8b2 | |||
| 6fe240b854 | |||
| 8bb1024d0f | |||
| 988bfc2a60 | |||
| 056e376ee0 | |||
| 819cccb11a | |||
| 8a5ece10c2 | |||
| 63bb0505d6 | |||
| 2e80ef47ee | |||
| a1feb524e9 | |||
| 06823da849 | |||
| 7bd3ed551f | |||
| c981eb1423 | |||
| c094b0c6bf | |||
| e2567ffc03 | |||
| 5d8d752b16 | |||
| a0b04be23c | |||
| 94a2809ecd | |||
| e29ef92564 | |||
| 6bdd4ac69d | |||
| 1ad25451a6 | |||
| cfb5257a1e | |||
| 046ff06157 | |||
| 8b81b17984 | |||
| f1c3e8df69 | |||
| 089916a148 | |||
| 3bb94da804 | |||
| 418bf9ba8a | |||
| e5d20b66f6 | |||
| ae7b30106d | |||
| 5fb64b74ca | |||
| e4665b6c0d | |||
| 5463d3bf4b | |||
| 7225e916fd | |||
| 897feb9914 | |||
| 66b5f38eda |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": true,
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Use `gpt-4-turbo` model as default. Upgrade Python llama-index to 0.10.28
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Remove asking for AI models and use defaults instead (OpenAIs GPT-4 Vision Preview and Embeddings v3). Use `--ask-models` CLI parameter to select models.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add observability for Python
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Use poetry run generate to generate embeddings for FastAPI
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Use Settings object for LlamaIndex configuration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add Qdrant support
|
||||
@@ -35,12 +35,13 @@ jobs:
|
||||
with:
|
||||
version: ${{ env.POETRY_VERSION }}
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -14,12 +14,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish to GitHub Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build tarball
|
||||
run: |
|
||||
pnpm pack
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "create-llama-*.tgz"
|
||||
name: Release ${{ github.ref }}
|
||||
bodyFile: "CHANGELOG.md"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -15,17 +15,41 @@ jobs:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Create Release Pull Request
|
||||
- name: Add auth token to .npmrc file
|
||||
run: |
|
||||
cat << EOF >> ".npmrc"
|
||||
//registry.npmjs.org/:_authToken=$NPM_TOKEN
|
||||
EOF
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Get changeset status
|
||||
id: get-changeset-status
|
||||
run: |
|
||||
pnpm changeset status --output .changeset/status.json
|
||||
new_version=$(jq -r '.releases[0].newVersion' < .changeset/status.json)
|
||||
rm -v .changeset/status.json
|
||||
echo "new-version=${new_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: Release ${{ steps.get-changeset-status.outputs.new-version }}
|
||||
title: Release ${{ steps.get-changeset-status.outputs.new-version }}
|
||||
# build package and call changeset publish
|
||||
publish: pnpm release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
# create-llama
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 416073d: Directly import vector stores to work with NextJS
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 056e376: Add support for displaying tool outputs (including weather widget as example)
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7bd3ed5: Support Anthropic and Gemini as model providers
|
||||
- 7bd3ed5: Support new agents from LITS 0.3
|
||||
- cfb5257: Display events (e.g. retrieving nodes) per chat message
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- f1c3e8d: Add Llama3 and Phi3 support using Ollama
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a0dec80: Use `gpt-4-turbo` model as default. Upgrade Python llama-index to 0.10.28
|
||||
- 753229d: Remove asking for AI models and use defaults instead (OpenAIs GPT-4 Vision Preview and Embeddings v3). Use `--ask-models` CLI parameter to select models.
|
||||
- 1d78202: Add observability for Python
|
||||
- 6acccd2: Use poetry run generate to generate embeddings for FastAPI
|
||||
- 9efcffe: Use Settings object for LlamaIndex configuration
|
||||
- 418bf9b: refactor: use tsx instead of ts-node
|
||||
- 1be69a5: Add Qdrant support
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ Install NodeJS. Preferably v18 using nvm or n.
|
||||
Inside the `create-llama` directory:
|
||||
|
||||
```
|
||||
npm i -g pnpm ts-node
|
||||
npm i -g pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
|
||||
+2
-6
@@ -30,10 +30,8 @@ export async function createApp({
|
||||
appPath,
|
||||
packageManager,
|
||||
frontend,
|
||||
openAiKey,
|
||||
modelConfig,
|
||||
llamaCloudKey,
|
||||
model,
|
||||
embeddingModel,
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
@@ -77,10 +75,8 @@ export async function createApp({
|
||||
ui,
|
||||
packageManager,
|
||||
isOnline,
|
||||
openAiKey,
|
||||
modelConfig,
|
||||
llamaCloudKey,
|
||||
model,
|
||||
embeddingModel,
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
|
||||
@@ -52,6 +52,7 @@ export const copy = async (
|
||||
export const assetRelocator = (name: string) => {
|
||||
switch (name) {
|
||||
case "gitignore":
|
||||
case "npmrc":
|
||||
case "eslintrc.json": {
|
||||
return `.${name}`;
|
||||
}
|
||||
|
||||
+112
-97
@@ -1,6 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import {
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateVectorDB,
|
||||
@@ -28,7 +29,10 @@ const renderEnvVar = (envVars: EnvVar[]): string => {
|
||||
);
|
||||
};
|
||||
|
||||
const getVectorDBEnvs = (vectorDb: TemplateVectorDB) => {
|
||||
const getVectorDBEnvs = (vectorDb?: TemplateVectorDB): EnvVar[] => {
|
||||
if (!vectorDb) {
|
||||
return [];
|
||||
}
|
||||
switch (vectorDb) {
|
||||
case "mongo":
|
||||
return [
|
||||
@@ -130,14 +134,111 @@ const getVectorDBEnvs = (vectorDb: TemplateVectorDB) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
return [
|
||||
{
|
||||
name: "MODEL_PROVIDER",
|
||||
description: "The provider for the AI models to use.",
|
||||
value: modelConfig.provider,
|
||||
},
|
||||
{
|
||||
name: "MODEL",
|
||||
description: "The name of LLM model to use.",
|
||||
value: modelConfig.model,
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_MODEL",
|
||||
description: "Name of the embedding model to use.",
|
||||
value: modelConfig.embeddingModel,
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_DIM",
|
||||
description: "Dimension of the embedding model to use.",
|
||||
value: modelConfig.dimensions.toString(),
|
||||
},
|
||||
...(modelConfig.provider === "openai"
|
||||
? [
|
||||
{
|
||||
name: "OPENAI_API_KEY",
|
||||
description: "The OpenAI API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
{
|
||||
name: "LLM_TEMPERATURE",
|
||||
description: "Temperature for sampling from the model.",
|
||||
},
|
||||
{
|
||||
name: "LLM_MAX_TOKENS",
|
||||
description: "Maximum number of tokens to generate.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "anthropic"
|
||||
? [
|
||||
{
|
||||
name: "ANTHROPIC_API_KEY",
|
||||
description: "The Anthropic API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "gemini"
|
||||
? [
|
||||
{
|
||||
name: "GOOGLE_API_KEY",
|
||||
description: "The Google API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
const getFrameworkEnvs = (
|
||||
framework?: TemplateFramework,
|
||||
port?: number,
|
||||
): EnvVar[] => {
|
||||
if (framework !== "fastapi") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: "APP_HOST",
|
||||
description: "The address to start the backend app.",
|
||||
value: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
name: "APP_PORT",
|
||||
description: "The port to start the backend app.",
|
||||
value: port?.toString() || "8000",
|
||||
},
|
||||
// TODO: Once LlamaIndexTS supports string templates, move this to `getEngineEnvs`
|
||||
{
|
||||
name: "SYSTEM_PROMPT",
|
||||
description: `Custom system prompt.
|
||||
Example:
|
||||
SYSTEM_PROMPT="You are a helpful assistant who helps users with their questions."`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getEngineEnvs = (): EnvVar[] => {
|
||||
return [
|
||||
{
|
||||
name: "TOP_K",
|
||||
description:
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
value: "3",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const createBackendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
openAiKey?: string;
|
||||
llamaCloudKey?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
model?: string;
|
||||
embeddingModel?: string;
|
||||
modelConfig: ModelConfig;
|
||||
framework?: TemplateFramework;
|
||||
dataSources?: TemplateDataSource[];
|
||||
port?: number;
|
||||
@@ -145,95 +246,20 @@ export const createBackendEnvFile = async (
|
||||
) => {
|
||||
// Init env values
|
||||
const envFileName = ".env";
|
||||
const defaultEnvs = [
|
||||
{
|
||||
render: true,
|
||||
name: "MODEL",
|
||||
description: "The name of LLM model to use.",
|
||||
value: opts.model,
|
||||
},
|
||||
{
|
||||
render: true,
|
||||
name: "OPENAI_API_KEY",
|
||||
description: "The OpenAI API key to use.",
|
||||
value: opts.openAiKey,
|
||||
},
|
||||
const envVars: EnvVar[] = [
|
||||
{
|
||||
name: "LLAMA_CLOUD_API_KEY",
|
||||
description: `The Llama Cloud API key.`,
|
||||
value: opts.llamaCloudKey,
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_MODEL",
|
||||
description: "Name of the embedding model to use.",
|
||||
value: opts.embeddingModel,
|
||||
},
|
||||
{
|
||||
name: "EMBEDDING_DIM",
|
||||
description: "Dimension of the embedding model to use.",
|
||||
value: 1536,
|
||||
},
|
||||
// Add model environment variables
|
||||
...getModelEnvs(opts.modelConfig),
|
||||
// Add engine environment variables
|
||||
...getEngineEnvs(),
|
||||
// Add vector database environment variables
|
||||
...(opts.vectorDb ? getVectorDBEnvs(opts.vectorDb) : []),
|
||||
...getVectorDBEnvs(opts.vectorDb),
|
||||
...getFrameworkEnvs(opts.framework, opts.port),
|
||||
];
|
||||
let envVars: EnvVar[] = [];
|
||||
if (opts.framework === "fastapi") {
|
||||
envVars = [
|
||||
...defaultEnvs,
|
||||
...[
|
||||
{
|
||||
name: "APP_HOST",
|
||||
description: "The address to start the backend app.",
|
||||
value: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
name: "APP_PORT",
|
||||
description: "The port to start the backend app.",
|
||||
value: opts.port?.toString() || "8000",
|
||||
},
|
||||
{
|
||||
name: "LLM_TEMPERATURE",
|
||||
description: "Temperature for sampling from the model.",
|
||||
},
|
||||
{
|
||||
name: "LLM_MAX_TOKENS",
|
||||
description: "Maximum number of tokens to generate.",
|
||||
},
|
||||
{
|
||||
name: "TOP_K",
|
||||
description:
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
value: "3",
|
||||
},
|
||||
{
|
||||
name: "SYSTEM_PROMPT",
|
||||
description: `Custom system prompt.
|
||||
Example:
|
||||
SYSTEM_PROMPT="
|
||||
We have provided context information below.
|
||||
---------------------
|
||||
{context_str}
|
||||
---------------------
|
||||
Given this information, please answer the question: {query_str}
|
||||
"`,
|
||||
},
|
||||
],
|
||||
];
|
||||
} else {
|
||||
envVars = [
|
||||
...defaultEnvs,
|
||||
...[
|
||||
opts.framework === "nextjs"
|
||||
? {
|
||||
name: "NEXT_PUBLIC_MODEL",
|
||||
description:
|
||||
"The LLM model to use (hardcode to front-end artifact).",
|
||||
value: opts.model,
|
||||
}
|
||||
: {},
|
||||
],
|
||||
];
|
||||
}
|
||||
// Render and write env file
|
||||
const content = renderEnvVar(envVars);
|
||||
await fs.writeFile(path.join(root, envFileName), content);
|
||||
@@ -244,20 +270,9 @@ export const createFrontendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
customApiPath?: string;
|
||||
model?: string;
|
||||
},
|
||||
) => {
|
||||
const defaultFrontendEnvs = [
|
||||
{
|
||||
name: "MODEL",
|
||||
description: "The OpenAI model to use.",
|
||||
value: opts.model,
|
||||
},
|
||||
{
|
||||
name: "NEXT_PUBLIC_MODEL",
|
||||
description: "The OpenAI model to use (hardcode to front-end artifact).",
|
||||
value: opts.model,
|
||||
},
|
||||
{
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description: "The backend API for chat endpoint.",
|
||||
|
||||
+7
-9
@@ -15,6 +15,7 @@ import { ConfigFileType, writeToolsConfig } from "./tools";
|
||||
import {
|
||||
FileSourceConfig,
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateVectorDB,
|
||||
@@ -24,8 +25,8 @@ import { installTSTemplate } from "./typescript";
|
||||
// eslint-disable-next-line max-params
|
||||
async function generateContextData(
|
||||
framework: TemplateFramework,
|
||||
modelConfig: ModelConfig,
|
||||
packageManager?: PackageManager,
|
||||
openAiKey?: string,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
llamaCloudKey?: string,
|
||||
useLlamaParse?: boolean,
|
||||
@@ -36,12 +37,12 @@ async function generateContextData(
|
||||
? "poetry run generate"
|
||||
: `${packageManager} run generate`,
|
||||
)}`;
|
||||
const openAiKeyConfigured = openAiKey || process.env["OPENAI_API_KEY"];
|
||||
const modelConfigured = modelConfig.isConfigured();
|
||||
const llamaCloudKeyConfigured = useLlamaParse
|
||||
? llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
: true;
|
||||
const hasVectorDb = vectorDb && vectorDb !== "none";
|
||||
if (openAiKeyConfigured && llamaCloudKeyConfigured && !hasVectorDb) {
|
||||
if (modelConfigured && llamaCloudKeyConfigured && !hasVectorDb) {
|
||||
// If all the required environment variables are set, run the generate script
|
||||
if (framework === "fastapi") {
|
||||
if (isHavingPoetryLockFile()) {
|
||||
@@ -63,7 +64,7 @@ async function generateContextData(
|
||||
|
||||
// generate the message of what to do to run the generate script manually
|
||||
const settings = [];
|
||||
if (!openAiKeyConfigured) settings.push("your OpenAI key");
|
||||
if (!modelConfigured) settings.push("your model provider API key");
|
||||
if (!llamaCloudKeyConfigured) settings.push("your Llama Cloud key");
|
||||
if (hasVectorDb) settings.push("your Vector DB environment variables");
|
||||
const settingsMessage =
|
||||
@@ -141,11 +142,9 @@ export const installTemplate = async (
|
||||
|
||||
// Copy the environment file to the target directory.
|
||||
await createBackendEnvFile(props.root, {
|
||||
openAiKey: props.openAiKey,
|
||||
modelConfig: props.modelConfig,
|
||||
llamaCloudKey: props.llamaCloudKey,
|
||||
vectorDb: props.vectorDb,
|
||||
model: props.model,
|
||||
embeddingModel: props.embeddingModel,
|
||||
framework: props.framework,
|
||||
dataSources: props.dataSources,
|
||||
port: props.externalPort,
|
||||
@@ -163,8 +162,8 @@ export const installTemplate = async (
|
||||
) {
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.modelConfig,
|
||||
props.packageManager,
|
||||
props.openAiKey,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.useLlamaParse,
|
||||
@@ -174,7 +173,6 @@ export const installTemplate = async (
|
||||
} else {
|
||||
// this is a frontend for a full-stack app, create .env file with model information
|
||||
await createFrontendEnvFile(props.root, {
|
||||
model: props.model,
|
||||
customApiPath: props.customApiPath,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
|
||||
const MODELS = [
|
||||
"claude-3-opus",
|
||||
"claude-3-sonnet",
|
||||
"claude-3-haiku",
|
||||
"claude-2.1",
|
||||
"claude-instant-1.2",
|
||||
];
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
|
||||
// TODO: get embedding vector dimensions from the anthropic sdk (currently not supported)
|
||||
// Use huggingface embedding models for now
|
||||
enum HuggingFaceEmbeddingModelType {
|
||||
XENOVA_ALL_MINILM_L6_V2 = "all-MiniLM-L6-v2",
|
||||
XENOVA_ALL_MPNET_BASE_V2 = "all-mpnet-base-v2",
|
||||
}
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
|
||||
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MINILM_L6_V2]: {
|
||||
dimensions: 384,
|
||||
},
|
||||
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MPNET_BASE_V2]: {
|
||||
dimensions: 768,
|
||||
},
|
||||
};
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type AnthropicQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askAnthropicQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: AnthropicQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["ANTHROPIC_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message:
|
||||
"Please provide your Anthropic API key (or leave blank to use ANTHROPIC_API_KEY env variable):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions =
|
||||
EMBEDDING_MODELS[
|
||||
embeddingModel as HuggingFaceEmbeddingModelType
|
||||
].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
|
||||
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
"embedding-001": { dimensions: 768 },
|
||||
"text-embedding-004": { dimensions: 768 },
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type GeminiQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askGeminiQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: GeminiQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["GOOGLE_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message:
|
||||
"Please provide your Google API key (or leave blank to use GOOGLE_API_KEY env variable):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.GOOGLE_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { ModelConfig, ModelProvider } from "../types";
|
||||
import { askAnthropicQuestions } from "./anthropic";
|
||||
import { askGeminiQuestions } from "./gemini";
|
||||
import { askOllamaQuestions } from "./ollama";
|
||||
import { askOpenAIQuestions } from "./openai";
|
||||
|
||||
const DEFAULT_MODEL_PROVIDER = "openai";
|
||||
|
||||
export type ModelConfigQuestionsParams = {
|
||||
openAiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export type ModelConfigParams = Omit<ModelConfig, "provider">;
|
||||
|
||||
export async function askModelConfig({
|
||||
askModels,
|
||||
openAiKey,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
|
||||
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
|
||||
if (askModels && !ciInfo.isCI) {
|
||||
const { provider } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "provider",
|
||||
message: "Which model provider would you like to use",
|
||||
choices: [
|
||||
{
|
||||
title: "OpenAI",
|
||||
value: "openai",
|
||||
},
|
||||
{ title: "Ollama", value: "ollama" },
|
||||
{ title: "Anthropic", value: "anthropic" },
|
||||
{ title: "Gemini", value: "gemini" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
modelProvider = provider;
|
||||
}
|
||||
|
||||
let modelConfig: ModelConfigParams;
|
||||
switch (modelProvider) {
|
||||
case "ollama":
|
||||
modelConfig = await askOllamaQuestions({ askModels });
|
||||
break;
|
||||
case "anthropic":
|
||||
modelConfig = await askAnthropicQuestions({ askModels });
|
||||
break;
|
||||
case "gemini":
|
||||
modelConfig = await askGeminiQuestions({ askModels });
|
||||
break;
|
||||
default:
|
||||
modelConfig = await askOpenAIQuestions({
|
||||
openAiKey,
|
||||
askModels,
|
||||
});
|
||||
}
|
||||
return {
|
||||
...modelConfig,
|
||||
provider: modelProvider,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import ciInfo from "ci-info";
|
||||
import ollama, { type ModelResponse } from "ollama";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
const MODELS = ["llama3:8b", "wizardlm2:7b", "gemma:7b", "phi3"];
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
// TODO: get embedding vector dimensions from the ollama sdk (currently not supported)
|
||||
const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
"nomic-embed-text": { dimensions: 768 },
|
||||
"mxbai-embed-large": { dimensions: 1024 },
|
||||
"all-minilm": { dimensions: 384 },
|
||||
};
|
||||
const DEFAULT_EMBEDDING_MODEL: string = Object.keys(EMBEDDING_MODELS)[0];
|
||||
|
||||
type OllamaQuestionsParams = {
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askOllamaQuestions({
|
||||
askModels,
|
||||
}: OllamaQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: EMBEDDING_MODELS[DEFAULT_EMBEDDING_MODEL].dimensions,
|
||||
isConfigured(): boolean {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
await ensureModel(model);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
await ensureModel(embeddingModel);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function ensureModel(modelName: string) {
|
||||
try {
|
||||
if (modelName.split(":").length === 1) {
|
||||
// model doesn't have a version suffix, use latest
|
||||
modelName = modelName + ":latest";
|
||||
}
|
||||
const { models } = await ollama.list();
|
||||
const found =
|
||||
models.find((model: ModelResponse) => model.name === modelName) !==
|
||||
undefined;
|
||||
if (!found) {
|
||||
console.log(
|
||||
red(
|
||||
`Model ${modelName} was not pulled yet. Call 'ollama pull ${modelName}' and try again.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
red("Listing Ollama models failed. Is 'ollama' running? " + error),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import ciInfo from "ci-info";
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { questionHandlers } from "../../questions";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
|
||||
const DEFAULT_MODEL = "gpt-3.5-turbo";
|
||||
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
|
||||
|
||||
export async function askOpenAIQuestions({
|
||||
openAiKey,
|
||||
askModels,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: openAiKey,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
isConfigured(): boolean {
|
||||
if (config.apiKey) {
|
||||
return true;
|
||||
}
|
||||
if (process.env["OPENAI_API_KEY"]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message: askModels
|
||||
? "Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):"
|
||||
: "Please provide your OpenAI API key (leave blank to skip):",
|
||||
validate: (value: string) => {
|
||||
if (askModels && !value) {
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
return "OPENAI_API_KEY env variable is not set - key is required";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.apiKey = key || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: await getAvailableModelChoices(false, config.apiKey),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: await getAvailableModelChoices(true, config.apiKey),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.embeddingModel = embeddingModel;
|
||||
config.dimensions = getDimensions(embeddingModel);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function getAvailableModelChoices(
|
||||
selectEmbedding: boolean,
|
||||
apiKey?: string,
|
||||
) {
|
||||
if (!apiKey) {
|
||||
throw new Error("need OpenAI key to retrieve model choices");
|
||||
}
|
||||
const isLLMModel = (modelId: string) => {
|
||||
return modelId.startsWith("gpt");
|
||||
};
|
||||
|
||||
const isEmbeddingModel = (modelId: string) => {
|
||||
return modelId.includes("embedding");
|
||||
};
|
||||
|
||||
const spinner = ora("Fetching available models").start();
|
||||
try {
|
||||
const response = await got(`${OPENAI_API_URL}/models`, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + apiKey,
|
||||
},
|
||||
timeout: 5000,
|
||||
responseType: "json",
|
||||
});
|
||||
const data: any = await response.body;
|
||||
spinner.stop();
|
||||
return data.data
|
||||
.filter((model: any) =>
|
||||
selectEmbedding ? isEmbeddingModel(model.id) : isLLMModel(model.id),
|
||||
)
|
||||
.map((el: any) => {
|
||||
return {
|
||||
title: el.id,
|
||||
value: el.id,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
if ((error as any).response?.statusCode === 401) {
|
||||
console.log(
|
||||
red(
|
||||
"Invalid OpenAI API key provided! Please provide a valid key and try again!",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(red("Request failed: " + error));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getDimensions(modelName: string) {
|
||||
// at 2024-04-24 all OpenAI embedding models support 1536 dimensions except
|
||||
// "text-embedding-3-large", see https://openai.com/blog/new-embedding-models-and-api-updates
|
||||
return modelName === "text-embedding-3-large" ? 1024 : 1536;
|
||||
}
|
||||
+87
-35
@@ -10,6 +10,7 @@ import { isPoetryAvailable, tryPoetryInstall } from "./poetry";
|
||||
import { Tool } from "./tools";
|
||||
import {
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
@@ -21,8 +22,9 @@ interface Dependency {
|
||||
}
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
modelConfig: ModelConfig,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
dataSource?: TemplateDataSource,
|
||||
dataSources?: TemplateDataSource[],
|
||||
tools?: Tool[],
|
||||
) => {
|
||||
const dependencies: Dependency[] = [];
|
||||
@@ -41,6 +43,7 @@ const getAdditionalDependencies = (
|
||||
name: "llama-index-vector-stores-postgres",
|
||||
version: "^0.1.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pinecone": {
|
||||
dependencies.push({
|
||||
@@ -70,44 +73,88 @@ const getAdditionalDependencies = (
|
||||
}
|
||||
|
||||
// Add data source dependencies
|
||||
const dataSourceType = dataSource?.type;
|
||||
switch (dataSourceType) {
|
||||
case "file":
|
||||
dependencies.push({
|
||||
name: "docx2txt",
|
||||
version: "^0.8",
|
||||
});
|
||||
break;
|
||||
case "web":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-web",
|
||||
version: "^0.1.6",
|
||||
});
|
||||
break;
|
||||
case "db":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-database",
|
||||
version: "^0.1.3",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymysql",
|
||||
version: "^1.1.0",
|
||||
extras: ["rsa"],
|
||||
});
|
||||
dependencies.push({
|
||||
name: "psycopg2",
|
||||
version: "^2.9.9",
|
||||
});
|
||||
break;
|
||||
if (dataSources) {
|
||||
for (const ds of dataSources) {
|
||||
const dsType = ds?.type;
|
||||
switch (dsType) {
|
||||
case "file":
|
||||
dependencies.push({
|
||||
name: "docx2txt",
|
||||
version: "^0.8",
|
||||
});
|
||||
break;
|
||||
case "web":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-web",
|
||||
version: "^0.1.6",
|
||||
});
|
||||
break;
|
||||
case "db":
|
||||
dependencies.push({
|
||||
name: "llama-index-readers-database",
|
||||
version: "^0.1.3",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymysql",
|
||||
version: "^1.1.0",
|
||||
extras: ["rsa"],
|
||||
});
|
||||
dependencies.push({
|
||||
name: "psycopg2",
|
||||
version: "^2.9.9",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tools dependencies
|
||||
console.log("Adding tools dependencies");
|
||||
tools?.forEach((tool) => {
|
||||
tool.dependencies?.forEach((dep) => {
|
||||
dependencies.push(dep);
|
||||
});
|
||||
});
|
||||
|
||||
switch (modelConfig.provider) {
|
||||
case "ollama":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-ollama",
|
||||
version: "0.1.2",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-ollama",
|
||||
version: "0.1.2",
|
||||
});
|
||||
break;
|
||||
case "openai":
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: "0.2.2",
|
||||
});
|
||||
break;
|
||||
case "anthropic":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-anthropic",
|
||||
version: "0.1.10",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-huggingface",
|
||||
version: "0.2.0",
|
||||
});
|
||||
break;
|
||||
case "gemini":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-gemini",
|
||||
version: "0.1.7",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-gemini",
|
||||
version: "0.1.6",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
};
|
||||
|
||||
@@ -205,8 +252,8 @@ export const installPythonTemplate = async ({
|
||||
dataSources,
|
||||
tools,
|
||||
postInstallAction,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
modelConfig,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "root"
|
||||
@@ -215,9 +262,9 @@ export const installPythonTemplate = async ({
|
||||
| "vectorDb"
|
||||
| "dataSources"
|
||||
| "tools"
|
||||
| "useLlamaParse"
|
||||
| "postInstallAction"
|
||||
| "observability"
|
||||
| "modelConfig"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
@@ -257,9 +304,14 @@ export const installPythonTemplate = async ({
|
||||
cwd: path.join(compPath, "engines", "python", engine),
|
||||
});
|
||||
|
||||
const addOnDependencies = dataSources
|
||||
.map((ds) => getAdditionalDependencies(vectorDb, ds, tools))
|
||||
.flat();
|
||||
console.log("Adding additional dependencies");
|
||||
|
||||
const addOnDependencies = getAdditionalDependencies(
|
||||
modelConfig,
|
||||
vectorDb,
|
||||
dataSources,
|
||||
tools,
|
||||
);
|
||||
|
||||
if (observability === "opentelemetry") {
|
||||
addOnDependencies.push({
|
||||
|
||||
+27
-2
@@ -5,12 +5,18 @@ import yaml from "yaml";
|
||||
import { makeDir } from "./make-dir";
|
||||
import { TemplateFramework } from "./types";
|
||||
|
||||
export enum ToolType {
|
||||
LLAMAHUB = "llamahub",
|
||||
LOCAL = "local",
|
||||
}
|
||||
|
||||
export type Tool = {
|
||||
display: string;
|
||||
name: string;
|
||||
config?: Record<string, any>;
|
||||
dependencies?: ToolDependencies[];
|
||||
supportedFrameworks?: Array<TemplateFramework>;
|
||||
type: ToolType;
|
||||
};
|
||||
|
||||
export type ToolDependencies = {
|
||||
@@ -35,6 +41,7 @@ export const supportedTools: Tool[] = [
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi"],
|
||||
type: ToolType.LLAMAHUB,
|
||||
},
|
||||
{
|
||||
display: "Wikipedia",
|
||||
@@ -46,6 +53,14 @@ export const supportedTools: Tool[] = [
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LLAMAHUB,
|
||||
},
|
||||
{
|
||||
display: "Weather",
|
||||
name: "weather",
|
||||
dependencies: [],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -90,9 +105,19 @@ export const writeToolsConfig = async (
|
||||
type: ConfigFileType = ConfigFileType.YAML,
|
||||
) => {
|
||||
if (tools.length === 0) return; // no tools selected, no config need
|
||||
const configContent: Record<string, any> = {};
|
||||
const configContent: {
|
||||
[key in ToolType]: Record<string, any>;
|
||||
} = {
|
||||
local: {},
|
||||
llamahub: {},
|
||||
};
|
||||
tools.forEach((tool) => {
|
||||
configContent[tool.name] = tool.config ?? {};
|
||||
if (tool.type === ToolType.LLAMAHUB) {
|
||||
configContent.llamahub[tool.name] = tool.config ?? {};
|
||||
}
|
||||
if (tool.type === ToolType.LOCAL) {
|
||||
configContent.local[tool.name] = tool.config ?? {};
|
||||
}
|
||||
});
|
||||
const configPath = path.join(root, "config");
|
||||
await makeDir(configPath);
|
||||
|
||||
+10
-3
@@ -1,6 +1,15 @@
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
import { Tool } from "./tools";
|
||||
|
||||
export type ModelProvider = "openai" | "ollama" | "anthropic" | "gemini";
|
||||
export type ModelConfig = {
|
||||
provider: ModelProvider;
|
||||
apiKey?: string;
|
||||
model: string;
|
||||
embeddingModel: string;
|
||||
dimensions: number;
|
||||
isConfigured(): boolean;
|
||||
};
|
||||
export type TemplateType = "streaming" | "community" | "llamapack";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateUI = "html" | "shadcn";
|
||||
@@ -59,11 +68,9 @@ export interface InstallTemplateArgs {
|
||||
ui: TemplateUI;
|
||||
dataSources: TemplateDataSource[];
|
||||
customApiPath?: string;
|
||||
openAiKey?: string;
|
||||
modelConfig: ModelConfig;
|
||||
llamaCloudKey?: string;
|
||||
useLlamaParse?: boolean;
|
||||
model: string;
|
||||
embeddingModel: string;
|
||||
communityProjectConfig?: CommunityProjectConfig;
|
||||
llamapack?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
|
||||
@@ -105,7 +105,7 @@ export const installTSTemplate = async ({
|
||||
const enginePath = path.join(root, relativeEngineDestPath, "engine");
|
||||
|
||||
// copy vector db component
|
||||
console.log("\nUsing vector DB:", vectorDb, "\n");
|
||||
console.log("\nUsing vector DB:", vectorDb ?? "none", "\n");
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
|
||||
@@ -205,7 +205,7 @@ async function updatePackageJson({
|
||||
// add generate script if using context engine
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
generate: `ts-node ${path.join(
|
||||
generate: `tsx ${path.join(
|
||||
relativeEngineDestPath,
|
||||
"engine",
|
||||
"generate.ts",
|
||||
|
||||
@@ -107,20 +107,6 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Whether to generate a frontend for your backend.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--model <model>",
|
||||
`
|
||||
|
||||
Select OpenAI model to use. E.g. gpt-3.5-turbo.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--embedding-model <embeddingModel>",
|
||||
`
|
||||
|
||||
Select OpenAI embedding model to use. E.g. text-embedding-ada-002.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -201,9 +187,7 @@ if (process.argv.includes("--tools")) {
|
||||
if (process.argv.includes("--no-llama-parse")) {
|
||||
program.useLlamaParse = false;
|
||||
}
|
||||
if (process.argv.includes("--ask-models")) {
|
||||
program.askModels = true;
|
||||
}
|
||||
program.askModels = process.argv.includes("--ask-models");
|
||||
if (process.argv.includes("--no-files")) {
|
||||
program.dataSources = [];
|
||||
} else {
|
||||
@@ -290,7 +274,11 @@ async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
const preferences = (conf.get("preferences") || {}) as QuestionArgs;
|
||||
await askQuestions(program as unknown as QuestionArgs, preferences);
|
||||
await askQuestions(
|
||||
program as unknown as QuestionArgs,
|
||||
preferences,
|
||||
program.openAiKey,
|
||||
);
|
||||
|
||||
await createApp({
|
||||
template: program.template,
|
||||
@@ -299,10 +287,8 @@ async function run(): Promise<void> {
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
frontend: program.frontend,
|
||||
openAiKey: program.openAiKey,
|
||||
modelConfig: program.modelConfig,
|
||||
llamaCloudKey: program.llamaCloudKey,
|
||||
model: program.model,
|
||||
embeddingModel: program.embeddingModel,
|
||||
communityProjectConfig: program.communityProjectConfig,
|
||||
llamapack: program.llamapack,
|
||||
vectorDb: program.vectorDb,
|
||||
|
||||
+26
-23
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.0.32",
|
||||
"version": "0.1.3",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
"llamaindex",
|
||||
"next.js"
|
||||
],
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS",
|
||||
@@ -20,32 +20,30 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"dev": "ncc build ./index.ts -w -o dist/",
|
||||
"build": "bash ./scripts/build.sh",
|
||||
"build:ncc": "pnpm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
|
||||
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
|
||||
"dev": "ncc build ./index.ts -w -o dist/",
|
||||
"e2e": "playwright test",
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
|
||||
"new-snapshot": "pnpm run build && changeset version --snapshot",
|
||||
"new-version": "pnpm run build && changeset version",
|
||||
"pack-install": "bash ./scripts/pack.sh",
|
||||
"prepare": "husky",
|
||||
"release": "pnpm run build && changeset publish",
|
||||
"new-version": "pnpm run build && changeset version",
|
||||
"release-snapshot": "pnpm run build && changeset publish --tag snapshot",
|
||||
"new-snapshot": "pnpm run build && changeset version --snapshot",
|
||||
"pack-install": "bash ./scripts/pack.sh"
|
||||
"release-snapshot": "pnpm run build && changeset publish --tag snapshot"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.1",
|
||||
"dependencies": {
|
||||
"@types/async-retry": "1.4.2",
|
||||
"@types/ci-info": "2.0.0",
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "^20.11.7",
|
||||
"@types/prompts": "2.0.1",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/validate-npm-package-name": "3.0.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"async-retry": "1.3.1",
|
||||
"async-sema": "3.0.1",
|
||||
"ci-info": "github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
|
||||
@@ -53,29 +51,34 @@
|
||||
"conf": "10.2.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"fast-glob": "3.3.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"got": "10.7.0",
|
||||
"ollama": "^0.5.0",
|
||||
"ora": "^8.0.1",
|
||||
"picocolors": "1.0.0",
|
||||
"prompts": "2.1.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"smol-toml": "^1.1.4",
|
||||
"tar": "6.1.15",
|
||||
"terminal-link": "^3.0.0",
|
||||
"update-check": "1.5.4",
|
||||
"validate-npm-package-name": "3.0.0",
|
||||
"wait-port": "^1.1.0",
|
||||
"yaml": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"husky": "^9.0.10",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"ora": "^8.0.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"yaml": "2.4.1"
|
||||
"wait-port": "^1.1.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.5",
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2412
-1915
File diff suppressed because it is too large
Load Diff
+34
-140
@@ -1,8 +1,6 @@
|
||||
import { execSync } from "child_process";
|
||||
import ciInfo from "ci-info";
|
||||
import fs from "fs";
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import path from "path";
|
||||
import { blue, green, red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
@@ -16,11 +14,10 @@ import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
|
||||
import { EXAMPLE_FILE } from "./helpers/datasources";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { getAvailableLlamapackOptions } from "./helpers/llama-pack";
|
||||
import { askModelConfig } from "./helpers/providers";
|
||||
import { getProjectOptions } from "./helpers/repo";
|
||||
import { supportedTools, toolsRequireConfig } from "./helpers/tools";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
|
||||
export type QuestionArgs = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager"
|
||||
@@ -67,16 +64,13 @@ if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
|
||||
}
|
||||
`;
|
||||
|
||||
const defaults: QuestionArgs = {
|
||||
const defaults: Omit<QuestionArgs, "modelConfig"> = {
|
||||
template: "streaming",
|
||||
framework: "nextjs",
|
||||
ui: "shadcn",
|
||||
frontend: false,
|
||||
openAiKey: "",
|
||||
llamaCloudKey: "",
|
||||
useLlamaParse: false,
|
||||
model: "gpt-4-turbo",
|
||||
embeddingModel: "text-embedding-3-large",
|
||||
communityProjectConfig: undefined,
|
||||
llamapack: "",
|
||||
postInstallAction: "dependencies",
|
||||
@@ -84,7 +78,7 @@ const defaults: QuestionArgs = {
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const handlers = {
|
||||
export const questionHandlers = {
|
||||
onCancel: () => {
|
||||
console.error("Exiting.");
|
||||
process.exit(1);
|
||||
@@ -232,63 +226,15 @@ export const onPromptState = (state: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableModelChoices = async (
|
||||
selectEmbedding: boolean,
|
||||
apiKey?: string,
|
||||
) => {
|
||||
const isLLMModel = (modelId: string) => {
|
||||
return modelId.startsWith("gpt");
|
||||
};
|
||||
|
||||
const isEmbeddingModel = (modelId: string) => {
|
||||
return modelId.includes("embedding");
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
const spinner = ora("Fetching available models").start();
|
||||
try {
|
||||
const response = await got(`${OPENAI_API_URL}/models`, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + apiKey,
|
||||
},
|
||||
timeout: 5000,
|
||||
responseType: "json",
|
||||
});
|
||||
const data: any = await response.body;
|
||||
spinner.stop();
|
||||
return data.data
|
||||
.filter((model: any) =>
|
||||
selectEmbedding ? isEmbeddingModel(model.id) : isLLMModel(model.id),
|
||||
)
|
||||
.map((el: any) => {
|
||||
return {
|
||||
title: el.id,
|
||||
value: el.id,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
if ((error as any).response?.statusCode === 401) {
|
||||
console.log(
|
||||
red(
|
||||
"Invalid OpenAI API key provided! Please provide a valid key and try again!",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(red("Request failed: " + error));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const askQuestions = async (
|
||||
program: QuestionArgs,
|
||||
preferences: QuestionArgs,
|
||||
openAiKey?: string,
|
||||
) => {
|
||||
const getPrefOrDefault = <K extends keyof QuestionArgs>(
|
||||
const getPrefOrDefault = <K extends keyof Omit<QuestionArgs, "modelConfig">>(
|
||||
field: K,
|
||||
): QuestionArgs[K] => preferences[field] ?? defaults[field];
|
||||
): Omit<QuestionArgs, "modelConfig">[K] =>
|
||||
preferences[field] ?? defaults[field];
|
||||
|
||||
// Ask for next action after installation
|
||||
async function askPostInstallAction() {
|
||||
@@ -311,8 +257,8 @@ export const askQuestions = async (
|
||||
},
|
||||
];
|
||||
|
||||
const openAiKeyConfigured =
|
||||
program.openAiKey || process.env["OPENAI_API_KEY"];
|
||||
const modelConfigured =
|
||||
!program.llamapack && program.modelConfig.isConfigured();
|
||||
// If using LlamaParse, require LlamaCloud API key
|
||||
const llamaCloudKeyConfigured = program.useLlamaParse
|
||||
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
@@ -321,10 +267,9 @@ export const askQuestions = async (
|
||||
// Can run the app if all tools do not require configuration
|
||||
if (
|
||||
!hasVectorDb &&
|
||||
openAiKeyConfigured &&
|
||||
modelConfigured &&
|
||||
llamaCloudKeyConfigured &&
|
||||
!toolsRequireConfig(program.tools) &&
|
||||
!program.llamapack
|
||||
!toolsRequireConfig(program.tools)
|
||||
) {
|
||||
actionChoices.push({
|
||||
title:
|
||||
@@ -341,7 +286,7 @@ export const askQuestions = async (
|
||||
choices: actionChoices,
|
||||
initial: 1,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.postInstallAction = action;
|
||||
@@ -374,7 +319,7 @@ export const askQuestions = async (
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
program.template = template;
|
||||
preferences.template = template;
|
||||
@@ -397,7 +342,7 @@ export const askQuestions = async (
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
const projectConfig = JSON.parse(communityProjectConfig);
|
||||
program.communityProjectConfig = projectConfig;
|
||||
@@ -418,7 +363,7 @@ export const askQuestions = async (
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamapack = llamapack;
|
||||
preferences.llamapack = llamapack;
|
||||
@@ -444,7 +389,7 @@ export const askQuestions = async (
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
program.framework = framework;
|
||||
preferences.framework = framework;
|
||||
@@ -453,7 +398,6 @@ export const askQuestions = async (
|
||||
|
||||
if (program.framework === "express" || program.framework === "fastapi") {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
// (only for streaming backends)
|
||||
if (program.frontend === undefined) {
|
||||
if (ciInfo.isCI) {
|
||||
program.frontend = getPrefOrDefault("frontend");
|
||||
@@ -504,7 +448,7 @@ export const askQuestions = async (
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.observability = observability;
|
||||
@@ -512,67 +456,13 @@ export const askQuestions = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.openAiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message: program.askModels
|
||||
? "Please provide your OpenAI API key (or leave blank to reuse OPENAI_API_KEY env variable):"
|
||||
: "Please provide your OpenAI API key (leave blank to skip):",
|
||||
validate: (value: string) => {
|
||||
if (program.askModels && !value) {
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
return "OpenAI API key is required";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
|
||||
program.openAiKey = key || process.env.OPENAI_API_KEY;
|
||||
preferences.openAiKey = key || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
|
||||
if (!program.model) {
|
||||
if (ciInfo.isCI || !program.askModels) {
|
||||
program.model = defaults.model;
|
||||
} else {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: await getAvailableModelChoices(false, program.openAiKey),
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.model = model;
|
||||
preferences.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.embeddingModel) {
|
||||
if (ciInfo.isCI || !program.askModels) {
|
||||
program.embeddingModel = defaults.embeddingModel;
|
||||
} else {
|
||||
const { embeddingModel } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "embeddingModel",
|
||||
message: "Which embedding model would you like to use?",
|
||||
choices: await getAvailableModelChoices(true, program.openAiKey),
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
);
|
||||
program.embeddingModel = embeddingModel;
|
||||
preferences.embeddingModel = embeddingModel;
|
||||
}
|
||||
if (!program.modelConfig) {
|
||||
const modelConfig = await askModelConfig({
|
||||
openAiKey,
|
||||
askModels: program.askModels ?? false,
|
||||
});
|
||||
program.modelConfig = modelConfig;
|
||||
preferences.modelConfig = modelConfig;
|
||||
}
|
||||
|
||||
if (!program.dataSources) {
|
||||
@@ -596,7 +486,7 @@ export const askQuestions = async (
|
||||
),
|
||||
initial: firstQuestion ? 1 : 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
if (selectedSource === "no" || selectedSource === "none") {
|
||||
@@ -642,7 +532,7 @@ export const askQuestions = async (
|
||||
return true;
|
||||
},
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.dataSources.push({
|
||||
@@ -687,7 +577,7 @@ export const askQuestions = async (
|
||||
];
|
||||
program.dataSources.push({
|
||||
type: "db",
|
||||
config: await prompts(dbPrompts, handlers),
|
||||
config: await prompts(dbPrompts, questionHandlers),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -714,7 +604,7 @@ export const askQuestions = async (
|
||||
active: "yes",
|
||||
inactive: "no",
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
program.useLlamaParse = useLlamaParse;
|
||||
|
||||
@@ -727,7 +617,7 @@ export const askQuestions = async (
|
||||
message:
|
||||
"Please provide your LlamaIndex Cloud API key (leave blank to skip):",
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamaCloudKey = llamaCloudKey;
|
||||
}
|
||||
@@ -746,7 +636,7 @@ export const askQuestions = async (
|
||||
choices: getVectorDbChoices(program.framework),
|
||||
initial: 0,
|
||||
},
|
||||
handlers,
|
||||
questionHandlers,
|
||||
);
|
||||
program.vectorDb = vectorDb;
|
||||
preferences.vectorDb = vectorDb;
|
||||
@@ -781,3 +671,7 @@ export const askQuestions = async (
|
||||
|
||||
await askPostInstallAction();
|
||||
};
|
||||
|
||||
export const toChoice = (value: string) => {
|
||||
return { title: value, value };
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import os
|
||||
import yaml
|
||||
import importlib
|
||||
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
|
||||
class ToolFactory:
|
||||
|
||||
@staticmethod
|
||||
def create_tool(tool_name: str, **kwargs) -> list[FunctionTool]:
|
||||
try:
|
||||
tool_package, tool_cls_name = tool_name.split(".")
|
||||
module_name = f"llama_index.tools.{tool_package}"
|
||||
module = importlib.import_module(module_name)
|
||||
tool_class = getattr(module, tool_cls_name)
|
||||
tool_spec: BaseToolSpec = tool_class(**kwargs)
|
||||
return tool_spec.to_tool_list()
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise ValueError(f"Unsupported tool: {tool_name}") from e
|
||||
except TypeError as e:
|
||||
raise ValueError(
|
||||
f"Could not create tool: {tool_name}. With config: {kwargs}"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> list[FunctionTool]:
|
||||
tools = []
|
||||
if os.path.exists("config/tools.yaml"):
|
||||
with open("config/tools.yaml", "r") as f:
|
||||
tool_configs = yaml.safe_load(f)
|
||||
for name, config in tool_configs.items():
|
||||
tools += ToolFactory.create_tool(name, **config)
|
||||
return tools
|
||||
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import yaml
|
||||
import importlib
|
||||
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
|
||||
class ToolType:
|
||||
LLAMAHUB = "llamahub"
|
||||
LOCAL = "local"
|
||||
|
||||
|
||||
class ToolFactory:
|
||||
|
||||
TOOL_SOURCE_PACKAGE_MAP = {
|
||||
ToolType.LLAMAHUB: "llama_index.tools",
|
||||
ToolType.LOCAL: "app.engine.tools",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def load_tools(tool_type: str, tool_name: str, config: dict) -> list[FunctionTool]:
|
||||
source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type]
|
||||
try:
|
||||
if "ToolSpec" in tool_name:
|
||||
tool_package, tool_cls_name = tool_name.split(".")
|
||||
module_name = f"{source_package}.{tool_package}"
|
||||
module = importlib.import_module(module_name)
|
||||
tool_class = getattr(module, tool_cls_name)
|
||||
tool_spec: BaseToolSpec = tool_class(**config)
|
||||
return tool_spec.to_tool_list()
|
||||
else:
|
||||
module = importlib.import_module(f"{source_package}.{tool_name}")
|
||||
tools = getattr(module, "tools")
|
||||
if not all(isinstance(tool, FunctionTool) for tool in tools):
|
||||
raise ValueError(
|
||||
f"The module {module} does not contain valid tools"
|
||||
)
|
||||
return tools
|
||||
except ImportError as e:
|
||||
raise ValueError(f"Failed to import tool {tool_name}: {e}")
|
||||
except AttributeError as e:
|
||||
raise ValueError(f"Failed to load tool {tool_name}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> list[FunctionTool]:
|
||||
tools = []
|
||||
if os.path.exists("config/tools.yaml"):
|
||||
with open("config/tools.yaml", "r") as f:
|
||||
tool_configs = yaml.safe_load(f)
|
||||
for tool_type, config_entries in tool_configs.items():
|
||||
for tool_name, config in config_entries.items():
|
||||
tools.extend(
|
||||
ToolFactory.load_tools(tool_type, tool_name, config)
|
||||
)
|
||||
return tools
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Open Meteo weather map tool spec."""
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import pytz
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenMeteoWeather:
|
||||
geo_api = "https://geocoding-api.open-meteo.com/v1"
|
||||
weather_api = "https://api.open-meteo.com/v1"
|
||||
|
||||
@classmethod
|
||||
def _get_geo_location(cls, location: str) -> dict:
|
||||
"""Get geo location from location name."""
|
||||
params = {"name": location, "count": 10, "language": "en", "format": "json"}
|
||||
response = requests.get(f"{cls.geo_api}/search", params=params)
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to fetch geo location: {response.status_code}")
|
||||
else:
|
||||
data = response.json()
|
||||
result = data["results"][0]
|
||||
geo_location = {
|
||||
"id": result["id"],
|
||||
"name": result["name"],
|
||||
"latitude": result["latitude"],
|
||||
"longitude": result["longitude"],
|
||||
}
|
||||
return geo_location
|
||||
|
||||
@classmethod
|
||||
def get_weather_information(cls, location: str) -> dict:
|
||||
"""Use this function to get the weather of any given location.
|
||||
Note that the weather code should follow WMO Weather interpretation codes (WW):
|
||||
0: Clear sky
|
||||
1, 2, 3: Mainly clear, partly cloudy, and overcast
|
||||
45, 48: Fog and depositing rime fog
|
||||
51, 53, 55: Drizzle: Light, moderate, and dense intensity
|
||||
56, 57: Freezing Drizzle: Light and dense intensity
|
||||
61, 63, 65: Rain: Slight, moderate and heavy intensity
|
||||
66, 67: Freezing Rain: Light and heavy intensity
|
||||
71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
|
||||
77: Snow grains
|
||||
80, 81, 82: Rain showers: Slight, moderate, and violent
|
||||
85, 86: Snow showers slight and heavy
|
||||
95: Thunderstorm: Slight or moderate
|
||||
96, 99: Thunderstorm with slight and heavy hail
|
||||
"""
|
||||
logger.info(
|
||||
f"Calling open-meteo api to get weather information of location: {location}"
|
||||
)
|
||||
geo_location = cls._get_geo_location(location)
|
||||
timezone = pytz.timezone("UTC").zone
|
||||
params = {
|
||||
"latitude": geo_location["latitude"],
|
||||
"longitude": geo_location["longitude"],
|
||||
"current": "temperature_2m,weather_code",
|
||||
"hourly": "temperature_2m,weather_code",
|
||||
"daily": "weather_code",
|
||||
"timezone": timezone,
|
||||
}
|
||||
response = requests.get(f"{cls.weather_api}/forecast", params=params)
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Failed to fetch weather information: {response.status_code}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
|
||||
tools = [FunctionTool.from_defaults(OpenMeteoWeather.get_weather_information)]
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
from app.engine.index import get_index
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
def get_chat_engine():
|
||||
@@ -8,8 +9,11 @@ def get_chat_engine():
|
||||
|
||||
index = get_index()
|
||||
if index is None:
|
||||
raise Exception(
|
||||
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(
|
||||
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
|
||||
),
|
||||
)
|
||||
|
||||
return index.as_chat_engine(
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import {
|
||||
BaseTool,
|
||||
OpenAIAgent,
|
||||
QueryEngineTool,
|
||||
Settings,
|
||||
ToolFactory,
|
||||
} from "llamaindex";
|
||||
import { BaseToolWithCall, OpenAIAgent, QueryEngineTool } from "llamaindex";
|
||||
import { ToolsFactory } from "llamaindex/tools/ToolsFactory";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getDataSource } from "./index";
|
||||
import { STORAGE_CACHE_DIR } from "./shared";
|
||||
import { createLocalTools } from "./tools";
|
||||
|
||||
export async function createChatEngine() {
|
||||
let tools: BaseTool[] = [];
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
|
||||
// Add a query engine tool if we have a data source
|
||||
// Delete this code if you don't have a data source
|
||||
@@ -33,12 +29,17 @@ export async function createChatEngine() {
|
||||
const config = JSON.parse(
|
||||
await fs.readFile(path.join("config", "tools.json"), "utf8"),
|
||||
);
|
||||
tools = tools.concat(await ToolFactory.createTools(config));
|
||||
|
||||
// add local tools from the 'tools' folder (if configured)
|
||||
const localTools = createLocalTools(config.local);
|
||||
tools.push(...localTools);
|
||||
|
||||
// add tools from LlamaIndexTS (if configured)
|
||||
const llamaTools = await ToolsFactory.createTools(config.llamahub);
|
||||
tools.push(...llamaTools);
|
||||
} catch {}
|
||||
|
||||
return new OpenAIAgent({
|
||||
tools,
|
||||
llm: Settings.llm,
|
||||
verbose: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BaseToolWithCall } from "llamaindex";
|
||||
import { WeatherTool, WeatherToolParams } from "./weather";
|
||||
|
||||
type ToolCreator = (config: unknown) => BaseToolWithCall;
|
||||
|
||||
const toolFactory: Record<string, ToolCreator> = {
|
||||
weather: (config: unknown) => {
|
||||
return new WeatherTool(config as WeatherToolParams);
|
||||
},
|
||||
};
|
||||
|
||||
export function createLocalTools(
|
||||
localConfig: Record<string, unknown>,
|
||||
): BaseToolWithCall[] {
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
|
||||
Object.keys(localConfig).forEach((key) => {
|
||||
if (key in toolFactory) {
|
||||
const toolConfig = localConfig[key];
|
||||
const tool = toolFactory[key](toolConfig);
|
||||
tools.push(tool);
|
||||
}
|
||||
});
|
||||
|
||||
return tools;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
|
||||
interface GeoLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export type WeatherParameter = {
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type WeatherToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<WeatherParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WeatherParameter>> = {
|
||||
name: "get_weather_information",
|
||||
description: `
|
||||
Use this function to get the weather of any given location.
|
||||
Note that the weather code should follow WMO Weather interpretation codes (WW):
|
||||
0: Clear sky
|
||||
1, 2, 3: Mainly clear, partly cloudy, and overcast
|
||||
45, 48: Fog and depositing rime fog
|
||||
51, 53, 55: Drizzle: Light, moderate, and dense intensity
|
||||
56, 57: Freezing Drizzle: Light and dense intensity
|
||||
61, 63, 65: Rain: Slight, moderate and heavy intensity
|
||||
66, 67: Freezing Rain: Light and heavy intensity
|
||||
71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
|
||||
77: Snow grains
|
||||
80, 81, 82: Rain showers: Slight, moderate, and violent
|
||||
85, 86: Snow showers slight and heavy
|
||||
95: Thunderstorm: Slight or moderate
|
||||
96, 99: Thunderstorm with slight and heavy hail
|
||||
`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "The location to get the weather information",
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
};
|
||||
|
||||
export class WeatherTool implements BaseTool<WeatherParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<WeatherParameter>>;
|
||||
|
||||
private getGeoLocation = async (location: string): Promise<GeoLocation> => {
|
||||
const apiUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${location}&count=10&language=en&format=json`;
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
const { id, name, latitude, longitude } = data.results[0];
|
||||
return { id, name, latitude, longitude };
|
||||
};
|
||||
|
||||
private getWeatherByLocation = async (location: string) => {
|
||||
console.log(
|
||||
"Calling open-meteo api to get weather information of location:",
|
||||
location,
|
||||
);
|
||||
const { latitude, longitude } = await this.getGeoLocation(location);
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,weather_code&hourly=temperature_2m,weather_code&daily=weather_code&timezone=${timezone}`;
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
constructor(params?: WeatherToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async call(input: WeatherParameter) {
|
||||
return await this.getWeatherByLocation(input.location);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ export async function createChatEngine() {
|
||||
);
|
||||
}
|
||||
const retriever = index.asRetriever();
|
||||
retriever.similarityTopK = 3;
|
||||
retriever.similarityTopK = process.env.TOP_K
|
||||
? parseInt(process.env.TOP_K)
|
||||
: 3;
|
||||
|
||||
return new ContextChatEngine({
|
||||
chatModel: Settings.llm,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
AstraDBVectorStore,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { AstraDBVectorStore } from "llamaindex/storage/vectorStore/AstraDBVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { AstraDBVectorStore, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { AstraDBVectorStore } from "llamaindex/storage/vectorStore/AstraDBVectorStore";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource() {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
MilvusVectorStore,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { MilvusVectorStore } from "llamaindex/storage/vectorStore/MilvusVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars, getMilvusClient } from "./shared";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MilvusVectorStore, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { MilvusVectorStore } from "llamaindex/storage/vectorStore/MilvusVectorStore";
|
||||
import { checkRequiredEnvVars, getMilvusClient } from "./shared";
|
||||
|
||||
export async function getDataSource() {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
MongoDBAtlasVectorSearch,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorSearch";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { MongoDBAtlasVectorSearch, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorSearch";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { storageContextFromDefaults } from "llamaindex/storage/StorageContext";
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
SimpleDocumentStore,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { SimpleDocumentStore, VectorStoreIndex } from "llamaindex";
|
||||
import { storageContextFromDefaults } from "llamaindex/storage/StorageContext";
|
||||
import { STORAGE_CACHE_DIR } from "./shared";
|
||||
|
||||
export async function getDataSource() {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
PGVectorStore,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { PGVectorStore } from "llamaindex/storage/vectorStore/PGVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { PGVectorStore, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { PGVectorStore } from "llamaindex/storage/vectorStore/PGVectorStore";
|
||||
import {
|
||||
PGVECTOR_SCHEMA,
|
||||
PGVECTOR_TABLE,
|
||||
|
||||
@@ -2,7 +2,7 @@ export const PGVECTOR_COLLECTION = "data";
|
||||
export const PGVECTOR_SCHEMA = "public";
|
||||
export const PGVECTOR_TABLE = "llamaindex_embedding";
|
||||
|
||||
const REQUIRED_ENV_VARS = ["PG_CONNECTION_STRING", "OPENAI_API_KEY"];
|
||||
const REQUIRED_ENV_VARS = ["PG_CONNECTION_STRING"];
|
||||
|
||||
export function checkRequiredEnvVars() {
|
||||
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
PineconeVectorStore,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { PineconeVectorStore } from "llamaindex/storage/vectorStore/PineconeVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { PineconeVectorStore, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { PineconeVectorStore } from "llamaindex/storage/vectorStore/PineconeVectorStore";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource() {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
QdrantVectorStore,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { QdrantVectorStore } from "llamaindex/storage/vectorStore/QdrantVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars, getQdrantClient } from "./shared";
|
||||
@@ -18,7 +15,10 @@ async function loadAndIndex() {
|
||||
const documents = await getDocuments();
|
||||
|
||||
// Connect to Qdrant
|
||||
const vectorStore = new QdrantVectorStore(collectionName, getQdrantClient());
|
||||
const vectorStore = new QdrantVectorStore({
|
||||
collectionName,
|
||||
client: getQdrantClient(),
|
||||
});
|
||||
|
||||
const storageContext = await storageContextFromDefaults({ vectorStore });
|
||||
await VectorStoreIndex.fromDocuments(documents, {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { QdrantVectorStore, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { QdrantVectorStore } from "llamaindex/storage/vectorStore/QdrantVectorStore";
|
||||
import { checkRequiredEnvVars, getQdrantClient } from "./shared";
|
||||
|
||||
dotenv.config();
|
||||
@@ -7,7 +8,10 @@ dotenv.config();
|
||||
export async function getDataSource() {
|
||||
checkRequiredEnvVars();
|
||||
const collectionName = process.env.QDRANT_COLLECTION;
|
||||
const store = new QdrantVectorStore(collectionName, getQdrantClient());
|
||||
const store = new QdrantVectorStore({
|
||||
collectionName,
|
||||
client: getQdrantClient(),
|
||||
});
|
||||
|
||||
return await VectorStoreIndex.fromVectorStore(store);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
node-linker=hoisted
|
||||
@@ -5,16 +5,18 @@
|
||||
"scripts": {
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"build": "tsup index.ts --format esm --dts",
|
||||
"start": "node dist/index.mjs",
|
||||
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.mjs\""
|
||||
"build": "tsup index.ts --format cjs --dts",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "concurrently \"tsup index.ts --format cjs --dts --watch\" \"nodemon -q dist/index.mjs\""
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "^2.2.25",
|
||||
"ai": "^3.0.21",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"llamaindex": "latest"
|
||||
"llamaindex": "0.3.9",
|
||||
"pdf2json": "3.0.5",
|
||||
"ajv": "^8.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.16",
|
||||
@@ -22,12 +24,12 @@
|
||||
"@types/node": "^20.9.5",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.2",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"ts-node": "^10.9.2"
|
||||
"tsx": "^4.7.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const chatRequest = async (req: Request, res: Response) => {
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return res.status(500).json({
|
||||
error: (error as Error).message,
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { streamToResponse } from "ai";
|
||||
import { Message, StreamData, streamToResponse } from "ai";
|
||||
import { Request, Response } from "express";
|
||||
import { ChatMessage, MessageContent } from "llamaindex";
|
||||
import { ChatMessage, MessageContent, Settings } from "llamaindex";
|
||||
import { createChatEngine } from "./engine/chat";
|
||||
import { LlamaIndexStream } from "./llamaindex-stream";
|
||||
import { createCallbackManager } from "./stream-helper";
|
||||
|
||||
const convertMessageContent = (
|
||||
textMessage: string,
|
||||
@@ -25,7 +26,7 @@ const convertMessageContent = (
|
||||
|
||||
export const chat = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { messages, data }: { messages: ChatMessage[]; data: any } = req.body;
|
||||
const { messages, data }: { messages: Message[]; data: any } = req.body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return res.status(400).json({
|
||||
@@ -42,36 +43,34 @@ export const chat = async (req: Request, res: Response) => {
|
||||
data?.imageUrl,
|
||||
);
|
||||
|
||||
// Init Vercel AI StreamData
|
||||
const vercelStreamData = new StreamData();
|
||||
|
||||
// Setup callbacks
|
||||
const callbackManager = createCallbackManager(vercelStreamData);
|
||||
|
||||
// Calling LlamaIndex's ChatEngine to get a streamed response
|
||||
const response = await chatEngine.chat({
|
||||
message: userMessageContent,
|
||||
chatHistory: messages,
|
||||
stream: true,
|
||||
const response = await Settings.withCallbackManager(callbackManager, () => {
|
||||
return chatEngine.chat({
|
||||
message: userMessageContent,
|
||||
chatHistory: messages as ChatMessage[],
|
||||
stream: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Return a stream, which can be consumed by the Vercel/AI client
|
||||
const { stream, data: streamData } = LlamaIndexStream(response, {
|
||||
const stream = LlamaIndexStream(response, vercelStreamData, {
|
||||
parserOptions: {
|
||||
image_url: data?.imageUrl,
|
||||
},
|
||||
});
|
||||
const processedStream = stream.pipeThrough(vercelStreamData.stream);
|
||||
|
||||
// Pipe LlamaIndexStream to response
|
||||
const processedStream = stream.pipeThrough(streamData.stream);
|
||||
return streamToResponse(processedStream, res, {
|
||||
headers: {
|
||||
// response MUST have the `X-Experimental-Stream-Data: 'true'` header
|
||||
// so that the client uses the correct parsing logic, see
|
||||
// https://sdk.vercel.ai/docs/api-reference/stream-data#on-the-server
|
||||
"X-Experimental-Stream-Data": "true",
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Access-Control-Expose-Headers": "X-Experimental-Stream-Data",
|
||||
},
|
||||
});
|
||||
return streamToResponse(processedStream, res);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return res.status(500).json({
|
||||
error: (error as Error).message,
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,87 @@
|
||||
import { OpenAI, OpenAIEmbedding, Settings } from "llamaindex";
|
||||
import {
|
||||
Anthropic,
|
||||
GEMINI_EMBEDDING_MODEL,
|
||||
GEMINI_MODEL,
|
||||
Gemini,
|
||||
GeminiEmbedding,
|
||||
OpenAI,
|
||||
OpenAIEmbedding,
|
||||
Settings,
|
||||
} from "llamaindex";
|
||||
import { HuggingFaceEmbedding } from "llamaindex/embeddings/HuggingFaceEmbedding";
|
||||
import { OllamaEmbedding } from "llamaindex/embeddings/OllamaEmbedding";
|
||||
import { ALL_AVAILABLE_ANTHROPIC_MODELS } from "llamaindex/llm/anthropic";
|
||||
import { Ollama } from "llamaindex/llm/ollama";
|
||||
|
||||
const CHUNK_SIZE = 512;
|
||||
const CHUNK_OVERLAP = 20;
|
||||
|
||||
export const initSettings = async () => {
|
||||
// HINT: you can delete the initialization code for unused model providers
|
||||
console.log(`Using '${process.env.MODEL_PROVIDER}' model provider`);
|
||||
|
||||
if (!process.env.MODEL || !process.env.EMBEDDING_MODEL) {
|
||||
throw new Error("'MODEL' and 'EMBEDDING_MODEL' env variables must be set.");
|
||||
}
|
||||
|
||||
switch (process.env.MODEL_PROVIDER) {
|
||||
case "ollama":
|
||||
initOllama();
|
||||
break;
|
||||
case "anthropic":
|
||||
initAnthropic();
|
||||
break;
|
||||
case "gemini":
|
||||
initGemini();
|
||||
break;
|
||||
default:
|
||||
initOpenAI();
|
||||
break;
|
||||
}
|
||||
Settings.chunkSize = CHUNK_SIZE;
|
||||
Settings.chunkOverlap = CHUNK_OVERLAP;
|
||||
};
|
||||
|
||||
function initOpenAI() {
|
||||
Settings.llm = new OpenAI({
|
||||
model: process.env.MODEL ?? "gpt-3.5-turbo",
|
||||
maxTokens: 512,
|
||||
});
|
||||
Settings.chunkSize = CHUNK_SIZE;
|
||||
Settings.chunkOverlap = CHUNK_OVERLAP;
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: process.env.EMBEDDING_MODEL,
|
||||
dimensions: process.env.EMBEDDING_DIM
|
||||
? parseInt(process.env.EMBEDDING_DIM)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initOllama() {
|
||||
Settings.llm = new Ollama({
|
||||
model: process.env.MODEL ?? "",
|
||||
});
|
||||
Settings.embedModel = new OllamaEmbedding({
|
||||
model: process.env.EMBEDDING_MODEL ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
function initAnthropic() {
|
||||
const embedModelMap: Record<string, string> = {
|
||||
"all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
|
||||
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
|
||||
};
|
||||
Settings.llm = new Anthropic({
|
||||
model: process.env.MODEL as keyof typeof ALL_AVAILABLE_ANTHROPIC_MODELS,
|
||||
});
|
||||
Settings.embedModel = new HuggingFaceEmbedding({
|
||||
modelType: embedModelMap[process.env.EMBEDDING_MODEL!],
|
||||
});
|
||||
}
|
||||
|
||||
function initGemini() {
|
||||
Settings.llm = new Gemini({
|
||||
model: process.env.MODEL as GEMINI_MODEL,
|
||||
});
|
||||
Settings.embedModel = new GeminiEmbedding({
|
||||
model: process.env.EMBEDDING_MODEL as GEMINI_EMBEDDING_MODEL,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,49 +1,65 @@
|
||||
import {
|
||||
JSONValue,
|
||||
StreamData,
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
experimental_StreamData,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import { Response, StreamingAgentChatResponse } from "llamaindex";
|
||||
import {
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Response,
|
||||
ToolCallLLMMessageOptions,
|
||||
} from "llamaindex";
|
||||
|
||||
import { AgentStreamChatResponse } from "llamaindex/agent/base";
|
||||
import { appendImageData, appendSourceData } from "./stream-helper";
|
||||
|
||||
type LlamaIndexResponse =
|
||||
| AgentStreamChatResponse<ToolCallLLMMessageOptions>
|
||||
| Response;
|
||||
|
||||
type ParserOptions = {
|
||||
image_url?: string;
|
||||
};
|
||||
|
||||
function createParser(
|
||||
res: AsyncIterable<Response>,
|
||||
data: experimental_StreamData,
|
||||
res: AsyncIterable<LlamaIndexResponse>,
|
||||
data: StreamData,
|
||||
opts?: ParserOptions,
|
||||
) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
|
||||
let sourceNodes: NodeWithScore<Metadata>[] | undefined;
|
||||
return new ReadableStream<string>({
|
||||
start() {
|
||||
// if image_url is provided, send it via the data stream
|
||||
if (opts?.image_url) {
|
||||
const message: JSONValue = {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: opts.image_url,
|
||||
},
|
||||
};
|
||||
data.append(message);
|
||||
} else {
|
||||
data.append({}); // send an empty image response for the user's message
|
||||
}
|
||||
appendImageData(data, opts?.image_url);
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
if (done) {
|
||||
if (sourceNodes) {
|
||||
appendSourceData(data, sourceNodes);
|
||||
}
|
||||
controller.close();
|
||||
data.append({}); // send an empty image response for the assistant's message
|
||||
data.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const text = trimStartOfStream(value.response ?? "");
|
||||
let delta;
|
||||
if (value instanceof Response) {
|
||||
// handle Response type
|
||||
if (value.sourceNodes) {
|
||||
// get source nodes from the first response
|
||||
sourceNodes = value.sourceNodes;
|
||||
}
|
||||
delta = value.response ?? "";
|
||||
} else {
|
||||
// handle other types
|
||||
delta = value.response.delta;
|
||||
}
|
||||
const text = trimStartOfStream(delta ?? "");
|
||||
if (text) {
|
||||
controller.enqueue(text);
|
||||
}
|
||||
@@ -52,21 +68,14 @@ function createParser(
|
||||
}
|
||||
|
||||
export function LlamaIndexStream(
|
||||
response: StreamingAgentChatResponse | AsyncIterable<Response>,
|
||||
response: AsyncIterable<LlamaIndexResponse>,
|
||||
data: StreamData,
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
parserOptions?: ParserOptions;
|
||||
},
|
||||
): { stream: ReadableStream; data: experimental_StreamData } {
|
||||
const data = new experimental_StreamData();
|
||||
const res =
|
||||
response instanceof StreamingAgentChatResponse
|
||||
? response.response
|
||||
: response;
|
||||
return {
|
||||
stream: createParser(res, data, opts?.parserOptions)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer(true)),
|
||||
data,
|
||||
};
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data, opts?.parserOptions)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { StreamData } from "ai";
|
||||
import {
|
||||
CallbackManager,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
ToolCall,
|
||||
ToolOutput,
|
||||
} from "llamaindex";
|
||||
|
||||
export function appendImageData(data: StreamData, imageUrl?: string) {
|
||||
if (!imageUrl) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "image",
|
||||
data: {
|
||||
url: imageUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
sourceNodes?: NodeWithScore<Metadata>[],
|
||||
) {
|
||||
if (!sourceNodes?.length) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "sources",
|
||||
data: {
|
||||
nodes: sourceNodes.map((node) => ({
|
||||
...node.node.toMutableJSON(),
|
||||
id: node.node.id_,
|
||||
score: node.score ?? null,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendEventData(data: StreamData, title?: string) {
|
||||
if (!title) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "events",
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendToolData(
|
||||
data: StreamData,
|
||||
toolCall: ToolCall,
|
||||
toolOutput: ToolOutput,
|
||||
) {
|
||||
data.appendMessageAnnotation({
|
||||
type: "tools",
|
||||
data: {
|
||||
toolCall: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
input: toolCall.input,
|
||||
},
|
||||
toolOutput: {
|
||||
output: toolOutput.output,
|
||||
isError: toolOutput.isError,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createCallbackManager(stream: StreamData) {
|
||||
const callbackManager = new CallbackManager();
|
||||
|
||||
callbackManager.on("retrieve", (data) => {
|
||||
const { nodes, query } = data.detail;
|
||||
appendEventData(stream, `Retrieving context for query: '${query}'`);
|
||||
appendEventData(
|
||||
stream,
|
||||
`Retrieved ${nodes.length} sources to use as context for the query`,
|
||||
);
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-call", (event) => {
|
||||
const { name, input } = event.detail.payload.toolCall;
|
||||
const inputString = Object.entries(input)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(", ");
|
||||
appendEventData(
|
||||
stream,
|
||||
`Using tool: '${name}' with inputs: '${inputString}'`,
|
||||
);
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-result", (event) => {
|
||||
const { toolCall, toolResult } = event.detail.payload;
|
||||
appendToolData(stream, toolCall, toolResult);
|
||||
});
|
||||
|
||||
return callbackManager;
|
||||
}
|
||||
@@ -9,10 +9,5 @@
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,7 @@ poetry install
|
||||
poetry shell
|
||||
```
|
||||
|
||||
By default, we use the OpenAI LLM (though you can customize, see `app/settings.py`). As a result, you need to specify an `OPENAI_API_KEY` in an .env file in this directory.
|
||||
|
||||
Example `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=<openai_api_key>
|
||||
```
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
|
||||
|
||||
If you are using any tools or data sources, you can update their config files in the `config` folder.
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Any, Optional, Dict, Tuple
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from llama_index.core.chat_engine.types import (
|
||||
BaseChatEngine,
|
||||
)
|
||||
from llama_index.core.chat_engine.types import BaseChatEngine
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
from llama_index.core.llms import ChatMessage, MessageRole
|
||||
from app.engine import get_chat_engine
|
||||
from app.api.routers.vercel_response import VercelStreamResponse
|
||||
from app.api.routers.messaging import EventCallbackHandler
|
||||
from aiostream import stream
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
|
||||
@@ -37,6 +37,7 @@ class _SourceNodes(BaseModel):
|
||||
id: str
|
||||
metadata: Dict[str, Any]
|
||||
score: Optional[float]
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
def from_source_node(cls, source_node: NodeWithScore):
|
||||
@@ -44,6 +45,7 @@ class _SourceNodes(BaseModel):
|
||||
id=source_node.node.node_id,
|
||||
metadata=source_node.node.metadata,
|
||||
score=source_node.score,
|
||||
text=source_node.node.text, # type: ignore
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -89,15 +91,46 @@ async def chat(
|
||||
):
|
||||
last_message_content, messages = await parse_chat_data(data)
|
||||
|
||||
event_handler = EventCallbackHandler()
|
||||
chat_engine.callback_manager.handlers.append(event_handler) # type: ignore
|
||||
response = await chat_engine.astream_chat(last_message_content, messages)
|
||||
|
||||
async def event_generator():
|
||||
async for token in response.async_response_gen():
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
yield token
|
||||
async def content_generator():
|
||||
# Yield the text response
|
||||
async def _text_generator():
|
||||
async for token in response.async_response_gen():
|
||||
yield VercelStreamResponse.convert_text(token)
|
||||
# the text_generator is the leading stream, once it's finished, also finish the event stream
|
||||
event_handler.is_done = True
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/plain")
|
||||
# Yield the events from the event handler
|
||||
async def _event_generator():
|
||||
async for event in event_handler.async_event_gen():
|
||||
event_response = event.to_response()
|
||||
if event_response is not None:
|
||||
yield VercelStreamResponse.convert_data(event_response)
|
||||
|
||||
combine = stream.merge(_text_generator(), _event_generator())
|
||||
async with combine.stream() as streamer:
|
||||
async for item in streamer:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
yield item
|
||||
|
||||
# Yield the source nodes
|
||||
yield VercelStreamResponse.convert_data(
|
||||
{
|
||||
"type": "sources",
|
||||
"data": {
|
||||
"nodes": [
|
||||
_SourceNodes.from_source_node(node).dict()
|
||||
for node in response.source_nodes
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return VercelStreamResponse(content=content_generator())
|
||||
|
||||
|
||||
# non-streaming endpoint - delete if not needed
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import json
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, Dict, Any, List, Optional
|
||||
from llama_index.core.callbacks.base import BaseCallbackHandler
|
||||
from llama_index.core.callbacks.schema import CBEventType
|
||||
from llama_index.core.tools.types import ToolOutput
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CallbackEvent(BaseModel):
|
||||
event_type: CBEventType
|
||||
payload: Optional[Dict[str, Any]] = None
|
||||
event_id: str = ""
|
||||
|
||||
def get_retrieval_message(self) -> dict | None:
|
||||
if self.payload:
|
||||
nodes = self.payload.get("nodes")
|
||||
if nodes:
|
||||
msg = f"Retrieved {len(nodes)} sources to use as context for the query"
|
||||
else:
|
||||
msg = f"Retrieving context for query: '{self.payload.get('query_str')}'"
|
||||
return {
|
||||
"type": "events",
|
||||
"data": {"title": msg},
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_tool_message(self) -> dict | None:
|
||||
func_call_args = self.payload.get("function_call")
|
||||
if func_call_args is not None and "tool" in self.payload:
|
||||
tool = self.payload.get("tool")
|
||||
return {
|
||||
"type": "events",
|
||||
"data": {
|
||||
"title": f"Calling tool: {tool.name} with inputs: {func_call_args}",
|
||||
},
|
||||
}
|
||||
|
||||
def _is_output_serializable(self, output: Any) -> bool:
|
||||
try:
|
||||
json.dumps(output)
|
||||
return True
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def get_agent_tool_response(self) -> dict | None:
|
||||
response = self.payload.get("response")
|
||||
if response is not None:
|
||||
sources = response.sources
|
||||
for source in sources:
|
||||
# Return the tool response here to include the toolCall information
|
||||
if isinstance(source, ToolOutput):
|
||||
if self._is_output_serializable(source.raw_output):
|
||||
output = source.raw_output
|
||||
else:
|
||||
output = source.content
|
||||
|
||||
return {
|
||||
"type": "tools",
|
||||
"data": {
|
||||
"toolOutput": {
|
||||
"output": output,
|
||||
"isError": source.is_error,
|
||||
},
|
||||
"toolCall": {
|
||||
"id": None, # There is no tool id in the ToolOutput
|
||||
"name": source.tool_name,
|
||||
"input": source.raw_input,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def to_response(self):
|
||||
match self.event_type:
|
||||
case "retrieve":
|
||||
return self.get_retrieval_message()
|
||||
case "function_call":
|
||||
return self.get_tool_message()
|
||||
case "agent_step":
|
||||
return self.get_agent_tool_response()
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
class EventCallbackHandler(BaseCallbackHandler):
|
||||
_aqueue: asyncio.Queue
|
||||
is_done: bool = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
"""Initialize the base callback handler."""
|
||||
ignored_events = [
|
||||
CBEventType.CHUNKING,
|
||||
CBEventType.NODE_PARSING,
|
||||
CBEventType.EMBEDDING,
|
||||
CBEventType.LLM,
|
||||
CBEventType.TEMPLATING,
|
||||
]
|
||||
super().__init__(ignored_events, ignored_events)
|
||||
self._aqueue = asyncio.Queue()
|
||||
|
||||
def on_event_start(
|
||||
self,
|
||||
event_type: CBEventType,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
event_id: str = "",
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
event = CallbackEvent(event_id=event_id, event_type=event_type, payload=payload)
|
||||
if event.to_response() is not None:
|
||||
self._aqueue.put_nowait(event)
|
||||
|
||||
def on_event_end(
|
||||
self,
|
||||
event_type: CBEventType,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
event_id: str = "",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
event = CallbackEvent(event_id=event_id, event_type=event_type, payload=payload)
|
||||
if event.to_response() is not None:
|
||||
self._aqueue.put_nowait(event)
|
||||
|
||||
def start_trace(self, trace_id: Optional[str] = None) -> None:
|
||||
"""No-op."""
|
||||
|
||||
def end_trace(
|
||||
self,
|
||||
trace_id: Optional[str] = None,
|
||||
trace_map: Optional[Dict[str, List[str]]] = None,
|
||||
) -> None:
|
||||
"""No-op."""
|
||||
|
||||
async def async_event_gen(self) -> AsyncGenerator[CallbackEvent, None]:
|
||||
while not self._aqueue.empty() or not self.is_done:
|
||||
try:
|
||||
yield await asyncio.wait_for(self._aqueue.get(), timeout=0.1)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
from typing import Any
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
|
||||
class VercelStreamResponse(StreamingResponse):
|
||||
"""
|
||||
Class to convert the response from the chat engine to the streaming format expected by Vercel
|
||||
"""
|
||||
|
||||
TEXT_PREFIX = "0:"
|
||||
DATA_PREFIX = "8:"
|
||||
|
||||
@classmethod
|
||||
def convert_text(cls, token: str):
|
||||
# Escape newlines and double quotes to avoid breaking the stream
|
||||
token = json.dumps(token)
|
||||
return f"{cls.TEXT_PREFIX}{token}\n"
|
||||
|
||||
@classmethod
|
||||
def convert_data(cls, data: dict):
|
||||
data_str = json.dumps(data)
|
||||
return f"{cls.DATA_PREFIX}[{data_str}]\n"
|
||||
|
||||
def __init__(self, content: Any, **kwargs):
|
||||
super().__init__(
|
||||
content=content,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -1,41 +1,92 @@
|
||||
import os
|
||||
from typing import Dict
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
|
||||
|
||||
def llm_config_from_env() -> Dict:
|
||||
from llama_index.core.constants import DEFAULT_TEMPERATURE
|
||||
|
||||
model = os.getenv("MODEL")
|
||||
temperature = os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)
|
||||
max_tokens = os.getenv("LLM_MAX_TOKENS")
|
||||
|
||||
config = {
|
||||
"model": model,
|
||||
"temperature": float(temperature),
|
||||
"max_tokens": int(max_tokens) if max_tokens is not None else None,
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
def embedding_config_from_env() -> Dict:
|
||||
model = os.getenv("EMBEDDING_MODEL")
|
||||
dimension = os.getenv("EMBEDDING_DIM")
|
||||
|
||||
config = {
|
||||
"model": model,
|
||||
"dimension": int(dimension) if dimension is not None else None,
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
def init_settings():
|
||||
llm_configs = llm_config_from_env()
|
||||
embedding_configs = embedding_config_from_env()
|
||||
|
||||
Settings.llm = OpenAI(**llm_configs)
|
||||
Settings.embed_model = OpenAIEmbedding(**embedding_configs)
|
||||
model_provider = os.getenv("MODEL_PROVIDER")
|
||||
if model_provider == "openai":
|
||||
init_openai()
|
||||
elif model_provider == "ollama":
|
||||
init_ollama()
|
||||
elif model_provider == "anthropic":
|
||||
init_anthropic()
|
||||
elif model_provider == "gemini":
|
||||
init_gemini()
|
||||
else:
|
||||
raise ValueError(f"Invalid model provider: {model_provider}")
|
||||
Settings.chunk_size = int(os.getenv("CHUNK_SIZE", "1024"))
|
||||
Settings.chunk_overlap = int(os.getenv("CHUNK_OVERLAP", "20"))
|
||||
|
||||
|
||||
def init_ollama():
|
||||
from llama_index.llms.ollama import Ollama
|
||||
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||
|
||||
Settings.embed_model = OllamaEmbedding(model_name=os.getenv("EMBEDDING_MODEL"))
|
||||
Settings.llm = Ollama(model=os.getenv("MODEL"))
|
||||
|
||||
|
||||
def init_openai():
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
from llama_index.core.constants import DEFAULT_TEMPERATURE
|
||||
|
||||
max_tokens = os.getenv("LLM_MAX_TOKENS")
|
||||
config = {
|
||||
"model": os.getenv("MODEL"),
|
||||
"temperature": float(os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)),
|
||||
"max_tokens": int(max_tokens) if max_tokens is not None else None,
|
||||
}
|
||||
Settings.llm = OpenAI(**config)
|
||||
|
||||
dimensions = os.getenv("EMBEDDING_DIM")
|
||||
config = {
|
||||
"model": os.getenv("EMBEDDING_MODEL"),
|
||||
"dimensions": int(dimensions) if dimensions is not None else None,
|
||||
}
|
||||
Settings.embed_model = OpenAIEmbedding(**config)
|
||||
|
||||
|
||||
def init_anthropic():
|
||||
from llama_index.llms.anthropic import Anthropic
|
||||
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
||||
|
||||
model_map: Dict[str, str] = {
|
||||
"claude-3-opus": "claude-3-opus-20240229",
|
||||
"claude-3-sonnet": "claude-3-sonnet-20240229",
|
||||
"claude-3-haiku": "claude-3-haiku-20240307",
|
||||
"claude-2.1": "claude-2.1",
|
||||
"claude-instant-1.2": "claude-instant-1.2",
|
||||
}
|
||||
|
||||
embed_model_map: Dict[str, str] = {
|
||||
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
|
||||
"all-mpnet-base-v2": "sentence-transformers/all-mpnet-base-v2",
|
||||
}
|
||||
|
||||
Settings.llm = Anthropic(model=model_map[os.getenv("MODEL")])
|
||||
Settings.embed_model = HuggingFaceEmbedding(
|
||||
model_name=embed_model_map[os.getenv("EMBEDDING_MODEL")]
|
||||
)
|
||||
|
||||
|
||||
def init_gemini():
|
||||
from llama_index.llms.gemini import Gemini
|
||||
from llama_index.embeddings.gemini import GeminiEmbedding
|
||||
|
||||
model_map: Dict[str, str] = {
|
||||
"gemini-1.5-pro-latest": "models/gemini-1.5-pro-latest",
|
||||
"gemini-pro": "models/gemini-pro",
|
||||
"gemini-pro-vision": "models/gemini-pro-vision",
|
||||
}
|
||||
|
||||
embed_model_map: Dict[str, str] = {
|
||||
"embedding-001": "models/embedding-001",
|
||||
"text-embedding-004": "models/text-embedding-004",
|
||||
}
|
||||
|
||||
Settings.llm = Gemini(model=model_map[os.getenv("MODEL")])
|
||||
Settings.embed_model = GeminiEmbedding(
|
||||
model_name=embed_model_map[os.getenv("EMBEDDING_MODEL")]
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ python = "^3.11,<3.12"
|
||||
fastapi = "^0.109.1"
|
||||
uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
python-dotenv = "^1.0.0"
|
||||
aiostream = "^0.5.2"
|
||||
llama-index = "0.10.28"
|
||||
llama-index-core = "0.10.28"
|
||||
llama-index-agent-openai = "0.2.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
@@ -1,19 +1,87 @@
|
||||
import { OpenAI, OpenAIEmbedding, Settings } from "llamaindex";
|
||||
import {
|
||||
Anthropic,
|
||||
GEMINI_EMBEDDING_MODEL,
|
||||
GEMINI_MODEL,
|
||||
Gemini,
|
||||
GeminiEmbedding,
|
||||
OpenAI,
|
||||
OpenAIEmbedding,
|
||||
Settings,
|
||||
} from "llamaindex";
|
||||
import { HuggingFaceEmbedding } from "llamaindex/embeddings/HuggingFaceEmbedding";
|
||||
import { OllamaEmbedding } from "llamaindex/embeddings/OllamaEmbedding";
|
||||
import { ALL_AVAILABLE_ANTHROPIC_MODELS } from "llamaindex/llm/anthropic";
|
||||
import { Ollama } from "llamaindex/llm/ollama";
|
||||
|
||||
const CHUNK_SIZE = 512;
|
||||
const CHUNK_OVERLAP = 20;
|
||||
|
||||
export const initSettings = async () => {
|
||||
// HINT: you can delete the initialization code for unused model providers
|
||||
console.log(`Using '${process.env.MODEL_PROVIDER}' model provider`);
|
||||
|
||||
if (!process.env.MODEL || !process.env.EMBEDDING_MODEL) {
|
||||
throw new Error("'MODEL' and 'EMBEDDING_MODEL' env variables must be set.");
|
||||
}
|
||||
|
||||
switch (process.env.MODEL_PROVIDER) {
|
||||
case "ollama":
|
||||
initOllama();
|
||||
break;
|
||||
case "anthropic":
|
||||
initAnthropic();
|
||||
break;
|
||||
case "gemini":
|
||||
initGemini();
|
||||
break;
|
||||
default:
|
||||
initOpenAI();
|
||||
break;
|
||||
}
|
||||
Settings.chunkSize = CHUNK_SIZE;
|
||||
Settings.chunkOverlap = CHUNK_OVERLAP;
|
||||
};
|
||||
|
||||
function initOpenAI() {
|
||||
Settings.llm = new OpenAI({
|
||||
model: process.env.MODEL ?? "gpt-3.5-turbo",
|
||||
maxTokens: 512,
|
||||
});
|
||||
Settings.chunkSize = CHUNK_SIZE;
|
||||
Settings.chunkOverlap = CHUNK_OVERLAP;
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: process.env.EMBEDDING_MODEL,
|
||||
dimensions: process.env.EMBEDDING_DIM
|
||||
? parseInt(process.env.EMBEDDING_DIM)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initOllama() {
|
||||
Settings.llm = new Ollama({
|
||||
model: process.env.MODEL ?? "",
|
||||
});
|
||||
Settings.embedModel = new OllamaEmbedding({
|
||||
model: process.env.EMBEDDING_MODEL ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
function initAnthropic() {
|
||||
const embedModelMap: Record<string, string> = {
|
||||
"all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
|
||||
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
|
||||
};
|
||||
Settings.llm = new Anthropic({
|
||||
model: process.env.MODEL as keyof typeof ALL_AVAILABLE_ANTHROPIC_MODELS,
|
||||
});
|
||||
Settings.embedModel = new HuggingFaceEmbedding({
|
||||
modelType: embedModelMap[process.env.EMBEDDING_MODEL!],
|
||||
});
|
||||
}
|
||||
|
||||
function initGemini() {
|
||||
Settings.llm = new Gemini({
|
||||
model: process.env.MODEL as GEMINI_MODEL,
|
||||
});
|
||||
Settings.embedModel = new GeminiEmbedding({
|
||||
model: process.env.EMBEDDING_MODEL as GEMINI_EMBEDDING_MODEL,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,49 +1,65 @@
|
||||
import {
|
||||
JSONValue,
|
||||
StreamData,
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
experimental_StreamData,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import { Response, StreamingAgentChatResponse } from "llamaindex";
|
||||
import {
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Response,
|
||||
ToolCallLLMMessageOptions,
|
||||
} from "llamaindex";
|
||||
|
||||
import { AgentStreamChatResponse } from "llamaindex/agent/base";
|
||||
import { appendImageData, appendSourceData } from "./stream-helper";
|
||||
|
||||
type LlamaIndexResponse =
|
||||
| AgentStreamChatResponse<ToolCallLLMMessageOptions>
|
||||
| Response;
|
||||
|
||||
type ParserOptions = {
|
||||
image_url?: string;
|
||||
};
|
||||
|
||||
function createParser(
|
||||
res: AsyncIterable<Response>,
|
||||
data: experimental_StreamData,
|
||||
res: AsyncIterable<LlamaIndexResponse>,
|
||||
data: StreamData,
|
||||
opts?: ParserOptions,
|
||||
) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
|
||||
let sourceNodes: NodeWithScore<Metadata>[] | undefined;
|
||||
return new ReadableStream<string>({
|
||||
start() {
|
||||
// if image_url is provided, send it via the data stream
|
||||
if (opts?.image_url) {
|
||||
const message: JSONValue = {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: opts.image_url,
|
||||
},
|
||||
};
|
||||
data.append(message);
|
||||
} else {
|
||||
data.append({}); // send an empty image response for the user's message
|
||||
}
|
||||
appendImageData(data, opts?.image_url);
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
if (done) {
|
||||
if (sourceNodes) {
|
||||
appendSourceData(data, sourceNodes);
|
||||
}
|
||||
controller.close();
|
||||
data.append({}); // send an empty image response for the assistant's message
|
||||
data.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const text = trimStartOfStream(value.response ?? "");
|
||||
let delta;
|
||||
if (value instanceof Response) {
|
||||
// handle Response type
|
||||
if (value.sourceNodes) {
|
||||
// get source nodes from the first response
|
||||
sourceNodes = value.sourceNodes;
|
||||
}
|
||||
delta = value.response ?? "";
|
||||
} else {
|
||||
// handle other types
|
||||
delta = value.response.delta;
|
||||
}
|
||||
const text = trimStartOfStream(delta ?? "");
|
||||
if (text) {
|
||||
controller.enqueue(text);
|
||||
}
|
||||
@@ -52,21 +68,14 @@ function createParser(
|
||||
}
|
||||
|
||||
export function LlamaIndexStream(
|
||||
response: StreamingAgentChatResponse | AsyncIterable<Response>,
|
||||
response: AsyncIterable<LlamaIndexResponse>,
|
||||
data: StreamData,
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
parserOptions?: ParserOptions;
|
||||
},
|
||||
): { stream: ReadableStream; data: experimental_StreamData } {
|
||||
const data = new experimental_StreamData();
|
||||
const res =
|
||||
response instanceof StreamingAgentChatResponse
|
||||
? response.response
|
||||
: response;
|
||||
return {
|
||||
stream: createParser(res, data, opts?.parserOptions)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer(true)),
|
||||
data,
|
||||
};
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data, opts?.parserOptions)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { initObservability } from "@/app/observability";
|
||||
import { StreamingTextResponse } from "ai";
|
||||
import { ChatMessage, MessageContent } from "llamaindex";
|
||||
import { Message, StreamData, StreamingTextResponse } from "ai";
|
||||
import { ChatMessage, MessageContent, Settings } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createChatEngine } from "./engine/chat";
|
||||
import { initSettings } from "./engine/settings";
|
||||
import { LlamaIndexStream } from "./llamaindex-stream";
|
||||
import { createCallbackManager } from "./stream-helper";
|
||||
|
||||
initObservability();
|
||||
initSettings();
|
||||
@@ -34,7 +35,7 @@ const convertMessageContent = (
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages, data }: { messages: ChatMessage[]; data: any } = body;
|
||||
const { messages, data }: { messages: Message[]; data: any } = body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return NextResponse.json(
|
||||
@@ -54,27 +55,35 @@ export async function POST(request: NextRequest) {
|
||||
data?.imageUrl,
|
||||
);
|
||||
|
||||
// Init Vercel AI StreamData
|
||||
const vercelStreamData = new StreamData();
|
||||
|
||||
// Setup callbacks
|
||||
const callbackManager = createCallbackManager(vercelStreamData);
|
||||
|
||||
// Calling LlamaIndex's ChatEngine to get a streamed response
|
||||
const response = await chatEngine.chat({
|
||||
message: userMessageContent,
|
||||
chatHistory: messages,
|
||||
stream: true,
|
||||
const response = await Settings.withCallbackManager(callbackManager, () => {
|
||||
return chatEngine.chat({
|
||||
message: userMessageContent,
|
||||
chatHistory: messages as ChatMessage[],
|
||||
stream: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Transform LlamaIndex stream to Vercel/AI format
|
||||
const { stream, data: streamData } = LlamaIndexStream(response, {
|
||||
const stream = LlamaIndexStream(response, vercelStreamData, {
|
||||
parserOptions: {
|
||||
image_url: data?.imageUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// Return a StreamingTextResponse, which can be consumed by the Vercel/AI client
|
||||
return new StreamingTextResponse(stream, {}, streamData);
|
||||
return new StreamingTextResponse(stream, {}, vercelStreamData);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: (error as Error).message,
|
||||
detail: (error as Error).message,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { StreamData } from "ai";
|
||||
import {
|
||||
CallbackManager,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
ToolCall,
|
||||
ToolOutput,
|
||||
} from "llamaindex";
|
||||
|
||||
export function appendImageData(data: StreamData, imageUrl?: string) {
|
||||
if (!imageUrl) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "image",
|
||||
data: {
|
||||
url: imageUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
sourceNodes?: NodeWithScore<Metadata>[],
|
||||
) {
|
||||
if (!sourceNodes?.length) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "sources",
|
||||
data: {
|
||||
nodes: sourceNodes.map((node) => ({
|
||||
...node.node.toMutableJSON(),
|
||||
id: node.node.id_,
|
||||
score: node.score ?? null,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendEventData(data: StreamData, title?: string) {
|
||||
if (!title) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "events",
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendToolData(
|
||||
data: StreamData,
|
||||
toolCall: ToolCall,
|
||||
toolOutput: ToolOutput,
|
||||
) {
|
||||
data.appendMessageAnnotation({
|
||||
type: "tools",
|
||||
data: {
|
||||
toolCall: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
input: toolCall.input,
|
||||
},
|
||||
toolOutput: {
|
||||
output: toolOutput.output,
|
||||
isError: toolOutput.isError,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createCallbackManager(stream: StreamData) {
|
||||
const callbackManager = new CallbackManager();
|
||||
|
||||
callbackManager.on("retrieve", (data) => {
|
||||
const { nodes, query } = data.detail;
|
||||
appendEventData(stream, `Retrieving context for query: '${query}'`);
|
||||
appendEventData(
|
||||
stream,
|
||||
`Retrieved ${nodes.length} sources to use as context for the query`,
|
||||
);
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-call", (event) => {
|
||||
const { name, input } = event.detail.payload.toolCall;
|
||||
const inputString = Object.entries(input)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(", ");
|
||||
appendEventData(
|
||||
stream,
|
||||
`Using tool: '${name}' with inputs: '${inputString}'`,
|
||||
);
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-result", (event) => {
|
||||
const { toolCall, toolResult } = event.detail.payload;
|
||||
appendToolData(stream, toolCall, toolResult);
|
||||
});
|
||||
|
||||
return callbackManager;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "ai/react";
|
||||
import { useMemo } from "react";
|
||||
import { insertDataIntoMessages } from "./transform";
|
||||
import { ChatInput, ChatMessages } from "./ui/chat";
|
||||
|
||||
export default function ChatSection() {
|
||||
@@ -14,22 +12,22 @@ export default function ChatSection() {
|
||||
handleInputChange,
|
||||
reload,
|
||||
stop,
|
||||
data,
|
||||
} = useChat({
|
||||
api: process.env.NEXT_PUBLIC_CHAT_API,
|
||||
headers: {
|
||||
"Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
if (!(error instanceof Error)) throw error;
|
||||
const message = JSON.parse(error.message);
|
||||
alert(message.detail);
|
||||
},
|
||||
});
|
||||
|
||||
const transformedMessages = useMemo(() => {
|
||||
return insertDataIntoMessages(messages, data);
|
||||
}, [messages, data]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-5xl w-full">
|
||||
<ChatMessages
|
||||
messages={transformedMessages}
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
reload={reload}
|
||||
stop={stop}
|
||||
@@ -39,7 +37,7 @@ export default function ChatSection() {
|
||||
handleSubmit={handleSubmit}
|
||||
handleInputChange={handleInputChange}
|
||||
isLoading={isLoading}
|
||||
multiModal={process.env.NEXT_PUBLIC_MODEL === "gpt-4-turbo"}
|
||||
multiModal={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { JSONValue, Message } from "ai";
|
||||
|
||||
export const isValidMessageData = (rawData: JSONValue | undefined) => {
|
||||
if (!rawData || typeof rawData !== "object") return false;
|
||||
if (Object.keys(rawData).length === 0) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const insertDataIntoMessages = (
|
||||
messages: Message[],
|
||||
data: JSONValue[] | undefined,
|
||||
) => {
|
||||
if (!data) return messages;
|
||||
messages.forEach((message, i) => {
|
||||
const rawData = data[i];
|
||||
if (isValidMessageData(rawData)) message.data = rawData;
|
||||
});
|
||||
return messages;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../collapsible";
|
||||
import { EventData } from "./index";
|
||||
|
||||
export function ChatEvents({
|
||||
data,
|
||||
isLoading,
|
||||
}: {
|
||||
data: EventData[];
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const buttonLabel = isOpen ? "Hide events" : "Show events";
|
||||
|
||||
const EventIcon = isOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-l-2 border-indigo-400 pl-2">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="secondary" className="space-x-2">
|
||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
<span>{buttonLabel}</span>
|
||||
{EventIcon}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent asChild>
|
||||
<div className="mt-4 text-sm space-y-2">
|
||||
{data.map((eventItem, index) => (
|
||||
<div key={index}>{eventItem.title}</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Image from "next/image";
|
||||
import { type ImageData } from "./index";
|
||||
|
||||
export function ChatImage({ data }: { data: ImageData }) {
|
||||
return (
|
||||
<div className="rounded-md max-w-[200px] shadow-md">
|
||||
<Image
|
||||
src={data.url}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
style={{ width: "100%", height: "auto" }}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +1,114 @@
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
import { JSONValue, Message } from "ai";
|
||||
import Image from "next/image";
|
||||
import { Message } from "ai";
|
||||
import { Fragment } from "react";
|
||||
import { Button } from "../button";
|
||||
import ChatAvatar from "./chat-avatar";
|
||||
import { ChatEvents } from "./chat-events";
|
||||
import { ChatImage } from "./chat-image";
|
||||
import { ChatSources } from "./chat-sources";
|
||||
import ChatTools from "./chat-tools";
|
||||
import {
|
||||
AnnotationData,
|
||||
EventData,
|
||||
ImageData,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
SourceData,
|
||||
ToolData,
|
||||
} from "./index";
|
||||
import Markdown from "./markdown";
|
||||
import { useCopyToClipboard } from "./use-copy-to-clipboard";
|
||||
|
||||
interface ChatMessageImageData {
|
||||
type: "image_url";
|
||||
image_url: {
|
||||
url: string;
|
||||
};
|
||||
type ContentDisplayConfig = {
|
||||
order: number;
|
||||
component: JSX.Element | null;
|
||||
};
|
||||
|
||||
function getAnnotationData<T extends AnnotationData>(
|
||||
annotations: MessageAnnotation[],
|
||||
type: MessageAnnotationType,
|
||||
): T[] {
|
||||
return annotations.filter((a) => a.type === type).map((a) => a.data as T);
|
||||
}
|
||||
|
||||
// This component will parse message data and render the appropriate UI.
|
||||
function ChatMessageData({ messageData }: { messageData: JSONValue }) {
|
||||
const { image_url, type } = messageData as unknown as ChatMessageImageData;
|
||||
if (type === "image_url") {
|
||||
return (
|
||||
<div className="rounded-md max-w-[200px] shadow-md">
|
||||
<Image
|
||||
src={image_url.url}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
style={{ width: "100%", height: "auto" }}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
function ChatMessageContent({
|
||||
message,
|
||||
isLoading,
|
||||
}: {
|
||||
message: Message;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const annotations = message.annotations as MessageAnnotation[] | undefined;
|
||||
if (!annotations?.length) return <Markdown content={message.content} />;
|
||||
|
||||
const imageData = getAnnotationData<ImageData>(
|
||||
annotations,
|
||||
MessageAnnotationType.IMAGE,
|
||||
);
|
||||
const eventData = getAnnotationData<EventData>(
|
||||
annotations,
|
||||
MessageAnnotationType.EVENTS,
|
||||
);
|
||||
const sourceData = getAnnotationData<SourceData>(
|
||||
annotations,
|
||||
MessageAnnotationType.SOURCES,
|
||||
);
|
||||
const toolData = getAnnotationData<ToolData>(
|
||||
annotations,
|
||||
MessageAnnotationType.TOOLS,
|
||||
);
|
||||
|
||||
const contents: ContentDisplayConfig[] = [
|
||||
{
|
||||
order: -3,
|
||||
component: imageData[0] ? <ChatImage data={imageData[0]} /> : null,
|
||||
},
|
||||
{
|
||||
order: -2,
|
||||
component:
|
||||
eventData.length > 0 ? (
|
||||
<ChatEvents isLoading={isLoading} data={eventData} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
order: -1,
|
||||
component: toolData[0] ? <ChatTools data={toolData[0]} /> : null,
|
||||
},
|
||||
{
|
||||
order: 0,
|
||||
component: <Markdown content={message.content} />,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
component: sourceData[0] ? <ChatSources data={sourceData[0]} /> : null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex-1 gap-4 flex flex-col">
|
||||
{contents
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((content, index) => (
|
||||
<Fragment key={index}>{content.component}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChatMessage(chatMessage: Message) {
|
||||
export default function ChatMessage({
|
||||
chatMessage,
|
||||
isLoading,
|
||||
}: {
|
||||
chatMessage: Message;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
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 space-y-4">
|
||||
{chatMessage.data && (
|
||||
<ChatMessageData messageData={chatMessage.data} />
|
||||
)}
|
||||
<Markdown content={chatMessage.content} />
|
||||
</div>
|
||||
<ChatMessageContent message={chatMessage} isLoading={isLoading} />
|
||||
<Button
|
||||
onClick={() => copyToClipboard(chatMessage.content)}
|
||||
size="icon"
|
||||
|
||||
@@ -40,9 +40,16 @@ export default function ChatMessages(
|
||||
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} />
|
||||
))}
|
||||
{props.messages.map((m, i) => {
|
||||
const isLoadingMessage = i === messageLength - 1 && props.isLoading;
|
||||
return (
|
||||
<ChatMessage
|
||||
key={m.id}
|
||||
chatMessage={m}
|
||||
isLoading={isLoadingMessage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isPending && (
|
||||
<div className="flex justify-center items-center pt-10">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ArrowUpRightSquare, Check, Copy } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "../button";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
|
||||
import { SourceData, SourceNode } from "./index";
|
||||
import { useCopyToClipboard } from "./use-copy-to-clipboard";
|
||||
|
||||
const SCORE_THRESHOLD = 0.5;
|
||||
|
||||
export function ChatSources({ data }: { data: SourceData }) {
|
||||
const sources = useMemo(() => {
|
||||
return (
|
||||
data.nodes
|
||||
?.filter((node) => Object.keys(node.metadata).length > 0)
|
||||
?.filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
|
||||
.sort((a, b) => (b.score ?? 1) - (a.score ?? 1)) || []
|
||||
);
|
||||
}, [data.nodes]);
|
||||
|
||||
if (sources.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-x-2 text-sm">
|
||||
<span className="font-semibold">Sources:</span>
|
||||
<div className="inline-flex gap-1 items-center">
|
||||
{sources.map((node: SourceNode, index: number) => (
|
||||
<div key={node.id}>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<div className="text-xs w-5 h-5 rounded-full bg-gray-100 mb-2 flex items-center justify-center hover:text-white hover:bg-primary hover:cursor-pointer">
|
||||
{index + 1}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<NodeInfo node={node} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeInfo({ node }: { node: SourceNode }) {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
|
||||
|
||||
if (typeof node.metadata["URL"] === "string") {
|
||||
// this is a node generated by the web loader, it contains an external URL
|
||||
// add a link to view this URL
|
||||
return (
|
||||
<a
|
||||
className="space-x-2 flex items-center my-2 hover:text-blue-900"
|
||||
href={node.metadata["URL"]}
|
||||
target="_blank"
|
||||
>
|
||||
<span>{node.metadata["URL"]}</span>
|
||||
<ArrowUpRightSquare className="w-4 h-4" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof node.metadata["file_path"] === "string") {
|
||||
// this is a node generated by the file loader, it contains file path
|
||||
// add a button to copy the path to the clipboard
|
||||
const filePath = node.metadata["file_path"];
|
||||
return (
|
||||
<div className="flex items-center px-2 py-1 justify-between my-2">
|
||||
<span>{filePath}</span>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(filePath)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-12 w-12"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// node generated by unknown loader, implement renderer by analyzing logged out metadata
|
||||
console.log("Node metadata", node.metadata);
|
||||
return (
|
||||
<p>
|
||||
Sorry, unknown node type. Please add a new renderer in the NodeInfo
|
||||
component.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ToolData } from "./index";
|
||||
import { WeatherCard, WeatherData } from "./widgets/WeatherCard";
|
||||
|
||||
// TODO: If needed, add displaying more tool outputs here
|
||||
export default function ChatTools({ data }: { data: ToolData }) {
|
||||
if (!data) return null;
|
||||
const { toolCall, toolOutput } = data;
|
||||
|
||||
if (toolOutput.isError) {
|
||||
return (
|
||||
<div className="border-l-2 border-red-400 pl-2">
|
||||
There was an error when calling the tool {toolCall.name} with input:{" "}
|
||||
<br />
|
||||
{JSON.stringify(toolCall.input)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (toolCall.name) {
|
||||
case "get_weather_information":
|
||||
const weatherData = toolOutput.output as unknown as WeatherData;
|
||||
return <WeatherCard data={weatherData} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,54 @@
|
||||
import { JSONValue } from "ai";
|
||||
import ChatInput from "./chat-input";
|
||||
import ChatMessages from "./chat-messages";
|
||||
|
||||
export { type ChatHandler } from "./chat.interface";
|
||||
export { ChatInput, ChatMessages };
|
||||
|
||||
export enum MessageAnnotationType {
|
||||
IMAGE = "image",
|
||||
SOURCES = "sources",
|
||||
EVENTS = "events",
|
||||
TOOLS = "tools",
|
||||
}
|
||||
|
||||
export type ImageData = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type SourceNode = {
|
||||
id: string;
|
||||
metadata: Record<string, unknown>;
|
||||
score?: number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SourceData = {
|
||||
nodes: SourceNode[];
|
||||
};
|
||||
|
||||
export type EventData = {
|
||||
title: string;
|
||||
isCollapsed: boolean;
|
||||
};
|
||||
|
||||
export type ToolData = {
|
||||
toolCall: {
|
||||
id: string;
|
||||
name: string;
|
||||
input: {
|
||||
[key: string]: JSONValue;
|
||||
};
|
||||
};
|
||||
toolOutput: {
|
||||
output: JSONValue;
|
||||
isError: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type AnnotationData = ImageData | SourceData | EventData | ToolData;
|
||||
|
||||
export type MessageAnnotation = {
|
||||
type: MessageAnnotationType;
|
||||
data: AnnotationData;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
export interface WeatherData {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
generationtime_ms: number;
|
||||
utc_offset_seconds: number;
|
||||
timezone: string;
|
||||
timezone_abbreviation: string;
|
||||
elevation: number;
|
||||
current_units: {
|
||||
time: string;
|
||||
interval: string;
|
||||
temperature_2m: string;
|
||||
weather_code: string;
|
||||
};
|
||||
current: {
|
||||
time: string;
|
||||
interval: number;
|
||||
temperature_2m: number;
|
||||
weather_code: number;
|
||||
};
|
||||
hourly_units: {
|
||||
time: string;
|
||||
temperature_2m: string;
|
||||
weather_code: string;
|
||||
};
|
||||
hourly: {
|
||||
time: string[];
|
||||
temperature_2m: number[];
|
||||
weather_code: number[];
|
||||
};
|
||||
daily_units: {
|
||||
time: string;
|
||||
weather_code: string;
|
||||
};
|
||||
daily: {
|
||||
time: string[];
|
||||
weather_code: number[];
|
||||
};
|
||||
}
|
||||
|
||||
// Follow WMO Weather interpretation codes (WW)
|
||||
const weatherCodeDisplayMap: Record<
|
||||
string,
|
||||
{
|
||||
icon: JSX.Element;
|
||||
status: string;
|
||||
}
|
||||
> = {
|
||||
"0": {
|
||||
icon: <span>☀️</span>,
|
||||
status: "Clear sky",
|
||||
},
|
||||
"1": {
|
||||
icon: <span>🌤️</span>,
|
||||
status: "Mainly clear",
|
||||
},
|
||||
"2": {
|
||||
icon: <span>☁️</span>,
|
||||
status: "Partly cloudy",
|
||||
},
|
||||
"3": {
|
||||
icon: <span>☁️</span>,
|
||||
status: "Overcast",
|
||||
},
|
||||
"45": {
|
||||
icon: <span>🌫️</span>,
|
||||
status: "Fog",
|
||||
},
|
||||
"48": {
|
||||
icon: <span>🌫️</span>,
|
||||
status: "Depositing rime fog",
|
||||
},
|
||||
"51": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Drizzle",
|
||||
},
|
||||
"53": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Drizzle",
|
||||
},
|
||||
"55": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Drizzle",
|
||||
},
|
||||
"56": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Drizzle",
|
||||
},
|
||||
"57": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Drizzle",
|
||||
},
|
||||
"61": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain",
|
||||
},
|
||||
"63": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain",
|
||||
},
|
||||
"65": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain",
|
||||
},
|
||||
"66": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Rain",
|
||||
},
|
||||
"67": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Rain",
|
||||
},
|
||||
"71": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow fall",
|
||||
},
|
||||
"73": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow fall",
|
||||
},
|
||||
"75": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow fall",
|
||||
},
|
||||
"77": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow grains",
|
||||
},
|
||||
"80": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain showers",
|
||||
},
|
||||
"81": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain showers",
|
||||
},
|
||||
"82": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain showers",
|
||||
},
|
||||
"85": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow showers",
|
||||
},
|
||||
"86": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow showers",
|
||||
},
|
||||
"95": {
|
||||
icon: <span>⛈️</span>,
|
||||
status: "Thunderstorm",
|
||||
},
|
||||
"96": {
|
||||
icon: <span>⛈️</span>,
|
||||
status: "Thunderstorm",
|
||||
},
|
||||
"99": {
|
||||
icon: <span>⛈️</span>,
|
||||
status: "Thunderstorm",
|
||||
},
|
||||
};
|
||||
|
||||
const displayDay = (time: string) => {
|
||||
return new Date(time).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
});
|
||||
};
|
||||
|
||||
export function WeatherCard({ data }: { data: WeatherData }) {
|
||||
const currentDayString = new Date(data.current.time).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-[#61B9F2] rounded-2xl shadow-xl p-5 space-y-4 text-white w-fit">
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xl">{currentDayString}</div>
|
||||
<div className="text-5xl font-semibold flex gap-4">
|
||||
<span>
|
||||
{data.current.temperature_2m} {data.current_units.temperature_2m}
|
||||
</span>
|
||||
{weatherCodeDisplayMap[data.current.weather_code].icon}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl">
|
||||
{weatherCodeDisplayMap[data.current.weather_code].status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="gap-2 grid grid-cols-6">
|
||||
{data.daily.time.map((time, index) => {
|
||||
if (index === 0) return null; // skip the current day
|
||||
return (
|
||||
<div key={time} className="flex flex-col items-center gap-4">
|
||||
<span>{displayDay(time)}</span>
|
||||
<div className="text-4xl">
|
||||
{weatherCodeDisplayMap[data.daily.weather_code[index]].icon}
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{weatherCodeDisplayMap[data.daily.weather_code[index]].status}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleContent, CollapsibleTrigger };
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
||||
@@ -2,6 +2,7 @@
|
||||
"experimental": {
|
||||
"outputFileTracingIncludes": {
|
||||
"/*": ["./cache/**/*"]
|
||||
}
|
||||
},
|
||||
"serverComponentsExternalPackages": ["sharp", "onnxruntime-node"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"ai": "^2.2.27",
|
||||
"ai": "^3.0.21",
|
||||
"ajv": "^8.12.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^1.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"llamaindex": "latest",
|
||||
"llamaindex": "0.3.9",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.0.3",
|
||||
"pdf2json": "3.0.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
@@ -33,17 +37,17 @@
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.3.2",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"cross-env": "^7.0.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"ts-node": "^10.9.2"
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,5 @@
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// webpack config must be a function in NextJS that is used to patch the default webpack config provided by NextJS, see https://nextjs.org/docs/pages/api-reference/next-config-js/webpack
|
||||
export default function webpack(config) {
|
||||
// See https://webpack.js.org/configuration/resolve/#resolvealias
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
sharp$: false,
|
||||
"onnxruntime-node$": false,
|
||||
config.resolve.fallback = {
|
||||
aws4: false,
|
||||
};
|
||||
|
||||
// Following lines will fix issues with onnxruntime-node when using pnpm
|
||||
// See: https://github.com/vercel/next.js/issues/43433
|
||||
config.externals.push({
|
||||
"onnxruntime-node": "commonjs onnxruntime-node",
|
||||
sharp: "commonjs sharp",
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export default function webpack(config, isServer) {
|
||||
// See https://webpack.js.org/configuration/resolve/#resolvealias
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
sharp$: false,
|
||||
"onnxruntime-node$": false,
|
||||
config.resolve.fallback = {
|
||||
aws4: false,
|
||||
};
|
||||
config.module.rules.push({
|
||||
test: /\.node$/,
|
||||
|
||||
Reference in New Issue
Block a user