Skip to content

feat: Show interrupts in chat #108

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion apps/web/src/app/inbox/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (/).
Expand All @@ -25,7 +26,9 @@ export default function ChatPage(): React.ReactNode {
<ChatBreadcrumb />
</div>
</header>
<ChatInterface />
<ThreadsProvider>
<ChatInterface />
</ThreadsProvider>
</React.Suspense>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ 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,
} from "../constants";
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,
Expand All @@ -37,7 +38,6 @@ interface ThreadActionsViewProps<
> {
threadData: ThreadData<ThreadValues>;
isInterrupted: boolean;
threadTitle: string;
showState: boolean;
showDescription: boolean;
handleShowSidePanel?: (
Expand Down Expand Up @@ -112,7 +112,6 @@ export function ThreadActionsView<
>({
threadData,
isInterrupted: _propIsInterrupted,
threadTitle,
showDescription,
showState,
handleShowSidePanel,
Expand All @@ -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" &&
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/components/agent-inbox/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = Record<string, any>,
Expand Down Expand Up @@ -71,6 +72,7 @@ export default function useInterruptedActions<
threadData,
setThreadData,
}: UseInterruptedActionsInput<ThreadValues>): UseInterruptedActionsValue {
const { agents } = useAgentsContext();
const [selectedInbox] = useQueryState(
INBOX_PARAM,
parseAsString.withDefault("interrupted"),
Expand All @@ -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<ThreadValues>();
Expand Down Expand Up @@ -132,10 +152,12 @@ export default function useInterruptedActions<
e: React.MouseEvent<HTMLButtonElement, 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;
Expand Down Expand Up @@ -276,7 +298,6 @@ export default function useInterruptedActions<
if (updatedThreadData && updatedThreadData?.status === "interrupted") {
setThreadData(updatedThreadData as ThreadData<ThreadValues>);
} 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
Expand All @@ -301,10 +322,12 @@ export default function useInterruptedActions<
e: React.MouseEvent<HTMLButtonElement, 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;
Expand All @@ -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);

Expand All @@ -341,10 +363,12 @@ export default function useInterruptedActions<
e: React.MouseEvent<HTMLButtonElement, 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;
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/agent-inbox/inbox-view.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
16 changes: 2 additions & 14 deletions apps/web/src/components/agent-inbox/thread-view.tsx
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -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") {
Expand Down Expand Up @@ -109,7 +98,6 @@ export function ThreadView<
<ThreadActionsView<ThreadValues>
threadData={threadData}
isInterrupted={isInterrupted}
threadTitle={threadTitle}
showState={showState}
showDescription={showDescription}
handleShowSidePanel={handleShowSidePanel}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/inbox-sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
export const IMPROPER_SCHEMA = "improper_schema";
35 changes: 21 additions & 14 deletions apps/web/src/features/chat/components/thread/messages/ai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -106,7 +114,14 @@ export function AssistantMessage({
return (
<div className="group mr-auto flex items-start gap-2">
{isToolResult ? (
<ToolResult message={message} />
<>
<ToolResult message={message} />
<Interrupt
interruptValue={threadInterrupt?.value}
isLastMessage={isLastMessage}
hasNoAIOrToolMessages={hasNoAIOrToolMessages}
/>
</>
) : (
<div className="flex flex-col gap-2">
{contentString.length > 0 && (
Expand All @@ -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) && (
<ThreadView interrupt={threadInterrupt.value} />
)}
{threadInterrupt?.value &&
!isAgentInboxInterruptSchema(threadInterrupt.value) &&
isLastMessage ? (
<GenericInterruptView interrupt={threadInterrupt.value} />
) : null} */}
<Interrupt
interruptValue={threadInterrupt?.value}
isLastMessage={isLastMessage}
hasNoAIOrToolMessages={hasNoAIOrToolMessages}
/>
<div
className={cn(
"mr-auto flex items-center gap-2 transition-opacity",
Expand Down
Loading