Skip to content

feat: support uploading pdf, docx, txt #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 78 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
8db5405
feat: upload pdf and send content to LLM
thucpn Jun 24, 2024
0d140f4
Merge branch 'main' into feat/upload-pdf
thucpn Jun 24, 2024
5a06cf9
fix: lint
thucpn Jun 26, 2024
97af04e
refactor: move embed to chat api folder
thucpn Jun 26, 2024
312b6d3
refactor: file content preview component
thucpn Jun 26, 2024
d73ee43
refactor: use content file type for all text files
thucpn Jun 26, 2024
d458783
Merge branch 'main' into feat/upload-pdf
thucpn Jun 26, 2024
035e96e
fix: lint
thucpn Jun 26, 2024
f452027
use pipeline transformation & upgrade llamaindex latest
thucpn Jun 26, 2024
f707c6f
use svg image for pdf, docx, txt
thucpn Jun 26, 2024
2d19560
fix: body collapsed when open dialog
thucpn Jun 26, 2024
04a4dc9
return backend for useClientConfig only
thucpn Jun 26, 2024
4ba4c64
fix: lint
thucpn Jun 26, 2024
fa991f4
Create late-weeks-sneeze.md
thucpn Jun 26, 2024
9c8192d
refactor: rename ContentFile to DocumentFile
thucpn Jun 26, 2024
7ff4843
refactor: rename annotation type
thucpn Jun 26, 2024
3ca2f65
refactor: document preview component
thucpn Jun 26, 2024
689ad9b
fix: lint
thucpn Jun 26, 2024
ee8cb00
refactor: move upload logic to useFile
thucpn Jun 26, 2024
28696d5
refactor: use PDFReader and TextNode
marcusschiesser Jun 26, 2024
cca49c5
fix: next.config
marcusschiesser Jun 26, 2024
afb5405
feat: support uploading docx, pdf, txt
thucpn Jun 27, 2024
4b66d29
move embeddng to chat folder
thucpn Jun 27, 2024
d07ffe9
feat: add embed api for express
thucpn Jun 27, 2024
948b1b6
fix: lint
thucpn Jun 27, 2024
0a195f8
feat: use local index
marcusschiesser Jun 27, 2024
d22310d
Merge branch 'main' into feat/upload-pdf
marcusschiesser Jul 3, 2024
aff87bb
add todos for using doc ids
marcusschiesser Jul 3, 2024
38f231c
add support for fastapi
leehuwuj Jul 5, 2024
2629c88
add filters
leehuwuj Jul 8, 2024
c21e843
fix chat filering python
leehuwuj Jul 8, 2024
34ab445
fix instantiate reader
leehuwuj Jul 8, 2024
321d77d
add save file and fix issues
leehuwuj Jul 8, 2024
259c3ec
change to /chat/upload route
leehuwuj Jul 8, 2024
d6afe28
refactor(frontend): support ref type for document content
thucpn Jul 8, 2024
6298e4b
refactor(nextjs): rename route /embed to /upload
thucpn Jul 8, 2024
608a338
feat: save document when uploading document
thucpn Jul 8, 2024
a9fa5cd
feat: add nodes to vectorstore and query
thucpn Jul 8, 2024
a00cb3d
feat: update document metadata from uploaded file infor
thucpn Jul 8, 2024
425580d
feat: get query filters from document ids
thucpn Jul 8, 2024
3b1c743
refactor(express): use new llamaindex for sharing chat logic between …
thucpn Jul 8, 2024
7a0ce3f
docs: remove useless log
thucpn Jul 8, 2024
d5f4395
fix: wrong import embedding path
thucpn Jul 8, 2024
cc15059
fix: persist vectordb
thucpn Jul 8, 2024
efdd43f
feat: don't open preview dialog for ref content
thucpn Jul 8, 2024
07e0821
use in filter operator
leehuwuj Jul 9, 2024
709ef1f
use mimetypes lib and change private file folder
leehuwuj Jul 9, 2024
43e8035
change tool-output to tool/output
leehuwuj Jul 9, 2024
fdb32b7
add back csv handler
leehuwuj Jul 9, 2024
445b4cc
add a prefix message if user uploaded a file
leehuwuj Jul 10, 2024
a2787ae
add txt reader and fix typo
leehuwuj Jul 10, 2024
d48bcb8
improve code
leehuwuj Jul 10, 2024
93fde20
fix: construct file url from private and file_name
thucpn Jul 10, 2024
884bc6d
refactor: rename embeddings to documents
thucpn Jul 10, 2024
2cc21ac
refactor: split llamaindex folder
thucpn Jul 10, 2024
ce9ce5e
fix: import path to llamaindex ts folder
thucpn Jul 10, 2024
27332e6
fix: wrong engine path
thucpn Jul 10, 2024
c3f70a1
remove redundant log
leehuwuj Jul 10, 2024
20a58c1
remove wrong log
leehuwuj Jul 10, 2024
ab279c6
update file upload to only send text content instead of list
leehuwuj Jul 10, 2024
b638eae
add missing fe and use flatreader
leehuwuj Jul 10, 2024
6aa7d57
improve code
leehuwuj Jul 10, 2024
1f85358
remove adding message prefix
leehuwuj Jul 10, 2024
498723a
update milvus package
leehuwuj Jul 11, 2024
be45b3f
improve log
leehuwuj Jul 11, 2024
5c8e79c
Merge remote-tracking branch 'origin/main' into feat/upload-pdf
leehuwuj Jul 11, 2024
695923c
update code comments
leehuwuj Jul 11, 2024
0e8786b
fix: use makeDir function with default recursive option
thucpn Jul 11, 2024
3404554
fix: lint
thucpn Jul 11, 2024
0ccb51e
fix: set request body size
thucpn Jul 11, 2024
55684b2
fix: lint
thucpn Jul 11, 2024
197cc90
cleanup PR
marcusschiesser Jul 11, 2024
e9ad3ed
fix: DocumentFileContent value can be string in backend ts
thucpn Jul 11, 2024
503141f
Update templates/components/vectordbs/python/none/generate.py
marcusschiesser Jul 11, 2024
30ebbe3
improve code
leehuwuj Jul 11, 2024
f670b1a
fix: while testing fastapi contextengine
marcusschiesser Jul 11, 2024
43149bf
refactor: clean streaming
marcusschiesser Jul 11, 2024
8068ad5
fix: use all annotations in TS code
marcusschiesser Jul 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "llamaindex";

