mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-21 10:25:24 -04:00
Enhance data type (#378)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Simplify and unify handling file uploads
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
|
||||
from app.engine.utils.file_helper import FileMetadata, save_file
|
||||
from app.services.file import DocumentFile, FileService
|
||||
from e2b_code_interpreter import CodeInterpreter
|
||||
from e2b_code_interpreter.models import Logs
|
||||
from llama_index.core.tools import FunctionTool
|
||||
@@ -32,7 +32,7 @@ class E2BCodeInterpreter:
|
||||
output_dir = "output/tools"
|
||||
uploaded_files_dir = "output/uploaded"
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
if api_key is None:
|
||||
api_key = os.getenv("E2B_API_KEY")
|
||||
filesever_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
@@ -72,15 +72,17 @@ class E2BCodeInterpreter:
|
||||
self.interpreter.files.write(file_path, content)
|
||||
logger.info(f"Uploaded {len(sandbox_files)} files to sandbox")
|
||||
|
||||
def _save_to_disk(self, base64_data: str, ext: str) -> FileMetadata:
|
||||
def _save_to_disk(self, base64_data: str, ext: str) -> DocumentFile:
|
||||
buffer = base64.b64decode(base64_data)
|
||||
|
||||
filename = f"{uuid.uuid4()}.{ext}" # generate a unique filename
|
||||
output_path = os.path.join(self.output_dir, filename)
|
||||
# Output from e2b doesn't have a name. Create a random name for it.
|
||||
filename = f"e2b_file_{uuid.uuid4()}.{ext}"
|
||||
|
||||
file_metadata = save_file(buffer, file_path=output_path)
|
||||
document_file = FileService.save_file(
|
||||
buffer, file_name=filename, save_dir=self.output_dir
|
||||
)
|
||||
|
||||
return file_metadata
|
||||
return document_file
|
||||
|
||||
def _parse_result(self, result) -> List[InterpreterExtraResult]:
|
||||
"""
|
||||
@@ -99,12 +101,12 @@ class E2BCodeInterpreter:
|
||||
for ext, data in zip(formats, results):
|
||||
match ext:
|
||||
case "png" | "svg" | "jpeg" | "pdf":
|
||||
file_metadata = self._save_to_disk(data, ext)
|
||||
document_file = self._save_to_disk(data, ext)
|
||||
output.append(
|
||||
InterpreterExtraResult(
|
||||
type=ext,
|
||||
filename=file_metadata.name,
|
||||
url=file_metadata.url,
|
||||
filename=document_file.name,
|
||||
url=document_file.url,
|
||||
)
|
||||
)
|
||||
case _:
|
||||
|
||||
@@ -111,13 +111,16 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
// upload files to sandbox
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
await this.codeInterpreter?.files.write(filePath, content);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
await this.codeInterpreter?.files.write(filePath, content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
console.log(`Uploaded ${input.sandboxFiles.length} files to sandbox`);
|
||||
}
|
||||
return this.codeInterpreter;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { getExtractors } from "../../engine/loader";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
|
||||
const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
"application/pdf": "pdf",
|
||||
@@ -14,27 +15,20 @@ const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
|
||||
const UPLOADED_FOLDER = "output/uploaded";
|
||||
|
||||
export type FileMetadata = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
refs: string[];
|
||||
};
|
||||
|
||||
export async function storeAndParseFile(
|
||||
filename: string,
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
): Promise<FileMetadata> {
|
||||
const fileMetadata = await storeFile(filename, fileBuffer, mimeType);
|
||||
const documents: Document[] = await parseFile(fileBuffer, filename, mimeType);
|
||||
): Promise<DocumentFile> {
|
||||
const file = await storeFile(name, fileBuffer, mimeType);
|
||||
const documents: Document[] = await parseFile(fileBuffer, name, mimeType);
|
||||
// Update document IDs in the file metadata
|
||||
fileMetadata.refs = documents.map((document) => document.id_ as string);
|
||||
return fileMetadata;
|
||||
file.refs = documents.map((document) => document.id_ as string);
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function storeFile(
|
||||
filename: string,
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
) {
|
||||
@@ -42,15 +36,17 @@ export async function storeFile(
|
||||
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
|
||||
const fileId = crypto.randomUUID();
|
||||
const newFilename = `${fileId}_${sanitizeFileName(filename)}`;
|
||||
const newFilename = `${sanitizeFileName(name)}_${fileId}.${fileExt}`;
|
||||
const filepath = path.join(UPLOADED_FOLDER, newFilename);
|
||||
const fileUrl = await saveDocument(filepath, fileBuffer);
|
||||
return {
|
||||
id: fileId,
|
||||
name: newFilename,
|
||||
size: fileBuffer.length,
|
||||
type: fileExt,
|
||||
url: fileUrl,
|
||||
refs: [] as string[],
|
||||
} as FileMetadata;
|
||||
} as DocumentFile;
|
||||
}
|
||||
|
||||
export async function parseFile(
|
||||
@@ -104,5 +100,6 @@ export async function saveDocument(filepath: string, content: string | Buffer) {
|
||||
}
|
||||
|
||||
function sanitizeFileName(fileName: string) {
|
||||
return fileName.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
||||
// Remove file extension and sanitize
|
||||
return fileName.split(".")[0].replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
@@ -2,40 +2,40 @@ import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { FileMetadata, parseFile, storeFile } from "./helper";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
import { parseFile, storeFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
export async function uploadDocument(
|
||||
index: VectorStoreIndex | LlamaCloudIndex | null,
|
||||
filename: string,
|
||||
name: string,
|
||||
raw: string,
|
||||
): Promise<FileMetadata> {
|
||||
): Promise<DocumentFile> {
|
||||
const [header, content] = raw.split(",");
|
||||
const mimeType = header.replace("data:", "").replace(";base64", "");
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
|
||||
// Store file
|
||||
const fileMetadata = await storeFile(filename, fileBuffer, mimeType);
|
||||
const fileMetadata = await storeFile(name, fileBuffer, mimeType);
|
||||
|
||||
// If the file is csv and has codeExecutorTool, we don't need to index the file.
|
||||
if (mimeType === "text/csv" && (await hasCodeExecutorTool())) {
|
||||
return fileMetadata;
|
||||
}
|
||||
|
||||
let documentIds: string[] = [];
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
// trigger LlamaCloudIndex API to upload the file and run the pipeline
|
||||
const projectId = await index.getProjectId();
|
||||
const pipelineId = await index.getPipelineId();
|
||||
try {
|
||||
const documentId = await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([fileBuffer], filename, { type: mimeType }),
|
||||
{ private: "true" },
|
||||
);
|
||||
// Update file metadata with document IDs
|
||||
fileMetadata.refs = [documentId];
|
||||
return fileMetadata;
|
||||
documentIds = [
|
||||
await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([fileBuffer], name, { type: mimeType }),
|
||||
{ private: "true" },
|
||||
),
|
||||
];
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ReferenceError &&
|
||||
@@ -47,14 +47,14 @@ export async function uploadDocument(
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// run the pipeline for other vector store indexes
|
||||
const documents: Document[] = await parseFile(fileBuffer, name, mimeType);
|
||||
documentIds = await runPipeline(index, documents);
|
||||
}
|
||||
|
||||
// run the pipeline for other vector store indexes
|
||||
const documents: Document[] = await parseFile(fileBuffer, filename, mimeType);
|
||||
// Update file metadata with document IDs
|
||||
fileMetadata.refs = documents.map((document) => document.id_ as string);
|
||||
// Run the pipeline
|
||||
await runPipeline(index, documents);
|
||||
fileMetadata.refs = documentIds;
|
||||
return fileMetadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,13 @@ import { MessageContent, MessageContentDetail } from "llamaindex";
|
||||
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
export type UploadedFileMeta = {
|
||||
export type DocumentFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
url?: string;
|
||||
refs?: string[];
|
||||
};
|
||||
|
||||
export type DocumentFile = {
|
||||
type: DocumentFileType;
|
||||
size: number;
|
||||
type: string;
|
||||
url: string;
|
||||
metadata: UploadedFileMeta;
|
||||
refs?: string[];
|
||||
};
|
||||
|
||||
type Annotation = {
|
||||
@@ -30,7 +26,7 @@ export function isValidMessages(messages: Message[]): boolean {
|
||||
export function retrieveDocumentIds(messages: Message[]): string[] {
|
||||
// retrieve document Ids from the annotations of all messages (if any)
|
||||
const documentFiles = retrieveDocumentFiles(messages);
|
||||
return documentFiles.map((file) => file.metadata?.refs || []).flat();
|
||||
return documentFiles.map((file) => file.refs || []).flat();
|
||||
}
|
||||
|
||||
export function retrieveDocumentFiles(messages: Message[]): DocumentFile[] {
|
||||
@@ -63,16 +59,15 @@ export function retrieveMessageContent(messages: Message[]): MessageContent {
|
||||
}
|
||||
|
||||
function getFileContent(file: DocumentFile): string {
|
||||
const fileMetadata = file.metadata;
|
||||
let defaultContent = `=====File: ${fileMetadata.name}=====\n`;
|
||||
let defaultContent = `=====File: ${file.name}=====\n`;
|
||||
// Include file URL if it's available
|
||||
const urlPrefix = process.env.FILESERVER_URL_PREFIX;
|
||||
let urlContent = "";
|
||||
if (urlPrefix) {
|
||||
if (fileMetadata.url) {
|
||||
urlContent = `File URL: ${fileMetadata.url}\n`;
|
||||
if (file.url) {
|
||||
urlContent = `File URL: ${file.url}\n`;
|
||||
} else {
|
||||
urlContent = `File URL (instruction: do not update this file URL yourself): ${urlPrefix}/output/uploaded/${fileMetadata.name}\n`;
|
||||
urlContent = `File URL (instruction: do not update this file URL yourself): ${urlPrefix}/output/uploaded/${file.name}\n`;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
@@ -82,11 +77,11 @@ function getFileContent(file: DocumentFile): string {
|
||||
defaultContent += urlContent;
|
||||
|
||||
// Include document IDs if it's available
|
||||
if (fileMetadata.refs) {
|
||||
defaultContent += `Document IDs: ${fileMetadata.refs}\n`;
|
||||
if (file.refs) {
|
||||
defaultContent += `Document IDs: ${file.refs}\n`;
|
||||
}
|
||||
// Include sandbox file paths
|
||||
const sandboxFilePath = `/tmp/${fileMetadata.name}`;
|
||||
const sandboxFilePath = `/tmp/${file.name}`;
|
||||
defaultContent += `Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): ${sandboxFilePath}\n`;
|
||||
|
||||
return defaultContent;
|
||||
|
||||
@@ -172,10 +172,14 @@ def _download_cell_results(cell_results: Optional[List]) -> List[Dict[str, str]]
|
||||
data = result[ext]
|
||||
|
||||
if ext in ["png", "svg", "jpeg", "pdf"]:
|
||||
file_path = os.path.join("output", "tools", f"{uuid.uuid4()}.{ext}")
|
||||
base64_data = data
|
||||
buffer = base64.b64decode(base64_data)
|
||||
file_meta = save_file(content=buffer, file_path=file_path)
|
||||
file_name = f"{uuid.uuid4()}.{ext}"
|
||||
file_meta = save_file(
|
||||
content=buffer,
|
||||
file_name=file_name,
|
||||
save_dir=os.path.join("output", "tools"),
|
||||
)
|
||||
output.append(
|
||||
{
|
||||
"type": ext,
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.utils.file_helper import FileMetadata, save_file
|
||||
from llama_index.core import VectorStoreIndex
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.readers.file.base import (
|
||||
_try_loading_included_file_formats as get_file_loaders_map,
|
||||
)
|
||||
from llama_index.core.schema import Document
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
|
||||
from llama_index.readers.file import FlatReader
|
||||
|
||||
|
||||
def get_llamaparse_parser():
|
||||
from app.engine.loaders import load_configs
|
||||
from app.engine.loaders.file import FileLoaderConfig, llama_parse_parser
|
||||
|
||||
config = load_configs()
|
||||
file_loader_config = FileLoaderConfig(**config["file"])
|
||||
if file_loader_config.use_llama_parse:
|
||||
return llama_parse_parser()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def default_file_loaders_map():
|
||||
default_loaders = get_file_loaders_map()
|
||||
default_loaders[".txt"] = FlatReader
|
||||
default_loaders[".csv"] = FlatReader
|
||||
return default_loaders
|
||||
|
||||
|
||||
class PrivateFileService:
|
||||
"""
|
||||
To store the files uploaded by the user and add them to the index.
|
||||
"""
|
||||
|
||||
PRIVATE_STORE_PATH = "output/uploaded"
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
|
||||
header, data = base64_content.split(",", 1)
|
||||
mime_type = header.split(";")[0].split(":", 1)[1]
|
||||
extension = mimetypes.guess_extension(mime_type)
|
||||
# File data as bytes
|
||||
return base64.b64decode(data), extension
|
||||
|
||||
@staticmethod
|
||||
def _store_file(file_name, file_data) -> FileMetadata:
|
||||
"""
|
||||
Store the file to the private directory and return the file metadata
|
||||
"""
|
||||
# Store file to the private directory
|
||||
os.makedirs(PrivateFileService.PRIVATE_STORE_PATH, exist_ok=True)
|
||||
file_path = Path(os.path.join(PrivateFileService.PRIVATE_STORE_PATH, file_name))
|
||||
|
||||
return save_file(file_data, file_path=str(file_path))
|
||||
|
||||
@staticmethod
|
||||
def _load_file_to_documents(file_metadata: FileMetadata) -> List[Document]:
|
||||
"""
|
||||
Load the file from the private directory and return the documents
|
||||
"""
|
||||
_, extension = os.path.splitext(file_metadata.name)
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
# Load file to documents
|
||||
# If LlamaParse is enabled, use it to parse the file
|
||||
# Otherwise, use the default file loaders
|
||||
reader = get_llamaparse_parser()
|
||||
if reader is None:
|
||||
reader_cls = default_file_loaders_map().get(f".{extension}")
|
||||
if reader_cls is None:
|
||||
raise ValueError(f"File extension {extension} is not supported")
|
||||
reader = reader_cls()
|
||||
documents = reader.load_data(Path(file_metadata.path))
|
||||
# Add custom metadata
|
||||
for doc in documents:
|
||||
doc.metadata["file_name"] = file_metadata.name
|
||||
doc.metadata["private"] = "true"
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def _add_documents_to_vector_store_index(
|
||||
documents: List[Document], index: VectorStoreIndex
|
||||
) -> None:
|
||||
"""
|
||||
Add the documents to the vector store index
|
||||
"""
|
||||
pipeline = IngestionPipeline()
|
||||
nodes = pipeline.run(documents=documents)
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
if index is None:
|
||||
index = VectorStoreIndex(nodes=nodes)
|
||||
else:
|
||||
index.insert_nodes(nodes=nodes)
|
||||
index.storage_context.persist(
|
||||
persist_dir=os.environ.get("STORAGE_DIR", "storage")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_file_to_llama_cloud_index(
|
||||
index: LlamaCloudIndex,
|
||||
file_name: str,
|
||||
file_data: bytes,
|
||||
) -> str:
|
||||
"""
|
||||
Add the file to the LlamaCloud index.
|
||||
LlamaCloudIndex is a managed index so we can directly use the files.
|
||||
"""
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
except ImportError:
|
||||
raise ValueError("LlamaCloudFileService is not found")
|
||||
|
||||
project_id = index._get_project_id()
|
||||
pipeline_id = index._get_pipeline_id()
|
||||
# LlamaCloudIndex is a managed index so we can directly use the files
|
||||
upload_file = (file_name, BytesIO(file_data))
|
||||
doc_id = LLamaCloudFileService.add_file_to_pipeline(
|
||||
project_id,
|
||||
pipeline_id,
|
||||
upload_file,
|
||||
custom_metadata={},
|
||||
)
|
||||
return doc_id
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_file_name(file_name: str) -> str:
|
||||
file_name, extension = os.path.splitext(file_name)
|
||||
return re.sub(r"[^a-zA-Z0-9]", "_", file_name) + extension
|
||||
|
||||
@classmethod
|
||||
def process_file(
|
||||
cls,
|
||||
file_name: str,
|
||||
base64_content: str,
|
||||
params: Optional[dict] = None,
|
||||
) -> FileMetadata:
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
|
||||
# Generate a new file name if the same file is uploaded multiple times
|
||||
file_id = str(uuid.uuid4())
|
||||
new_file_name = f"{file_id}_{cls._sanitize_file_name(file_name)}"
|
||||
|
||||
# Preprocess and store the file
|
||||
file_data, extension = cls._preprocess_base64_file(base64_content)
|
||||
file_metadata = cls._store_file(new_file_name, file_data)
|
||||
|
||||
tools = cls._get_available_tools()
|
||||
code_executor_tools = ["interpreter", "artifact"]
|
||||
# If the file is CSV and there is a code executor tool, we don't need to index.
|
||||
if extension == ".csv" and any(tool in tools for tool in code_executor_tools):
|
||||
return file_metadata
|
||||
else:
|
||||
# Insert the file into the index and update document ids to the file metadata
|
||||
if isinstance(index, LlamaCloudIndex):
|
||||
doc_id = cls._add_file_to_llama_cloud_index(
|
||||
index, new_file_name, file_data
|
||||
)
|
||||
# Add document ids to the file metadata
|
||||
file_metadata.refs = [doc_id]
|
||||
else:
|
||||
documents = cls._load_file_to_documents(file_metadata)
|
||||
cls._add_documents_to_vector_store_index(documents, index)
|
||||
# Add document ids to the file metadata
|
||||
file_metadata.refs = [doc.doc_id for doc in documents]
|
||||
|
||||
# Return the file metadata
|
||||
return file_metadata
|
||||
|
||||
@staticmethod
|
||||
def _get_available_tools() -> Dict[str, List[FunctionTool]]:
|
||||
try:
|
||||
from app.engine.tools import ToolFactory
|
||||
|
||||
tools = ToolFactory.from_env(map_result=True)
|
||||
return tools
|
||||
except ImportError:
|
||||
# There is no tool code
|
||||
return {}
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to get available tools: {e}") from e
|
||||
@@ -4,11 +4,11 @@ import { uploadDocument } from "./llamaindex/documents/upload";
|
||||
|
||||
export const chatUpload = async (req: Request, res: Response) => {
|
||||
const {
|
||||
filename,
|
||||
name,
|
||||
base64,
|
||||
params,
|
||||
}: { filename: string; base64: string; params?: any } = req.body;
|
||||
if (!base64 || !filename) {
|
||||
}: { name: string; base64: string; params?: any } = req.body;
|
||||
if (!base64 || !name) {
|
||||
return res.status(400).json({
|
||||
error: "base64 and filename is required in the request body",
|
||||
});
|
||||
@@ -20,5 +20,5 @@ export const chatUpload = async (req: Request, res: Response) => {
|
||||
"StorageContext is empty - call 'npm run generate' to generate the storage first",
|
||||
});
|
||||
}
|
||||
return res.status(200).json(await uploadDocument(index, filename, base64));
|
||||
return res.status(200).json(await uploadDocument(index, name, base64));
|
||||
};
|
||||
|
||||
@@ -10,6 +10,42 @@ config_router = r = APIRouter()
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def _is_llama_cloud_service_configured():
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService # noqa
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
async def chat_llama_cloud_config():
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
|
||||
if not os.getenv("LLAMA_CLOUD_API_KEY"):
|
||||
raise HTTPException(
|
||||
status_code=500, detail="LlamaCloud API KEY is not configured"
|
||||
)
|
||||
projects = LLamaCloudFileService.get_all_projects_with_pipelines()
|
||||
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
|
||||
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
|
||||
pipeline_config = None
|
||||
if pipeline and project:
|
||||
pipeline_config = {
|
||||
"pipeline": pipeline,
|
||||
"project": project,
|
||||
}
|
||||
return {
|
||||
"projects": projects,
|
||||
"pipeline": pipeline_config,
|
||||
}
|
||||
|
||||
|
||||
if _is_llama_cloud_service_configured():
|
||||
logger.info("LlamaCloud is configured. Adding /config/llamacloud route.")
|
||||
r.add_api_route("/llamacloud", chat_llama_cloud_config, methods=["GET"])
|
||||
|
||||
|
||||
@r.get("")
|
||||
async def chat_config() -> ChatConfig:
|
||||
starter_questions = None
|
||||
@@ -17,33 +53,3 @@ async def chat_config() -> ChatConfig:
|
||||
if conversation_starters and conversation_starters.strip():
|
||||
starter_questions = conversation_starters.strip().split("\n")
|
||||
return ChatConfig(starter_questions=starter_questions)
|
||||
|
||||
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
|
||||
print("LlamaCloud is configured. Adding /config/llamacloud route.")
|
||||
|
||||
@r.get("/llamacloud")
|
||||
async def chat_llama_cloud_config():
|
||||
if not os.getenv("LLAMA_CLOUD_API_KEY"):
|
||||
raise HTTPException(
|
||||
status_code=500, detail="LlamaCloud API KEY is not configured"
|
||||
)
|
||||
projects = LLamaCloudFileService.get_all_projects_with_pipelines()
|
||||
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
|
||||
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
|
||||
pipeline_config = None
|
||||
if pipeline and project:
|
||||
pipeline_config = {
|
||||
"pipeline": pipeline,
|
||||
"project": project,
|
||||
}
|
||||
return {
|
||||
"projects": projects,
|
||||
"pipeline": pipeline_config,
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
print("LlamaCloud is not configured. Skipping adding /config/llamacloud route.")
|
||||
pass
|
||||
|
||||
@@ -8,60 +8,13 @@ from pydantic import BaseModel, Field, validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from app.config import DATA_DIR
|
||||
from app.services.file import DocumentFile
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class FileMetadata(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
url: Optional[str] = None
|
||||
refs: Optional[List[str]] = None
|
||||
|
||||
def _get_url_llm_content(self) -> Optional[str]:
|
||||
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if url_prefix:
|
||||
if self.url is not None:
|
||||
return f"File URL: {self.url}\n"
|
||||
else:
|
||||
# Construct url from file name
|
||||
return f"File URL (instruction: do not update this file URL yourself): {url_prefix}/output/uploaded/{self.name}\n"
|
||||
else:
|
||||
logger.warning(
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
|
||||
)
|
||||
return None
|
||||
|
||||
def to_llm_content(self) -> str:
|
||||
"""
|
||||
Construct content for LLM from the file metadata
|
||||
"""
|
||||
default_content = f"=====File: {self.name}=====\n"
|
||||
# Include file URL if it's available
|
||||
url_content = self._get_url_llm_content()
|
||||
if url_content:
|
||||
default_content += url_content
|
||||
# Include document IDs if it's available
|
||||
if self.refs is not None:
|
||||
default_content += f"Document IDs: {self.refs}\n"
|
||||
# Include sandbox file path
|
||||
sandbox_file_path = f"/tmp/{self.name}"
|
||||
default_content += f"Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): {sandbox_file_path}\n"
|
||||
return default_content
|
||||
|
||||
|
||||
class File(BaseModel):
|
||||
filetype: str
|
||||
metadata: FileMetadata
|
||||
|
||||
def _load_file_content(self) -> str:
|
||||
file_path = f"output/uploaded/{self.metadata.name}"
|
||||
with open(file_path, "r") as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
class AnnotationFileData(BaseModel):
|
||||
files: List[File] = Field(
|
||||
files: List[DocumentFile] = Field(
|
||||
default=[],
|
||||
description="List of files",
|
||||
)
|
||||
@@ -69,19 +22,55 @@ class AnnotationFileData(BaseModel):
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"csvFiles": [
|
||||
"files": [
|
||||
{
|
||||
"content": "Name, Age\nAlice, 25\nBob, 30",
|
||||
"filename": "example.csv",
|
||||
"filesize": 123,
|
||||
"id": "123",
|
||||
"type": "text/csv",
|
||||
"content": "data:text/plain;base64,aGVsbG8gd29ybGQK=",
|
||||
"name": "example.txt",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
alias_generator = to_camel
|
||||
|
||||
@staticmethod
|
||||
def _get_url_llm_content(file: DocumentFile) -> Optional[str]:
|
||||
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if url_prefix:
|
||||
if file.url is not None:
|
||||
return f"File URL: {file.url}\n"
|
||||
else:
|
||||
# Construct url from file name
|
||||
return f"File URL (instruction: do not update this file URL yourself): {url_prefix}/output/uploaded/{file.name}\n"
|
||||
else:
|
||||
logger.warning(
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_file_content(cls, file: DocumentFile) -> str:
|
||||
"""
|
||||
Construct content for LLM from the file metadata
|
||||
"""
|
||||
default_content = f"=====File: {file.name}=====\n"
|
||||
# Include file URL if it's available
|
||||
url_content = cls._get_url_llm_content(file)
|
||||
if url_content:
|
||||
default_content += url_content
|
||||
# Include document IDs if it's available
|
||||
if file.refs is not None:
|
||||
default_content += f"Document IDs: {file.refs}\n"
|
||||
# Include sandbox file path
|
||||
sandbox_file_path = f"/tmp/{file.name}"
|
||||
default_content += f"Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): {sandbox_file_path}\n"
|
||||
return default_content
|
||||
|
||||
def to_llm_content(self) -> Optional[str]:
|
||||
file_contents = [self._get_file_content(file) for file in self.files]
|
||||
if len(file_contents) == 0:
|
||||
return None
|
||||
return "Use data from following files content\n" + "\n".join(file_contents)
|
||||
|
||||
|
||||
class AgentAnnotation(BaseModel):
|
||||
agent: str
|
||||
@@ -99,12 +88,7 @@ class Annotation(BaseModel):
|
||||
|
||||
def to_content(self) -> Optional[str]:
|
||||
if self.type == "document_file" and isinstance(self.data, AnnotationFileData):
|
||||
# iterate through all files and construct content for LLM
|
||||
file_contents = [file.metadata.to_llm_content() for file in self.data.files]
|
||||
if len(file_contents) > 0:
|
||||
return "Use data from following files content\n" + "\n".join(
|
||||
file_contents
|
||||
)
|
||||
return self.data.to_llm_content()
|
||||
elif self.type == "image":
|
||||
raise NotImplementedError("Use image file is not supported yet!")
|
||||
else:
|
||||
@@ -247,14 +231,14 @@ class ChatData(BaseModel):
|
||||
Get the document IDs from the chat messages
|
||||
"""
|
||||
document_ids: List[str] = []
|
||||
uploaded_files = self.get_uploaded_files()
|
||||
uploaded_files = self.get_document_files()
|
||||
for _file in uploaded_files:
|
||||
refs = _file.metadata.refs
|
||||
refs = getattr(_file, "refs", None)
|
||||
if refs is not None:
|
||||
document_ids.extend(refs)
|
||||
return list(set(document_ids))
|
||||
|
||||
def get_uploaded_files(self) -> List[File]:
|
||||
def get_document_files(self) -> List[DocumentFile]:
|
||||
"""
|
||||
Get the uploaded files from the chat data
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.services.file import PrivateFileService
|
||||
from app.api.routers.models import DocumentFile
|
||||
from app.services.file import FileService
|
||||
|
||||
file_upload_router = r = APIRouter()
|
||||
|
||||
@@ -13,23 +14,20 @@ logger = logging.getLogger("uvicorn")
|
||||
|
||||
class FileUploadRequest(BaseModel):
|
||||
base64: str
|
||||
filename: str
|
||||
name: str
|
||||
params: Any = None
|
||||
|
||||
|
||||
@r.post("")
|
||||
def upload_file(request: FileUploadRequest) -> Dict[str, Any]:
|
||||
def upload_file(request: FileUploadRequest) -> DocumentFile:
|
||||
"""
|
||||
To upload a private file from the chat UI.
|
||||
Returns:
|
||||
The metadata of the uploaded file.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Processing file: {request.filename}")
|
||||
file_meta = PrivateFileService.process_file(
|
||||
request.filename, request.base64, request.params
|
||||
logger.info(f"Processing file: {request.name}")
|
||||
return FileService.process_private_file(
|
||||
request.name, request.base64, request.params
|
||||
)
|
||||
return file_meta.to_upload_response()
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Error processing file")
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileMetadata(BaseModel):
|
||||
path: str = Field(..., description="The stored path of the file")
|
||||
name: str = Field(..., description="The name of the file")
|
||||
url: str = Field(..., description="The URL of the file")
|
||||
refs: Optional[List[str]] = Field(
|
||||
None, description="The indexed document IDs that the file is referenced to"
|
||||
)
|
||||
|
||||
@computed_field
|
||||
def file_id(self) -> Optional[str]:
|
||||
file_els = self.name.split("_", maxsplit=1)
|
||||
if len(file_els) == 2:
|
||||
return file_els[0]
|
||||
return None
|
||||
|
||||
def to_upload_response(self) -> Dict[str, Any]:
|
||||
response = {
|
||||
"id": self.file_id,
|
||||
"name": self.name,
|
||||
"url": self.url,
|
||||
"refs": self.refs,
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
def save_file(
|
||||
content: bytes | str,
|
||||
file_name: Optional[str] = None,
|
||||
file_path: Optional[str] = None,
|
||||
) -> FileMetadata:
|
||||
"""
|
||||
Save the content to a file in the local file server (accessible via URL)
|
||||
Args:
|
||||
content (bytes | str): The content to save, either bytes or string.
|
||||
file_name (Optional[str]): The name of the file. If not provided, a random name will be generated with .txt extension.
|
||||
file_path (Optional[str]): The path to save the file to. If not provided, a random name will be generated.
|
||||
Returns:
|
||||
The metadata of the saved file.
|
||||
"""
|
||||
if file_name is not None and file_path is not None:
|
||||
raise ValueError("Either file_name or file_path should be provided")
|
||||
|
||||
if file_path is None:
|
||||
if file_name is None:
|
||||
file_name = f"{uuid.uuid4()}.txt"
|
||||
file_path = os.path.join(os.getcwd(), file_name)
|
||||
else:
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
if isinstance(content, str):
|
||||
content = content.encode()
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(content)
|
||||
except PermissionError as e:
|
||||
logger.error(f"Permission denied when writing to file {file_path}: {str(e)}")
|
||||
raise
|
||||
except IOError as e:
|
||||
logger.error(f"IO error occurred when writing to file {file_path}: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error when writing to file {file_path}: {str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"Saved file to {file_path}")
|
||||
|
||||
return FileMetadata(
|
||||
path=file_path if isinstance(file_path, str) else str(file_path),
|
||||
name=file_name,
|
||||
url=f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}",
|
||||
)
|
||||
@@ -0,0 +1,300 @@
|
||||
import base64
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from llama_index.core import VectorStoreIndex
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.readers.file.base import (
|
||||
_try_loading_included_file_formats as get_file_loaders_map,
|
||||
)
|
||||
from llama_index.core.schema import Document
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
|
||||
from llama_index.readers.file import FlatReader
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PRIVATE_STORE_PATH = str(Path("output", "uploaded"))
|
||||
TOOL_STORE_PATH = str(Path("output", "tools"))
|
||||
LLAMA_CLOUD_STORE_PATH = str(Path("output", "llamacloud"))
|
||||
|
||||
|
||||
class DocumentFile(BaseModel):
|
||||
id: str
|
||||
name: str # Stored file name
|
||||
type: str = None
|
||||
size: int = None
|
||||
url: str = None
|
||||
path: Optional[str] = Field(
|
||||
None,
|
||||
description="The stored file path. Used internally in the server.",
|
||||
exclude=True,
|
||||
)
|
||||
refs: Optional[List[str]] = Field(
|
||||
None, description="The document ids in the index."
|
||||
)
|
||||
|
||||
|
||||
class FileService:
|
||||
"""
|
||||
To store the files uploaded by the user and add them to the index.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def process_private_file(
|
||||
cls,
|
||||
file_name: str,
|
||||
base64_content: str,
|
||||
params: Optional[dict] = None,
|
||||
) -> DocumentFile:
|
||||
"""
|
||||
Store the uploaded file and index it if necessary.
|
||||
"""
|
||||
try:
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
except ImportError as e:
|
||||
raise ValueError("IndexConfig or get_index is not found") from e
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
index_config = IndexConfig(**params)
|
||||
index = get_index(index_config)
|
||||
|
||||
# Preprocess and store the file
|
||||
file_data, extension = cls._preprocess_base64_file(base64_content)
|
||||
|
||||
document_file = cls.save_file(
|
||||
file_data,
|
||||
file_name=file_name,
|
||||
save_dir=PRIVATE_STORE_PATH,
|
||||
)
|
||||
|
||||
tools = _get_available_tools()
|
||||
code_executor_tools = ["interpreter", "artifact"]
|
||||
# If the file is CSV and there is a code executor tool, we don't need to index.
|
||||
if extension == "csv" and any(tool in tools for tool in code_executor_tools):
|
||||
return document_file
|
||||
else:
|
||||
# Insert the file into the index and update document ids to the file metadata
|
||||
if isinstance(index, LlamaCloudIndex):
|
||||
doc_id = cls._add_file_to_llama_cloud_index(
|
||||
index, document_file.name, file_data
|
||||
)
|
||||
# Add document ids to the file metadata
|
||||
document_file.refs = [doc_id]
|
||||
else:
|
||||
documents = cls._load_file_to_documents(document_file)
|
||||
cls._add_documents_to_vector_store_index(documents, index)
|
||||
# Add document ids to the file metadata
|
||||
document_file.refs = [doc.doc_id for doc in documents]
|
||||
|
||||
# Return the file metadata
|
||||
return document_file
|
||||
|
||||
@classmethod
|
||||
def save_file(
|
||||
cls,
|
||||
content: bytes | str,
|
||||
file_name: str,
|
||||
save_dir: Optional[str] = None,
|
||||
) -> DocumentFile:
|
||||
"""
|
||||
Save the content to a file in the local file server (accessible via URL)
|
||||
|
||||
Args:
|
||||
content (bytes | str): The content to save, either bytes or string.
|
||||
file_name (str): The original name of the file.
|
||||
save_dir (Optional[str]): The relative path from the current working directory. Defaults to the `output/uploaded` directory.
|
||||
Returns:
|
||||
The metadata of the saved file.
|
||||
"""
|
||||
if save_dir is None:
|
||||
save_dir = os.path.join("output", "uploaded")
|
||||
|
||||
file_id = str(uuid.uuid4())
|
||||
name, extension = os.path.splitext(file_name)
|
||||
extension = extension.lstrip(".")
|
||||
sanitized_name = _sanitize_file_name(name)
|
||||
if extension == "":
|
||||
raise ValueError("File is not supported!")
|
||||
new_file_name = f"{sanitized_name}_{file_id}.{extension}"
|
||||
|
||||
file_path = os.path.join(save_dir, new_file_name)
|
||||
|
||||
if isinstance(content, str):
|
||||
content = content.encode()
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(content)
|
||||
except PermissionError as e:
|
||||
logger.error(
|
||||
f"Permission denied when writing to file {file_path}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
except IOError as e:
|
||||
logger.error(
|
||||
f"IO error occurred when writing to file {file_path}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error when writing to file {file_path}: {str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"Saved file to {file_path}")
|
||||
|
||||
file_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if file_url_prefix is None:
|
||||
logger.warning(
|
||||
"FILESERVER_URL_PREFIX is not set, fallback to http://localhost:8000/api/files"
|
||||
)
|
||||
file_url_prefix = "http://localhost:8000/api/files"
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
file_url = os.path.join(
|
||||
file_url_prefix,
|
||||
save_dir,
|
||||
new_file_name,
|
||||
)
|
||||
|
||||
return DocumentFile(
|
||||
id=file_id,
|
||||
name=new_file_name,
|
||||
type=extension,
|
||||
size=file_size,
|
||||
path=file_path,
|
||||
url=file_url,
|
||||
refs=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
|
||||
header, data = base64_content.split(",", 1)
|
||||
mime_type = header.split(";")[0].split(":", 1)[1]
|
||||
extension = mimetypes.guess_extension(mime_type).lstrip(".")
|
||||
# File data as bytes
|
||||
return base64.b64decode(data), extension
|
||||
|
||||
@staticmethod
|
||||
def _load_file_to_documents(file: DocumentFile) -> List[Document]:
|
||||
"""
|
||||
Load the file from the private directory and return the documents
|
||||
"""
|
||||
_, extension = os.path.splitext(file.name)
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
# Load file to documents
|
||||
# If LlamaParse is enabled, use it to parse the file
|
||||
# Otherwise, use the default file loaders
|
||||
reader = _get_llamaparse_parser()
|
||||
if reader is None:
|
||||
reader_cls = _default_file_loaders_map().get(f".{extension}")
|
||||
if reader_cls is None:
|
||||
raise ValueError(f"File extension {extension} is not supported")
|
||||
reader = reader_cls()
|
||||
if file.path is None:
|
||||
raise ValueError("Document file path is not set")
|
||||
documents = reader.load_data(Path(file.path))
|
||||
# Add custom metadata
|
||||
for doc in documents:
|
||||
doc.metadata["file_name"] = file.name
|
||||
doc.metadata["private"] = "true"
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def _add_documents_to_vector_store_index(
|
||||
documents: List[Document], index: VectorStoreIndex
|
||||
) -> None:
|
||||
"""
|
||||
Add the documents to the vector store index
|
||||
"""
|
||||
pipeline = IngestionPipeline()
|
||||
nodes = pipeline.run(documents=documents)
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
if index is None:
|
||||
index = VectorStoreIndex(nodes=nodes)
|
||||
else:
|
||||
index.insert_nodes(nodes=nodes)
|
||||
index.storage_context.persist(
|
||||
persist_dir=os.environ.get("STORAGE_DIR", "storage")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_file_to_llama_cloud_index(
|
||||
index: LlamaCloudIndex,
|
||||
file_name: str,
|
||||
file_data: bytes,
|
||||
) -> str:
|
||||
"""
|
||||
Add the file to the LlamaCloud index.
|
||||
LlamaCloudIndex is a managed index so we can directly use the files.
|
||||
"""
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
except ImportError as e:
|
||||
raise ValueError("LlamaCloudFileService is not found") from e
|
||||
|
||||
project_id = index._get_project_id()
|
||||
pipeline_id = index._get_pipeline_id()
|
||||
# LlamaCloudIndex is a managed index so we can directly use the files
|
||||
upload_file = (file_name, BytesIO(file_data))
|
||||
doc_id = LLamaCloudFileService.add_file_to_pipeline(
|
||||
project_id,
|
||||
pipeline_id,
|
||||
upload_file,
|
||||
custom_metadata={},
|
||||
)
|
||||
return doc_id
|
||||
|
||||
|
||||
def _sanitize_file_name(file_name: str) -> str:
|
||||
"""
|
||||
Sanitize the file name by replacing all non-alphanumeric characters with underscores
|
||||
"""
|
||||
sanitized_name = re.sub(r"[^a-zA-Z0-9.]", "_", file_name)
|
||||
return sanitized_name
|
||||
|
||||
|
||||
def _get_llamaparse_parser():
|
||||
from app.engine.loaders import load_configs
|
||||
from app.engine.loaders.file import FileLoaderConfig, llama_parse_parser
|
||||
|
||||
config = load_configs()
|
||||
file_loader_config = FileLoaderConfig(**config["file"])
|
||||
if file_loader_config.use_llama_parse:
|
||||
return llama_parse_parser()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _default_file_loaders_map():
|
||||
default_loaders = get_file_loaders_map()
|
||||
default_loaders[".txt"] = FlatReader
|
||||
default_loaders[".csv"] = FlatReader
|
||||
return default_loaders
|
||||
|
||||
|
||||
def _get_available_tools() -> Dict[str, List[FunctionTool]]:
|
||||
try:
|
||||
from app.engine.tools import ToolFactory
|
||||
except ImportError:
|
||||
logger.warning("ToolFactory not found, no tools will be available")
|
||||
return {}
|
||||
|
||||
try:
|
||||
tools = ToolFactory.from_env(map_result=True)
|
||||
return tools # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading tools from environment: {str(e)}")
|
||||
raise ValueError(f"Failed to get available tools: {str(e)}") from e
|
||||
@@ -11,19 +11,23 @@ export const dynamic = "force-dynamic";
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const {
|
||||
filename,
|
||||
name,
|
||||
base64,
|
||||
params,
|
||||
}: { filename: string; base64: string; params?: any } =
|
||||
await request.json();
|
||||
if (!base64 || !filename) {
|
||||
}: {
|
||||
name: string;
|
||||
base64: string;
|
||||
params?: any;
|
||||
} = await request.json();
|
||||
if (!base64 || !name) {
|
||||
return NextResponse.json(
|
||||
{ error: "base64 and filename is required in the request body" },
|
||||
{ error: "base64 and name is required in the request body" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const index = await getDataSource(params);
|
||||
return NextResponse.json(await uploadDocument(index, filename, base64));
|
||||
const documentFile = await uploadDocument(index, name, base64);
|
||||
return NextResponse.json(documentFile);
|
||||
} catch (error) {
|
||||
console.error("[Upload API]", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JSONValue } from "ai";
|
||||
import React from "react";
|
||||
import { DocumentFile } from ".";
|
||||
import { Button } from "../button";
|
||||
import { DocumentPreview } from "../document-preview";
|
||||
import FileUploader from "../file-uploader";
|
||||
@@ -65,8 +66,8 @@ export default function ChatInput(
|
||||
};
|
||||
|
||||
const handleUploadFile = async (file: File) => {
|
||||
if (imageUrl || files.length > 0) {
|
||||
alert("You can only upload one file at a time.");
|
||||
if (imageUrl) {
|
||||
alert("You can only upload one image at a time.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -95,9 +96,9 @@ export default function ChatInput(
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-4 w-full overflow-auto py-2">
|
||||
{files.map((file, index) => (
|
||||
{files.map((file: DocumentFile) => (
|
||||
<DocumentPreview
|
||||
key={file.metadata?.id ?? `${file.filename}-${index}`}
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRemove={() => removeDoc(file)}
|
||||
/>
|
||||
|
||||
@@ -6,10 +6,7 @@ export function ChatFiles({ data }: { data: DocumentFileData }) {
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
{data.files.map((file, index) => (
|
||||
<DocumentPreview
|
||||
key={file.metadata?.id ?? `${file.filename}-${index}`}
|
||||
file={file}
|
||||
/>
|
||||
<DocumentPreview key={file.id} file={file} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
-2
@@ -104,8 +104,8 @@ export function DocumentInfo({
|
||||
const fileExt = fileName?.split(".").pop() as DocumentFileType | undefined;
|
||||
|
||||
const previewFile = {
|
||||
filename: fileName,
|
||||
filetype: fileExt,
|
||||
name: fileName,
|
||||
type: fileExt as DocumentFileType,
|
||||
};
|
||||
|
||||
const DocumentDetail = (
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
DocumentFileType,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
UploadedFileMeta,
|
||||
} from "..";
|
||||
import { useClientConfig } from "./use-config";
|
||||
|
||||
@@ -24,14 +23,8 @@ export function useFile() {
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [files, setFiles] = useState<DocumentFile[]>([]);
|
||||
|
||||
const docEqual = (a: DocumentFile, b: DocumentFile) => {
|
||||
if (a.metadata?.id === b.metadata?.id) return true;
|
||||
if (a.filename === b.filename && a.filesize === b.filesize) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const addDoc = (file: DocumentFile) => {
|
||||
const existedFile = files.find((f) => docEqual(f, file));
|
||||
const existedFile = files.find((f) => f.id === file.id);
|
||||
if (!existedFile) {
|
||||
setFiles((prev) => [...prev, file]);
|
||||
return true;
|
||||
@@ -40,9 +33,7 @@ export function useFile() {
|
||||
};
|
||||
|
||||
const removeDoc = (file: DocumentFile) => {
|
||||
setFiles((prev) =>
|
||||
prev.filter((f) => f.metadata?.id !== file.metadata?.id),
|
||||
);
|
||||
setFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
@@ -53,7 +44,7 @@ export function useFile() {
|
||||
const uploadContent = async (
|
||||
file: File,
|
||||
requestParams: any = {},
|
||||
): Promise<UploadedFileMeta> => {
|
||||
): Promise<DocumentFile> => {
|
||||
const base64 = await readContent({ file, asUrl: true });
|
||||
const uploadAPI = `${backend}/api/chat/upload`;
|
||||
const response = await fetch(uploadAPI, {
|
||||
@@ -64,11 +55,11 @@ export function useFile() {
|
||||
body: JSON.stringify({
|
||||
...requestParams,
|
||||
base64,
|
||||
filename: file.name,
|
||||
name: file.name,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to upload document.");
|
||||
return (await response.json()) as UploadedFileMeta;
|
||||
return (await response.json()) as DocumentFile;
|
||||
};
|
||||
|
||||
const getAnnotations = () => {
|
||||
@@ -114,13 +105,7 @@ export function useFile() {
|
||||
|
||||
const filetype = docMineTypeMap[file.type];
|
||||
if (!filetype) throw new Error("Unsupported document type.");
|
||||
const uploadedFileMeta = await uploadContent(file, requestParams);
|
||||
const newDoc: DocumentFile = {
|
||||
filename: file.name,
|
||||
filesize: file.size,
|
||||
filetype,
|
||||
metadata: uploadedFileMeta,
|
||||
};
|
||||
const newDoc = await uploadContent(file, requestParams);
|
||||
return addDoc(newDoc);
|
||||
};
|
||||
|
||||
|
||||
@@ -27,18 +27,13 @@ export const DOCUMENT_FILE_TYPES: DocumentFileType[] = [
|
||||
"docx",
|
||||
];
|
||||
|
||||
export type UploadedFileMeta = {
|
||||
id: string;
|
||||
name: string; // The uploaded file name in the backend (including uuid and sanitized)
|
||||
url?: string;
|
||||
refs?: string[];
|
||||
};
|
||||
|
||||
export type DocumentFile = {
|
||||
filename: string; // The original file name
|
||||
filesize: number;
|
||||
filetype: DocumentFileType;
|
||||
metadata?: UploadedFileMeta; // undefined when the file is not uploaded yet
|
||||
id: string;
|
||||
name: string; // The uploaded file name in the backend
|
||||
size: number; // The file size in bytes
|
||||
type: DocumentFileType;
|
||||
url: string; // The URL of the uploaded file in the backend
|
||||
refs?: string[]; // DocumentIDs of the uploaded file in the vector index
|
||||
};
|
||||
|
||||
export type DocumentFileData = {
|
||||
|
||||
@@ -23,11 +23,11 @@ export interface DocumentPreviewProps {
|
||||
}
|
||||
|
||||
export function DocumentPreview(props: DocumentPreviewProps) {
|
||||
const { filename, filesize, filetype, metadata } = props.file;
|
||||
const { name, size, type, refs } = props.file;
|
||||
|
||||
if (metadata?.refs?.length) {
|
||||
if (refs?.length) {
|
||||
return (
|
||||
<div title={`Document IDs: ${metadata.refs.join(", ")}`}>
|
||||
<div title={`Document IDs: ${refs.join(", ")}`}>
|
||||
<PreviewCard {...props} />
|
||||
</div>
|
||||
);
|
||||
@@ -43,9 +43,9 @@ export function DocumentPreview(props: DocumentPreviewProps) {
|
||||
<DrawerContent className="w-3/5 mt-24 h-full max-h-[96%] ">
|
||||
<DrawerHeader className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<DrawerTitle>{filetype.toUpperCase()} Raw Content</DrawerTitle>
|
||||
<DrawerTitle>{type.toUpperCase()} Raw Content</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
{filename} ({inKB(filesize)} KB)
|
||||
{name} ({inKB(size)} KB)
|
||||
</DrawerDescription>
|
||||
</div>
|
||||
<DrawerClose asChild>
|
||||
@@ -53,9 +53,9 @@ export function DocumentPreview(props: DocumentPreviewProps) {
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<div className="m-4 max-h-[80%] overflow-auto">
|
||||
{metadata?.refs?.length && (
|
||||
{refs?.length && (
|
||||
<pre className="bg-secondary rounded-md p-4 block text-sm">
|
||||
{metadata.refs.join(", ")}
|
||||
{refs.join(", ")}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
@@ -73,9 +73,9 @@ export const FileIcon: Record<DocumentFileType, string> = {
|
||||
|
||||
export function PreviewCard(props: {
|
||||
file: {
|
||||
filename: string;
|
||||
filesize?: number;
|
||||
filetype?: DocumentFileType;
|
||||
name: string;
|
||||
size?: number;
|
||||
type: DocumentFileType;
|
||||
};
|
||||
onRemove?: () => void;
|
||||
className?: string;
|
||||
@@ -93,17 +93,17 @@ export function PreviewCard(props: {
|
||||
<Image
|
||||
className="h-full w-auto object-contain"
|
||||
priority
|
||||
src={FileIcon[file.filetype || "txt"]}
|
||||
src={FileIcon[file.type]}
|
||||
alt="Icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<div className="truncate font-semibold">
|
||||
{file.filename} {file.filesize ? `(${inKB(file.filesize)} KB)` : ""}
|
||||
{file.name} {file.size ? `(${inKB(file.size)} KB)` : ""}
|
||||
</div>
|
||||
{file.filetype && (
|
||||
{file.type && (
|
||||
<div className="truncate text-token-text-tertiary flex items-center gap-2">
|
||||
<span>{file.filetype.toUpperCase()} File</span>
|
||||
<span>{file.type.toUpperCase()} File</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user