mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-03 08:24:39 -04:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a2c48fd50 | |||
| 508eebe39c | |||
| f9a4f7644a | |||
| 34f95bfa29 | |||
| 1148ddba53 | |||
| 9e945ed355 | |||
| 6342163df2 | |||
| a42fa53a6b | |||
| 099f626586 | |||
| 956538eeb0 | |||
| 555f6b2905 | |||
| d8bc271a21 | |||
| f29561cde2 | |||
| 442abae8ac | |||
| 0ad2207684 | |||
| bfde30deed | |||
| 96fdb83abf | |||
| b7e0072c9c | |||
| 81bc340dda | |||
| ddf3aef7dc | |||
| 1f5a26f3a8 | |||
| 48188ca3f9 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add CSV upload
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Add OpenAPI action tool
|
||||
@@ -276,6 +276,12 @@ const getEngineEnvs = (): EnvVar[] => {
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
value: "3",
|
||||
},
|
||||
{
|
||||
name: "STREAM_TIMEOUT",
|
||||
description:
|
||||
"The time in milliseconds to wait for the stream to return a response.",
|
||||
value: "60000",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -144,7 +144,7 @@ const getAdditionalDependencies = (
|
||||
case "openai":
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: "0.2.2",
|
||||
version: "0.2.6",
|
||||
});
|
||||
break;
|
||||
case "anthropic":
|
||||
@@ -160,7 +160,7 @@ const getAdditionalDependencies = (
|
||||
case "gemini":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-gemini",
|
||||
version: "0.1.7",
|
||||
version: "0.1.10",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-gemini",
|
||||
|
||||
+31
-1
@@ -117,6 +117,37 @@ export const supportedTools: Tool[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "OpenAPI action",
|
||||
name: "openapi_action.OpenAPIActionToolSpec",
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-openapi",
|
||||
version: "0.1.3",
|
||||
},
|
||||
{
|
||||
name: "jsonschema",
|
||||
version: "^4.22.0",
|
||||
},
|
||||
{
|
||||
name: "llama-index-tools-requests",
|
||||
version: "0.1.3",
|
||||
},
|
||||
],
|
||||
config: {
|
||||
openapi_uri: "The URL or file path of the OpenAPI schema",
|
||||
},
|
||||
supportedFrameworks: ["fastapi"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for openapi action tool.",
|
||||
value:
|
||||
"You are an OpenAPI action agent. You help users to make requests to the provided OpenAPI schema.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getTool = (toolName: string): Tool | undefined => {
|
||||
@@ -159,7 +190,6 @@ export const writeToolsConfig = async (
|
||||
tools: Tool[] = [],
|
||||
type: ConfigFileType = ConfigFileType.YAML,
|
||||
) => {
|
||||
if (tools.length === 0) return; // no tools selected, no config need
|
||||
const configContent: {
|
||||
[key in ToolType]: Record<string, any>;
|
||||
} = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import yaml
|
||||
import json
|
||||
import importlib
|
||||
|
||||
from cachetools import cached, LRUCache
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
@@ -19,6 +20,14 @@ class ToolFactory:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@cached(
|
||||
LRUCache(maxsize=100),
|
||||
key=lambda tool_type, tool_name, config: (
|
||||
tool_type,
|
||||
tool_name,
|
||||
json.dumps(config, sort_keys=True),
|
||||
),
|
||||
)
|
||||
def load_tools(tool_type: str, tool_name: str, config: dict) -> list[FunctionTool]:
|
||||
source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type]
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from typing import Dict, List, Tuple
|
||||
from llama_index.tools.openapi import OpenAPIToolSpec
|
||||
from llama_index.tools.requests import RequestsToolSpec
|
||||
|
||||
|
||||
class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec):
|
||||
"""
|
||||
A combination of OpenAPI and Requests tool specs that can parse OpenAPI specs and make requests.
|
||||
|
||||
openapi_uri: str: The file path or URL to the OpenAPI spec.
|
||||
domain_headers: dict: Whitelist domains and the headers to use.
|
||||
"""
|
||||
|
||||
spec_functions = OpenAPIToolSpec.spec_functions + RequestsToolSpec.spec_functions
|
||||
|
||||
def __init__(self, openapi_uri: str, domain_headers: dict = {}, **kwargs):
|
||||
# Load the OpenAPI spec
|
||||
openapi_spec, servers = self.load_openapi_spec(openapi_uri)
|
||||
|
||||
# Add the servers to the domain headers if they are not already present
|
||||
for server in servers:
|
||||
if server not in domain_headers:
|
||||
domain_headers[server] = {}
|
||||
|
||||
OpenAPIToolSpec.__init__(self, spec=openapi_spec)
|
||||
RequestsToolSpec.__init__(self, domain_headers)
|
||||
|
||||
@staticmethod
|
||||
def load_openapi_spec(uri: str) -> Tuple[Dict, List[str]]:
|
||||
"""
|
||||
Load an OpenAPI spec from a URI.
|
||||
|
||||
Args:
|
||||
uri (str): A file path or URL to the OpenAPI spec.
|
||||
|
||||
Returns:
|
||||
List[Document]: A list of Document objects.
|
||||
"""
|
||||
import yaml
|
||||
from urllib.parse import urlparse
|
||||
|
||||
if uri.startswith("http"):
|
||||
import requests
|
||||
|
||||
response = requests.get(uri)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: "
|
||||
f"Failed to load OpenAPI spec from {uri}, status code: {response.status_code}"
|
||||
)
|
||||
spec = yaml.safe_load(response.text)
|
||||
elif uri.startswith("file"):
|
||||
filepath = urlparse(uri).path
|
||||
with open(filepath, "r") as file:
|
||||
spec = yaml.safe_load(file)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI URI provided. "
|
||||
"Only HTTP and file path are supported."
|
||||
)
|
||||
# Add the servers to the whitelist
|
||||
try:
|
||||
servers = [
|
||||
urlparse(server["url"]).netloc for server in spec.get("servers", [])
|
||||
]
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI spec provided. "
|
||||
"Could not get `servers` from the spec."
|
||||
) from e
|
||||
return spec, servers
|
||||
@@ -15,7 +15,7 @@ export type InterpreterToolParams = {
|
||||
fileServerURLPrefix?: string;
|
||||
};
|
||||
|
||||
export type InterpreterToolOuput = {
|
||||
export type InterpreterToolOutput = {
|
||||
isError: boolean;
|
||||
logs: Logs;
|
||||
extraResult: InterpreterExtraResult[];
|
||||
@@ -88,7 +88,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
return this.codeInterpreter;
|
||||
}
|
||||
|
||||
public async codeInterpret(code: string): Promise<InterpreterToolOuput> {
|
||||
public async codeInterpret(code: string): Promise<InterpreterToolOutput> {
|
||||
console.log(
|
||||
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
|
||||
);
|
||||
@@ -96,7 +96,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
const exec = await interpreter.notebook.execCell(code);
|
||||
if (exec.error) console.error("[Code Interpreter error]", exec.error);
|
||||
const extraResult = await this.getExtraResult(exec.results[0]);
|
||||
const result: InterpreterToolOuput = {
|
||||
const result: InterpreterToolOutput = {
|
||||
isError: !!exec.error,
|
||||
logs: exec.logs,
|
||||
extraResult,
|
||||
@@ -104,7 +104,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async call(input: InterpreterParameter): Promise<InterpreterToolOuput> {
|
||||
async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
|
||||
const result = await this.codeInterpret(input.code);
|
||||
await this.codeInterpreter?.close();
|
||||
return result;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Message } from "./chat-messages";
|
||||
|
||||
export interface ChatInputProps {
|
||||
/** The current value of the input */
|
||||
input?: string;
|
||||
@@ -12,7 +14,7 @@ export interface ChatInputProps {
|
||||
/** Form submission handler to automatically reset input and append a user message */
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
isLoading: boolean;
|
||||
multiModal?: boolean;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export default function ChatInput(props: ChatInputProps) {
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Message, StreamData, streamToResponse } from "ai";
|
||||
import { Request, Response } from "express";
|
||||
import { ChatMessage, MessageContent, Settings } from "llamaindex";
|
||||
import { ChatMessage, Settings } from "llamaindex";
|
||||
import { createChatEngine } from "./engine/chat";
|
||||
import { LlamaIndexStream } from "./llamaindex-stream";
|
||||
import { createCallbackManager } from "./stream-helper";
|
||||
|
||||
const convertMessageContent = (
|
||||
textMessage: string,
|
||||
imageUrl: string | undefined,
|
||||
): MessageContent => {
|
||||
if (!imageUrl) return textMessage;
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: textMessage,
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: imageUrl,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
import {
|
||||
DataParserOptions,
|
||||
LlamaIndexStream,
|
||||
convertMessageContent,
|
||||
} from "./llamaindex-stream";
|
||||
import { createCallbackManager, createStreamTimeout } from "./stream-helper";
|
||||
|
||||
export const chat = async (req: Request, res: Response) => {
|
||||
// Init Vercel AI StreamData and timeout
|
||||
const vercelStreamData = new StreamData();
|
||||
const streamTimeout = createStreamTimeout(vercelStreamData);
|
||||
try {
|
||||
const { messages, data }: { messages: Message[]; data: any } = req.body;
|
||||
const {
|
||||
messages,
|
||||
data,
|
||||
}: { messages: Message[]; data: DataParserOptions | undefined } = req.body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return res.status(400).json({
|
||||
@@ -38,13 +29,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
const chatEngine = await createChatEngine();
|
||||
|
||||
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
|
||||
const userMessageContent = convertMessageContent(
|
||||
userMessage.content,
|
||||
data?.imageUrl,
|
||||
);
|
||||
|
||||
// Init Vercel AI StreamData
|
||||
const vercelStreamData = new StreamData();
|
||||
const userMessageContent = convertMessageContent(userMessage.content, data);
|
||||
|
||||
// Setup callbacks
|
||||
const callbackManager = createCallbackManager(vercelStreamData);
|
||||
@@ -61,7 +46,8 @@ export const chat = async (req: Request, res: Response) => {
|
||||
// Return a stream, which can be consumed by the Vercel/AI client
|
||||
const stream = LlamaIndexStream(response, vercelStreamData, {
|
||||
parserOptions: {
|
||||
image_url: data?.imageUrl,
|
||||
imageUrl: data?.imageUrl,
|
||||
csvFiles: data?.csvFiles,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -71,5 +57,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
return res.status(500).json({
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(streamTimeout);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import {
|
||||
MessageContent,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Response,
|
||||
@@ -13,20 +14,61 @@ import {
|
||||
} from "llamaindex";
|
||||
|
||||
import { AgentStreamChatResponse } from "llamaindex/agent/base";
|
||||
import { appendImageData, appendSourceData } from "./stream-helper";
|
||||
import {
|
||||
CsvFile,
|
||||
appendCsvData,
|
||||
appendImageData,
|
||||
appendSourceData,
|
||||
} from "./stream-helper";
|
||||
|
||||
type LlamaIndexResponse =
|
||||
| AgentStreamChatResponse<ToolCallLLMMessageOptions>
|
||||
| Response;
|
||||
|
||||
type ParserOptions = {
|
||||
image_url?: string;
|
||||
export type DataParserOptions = {
|
||||
imageUrl?: string;
|
||||
csvFiles?: CsvFile[];
|
||||
};
|
||||
|
||||
export const convertMessageContent = (
|
||||
textMessage: string,
|
||||
additionalData?: DataParserOptions,
|
||||
): MessageContent => {
|
||||
if (!additionalData) return textMessage;
|
||||
const content: MessageContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: textMessage,
|
||||
},
|
||||
];
|
||||
if (additionalData?.imageUrl) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: additionalData?.imageUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalData?.csvFiles?.length) {
|
||||
const rawContents = additionalData.csvFiles.map((csv) => {
|
||||
return "```csv\n" + csv.content + "\n```";
|
||||
});
|
||||
const csvContent =
|
||||
"Use data from following CSV raw contents:\n" + rawContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `${csvContent}\n\n${textMessage}`,
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
function createParser(
|
||||
res: AsyncIterable<LlamaIndexResponse>,
|
||||
data: StreamData,
|
||||
opts?: ParserOptions,
|
||||
opts?: DataParserOptions,
|
||||
) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
@@ -34,7 +76,8 @@ function createParser(
|
||||
let sourceNodes: NodeWithScore<Metadata>[] | undefined;
|
||||
return new ReadableStream<string>({
|
||||
start() {
|
||||
appendImageData(data, opts?.image_url);
|
||||
appendImageData(data, opts?.imageUrl);
|
||||
appendCsvData(data, opts?.csvFiles);
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
@@ -72,7 +115,7 @@ export function LlamaIndexStream(
|
||||
data: StreamData,
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
parserOptions?: ParserOptions;
|
||||
parserOptions?: DataParserOptions;
|
||||
},
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data, opts?.parserOptions)
|
||||
|
||||
@@ -17,6 +17,22 @@ export function appendImageData(data: StreamData, imageUrl?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeUrl(metadata: Metadata) {
|
||||
const url = metadata["URL"];
|
||||
if (url) return url;
|
||||
const fileName = metadata["file_name"];
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
console.warn(
|
||||
"FILESERVER_URL_PREFIX is not set. File URLs will not be generated.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (fileName) {
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/data/${fileName}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
sourceNodes?: NodeWithScore<Metadata>[],
|
||||
@@ -29,6 +45,7 @@ export function appendSourceData(
|
||||
...node.node.toMutableJSON(),
|
||||
id: node.node.id_,
|
||||
score: node.score ?? null,
|
||||
url: getNodeUrl(node.node.metadata),
|
||||
})),
|
||||
},
|
||||
});
|
||||
@@ -65,6 +82,15 @@ export function appendToolData(
|
||||
});
|
||||
}
|
||||
|
||||
export function createStreamTimeout(stream: StreamData) {
|
||||
const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
|
||||
const t = setTimeout(() => {
|
||||
appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
|
||||
stream.close();
|
||||
}, timeout);
|
||||
return t;
|
||||
}
|
||||
|
||||
export function createCallbackManager(stream: StreamData) {
|
||||
const callbackManager = new CallbackManager();
|
||||
|
||||
@@ -95,3 +121,20 @@ export function createCallbackManager(stream: StreamData) {
|
||||
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function appendCsvData(data: StreamData, csvFiles?: CsvFile[]) {
|
||||
if (!csvFiles) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "csv",
|
||||
data: {
|
||||
csvFiles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
import logging
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Any, Optional, Dict, Tuple
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@@ -11,6 +13,7 @@ from aiostream import stream
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
class _Message(BaseModel):
|
||||
role: MessageRole
|
||||
@@ -38,14 +41,27 @@ class _SourceNodes(BaseModel):
|
||||
metadata: Dict[str, Any]
|
||||
score: Optional[float]
|
||||
text: str
|
||||
url: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def from_source_node(cls, source_node: NodeWithScore):
|
||||
metadata = source_node.node.metadata
|
||||
url = metadata.get("URL")
|
||||
|
||||
if not url:
|
||||
file_name = metadata.get("file_name")
|
||||
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if not url_prefix:
|
||||
logger.warning("Warning: FILESERVER_URL_PREFIX not set in environment variables")
|
||||
if file_name and url_prefix:
|
||||
url = f"{url_prefix}/data/{file_name}"
|
||||
|
||||
return cls(
|
||||
id=source_node.node.node_id,
|
||||
metadata=source_node.node.metadata,
|
||||
metadata=metadata,
|
||||
score=source_node.score,
|
||||
text=source_node.node.text, # type: ignore
|
||||
url=url
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -14,8 +14,8 @@ 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 = "0.10.41"
|
||||
llama-index-core = "0.10.41"
|
||||
cachetools = "^5.3.3"
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import {
|
||||
MessageContent,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Response,
|
||||
@@ -13,20 +14,61 @@ import {
|
||||
} from "llamaindex";
|
||||
|
||||
import { AgentStreamChatResponse } from "llamaindex/agent/base";
|
||||
import { appendImageData, appendSourceData } from "./stream-helper";
|
||||
import {
|
||||
CsvFile,
|
||||
appendCsvData,
|
||||
appendImageData,
|
||||
appendSourceData,
|
||||
} from "./stream-helper";
|
||||
|
||||
type LlamaIndexResponse =
|
||||
| AgentStreamChatResponse<ToolCallLLMMessageOptions>
|
||||
| Response;
|
||||
|
||||
type ParserOptions = {
|
||||
image_url?: string;
|
||||
export type DataParserOptions = {
|
||||
imageUrl?: string;
|
||||
csvFiles?: CsvFile[];
|
||||
};
|
||||
|
||||
export const convertMessageContent = (
|
||||
textMessage: string,
|
||||
additionalData?: DataParserOptions,
|
||||
): MessageContent => {
|
||||
if (!additionalData) return textMessage;
|
||||
const content: MessageContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: textMessage,
|
||||
},
|
||||
];
|
||||
if (additionalData?.imageUrl) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: additionalData?.imageUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalData?.csvFiles?.length) {
|
||||
const rawContents = additionalData.csvFiles.map((csv) => {
|
||||
return "```csv\n" + csv.content + "\n```";
|
||||
});
|
||||
const csvContent =
|
||||
"Use data from following CSV raw contents:\n" + rawContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `${csvContent}\n\n${textMessage}`,
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
function createParser(
|
||||
res: AsyncIterable<LlamaIndexResponse>,
|
||||
data: StreamData,
|
||||
opts?: ParserOptions,
|
||||
opts?: DataParserOptions,
|
||||
) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
@@ -34,7 +76,8 @@ function createParser(
|
||||
let sourceNodes: NodeWithScore<Metadata>[] | undefined;
|
||||
return new ReadableStream<string>({
|
||||
start() {
|
||||
appendImageData(data, opts?.image_url);
|
||||
appendImageData(data, opts?.imageUrl);
|
||||
appendCsvData(data, opts?.csvFiles);
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
@@ -72,7 +115,7 @@ export function LlamaIndexStream(
|
||||
data: StreamData,
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
parserOptions?: ParserOptions;
|
||||
parserOptions?: DataParserOptions;
|
||||
},
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data, opts?.parserOptions)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { initObservability } from "@/app/observability";
|
||||
import { Message, StreamData, StreamingTextResponse } from "ai";
|
||||
import { ChatMessage, MessageContent, Settings } from "llamaindex";
|
||||
import { ChatMessage, 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";
|
||||
import {
|
||||
DataParserOptions,
|
||||
LlamaIndexStream,
|
||||
convertMessageContent,
|
||||
} from "./llamaindex-stream";
|
||||
import { createCallbackManager, createStreamTimeout } from "./stream-helper";
|
||||
|
||||
initObservability();
|
||||
initSettings();
|
||||
@@ -13,29 +17,17 @@ initSettings();
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const convertMessageContent = (
|
||||
textMessage: string,
|
||||
imageUrl: string | undefined,
|
||||
): MessageContent => {
|
||||
if (!imageUrl) return textMessage;
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: textMessage,
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: imageUrl,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Init Vercel AI StreamData and timeout
|
||||
const vercelStreamData = new StreamData();
|
||||
const streamTimeout = createStreamTimeout(vercelStreamData);
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages, data }: { messages: Message[]; data: any } = body;
|
||||
const {
|
||||
messages,
|
||||
data,
|
||||
}: { messages: Message[]; data: DataParserOptions | undefined } = body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return NextResponse.json(
|
||||
@@ -50,13 +42,7 @@ export async function POST(request: NextRequest) {
|
||||
const chatEngine = await createChatEngine();
|
||||
|
||||
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
|
||||
const userMessageContent = convertMessageContent(
|
||||
userMessage.content,
|
||||
data?.imageUrl,
|
||||
);
|
||||
|
||||
// Init Vercel AI StreamData
|
||||
const vercelStreamData = new StreamData();
|
||||
const userMessageContent = convertMessageContent(userMessage.content, data);
|
||||
|
||||
// Setup callbacks
|
||||
const callbackManager = createCallbackManager(vercelStreamData);
|
||||
@@ -73,7 +59,8 @@ export async function POST(request: NextRequest) {
|
||||
// Transform LlamaIndex stream to Vercel/AI format
|
||||
const stream = LlamaIndexStream(response, vercelStreamData, {
|
||||
parserOptions: {
|
||||
image_url: data?.imageUrl,
|
||||
imageUrl: data?.imageUrl,
|
||||
csvFiles: data?.csvFiles,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,5 +76,7 @@ export async function POST(request: NextRequest) {
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(streamTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,22 @@ export function appendImageData(data: StreamData, imageUrl?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeUrl(metadata: Metadata) {
|
||||
const url = metadata["URL"];
|
||||
if (url) return url;
|
||||
const fileName = metadata["file_name"];
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
console.warn(
|
||||
"FILESERVER_URL_PREFIX is not set. File URLs will not be generated.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (fileName) {
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/data/${fileName}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
sourceNodes?: NodeWithScore<Metadata>[],
|
||||
@@ -29,6 +45,7 @@ export function appendSourceData(
|
||||
...node.node.toMutableJSON(),
|
||||
id: node.node.id_,
|
||||
score: node.score ?? null,
|
||||
url: getNodeUrl(node.node.metadata),
|
||||
})),
|
||||
},
|
||||
});
|
||||
@@ -65,6 +82,15 @@ export function appendToolData(
|
||||
});
|
||||
}
|
||||
|
||||
export function createStreamTimeout(stream: StreamData) {
|
||||
const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
|
||||
const t = setTimeout(() => {
|
||||
appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
|
||||
stream.close();
|
||||
}, timeout);
|
||||
return t;
|
||||
}
|
||||
|
||||
export function createCallbackManager(stream: StreamData) {
|
||||
const callbackManager = new CallbackManager();
|
||||
|
||||
@@ -95,3 +121,20 @@ export function createCallbackManager(stream: StreamData) {
|
||||
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function appendCsvData(data: StreamData, csvFiles?: CsvFile[]) {
|
||||
if (!csvFiles) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "csv",
|
||||
data: {
|
||||
csvFiles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function ChatSection() {
|
||||
handleSubmit={handleSubmit}
|
||||
handleInputChange={handleInputChange}
|
||||
isLoading={isLoading}
|
||||
multiModal={true}
|
||||
messages={messages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Button } from "../button";
|
||||
import FileUploader from "../file-uploader";
|
||||
import { Input } from "../input";
|
||||
import UploadCsvPreview from "../upload-csv-preview";
|
||||
import UploadImagePreview from "../upload-image-preview";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
import { useCsv } from "./use-csv";
|
||||
|
||||
export default function ChatInput(
|
||||
props: Pick<
|
||||
@@ -14,11 +18,13 @@ export default function ChatInput(
|
||||
| "onFileError"
|
||||
| "handleSubmit"
|
||||
| "handleInputChange"
|
||||
> & {
|
||||
multiModal?: boolean;
|
||||
},
|
||||
| "messages"
|
||||
>,
|
||||
) {
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const { files, uploadNew, removeFile, resetUploadedFiles } = useCsv(
|
||||
props.messages,
|
||||
);
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
if (imageUrl) {
|
||||
@@ -28,6 +34,15 @@ export default function ChatInput(
|
||||
setImageUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
props.handleSubmit(e, {
|
||||
data: { csvFiles: files },
|
||||
});
|
||||
resetUploadedFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
props.handleSubmit(e);
|
||||
};
|
||||
|
||||
@@ -43,11 +58,32 @@ export default function ChatInput(
|
||||
setImageUrl(base64);
|
||||
};
|
||||
|
||||
const handleUploadCsvFile = async (file: File) => {
|
||||
const content = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
const isSuccess = uploadNew({
|
||||
id: uuidv4(),
|
||||
content,
|
||||
filename: file.name,
|
||||
filesize: file.size,
|
||||
});
|
||||
if (!isSuccess) {
|
||||
alert("File already exists in the list.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFile = async (file: File) => {
|
||||
try {
|
||||
if (props.multiModal && file.type.startsWith("image/")) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
return await handleUploadImageFile(file);
|
||||
}
|
||||
if (file.type === "text/csv") {
|
||||
return await handleUploadCsvFile(file);
|
||||
}
|
||||
props.onFileUpload?.(file);
|
||||
} catch (error: any) {
|
||||
props.onFileError?.(error.message);
|
||||
@@ -62,6 +98,30 @@ export default function ChatInput(
|
||||
{imageUrl && (
|
||||
<UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-4 w-full overflow-auto py-2">
|
||||
{props.isLoading ? (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />{" "}
|
||||
<span>Handling csv files...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{files.map((csv) => {
|
||||
return (
|
||||
<UploadCsvPreview
|
||||
key={csv.id}
|
||||
filename={csv.filename}
|
||||
filesize={csv.filesize}
|
||||
onRemove={() => removeFile(csv)}
|
||||
isNew={csv.type === "new_upload"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full items-start justify-between gap-4 ">
|
||||
<Input
|
||||
autoFocus
|
||||
|
||||
@@ -8,14 +8,16 @@ import { ChatEvents } from "./chat-events";
|
||||
import { ChatImage } from "./chat-image";
|
||||
import { ChatSources } from "./chat-sources";
|
||||
import ChatTools from "./chat-tools";
|
||||
import CsvContent from "./csv-content";
|
||||
import {
|
||||
AnnotationData,
|
||||
CsvData,
|
||||
EventData,
|
||||
ImageData,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
SourceData,
|
||||
ToolData,
|
||||
getAnnotationData,
|
||||
} from "./index";
|
||||
import Markdown from "./markdown";
|
||||
import { useCopyToClipboard } from "./use-copy-to-clipboard";
|
||||
@@ -25,13 +27,6 @@ type ContentDisplayConfig = {
|
||||
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);
|
||||
}
|
||||
|
||||
function ChatMessageContent({
|
||||
message,
|
||||
isLoading,
|
||||
@@ -46,6 +41,10 @@ function ChatMessageContent({
|
||||
annotations,
|
||||
MessageAnnotationType.IMAGE,
|
||||
);
|
||||
const csvData = getAnnotationData<CsvData>(
|
||||
annotations,
|
||||
MessageAnnotationType.CSV,
|
||||
);
|
||||
const eventData = getAnnotationData<EventData>(
|
||||
annotations,
|
||||
MessageAnnotationType.EVENTS,
|
||||
@@ -61,16 +60,20 @@ function ChatMessageContent({
|
||||
|
||||
const contents: ContentDisplayConfig[] = [
|
||||
{
|
||||
order: -3,
|
||||
order: -4,
|
||||
component: imageData[0] ? <ChatImage data={imageData[0]} /> : null,
|
||||
},
|
||||
{
|
||||
order: -2,
|
||||
order: -3,
|
||||
component:
|
||||
eventData.length > 0 ? (
|
||||
<ChatEvents isLoading={isLoading} data={eventData} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
order: -2,
|
||||
component: csvData[0] ? <CsvContent data={csvData[0]} /> : null,
|
||||
},
|
||||
{
|
||||
order: -1,
|
||||
component: toolData[0] ? <ChatTools data={toolData[0]} /> : null,
|
||||
|
||||
@@ -2,12 +2,10 @@ import { Check, Copy } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "../button";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
|
||||
import { getStaticFileDataUrl } from "../lib/url";
|
||||
import { SourceData, SourceNode } from "./index";
|
||||
import { SourceData } from "./index";
|
||||
import { useCopyToClipboard } from "./use-copy-to-clipboard";
|
||||
import PdfDialog from "./widgets/PdfDialog";
|
||||
|
||||
const DATA_SOURCE_FOLDER = "data";
|
||||
const SCORE_THRESHOLD = 0.3;
|
||||
|
||||
function SourceNumberButton({ index }: { index: number }) {
|
||||
@@ -18,46 +16,11 @@ function SourceNumberButton({ index }: { index: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
enum NODE_TYPE {
|
||||
URL,
|
||||
FILE,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
type NodeInfo = {
|
||||
id: string;
|
||||
type: NODE_TYPE;
|
||||
path?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
function getNodeInfo(node: SourceNode): NodeInfo {
|
||||
if (typeof node.metadata["URL"] === "string") {
|
||||
const url = node.metadata["URL"];
|
||||
return {
|
||||
id: node.id,
|
||||
type: NODE_TYPE.URL,
|
||||
path: url,
|
||||
url,
|
||||
};
|
||||
}
|
||||
if (typeof node.metadata["file_path"] === "string") {
|
||||
const fileName = node.metadata["file_name"] as string;
|
||||
const filePath = `${DATA_SOURCE_FOLDER}/${fileName}`;
|
||||
return {
|
||||
id: node.id,
|
||||
type: NODE_TYPE.FILE,
|
||||
path: node.metadata["file_path"],
|
||||
url: getStaticFileDataUrl(filePath),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
type: NODE_TYPE.UNKNOWN,
|
||||
};
|
||||
}
|
||||
|
||||
export function ChatSources({ data }: { data: SourceData }) {
|
||||
const sources: NodeInfo[] = useMemo(() => {
|
||||
// aggregate nodes by url or file_path (get the highest one by score)
|
||||
@@ -67,8 +30,11 @@ export function ChatSources({ data }: { data: SourceData }) {
|
||||
.filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
|
||||
.sort((a, b) => (b.score ?? 1) - (a.score ?? 1))
|
||||
.forEach((node) => {
|
||||
const nodeInfo = getNodeInfo(node);
|
||||
const key = nodeInfo.path ?? nodeInfo.id; // use id as key for UNKNOWN type
|
||||
const nodeInfo = {
|
||||
id: node.id,
|
||||
url: node.url,
|
||||
};
|
||||
const key = nodeInfo.url ?? nodeInfo.id; // use id as key for UNKNOWN type
|
||||
if (!nodesByPath[key]) {
|
||||
nodesByPath[key] = nodeInfo;
|
||||
}
|
||||
@@ -84,13 +50,12 @@ export function ChatSources({ data }: { data: SourceData }) {
|
||||
<span className="font-semibold">Sources:</span>
|
||||
<div className="inline-flex gap-1 items-center">
|
||||
{sources.map((nodeInfo: NodeInfo, index: number) => {
|
||||
if (nodeInfo.path?.endsWith(".pdf")) {
|
||||
if (nodeInfo.url?.endsWith(".pdf")) {
|
||||
return (
|
||||
<PdfDialog
|
||||
key={nodeInfo.id}
|
||||
documentId={nodeInfo.id}
|
||||
url={nodeInfo.url!}
|
||||
path={nodeInfo.path}
|
||||
trigger={<SourceNumberButton index={index} />}
|
||||
/>
|
||||
);
|
||||
@@ -116,16 +81,16 @@ export function ChatSources({ data }: { data: SourceData }) {
|
||||
function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
|
||||
|
||||
if (nodeInfo.type !== NODE_TYPE.UNKNOWN) {
|
||||
if (nodeInfo.url) {
|
||||
// this is a node generated by the web loader or file loader,
|
||||
// add a link to view its URL and a button to copy the URL to the clipboard
|
||||
return (
|
||||
<div className="flex items-center my-2">
|
||||
<a className="hover:text-blue-900" href={nodeInfo.url} target="_blank">
|
||||
<span>{nodeInfo.path}</span>
|
||||
<span>{nodeInfo.url}</span>
|
||||
</a>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(nodeInfo.path!)}
|
||||
onClick={() => copyToClipboard(nodeInfo.url!)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-12 w-12 shrink-0"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CsvData } from ".";
|
||||
import CsvDialog from "./widgets/CsvDialog";
|
||||
|
||||
export default function CsvContent({ data }: { data: CsvData }) {
|
||||
if (!data.csvFiles.length) return null;
|
||||
return (
|
||||
<div>
|
||||
<p className="font-semibold mb-2">Using data from following CSV files:</p>
|
||||
<div className="flex gap-2 items-center">
|
||||
{data.csvFiles.map((csv, index) => (
|
||||
<CsvDialog key={index} csv={csv} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export { type ChatHandler } from "./chat.interface";
|
||||
export { ChatInput, ChatMessages };
|
||||
|
||||
export enum MessageAnnotationType {
|
||||
CSV = "csv",
|
||||
IMAGE = "image",
|
||||
SOURCES = "sources",
|
||||
EVENTS = "events",
|
||||
@@ -16,11 +17,23 @@ export type ImageData = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type CsvData = {
|
||||
csvFiles: CsvFile[];
|
||||
};
|
||||
|
||||
export type SourceNode = {
|
||||
id: string;
|
||||
metadata: Record<string, unknown>;
|
||||
score?: number;
|
||||
text: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type SourceData = {
|
||||
@@ -46,9 +59,21 @@ export type ToolData = {
|
||||
};
|
||||
};
|
||||
|
||||
export type AnnotationData = ImageData | SourceData | EventData | ToolData;
|
||||
export type AnnotationData =
|
||||
| ImageData
|
||||
| CsvData
|
||||
| SourceData
|
||||
| EventData
|
||||
| ToolData;
|
||||
|
||||
export type MessageAnnotation = {
|
||||
type: MessageAnnotationType;
|
||||
data: AnnotationData;
|
||||
};
|
||||
|
||||
export function getAnnotationData<T extends AnnotationData>(
|
||||
annotations: MessageAnnotation[],
|
||||
type: MessageAnnotationType,
|
||||
): T[] {
|
||||
return annotations.filter((a) => a.type === type).map((a) => a.data as T);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { Message } from "ai";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CsvData,
|
||||
CsvFile,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
getAnnotationData,
|
||||
} from ".";
|
||||
|
||||
interface FrontendCSVData extends CsvFile {
|
||||
type: "available" | "new_upload";
|
||||
}
|
||||
|
||||
export function useCsv(messages: Message[]) {
|
||||
const [availableFiles, setAvailableFiles] = useState<FrontendCSVData[]>([]);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<FrontendCSVData[]>([]);
|
||||
|
||||
const files = useMemo(() => {
|
||||
return [...availableFiles, ...uploadedFiles];
|
||||
}, [availableFiles, uploadedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = getAvailableCsvFiles(messages);
|
||||
setAvailableFiles(items.map((data) => ({ ...data, type: "available" })));
|
||||
}, [messages]);
|
||||
|
||||
const csvEqual = (a: CsvFile, b: CsvFile) => {
|
||||
if (a.id === b.id) return true;
|
||||
if (a.filename === b.filename && a.filesize === b.filesize) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get available csv files from annotations chat history
|
||||
// returns the unique csv files by id
|
||||
const getAvailableCsvFiles = (messages: Message[]): Array<CsvFile> => {
|
||||
const docHash: Record<string, CsvFile> = {};
|
||||
messages.forEach((message) => {
|
||||
if (message.annotations) {
|
||||
const csvData = getAnnotationData<CsvData>(
|
||||
message.annotations as MessageAnnotation[],
|
||||
MessageAnnotationType.CSV,
|
||||
);
|
||||
csvData.forEach((data) => {
|
||||
data.csvFiles.forEach((file) => {
|
||||
if (!docHash[file.id]) {
|
||||
docHash[file.id] = file;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
return Object.values(docHash);
|
||||
};
|
||||
|
||||
const uploadNew = (file: CsvFile) => {
|
||||
const existedCsv = files.find((f) => csvEqual(f, file));
|
||||
if (!existedCsv) {
|
||||
setUploadedFiles((prev) => [...prev, { ...file, type: "new_upload" }]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeFile = (file: FrontendCSVData) => {
|
||||
if (file.type === "new_upload") {
|
||||
setUploadedFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
} else {
|
||||
setAvailableFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
}
|
||||
};
|
||||
|
||||
const resetUploadedFiles = () => {
|
||||
setUploadedFiles([]);
|
||||
};
|
||||
|
||||
return {
|
||||
files,
|
||||
uploadNew,
|
||||
removeFile,
|
||||
resetUploadedFiles,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import Image from "next/image";
|
||||
import { CsvFile } from "..";
|
||||
import SheetIcon from "../../../ui/icons/sheet.svg";
|
||||
import { Button } from "../../button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "../../drawer";
|
||||
|
||||
export interface CsvDialogProps {
|
||||
csv: CsvFile;
|
||||
}
|
||||
|
||||
export default function CsvDialog(props: CsvDialogProps) {
|
||||
const { filename, filesize, content } = props.csv;
|
||||
const fileSizeInKB = Math.round((filesize / 1024) * 10) / 10;
|
||||
return (
|
||||
<Drawer direction="left">
|
||||
<DrawerTrigger asChild>
|
||||
<div
|
||||
className="border-2 border-green-700 py-1.5 px-3 rounded-lg flex gap-2 items-center cursor-pointer text-sm hover:bg-green-700 hover:text-white transition-colors duration-200 ease-in-out"
|
||||
key={filename}
|
||||
>
|
||||
<div className="h-4 w-4 shrink-0 rounded-md">
|
||||
<Image
|
||||
className="h-full w-auto"
|
||||
priority
|
||||
src={SheetIcon}
|
||||
alt="SheetIcon"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
{filename} - {fileSizeInKB} KB
|
||||
</span>
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="w-3/5 mt-24 h-full max-h-[96%] ">
|
||||
<DrawerHeader className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<DrawerTitle>Csv Raw Content</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
{filename} ({fileSizeInKB} KB)
|
||||
</DrawerDescription>
|
||||
</div>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<div className="m-4 max-h-[80%] overflow-auto">
|
||||
<pre className="bg-secondary rounded-md p-4 block text-sm">
|
||||
{content}
|
||||
</pre>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
|
||||
export interface PdfDialogProps {
|
||||
documentId: string;
|
||||
path: string;
|
||||
url: string;
|
||||
trigger: React.ReactNode;
|
||||
}
|
||||
@@ -26,13 +25,13 @@ export default function PdfDialog(props: PdfDialogProps) {
|
||||
<div className="space-y-2">
|
||||
<DrawerTitle>PDF Content</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
File path:{" "}
|
||||
File URL:{" "}
|
||||
<a
|
||||
className="hover:text-blue-900"
|
||||
href={props.url}
|
||||
target="_blank"
|
||||
>
|
||||
{props.path}
|
||||
{props.url}
|
||||
</a>
|
||||
</DrawerDescription>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="49px" height="67px" viewBox="0 0 49 67" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Sheets-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-1"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-3"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-5"></path>
|
||||
<linearGradient x1="50.0053945%" y1="8.58610612%" x2="50.0053945%" y2="100.013939%" id="linearGradient-7">
|
||||
<stop stop-color="#263238" stop-opacity="0.2" offset="0%"></stop>
|
||||
<stop stop-color="#263238" stop-opacity="0.02" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-8"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-10"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-12"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-14"></path>
|
||||
<radialGradient cx="3.16804688%" cy="2.71744318%" fx="3.16804688%" fy="2.71744318%" r="161.248516%" gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" id="radialGradient-16">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Consumer-Apps-Sheets-Large-VD-R8-" transform="translate(-451.000000, -451.000000)">
|
||||
<g id="Hero" transform="translate(0.000000, 63.000000)">
|
||||
<g id="Personal" transform="translate(277.000000, 299.000000)">
|
||||
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
|
||||
<g id="Group">
|
||||
<g id="Clipped">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" id="Path" fill="#0F9D58" fill-rule="nonzero" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" id="Shape" fill="#F1F1F1" fill-rule="nonzero" mask="url(#mask-4)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-6" fill="white">
|
||||
<use xlink:href="#path-5"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<polygon id="Path" fill="url(#linearGradient-7)" fill-rule="nonzero" mask="url(#mask-6)" points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"></polygon>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-9" fill="white">
|
||||
<use xlink:href="#path-8"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<g id="Group" mask="url(#mask-9)">
|
||||
<g transform="translate(26.625000, -2.958333)">
|
||||
<path d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" id="Path" fill="#87CEAC" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-11" fill="white">
|
||||
<use xlink:href="#path-10"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" id="Path" fill-opacity="0.2" fill="#FFFFFF" fill-rule="nonzero" mask="url(#mask-11)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-13" fill="white">
|
||||
<use xlink:href="#path-12"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" id="Path" fill-opacity="0.2" fill="#263238" fill-rule="nonzero" mask="url(#mask-13)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-15" fill="white">
|
||||
<use xlink:href="#path-14"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" id="Path" fill-opacity="0.1" fill="#263238" fill-rule="nonzero" mask="url(#mask-15)"></path>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="Path" fill="url(#radialGradient-16)" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
@@ -1,11 +0,0 @@
|
||||
const staticFileAPI = "/api/files";
|
||||
|
||||
export const getStaticFileDataUrl = (filePath: string) => {
|
||||
const isUsingBackend = !!process.env.NEXT_PUBLIC_CHAT_API;
|
||||
const fileUrl = `${staticFileAPI}/${filePath}`;
|
||||
if (isUsingBackend) {
|
||||
const backendOrigin = new URL(process.env.NEXT_PUBLIC_CHAT_API!).origin;
|
||||
return `${backendOrigin}${fileUrl}`;
|
||||
}
|
||||
return fileUrl;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { XCircleIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import SheetIcon from "../ui/icons/sheet.svg";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
export default function UploadCsvPreview({
|
||||
filename,
|
||||
filesize,
|
||||
onRemove,
|
||||
isNew,
|
||||
}: {
|
||||
filename: string;
|
||||
filesize: number;
|
||||
onRemove: () => void;
|
||||
isNew?: boolean;
|
||||
}) {
|
||||
const fileSizeInKB = Math.round((filesize / 1024) * 10) / 10;
|
||||
return (
|
||||
<div className="p-2 w-60 max-w-60 bg-secondary rounded-lg text-sm relative">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md">
|
||||
<Image
|
||||
className="h-full w-auto"
|
||||
priority
|
||||
src={SheetIcon}
|
||||
alt="SheetIcon"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<div className="truncate font-semibold">
|
||||
{filename} ({fileSizeInKB} KB)
|
||||
</div>
|
||||
<div className="truncate text-token-text-tertiary flex items-center gap-2">
|
||||
<span>Spreadsheet</span>
|
||||
{isNew && (
|
||||
<span className="px-2 py-0.5 bg-red-400 text-white text-xs rounded-2xl">
|
||||
new
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-2 -right-2 w-6 h-6 z-10 bg-gray-500 text-white rounded-full",
|
||||
)}
|
||||
>
|
||||
<XCircleIcon
|
||||
className="w-6 h-6 bg-gray-500 text-white rounded-full"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,8 @@
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"vaul": "^0.9.1",
|
||||
"@llamaindex/pdf-viewer": "^1.1.1",
|
||||
"@e2b/code-interpreter": "^0.0.5"
|
||||
"@e2b/code-interpreter": "^0.0.5",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
@@ -52,6 +53,7 @@
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"@types/uuid": "^9.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user