import { isAIMessageSDK, isHumanMessageSDK } from "@/lib/langchain-messages"; import { UseStream, useStream } from "@langchain/langgraph-sdk/react"; import { AssistantMessage } from "../thread/messages/ai"; import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; import { isCustomNodeEvent, CustomNodeEvent, INITIALIZE_NODE_ID, mapCustomEventsToSteps, } from "@open-swe/shared/open-swe/custom-node-events"; import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants"; import { Message } from "@langchain/langgraph-sdk"; import { InitializeStep } from "../gen-ui/initialize-step"; import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { GraphState } from "@open-swe/shared/open-swe/types"; interface ActionsRendererProps { graphId: string; threadId: string; runId?: string; setProgrammerSession?: ( session: ManagerGraphState["programmerSession"], ) => void; programmerSession?: ManagerGraphState["programmerSession"]; setSelectedTab?: Dispatch>; } const getCustomNodeEventsFromMessages = ( messages?: Message[], nodeId?: string, ): CustomNodeEvent[] => { if (!messages) return []; return messages .filter((m) => isAIMessageSDK(m)) .filter((m) => { const events = m.additional_kwargs?.customNodeEvents as | CustomNodeEvent[] | undefined; if (!events?.length) return false; if (!nodeId) { return true; } return events.some((e) => e.nodeId === nodeId); }) .map((m) => m.additional_kwargs?.customNodeEvents as CustomNodeEvent[]) .flat(); }; export function ActionsRenderer({ graphId, threadId, runId, setProgrammerSession, programmerSession, setSelectedTab, }: ActionsRendererProps) { const [customNodeEvents, setCustomNodeEvents] = useState( [], ); const stream = useStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL, assistantId: graphId, reconnectOnMount: true, threadId, onCustomEvent: (event) => { if (isCustomNodeEvent(event)) { setCustomNodeEvents((prev) => [...prev, event]); } }, }); const initializeEvents = customNodeEvents.filter( (e) => e.nodeId === INITIALIZE_NODE_ID, ); const steps = mapCustomEventsToSteps(initializeEvents); const allSuccess = steps.length > 0 && steps.every((s) => s.status === "success"); let initStatus: "loading" | "generating" | "done" = "generating"; if (allSuccess) { initStatus = "done"; } useEffect(() => { const customInitEvents = getCustomNodeEventsFromMessages( stream.messages, INITIALIZE_NODE_ID, ); // If there are no custom init events found in messages, or we already have steps from custom events, return if (!customInitEvents?.length || initializeEvents.length) { return; } setCustomNodeEvents(customInitEvents); }, [stream.messages]); const streamJoined = useRef(false); useEffect(() => { if (!streamJoined.current && runId) { streamJoined.current = true; // TODO: If the SDK changes go in, use this instead: // stream.joinStream(runId, undefined, { streamMode: ["values", "messages", "custom"]}).catch(console.error); stream.joinStream(runId).catch(console.error); } }, [runId]); // Filter out human & do not render messages const filteredMessages = stream.messages?.filter( (m) => !isHumanMessageSDK(m) && !(m.id && m.id.startsWith(DO_NOT_RENDER_ID_PREFIX)), ); // TODO: Need a better way to handle this. Not great like this... useEffect(() => { if ( "programmerSession" in stream.values && stream.values.programmerSession && (stream.values.programmerSession.runId !== programmerSession?.runId || stream.values.programmerSession.threadId !== programmerSession?.threadId) ) { setProgrammerSession?.(stream.values.programmerSession); setSelectedTab?.("programmer"); } }, [stream.values]); return (
{initializeEvents.length > 0 && steps.length > 0 && ( )} {filteredMessages?.map((m) => ( >} message={m} isLoading={false} handleRegenerate={() => {}} /> ))}
); }