Skip to content

Commit 75fad44

Browse files
feat(UI): Implementing File Upload and VectorDB Creation/Configuration in Playground (#3266)
1 parent 1a9fa3c commit 75fad44

File tree

8 files changed

+1945
-193
lines changed

8 files changed

+1945
-193
lines changed

llama_stack/ui/app/chat-playground/chunk-processor.test.tsx

Lines changed: 610 additions & 0 deletions
Large diffs are not rendered by default.

llama_stack/ui/app/chat-playground/page.test.tsx

Lines changed: 210 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const mockClient = {
3131
toolgroups: {
3232
list: jest.fn(),
3333
},
34+
vectorDBs: {
35+
list: jest.fn(),
36+
},
3437
};
3538

3639
jest.mock("@/hooks/use-auth-client", () => ({
@@ -164,7 +167,7 @@ describe("ChatPlaygroundPage", () => {
164167
session_name: "Test Session",
165168
started_at: new Date().toISOString(),
166169
turns: [],
167-
}); // No turns by default
170+
});
168171
mockClient.agents.retrieve.mockResolvedValue({
169172
agent_id: "test-agent",
170173
agent_config: {
@@ -417,7 +420,6 @@ describe("ChatPlaygroundPage", () => {
417420
});
418421

419422
await waitFor(() => {
420-
// first agent should be auto-selected
421423
expect(mockClient.agents.session.create).toHaveBeenCalledWith(
422424
"agent_123",
423425
{ session_name: "Default Session" }
@@ -464,7 +466,7 @@ describe("ChatPlaygroundPage", () => {
464466
});
465467
});
466468

467-
test("hides delete button when only one agent exists", async () => {
469+
test("shows delete button even when only one agent exists", async () => {
468470
mockClient.agents.list.mockResolvedValue({
469471
data: [mockAgents[0]],
470472
});
@@ -474,9 +476,7 @@ describe("ChatPlaygroundPage", () => {
474476
});
475477

476478
await waitFor(() => {
477-
expect(
478-
screen.queryByTitle("Delete current agent")
479-
).not.toBeInTheDocument();
479+
expect(screen.getByTitle("Delete current agent")).toBeInTheDocument();
480480
});
481481
});
482482

@@ -505,7 +505,7 @@ describe("ChatPlaygroundPage", () => {
505505
await waitFor(() => {
506506
expect(mockClient.agents.delete).toHaveBeenCalledWith("agent_123");
507507
expect(global.confirm).toHaveBeenCalledWith(
508-
"Are you sure you want to delete this agent? This action cannot be undone and will delete all associated sessions."
508+
"Are you sure you want to delete this agent? This action cannot be undone and will delete the agent and all its sessions."
509509
);
510510
});
511511

@@ -584,4 +584,207 @@ describe("ChatPlaygroundPage", () => {
584584
consoleSpy.mockRestore();
585585
});
586586
});
587+
588+
describe("RAG File Upload", () => {
589+
let mockFileReader: {
590+
readAsDataURL: jest.Mock;
591+
readAsText: jest.Mock;
592+
result: string | null;
593+
onload: (() => void) | null;
594+
onerror: (() => void) | null;
595+
};
596+
let mockRAGTool: {
597+
insert: jest.Mock;
598+
};
599+
600+
beforeEach(() => {
601+
mockFileReader = {
602+
readAsDataURL: jest.fn(),
603+
readAsText: jest.fn(),
604+
result: null,
605+
onload: null,
606+
onerror: null,
607+
};
608+
global.FileReader = jest.fn(() => mockFileReader);
609+
610+
mockRAGTool = {
611+
insert: jest.fn().mockResolvedValue({}),
612+
};
613+
mockClient.toolRuntime = {
614+
ragTool: mockRAGTool,
615+
};
616+
});
617+
618+
afterEach(() => {
619+
jest.clearAllMocks();
620+
});
621+
622+
test("handles text file upload", async () => {
623+
new File(["Hello, world!"], "test.txt", {
624+
type: "text/plain",
625+
});
626+
627+
mockClient.agents.retrieve.mockResolvedValue({
628+
agent_id: "test-agent",
629+
agent_config: {
630+
toolgroups: [
631+
{
632+
name: "builtin::rag/knowledge_search",
633+
args: { vector_db_ids: ["test-vector-db"] },
634+
},
635+
],
636+
},
637+
});
638+
639+
await act(async () => {
640+
render(<ChatPlaygroundPage />);
641+
});
642+
643+
await waitFor(() => {
644+
expect(screen.getByTestId("chat-component")).toBeInTheDocument();
645+
});
646+
647+
const chatComponent = screen.getByTestId("chat-component");
648+
chatComponent.getAttribute("data-onragfileupload");
649+
650+
// this is a simplified test
651+
expect(mockRAGTool.insert).not.toHaveBeenCalled();
652+
});
653+
654+
test("handles PDF file upload with FileReader", async () => {
655+
new File([new ArrayBuffer(1000)], "test.pdf", {
656+
type: "application/pdf",
657+
});
658+
659+
const mockDataURL = "data:application/pdf;base64,JVBERi0xLjQK";
660+
mockFileReader.result = mockDataURL;
661+
662+
mockClient.agents.retrieve.mockResolvedValue({
663+
agent_id: "test-agent",
664+
agent_config: {
665+
toolgroups: [
666+
{
667+
name: "builtin::rag/knowledge_search",
668+
args: { vector_db_ids: ["test-vector-db"] },
669+
},
670+
],
671+
},
672+
});
673+
674+
await act(async () => {
675+
render(<ChatPlaygroundPage />);
676+
});
677+
678+
await waitFor(() => {
679+
expect(screen.getByTestId("chat-component")).toBeInTheDocument();
680+
});
681+
682+
expect(global.FileReader).toBeDefined();
683+
});
684+
685+
test("handles different file types correctly", () => {
686+
const getContentType = (filename: string): string => {
687+
const ext = filename.toLowerCase().split(".").pop();
688+
switch (ext) {
689+
case "pdf":
690+
return "application/pdf";
691+
case "txt":
692+
return "text/plain";
693+
case "md":
694+
return "text/markdown";
695+
case "html":
696+
return "text/html";
697+
case "csv":
698+
return "text/csv";
699+
case "json":
700+
return "application/json";
701+
case "docx":
702+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
703+
case "doc":
704+
return "application/msword";
705+
default:
706+
return "application/octet-stream";
707+
}
708+
};
709+
710+
expect(getContentType("test.pdf")).toBe("application/pdf");
711+
expect(getContentType("test.txt")).toBe("text/plain");
712+
expect(getContentType("test.md")).toBe("text/markdown");
713+
expect(getContentType("test.html")).toBe("text/html");
714+
expect(getContentType("test.csv")).toBe("text/csv");
715+
expect(getContentType("test.json")).toBe("application/json");
716+
expect(getContentType("test.docx")).toBe(
717+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
718+
);
719+
expect(getContentType("test.doc")).toBe("application/msword");
720+
expect(getContentType("test.unknown")).toBe("application/octet-stream");
721+
});
722+
723+
test("determines text vs binary file types correctly", () => {
724+
const isTextFile = (mimeType: string): boolean => {
725+
return (
726+
mimeType.startsWith("text/") ||
727+
mimeType === "application/json" ||
728+
mimeType === "text/markdown" ||
729+
mimeType === "text/html" ||
730+
mimeType === "text/csv"
731+
);
732+
};
733+
734+
expect(isTextFile("text/plain")).toBe(true);
735+
expect(isTextFile("text/markdown")).toBe(true);
736+
expect(isTextFile("text/html")).toBe(true);
737+
expect(isTextFile("text/csv")).toBe(true);
738+
expect(isTextFile("application/json")).toBe(true);
739+
740+
expect(isTextFile("application/pdf")).toBe(false);
741+
expect(isTextFile("application/msword")).toBe(false);
742+
expect(
743+
isTextFile(
744+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
745+
)
746+
).toBe(false);
747+
expect(isTextFile("application/octet-stream")).toBe(false);
748+
});
749+
750+
test("handles FileReader error gracefully", async () => {
751+
const pdfFile = new File([new ArrayBuffer(1000)], "test.pdf", {
752+
type: "application/pdf",
753+
});
754+
755+
mockFileReader.onerror = jest.fn();
756+
const mockError = new Error("FileReader failed");
757+
758+
const fileReaderPromise = new Promise<string>((resolve, reject) => {
759+
const reader = new FileReader();
760+
reader.onload = () => resolve(reader.result as string);
761+
reader.onerror = () => reject(reader.error || mockError);
762+
reader.readAsDataURL(pdfFile);
763+
764+
setTimeout(() => {
765+
reader.onerror?.(new ProgressEvent("error"));
766+
}, 0);
767+
});
768+
769+
await expect(fileReaderPromise).rejects.toBeDefined();
770+
});
771+
772+
test("handles large file upload with FileReader approach", () => {
773+
// create a large file
774+
const largeFile = new File(
775+
[new ArrayBuffer(10 * 1024 * 1024)],
776+
"large.pdf",
777+
{
778+
type: "application/pdf",
779+
}
780+
);
781+
782+
expect(largeFile.size).toBe(10 * 1024 * 1024); // 10MB
783+
784+
expect(global.FileReader).toBeDefined();
785+
786+
const reader = new FileReader();
787+
expect(reader.readAsDataURL).toBeDefined();
788+
});
789+
});
587790
});

0 commit comments

Comments
 (0)