From 0a747bb96abcbb14745c191ee1a5a05993fd5a53 Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe <46546486+ryanhopperlowe@users.noreply.github.com> Date: Mon, 30 Dec 2024 09:24:44 -0600 Subject: [PATCH] Feat/admin/thread-knowledge (#1016) * chore: refactor knowledge apis to dynamically route to various knowledge namespaces (agents, workflows, threads) Signed-off-by: Ryan Hopper-Lowe * 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 --- pkg/api/router/router.go | 6 +- ui/admin/app/components/chat/ChatActions.tsx | 3 + .../chat/chat-actions/KnowledgeInfo.tsx | 140 +++++++++---- .../app/components/chat/thread-helpers.ts | 7 +- .../knowledge/AgentKnowledgePanel.tsx | 4 +- .../knowledge/KnowledgeFileItem.tsx | 4 +- .../knowledge/KnowledgeSourceDetail.tsx | 1 + .../app/hooks/knowledge/useKnowledgeFiles.ts | 28 ++- .../knowledge/useKnowledgeSourceFiles.ts | 32 ++- .../hooks/knowledge/useKnowledgeSources.ts | 44 ++-- ui/admin/app/lib/model/knowledge.ts | 15 ++ ui/admin/app/lib/routers/apiRoutes.ts | 127 ++++++++---- .../service/api/knowledgeFileApiService.ts | 90 +++++++++ .../app/lib/service/api/knowledgeService.ts | 171 ---------------- .../service/api/knowledgeSourceApiService.ts | 188 ++++++++++++++++++ .../app/lib/service/api/threadsService.ts | 16 -- ui/admin/app/routes/_auth.threads.$id.tsx | 9 +- 17 files changed, 576 insertions(+), 309 deletions(-) create mode 100644 ui/admin/app/lib/service/api/knowledgeFileApiService.ts delete mode 100644 ui/admin/app/lib/service/api/knowledgeService.ts create mode 100644 ui/admin/app/lib/service/api/knowledgeSourceApiService.ts diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index f74703a25..9d8bb76a9 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -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) diff --git a/ui/admin/app/components/chat/ChatActions.tsx b/ui/admin/app/components/chat/ChatActions.tsx index 7d002bfbc..8031b54da 100644 --- a/ui/admin/app/components/chat/ChatActions.tsx +++ b/ui/admin/app/components/chat/ChatActions.tsx @@ -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, @@ -24,6 +25,8 @@ export function ChatActions({ className }: { className?: string }) { agent={agent} disabled={!thread} /> + + {threadId && } ); diff --git a/ui/admin/app/components/chat/chat-actions/KnowledgeInfo.tsx b/ui/admin/app/components/chat/chat-actions/KnowledgeInfo.tsx index 80a8c8b87..165a4e7a8 100644 --- a/ui/admin/app/components/chat/chat-actions/KnowledgeInfo.tsx +++ b/ui/admin/app/components/chat/chat-actions/KnowledgeInfo.tsx @@ -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, @@ -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(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 ( - - Knowledge - - - - - + + + + + + { + if (!e.target.files) return; + startUpload(e.target.files); + }} + /> + ); } diff --git a/ui/admin/app/components/chat/thread-helpers.ts b/ui/admin/app/components/chat/thread-helpers.ts index 4949e76e9..b8f518475 100644 --- a/ui/admin/app/components/chat/thread-helpers.ts +++ b/ui/admin/app/components/chat/thread-helpers.ts @@ -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"; @@ -43,8 +44,10 @@ export function useOptimisticThread(threadId?: Nullish) { } export function useThreadKnowledge(threadId?: Nullish) { - return useSWR(ThreadsService.getKnowledge.key(threadId), ({ threadId }) => - ThreadsService.getKnowledge(threadId) + return useSWR( + KnowledgeFileService.getKnowledgeFiles.key("threads", threadId), + ({ agentId, namespace }) => + KnowledgeFileService.getKnowledgeFiles(namespace, agentId) ); } diff --git a/ui/admin/app/components/knowledge/AgentKnowledgePanel.tsx b/ui/admin/app/components/knowledge/AgentKnowledgePanel.tsx index 8d7285ea2..7a246bb75 100644 --- a/ui/admin/app/components/knowledge/AgentKnowledgePanel.tsx +++ b/ui/admin/app/components/knowledge/AgentKnowledgePanel.tsx @@ -57,7 +57,7 @@ export default function AgentKnowledgePanel({ ); const { localFiles, addKnowledgeFile, deleteKnowledgeFile, reingestFile } = - useKnowledgeFiles(agentId); + useKnowledgeFiles("agents", agentId); const { knowledgeSources, @@ -67,7 +67,7 @@ export default function AgentKnowledgePanel({ addWebsite, addOneDrive, addNotion, - } = useKnowledgeSources(agentId); + } = useKnowledgeSources("agents", agentId); const selectedKnowledgeSource = knowledgeSources.find( (source) => source.id === selectedKnowledgeSourceId diff --git a/ui/admin/app/components/knowledge/KnowledgeFileItem.tsx b/ui/admin/app/components/knowledge/KnowledgeFileItem.tsx index f9cb5c089..0aa0c15aa 100644 --- a/ui/admin/app/components/knowledge/KnowledgeFileItem.tsx +++ b/ui/admin/app/components/knowledge/KnowledgeFileItem.tsx @@ -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({ @@ -65,7 +65,7 @@ export function KnowledgeFileItem({ variant="ghost" size="icon" onClick={() => - onViewError(file.error ?? "") + onViewError?.(file.error ?? "") } > diff --git a/ui/admin/app/components/knowledge/KnowledgeSourceDetail.tsx b/ui/admin/app/components/knowledge/KnowledgeSourceDetail.tsx index 20c30fc65..86c161180 100644 --- a/ui/admin/app/components/knowledge/KnowledgeSourceDetail.tsx +++ b/ui/admin/app/components/knowledge/KnowledgeSourceDetail.tsx @@ -71,6 +71,7 @@ export const KnowledgeSourceDetail: FC = ({ const scrollPosition = useRef(0); const { files, reingestFile, approveFile } = useKnowledgeSourceFiles( + "agents", agentId, knowledgeSource ); diff --git a/ui/admin/app/hooks/knowledge/useKnowledgeFiles.ts b/ui/admin/app/hooks/knowledge/useKnowledgeFiles.ts index 8026d37fa..dfe23f71f 100644 --- a/ui/admin/app/hooks/knowledge/useKnowledgeFiles.ts +++ b/ui/admin/app/hooks/knowledge/useKnowledgeFiles.ts @@ -1,10 +1,17 @@ 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 { @@ -12,9 +19,9 @@ export function useKnowledgeFiles(agentId: string) { 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, @@ -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 ); @@ -54,7 +62,8 @@ export function useKnowledgeFiles(agentId: string) { }; const deleteKnowledgeFile = async (file: KnowledgeFile) => { - await KnowledgeService.deleteKnowledgeFileFromAgent( + await KnowledgeFileService.deleteKnowledgeFile( + namespace, agentId, file.fileName ); @@ -62,7 +71,8 @@ export function useKnowledgeFiles(agentId: string) { }; const reingestFile = async (fileId: string) => { - const reingestedFile = await KnowledgeService.reingestFile( + const reingestedFile = await KnowledgeFileService.reingestFile( + namespace, agentId, fileId ); diff --git a/ui/admin/app/hooks/knowledge/useKnowledgeSourceFiles.ts b/ui/admin/app/hooks/knowledge/useKnowledgeSourceFiles.ts index da3c74550..e4710e3dd 100644 --- a/ui/admin/app/hooks/knowledge/useKnowledgeSourceFiles.ts +++ b/ui/admin/app/hooks/knowledge/useKnowledgeSourceFiles.ts @@ -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 ) { @@ -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, @@ -76,11 +83,13 @@ 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)) ); @@ -88,7 +97,12 @@ export function useKnowledgeSourceFiles( 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) { diff --git a/ui/admin/app/hooks/knowledge/useKnowledgeSources.ts b/ui/admin/app/hooks/knowledge/useKnowledgeSources.ts index fe7f50ad0..cc6418ea2 100644 --- a/ui/admin/app/hooks/knowledge/useKnowledgeSources.ts +++ b/ui/admin/app/hooks/knowledge/useKnowledgeSources.ts @@ -4,11 +4,15 @@ import useSWR from "swr"; import { KnowledgeSource, KnowledgeSourceInput, + KnowledgeSourceNamespace, KnowledgeSourceStatus, } from "~/lib/model/knowledge"; -import { KnowledgeService } from "~/lib/service/api/knowledgeService"; +import { KnowledgeSourceApiService } from "~/lib/service/api/knowledgeSourceApiService"; -export function useKnowledgeSources(agentId: string) { +export function useKnowledgeSources( + namespace: KnowledgeSourceNamespace, + agentId: string +) { const [blockPollingSources, setBlockPollingSources] = useState(false); const startPolling = () => setBlockPollingSources(false); @@ -17,8 +21,9 @@ export function useKnowledgeSources(agentId: string) { mutate: mutateSources, ...rest } = useSWR( - KnowledgeService.getKnowledgeSourcesForAgent.key(agentId), - ({ agentId }) => KnowledgeService.getKnowledgeSourcesForAgent(agentId), + KnowledgeSourceApiService.getKnowledgeSources.key(namespace, agentId), + ({ namespace, agentId }) => + KnowledgeSourceApiService.getKnowledgeSources(namespace, agentId), { revalidateOnFocus: false, refreshInterval: blockPollingSources ? undefined : 5000, @@ -42,10 +47,12 @@ export function useKnowledgeSources(agentId: string) { } const syncKnowledgeSource = async (sourceId: string) => { - const syncedSource = await KnowledgeService.resyncKnowledgeSource( - agentId, - sourceId - ); + const syncedSource = + await KnowledgeSourceApiService.resyncKnowledgeSource( + namespace, + agentId, + sourceId + ); mutateSources((prev) => prev?.map((source) => source.id === syncedSource.id ? syncedSource : source @@ -55,7 +62,11 @@ export function useKnowledgeSources(agentId: string) { }; const deleteKnowledgeSource = async (sourceId: string) => { - await KnowledgeService.deleteKnowledgeSource(agentId, sourceId); + await KnowledgeSourceApiService.deleteKnowledgeSource( + namespace, + agentId, + sourceId + ); mutateSources( (prev) => prev?.filter((source) => source.id !== sourceId), false @@ -69,11 +80,13 @@ export function useKnowledgeSources(agentId: string) { const source = knowledgeSources.find((s) => s.id === sourceId); if (!source) throw new Error("Source not found"); - const updatedSource = await KnowledgeService.updateKnowledgeSource( - agentId, - sourceId, - { ...source, ...updates } - ); + const updatedSource = + await KnowledgeSourceApiService.updateKnowledgeSource( + namespace, + agentId, + sourceId, + { ...source, ...updates } + ); mutateSources((prev) => prev?.map((s) => (s.id === updatedSource.id ? updatedSource : s)) ); @@ -81,7 +94,8 @@ export function useKnowledgeSources(agentId: string) { }; const createKnowledgeSource = async (config: KnowledgeSourceInput) => { - const newSource = await KnowledgeService.createKnowledgeSource( + const newSource = await KnowledgeSourceApiService.createKnowledgeSource( + namespace, agentId, config ); diff --git a/ui/admin/app/lib/model/knowledge.ts b/ui/admin/app/lib/model/knowledge.ts index feebbf582..dc5083274 100644 --- a/ui/admin/app/lib/model/knowledge.ts +++ b/ui/admin/app/lib/model/knowledge.ts @@ -1,3 +1,18 @@ +export const KnowledgeSourceNamespace = { + Agents: "agents", + Workflows: "workflows", +} as const; +export type KnowledgeSourceNamespace = + (typeof KnowledgeSourceNamespace)[keyof typeof KnowledgeSourceNamespace]; + +export const KnowledgeFileNamespace = { + Threads: "threads", + Agents: "agents", + Workflows: "workflows", +} as const; +export type KnowledgeFileNamespace = + (typeof KnowledgeFileNamespace)[keyof typeof KnowledgeFileNamespace]; + export const KnowledgeSourceType = { OneDrive: "OneDrive", Notion: "Notion", diff --git a/ui/admin/app/lib/routers/apiRoutes.ts b/ui/admin/app/lib/routers/apiRoutes.ts index 5fb19a3a3..389a6a1c3 100644 --- a/ui/admin/app/lib/routers/apiRoutes.ts +++ b/ui/admin/app/lib/routers/apiRoutes.ts @@ -1,6 +1,10 @@ import queryString from "query-string"; import { mutate } from "swr"; +import { + KnowledgeFileNamespace, + KnowledgeSourceNamespace, +} from "~/lib/model/knowledge"; import { ToolReferenceType } from "~/lib/model/toolReferences"; import { ApiUrl } from "~/lib/routers/baseRouter"; @@ -60,55 +64,102 @@ export const ApiRoutes = { deleteKnowledge: (assistantId: string, fileName: string) => buildUrl(`/assistants/${assistantId}/knowledge/${fileName}`), }, - agents: { - base: () => buildUrl("/agents"), - getById: (agentId: string) => buildUrl(`/agents/${agentId}`), - getLocalKnowledgeFiles: (agentId: string) => - buildUrl(`/agents/${agentId}/knowledge-files`), - addKnowledgeFiles: (agentId: string, fileName: string) => - buildUrl(`/agents/${agentId}/knowledge-files/${fileName}`), - deleteKnowledgeFiles: (agentId: string, fileName: string) => - buildUrl(`/agents/${agentId}/knowledge-files/${fileName}`), - createKnowledgeSource: (agentId: string) => - buildUrl(`/agents/${agentId}/knowledge-sources`), - getKnowledgeSource: (agentId: string) => - buildUrl(`/agents/${agentId}/knowledge-sources`), - updateKnowledgeSource: (agentId: string, knowledgeSourceId: string) => + knowledgeSources: { + getKnowledgeSources: ( + namespace: KnowledgeSourceNamespace, + entityId: string + ) => buildUrl(`/${namespace}/${entityId}/knowledge-sources`), + createKnowledgeSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string + ) => buildUrl(`/${namespace}/${entityId}/knowledge-sources`), + getKnowledgeSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + sourceId: string + ) => + buildUrl(`/${namespace}/${entityId}/knowledge-sources/${sourceId}`), + updateKnowledgeSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + sourceId: string + ) => + buildUrl(`/${namespace}/${entityId}/knowledge-sources/${sourceId}`), + deleteKnowledgeSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + sourceId: string + ) => + buildUrl(`/${namespace}/${entityId}/knowledge-sources/${sourceId}`), + syncKnowledgeSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + sourceId: string + ) => buildUrl( - `/agents/${agentId}/knowledge-sources/${knowledgeSourceId}` + `/${namespace}/${entityId}/knowledge-sources/${sourceId}/sync` ), - syncKnowledgeSource: (agentId: string, knowledgeSourceId: string) => - buildUrl( - `/agents/${agentId}/knowledge-sources/${knowledgeSourceId}/sync` - ), - getAuthUrlForAgent: (agentId: string, toolRef: string) => - buildUrl(`/agents/${agentId}/oauth-credentials/${toolRef}/login`), - deleteKnowledgeSource: (agentId: string, knowledgeSourceId: string) => + getFilesForKnowledgeSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + sourceId: string + ) => buildUrl( - `/agents/${agentId}/knowledge-sources/${knowledgeSourceId}` + `/${namespace}/${entityId}/knowledge-sources/${sourceId}/knowledge-files` ), - getFilesForKnowledgeSource: (agentId: string, sourceId: string) => + reingestKnowledgeFileFromSource: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + sourceId: string, + fileName: string + ) => buildUrl( - `/agents/${agentId}/knowledge-sources/${sourceId}/knowledge-files` + `/${namespace}/${entityId}/knowledge-sources/${sourceId}/knowledge-files/${fileName}/ingest` ), - approveFile: (agentId: string, fileID: string) => - buildUrl(`/agents/${agentId}/approve-file/${fileID}`), - reingestFile: (agentId: string, fileID: string, sourceId?: string) => + approveFile: ( + namespace: KnowledgeSourceNamespace, + entityId: string, + fileName: string + ) => buildUrl(`/${namespace}/${entityId}/approve-file/${fileName}`), + }, + knowledgeFiles: { + getKnowledgeFiles: ( + namespace: KnowledgeFileNamespace, + entityId: string + ) => buildUrl(`/${namespace}/${entityId}/knowledge-files`), + addKnowledgeFile: ( + namespace: KnowledgeFileNamespace, + entityId: string, + fileName: string + ) => buildUrl(`/${namespace}/${entityId}/knowledge-files/${fileName}`), + updateKnowledgeFile: ( + namespace: KnowledgeFileNamespace, + entityId: string, + fileName: string + ) => buildUrl(`/${namespace}/${entityId}/knowledge-files/${fileName}`), + deleteKnowledgeFile: ( + namespace: KnowledgeFileNamespace, + entityId: string, + fileName: string + ) => buildUrl(`/${namespace}/${entityId}/knowledge-files/${fileName}`), + reingestKnowledgeFile: ( + namespace: KnowledgeFileNamespace, + entityId: string, + fileName: string + ) => buildUrl( - sourceId - ? `/agents/${agentId}/knowledge-sources/${sourceId}/knowledge-files/${fileID}/ingest` - : `/agents/${agentId}/knowledge-files/${fileID}/ingest` + `/${namespace}/${entityId}/knowledge-files/${fileName}/ingest` ), }, + agents: { + base: () => buildUrl("/agents"), + getById: (agentId: string) => buildUrl(`/agents/${agentId}`), + getAuthUrlForAgent: (agentId: string, toolRef: string) => + buildUrl(`/agents/${agentId}/oauth-credentials/${toolRef}/login`), + }, workflows: { base: () => buildUrl("/workflows"), getById: (workflowId: string) => buildUrl(`/workflows/${workflowId}`), - getKnowledge: (workflowId: string) => - buildUrl(`/workflows/${workflowId}/files`), - addKnowledge: (workflowId: string, fileName: string) => - buildUrl(`/workflows/${workflowId}/files/${fileName}`), - deleteKnowledge: (workflowId: string, fileName: string) => - buildUrl(`/workflows/${workflowId}/files/${fileName}`), authenticate: (workflowId: string) => buildUrl(`/workflows/${workflowId}/authenticate`), }, @@ -130,8 +181,6 @@ export const ApiRoutes = { maxRuns?: number; } ) => buildUrl(`/threads/${threadId}/events`, params), - getKnowledge: (threadId: string) => - buildUrl(`/threads/${threadId}/knowledge`), getFiles: (threadId: string) => buildUrl(`/threads/${threadId}/files`), abortById: (threadId: string) => buildUrl(`/threads/${threadId}/abort`), }, diff --git a/ui/admin/app/lib/service/api/knowledgeFileApiService.ts b/ui/admin/app/lib/service/api/knowledgeFileApiService.ts new file mode 100644 index 000000000..4fb0840c5 --- /dev/null +++ b/ui/admin/app/lib/service/api/knowledgeFileApiService.ts @@ -0,0 +1,90 @@ +import { KnowledgeFile, KnowledgeFileNamespace } from "~/lib/model/knowledge"; +import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { request } from "~/lib/service/api/primitives"; + +async function getKnowledgeFiles( + namespace: KnowledgeFileNamespace, + agentId: string +) { + const res = await request<{ items: KnowledgeFile[] }>({ + url: ApiRoutes.knowledgeFiles.getKnowledgeFiles(namespace, agentId).url, + errorMessage: "Failed to fetch knowledge for agent", + }); + + return res.data.items; +} +getKnowledgeFiles.key = ( + namespace?: Nullish, + agentId?: Nullish +) => { + if (!namespace || !agentId) return null; + + return { + url: ApiRoutes.knowledgeFiles.getKnowledgeFiles(namespace, agentId) + .path, + agentId, + namespace, + }; +}; + +async function addKnowledgeFiles( + namespace: KnowledgeFileNamespace, + agentId: string, + file: File +) { + const res = await request({ + url: ApiRoutes.knowledgeFiles.addKnowledgeFile( + namespace, + agentId, + file.name + ).url, + method: "POST", + data: await file.arrayBuffer(), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + errorMessage: "Failed to add knowledge to agent", + }); + return res.data; +} + +async function deleteKnowledgeFile( + namespace: KnowledgeFileNamespace, + agentId: string, + fileName: string +) { + await request({ + url: ApiRoutes.knowledgeFiles.deleteKnowledgeFile( + namespace, + agentId, + fileName + ).url, + method: "DELETE", + errorMessage: "Failed to delete knowledge from agent", + }); +} + +async function reingestFile( + namespace: KnowledgeFileNamespace, + agentId: string, + fileID: string +) { + const { url } = ApiRoutes.knowledgeFiles.reingestKnowledgeFile( + namespace, + agentId, + fileID + ); + + const res = await request({ + url, + method: "POST", + errorMessage: "Failed to reingest knowledge file", + }); + + return res.data; +} + +export const KnowledgeFileService = { + getKnowledgeFiles, + addKnowledgeFiles, + deleteKnowledgeFile, + reingestFile, +}; diff --git a/ui/admin/app/lib/service/api/knowledgeService.ts b/ui/admin/app/lib/service/api/knowledgeService.ts deleted file mode 100644 index 2f9e3a295..000000000 --- a/ui/admin/app/lib/service/api/knowledgeService.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { - KnowledgeFile, - KnowledgeSource, - KnowledgeSourceInput, -} from "~/lib/model/knowledge"; -import { ApiRoutes } from "~/lib/routers/apiRoutes"; -import { request } from "~/lib/service/api/primitives"; - -async function getLocalKnowledgeFilesForAgent(agentId: string) { - const res = await request<{ items: KnowledgeFile[] }>({ - url: ApiRoutes.agents.getLocalKnowledgeFiles(agentId).url, - errorMessage: "Failed to fetch knowledge for agent", - }); - - return res.data.items; -} -getLocalKnowledgeFilesForAgent.key = (agentId?: Nullish) => { - if (!agentId) return null; - - return { - url: ApiRoutes.agents.getLocalKnowledgeFiles(agentId).path, - agentId, - }; -}; - -async function addKnowledgeFilesToAgent(agentId: string, file: File) { - const res = await request({ - url: ApiRoutes.agents.addKnowledgeFiles(agentId, file.name).url, - method: "POST", - data: await file.arrayBuffer(), - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - errorMessage: "Failed to add knowledge to agent", - }); - return res.data; -} - -async function deleteKnowledgeFileFromAgent(agentId: string, fileName: string) { - await request({ - url: ApiRoutes.agents.deleteKnowledgeFiles(agentId, fileName).url, - method: "DELETE", - errorMessage: "Failed to delete knowledge from agent", - }); -} - -async function createKnowledgeSource( - agentId: string, - input: KnowledgeSourceInput -) { - const res = await request({ - url: ApiRoutes.agents.createKnowledgeSource(agentId).url, - method: "POST", - data: JSON.stringify(input), - errorMessage: "Failed to create remote knowledge source", - }); - return res.data; -} - -async function updateKnowledgeSource( - agentId: string, - knowledgeSourceId: string, - input: KnowledgeSourceInput -) { - const res = await request({ - url: ApiRoutes.agents.updateKnowledgeSource(agentId, knowledgeSourceId) - .url, - method: "PUT", - data: JSON.stringify(input), - errorMessage: "Failed to update remote knowledge source", - }); - return res.data; -} - -async function resyncKnowledgeSource( - agentId: string, - knowledgeSourceId: string -) { - const res = await request({ - url: ApiRoutes.agents.syncKnowledgeSource(agentId, knowledgeSourceId) - .url, - method: "POST", - errorMessage: "Failed to resync remote knowledge source", - }); - return res.data; -} - -async function approveFile(agentId: string, fileID: string, approve: boolean) { - const res = await request({ - url: ApiRoutes.agents.approveFile(agentId, fileID).url, - method: "POST", - data: JSON.stringify({ Approved: approve }), - errorMessage: "Failed to approve knowledge file", - }); - return res.data; -} - -async function getKnowledgeSourcesForAgent(agentId: string) { - const res = await request<{ - items: KnowledgeSource[]; - }>({ - url: ApiRoutes.agents.getKnowledgeSource(agentId).url, - errorMessage: "Failed to fetch remote knowledge source", - }); - return res.data.items; -} - -getKnowledgeSourcesForAgent.key = (agentId?: Nullish) => { - if (!agentId) return null; - - return { - url: ApiRoutes.agents.getKnowledgeSource(agentId).path, - agentId, - }; -}; - -async function getFilesForKnowledgeSource(agentId: string, sourceId: string) { - if (!sourceId) return []; - const res = await request<{ items: KnowledgeFile[] }>({ - url: ApiRoutes.agents.getFilesForKnowledgeSource(agentId, sourceId).url, - errorMessage: "Failed to fetch knowledge files for knowledgesource", - }); - return res.data.items; -} - -getFilesForKnowledgeSource.key = ( - agentId?: Nullish, - sourceId?: Nullish -) => { - if (!agentId || !sourceId) return null; - - return { - url: ApiRoutes.agents.getFilesForKnowledgeSource(agentId, sourceId) - .path, - agentId, - sourceId, - }; -}; - -async function reingestFile( - agentId: string, - fileID: string, - sourceId?: string -) { - const rest = await request({ - url: ApiRoutes.agents.reingestFile(agentId, fileID, sourceId).url, - method: "POST", - errorMessage: "Failed to reingest knowledge file", - }); - return rest.data; -} - -async function deleteKnowledgeSource(agentId: string, sourceId: string) { - await request({ - url: ApiRoutes.agents.deleteKnowledgeSource(agentId, sourceId).url, - method: "DELETE", - errorMessage: "Failed to delete knowledge source", - }); -} - -export const KnowledgeService = { - approveFile, - getLocalKnowledgeFilesForAgent, - addKnowledgeFilesToAgent, - deleteKnowledgeFileFromAgent, - createKnowledgeSource, - updateKnowledgeSource, - resyncKnowledgeSource, - getKnowledgeSourcesForAgent, - getFilesForKnowledgeSource, - reingestFile, - deleteKnowledgeSource, -}; diff --git a/ui/admin/app/lib/service/api/knowledgeSourceApiService.ts b/ui/admin/app/lib/service/api/knowledgeSourceApiService.ts new file mode 100644 index 000000000..63aa51cef --- /dev/null +++ b/ui/admin/app/lib/service/api/knowledgeSourceApiService.ts @@ -0,0 +1,188 @@ +import { + KnowledgeFile, + KnowledgeSource, + KnowledgeSourceInput, + KnowledgeSourceNamespace, +} from "~/lib/model/knowledge"; +import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { request } from "~/lib/service/api/primitives"; + +async function createKnowledgeSource( + namespace: KnowledgeSourceNamespace, + agentId: string, + input: KnowledgeSourceInput +) { + const res = await request({ + url: ApiRoutes.knowledgeSources.createKnowledgeSource( + namespace, + agentId + ).url, + method: "POST", + data: JSON.stringify(input), + errorMessage: "Failed to create remote knowledge source", + }); + return res.data; +} + +async function updateKnowledgeSource( + namespace: KnowledgeSourceNamespace, + agentId: string, + knowledgeSourceId: string, + input: KnowledgeSourceInput +) { + const res = await request({ + url: ApiRoutes.knowledgeSources.updateKnowledgeSource( + namespace, + agentId, + knowledgeSourceId + ).url, + method: "PUT", + data: JSON.stringify(input), + errorMessage: "Failed to update remote knowledge source", + }); + return res.data; +} + +async function resyncKnowledgeSource( + namespace: KnowledgeSourceNamespace, + agentId: string, + knowledgeSourceId: string +) { + const res = await request({ + url: ApiRoutes.knowledgeSources.syncKnowledgeSource( + namespace, + agentId, + knowledgeSourceId + ).url, + method: "POST", + errorMessage: "Failed to resync remote knowledge source", + }); + return res.data; +} + +async function approveFile( + namespace: KnowledgeSourceNamespace, + agentId: string, + fileID: string, + approve: boolean +) { + const res = await request({ + url: ApiRoutes.knowledgeSources.approveFile(namespace, agentId, fileID) + .url, + method: "POST", + data: JSON.stringify({ Approved: approve }), + errorMessage: "Failed to approve knowledge file", + }); + return res.data; +} + +async function getKnowledgeSources( + namespace: KnowledgeSourceNamespace, + agentId: string +) { + const res = await request<{ + items: KnowledgeSource[]; + }>({ + url: ApiRoutes.knowledgeSources.getKnowledgeSources(namespace, agentId) + .url, + errorMessage: "Failed to fetch remote knowledge source", + }); + return res.data.items; +} +getKnowledgeSources.key = ( + namespace?: Nullish, + agentId?: Nullish +) => { + if (!namespace || !agentId) return null; + + return { + url: ApiRoutes.knowledgeSources.getKnowledgeSources(namespace, agentId) + .path, + agentId, + namespace, + }; +}; + +async function getFilesForKnowledgeSource( + namespace: KnowledgeSourceNamespace, + agentId: string, + sourceId: string +) { + if (!sourceId) return []; + const res = await request<{ items: KnowledgeFile[] }>({ + url: ApiRoutes.knowledgeSources.getFilesForKnowledgeSource( + namespace, + agentId, + sourceId + ).url, + errorMessage: "Failed to fetch knowledge files for knowledgesource", + }); + return res.data.items; +} + +getFilesForKnowledgeSource.key = ( + namespace?: Nullish, + agentId?: Nullish, + sourceId?: Nullish +) => { + if (!namespace || !agentId || !sourceId) return null; + + return { + url: ApiRoutes.knowledgeSources.getFilesForKnowledgeSource( + namespace, + agentId, + sourceId + ).path, + agentId, + sourceId, + }; +}; + +async function reingestFileFromSource( + namespace: KnowledgeSourceNamespace, + agentId: string, + sourceId: string, + fileID: string +) { + const { url } = ApiRoutes.knowledgeSources.reingestKnowledgeFileFromSource( + namespace, + agentId, + sourceId, + fileID + ); + + const res = await request({ + url, + method: "POST", + errorMessage: "Failed to reingest knowledge file from source", + }); + + return res.data; +} + +async function deleteKnowledgeSource( + namespace: KnowledgeSourceNamespace, + agentId: string, + sourceId: string +) { + await request({ + url: ApiRoutes.knowledgeSources.deleteKnowledgeSource( + namespace, + agentId, + sourceId + ).url, + method: "DELETE", + errorMessage: "Failed to delete knowledge source", + }); +} + +export const KnowledgeSourceApiService = { + approveFile, + createKnowledgeSource, + updateKnowledgeSource, + resyncKnowledgeSource, + getKnowledgeSources, + getFilesForKnowledgeSource, + reingestFileFromSource, + deleteKnowledgeSource, +}; diff --git a/ui/admin/app/lib/service/api/threadsService.ts b/ui/admin/app/lib/service/api/threadsService.ts index e2484a2d1..ae85c81b5 100644 --- a/ui/admin/app/lib/service/api/threadsService.ts +++ b/ui/admin/app/lib/service/api/threadsService.ts @@ -1,5 +1,4 @@ import { ChatEvent } from "~/lib/model/chatEvents"; -import { KnowledgeFile } from "~/lib/model/knowledge"; import { Thread, UpdateThread } from "~/lib/model/threads"; import { WorkspaceFile } from "~/lib/model/workspace"; import { ApiRoutes, revalidateWhere } from "~/lib/routers/apiRoutes"; @@ -99,20 +98,6 @@ const deleteThread = async (threadId: string) => { }); }; -const getKnowledge = async (threadId: string) => { - const res = await request<{ items: KnowledgeFile[] }>({ - url: ApiRoutes.threads.getKnowledge(threadId).url, - errorMessage: "Failed to fetch knowledge for thread", - }); - - return res.data.items ?? ([] as KnowledgeFile[]); -}; -getKnowledge.key = (threadId?: Nullish) => { - if (!threadId) return null; - - return { url: ApiRoutes.threads.getKnowledge(threadId).path, threadId }; -}; - const getFiles = async (threadId: string) => { const res = await request<{ items: WorkspaceFile[] }>({ url: ApiRoutes.threads.getFiles(threadId).url, @@ -147,7 +132,6 @@ export const ThreadsService = { updateThreadById, deleteThread, revalidateThreads, - getKnowledge, getFiles, abortThread, }; diff --git a/ui/admin/app/routes/_auth.threads.$id.tsx b/ui/admin/app/routes/_auth.threads.$id.tsx index 52b9ab48e..385a3d073 100644 --- a/ui/admin/app/routes/_auth.threads.$id.tsx +++ b/ui/admin/app/routes/_auth.threads.$id.tsx @@ -8,7 +8,9 @@ import { } from "react-router"; import { $path } from "safe-routes"; +import { KnowledgeFileNamespace } from "~/lib/model/knowledge"; import { AgentService } from "~/lib/service/api/agentService"; +import { KnowledgeFileService } from "~/lib/service/api/knowledgeFileApiService"; import { ThreadsService } from "~/lib/service/api/threadsService"; import { WorkflowService } from "~/lib/service/api/workflowService"; import { RouteHandle } from "~/lib/service/routeHandles"; @@ -59,7 +61,10 @@ export const clientLoader = async ({ : null; const files = await ThreadsService.getFiles(id); - const knowledge = await ThreadsService.getKnowledge(id); + const knowledge = await KnowledgeFileService.getKnowledgeFiles( + KnowledgeFileNamespace.Threads, + thread.id + ); return { thread, agent, workflow, files, knowledge }; }; @@ -74,7 +79,7 @@ export default function ChatAgent() { throw new Error("Trying to view a thread with an unsupported parent."); }; - const [isAgent, entity] = [agent !== null, getEntity()]; + const entity = getEntity(); return (