import { AgentStreamChatResponse } from "llamaindex/agent/base";
import { CsvFile, appendSourceData } from "./stream-helper";
import { CsvFile, PdfFile, appendSourceData } from "./stream-helper";

type LlamaIndexResponse =
| AgentStreamChatResponse<ToolCallLLMMessageOptions>
Expand Down Expand Up @@ -81,6 +81,18 @@ const convertAnnotations = (
text: csvContent,
});
}
// convert PDF files to text
if (type === "pdf" && "pdfFiles" in data && Array.isArray(data.pdfFiles)) {
const pdfContent = data.pdfFiles
.map((pdf) => {
return "```pdf\n" + (pdf as PdfFile).content + "\n```";
})
.join("\n\n");
content.push({
type: "text",
text: pdfContent,
});
}
});

return content;
Expand Down
13 changes: 13 additions & 0 deletions templates/types/streaming/express/src/controllers/stream-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,16 @@ export type CsvFile = {
filesize: number;
id: string;
};

export type TextEmbedding = {
text: string;
embedding: number[];
};

export type PdfFile = {
id: string;
content: string;
filename: string;
filesize: number;
embeddings: TextEmbedding[];
};
28 changes: 28 additions & 0 deletions templates/types/streaming/nextjs/app/api/chat/embed/embeddings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Document, MetadataMode, Settings, SimpleNodeParser } from "llamaindex";
import pdf from "pdf-parse";

export async function splitAndEmbed(document: string) {
const nodeParser = new SimpleNodeParser({
chunkSize: Settings.chunkSize,
chunkOverlap: Settings.chunkOverlap,
});
const nodes = nodeParser.getNodesFromDocuments([
new Document({ text: document }),
]);
const texts = nodes.map((node) => node.getContent(MetadataMode.EMBED));
const embeddings = await Settings.embedModel.getTextEmbeddingsBatch(texts);
return nodes.map((node, i) => ({
text: node.getContent(MetadataMode.NONE),
embedding: embeddings[i],
}));
}

export async function getPdfDetail(rawPdf: string) {
const pdfBuffer = Buffer.from(rawPdf.split(",")[1], "base64");
const content = (await pdf(pdfBuffer)).text;
const embeddings = await splitAndEmbed(content);
return {
content,
embeddings,
};
}
28 changes: 28 additions & 0 deletions templates/types/streaming/nextjs/app/api/chat/embed/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from "next/server";
import { initSettings } from "../engine/settings";
import { getPdfDetail } from "./embeddings";

