Skip to content
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

Feat/admin/thread-knowledge #1016

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
6 changes: 3 additions & 3 deletions pkg/api/router/router.go
Copy link
Contributor Author

Choose a reason for hiding this comment

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

supermassive backend changes here

Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ func Router(services *services.Services) (http.Handler, error) {
mux.HandleFunc("DELETE /api/threads/{id}/files/{file...}", threads.DeleteFile)

// Thread knowledge files
mux.HandleFunc("GET /api/threads/{id}/knowledge", threads.Knowledge)
mux.HandleFunc("POST /api/threads/{id}/knowledge/{file}", threads.UploadKnowledge)
mux.HandleFunc("DELETE /api/threads/{id}/knowledge/{file...}", threads.DeleteKnowledge)
mux.HandleFunc("GET /api/threads/{id}/knowledge-files", threads.Knowledge)
mux.HandleFunc("POST /api/threads/{id}/knowledge-files/{file}", threads.UploadKnowledge)
mux.HandleFunc("DELETE /api/threads/{id}/knowledge-files/{file...}", threads.DeleteKnowledge)

// ToolRefs
mux.HandleFunc("GET /api/tool-references", toolRefs.List)
Expand Down
3 changes: 3 additions & 0 deletions ui/admin/app/components/chat/ChatActions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cn } from "~/lib/utils";

