Skip to content

Commit

Permalink
Feat/admin/thread-knowledge (#1016)
Browse files Browse the repository at this point in the history
* chore: refactor knowledge apis to dynamically route to various knowledge namespaces (agents, workflows, threads)

Signed-off-by: Ryan Hopper-Lowe <[email protected]>

* feat: add knowledge to chat actions

* chore: rename KnowledgeNamespace to KnowledgeSourceNamespace

Helps to differentiate between what entities can pull from knowledge files vs knowledge sources

* fix: remove unused isAgent variable

---------

Signed-off-by: Ryan Hopper-Lowe <[email protected]>
  • Loading branch information
ryanhopperlowe authored Dec 30, 2024
1 parent d681197 commit 0a747bb
Show file tree
Hide file tree
Showing 17 changed files with 576 additions and 309 deletions.
6 changes: 3 additions & 3 deletions pkg/api/router/router.go
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 @@ -5,12 +5,14 @@ import {
KnowledgeFile,
KnowledgeFileState,
KnowledgeSource,
KnowledgeSourceNamespace,
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: KnowledgeSourceNamespace,
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

0 comments on commit 0a747bb

Please sign in to comment.