initSettings();

export async function POST(request: NextRequest) {
try {
const { pdf }: { pdf: string } = await request.json();
if (!pdf) {
return NextResponse.json(
{ error: "pdf is required in the request body" },
{ status: 400 },
);
}
const pdfDetail = await getPdfDetail(pdf);
return NextResponse.json(pdfDetail);
} catch (error) {
console.error("[Embed API]", error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 },
);
}
}

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "llamaindex";

import { AgentStreamChatResponse } from "llamaindex/agent/base";
import { CsvFile, appendSourceData } from "./stream-helper";
import { CsvFile, PdfFile, appendSourceData } from "./stream-helper";

type LlamaIndexResponse =
| AgentStreamChatResponse<ToolCallLLMMessageOptions>
Expand Down Expand Up @@ -81,6 +81,18 @@ const convertAnnotations = (
text: csvContent,
});
}
// convert PDF files to text
if (type === "pdf" && "pdfFiles" in data && Array.isArray(data.pdfFiles)) {
const pdfContent = data.pdfFiles
.map((pdf) => {
return "```pdf\n" + (pdf as PdfFile).content + "\n```";
})
.join("\n\n");
content.push({
type: "text",
text: pdfContent,
});
}
});

return content;
Expand Down
13 changes: 13 additions & 0 deletions templates/types/streaming/nextjs/app/api/chat/stream-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,16 @@ export type CsvFile = {
filesize: number;
id: string;
};

export type TextEmbedding = {
text: string;
embedding: number[];
};