import { useChat } from "~/components/chat/ChatContext";
import { KnowledgeInfo } from "~/components/chat/chat-actions/KnowledgeInfo";
import { ToolsInfo } from "~/components/chat/chat-actions/ToolsInfo";
import {
useOptimisticThread,
Expand All @@ -24,6 +25,8 @@ export function ChatActions({ className }: { className?: string }) {
agent={agent}
disabled={!thread}
/>

{threadId && <KnowledgeInfo threadId={threadId} />}
</div>
</div>
);
Expand Down
140 changes: 101 additions & 39 deletions ui/admin/app/components/chat/chat-actions/KnowledgeInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { LibraryIcon } from "lucide-react";
import { LibraryIcon, PlusIcon } from "lucide-react";
import { useRef } from "react";

import { KnowledgeFile } from "~/lib/model/knowledge";
import { KNOWLEDGE_TOOL } from "~/lib/model/agents";
import { KnowledgeFileNamespace } from "~/lib/model/knowledge";
import { cn } from "~/lib/utils";

import { TypographyMuted } from "~/components/Typography";
import { TypographyLead, TypographySmall } from "~/components/Typography";
import { useThreadAgents } from "~/components/chat/thread-helpers";
import { KnowledgeFileItem } from "~/components/knowledge/KnowledgeFileItem";
import { Button } from "~/components/ui/button";
import {
Popover,
Expand All @@ -15,49 +19,107 @@ import {
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { useKnowledgeFiles } from "~/hooks/knowledge/useKnowledgeFiles";
import { useMultiAsync } from "~/hooks/useMultiAsync";

export function KnowledgeInfo({
knowledge,
threadId,
className,
disabled,
}: {
knowledge: KnowledgeFile[];
threadId: string;
className?: string;
disabled?: boolean;
}) {
const inputRef = useRef<HTMLInputElement>(null);

const {
localFiles: knowledge,
addKnowledgeFile,
deleteKnowledgeFile,
reingestFile,
} = useKnowledgeFiles(KnowledgeFileNamespace.Threads, threadId);

const { data: agent } = useThreadAgents(threadId);

const uploadKnowledge = useMultiAsync((_index: number, file: File) =>
addKnowledgeFile(file)
);

const startUpload = (files: FileList) => {
if (!files.length) return;

uploadKnowledge.execute(Array.from(files).map((file) => [file]));

if (inputRef.current) inputRef.current.value = "";
};

const disabled = !agent?.tools?.includes(KNOWLEDGE_TOOL);

return (
<Tooltip>
<TooltipContent>Knowledge</TooltipContent>

<Popover>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
size="icon-sm"
variant="outline"
className={cn("gap-2", className)}
startContent={<LibraryIcon />}
disabled={disabled}
/>
</PopoverTrigger>
</TooltipTrigger>

<PopoverContent>
{knowledge.length > 0 ? (
<div className="space-y-2">
{knowledge.map((file) => (
<TypographyMuted key={file.id}>
{file.fileName}
</TypographyMuted>
))}
<>
<Tooltip>
<TooltipContent>
Knowledge {disabled && "(disabled for agent)"}
</TooltipContent>

<Popover>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
size="icon-sm"
variant="outline"
className={cn("gap-2", className)}
startContent={<LibraryIcon />}
disabled={disabled}
/>
</PopoverTrigger>
</TooltipTrigger>

<PopoverContent align="start" className="w-[30vw]">
<div className="flex justify-between items-center gap-2 mb-4">
<TypographyLead>Knowledge</TypographyLead>

<TypographySmall className="text-muted-foreground">
{knowledge.length || "No"} files
</TypographySmall>
</div>
) : (
<TypographyMuted>
No knowledge available
</TypographyMuted>
)}
</PopoverContent>
</Popover>
</Tooltip>

<div className="flex flex-col gap-2">
<div className="space-y-2">
{knowledge.map((file) => (
<KnowledgeFileItem
key={file.id}
file={file}
onDelete={deleteKnowledgeFile}
onReingest={(file) =>
reingestFile(file.id!)
}
/>
))}
</div>

<Button
onClick={() => inputRef.current?.click()}
startContent={<PlusIcon />}
variant="ghost"
className="self-end"
>
Add Knowledge
</Button>
</div>
</PopoverContent>
</Popover>
</Tooltip>

<input
type="file"
className="hidden"
ref={inputRef}
multiple
onChange={(e) => {
if (!e.target.files) return;
startUpload(e.target.files);
}}
/>
</>
);
}
7 changes: 5 additions & 2 deletions ui/admin/app/components/chat/thread-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import useSWR from "swr";

import { UpdateThread } from "~/lib/model/threads";
import { AgentService } from "~/lib/service/api/agentService";
import { KnowledgeFileService } from "~/lib/service/api/knowledgeFileApiService";
import { ThreadsService } from "~/lib/service/api/threadsService";

import { useAsync } from "~/hooks/useAsync";
Expand Down Expand Up @@ -43,8 +44,10 @@ export function useOptimisticThread(threadId?: Nullish<string>) {
}

export function useThreadKnowledge(threadId?: Nullish<string>) {
return useSWR(ThreadsService.getKnowledge.key(threadId), ({ threadId }) =>
ThreadsService.getKnowledge(threadId)
return useSWR(
KnowledgeFileService.getKnowledgeFiles.key("threads", threadId),
({ agentId, namespace }) =>
KnowledgeFileService.getKnowledgeFiles(namespace, agentId)
);
}

Expand Down
4 changes: 2 additions & 2 deletions ui/admin/app/components/knowledge/AgentKnowledgePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function AgentKnowledgePanel({
);

const { localFiles, addKnowledgeFile, deleteKnowledgeFile, reingestFile } =
useKnowledgeFiles(agentId);
useKnowledgeFiles("agents", agentId);

const {
knowledgeSources,
Expand All @@ -67,7 +67,7 @@ export default function AgentKnowledgePanel({
addWebsite,
addOneDrive,
addNotion,
} = useKnowledgeSources(agentId);
} = useKnowledgeSources("agents", agentId);

const selectedKnowledgeSource = knowledgeSources.find(
(source) => source.id === selectedKnowledgeSourceId
Expand Down
4 changes: 2 additions & 2 deletions ui/admin/app/components/knowledge/KnowledgeFileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface KnowledgeFileItemProps {
file: KnowledgeFile;
onDelete: (file: KnowledgeFile) => void;
onReingest: (file: KnowledgeFile) => void;
onViewError: (error: string) => void;
onViewError?: (error: string) => void;
}

export function KnowledgeFileItem({
Expand Down Expand Up @@ -65,7 +65,7 @@ export function KnowledgeFileItem({
variant="ghost"
size="icon"
onClick={() =>
onViewError(file.error ?? "")
onViewError?.(file.error ?? "")
}
>
<EyeIcon className="w-4 h-4 text-destructive" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const KnowledgeSourceDetail: FC<KnowledgeSourceDetailProps> = ({
const scrollPosition = useRef(0);

const { files, reingestFile, approveFile } = useKnowledgeSourceFiles(
"agents",
agentId,
knowledgeSource
);
Expand Down
28 changes: 19 additions & 9 deletions ui/admin/app/hooks/knowledge/useKnowledgeFiles.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { useMemo, useState } from "react";
import useSWR from "swr";

import { KnowledgeFile, KnowledgeFileState } from "~/lib/model/knowledge";
import { KnowledgeService } from "~/lib/service/api/knowledgeService";
import {
KnowledgeFile,
KnowledgeFileNamespace,
KnowledgeFileState,
} from "~/lib/model/knowledge";
import { KnowledgeFileService } from "~/lib/service/api/knowledgeFileApiService";

export function useKnowledgeFiles(agentId: string) {
export function useKnowledgeFiles(
namespace: KnowledgeFileNamespace,
agentId: string
) {
const [blockPollingLocalFiles, setBlockPollingLocalFiles] = useState(false);

const {
data: files,
mutate: mutateFiles,
...rest
} = useSWR(
KnowledgeService.getLocalKnowledgeFilesForAgent.key(agentId),
({ agentId }) =>
KnowledgeService.getLocalKnowledgeFilesForAgent(agentId),
KnowledgeFileService.getKnowledgeFiles.key(namespace, agentId),
({ namespace, agentId }) =>
KnowledgeFileService.getKnowledgeFiles(namespace, agentId),
{
revalidateOnFocus: false,
refreshInterval: blockPollingLocalFiles ? undefined : 5000,
Expand Down Expand Up @@ -45,7 +52,8 @@ export function useKnowledgeFiles(agentId: string) {
}

const addKnowledgeFile = async (file: File) => {
const addedFile = await KnowledgeService.addKnowledgeFilesToAgent(
const addedFile = await KnowledgeFileService.addKnowledgeFiles(
namespace,
agentId,
file
);
Expand All @@ -54,15 +62,17 @@ export function useKnowledgeFiles(agentId: string) {
};

const deleteKnowledgeFile = async (file: KnowledgeFile) => {
await KnowledgeService.deleteKnowledgeFileFromAgent(
await KnowledgeFileService.deleteKnowledgeFile(
namespace,
agentId,
file.fileName
);
mutateFiles((prev) => prev?.filter((f) => f.id !== file.id));
};

const reingestFile = async (fileId: string) => {
const reingestedFile = await KnowledgeService.reingestFile(
const reingestedFile = await KnowledgeFileService.reingestFile(
namespace,
agentId,
fileId
);
Expand Down
32 changes: 23 additions & 9 deletions ui/admin/app/hooks/knowledge/useKnowledgeSourceFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import useSWR from "swr";
import {
KnowledgeFile,
KnowledgeFileState,
KnowledgeNamespace,
KnowledgeSource,
KnowledgeSourceStatus,
} from "~/lib/model/knowledge";
import { KnowledgeService } from "~/lib/service/api/knowledgeService";
import { KnowledgeSourceApiService } from "~/lib/service/api/knowledgeSourceApiService";
import { handlePromise } from "~/lib/service/async";

export function useKnowledgeSourceFiles(
namespace: KnowledgeNamespace,
agentId: string,
knowledgeSource: KnowledgeSource
) {
Expand All @@ -32,12 +34,17 @@ export function useKnowledgeSourceFiles(
mutate: mutateFiles,
...rest
} = useSWR(
KnowledgeService.getFilesForKnowledgeSource.key(
KnowledgeSourceApiService.getFilesForKnowledgeSource.key(
namespace,
agentId,
knowledgeSource.id
),
({ agentId, sourceId }) =>
KnowledgeService.getFilesForKnowledgeSource(agentId, sourceId),
KnowledgeSourceApiService.getFilesForKnowledgeSource(
namespace,
agentId,
sourceId
),
{
revalidateOnFocus: false,
refreshInterval: blockPollingFiles ? undefined : 5000,
Expand Down Expand Up @@ -76,19 +83,26 @@ export function useKnowledgeSourceFiles(
}, [sortedFiles]);

const reingestFile = async (fileId: string) => {
const updatedFile = await KnowledgeService.reingestFile(
agentId,
fileId,
knowledgeSource.id
);
const updatedFile =
await KnowledgeSourceApiService.reingestFileFromSource(
namespace,
agentId,
knowledgeSource.id,
fileId
);
mutateFiles((prev) =>
prev?.map((f) => (f.id === fileId ? updatedFile : f))
);
};

const approveFile = async (file: KnowledgeFile, approved: boolean) => {
const { error, data: updatedFile } = await handlePromise(
KnowledgeService.approveFile(agentId, file.id, approved)
KnowledgeSourceApiService.approveFile(
namespace,
agentId,
file.id,
approved
)
);

if (error) {
Expand Down
Loading