Skip to content

fix: Handle unknown interrupt objects #99

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

Closed
wants to merge 2 commits into from
Closed
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
12 changes: 9 additions & 3 deletions src/components/thread/messages/ai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { Fragment } from "react/jsx-runtime";
import { isAgentInboxInterruptSchema } from "@/lib/agent-inbox-interrupt";
import { ThreadView } from "../agent-inbox";
import { useQueryState, parseAsBoolean } from "nuqs";
import { GenericInterruptView } from "./generic-interrupt";
import {
GenericInterruptView,
ResumeGenericInterrupt,
} from "./generic-interrupt";

function CustomComponent({
message,
Expand Down Expand Up @@ -143,8 +146,11 @@ export function AssistantMessage({
)}
{threadInterrupt?.value &&
!isAgentInboxInterruptSchema(threadInterrupt.value) &&
isLastMessage ? (
<GenericInterruptView interrupt={threadInterrupt.value} />
(isLastMessage || hasNoAIOrToolMessages) ? (
<div className="flex flex-col gap-2">
<GenericInterruptView interrupt={threadInterrupt.value} />
<ResumeGenericInterrupt />
</div>
) : null}
<div
className={cn(
Expand Down
141 changes: 111 additions & 30 deletions src/components/thread/messages/generic-interrupt.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { useState } from "react";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown, ChevronUp } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useStreamContext } from "@/providers/Stream";

function isComplexValue(value: any): boolean {
return Array.isArray(value) || (typeof value === "object" && value !== null);
}

export function GenericInterruptView({
interrupt,
}: {
interrupt: Record<string, any> | Record<string, any>[];
}) {
export function GenericInterruptView({ interrupt }: { interrupt: unknown }) {
const [isExpanded, setIsExpanded] = useState(false);

const contentStr = JSON.stringify(interrupt, null, 2);
Expand Down Expand Up @@ -39,23 +44,25 @@ export function GenericInterruptView({
};

// Process entries based on expanded state
const processEntries = () => {
const processEntries = (): unknown[] | string => {
if (Array.isArray(interrupt)) {
return isExpanded ? interrupt : interrupt.slice(0, 5);
} else {
} else if (typeof interrupt === "object" && interrupt !== null) {
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;
} else {
return interrupt?.toString() ?? "";
}
};

const displayEntries = processEntries();

return (
<div className="border border-gray-200 rounded-lg overflow-hidden">
<div className="border border-gray-200 rounded-lg overflow-hidden max-w-sm sm:max-w-xl md:max-w-3xl">
<div className="bg-gray-50 px-4 py-2 border-b border-gray-200">
<div className="flex items-center justify-between gap-2 flex-wrap">
<h3 className="font-medium text-gray-900">Human Interrupt</h3>
Expand All @@ -82,27 +89,35 @@ export function GenericInterruptView({
>
<table className="min-w-full divide-y divide-gray-200">
<tbody className="divide-y divide-gray-200">
{displayEntries.map((item, argIdx) => {
const [key, value] = Array.isArray(interrupt)
? [argIdx.toString(), item]
: (item as [string, any]);
return (
<tr key={argIdx}>
<td className="px-4 py-2 text-sm font-medium text-gray-900 whitespace-nowrap">
{key}
</td>
<td className="px-4 py-2 text-sm text-gray-500">
{isComplexValue(value) ? (
<code className="bg-gray-50 rounded px-2 py-1 font-mono text-sm">
{JSON.stringify(value, null, 2)}
</code>
) : (
String(value)
)}
</td>
</tr>
);
})}
{Array.isArray(displayEntries) ? (
displayEntries.map((item, argIdx) => {
const [key, value] = Array.isArray(interrupt)
? [argIdx.toString(), item]
: (item as [string, any]);
return (
<tr key={argIdx}>
<td className="px-4 py-2 text-sm font-medium text-gray-900 whitespace-nowrap">
{key}
</td>
<td className="px-4 py-2 text-sm text-gray-500">
{isComplexValue(value) ? (
<code className="bg-gray-50 rounded px-2 py-1 font-mono text-sm">
{JSON.stringify(value, null, 2)}
</code>
) : (
String(value)
)}
</td>
</tr>
);
})
) : (
<tr>
<td className="px-4 py-2 text-sm font-medium text-gray-900 whitespace-nowrap">
{displayEntries}
</td>
</tr>
)}
</tbody>
</table>
</motion.div>
Expand All @@ -124,3 +139,69 @@ export function GenericInterruptView({
</div>
);
}

const QuestionCircle = () => (
<div className="flex w-4 h-4 items-center justify-center rounded-full border-[1px] border-gray-300">
<p className="text-xs text-gray-500">?</p>
</div>
);

const Code = ({ children }: { children: React.ReactNode }) => (
<code className="text-orange-600 bg-gray-300 rounded px-1">{children}</code>
);

export function ResumeGenericInterrupt() {
const [resumeValue, setResumeValue] = useState("");
const stream = useStreamContext();

const handleResume = (
e:
| React.MouseEvent<HTMLButtonElement, MouseEvent>
| React.FormEvent<HTMLFormElement>,
) => {
e.preventDefault();
stream.submit(null, {
command: {
resume: resumeValue,
},
});
};

return (
<div className="flex flex-col items-start justify-start gap-2 mt-3 max-w-sm sm:max-w-xl md:max-w-3xl">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="flex items-center justify-center gap-1">
<p className="text-xs text-gray-500">What&apos;s this</p>
<QuestionCircle />
</TooltipTrigger>
<TooltipContent className="flex flex-col gap-2 py-3">
<p>
This input offers a way to resume your graph from the point of the
interrupt.
</p>
<p>
The graph will be resumed using the <Code>Command</Code> API, with
the input you provide passed as a string value to the{" "}
<Code>resume</Code> key.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<form
className="flex items-center gap-2 h-16 bg-muted rounded-2xl border w-full"
onSubmit={handleResume}
>
<Input
placeholder="Resume input"
value={resumeValue}
onChange={(e) => setResumeValue(e.target.value)}
className="bg-inherit border-none shadow-none"
/>
<Button size="sm" variant="brand" className="mr-3" type="submit">
Resume
</Button>
</form>
</div>
);
}
Loading