diff --git a/apps/web/src/app/inbox/page.tsx b/apps/web/src/app/inbox/page.tsx index 14c6e1dd..fd55837f 100644 --- a/apps/web/src/app/inbox/page.tsx +++ b/apps/web/src/app/inbox/page.tsx @@ -2,7 +2,7 @@ import { AgentInbox } from "@/components/agent-inbox"; import React from "react"; -import { ThreadsProvider } from "@/components/agent-inbox/contexts/ThreadContext"; +import { ThreadsProvider } from "@/providers/Thread"; import { Separator } from "@/components/ui/separator"; import { SidebarTrigger } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/sonner"; diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index cf62b5c8..9adf94c3 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -6,6 +6,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/sonner"; import React from "react"; import { ChatBreadcrumb } from "@/features/chat/components/chat-breadcrumb"; +import { ThreadsProvider } from "@/providers/Thread"; /** * The default page (/). @@ -25,7 +26,9 @@ export default function ChatPage(): React.ReactNode { - + + + ); } diff --git a/apps/web/src/components/agent-inbox/components/interrupted-inbox-item.tsx b/apps/web/src/components/agent-inbox/components/interrupted-inbox-item.tsx index bab2fa9b..ecaddbac 100644 --- a/apps/web/src/components/agent-inbox/components/interrupted-inbox-item.tsx +++ b/apps/web/src/components/agent-inbox/components/interrupted-inbox-item.tsx @@ -4,7 +4,8 @@ import React from "react"; import { InboxItemStatuses } from "./statuses"; import { format } from "date-fns"; import { useQueryState, parseAsString } from "nuqs"; -import { IMPROPER_SCHEMA, VIEW_STATE_THREAD_QUERY_PARAM } from "../constants"; +import { VIEW_STATE_THREAD_QUERY_PARAM } from "../constants"; +import { IMPROPER_SCHEMA } from "@/constants"; import { ThreadIdCopyable } from "./thread-id"; interface InterruptedInboxItem< diff --git a/apps/web/src/components/agent-inbox/components/pagination.tsx b/apps/web/src/components/agent-inbox/components/pagination.tsx index 154719ce..ac360c58 100644 --- a/apps/web/src/components/agent-inbox/components/pagination.tsx +++ b/apps/web/src/components/agent-inbox/components/pagination.tsx @@ -7,7 +7,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useThreadsContext } from "../contexts/ThreadContext"; +import { useThreadsContext } from "@/providers/Thread"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useQueryState, parseAsInteger, parseAsString } from "nuqs"; diff --git a/apps/web/src/components/agent-inbox/components/thread-actions-view.tsx b/apps/web/src/components/agent-inbox/components/thread-actions-view.tsx index 08a38832..f9d90b5e 100644 --- a/apps/web/src/components/agent-inbox/components/thread-actions-view.tsx +++ b/apps/web/src/components/agent-inbox/components/thread-actions-view.tsx @@ -13,6 +13,7 @@ import { constructOpenInStudioURL } from "../utils"; import { ThreadIdCopyable } from "./thread-id"; import { InboxItemInput } from "./inbox-item-input"; import { TooltipIconButton } from "@/components/ui/tooltip-icon-button"; +import { IMPROPER_SCHEMA } from "@/constants"; import { STUDIO_NOT_WORKING_TROUBLESHOOTING_URL, VIEW_STATE_THREAD_QUERY_PARAM, @@ -20,8 +21,8 @@ import { import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { useQueryStates, parseAsString, useQueryState } from "nuqs"; -import { useThreadsContext } from "../contexts/ThreadContext"; -import { useState } from "react"; +import { useThreadsContext } from "@/providers/Thread"; +import { useMemo, useState } from "react"; import { Tooltip, @@ -37,7 +38,6 @@ interface ThreadActionsViewProps< > { threadData: ThreadData; isInterrupted: boolean; - threadTitle: string; showState: boolean; showDescription: boolean; handleShowSidePanel?: ( @@ -112,7 +112,6 @@ export function ThreadActionsView< >({ threadData, isInterrupted: _propIsInterrupted, - threadTitle, showDescription, showState, handleShowSidePanel, @@ -125,6 +124,17 @@ export function ThreadActionsView< }); const [refreshing, setRefreshing] = useState(false); + // Derive thread title + const threadTitle = useMemo(() => { + if ( + threadData?.interrupts?.[0]?.action_request?.action && + threadData.interrupts[0].action_request.action !== IMPROPER_SCHEMA + ) { + return threadData.interrupts[0].action_request.action; + } + return `Thread: ${threadData?.thread.thread_id.slice(0, 6)}...`; + }, [threadData]); + // Only use interrupted actions for interrupted threads const isInterrupted = threadData.status === "interrupted" && diff --git a/apps/web/src/components/agent-inbox/constants.ts b/apps/web/src/components/agent-inbox/constants.ts index af42125d..f683856f 100644 --- a/apps/web/src/components/agent-inbox/constants.ts +++ b/apps/web/src/components/agent-inbox/constants.ts @@ -9,5 +9,4 @@ export const NO_INBOXES_FOUND_PARAM = "no_inboxes_found"; export const AGENT_INBOX_GITHUB_README_URL = "https://github.com/langchain-ai/agent-inbox/blob/main/README.md"; -export const IMPROPER_SCHEMA = "improper_schema"; export const STUDIO_NOT_WORKING_TROUBLESHOOTING_URL = `${AGENT_INBOX_GITHUB_README_URL}#the-open-in-studio-button-doesnt-work-for-my-deployed-graphs`; diff --git a/apps/web/src/components/agent-inbox/hooks/use-interrupted-actions.tsx b/apps/web/src/components/agent-inbox/hooks/use-interrupted-actions.tsx index 75551567..fd1a4c29 100644 --- a/apps/web/src/components/agent-inbox/hooks/use-interrupted-actions.tsx +++ b/apps/web/src/components/agent-inbox/hooks/use-interrupted-actions.tsx @@ -7,12 +7,13 @@ import { } from "../types"; import { toast } from "sonner"; import React from "react"; -import { useThreadsContext } from "../contexts/ThreadContext"; +import { useThreadsContext } from "@/providers/Thread"; import { createDefaultHumanResponse } from "../utils"; import { INBOX_PARAM, VIEW_STATE_THREAD_QUERY_PARAM } from "../constants"; import { useQueryState, parseAsString } from "nuqs"; import { logger } from "../utils/logger"; +import { useAgentsContext } from "@/providers/Agents"; interface UseInterruptedActionsInput< ThreadValues extends Record = Record, @@ -71,6 +72,7 @@ export default function useInterruptedActions< threadData, setThreadData, }: UseInterruptedActionsInput): UseInterruptedActionsValue { + const { agents } = useAgentsContext(); const [selectedInbox] = useQueryState( INBOX_PARAM, parseAsString.withDefault("interrupted"), @@ -80,6 +82,24 @@ export default function useInterruptedActions< VIEW_STATE_THREAD_QUERY_PARAM, parseAsString, ); + const [agentId_] = useQueryState("agentId"); + + const getAgentInboxIds = (): [string, string] | undefined => { + if (agentInboxId) { + const [assistantId, deploymentId] = agentInboxId.split(":"); + return [assistantId, deploymentId]; + } + if (!agentId_) { + return undefined; + } + const deploymentId = agents.find( + (a) => a.assistant_id === agentId_, + )?.deploymentId; + if (!deploymentId) { + return undefined; + } + return [agentId_, deploymentId]; + }; const { fetchSingleThread, fetchThreads, sendHumanResponse, ignoreThread } = useThreadsContext(); @@ -132,10 +152,12 @@ export default function useInterruptedActions< e: React.MouseEvent | React.KeyboardEvent, ) => { e.preventDefault(); - if (!agentInboxId) { - toast.error("No agent inbox ID found"); + const agentInboxIds = getAgentInboxIds() ?? []; + if (!agentInboxIds.length) { return; } + const [assistantId, deploymentId] = agentInboxIds; + if (!threadData || !setThreadData) { toast.error("Thread data is not available"); return; @@ -276,7 +298,6 @@ export default function useInterruptedActions< if (updatedThreadData && updatedThreadData?.status === "interrupted") { setThreadData(updatedThreadData as ThreadData); } else { - const [assistantId, deploymentId] = agentInboxId.split(":"); // Re-fetch threads before routing back so the inbox is up to date await fetchThreads(assistantId, deploymentId); // Clear the selected thread ID to go back to inbox view @@ -301,10 +322,12 @@ export default function useInterruptedActions< e: React.MouseEvent, ) => { e.preventDefault(); - if (!agentInboxId) { - toast.error("No agent inbox ID found"); + const agentInboxIds = getAgentInboxIds() ?? []; + if (!agentInboxIds.length) { return; } + const [assistantId, deploymentId] = agentInboxIds; + if (!threadData || !setThreadData) { toast.error("Thread data is not available"); return; @@ -325,7 +348,6 @@ export default function useInterruptedActions< initialHumanInterruptEditValue.current = {}; await sendHumanResponse(threadData.thread.thread_id, [ignoreResponse]); - const [assistantId, deploymentId] = agentInboxId.split(":"); // Re-fetch threads before routing back so the inbox is up to date await fetchThreads(assistantId, deploymentId); @@ -341,10 +363,12 @@ export default function useInterruptedActions< e: React.MouseEvent, ) => { e.preventDefault(); - if (!agentInboxId) { - toast.error("No agent inbox ID found"); + const agentInboxIds = getAgentInboxIds() ?? []; + if (!agentInboxIds.length) { return; } + const [assistantId, deploymentId] = agentInboxIds; + if (!threadData || !setThreadData) { toast.error("Thread data is not available"); return; @@ -358,7 +382,6 @@ export default function useInterruptedActions< initialHumanInterruptEditValue.current = {}; await ignoreThread(threadData.thread.thread_id); - const [assistantId, deploymentId] = agentInboxId.split(":"); await fetchThreads(assistantId, deploymentId); setLoading(false); diff --git a/apps/web/src/components/agent-inbox/inbox-view.tsx b/apps/web/src/components/agent-inbox/inbox-view.tsx index 73ed3612..b392a9fd 100644 --- a/apps/web/src/components/agent-inbox/inbox-view.tsx +++ b/apps/web/src/components/agent-inbox/inbox-view.tsx @@ -1,4 +1,4 @@ -import { useThreadsContext } from "@/components/agent-inbox/contexts/ThreadContext"; +import { useThreadsContext } from "@/providers/Thread"; import { InboxItem } from "./components/inbox-item"; import React from "react"; import { Pagination } from "./components/pagination"; diff --git a/apps/web/src/components/agent-inbox/thread-view.tsx b/apps/web/src/components/agent-inbox/thread-view.tsx index 77e81724..6fb82229 100644 --- a/apps/web/src/components/agent-inbox/thread-view.tsx +++ b/apps/web/src/components/agent-inbox/thread-view.tsx @@ -1,11 +1,11 @@ import { StateView } from "./components/state-view"; import { ThreadActionsView } from "./components/thread-actions-view"; -import { useThreadsContext } from "./contexts/ThreadContext"; +import { useThreadsContext } from "@/providers/Thread"; import { ThreadData } from "./types"; import React from "react"; import { cn } from "@/lib/utils"; import { useQueryState, parseAsString } from "nuqs"; -import { IMPROPER_SCHEMA, VIEW_STATE_THREAD_QUERY_PARAM } from "./constants"; +import { VIEW_STATE_THREAD_QUERY_PARAM } from "./constants"; import { logger } from "./utils/logger"; export function ThreadView< @@ -27,17 +27,6 @@ export function ThreadView< // Show side panel for all thread types const showSidePanel = showDescription || showState; - // Derive thread title - const threadTitle = React.useMemo(() => { - if ( - threadData?.interrupts?.[0]?.action_request?.action && - threadData.interrupts[0].action_request.action !== IMPROPER_SCHEMA - ) { - return threadData.interrupts[0].action_request.action; - } - return `Thread: ${threadData?.thread.thread_id.slice(0, 6)}...`; - }, [threadData]); - // Scroll to top when thread view is mounted React.useEffect(() => { if (typeof window !== "undefined") { @@ -109,7 +98,6 @@ export function ThreadView< threadData={threadData} isInterrupted={isInterrupted} - threadTitle={threadTitle} showState={showState} showDescription={showDescription} handleShowSidePanel={handleShowSidePanel} diff --git a/apps/web/src/components/inbox-sidebar/index.tsx b/apps/web/src/components/inbox-sidebar/index.tsx index a41444d8..b3c9c41e 100644 --- a/apps/web/src/components/inbox-sidebar/index.tsx +++ b/apps/web/src/components/inbox-sidebar/index.tsx @@ -27,7 +27,7 @@ import { Deployment } from "@/types/deployment"; import { parseAsString, parseAsInteger, useQueryState } from "nuqs"; import { useAgentsContext } from "@/providers/Agents"; import { Agent } from "@/types/agent"; -import { useThreadsContext } from "../agent-inbox/contexts/ThreadContext"; +import { useThreadsContext } from "@/providers/Thread"; // Internal component that uses the context function InboxSidebarInternal() { diff --git a/apps/web/src/constants.ts b/apps/web/src/constants.ts index b4397ffb..53a16288 100644 --- a/apps/web/src/constants.ts +++ b/apps/web/src/constants.ts @@ -1 +1,2 @@ export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-"; +export const IMPROPER_SCHEMA = "improper_schema"; diff --git a/apps/web/src/features/chat/components/thread/messages/ai.tsx b/apps/web/src/features/chat/components/thread/messages/ai.tsx index 4e1b0461..abf962bf 100644 --- a/apps/web/src/features/chat/components/thread/messages/ai.tsx +++ b/apps/web/src/features/chat/components/thread/messages/ai.tsx @@ -10,6 +10,7 @@ import { ToolCalls, ToolResult } from "./tool-calls"; import { MessageContentComplex } from "@langchain/core/messages"; import { Fragment } from "react/jsx-runtime"; import { useQueryState, parseAsBoolean } from "nuqs"; +import { Interrupt } from "./interrupt"; function CustomComponent({ message, @@ -79,7 +80,14 @@ export function AssistantMessage({ ); const thread = useStreamContext(); + const isLastMessage = + thread.messages[thread.messages.length - 1].id === message?.id; + const hasNoAIOrToolMessages = !thread.messages.find( + (m) => m.type === "ai" || m.type === "tool", + ); + const meta = message ? thread.getMessagesMetadata(message) : undefined; + const threadInterrupt = thread.interrupt; const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint; const anthropicStreamedToolCalls = Array.isArray(content) @@ -106,7 +114,14 @@ export function AssistantMessage({ return (
{isToolResult ? ( - + <> + + + ) : (
{contentString.length > 0 && ( @@ -133,19 +148,11 @@ export function AssistantMessage({ thread={thread} /> )} - {/** - * TODO: Support rendering interrupts. - * Tracking issue: https://github.com/langchain-ai/open-agent-platform/issues/22 - */} - {/* {isAgentInboxInterruptSchema(threadInterrupt?.value) && - (isLastMessage || hasNoAIOrToolMessages) && ( - - )} - {threadInterrupt?.value && - !isAgentInboxInterruptSchema(threadInterrupt.value) && - isLastMessage ? ( - - ) : null} */} +
| Record[]; +}) { + const [isExpanded, setIsExpanded] = useState(false); + + const contentStr = JSON.stringify(interrupt, null, 2); + const contentLines = contentStr.split("\n"); + const shouldTruncate = contentLines.length > 4 || contentStr.length > 500; + + // Function to truncate long string values + const truncateValue = (value: any): any => { + if (typeof value === "string" && value.length > 100) { + return value.substring(0, 100) + "..."; + } + + if (Array.isArray(value) && !isExpanded) { + return value.slice(0, 2).map(truncateValue); + } + + if (isComplexValue(value) && !isExpanded) { + const strValue = JSON.stringify(value, null, 2); + if (strValue.length > 100) { + // Return plain text for truncated content instead of a JSON object + return `Truncated ${strValue.length} characters...`; + } + } + + return value; + }; + + // Process entries based on expanded state + const processEntries = () => { + if (Array.isArray(interrupt)) { + return isExpanded ? interrupt : interrupt.slice(0, 5); + } else { + const entries = Object.entries(interrupt); + if (!isExpanded && shouldTruncate) { + // When collapsed, process each value to potentially truncate it + return entries.map(([key, value]) => [key, truncateValue(value)]); + } + return entries; + } + }; + + const displayEntries = processEntries(); + + return ( +
+
+
+

Human Interrupt

+
+
+ +
+ + + + + {displayEntries.map((item, argIdx) => { + const [key, value] = Array.isArray(interrupt) + ? [argIdx.toString(), item] + : (item as [string, any]); + return ( + + + + + ); + })} + +
+ {key} + + {isComplexValue(value) ? ( + + {JSON.stringify(value, null, 2)} + + ) : ( + String(value) + )} +
+
+
+
+ {(shouldTruncate || + (Array.isArray(interrupt) && interrupt.length > 5)) && ( + setIsExpanded(!isExpanded)} + className="flex w-full cursor-pointer items-center justify-center border-t-[1px] border-gray-200 py-2 text-gray-500 transition-all duration-200 ease-in-out hover:bg-gray-50 hover:text-gray-600" + initial={{ scale: 1 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {isExpanded ? : } + + )} +
+
+ ); +} diff --git a/apps/web/src/features/chat/components/thread/messages/interrupt-types.ts b/apps/web/src/features/chat/components/thread/messages/interrupt-types.ts new file mode 100644 index 00000000..6ad8b53b --- /dev/null +++ b/apps/web/src/features/chat/components/thread/messages/interrupt-types.ts @@ -0,0 +1,56 @@ +/** + * Configuration interface that defines what actions are allowed for a human interrupt. + * This controls the available interaction options when the graph is paused for human input. + * + * @property {boolean} allow_ignore - Whether the human can choose to ignore/skip the current step + * @property {boolean} allow_respond - Whether the human can provide a text response/feedback + * @property {boolean} allow_edit - Whether the human can edit the provided content/state + * @property {boolean} allow_accept - Whether the human can accept/approve the current state + */ +export interface HumanInterruptConfig { + allow_ignore: boolean; + allow_respond: boolean; + allow_edit: boolean; + allow_accept: boolean; +} +/** + * Represents a request for human action within the graph execution. + * Contains the action type and any associated arguments needed for the action. + * + * @property {string} action - The type or name of action being requested (e.g., "Approve XYZ action") + * @property {Record} args - Key-value pairs of arguments needed for the action + */ +export interface ActionRequest { + action: string; + args: Record; +} +/** + * Represents an interrupt triggered by the graph that requires human intervention. + * This is passed to the `interrupt` function when execution is paused for human input. + * + * @property {ActionRequest} action_request - The specific action being requested from the human + * @property {HumanInterruptConfig} config - Configuration defining what actions are allowed + * @property {string} [description] - Optional detailed description of what input is needed + */ +export interface HumanInterrupt { + action_request: ActionRequest; + config: HumanInterruptConfig; + description?: string; +} +/** + * The response provided by a human to an interrupt, which is returned when graph execution resumes. + * + * @property {("accept"|"ignore"|"response"|"edit")} type - The type of response: + * - "accept": Approves the current state without changes + * - "ignore": Skips/ignores the current step + * - "response": Provides text feedback or instructions + * - "edit": Modifies the current state/content + * @property {null|string|ActionRequest} args - The response payload: + * - null: For ignore/accept actions + * - string: For text responses + * - ActionRequest: For edit actions with updated content + */ +export type HumanResponse = { + type: "accept" | "ignore" | "response" | "edit"; + args: null | string | ActionRequest; +}; diff --git a/apps/web/src/features/chat/components/thread/messages/interrupt.tsx b/apps/web/src/features/chat/components/thread/messages/interrupt.tsx new file mode 100644 index 00000000..f290afd5 --- /dev/null +++ b/apps/web/src/features/chat/components/thread/messages/interrupt.tsx @@ -0,0 +1,47 @@ +import { HumanInterrupt } from "./interrupt-types"; +import { GenericInterruptView } from "./generic-interrupt"; +import { ThreadViewChat } from "./thread-view-chat"; + +export function isAgentInboxInterruptSchema( + value: unknown, +): value is HumanInterrupt | HumanInterrupt[] { + const valueAsObject = Array.isArray(value) ? value[0] : value; + return ( + valueAsObject && + typeof valueAsObject === "object" && + "action_request" in valueAsObject && + typeof valueAsObject.action_request === "object" && + "config" in valueAsObject && + typeof valueAsObject.config === "object" && + "allow_respond" in valueAsObject.config && + "allow_accept" in valueAsObject.config && + "allow_edit" in valueAsObject.config && + "allow_ignore" in valueAsObject.config + ); +} + +interface InterruptProps { + interruptValue?: unknown; + isLastMessage: boolean; + hasNoAIOrToolMessages: boolean; +} + +export function Interrupt({ + interruptValue, + isLastMessage, + hasNoAIOrToolMessages, +}: InterruptProps) { + return ( + <> + {isAgentInboxInterruptSchema(interruptValue) && + (isLastMessage || hasNoAIOrToolMessages) && ( + + )} + {interruptValue && + !isAgentInboxInterruptSchema(interruptValue) && + isLastMessage ? ( + + ) : null} + + ); +} diff --git a/apps/web/src/features/chat/components/thread/messages/thread-view-chat.tsx b/apps/web/src/features/chat/components/thread/messages/thread-view-chat.tsx new file mode 100644 index 00000000..8f4b75bd --- /dev/null +++ b/apps/web/src/features/chat/components/thread/messages/thread-view-chat.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; +import { HumanInterrupt } from "./interrupt-types"; +import { StateView } from "@/components/agent-inbox/components/state-view"; +import { ThreadActionsView } from "@/components/agent-inbox/components/thread-actions-view"; +import { ThreadData } from "@/components/agent-inbox/types"; +import { useQueryState } from "nuqs"; +import { useStreamContext } from "@/features/chat/providers/Stream"; + +interface ThreadViewChatProps { + interrupt: HumanInterrupt | HumanInterrupt[]; +} + +export function ThreadViewChat({ interrupt }: ThreadViewChatProps) { + const thread = useStreamContext(); + const [showDescription, setShowDescription] = useState(false); + const [showState, setShowState] = useState(false); + const showSidePanel = showDescription || showState; + const [threadId] = useQueryState("threadId"); + const [threadData, setThreadData] = useState(); + + useEffect(() => { + if (!threadId) { + return; + } + thread.client.threads.get(threadId).then((thread) => { + setThreadData({ + thread, + interrupts: Array.isArray(interrupt) ? interrupt : [interrupt], + status: "interrupted", + }); + }); + }, [interrupt, threadId]); + + const handleShowSidePanel = ( + showState: boolean, + showDescription: boolean, + ) => { + if (showState && showDescription) { + console.error("Cannot show both state and description"); + return; + } + if (showState) { + setShowDescription(false); + setShowState(true); + } else if (showDescription) { + setShowState(false); + setShowDescription(true); + } else { + setShowState(false); + setShowDescription(false); + } + }; + + if (!threadData) { + return null; + } + + return ( +
+ {showSidePanel ? ( + + ) : ( + + )} +
+ ); +} diff --git a/apps/web/src/components/agent-inbox/contexts/ThreadContext.tsx b/apps/web/src/providers/Thread/index.tsx similarity index 88% rename from apps/web/src/components/agent-inbox/contexts/ThreadContext.tsx rename to apps/web/src/providers/Thread/index.tsx index 780bdf21..37ca21a9 100644 --- a/apps/web/src/components/agent-inbox/contexts/ThreadContext.tsx +++ b/apps/web/src/providers/Thread/index.tsx @@ -10,13 +10,13 @@ import { createClient } from "@/lib/client"; import { Run, Thread, ThreadStatus } from "@langchain/langgraph-sdk"; import React, { Dispatch, SetStateAction, useTransition } from "react"; import { parseAsInteger, parseAsString, useQueryState } from "nuqs"; -import { IMPROPER_SCHEMA } from "../constants"; import { getInterruptFromThread, processInterruptedThread, processThreadWithoutInterrupts, } from "./utils"; -import { logger } from "../utils/logger"; +import { IMPROPER_SCHEMA } from "@/constants"; +import { useAgentsContext } from "../Agents"; type ThreadContentType< ThreadValues extends Record = Record, @@ -55,9 +55,29 @@ const ThreadsContext = React.createContext( function ThreadsProviderInternal< ThreadValues extends Record = Record, >({ children }: { children: React.ReactNode }): React.ReactElement { + const { agents } = useAgentsContext(); const [agentInboxId] = useQueryState("agentInbox"); + // Need to track agent ID too since this context is used in the chat page which doesn't follow the agent inbox pattern + const [agentId_] = useQueryState("agentId"); const [isPending] = useTransition(); + const getAgentInboxIds = (): [string, string] | undefined => { + if (agentInboxId) { + const [assistantId, deploymentId] = agentInboxId.split(":"); + return [assistantId, deploymentId]; + } + if (!agentId_) { + return undefined; + } + const deploymentId = agents.find( + (a) => a.assistant_id === agentId_, + )?.deploymentId; + if (!deploymentId) { + return undefined; + } + return [agentId_, deploymentId]; + }; + // Get thread filter query params using the custom hook const [inboxParam] = useQueryState( "inbox", @@ -74,13 +94,6 @@ function ThreadsProviderInternal< const fetchThreads = React.useCallback( async (agentId: string, deploymentId: string) => { - if (!agentInboxId) { - toast.error("No agent inbox ID found", { - richColors: true, - }); - return; - } - setLoading(true); const client = createClient(deploymentId); @@ -215,8 +228,7 @@ function ThreadsProviderInternal< setThreadData(sortedData); setHasMoreThreads(threads.length === limit); - } catch (e) { - logger.error("Failed to fetch threads", e); + } catch (_) { toast.error("Failed to load threads. Please try again."); } finally { // Always reset loading state, even after errors @@ -240,8 +252,7 @@ function ThreadsProviderInternal< try { // Fetch threads fetchThreads(assistantId, deploymentId); - } catch (e) { - logger.error("Error occurred while fetching threads", e); + } catch (_) { toast.error("Failed to load threads. Please try again."); // Always reset loading state in case of error setLoading(false); @@ -250,14 +261,11 @@ function ThreadsProviderInternal< const fetchSingleThread = React.useCallback( async (threadId: string): Promise | undefined> => { - if (!agentInboxId) { - toast.error("No agent inbox ID found when fetching thread.", { - richColors: true, - }); + const agentInboxIds = getAgentInboxIds() ?? []; + if (!agentInboxIds.length) { return; } - - const [_, deploymentId] = agentInboxId.split(":"); + const [_, deploymentId] = agentInboxIds; const client = createClient(deploymentId); try { @@ -315,8 +323,7 @@ function ThreadsProviderInternal< interrupts: undefined, invalidSchema: undefined, }; - } catch (error) { - logger.error("Error fetching single thread", error); + } catch (_) { toast.error("Failed to load thread details. Please try again."); return undefined; } @@ -325,14 +332,11 @@ function ThreadsProviderInternal< ); const ignoreThread = async (threadId: string) => { - if (!agentInboxId) { - toast.error("No agent inbox ID found when fetching thread.", { - richColors: true, - }); + const agentInboxIds = getAgentInboxIds() ?? []; + if (!agentInboxIds.length) { return; } - - const [_, deploymentId] = agentInboxId.split(":"); + const [_, deploymentId] = agentInboxIds; const client = createClient(deploymentId); try { @@ -349,8 +353,7 @@ function ThreadsProviderInternal< description: "Ignored thread", duration: 3000, }); - } catch (e) { - logger.error("Error ignoring thread", e); + } catch (_) { toast.error("Failed to ignore thread. Please try again.", { duration: 3000, }); @@ -373,34 +376,26 @@ function ThreadsProviderInternal< }> | undefined : Promise | undefined => { - if (!agentInboxId) { - toast.error("No agent inbox ID found when fetching thread.", { - richColors: true, - }); + const agentInboxIds = getAgentInboxIds() ?? []; + if (!agentInboxIds.length) { return; } - - const [assistantId, deploymentId] = agentInboxId.split(":"); + const [assistantId, deploymentId] = agentInboxIds; const client = createClient(deploymentId); - try { - if (options?.stream) { - return client.runs.stream(threadId, assistantId, { - command: { - resume: response, - }, - streamMode: "events", - }) as any; // Type assertion needed due to conditional return type - } - return client.runs.create(threadId, assistantId, { + if (options?.stream) { + return client.runs.stream(threadId, assistantId, { command: { resume: response, }, + streamMode: "events", }) as any; // Type assertion needed due to conditional return type - } catch (e: any) { - logger.error("Error sending human response", e); - throw e; } + return client.runs.create(threadId, assistantId, { + command: { + resume: response, + }, + }) as any; // Type assertion needed due to conditional return type }; const contextValue: ThreadContentType = { diff --git a/apps/web/src/components/agent-inbox/contexts/utils.ts b/apps/web/src/providers/Thread/utils.ts similarity index 98% rename from apps/web/src/components/agent-inbox/contexts/utils.ts rename to apps/web/src/providers/Thread/utils.ts index a10f57bd..80f3cdeb 100644 --- a/apps/web/src/components/agent-inbox/contexts/utils.ts +++ b/apps/web/src/providers/Thread/utils.ts @@ -1,6 +1,6 @@ import { Thread, ThreadState } from "@langchain/langgraph-sdk"; -import { HumanInterrupt, ThreadData } from "../types"; -import { IMPROPER_SCHEMA } from "../constants"; +import { IMPROPER_SCHEMA } from "@/constants"; +import { HumanInterrupt, ThreadData } from "@/components/agent-inbox/types"; // TODO: Delete this once interrupt issue fixed. export const tmpCleanInterrupts = (interrupts: Record) => {