2025-12-01 14:07:50 +09:00
import { streamText , convertToModelMessages , createUIMessageStream , createUIMessageStreamResponse } from 'ai' ;
2025-11-15 13:36:42 +09:00
import { getAIModel } from '@/lib/ai-providers' ;
2025-12-01 14:07:50 +09:00
import { findCachedResponse } from '@/lib/cached-responses' ;
2025-12-02 19:20:06 +09:00
import { formatXML } from '@/lib/utils' ;
2025-11-15 13:36:42 +09:00
import { z } from "zod" ;
2025-04-04 02:10:24 +00:00
2025-12-01 00:46:40 +09:00
export const maxDuration = 300 ;
2025-03-19 11:03:37 +00:00
2025-12-02 19:20:06 +09:00
// Prefix for cached tool call IDs (used by client to detect cached responses)
export const CACHED_TOOL_PREFIX = 'cached-' ;
2025-12-01 14:07:50 +09:00
// Helper function to check if diagram is minimal/empty
function isMinimalDiagram ( xml : string ) : boolean {
const stripped = xml . replace ( /\s/g , '' ) ;
return ! stripped . includes ( 'id="2"' ) ;
}
// Helper function to create cached stream response
function createCachedStreamResponse ( xml : string ) : Response {
2025-12-02 19:20:06 +09:00
const toolCallId = ` ${ CACHED_TOOL_PREFIX } ${ Date . now ( ) } ` ;
2025-12-01 14:07:50 +09:00
const stream = createUIMessageStream ( {
execute : async ( { writer } ) = > {
writer . write ( { type : 'start' } ) ;
writer . write ( { type : 'tool-input-start' , toolCallId , toolName : 'display_diagram' } ) ;
2025-12-02 19:11:23 +09:00
// Stream the XML as JSON input so it matches the tool schema exactly
writer . write ( { type : 'tool-input-delta' , toolCallId , inputTextDelta : JSON.stringify ( { xml } ) } ) ;
// Input must match the tool schema (only xml field, no extra fields like fromCache)
2025-12-01 14:07:50 +09:00
writer . write ( { type : 'tool-input-available' , toolCallId , toolName : 'display_diagram' , input : { xml } } ) ;
2025-12-02 19:11:23 +09:00
// Include tool output so the message is complete for follow-up conversations
writer . write ( { type : 'tool-output-available' , toolCallId , output : 'Successfully displayed the diagram.' } ) ;
2025-12-01 14:07:50 +09:00
writer . write ( { type : 'finish' } ) ;
} ,
} ) ;
return createUIMessageStreamResponse ( { stream } ) ;
}
2025-11-10 00:00:02 +09:00
export async function POST ( req : Request ) {
try {
2025-12-02 19:11:23 +09:00
const { messages , xml , lastGeneratedXml } = await req . json ( ) ;
2025-03-19 11:03:37 +00:00
2025-12-02 19:20:06 +09:00
// Basic validation for demo app
if ( ! messages || ! Array . isArray ( messages ) || messages . length === 0 ) {
return Response . json ( { error : 'Invalid messages' } , { status : 400 } ) ;
}
2025-12-01 14:07:50 +09:00
// === CACHE CHECK START ===
const isFirstMessage = messages . length === 1 ;
const isEmptyDiagram = ! xml || xml . trim ( ) === '' || isMinimalDiagram ( xml ) ;
if ( isFirstMessage && isEmptyDiagram ) {
const lastMessage = messages [ 0 ] ;
const textPart = lastMessage . parts ? . find ( ( p : any ) = > p . type === 'text' ) ;
const filePart = lastMessage . parts ? . find ( ( p : any ) = > p . type === 'file' ) ;
const cached = findCachedResponse ( textPart ? . text || '' , ! ! filePart ) ;
if ( cached ) {
console . log ( '[Cache] Returning cached response for:' , textPart ? . text ) ;
return createCachedStreamResponse ( cached . xml ) ;
}
}
// === CACHE CHECK END ===
2025-11-10 00:00:02 +09:00
const systemMessage = `
You are an expert diagram creation assistant specializing in draw . io XML generation .
Your primary function is crafting clear , well - organized visual diagrams through precise XML specifications .
2025-03-23 11:03:25 +00:00
You can see the image that user uploaded .
2025-11-10 11:27:25 +09:00
Note that when you need to generate diagram about aws architecture , use * * AWS 2025 icons * * .
2025-08-31 20:52:04 +09:00
You utilize the following tools :
2025-03-19 11:03:37 +00:00
-- - Tool1 -- -
2025-03-22 14:28:55 +00:00
tool name : display_diagram
2025-11-10 11:27:25 +09:00
description : Display a NEW diagram on draw . io . Use this when creating a diagram from scratch or when major structural changes are needed .
2025-03-19 11:03:37 +00:00
parameters : {
xml : string
}
2025-08-31 20:52:04 +09:00
-- - Tool2 -- -
tool name : edit_diagram
2025-11-10 11:27:25 +09:00
description : Edit specific parts of the EXISTING diagram . Use this when making small targeted changes like adding / removing elements , changing labels , or adjusting properties . This is more efficient than regenerating the entire diagram .
2025-08-31 20:52:04 +09:00
parameters : {
edits : Array < { search : string , replace : string } >
}
2025-03-19 11:03:37 +00:00
-- - End of tools -- -
2025-11-10 11:27:25 +09:00
IMPORTANT : Choose the right tool :
- Use display_diagram for : Creating new diagrams , major restructuring , or when the current diagram XML is empty
- Use edit_diagram for : Small modifications , adding / removing elements , changing text / colors , repositioning items
2025-12-02 19:11:23 +09:00
- When using edit_diagram : If the current diagram XML is provided in the user message context , use it as the source of truth for constructing search patterns . If no XML is provided , you can use your memory of the diagram structure .
2025-11-10 11:27:25 +09:00
2025-03-23 11:03:25 +00:00
Core capabilities :
- Generate valid , well - formed XML strings for draw . io diagrams
2025-11-10 11:27:25 +09:00
- Create professional flowcharts , mind maps , entity diagrams , and technical illustrations
2025-03-23 11:03:25 +00:00
- Convert user descriptions into visually appealing diagrams using basic shapes and connectors
- Apply proper spacing , alignment and visual hierarchy in diagram layouts
- Adapt artistic concepts into abstract diagram representations using available shapes
- Optimize element positioning to prevent overlapping and maintain readability
- Structure complex systems into clear , organized visual components
2025-11-10 11:27:25 +09:00
Layout constraints :
- CRITICAL : Keep all diagram elements within a single page viewport to avoid page breaks
- Position all elements with x coordinates between 0 - 800 and y coordinates between 0 - 600
- Maximum width for containers ( like AWS cloud boxes ) : 700 pixels
- Maximum height for containers : 550 pixels
- Use compact , efficient layouts that fit the entire diagram in one view
- Start positioning from reasonable margins ( e . g . , x = 40 , y = 40 ) and keep elements grouped closely
- For large diagrams with many elements , use vertical stacking or grid layouts that stay within bounds
- Avoid spreading elements too far apart horizontally - users should see the complete diagram without a page break line
2025-03-23 11:03:25 +00:00
Note that :
- Focus on producing clean , professional diagrams that effectively communicate the intended information through thoughtful layout and design choices .
- When artistic drawings are requested , creatively compose them using standard diagram shapes and connectors while maintaining visual clarity .
2025-11-10 09:12:30 +09:00
- Return XML only via tool calls , never in text responses .
2025-03-23 11:03:25 +00:00
- If user asks you to replicate a diagram based on an image , remember to match the diagram style and layout as closely as possible . Especially , pay attention to the lines and shapes , for example , if the lines are straight or curved , and if the shapes are rounded or square .
2025-11-10 11:27:25 +09:00
- Note that when you need to generate diagram about aws architecture , use * * AWS 2025 icons * * .
2025-05-22 00:44:24 +00:00
2025-08-31 20:52:04 +09:00
When using edit_diagram tool :
- Keep edits minimal - only include the specific line being changed plus 1 - 2 context lines
- Example GOOD edit : { "search" : " <mxCell id=\"2\" value=\"Old Text\">" , "replace" : " <mxCell id=\"2\" value=\"New Text\">" }
- Example BAD edit : Including 10 + unchanged lines just to change one attribute
- For multiple changes , use separate edits : [ { "search" : "line1" , "replace" : "new1" } , { "search" : "line2" , "replace" : "new2" } ]
2025-11-13 22:27:11 +09:00
- RETRY POLICY : If edit_diagram fails because the search pattern cannot be found :
* You may retry edit_diagram up to 3 times with adjusted search patterns
* After 3 failed attempts , you MUST fall back to using display_diagram to regenerate the entire diagram
* The error message will indicate how many retries remain
2025-03-23 11:03:25 +00:00
` ;
2025-03-19 11:03:37 +00:00
2025-11-10 00:00:02 +09:00
const lastMessage = messages [ messages . length - 1 ] ;
2025-08-31 12:54:14 +09:00
2025-11-10 00:00:02 +09:00
// Extract text from the last message parts
const lastMessageText = lastMessage . parts ? . find ( ( part : any ) = > part . type === 'text' ) ? . text || '' ;
2025-08-31 12:54:14 +09:00
2025-11-10 00:00:02 +09:00
// Extract file parts (images) from the last message
const fileParts = lastMessage . parts ? . filter ( ( part : any ) = > part . type === 'file' ) || [ ] ;
2025-12-02 19:20:06 +09:00
// Check diagram state - use formatted XML for reliable comparison
2025-12-02 19:11:23 +09:00
const hasDiagram = xml && ! isMinimalDiagram ( xml ) ;
const noHistory = ! lastGeneratedXml || lastGeneratedXml . trim ( ) === '' ;
2025-12-02 19:20:06 +09:00
const formattedXml = hasDiagram ? formatXML ( xml ) : '' ;
const formattedLastGenXml = lastGeneratedXml ? formatXML ( lastGeneratedXml ) : '' ;
const userModified = hasDiagram && formattedLastGenXml && formattedXml !== formattedLastGenXml ;
2025-12-02 19:11:23 +09:00
// Build context based on diagram state
let diagramContext = '' ;
if ( hasDiagram && noHistory ) {
// No history (e.g., cached response) - include XML directly
diagramContext = ` \ n \ n[Current diagram XML - use this as source of truth for edits:] \ n \` \` \` xml \ n ${ xml } \ n \` \` \` ` ;
} else if ( userModified ) {
// User modified - include XML
diagramContext = ` \ n \ n[User modified the diagram. Current XML:] \ n \` \` \` xml \ n ${ xml } \ n \` \` \` ` ;
}
// If unchanged and has history, agent can use memory (no XML sent = save tokens)
const formattedTextContent = ` User input:
2025-03-24 02:38:27 +00:00
"" " md
2025-08-31 12:54:14 +09:00
$ { lastMessageText }
2025-12-02 19:11:23 +09:00
"" " $ { diagramContext } ` ;
2025-08-31 12:54:14 +09:00
2025-11-10 00:00:02 +09:00
// Convert UIMessages to ModelMessages and add system message
const modelMessages = convertToModelMessages ( messages ) ;
2025-12-01 01:15:43 +09:00
// Filter out messages with empty content arrays (Bedrock API rejects these)
// This is a safety measure - ideally convertToModelMessages should handle all cases
let enhancedMessages = modelMessages . filter ( ( msg : any ) = >
msg . content && Array . isArray ( msg . content ) && msg . content . length > 0
) ;
2025-11-10 00:00:02 +09:00
// Update the last message with formatted content if it's a user message
if ( enhancedMessages . length >= 1 ) {
const lastModelMessage = enhancedMessages [ enhancedMessages . length - 1 ] ;
if ( lastModelMessage . role === 'user' ) {
// Build content array with text and file parts
const contentParts : any [ ] = [
{ type : 'text' , text : formattedTextContent }
] ;
// Add image parts back
for ( const filePart of fileParts ) {
contentParts . push ( {
type : 'image' ,
image : filePart.url ,
mimeType : filePart.mediaType
} ) ;
}
enhancedMessages = [
. . . enhancedMessages . slice ( 0 , - 1 ) ,
{ . . . lastModelMessage , content : contentParts }
] ;
}
2025-08-31 12:54:14 +09:00
}
2025-12-01 10:43:33 +09:00
// Add cache point to the last assistant message in conversation history
// This caches the entire conversation prefix for subsequent requests
// Strategy: system (cached) + history with last assistant (cached) + new user message
if ( enhancedMessages . length >= 2 ) {
// Find the last assistant message (should be second-to-last, before current user message)
for ( let i = enhancedMessages . length - 2 ; i >= 0 ; i -- ) {
if ( enhancedMessages [ i ] . role === 'assistant' ) {
enhancedMessages [ i ] = {
. . . enhancedMessages [ i ] ,
providerOptions : {
bedrock : { cachePoint : { type : 'default' } } ,
} ,
} ;
break ; // Only cache the last assistant message
}
}
}
2025-11-10 00:00:02 +09:00
2025-11-15 13:36:42 +09:00
// Get AI model from environment configuration
2025-11-30 16:34:42 +09:00
const { model , providerOptions , headers } = getAIModel ( ) ;
2025-11-15 13:36:42 +09:00
2025-12-01 10:43:33 +09:00
// System message with cache point for Bedrock (requires 1024+ tokens)
const systemMessageWithCache = {
role : 'system' as const ,
content : systemMessage ,
providerOptions : {
bedrock : { cachePoint : { type : 'default' } } ,
} ,
} ;
2025-11-10 00:00:02 +09:00
const result = streamText ( {
2025-11-15 13:36:42 +09:00
model ,
2025-12-01 10:43:33 +09:00
messages : [ systemMessageWithCache , . . . enhancedMessages ] ,
2025-11-15 13:36:42 +09:00
. . . ( providerOptions && { providerOptions } ) ,
2025-11-30 16:34:42 +09:00
. . . ( headers && { headers } ) ,
2025-12-02 19:20:06 +09:00
onFinish : ( { usage } ) = > {
console . log ( '[API] Tokens:' , usage ? . inputTokens , 'in /' , usage ? . outputTokens , 'out, cached:' , usage ? . cachedInputTokens ) ;
2025-12-01 10:43:33 +09:00
} ,
2025-11-10 00:00:02 +09:00
tools : {
// Client-side tool that will be executed on the client
display_diagram : {
description : ` Display a diagram on draw.io. You only need to pass the nodes inside the <root> tag (including the <root> tag itself) in the XML string.
For example :
< root >
< mxCell id = "0" / >
< mxCell id = "1" parent = "0" / >
2025-03-25 08:56:24 +00:00
< mxGeometry x = "20" y = "20" width = "100" height = "100" as = "geometry" / >
2025-11-10 00:00:02 +09:00
< mxCell id = "2" value = "Hello, World!" style = "shape=rectangle" parent = "1" >
< mxGeometry x = "20" y = "20" width = "100" height = "100" as = "geometry" / >
< / mxCell >
2025-11-10 11:27:25 +09:00
< / root >
- Note that when you need to generate diagram about aws architecture , use * * AWS 2025 icons * * .
2025-11-17 15:12:16 +09:00
- If you are asked to generate animated connectors , make sure to include "flowAnimation=1" in the style of the connector elements .
2025-11-10 11:27:25 +09:00
` ,
2025-11-10 00:00:02 +09:00
inputSchema : z.object ( {
xml : z.string ( ) . describe ( "XML string to be displayed on draw.io" )
} )
} ,
edit_diagram : {
description : ` Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
2025-08-31 20:52:04 +09:00
IMPORTANT : Keep edits concise :
- Only include the lines that are changing , plus 1 - 2 surrounding lines for context if needed
- Break large changes into multiple smaller edits
- Each search must contain complete lines ( never truncate mid - line )
- First match only - be specific enough to target the right element ` ,
2025-11-10 00:00:02 +09:00
inputSchema : z.object ( {
edits : z.array ( z . object ( {
search : z.string ( ) . describe ( "Exact lines to search for (including whitespace and indentation)" ) ,
replace : z.string ( ) . describe ( "Replacement lines" )
} ) ) . describe ( "Array of search/replace pairs to apply sequentially" )
} )
} ,
2025-08-31 20:52:04 +09:00
} ,
2025-11-10 00:00:02 +09:00
temperature : 0 ,
2025-12-02 19:11:23 +09:00
maxSteps : 5 , // Allow model to continue after server-side tool execution
2025-11-10 00:00:02 +09:00
} ) ;
2025-04-04 02:10:24 +00:00
2025-11-10 00:00:02 +09:00
// Error handler function to provide detailed error messages
function errorHandler ( error : unknown ) {
if ( error == null ) {
return 'unknown error' ;
}
2025-08-19 01:17:17 +00:00
2025-11-10 00:00:02 +09:00
if ( typeof error === 'string' ) {
return error ;
}
2025-08-19 01:17:17 +00:00
2025-11-10 00:00:02 +09:00
if ( error instanceof Error ) {
return error . message ;
}
2025-08-19 01:17:17 +00:00
2025-11-10 00:00:02 +09:00
return JSON . stringify ( error ) ;
2025-08-19 01:17:17 +00:00
}
2025-11-10 00:00:02 +09:00
return result . toUIMessageStreamResponse ( {
onError : errorHandler ,
} ) ;
} catch ( error ) {
console . error ( 'Error in chat route:' , error ) ;
return Response . json (
{ error : 'Internal server error' } ,
{ status : 500 }
) ;
2025-08-19 01:17:17 +00:00
}
2025-03-19 08:16:44 +00:00
}