export type PdfFile = {
id: string;
content: string;
filename: string;
filesize: number;
embeddings: TextEmbedding[];
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import FileUploader from "../file-uploader";
import { Input } from "../input";
import UploadCsvPreview from "../upload-csv-preview";
import UploadImagePreview from "../upload-image-preview";
import UploadPdfPreview from "../upload-pdf-preview";
import { ChatHandler } from "./chat.interface";
import { useCsv } from "./hooks/use-csv";
import { usePdf } from "./hooks/use-pdf ";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the import statement for usePdf.

There is an extra space at the end of the import path for usePdf. This could potentially lead to module resolution errors.

- import { usePdf } from "./hooks/use-pdf ";
+ import { usePdf } from "./hooks/use-pdf";
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { usePdf } from "./hooks/use-pdf ";
import { usePdf } from "./hooks/use-pdf";


export default function ChatInput(
props: Pick<
Expand All @@ -26,9 +28,10 @@ export default function ChatInput(
) {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const { files: csvFiles, upload, remove, reset } = useCsv();
const { pdf, setPdf, uploadAndEmbed } = usePdf();

const getAnnotations = () => {
if (!imageUrl && csvFiles.length === 0) return undefined;
if (!imageUrl && csvFiles.length === 0 && !pdf) return undefined;
const annotations: MessageAnnotation[] = [];
if (imageUrl) {
annotations.push({
Expand All @@ -49,6 +52,22 @@ export default function ChatInput(
},
});
}
if (pdf) {
annotations.push({
type: MessageAnnotationType.PDF,
data: {
pdfFiles: [
{
id: pdf.id,
content: pdf.content,
filename: pdf.filename,
filesize: pdf.filesize,
embeddings: pdf.embeddings,
},
],
},
});
}
return annotations as JSONValue[];
};

Expand All @@ -74,6 +93,7 @@ export default function ChatInput(
handleSubmitWithAnnotations(e, annotations);
imageUrl && setImageUrl(null);
csvFiles.length && reset();
pdf && setPdf(null);
return;
}
props.handleSubmit(e);
Expand All @@ -84,7 +104,7 @@ export default function ChatInput(
const readContent = async (file: File): Promise<string> => {
const content = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
if (file.type.startsWith("image/")) {
if (file.type.startsWith("image/") || file.type === "application/pdf") {
reader.readAsDataURL(file);
} else {
reader.readAsText(file);
Expand Down Expand Up @@ -113,6 +133,16 @@ export default function ChatInput(
}
};

const handleUploadPdfFile = async (file: File) => {
const base64 = await readContent(file);
await uploadAndEmbed({
id: uuidv4(),
filename: file.name,
filesize: file.size,
pdfBase64: base64,
});
};

const handleUploadFile = async (file: File) => {
try {
if (file.type.startsWith("image/")) {
Expand All @@ -125,6 +155,13 @@ export default function ChatInput(
}
return await handleUploadCsvFile(file);
}
if (file.type === "application/pdf") {
if (pdf) {
alert("You can only upload one pdf file at a time.");
return;
}
return await handleUploadPdfFile(file);
}
props.onFileUpload?.(file);
} catch (error: any) {
props.onFileError?.(error.message);
Expand Down Expand Up @@ -152,6 +189,7 @@ export default function ChatInput(
})}
</div>
)}
{pdf && <UploadPdfPreview pdf={pdf} onRemove={() => setPdf(null)} />}
<div className="flex w-full items-start justify-between gap-4 ">
<Input
autoFocus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { Check, Copy } from "lucide-react";
import { Message } from "ai";
import { Fragment } from "react";
import { Button } from "../../button";
import UploadPdfPreview from "../../upload-pdf-preview";
import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
import {
CsvData,
EventData,
ImageData,
MessageAnnotation,
MessageAnnotationType,
PDFData,
SourceData,
ToolData,
getAnnotationData,
Expand Down Expand Up @@ -45,6 +47,10 @@ function ChatMessageContent({
annotations,
MessageAnnotationType.CSV,
);
const pdfData = getAnnotationData<PDFData>(
annotations,
MessageAnnotationType.PDF,
);
const eventData = getAnnotationData<EventData>(
annotations,
MessageAnnotationType.EVENTS,
Expand Down Expand Up @@ -74,6 +80,12 @@ function ChatMessageContent({
order: 2,
component: csvData[0] ? <CsvContent data={csvData[0]} /> : null,
},
{
order: 3,
component: pdfData[0] ? (
<UploadPdfPreview pdf={pdfData[0].pdfFiles[0]} />
) : null,
},
{
order: -1,
component: toolData[0] ? <ChatTools data={toolData[0]} /> : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ import { useEffect, useMemo, useState } from "react";

export interface ChatConfig {
chatAPI?: string;
embedAPI?: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add error handling for configuration fetching.

While the integration of embedAPI is well-handled, consider adding error handling for the fetch operation in the useEffect to manage potential failures more gracefully.

-      .catch((error) => console.error("Error fetching config", error));
+      .catch((error) => {
+          console.error("Error fetching config", error);
+          setConfig({ ...config, error: "Failed to fetch configuration" });
+      });

Also applies to: 33-33

starterQuestions?: string[];
}

export function useClientConfig() {
const API_ROUTE = "/api/chat/config";
export function useClientConfig(): ChatConfig {
const chatAPI = process.env.NEXT_PUBLIC_CHAT_API;
const [config, setConfig] = useState<ChatConfig>({
chatAPI,
});

const configAPI = useMemo(() => {
const backendOrigin = chatAPI ? new URL(chatAPI).origin : "";
return `${backendOrigin}${API_ROUTE}`;
const backendOrigin = useMemo(() => {
return chatAPI ? new URL(chatAPI).origin : "";
}, [chatAPI]);

const configAPI = `${backendOrigin}/api/chat/config`;
const embedAPI = `${backendOrigin}/api/chat/embed`;

useEffect(() => {
fetch(configAPI)
.then((response) => response.json())
.then((data) => setConfig({ ...data, chatAPI }))
.catch((error) => console.error("Error fetching config", error));
}, [chatAPI, configAPI]);

return config;
return {
chatAPI,
embedAPI,
starterQuestions: config.starterQuestions,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { useState } from "react";
import { PdfFile } from "..";
import { useClientConfig } from "./use-config";

export function usePdf() {
const { embedAPI } = useClientConfig();
const [pdf, setPdf] = useState<PdfFile | null>(null);

const getPdfDetail = async (
pdfBase64: string,
): Promise<Pick<PdfFile, "content" | "embeddings">> => {
if (!embedAPI) throw new Error("Embed API is not defined");
const response = await fetch(embedAPI, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
pdf: pdfBase64,
}),
});
if (!response.ok) throw new Error("Failed to get pdf detail");
const data = await response.json();
return data;
};

const uploadAndEmbed = async (pdf: {
id: string;
filename: string;
filesize: number;
pdfBase64: string;
}) => {
const { pdfBase64, ...rest } = pdf;
const pdfDetail = await getPdfDetail(pdfBase64);
setPdf({ ...pdfDetail, ...rest });
return pdfDetail;
};

return { pdf, setPdf, uploadAndEmbed };
}
Loading
Loading