From 89fca5f4173520a6a0dd366c8f62f7160ad10152 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 31 Jan 2026 20:00:24 +1300 Subject: [PATCH] feat(35-03): add subgraph extraction and scoped context to chat API - Import extractSubgraph from subgraphExtractor - Parse selectedNodeIds from request body - Call extractSubgraph before building workflow context - Pass subgraph.selectedNodes and selectedEdges to buildWorkflowContext - Update buildEditSystemPrompt to accept optional restSummary parameter - Add WORKFLOW CONTEXT (SELECTED SUBSET) section when scoped - Include rest-of-workflow summary with type breakdown and boundary connections - Add metadata placeholder note to system prompt - Enhance error handling to return 413 for oversized payloads Co-Authored-By: Claude Opus 4.5 --- src/app/api/chat/route.ts | 29 +++++++++++++++++++++++------ src/lib/chat/tools.ts | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index ba63fe4b..777dd1cc 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -2,6 +2,7 @@ import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { createChatTools, buildEditSystemPrompt } from '@/lib/chat/tools'; import { buildWorkflowContext } from '@/lib/chat/contextBuilder'; +import { extractSubgraph } from '@/lib/chat/subgraphExtractor'; import { WorkflowNode } from '@/types'; import { WorkflowEdge } from '@/types/workflow'; @@ -9,9 +10,10 @@ export const maxDuration = 60; // 1 minute timeout export async function POST(request: Request) { try { - const { messages, workflowState } = await request.json() as { + const { messages, workflowState, selectedNodeIds } = await request.json() as { messages: UIMessage[]; workflowState?: { nodes: WorkflowNode[]; edges: WorkflowEdge[] }; + selectedNodeIds?: string[]; }; // Get API key from environment @@ -20,14 +22,21 @@ export async function POST(request: Request) { return new Response('GEMINI_API_KEY not configured', { status: 500 }); } - // Build workflow context from client-provided state - const context = buildWorkflowContext( + // Extract subgraph if nodes are selected, otherwise use full workflow + const subgraph = extractSubgraph( workflowState?.nodes || [], - workflowState?.edges || [] + workflowState?.edges || [], + selectedNodeIds || [] + ); + + // Build workflow context from selected subgraph + const context = buildWorkflowContext( + subgraph.selectedNodes, + subgraph.selectedEdges ); - // Build context-aware system prompt - const systemPrompt = buildEditSystemPrompt(context); + // Build context-aware system prompt with optional rest summary + const systemPrompt = buildEditSystemPrompt(context, subgraph.restSummary); // Extract node IDs for tool validation const nodeIds = (workflowState?.nodes || []).map(n => n.id); @@ -60,6 +69,14 @@ export async function POST(request: Request) { return new Response('Rate limit reached. Please wait and try again.', { status: 429 }); } + // Check for token/size errors and return 413 + if (error instanceof Error) { + const errorMsg = error.message.toLowerCase(); + if (errorMsg.includes('too large') || errorMsg.includes('token limit') || errorMsg.includes('payload') || errorMsg.includes('request entity too large')) { + return new Response('This workflow is too large for the AI to process. Try selecting fewer nodes.', { status: 413 }); + } + } + return new Response( error instanceof Error ? error.message : 'Chat request failed', { status: 500 } diff --git a/src/lib/chat/tools.ts b/src/lib/chat/tools.ts index b7e658b5..beea39b2 100644 --- a/src/lib/chat/tools.ts +++ b/src/lib/chat/tools.ts @@ -2,6 +2,7 @@ import { tool } from "ai"; import { z } from "zod"; import { EditOperation } from "./editOperations"; import { WorkflowContext, formatContextForPrompt } from "./contextBuilder"; +import { SubgraphResult } from "./subgraphExtractor"; import { NodeType } from "@/types"; /** @@ -23,9 +24,13 @@ const VALID_NODE_TYPES: NodeType[] = [ * Builds the enhanced system prompt with current workflow context and tool usage rules. * * @param workflowContext - Current workflow state summary + * @param restSummary - Optional summary of unselected nodes (when selection scoped) * @returns Complete system prompt with context and rules */ -export function buildEditSystemPrompt(workflowContext: WorkflowContext): string { +export function buildEditSystemPrompt( + workflowContext: WorkflowContext, + restSummary?: SubgraphResult['restSummary'] +): string { // Base domain expertise from existing SYSTEM_PROMPT const baseDomainExpertise = `You are a workflow expert for Node Banana, a visual node-based AI image generation tool. Be concise and direct — short bullet points, no fluff. Use the same language the user sees in the UI. Never expose internal property names, JSON structure, or code. @@ -92,12 +97,39 @@ Displays the final generated image or video. Connect any image or video output h - Ask one clarifying question at a time if goal is unclear`; // Current workflow context - const contextSection = ` + let contextSection = ` ## CURRENT WORKFLOW ${formatContextForPrompt(workflowContext)}`; + // Add subgraph summary if scoped to selected nodes + if (restSummary && restSummary.nodeCount > 0) { + const typeBreakdown = Object.entries(restSummary.typeBreakdown) + .map(([type, count]) => `${count} ${type}`) + .join(', '); + + const boundaryInfo = restSummary.boundaryConnections.length > 0 + ? `\nConnections to selected nodes: ${restSummary.boundaryConnections.map(bc => + `${bc.direction === 'incoming' ? 'Input from' : 'Output to'} ${bc.otherNodeId} (${bc.handleType})` + ).join(', ')}` + : ''; + + contextSection += ` + +## WORKFLOW CONTEXT (SELECTED SUBSET) + +You are focused on the selected nodes. The rest of the workflow: +- ${restSummary.nodeCount} other node(s): ${typeBreakdown}${boundaryInfo} + +Note: Binary data (images, videos) has been replaced with metadata descriptions like [image: 1024x768, 245KB]. These are not editable - they represent existing content that will be preserved.`; + } else if (!restSummary) { + // Full workflow - add metadata note + contextSection += ` + +Note: Binary data (images, videos) has been replaced with metadata descriptions like [image: 1024x768, 245KB]. These are not editable - they represent existing content that will be preserved.`; + } + // Tool usage rules const toolUsageRules = `